mirror of
https://github.com/wassname/talk.git
synced 2026-08-09 12:30:42 +08:00
[CORL-929] SSO Key Rotation (Front-end) (#2877)
* feat: initial impl * Create preliminary SSO rotation components CORL-929 * Create rotateSSOKey mutation CORL-929 * Fix sorting by date for SSO keys CORL-929 * Show tool tips beside expiring/expired statuses CORL-929 * Hook up Deactivate and Delete SSO key mutations CORL-929 * Tweak expired tooltip on SSO keys CORL-929 * Replace old SSO key config with key rotation CORL-929 * Fix copy button for SSO key's secret CORL-929 * Refactor SSOKeyCard func's into components All the func's building the sub-components were prime targets for component's with props. CORL-929 * Update tests to match SSO Key rotation CORL-929 * Fix typo in translation id CORL-929 * Test key rotation CORL-929 * Plumb disabled through SSO key rotation CORL-929 * Remove duplicate input/payload pairs from schema For some reason it perfectly duplicated the input/payload pairs for SSO key rotation on rebase. CORL-929 * Remove debug logging CORL-929 * Use switch instead of if's to compute SSO date field CORL-929 * Use switch to compute SSO action button state CORL-929 * Use switch to compute SSO StatusField state CORL-929 * Remove 10 second key rotation option CORL-929 * Use memo in sorting SSO keys CORL-929 Co-authored-by: Wyatt Johnson <wyattjoh@gmail.com>
This commit is contained in:
co-authored by
Wyatt Johnson
parent
2dc63d3a27
commit
9a58673545
@@ -2,12 +2,13 @@ import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { PropTypesOf } from "coral-framework/types";
|
||||
import { ExternalLink } from "coral-framework/lib/i18n/components";
|
||||
import { FormFieldDescription } from "coral-ui/components/v2";
|
||||
|
||||
import Header from "../../Header";
|
||||
import ConfigBoxWithToggleField from "./ConfigBoxWithToggleField";
|
||||
import RegistrationField from "./RegistrationField";
|
||||
import SSOKeyFieldContainer from "./SSOKeyFieldContainer";
|
||||
import SSOKeyRotationQuery from "./SSOKeyRotation/SSOKeyRotationQuery";
|
||||
import TargetFilterField from "./TargetFilterField";
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
@@ -28,10 +29,9 @@ graphql`
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
sso: PropTypesOf<typeof SSOKeyFieldContainer>["sso"];
|
||||
}
|
||||
|
||||
const SSOConfig: FunctionComponent<Props> = ({ disabled, sso }) => (
|
||||
const SSOConfig: FunctionComponent<Props> = ({ disabled }) => (
|
||||
<ConfigBoxWithToggleField
|
||||
title={
|
||||
<Localized id="configure-auth-sso-loginWith">
|
||||
@@ -44,7 +44,23 @@ const SSOConfig: FunctionComponent<Props> = ({ disabled, sso }) => (
|
||||
>
|
||||
{disabledInside => (
|
||||
<>
|
||||
<SSOKeyFieldContainer sso={sso} disabled={disabledInside} />
|
||||
<Localized
|
||||
id="configure-auth-sso-description"
|
||||
IntroLink={
|
||||
<ExternalLink href="https://jwt.io/introduction/"></ExternalLink>
|
||||
}
|
||||
DocLink={
|
||||
<ExternalLink href="https://docs.coralproject.net/coral/v5/integrating/sso/"></ExternalLink>
|
||||
}
|
||||
>
|
||||
<FormFieldDescription>
|
||||
To enable integration with your existing authentication system, you
|
||||
will need to create a JWT Token to connect. You can learn more about
|
||||
creating a JWT Token with this introduction. See our documentation
|
||||
for additional information on single sign on.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<SSOKeyRotationQuery disabled={disabledInside}></SSOKeyRotationQuery>
|
||||
<TargetFilterField
|
||||
label={
|
||||
<Localized id="configure-auth-sso-useLoginOn">
|
||||
|
||||
@@ -16,17 +16,13 @@ const SSOConfigContainer: React.FunctionComponent<Props> = ({
|
||||
disabled,
|
||||
auth,
|
||||
}) => {
|
||||
return <SSOConfig disabled={disabled} sso={auth.integrations.sso} />;
|
||||
return <SSOConfig disabled={disabled} />;
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
auth: graphql`
|
||||
fragment SSOConfigContainer_auth on Auth {
|
||||
integrations {
|
||||
sso {
|
||||
...SSOKeyFieldContainer_sso
|
||||
}
|
||||
}
|
||||
...SSOConfig_formValues
|
||||
}
|
||||
`,
|
||||
})(SSOConfigContainer);
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
.root {
|
||||
padding-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.keyGenerated {
|
||||
composes: button from "coral-ui/shared/typography.css";
|
||||
color: var(--palette-text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.warnIcon {
|
||||
color: var(--palette-text-secondary);
|
||||
flex-shrink: 0;
|
||||
padding-top: 3px;
|
||||
padding-right: var(--spacing-1);
|
||||
}
|
||||
|
||||
.warn {
|
||||
color: var(--palette-text-secondary);
|
||||
}
|
||||
|
||||
.warningSection {
|
||||
padding-top: var(--spacing-1);
|
||||
padding-bottom: var(--spacing-1);
|
||||
}
|
||||
|
||||
.regenerateButton {
|
||||
float: right;
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
FormField,
|
||||
Icon,
|
||||
Label,
|
||||
PasswordField,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import HelperText from "../../HelperText";
|
||||
|
||||
import styles from "./SSOKeyField.css";
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
generatedKey?: string;
|
||||
keyGeneratedAt?: any;
|
||||
onRegenerate?: () => void;
|
||||
}
|
||||
|
||||
const SSOKeyField: FunctionComponent<Props> = ({
|
||||
generatedKey,
|
||||
keyGeneratedAt,
|
||||
disabled,
|
||||
onRegenerate,
|
||||
}) => (
|
||||
<FormField className={styles.root}>
|
||||
<Localized id="configure-auth-sso-key">
|
||||
<Label htmlFor="configure-auth-sso-key">Key</Label>
|
||||
</Localized>
|
||||
<PasswordField
|
||||
id="configure-auth-sso-key"
|
||||
name="key"
|
||||
value={generatedKey}
|
||||
readOnly
|
||||
// TODO: (wyattjoh) figure out how to add translations to these props
|
||||
hidePasswordTitle="Show SSO Key"
|
||||
showPasswordTitle="Hide SSO Key"
|
||||
fullWidth
|
||||
/>
|
||||
{keyGeneratedAt && (
|
||||
<Localized
|
||||
id="configure-auth-sso-regenerateAt"
|
||||
$date={new Date(keyGeneratedAt)}
|
||||
>
|
||||
<HelperText className={styles.keyGenerated}>
|
||||
KEY GENERATED AT: {keyGeneratedAt}
|
||||
</HelperText>
|
||||
</Localized>
|
||||
)}
|
||||
<div className={styles.warningSection}>
|
||||
<Flex direction="row" itemGutter="half">
|
||||
<Icon className={styles.warnIcon}>warning</Icon>
|
||||
<Localized id="configure-auth-sso-regenerateHonoredWarning">
|
||||
<HelperText>
|
||||
When regenerating a key, tokens signed with the previous key will be
|
||||
honored for 30 days.
|
||||
</HelperText>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<Localized id="configure-auth-sso-regenerate">
|
||||
<Button
|
||||
id="configure-auth-sso-regenerate"
|
||||
disabled={disabled}
|
||||
onClick={onRegenerate}
|
||||
className={styles.regenerateButton}
|
||||
>
|
||||
Regenerate
|
||||
</Button>
|
||||
</Localized>
|
||||
</FormField>
|
||||
);
|
||||
|
||||
export default SSOKeyField;
|
||||
@@ -1,60 +0,0 @@
|
||||
import React from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import {
|
||||
MutationProp,
|
||||
withFragmentContainer,
|
||||
withMutation,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { SSOKeyFieldContainer_sso as SSOData } from "coral-admin/__generated__/SSOKeyFieldContainer_sso.graphql";
|
||||
|
||||
import RegenerateSSOKeyMutation from "./RegenerateSSOKeyMutation";
|
||||
import SSOKeyField from "./SSOKeyField";
|
||||
|
||||
interface Props {
|
||||
sso: SSOData;
|
||||
disabled?: boolean;
|
||||
regenerateSSOKey: MutationProp<typeof RegenerateSSOKeyMutation>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
awaitingResponse: boolean;
|
||||
}
|
||||
|
||||
class SSOKeyFieldContainer extends React.Component<Props, State> {
|
||||
public state = {
|
||||
awaitingResponse: false,
|
||||
};
|
||||
|
||||
private handleRegenerate = async () => {
|
||||
this.setState({ awaitingResponse: true });
|
||||
await this.props.regenerateSSOKey();
|
||||
this.setState({ awaitingResponse: false });
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { disabled } = this.props;
|
||||
return (
|
||||
<SSOKeyField
|
||||
disabled={disabled || this.state.awaitingResponse}
|
||||
generatedKey={this.props.sso.key || undefined}
|
||||
keyGeneratedAt={this.props.sso.keyGeneratedAt || undefined}
|
||||
onRegenerate={this.handleRegenerate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withMutation(RegenerateSSOKeyMutation)(
|
||||
withFragmentContainer<Props>({
|
||||
sso: graphql`
|
||||
fragment SSOKeyFieldContainer_sso on SSOAuthIntegration {
|
||||
key
|
||||
keyGeneratedAt
|
||||
}
|
||||
`,
|
||||
})(SSOKeyFieldContainer)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
@@ -0,0 +1,10 @@
|
||||
.label {
|
||||
padding-bottom: var(--v2-spacing-2);
|
||||
}
|
||||
|
||||
.date {
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-weight: var(--v2-font-weight-primary-regular);
|
||||
font-size: var(--v2-font-size-2);
|
||||
line-height: var(--v2-line-height-reset);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Flex, Label } from "coral-ui/components/v2";
|
||||
|
||||
import { SSOKeyStatus } from "./StatusField";
|
||||
|
||||
import styles from "./DateField.css";
|
||||
|
||||
export interface SSOKeyDates {
|
||||
readonly createdAt: string;
|
||||
readonly lastUsedAt: string | null;
|
||||
readonly rotatedAt: string | null;
|
||||
readonly inactiveAt: string | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
status: SSOKeyStatus;
|
||||
dates: SSOKeyDates;
|
||||
}
|
||||
|
||||
const DateField: FunctionComponent<Props> = ({ status, dates }) => {
|
||||
switch (status) {
|
||||
case SSOKeyStatus.ACTIVE:
|
||||
return (
|
||||
<>
|
||||
<div className={styles.label}>
|
||||
<Localized id="configure-auth-sso-rotate-activeSince">
|
||||
<Label>Active Since</Label>
|
||||
</Localized>
|
||||
</div>
|
||||
<Localized
|
||||
id="configure-auth-sso-rotate-date"
|
||||
$date={new Date(dates.createdAt)}
|
||||
>
|
||||
<span className={styles.date}>{dates.createdAt}</span>
|
||||
</Localized>
|
||||
</>
|
||||
);
|
||||
case SSOKeyStatus.EXPIRING:
|
||||
return (
|
||||
<>
|
||||
<div className={styles.label}>
|
||||
<Localized id="configure-auth-sso-rotate-inactiveAt">
|
||||
<Label>Inactive At</Label>
|
||||
</Localized>
|
||||
</div>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
className={styles.date}
|
||||
>
|
||||
<Localized
|
||||
id="configure-auth-sso-rotate-date"
|
||||
$date={
|
||||
dates.inactiveAt
|
||||
? new Date(dates.inactiveAt)
|
||||
: new Date(dates.createdAt)
|
||||
}
|
||||
>
|
||||
{dates.inactiveAt}
|
||||
</Localized>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
case SSOKeyStatus.EXPIRED:
|
||||
return (
|
||||
<>
|
||||
<div className={styles.label}>
|
||||
<Localized id="configure-auth-sso-rotate-inactiveSince">
|
||||
<Label>Inactive Since</Label>
|
||||
</Localized>
|
||||
</div>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
className={styles.date}
|
||||
>
|
||||
<Localized
|
||||
id="configure-auth-sso-rotate-date"
|
||||
$date={
|
||||
dates.inactiveAt
|
||||
? new Date(dates.inactiveAt)
|
||||
: new Date(dates.createdAt)
|
||||
}
|
||||
>
|
||||
{dates.inactiveAt}
|
||||
</Localized>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default DateField;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { DeactivateSSOKeyMutation as MutationTypes } from "coral-admin/__generated__/DeactivateSSOKeyMutation.graphql";
|
||||
|
||||
const clientMutationId = 0;
|
||||
|
||||
const DeactivateSSOKeyMutation = createMutation(
|
||||
"deactivateSSOKey",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) => {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation DeactivateSSOKeyMutation($input: DeactivateSSOKeyInput!) {
|
||||
deactivateSSOKey(input: $input) {
|
||||
settings {
|
||||
auth {
|
||||
integrations {
|
||||
sso {
|
||||
enabled
|
||||
keys {
|
||||
kid
|
||||
secret
|
||||
createdAt
|
||||
lastUsedAt
|
||||
rotatedAt
|
||||
inactiveAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default DeactivateSSOKeyMutation;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { DeleteSSOKeyMutation as MutationTypes } from "coral-admin/__generated__/DeleteSSOKeyMutation.graphql";
|
||||
|
||||
const clientMutationId = 0;
|
||||
|
||||
const DeleteSSOKeyMutation = createMutation(
|
||||
"deleteSSOKey",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) => {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation DeleteSSOKeyMutation($input: DeleteSSOKeyInput!) {
|
||||
deleteSSOKey(input: $input) {
|
||||
settings {
|
||||
auth {
|
||||
integrations {
|
||||
sso {
|
||||
enabled
|
||||
keys {
|
||||
kid
|
||||
secret
|
||||
createdAt
|
||||
lastUsedAt
|
||||
rotatedAt
|
||||
inactiveAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default DeleteSSOKeyMutation;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { RotateSSOKeyMutation as MutationTypes } from "coral-admin/__generated__/RotateSSOKeyMutation.graphql";
|
||||
|
||||
const clientMutationId = 0;
|
||||
|
||||
const RotateSSOKeyMutation = createMutation(
|
||||
"rotateSSOKey",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) => {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RotateSSOKeyMutation($input: RotateSSOKeyInput!) {
|
||||
rotateSSOKey(input: $input) {
|
||||
settings {
|
||||
auth {
|
||||
integrations {
|
||||
sso {
|
||||
enabled
|
||||
keys {
|
||||
kid
|
||||
secret
|
||||
createdAt
|
||||
lastUsedAt
|
||||
rotatedAt
|
||||
inactiveAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: clientMutationId.toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default RotateSSOKeyMutation;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.rotate {
|
||||
margin-right: var(--v2-spacing-1)
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
ClickOutside,
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
Icon,
|
||||
Popover,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import RotateOption, { RotateOptions } from "./RotationOption";
|
||||
|
||||
import styles from "./RotationDropdown.css";
|
||||
|
||||
interface Props {
|
||||
onRotateKey: (rotation: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const RotationDropDown: FunctionComponent<Props> = ({
|
||||
onRotateKey,
|
||||
disabled,
|
||||
}) => {
|
||||
return (
|
||||
<Localized
|
||||
id="configure-auth-sso-rotate-dropdown-description"
|
||||
attrs={{ description: true }}
|
||||
>
|
||||
<Popover
|
||||
id="sso-key-rotate"
|
||||
placement="bottom-start"
|
||||
description="A dropdown to rotate the SSO key"
|
||||
body={({ toggleVisibility }) => (
|
||||
<ClickOutside onClickOutside={toggleVisibility}>
|
||||
<Dropdown>
|
||||
{Object.keys(RotateOptions).map((opt: string) => (
|
||||
<DropdownButton
|
||||
key={opt}
|
||||
onClick={() => {
|
||||
onRotateKey(opt);
|
||||
toggleVisibility();
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<RotateOption value={opt}></RotateOption>
|
||||
</DropdownButton>
|
||||
))}
|
||||
</Dropdown>
|
||||
</ClickOutside>
|
||||
)}
|
||||
>
|
||||
{({ toggleVisibility, ref, visible }) => (
|
||||
<Button
|
||||
onClick={toggleVisibility}
|
||||
ref={ref}
|
||||
color="regular"
|
||||
disabled={disabled}
|
||||
>
|
||||
<Localized id="configure-auth-sso-rotate-rotate">
|
||||
<span className={styles.rotate}>Rotate</span>
|
||||
</Localized>
|
||||
<Icon>arrow_drop_down</Icon>
|
||||
</Button>
|
||||
)}
|
||||
</Popover>
|
||||
</Localized>
|
||||
);
|
||||
};
|
||||
|
||||
export default RotationDropDown;
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
export enum RotateOptions {
|
||||
NOW = "NOW",
|
||||
IN1DAY = "IN1DAY",
|
||||
IN1WEEK = "IN1WEEK",
|
||||
IN30DAYS = "IN30DAYS",
|
||||
}
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
}
|
||||
|
||||
const RotationOption: FunctionComponent<Props> = ({ value }) => {
|
||||
switch (value) {
|
||||
case RotateOptions.NOW: {
|
||||
return <Localized id="configure-auth-sso-rotate-now">Now</Localized>;
|
||||
}
|
||||
case RotateOptions.IN1DAY: {
|
||||
return (
|
||||
<Localized id="configure-auth-sso-rotate-1day">
|
||||
1 day from now
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
case RotateOptions.IN1WEEK: {
|
||||
return (
|
||||
<Localized id="configure-auth-sso-rotate-1week">
|
||||
1 week from now
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
case RotateOptions.IN30DAYS: {
|
||||
return (
|
||||
<Localized id="configure-auth-sso-rotate-30days">
|
||||
30 days from now
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return <Localized id="configure-auth-sso-rotate-now">Now</Localized>;
|
||||
}
|
||||
};
|
||||
|
||||
export default RotationOption;
|
||||
@@ -0,0 +1,23 @@
|
||||
.label {
|
||||
padding-bottom: var(--v2-spacing-2);
|
||||
}
|
||||
|
||||
.keySection {
|
||||
flex-grow: 1;
|
||||
min-width: 50px;
|
||||
|
||||
padding-right: var(--v2-spacing-3);
|
||||
}
|
||||
|
||||
.statusSection {
|
||||
margin-right: var(--v2-spacing-3);
|
||||
}
|
||||
|
||||
.secretSection {
|
||||
flex-grow: 1;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-right: var(--v2-spacing-1)
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import CopyToClipboard from "react-copy-to-clipboard";
|
||||
|
||||
import { useMutation } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Flex,
|
||||
HorizontalGutter,
|
||||
Icon,
|
||||
Label,
|
||||
PasswordField,
|
||||
TextField,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import DateField from "./DateField";
|
||||
import DeactivateSSOKeyMutation from "./DeactivateSSOKeyMutation";
|
||||
import DeleteSSOKeyMutation from "./DeleteSSOKeyMutation";
|
||||
import RotateSSOKeyMutation from "./RotateSSOKeyMutation";
|
||||
import RotationDropDown from "./RotationDropdown";
|
||||
import { RotateOptions } from "./RotationOption";
|
||||
import StatusField, { SSOKeyStatus } from "./StatusField";
|
||||
|
||||
import styles from "./SSOKeyCard.css";
|
||||
|
||||
export interface SSOKeyDates {
|
||||
readonly createdAt: string;
|
||||
readonly lastUsedAt: string | null;
|
||||
readonly rotatedAt: string | null;
|
||||
readonly inactiveAt: string | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
secret: string;
|
||||
status: SSOKeyStatus;
|
||||
dates: SSOKeyDates;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function createActionButton(
|
||||
status: SSOKeyStatus,
|
||||
onRotateKey: (rotation: string) => void,
|
||||
onDeactivateKey: () => void,
|
||||
onDelete: () => void,
|
||||
disabled?: boolean
|
||||
) {
|
||||
switch (status) {
|
||||
case SSOKeyStatus.ACTIVE:
|
||||
return <RotationDropDown onRotateKey={onRotateKey} disabled={disabled} />;
|
||||
case SSOKeyStatus.EXPIRING:
|
||||
return (
|
||||
<Localized id="configure-auth-sso-rotate-deactivateNow">
|
||||
<Button color="alert" onClick={onDeactivateKey} disabled={disabled}>
|
||||
Deactivate Now
|
||||
</Button>
|
||||
</Localized>
|
||||
);
|
||||
case SSOKeyStatus.EXPIRED:
|
||||
return (
|
||||
<Localized id="configure-auth-sso-rotate-delete">
|
||||
<Button color="alert" onClick={onDelete} disabled={disabled}>
|
||||
Delete
|
||||
</Button>
|
||||
</Localized>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const SSOKeyCard: FunctionComponent<Props> = ({
|
||||
id,
|
||||
secret,
|
||||
status,
|
||||
dates,
|
||||
disabled,
|
||||
}) => {
|
||||
const rotateSSOKey = useMutation(RotateSSOKeyMutation);
|
||||
const deactivateSSOKey = useMutation(DeactivateSSOKeyMutation);
|
||||
const deleteSSOKey = useMutation(DeleteSSOKeyMutation);
|
||||
|
||||
const onRotate = useCallback(
|
||||
(rotation: string) => {
|
||||
switch (rotation) {
|
||||
case RotateOptions.NOW:
|
||||
rotateSSOKey({ inactiveIn: 0 });
|
||||
break;
|
||||
case RotateOptions.IN1DAY:
|
||||
rotateSSOKey({ inactiveIn: 24 * 60 * 60 });
|
||||
break;
|
||||
case RotateOptions.IN1WEEK:
|
||||
rotateSSOKey({ inactiveIn: 7 * 24 * 60 * 60 });
|
||||
break;
|
||||
case RotateOptions.IN30DAYS:
|
||||
rotateSSOKey({ inactiveIn: 30 * 24 * 60 * 60 });
|
||||
break;
|
||||
default:
|
||||
rotateSSOKey({ inactiveIn: 0 });
|
||||
}
|
||||
},
|
||||
[rotateSSOKey]
|
||||
);
|
||||
const onDeactivate = useCallback(() => {
|
||||
deactivateSSOKey({
|
||||
kid: id,
|
||||
});
|
||||
}, [deactivateSSOKey, id]);
|
||||
const onDelete = useCallback(() => {
|
||||
deleteSSOKey({
|
||||
kid: id,
|
||||
});
|
||||
}, [deleteSSOKey, id]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<HorizontalGutter>
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<div className={styles.keySection}>
|
||||
<div className={styles.label}>
|
||||
<Localized id="configure-auth-sso-rotate-keyID">
|
||||
<Label>Key ID</Label>
|
||||
</Localized>
|
||||
</div>
|
||||
<TextField value={id} readOnly fullWidth data-testid="SSO-Key-ID" />
|
||||
</div>
|
||||
<div className={styles.secretSection}>
|
||||
<div className={styles.label}>
|
||||
<Localized id="configure-auth-sso-rotate-secret">
|
||||
<Label>Secret</Label>
|
||||
</Localized>
|
||||
</div>
|
||||
<Flex alignItems="center" justifyContent="flex-start">
|
||||
<PasswordField
|
||||
id="configure-auth-sso-rotate-secretField"
|
||||
name="key"
|
||||
value={secret}
|
||||
readOnly
|
||||
// TODO: (nick-funk) figure out how to add translations to these props
|
||||
hidePasswordTitle="Show Secret"
|
||||
showPasswordTitle="Hide Secret"
|
||||
fullWidth
|
||||
/>
|
||||
<CopyToClipboard text={secret}>
|
||||
<Button color="mono" variant="flat">
|
||||
<Localized
|
||||
id="configure-auth-sso-rotate-copySecret"
|
||||
attrs={{ "aria-label": true }}
|
||||
>
|
||||
<Icon size="md" aria-label="Copy Secret">
|
||||
content_copy
|
||||
</Icon>
|
||||
</Localized>
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</Flex>
|
||||
</div>
|
||||
</Flex>
|
||||
<Flex alignItems="flex-end" justifyContent="space-between">
|
||||
<Flex alignItems="center" justifyContent="flex-start">
|
||||
<div className={styles.statusSection}>
|
||||
<div className={styles.label}>
|
||||
<Localized id="configure-auth-sso-rotate-status">
|
||||
<Label>Status</Label>
|
||||
</Localized>
|
||||
</div>
|
||||
<StatusField status={status}></StatusField>
|
||||
</div>
|
||||
<div>
|
||||
<DateField status={status} dates={dates} />
|
||||
</div>
|
||||
</Flex>
|
||||
{createActionButton(
|
||||
status,
|
||||
onRotate,
|
||||
onDeactivate,
|
||||
onDelete,
|
||||
disabled
|
||||
)}
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SSOKeyCard;
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
|
||||
import { graphql, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { Label } from "coral-ui/components/v2";
|
||||
|
||||
import { SSOKeyRotationContainer_settings } from "coral-admin/__generated__/SSOKeyRotationContainer_settings.graphql";
|
||||
|
||||
import SSOKeyCard, { SSOKeyDates } from "./SSOKeyCard";
|
||||
import { SSOKeyStatus } from "./StatusField";
|
||||
|
||||
interface Props {
|
||||
settings: SSOKeyRotationContainer_settings;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface Key {
|
||||
readonly kid: string;
|
||||
readonly secret: string;
|
||||
readonly createdAt: string;
|
||||
readonly lastUsedAt: string | null;
|
||||
readonly rotatedAt: string | null;
|
||||
readonly inactiveAt: string | null;
|
||||
}
|
||||
|
||||
function getStatus(dates: SSOKeyDates) {
|
||||
if (
|
||||
dates.inactiveAt &&
|
||||
dates.rotatedAt &&
|
||||
new Date(dates.inactiveAt) > new Date()
|
||||
) {
|
||||
return SSOKeyStatus.EXPIRING;
|
||||
}
|
||||
|
||||
if (dates.inactiveAt && new Date(dates.inactiveAt) <= new Date()) {
|
||||
return SSOKeyStatus.EXPIRED;
|
||||
}
|
||||
|
||||
return SSOKeyStatus.ACTIVE;
|
||||
}
|
||||
|
||||
const SSOKeyRotationContainer: FunctionComponent<Props> = ({
|
||||
disabled,
|
||||
settings,
|
||||
}) => {
|
||||
const {
|
||||
auth: {
|
||||
integrations: {
|
||||
sso: { keys },
|
||||
},
|
||||
},
|
||||
} = settings;
|
||||
|
||||
const sortedKeys = useMemo(
|
||||
() =>
|
||||
keys
|
||||
// Copy this map because we don't want to modify the underlying copy.
|
||||
.map(key => key)
|
||||
.sort((a: Key, b: Key) => {
|
||||
// Both active, sort on createdAt date.
|
||||
if (!a.inactiveAt && !b.inactiveAt) {
|
||||
return (
|
||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||
);
|
||||
}
|
||||
// A is active, B is not, A comes before B.
|
||||
if (!a.inactiveAt && b.inactiveAt) {
|
||||
return -1;
|
||||
}
|
||||
// B is active, A is not, B comes before A.
|
||||
if (a.inactiveAt && !b.inactiveAt) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Sort primarily on inactiveAt, fall back to createdAt if
|
||||
// for some reason it's not available.
|
||||
const aDate = a.inactiveAt
|
||||
? new Date(a.inactiveAt)
|
||||
: new Date(a.createdAt);
|
||||
const bDate = b.inactiveAt
|
||||
? new Date(b.inactiveAt)
|
||||
: new Date(b.createdAt);
|
||||
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
}),
|
||||
[keys]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Localized id="configure-auth-sso-rotate-keys">
|
||||
<Label htmlFor="configure-auth-sso-rotate-keys">Keys</Label>
|
||||
</Localized>
|
||||
{sortedKeys.map(key => (
|
||||
<SSOKeyCard
|
||||
key={key.kid}
|
||||
id={key.kid}
|
||||
secret={key.secret}
|
||||
status={getStatus(key)}
|
||||
dates={key}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
settings: graphql`
|
||||
fragment SSOKeyRotationContainer_settings on Settings {
|
||||
auth {
|
||||
integrations {
|
||||
sso {
|
||||
enabled
|
||||
keys {
|
||||
kid
|
||||
secret
|
||||
createdAt
|
||||
lastUsedAt
|
||||
rotatedAt
|
||||
inactiveAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(SSOKeyRotationContainer);
|
||||
|
||||
export default enhanced;
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import {
|
||||
graphql,
|
||||
QueryRenderData,
|
||||
QueryRenderer,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { CallOut, Spinner } from "coral-ui/components/v2";
|
||||
|
||||
import { SSOKeyRotationQuery as QueryTypes } from "coral-admin/__generated__/SSOKeyRotationQuery.graphql";
|
||||
|
||||
import SSOKeyRotationContainer from "./SSOKeyRotationContainer";
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SSOKeyRotationQuery: FunctionComponent<Props> = ({ disabled }) => {
|
||||
return (
|
||||
<QueryRenderer<QueryTypes>
|
||||
query={graphql`
|
||||
query SSOKeyRotationQuery {
|
||||
settings {
|
||||
...SSOKeyRotationContainer_settings
|
||||
}
|
||||
}
|
||||
`}
|
||||
variables={{}}
|
||||
cacheConfig={{ force: true }}
|
||||
render={({ error, props }: QueryRenderData<QueryTypes>) => {
|
||||
if (error) {
|
||||
return <CallOut>{error.message}</CallOut>;
|
||||
}
|
||||
|
||||
if (!props) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (!props.settings) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SSOKeyRotationContainer
|
||||
settings={props.settings}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SSOKeyRotationQuery;
|
||||
@@ -0,0 +1,35 @@
|
||||
.status {
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-weight: var(--v2-font-weight-primary-regular);
|
||||
font-size: var(--v2-font-size-2);
|
||||
line-height: var(--v2-line-height-reset);
|
||||
|
||||
border-radius: 2px;
|
||||
padding-left: var(--v2-spacing-1);
|
||||
padding-right: var(--v2-spacing-1);
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: var(--v2-colors-green-500);
|
||||
color: var(--v2-colors-pure-white);
|
||||
}
|
||||
|
||||
.expiring {
|
||||
background-color: var(--v2-colors-yellow-500);
|
||||
color: var(--v2-colors-mono-500);
|
||||
|
||||
padding-top: var(--v2-spacing-1);
|
||||
padding-bottom: var(--v2-spacing-1);
|
||||
}
|
||||
|
||||
.expired {
|
||||
background-color: var(--v2-colors-red-500);
|
||||
color: var(--v2-colors-pure-white);
|
||||
|
||||
padding-top: var(--v2-spacing-1);
|
||||
padding-bottom: var(--v2-spacing-1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding-right: var(--v2-spacing-1);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import cn from "classnames";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Flex, Icon, Tooltip, TooltipButton } from "coral-ui/components/v2";
|
||||
|
||||
import styles from "./StatusField.css";
|
||||
|
||||
export enum SSOKeyStatus {
|
||||
EXPIRED,
|
||||
EXPIRING,
|
||||
ACTIVE,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
status: SSOKeyStatus;
|
||||
}
|
||||
|
||||
const StatusField: FunctionComponent<Props> = ({ status }) => {
|
||||
switch (status) {
|
||||
case SSOKeyStatus.ACTIVE:
|
||||
return (
|
||||
<Localized id="configure-auth-sso-rotate-statusActive">
|
||||
<span
|
||||
className={cn(styles.status, styles.active)}
|
||||
data-testid="SSO-Key-Status"
|
||||
>
|
||||
Active
|
||||
</span>
|
||||
</Localized>
|
||||
);
|
||||
case SSOKeyStatus.EXPIRING:
|
||||
return (
|
||||
<Flex alignItems="center" justifyContent="center">
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
className={cn(styles.status, styles.expiring)}
|
||||
>
|
||||
<Icon className={styles.icon}>alarm</Icon>
|
||||
<Localized id="configure-auth-sso-rotate-statusExpiring">
|
||||
<span data-testid="SSO-Key-Status">Expiring</span>
|
||||
</Localized>
|
||||
</Flex>
|
||||
<Tooltip
|
||||
id="configure-auth-sso-rotate-expiringTooltip"
|
||||
title=""
|
||||
body={
|
||||
<Localized id="configure-auth-sso-rotate-expiringTooltip">
|
||||
<span>
|
||||
An SSO key is expiring when it is scheduled for rotation.
|
||||
</span>
|
||||
</Localized>
|
||||
}
|
||||
button={({ toggleVisibility, ref, visible }) => (
|
||||
<Localized
|
||||
id="configure-auth-sso-rotate-expiringTooltip-toggleButton"
|
||||
attrs={{ "aria-label": true }}
|
||||
>
|
||||
<TooltipButton
|
||||
active
|
||||
aria-label="Toggle expiring tooltip visibility"
|
||||
toggleVisibility={toggleVisibility}
|
||||
ref={ref}
|
||||
/>
|
||||
</Localized>
|
||||
)}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
case SSOKeyStatus.EXPIRED:
|
||||
return (
|
||||
<Flex alignItems="center" justifyContent="center">
|
||||
<Localized id="configure-auth-sso-rotate-statusExpired">
|
||||
<span
|
||||
className={cn(styles.status, styles.expired)}
|
||||
data-testid="SSO-Key-Status"
|
||||
>
|
||||
Expired
|
||||
</span>
|
||||
</Localized>
|
||||
<Tooltip
|
||||
id="configure-auth-sso-rotate-expiredTooltip"
|
||||
title=""
|
||||
body={
|
||||
<Localized id="configure-auth-sso-rotate-expiredTooltip">
|
||||
<span>
|
||||
An SSO key is expired when it has been rotated out of use.
|
||||
</span>
|
||||
</Localized>
|
||||
}
|
||||
button={({ toggleVisibility, ref, visible }) => (
|
||||
<Localized
|
||||
id="configure-auth-sso-rotate-expiredTooltip-toggleButton"
|
||||
attrs={{ "aria-label": true }}
|
||||
>
|
||||
<TooltipButton
|
||||
active
|
||||
aria-label="Toggle expired tooltip visibility"
|
||||
toggleVisibility={toggleVisibility}
|
||||
ref={ref}
|
||||
/>
|
||||
</Localized>
|
||||
)}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Localized id="configure-auth-sso-rotate-statusUnknown">
|
||||
<span data-testid="SSO-Key-Status">Unknown</span>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default StatusField;
|
||||
@@ -1242,86 +1242,233 @@ integration to register for a new account.
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-4"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root FormField-root SSOKeyField-root HorizontalGutter-spacing-2"
|
||||
<p
|
||||
className="FormFieldDescription-root"
|
||||
>
|
||||
<label
|
||||
className="Label-root"
|
||||
htmlFor="configure-auth-sso-key"
|
||||
To enable integration with your existing authentication system,
|
||||
you will need to create a JWT Token to connect. You can learn
|
||||
more about creating a JWT Token with
|
||||
<a
|
||||
className="ExternalLink-root"
|
||||
href="https://jwt.io/introduction/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Key
|
||||
</label>
|
||||
this introduction
|
||||
</a>
|
||||
. See our
|
||||
|
||||
<a
|
||||
className="ExternalLink-root"
|
||||
href="https://docs.coralproject.net/coral/v5/integrating/sso/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
documentation
|
||||
</a>
|
||||
for additional information on single sign on.
|
||||
</p>
|
||||
<label
|
||||
className="Label-root"
|
||||
htmlFor="configure-auth-sso-rotate-keys"
|
||||
>
|
||||
Keys
|
||||
</label>
|
||||
<div
|
||||
className="Card-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignCenter"
|
||||
>
|
||||
<input
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
|
||||
data-testid="password-field"
|
||||
id="configure-auth-sso-key"
|
||||
name="key"
|
||||
placeholder=""
|
||||
readOnly={true}
|
||||
spellCheck={false}
|
||||
type="password"
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Hide SSO Key"
|
||||
className="SSOKeyCard-keySection"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
<div
|
||||
className="SSOKeyCard-label"
|
||||
>
|
||||
visibility
|
||||
</i>
|
||||
<label
|
||||
className="Label-root"
|
||||
>
|
||||
Key ID
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorRegular"
|
||||
data-testid="SSO-Key-ID"
|
||||
placeholder=""
|
||||
readOnly={true}
|
||||
type="text"
|
||||
value="kid-01"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="SSOKeyCard-secretSection"
|
||||
>
|
||||
<div
|
||||
className="SSOKeyCard-label"
|
||||
>
|
||||
<label
|
||||
className="Label-root"
|
||||
>
|
||||
Secret
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-justifyFlexStart Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
>
|
||||
<input
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
|
||||
data-testid="password-field"
|
||||
id="configure-auth-sso-rotate-secretField"
|
||||
name="key"
|
||||
placeholder=""
|
||||
readOnly={true}
|
||||
spellCheck={false}
|
||||
type="password"
|
||||
value="secret"
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Hide Secret"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorMono Button-variantFlat Button-uppercase"
|
||||
data-color="mono"
|
||||
data-variant="flat"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
aria-label="Copy Secret"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
content_copy
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignFlexEnd"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-justifyFlexStart Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
className="SSOKeyCard-statusSection"
|
||||
>
|
||||
<div
|
||||
className="SSOKeyCard-label"
|
||||
>
|
||||
<label
|
||||
className="Label-root"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
</div>
|
||||
<span
|
||||
className="StatusField-status StatusField-active"
|
||||
data-testid="SSO-Key-Status"
|
||||
>
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="DateField-label"
|
||||
>
|
||||
<label
|
||||
className="Label-root"
|
||||
>
|
||||
Active Since
|
||||
</label>
|
||||
</div>
|
||||
<span
|
||||
className="DateField-date"
|
||||
>
|
||||
1/1/2020, 1:00 AM
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Popover-root"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantRegular Button-uppercase Button-disabled"
|
||||
data-color="regular"
|
||||
data-variant="regular"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="RotationDropdown-rotate"
|
||||
>
|
||||
Rotate
|
||||
</span>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
arrow_drop_down
|
||||
</i>
|
||||
</button>
|
||||
<div
|
||||
aria-hidden={true}
|
||||
aria-labelledby="sso-key-rotate-ariainfo"
|
||||
id="sso-key-rotate"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
className="AriaInfo-root"
|
||||
id="sso-key-rotate-ariainfo"
|
||||
>
|
||||
A dropdown to rotate the SSO key
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="SSOKeyField-warningSection"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-halfItemGutter Flex-directionRow gutter"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm SSOKeyField-warnIcon"
|
||||
>
|
||||
warning
|
||||
</i>
|
||||
<p
|
||||
className="HelperText-root"
|
||||
>
|
||||
When regenerating a key, tokens signed with the previous key will be honored for 30 days.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantRegular Button-uppercase Button-disabled SSOKeyField-regenerateButton"
|
||||
data-color="regular"
|
||||
data-variant="regular"
|
||||
disabled={true}
|
||||
id="configure-auth-sso-regenerate"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Regenerate
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root FormField-root HorizontalGutter-spacing-2"
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
CreateTestRendererParams,
|
||||
findParentWithType,
|
||||
replaceHistoryLocation,
|
||||
toJSON,
|
||||
wait,
|
||||
waitForElement,
|
||||
within,
|
||||
@@ -57,11 +58,11 @@ it("renders configure auth", async () => {
|
||||
expect(within(configureContainer).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("regenerate sso key", async () => {
|
||||
it("rotate sso key", async () => {
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Mutation: {
|
||||
regenerateSSOKey: () => {
|
||||
rotateSSOKey: () => {
|
||||
return {
|
||||
settings: pureMerge<typeof settingsWithEmptyAuth>(
|
||||
settingsWithEmptyAuth,
|
||||
@@ -69,8 +70,22 @@ it("regenerate sso key", async () => {
|
||||
auth: {
|
||||
integrations: {
|
||||
sso: {
|
||||
key: "==GENERATED_KEY==",
|
||||
keyGeneratedAt: "2018-11-12T23:26:06.239Z",
|
||||
enabled: true,
|
||||
keys: [
|
||||
{
|
||||
kid: "kid-01",
|
||||
secret: "secret",
|
||||
createdAt: "2015-01-01T00:00:00.000Z",
|
||||
lastUsedAt: "2016-01-01T01:45:00.000Z",
|
||||
rotatedAt: "2016-01-01T01:45:00.000Z",
|
||||
inactiveAt: "2016-01-01T01:45:00.000Z",
|
||||
},
|
||||
{
|
||||
kid: "kid-02",
|
||||
secret: "new-secret",
|
||||
createdAt: "2019-01-01T01:45:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -90,15 +105,40 @@ it("regenerate sso key", async () => {
|
||||
|
||||
act(() => {
|
||||
within(container)
|
||||
.getByText("Regenerate", { selector: "button" })
|
||||
.getByText("Rotate", { selector: "button" })
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
await wait(() =>
|
||||
expect(within(container).getByLabelText("Key").props.value).toBe(
|
||||
"==GENERATED_KEY=="
|
||||
)
|
||||
);
|
||||
const rotateNow = await waitForElement(() => {
|
||||
return within(container).getByText("Now", { selector: "button" });
|
||||
});
|
||||
|
||||
act(() => {
|
||||
rotateNow.props.onClick();
|
||||
});
|
||||
|
||||
await wait(() => {
|
||||
// Check that we have two SSO Keys that match
|
||||
// our expected key IDs
|
||||
const keyIDs = within(container).getAllByTestID("SSO-Key-ID");
|
||||
const hasOldKey = keyIDs.some(k => k.props.value === "kid-01");
|
||||
const hasNewKey = keyIDs.some(k => k.props.value === "kid-02");
|
||||
expect(hasNewKey).toBe(true);
|
||||
expect(hasOldKey).toBe(true);
|
||||
|
||||
const statuses = within(container).getAllByTestID("SSO-Key-Status");
|
||||
expect(statuses.length).toBe(2);
|
||||
const firstStatus: any = toJSON(statuses[0]);
|
||||
const firstStatusIsActive = firstStatus.children.some(
|
||||
(s: string) => s === "Active"
|
||||
);
|
||||
expect(firstStatusIsActive).toBe(true);
|
||||
const secondStatus: any = toJSON(statuses[1]);
|
||||
const secondStatusIsActive = secondStatus.children.some(
|
||||
(s: string) => s === "Active"
|
||||
);
|
||||
expect(secondStatusIsActive).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("prevents admin lock out", async () => {
|
||||
|
||||
@@ -114,6 +114,16 @@ export const settings = createFixture<GQLSettings>({
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
keys: [
|
||||
{
|
||||
kid: "kid-01",
|
||||
secret: "secret",
|
||||
createdAt: "2020-01-01T01:00:00.000Z",
|
||||
lastUsedAt: undefined,
|
||||
rotatedAt: undefined,
|
||||
inactiveAt: undefined,
|
||||
},
|
||||
],
|
||||
key: "",
|
||||
keyGeneratedAt: null,
|
||||
},
|
||||
@@ -202,6 +212,16 @@ export const settingsWithEmptyAuth = createFixture<GQLSettings>(
|
||||
stream: true,
|
||||
},
|
||||
key: "",
|
||||
keys: [
|
||||
{
|
||||
kid: "kid-01",
|
||||
secret: "secret",
|
||||
createdAt: "2020-01-01T01:00:00.000Z",
|
||||
lastUsedAt: undefined,
|
||||
rotatedAt: undefined,
|
||||
inactiveAt: undefined,
|
||||
},
|
||||
],
|
||||
keyGeneratedAt: null,
|
||||
},
|
||||
google: {
|
||||
|
||||
@@ -5970,6 +5970,34 @@ type DeactivateSSOKeyPayload {
|
||||
settings: Settings
|
||||
}
|
||||
|
||||
#########################
|
||||
## deleteSSOKey
|
||||
#########################
|
||||
|
||||
input DeleteSSOKeyInput {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
kid is the ID of the SSOKey being deleted.
|
||||
"""
|
||||
kid: ID!
|
||||
}
|
||||
|
||||
type DeleteSSOKeyPayload {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
settings is the Settings that the SSO key was regenerated on.
|
||||
"""
|
||||
settings: Settings
|
||||
}
|
||||
|
||||
#########################
|
||||
# disableFeatureFlag
|
||||
#########################
|
||||
@@ -6064,34 +6092,6 @@ type RemoveStoryExpertPayload {
|
||||
story: Story!
|
||||
}
|
||||
|
||||
#########################
|
||||
## deleteSSOKey
|
||||
#########################
|
||||
|
||||
input DeleteSSOKeyInput {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
kid is the ID of the SSOKey being deleted.
|
||||
"""
|
||||
kid: ID!
|
||||
}
|
||||
|
||||
type DeleteSSOKeyPayload {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
settings is the Settings that the SSO key was regenerated on.
|
||||
"""
|
||||
settings: Settings
|
||||
}
|
||||
|
||||
#########################
|
||||
## updateStoryMode
|
||||
#########################
|
||||
|
||||
@@ -427,6 +427,51 @@ configure-auth-sso-regenerateAt = KEY GENERATED AT:
|
||||
configure-auth-sso-regenerateHonoredWarning =
|
||||
When regenerating a key, tokens signed with the previous key will be honored for 30 days.
|
||||
|
||||
configure-auth-sso-description =
|
||||
To enable integration with your existing authentication system,
|
||||
you will need to create a JWT Token to connect. You can learn
|
||||
more about creating a JWT Token with <IntroLink>this introduction</IntroLink>. See our
|
||||
<DocLink>documentation</DocLink> for additional information on single sign on.
|
||||
|
||||
configure-auth-sso-rotate-keys = Keys
|
||||
configure-auth-sso-rotate-keyID = Key ID
|
||||
configure-auth-sso-rotate-secret = Secret
|
||||
configure-auth-sso-rotate-copySecret =
|
||||
.aria-label = Copy Secret
|
||||
|
||||
configure-auth-sso-rotate-date =
|
||||
{ DATETIME($date, year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric") }
|
||||
configure-auth-sso-rotate-activeSince = Active Since
|
||||
configure-auth-sso-rotate-inactiveAt = Inactive At
|
||||
configure-auth-sso-rotate-inactiveSince = Inactive Since
|
||||
|
||||
configure-auth-sso-rotate-status = Status
|
||||
configure-auth-sso-rotate-statusActive = Active
|
||||
configure-auth-sso-rotate-statusExpiring = Expiring
|
||||
configure-auth-sso-rotate-statusExpired = Expired
|
||||
configure-auth-sso-rotate-statusUnknown = Unknown
|
||||
|
||||
configure-auth-sso-rotate-expiringTooltip =
|
||||
An SSO key is expiring when it is scheduled for rotation.
|
||||
configure-auth-sso-rotate-expiringTooltip-toggleButton =
|
||||
.aria-label = Toggle expiring tooltip visibility
|
||||
configure-auth-sso-rotate-expiredTooltip =
|
||||
An SSO key is expired when it has been rotated out of use.
|
||||
configure-auth-sso-rotate-expiredTooltip-toggleButton =
|
||||
Toggle expired tooltip visibility
|
||||
|
||||
configure-auth-sso-rotate-rotate = Rotate
|
||||
configure-auth-sso-rotate-deactivateNow = Deactivate Now
|
||||
configure-auth-sso-rotate-delete = Delete
|
||||
|
||||
configure-auth-sso-rotate-now = Now
|
||||
configure-auth-sso-rotate-10seconds = 10 seconds from now
|
||||
configure-auth-sso-rotate-1day = 1 day from now
|
||||
configure-auth-sso-rotate-1week = 1 week from now
|
||||
configure-auth-sso-rotate-30days = 30 days from now
|
||||
configure-auth-sso-rotate-dropdown-description =
|
||||
.description = A dropdown to rotate the SSO key
|
||||
|
||||
configure-auth-local-loginWith = Login with email authentication
|
||||
configure-auth-local-useLoginOn = Use email authentication login on
|
||||
|
||||
|
||||
Reference in New Issue
Block a user