From 9af523318cf159a9884409e1f70c3ec5a89be63c Mon Sep 17 00:00:00 2001 From: Kiwi Date: Tue, 20 Nov 2018 17:41:35 +0100 Subject: [PATCH] [next] Prevent accidental lock out from admin or stream (#2084) * feat: prevent auth lockout by accident * test: add integration tests * test: update snapshots --- .../routes/configure/components/ConfigBox.tsx | 3 +- .../routes/configure/components/Configure.tsx | 39 +++++---- .../__snapshots__/Configure.spec.tsx.snap | 2 +- .../containers/ConfigureContainer.tsx | 12 ++- .../components/ConfigBoxWithToggleField.tsx | 2 + .../auth/components/FacebookConfig.tsx | 2 +- .../sections/auth/components/OIDCConfig.tsx | 2 +- .../sections/auth/components/SSOKeyField.tsx | 2 +- .../auth/containers/AuthContainer.tsx | 62 +++++++++++++- .../submitHook/SubmitHookContext.tsx | 5 +- .../__snapshots__/auth.spec.tsx.snap | 65 +++++++++------ .../client/admin/test/configure/auth.spec.tsx | 81 ++++++++++++++++++- src/core/client/admin/test/fixtures.ts | 2 +- .../framework/testHelpers/limitSnapshotTo.ts | 6 +- src/locales/en-US/admin.ftl | 5 ++ 15 files changed, 235 insertions(+), 55 deletions(-) diff --git a/src/core/client/admin/routes/configure/components/ConfigBox.tsx b/src/core/client/admin/routes/configure/components/ConfigBox.tsx index 8a6207cec..7cb0ed998 100644 --- a/src/core/client/admin/routes/configure/components/ConfigBox.tsx +++ b/src/core/client/admin/routes/configure/components/ConfigBox.tsx @@ -16,8 +16,9 @@ const ConfigBox: StatelessComponent = ({ title, topRight, children, + ...rest }) => ( -
+
{title}
{topRight}
diff --git a/src/core/client/admin/routes/configure/components/Configure.tsx b/src/core/client/admin/routes/configure/components/Configure.tsx index 310ba13ec..262cfc264 100644 --- a/src/core/client/admin/routes/configure/components/Configure.tsx +++ b/src/core/client/admin/routes/configure/components/Configure.tsx @@ -3,7 +3,7 @@ import { Localized } from "fluent-react/compat"; import React, { StatelessComponent } from "react"; import { Form, FormSpy } from "react-final-form"; -import { Button, HorizontalGutter } from "talk-ui/components"; +import { Button, CallOut, HorizontalGutter } from "talk-ui/components"; import Layout from "./Layout"; import Main from "./Main"; import { Link, Navigation } from "./Navigation"; @@ -19,9 +19,9 @@ const Configure: StatelessComponent = ({ onChange, children, }) => ( -
+
- {({ handleSubmit, submitting, pristine, form }) => ( + {({ handleSubmit, submitting, pristine, form, submitError }) => ( @@ -34,17 +34,28 @@ const Configure: StatelessComponent = ({ Misc - - - + + + + + {submitError && ( + + {submitError} + + )} +
{React.cloneElement(React.Children.only(children), { diff --git a/src/core/client/admin/routes/configure/components/__snapshots__/Configure.spec.tsx.snap b/src/core/client/admin/routes/configure/components/__snapshots__/Configure.spec.tsx.snap index 5495b1145..d289ff2a4 100644 --- a/src/core/client/admin/routes/configure/components/__snapshots__/Configure.spec.tsx.snap +++ b/src/core/client/admin/routes/configure/components/__snapshots__/Configure.spec.tsx.snap @@ -2,7 +2,7 @@ exports[`renders correctly 1`] = `
{ super(props); this.dirty = false; + const warningMessage = getMessage( props.localeBundles, "configure-unsavedInputWarning", @@ -53,12 +54,21 @@ class ConfigureContainer extends React.Component { data: UpdateSettingsInput["settings"], form: FormApi ) => { + let cancelled = false; + let formErrors: Record = {}; + const cancel = (errors: Record) => { + cancelled = true; + formErrors = { ...errors, ...formErrors }; + }; try { // Call submit hooks, that can manipulate what // we send as the mutation. let nextData = data; for (const hook of this.submitHooks) { - const result = await hook(nextData); + const result = await hook(nextData, cancel); + if (cancelled) { + return formErrors; + } if (result) { nextData = result; } diff --git a/src/core/client/admin/routes/configure/sections/auth/components/ConfigBoxWithToggleField.tsx b/src/core/client/admin/routes/configure/sections/auth/components/ConfigBoxWithToggleField.tsx index fbc3c91c1..50c09c4b2 100644 --- a/src/core/client/admin/routes/configure/sections/auth/components/ConfigBoxWithToggleField.tsx +++ b/src/core/client/admin/routes/configure/sections/auth/components/ConfigBoxWithToggleField.tsx @@ -22,10 +22,12 @@ const ConfigBoxWithToggleField: StatelessComponent = ({ title, disabled, children, + ...rest }) => ( {({ input }) => ( = ({ callbackURL, }) => ( Login with Facebook diff --git a/src/core/client/admin/routes/configure/sections/auth/components/OIDCConfig.tsx b/src/core/client/admin/routes/configure/sections/auth/components/OIDCConfig.tsx index d1d588dce..36936f86c 100644 --- a/src/core/client/admin/routes/configure/sections/auth/components/OIDCConfig.tsx +++ b/src/core/client/admin/routes/configure/sections/auth/components/OIDCConfig.tsx @@ -62,7 +62,7 @@ const OIDCConfig: StatelessComponent = ({ }; return ( Login with OIDC diff --git a/src/core/client/admin/routes/configure/sections/auth/components/SSOKeyField.tsx b/src/core/client/admin/routes/configure/sections/auth/components/SSOKeyField.tsx index ed34b8f37..24ded3436 100644 --- a/src/core/client/admin/routes/configure/sections/auth/components/SSOKeyField.tsx +++ b/src/core/client/admin/routes/configure/sections/auth/components/SSOKeyField.tsx @@ -26,7 +26,7 @@ const SSOKeyField: StatelessComponent = ({ disabled, onRegenerate, }) => ( - + Key diff --git a/src/core/client/admin/routes/configure/sections/auth/containers/AuthContainer.tsx b/src/core/client/admin/routes/configure/sections/auth/containers/AuthContainer.tsx index 916518e6e..0283a1128 100644 --- a/src/core/client/admin/routes/configure/sections/auth/containers/AuthContainer.tsx +++ b/src/core/client/admin/routes/configure/sections/auth/containers/AuthContainer.tsx @@ -1,31 +1,83 @@ -import { FormApi } from "final-form"; +import { FORM_ERROR, FormApi } from "final-form"; +import { Localized } from "fluent-react/compat"; import { RouteProps } from "found"; -import { merge } from "lodash"; +import { get, merge } from "lodash"; import React from "react"; import { graphql } from "react-relay"; import { AuthContainerQueryResponse } from "talk-admin/__generated__/AuthContainerQuery.graphql"; +import { TalkContext, withContext } from "talk-framework/lib/bootstrap"; +import { getMessage } from "talk-framework/lib/i18n"; import { Spinner } from "talk-ui/components"; +import { + AddSubmitHook, + RemoveSubmitHook, + SubmitHook, + withSubmitHookContext, +} from "../../../submitHook"; import Auth from "../components/Auth"; interface Props extends AuthContainerQueryResponse { + localeBundles: TalkContext["localeBundles"]; form: FormApi; submitting?: boolean; + addSubmitHook: AddSubmitHook; } export default class AuthContainer extends React.Component { public static routeConfig: RouteProps; private initialValues = {}; + private removeSubmitHook: RemoveSubmitHook; constructor(props: Props) { super(props); + this.removeSubmitHook = this.props.addSubmitHook(this.submitHook); } public componentDidMount() { this.props.form.initialize({ auth: this.initialValues }); } + public componentWillUnmount() { + this.removeSubmitHook(); + } + + private submitHook: SubmitHook = async (data, cancel) => { + const integrations = [ + get(data, "auth.integrations.google"), + get(data, "auth.integrations.facebook"), + get(data, "auth.integrations.sso"), + get(data, "auth.integrations.local"), + ...(get(data, "auth.integrations.oidc") || []), + ]; + if (!integrations.some((i: any) => i.enabled && i.targetFilter.admin)) { + cancel({ + [FORM_ERROR]: ( + + + Please enable at least one authentication integration for Talk + Admin + + + ), + }); + } else if ( + !integrations.some((i: any) => i.enabled && i.targetFilter.stream) + ) { + const confirmMessage = getMessage( + this.props.localeBundles, + "configure-auth-confirmNoAuthForCommentStream", + "No authentication integration has been enabled for the Comment Stream. Do you really want to continue?" + ); + + if (!window.confirm(confirmMessage)) { + cancel(); + } + } + return; + }; + private handleOnInitValues = (values: any) => { this.initialValues = merge(this.initialValues, values); }; @@ -41,8 +93,12 @@ export default class AuthContainer extends React.Component { } } +const enhanced = withSubmitHookContext(addSubmitHook => ({ addSubmitHook }))( + withContext(({ localeBundles }) => ({ localeBundles }))(AuthContainer) +); + AuthContainer.routeConfig = { - Component: AuthContainer, + Component: enhanced, query: graphql` query AuthContainerQuery { settings { diff --git a/src/core/client/admin/routes/configure/submitHook/SubmitHookContext.tsx b/src/core/client/admin/routes/configure/submitHook/SubmitHookContext.tsx index aaf25133f..046546628 100644 --- a/src/core/client/admin/routes/configure/submitHook/SubmitHookContext.tsx +++ b/src/core/client/admin/routes/configure/submitHook/SubmitHookContext.tsx @@ -1,7 +1,10 @@ import { noop } from "lodash"; import React from "react"; -export type SubmitHook = (data: any) => Promise | any; +export type SubmitHook = ( + data: any, + cancel: (errors?: Record) => void +) => Promise | any; export type RemoveSubmitHook = () => void; export type AddSubmitHook = (hook: SubmitHook) => RemoveSubmitHook; export type SubmitHookContext = AddSubmitHook; diff --git a/src/core/client/admin/test/configure/__snapshots__/auth.spec.tsx.snap b/src/core/client/admin/test/configure/__snapshots__/auth.spec.tsx.snap index ff25323e7..33590a7c2 100644 --- a/src/core/client/admin/test/configure/__snapshots__/auth.spec.tsx.snap +++ b/src/core/client/admin/test/configure/__snapshots__/auth.spec.tsx.snap @@ -3,7 +3,7 @@ exports[`change settings: during submit: oidc without errors 1`] = `
`; +exports[`prevents admin lock out 1`] = ` +
+ + Please enable at least one authentication integration for Talk Admin + +
+`; + exports[`regenerate sso key 1`] = `
- + +