[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:
Nick Funk
2020-03-20 20:59:59 +00:00
committed by GitHub
co-authored by Wyatt Johnson
parent 2dc63d3a27
commit 9a58673545
24 changed files with 1311 additions and 285 deletions
@@ -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 () => {
+20
View File
@@ -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: {