Skip to content
Snippets Groups Projects
Unverified Commit 9cff87e7 authored by metabase-bot[bot]'s avatar metabase-bot[bot] Committed by GitHub
Browse files

:robot: backported "Admin UI for disabling user auto provisioning" (#38928)


* Admin UI for disabling user auto provisioning (#38716)

* impl feature

* makes use of existing form components and deletes custom widget

* e2e tests

* fix lints

* fixes lint around Metabase name usage

* removed the select keys line to allow for whatever setting to be updated

* fix defaulting logic for ldap and jwt, fixes unit test

* snip snap snip snap

* fix unit and maybe lint tests

* fix ldap code

* fixes trimming null

* fixes e2e ldap tests

* e2e progress

* breaking local e2e in hopes of fixing ci...

---------

Co-authored-by: default avatarJerry Huang <jhuang37050@gmail.com>

* fix lint

---------

Co-authored-by: default avatarSloan Sparger <sloansparger@users.noreply.github.com>
Co-authored-by: default avatarJerry Huang <jhuang37050@gmail.com>
Co-authored-by: default avatarSloan Sparger <sloansparger@gmail.com>
parent 1b60fb8b
Branches
Tags
No related merge requests found
Showing
with 181 additions and 23 deletions
......@@ -11,6 +11,7 @@ import {
crudGroupMappingsWidget,
checkGroupConsistencyAfterDeletingMappings,
} from "./shared/group-mappings-widget";
import { getUserProvisioningInput, getSuccessUi } from "./shared/helpers";
describeEE("scenarios > admin > settings > SSO > JWT", () => {
beforeEach(() => {
......@@ -47,6 +48,17 @@ describeEE("scenarios > admin > settings > SSO > JWT", () => {
getJwtCard().findByText("Active").should("exist");
});
it("should allow the user to enable/disable user provisioning", () => {
setupJwt();
cy.visit("/admin/settings/authentication/jwt");
getUserProvisioningInput().click();
cy.button("Save changes").click();
cy.wait("@updateSettings");
getSuccessUi().should("exist");
});
it("should allow to reset jwt settings", () => {
setupJwt();
cy.visit("/admin/settings/authentication");
......
......@@ -12,6 +12,7 @@ import {
crudGroupMappingsWidget,
checkGroupConsistencyAfterDeletingMappings,
} from "./shared/group-mappings-widget";
import { getUserProvisioningInput, getSuccessUi } from "./shared/helpers";
describe(
"scenarios > admin > settings > SSO > LDAP",
......@@ -64,6 +65,17 @@ describe(
getLdapCard().findByText("Active").should("exist");
});
it("should allow the user to enable/disable user provisioning", () => {
setupLdap();
cy.visit("/admin/settings/authentication/ldap");
getUserProvisioningInput().click();
cy.button("Save changes").click();
cy.wait("@updateLdapSettings");
getSuccessUi().should("exist");
});
it("should allow to reset ldap settings", () => {
setupLdap();
cy.visit("/admin/settings/authentication");
......
......@@ -11,6 +11,7 @@ import {
crudGroupMappingsWidget,
checkGroupConsistencyAfterDeletingMappings,
} from "./shared/group-mappings-widget";
import { getUserProvisioningInput, getSuccessUi } from "./shared/helpers";
describeEE("scenarios > admin > settings > SSO > SAML", () => {
beforeEach(() => {
......@@ -76,6 +77,17 @@ describeEE("scenarios > admin > settings > SSO > SAML", () => {
getSamlCard().findByText("Set up").should("exist");
});
it("should allow the user to enable/disable user provisioning", () => {
setupSaml();
cy.visit("/admin/settings/authentication/saml");
getUserProvisioningInput().click();
cy.button("Save changes").click();
cy.wait("@updateSamlSettings");
getSuccessUi().should("exist");
});
describe("Group Mappings Widget", () => {
beforeEach(() => {
cy.intercept("GET", "/api/setting").as("getSettings");
......
export function getUserProvisioningInput() {
return cy
.findByTestId("admin-layout-content")
.findByText("User Provisioning");
}
export function getSuccessUi() {
return cy.findByTestId("admin-layout-content").findByText("Success");
}
......@@ -9,6 +9,7 @@ import {
FormProvider,
FormSecretKey,
FormSubmitButton,
FormSwitch,
FormTextInput,
} from "metabase/forms";
import Breadcrumbs from "metabase/components/Breadcrumbs";
......@@ -17,6 +18,7 @@ import { FormSection } from "metabase/containers/FormikForm";
import GroupMappingsWidget from "metabase/admin/settings/containers/GroupMappingsWidget";
import type { SettingValue } from "metabase-types/api";
import type { SettingElement } from "metabase/admin/settings/types";
import SettingHeader from "metabase/admin/settings/components/SettingHeader";
type SettingValues = { [key: string]: SettingValue };
......@@ -24,7 +26,7 @@ type JWTFormSettingElement = Omit<SettingElement, "key"> & {
key: string; // ensuring key is required
is_env_setting?: boolean;
env_name?: string;
default?: string;
default?: any;
};
type Props = {
......@@ -52,14 +54,16 @@ export const SettingsJWTForm = ({
placeholder: setting.is_env_setting
? t`Using ${setting.env_name}`
: setting.placeholder || setting.default,
default: setting.default,
required: setting.required,
autoFocus: setting.autoFocus,
onChanged: setting.onChanged,
}));
}, [settings]);
const attributeValues = useMemo(() => {
return getAttributeValues(settingValues);
}, [settingValues]);
return getAttributeValues(settings, settingValues);
}, [settings, settingValues]);
const handleSubmit = useCallback(
values => {
......@@ -83,6 +87,16 @@ export const SettingsJWTForm = ({
[t`JWT`],
]}
/>
<Stack spacing="0.75rem" m="2.5rem 0">
<SettingHeader
id="jwt-user-provisioning-enabled?"
setting={settings["jwt-user-provisioning-enabled?"]}
/>
<FormSwitch
id="jwt-user-provisioning-enabled?"
name={fields["jwt-user-provisioning-enabled?"].name}
/>
</Stack>
<FormSection title={"Server Settings"}>
<Stack spacing="md">
<FormTextInput {...fields["jwt-identity-provider-uri"]} />
......@@ -105,7 +119,7 @@ export const SettingsJWTForm = ({
<FormTextInput {...fields["jwt-attribute-lastname"]} />
</Stack>
</FormSection>
<FormSection title={"Group Schema"}>
<FormSection title={"Group Schema"} data-testid="jwt-group-schema">
<GroupMappingsWidget
isFormik
setting={{ key: "jwt-group-sync" }}
......@@ -116,7 +130,6 @@ export const SettingsJWTForm = ({
groupPlaceholder={t`Group Name`}
/>
</FormSection>
<Flex direction={"column"} align={"start"} gap={"1rem"}>
<FormErrorMessage />
<FormSubmitButton
......@@ -132,6 +145,7 @@ export const SettingsJWTForm = ({
};
const JWT_ATTRS = [
"jwt-user-provisioning-enabled?",
"jwt-identity-provider-uri",
"jwt-shared-secret",
"jwt-attribute-email",
......@@ -140,8 +154,20 @@ const JWT_ATTRS = [
"jwt-group-sync",
];
const getAttributeValues = (values: SettingValues) => {
return Object.fromEntries(JWT_ATTRS.map(key => [key, values[key]]));
const DEFAULTABLE_JWT_ATTRS = new Set(["jwt-user-provisioning-enabled?"]);
const getAttributeValues = (
settings: Record<string, JWTFormSettingElement>,
values: SettingValues,
) => {
return Object.fromEntries(
JWT_ATTRS.map(key => [
key,
DEFAULTABLE_JWT_ATTRS.has(key)
? values[key] ?? settings[key]?.default
: values[key],
]),
);
};
const mapDispatchToProps = {
......
import fetchMock from "fetch-mock";
import userEvent from "@testing-library/user-event";
import { renderWithProviders, screen, waitFor } from "__support__/ui";
import { renderWithProviders, screen, waitFor, within } from "__support__/ui";
import { createMockGroup } from "metabase-types/api/mocks";
import { SettingsJWTForm } from "./SettingsJWTForm";
......@@ -25,6 +25,16 @@ const elements = [
display_name: "JWT Authentication",
type: "boolean",
},
{
key: "jwt-user-provisioning-enabled?",
value: null,
is_env_setting: false,
env_name: "MB_JWT_USER_PROVISIONING_ENABLED",
display_name: "User Provisioning",
description:
"When we enable JWT user provisioning, we automatically create a Metabase account on LDAP signin for users who\ndon't have one.",
default: true,
},
{
placeholder: "https://jwt.yourdomain.org",
key: "jwt-identity-provider-uri",
......@@ -132,6 +142,7 @@ describe("SettingsJWTForm", () => {
const { onSubmit } = setup();
const ATTRS = {
"jwt-user-provisioning-enabled?": false,
"jwt-identity-provider-uri": "http://example.com",
"jwt-shared-secret":
"590ab155f412d477b8ab9c8b0e7b2e3ab4d4523e83770a724a2088edbde7f19a",
......@@ -142,6 +153,7 @@ describe("SettingsJWTForm", () => {
"jwt-group-sync": true,
};
userEvent.click(screen.getByLabelText(/User Provisioning/));
userEvent.type(
await screen.findByRole("textbox", { name: /JWT Identity Provider URI/ }),
ATTRS["jwt-identity-provider-uri"],
......@@ -164,7 +176,8 @@ describe("SettingsJWTForm", () => {
await screen.findByRole("textbox", { name: /Last name attribute/ }),
ATTRS["jwt-attribute-lastname"],
);
userEvent.click(screen.getByRole("checkbox")); // checkbox for "jwt-group-sync"
const groupSchema = await screen.findByTestId("jwt-group-schema");
userEvent.click(within(groupSchema).getByRole("checkbox")); // checkbox for "jwt-group-sync"
userEvent.click(await screen.findByRole("button", { name: /Save/ }));
......
......@@ -11,15 +11,16 @@ import {
FormErrorMessage,
FormProvider,
FormSubmitButton,
FormSwitch,
FormTextarea,
FormTextInput,
} from "metabase/forms";
import { Stack } from "metabase/ui";
import MetabaseSettings from "metabase/lib/settings";
import GroupMappingsWidget from "metabase/admin/settings/containers/GroupMappingsWidget";
import { updateSamlSettings } from "metabase/admin/settings/settings";
import { settingToFormField } from "metabase/admin/settings/utils";
import SettingHeader from "metabase/admin/settings/components/SettingHeader";
import {
SAMLFormCaption,
SAMLFormFooter,
......@@ -79,7 +80,7 @@ const SettingsSAMLForm = ({ elements = [], settingValues = {}, onSubmit }) => {
[t`SAML`],
]}
/>
<h2 className="mb3">{t`Set up SAML-based SSO`}</h2>
<h2 className="mb2">{t`Set up SAML-based SSO`}</h2>
<SAMLFormCaption>
{jt`Use the settings below to configure your SSO via SAML. If you have any questions, check out our ${(
<ExternalLink
......@@ -87,6 +88,16 @@ const SettingsSAMLForm = ({ elements = [], settingValues = {}, onSubmit }) => {
>{t`documentation`}</ExternalLink>
)}.`}
</SAMLFormCaption>
<Stack spacing="0.75rem" m="2.5rem 0">
<SettingHeader
id="saml-user-provisioning-enabled?"
setting={settings["saml-user-provisioning-enabled?"]}
/>
<FormSwitch
id="saml-user-provisioning-enabled?"
name={fields["saml-user-provisioning-enabled?"].name}
/>
</Stack>
<SAMLFormSection>
<h3 className="mb0">{t`Configure your identity provider (IdP)`}</h3>
<p className="mb4 mt1 text-medium">{t`Your identity provider will need the following info about Metabase.`}</p>
......@@ -212,6 +223,7 @@ const SettingsSAMLForm = ({ elements = [], settingValues = {}, onSubmit }) => {
// 1) Our `settingValues` has settings unrelated to SAML, which was previously sifted by collecting only those matching inline field names in our form.
// 2) Some values should be replaced by defaults.
const IS_SAML_ATTR_DEFAULTABLE = {
"saml-user-provisioning-enabled?": true,
"saml-attribute-email": true,
"saml-attribute-firstname": true,
"saml-attribute-lastname": true,
......
......@@ -22,7 +22,7 @@ export const SAMLFormSection = styled.div<SAMLFormSectionProps>`
export const SAMLFormCaption = styled.div`
color: ${color("text-medium")};
margin-bottom: 1rem;
margin-bottom: 2rem;
`;
export const SAMLFormFooter = styled.div`
......
......@@ -2,6 +2,7 @@ import * as Yup from "yup";
export const JWT_SCHEMA = Yup.object({
"jwt-enabled": Yup.boolean().default(false),
"jwt-user-provisioning-enabled?": Yup.boolean().default(null),
"jwt-identity-provider-uri": Yup.string().nullable().default(null),
"jwt-shared-secret": Yup.string().nullable().default(null),
"jwt-attribute-email": Yup.string().nullable().default(null),
......@@ -13,6 +14,7 @@ export const JWT_SCHEMA = Yup.object({
export const SAML_SCHEMA = Yup.object({
"saml-enabled": Yup.boolean().default(false),
"saml-user-provisioning-enabled?": Yup.boolean().default(null),
"saml-identity-provider-uri": Yup.string().nullable().default(null),
"saml-identity-provider-issuer": Yup.string().nullable().default(null),
"saml-identity-provider-certificate": Yup.string().nullable().default(null),
......
......@@ -94,6 +94,12 @@ PLUGIN_ADMIN_SETTINGS_UPDATES.push(sections => ({
key: "saml-enabled",
getHidden: () => true,
},
{
key: "saml-user-provisioning-enabled?",
display_name: t`User Provisioning`,
description: t`When a user logs in via SAML, create a Metabase account for them automatically if they don't have one.`,
type: "boolean",
},
{
key: "saml-identity-provider-uri",
display_name: t`SAML Identity Provider SSO URL`,
......@@ -179,7 +185,12 @@ PLUGIN_ADMIN_SETTINGS_UPDATES.push(sections => ({
key: "jwt-enabled",
display_name: t`JWT Authentication`,
type: "boolean",
getHidden: () => true,
},
{
key: "jwt-user-provisioning-enabled?",
display_name: t`User Provisioning`,
description: t`When a user logs in via JWT, create a Metabase account for them automatically if they don't have one.`,
type: "boolean",
},
{
key: "jwt-identity-provider-uri",
......
......@@ -21,6 +21,7 @@ export const GOOGLE_SCHEMA = Yup.object({
export const LDAP_SCHEMA = Yup.object({
"ldap-enabled": Yup.boolean().nullable().default(false),
"ldap-user-provisioning-enabled?": Yup.boolean().default(null),
"ldap-host": Yup.string().nullable().default(null),
"ldap-port": Yup.number().nullable().default(null),
"ldap-security": Yup.string().nullable().default("none"),
......
......@@ -22,6 +22,7 @@ import { FormSection } from "metabase/containers/FormikForm";
import GroupMappingsWidget from "metabase/admin/settings/containers/GroupMappingsWidget";
import type { SettingValue } from "metabase-types/api";
import type { SettingElement } from "metabase/admin/settings/types";
import SettingHeader from "metabase/admin/settings/components/SettingHeader";
const testParentheses: TestConfig<string | null | undefined> = {
name: "test-parentheses",
......@@ -76,14 +77,15 @@ export const SettingsLdapFormView = ({
placeholder: setting.is_env_setting
? t`Using ${setting.env_name}`
: setting.placeholder || setting.default,
default: setting.default,
required: setting.required,
autoFocus: setting.autoFocus,
}));
}, [settings]);
const attributeValues = useMemo(() => {
return getAttributeValues(settingValues);
}, [settingValues]);
return getAttributeValues(settings, settingValues);
}, [settings, settingValues]);
const handleSubmit = useCallback(
values => {
......@@ -112,6 +114,17 @@ export const SettingsLdapFormView = ({
[t`LDAP`],
]}
/>
<Stack spacing="0.75rem" m="2.5rem 0">
<SettingHeader
id="ldap-user-provisioning-enabled?"
setting={settings["ldap-user-provisioning-enabled?"]}
/>
<FormSwitch
id="ldap-user-provisioning-enabled?"
name={fields["ldap-user-provisioning-enabled?"].name}
defaultChecked={fields["ldap-user-provisioning-enabled?"].default}
/>
</Stack>
<FormSection title={"Server Settings"}>
<Stack spacing="md">
<FormTextInput {...fields["ldap-host"]} />
......@@ -177,6 +190,9 @@ export const SettingsLdapFormView = ({
};
const LDAP_ATTRS = [
// User Provision Settings
"ldap-user-provisioning-enabled?",
// Server Settings
"ldap-host",
"ldap-port",
......@@ -200,8 +216,20 @@ const LDAP_ATTRS = [
"ldap-sync-admin-group",
];
const getAttributeValues = (values: SettingValues) => {
return Object.fromEntries(LDAP_ATTRS.map(key => [key, values[key]]));
const DEFAULTABLE_LDAP_ATTRS = new Set(["ldap-user-provisioning-enabled?"]);
const getAttributeValues = (
settings: Record<string, LdapFormSettingElement>,
values: SettingValues,
) => {
return Object.fromEntries(
LDAP_ATTRS.map(key => [
key,
DEFAULTABLE_LDAP_ATTRS.has(key)
? values[key] ?? settings[key]?.default
: values[key],
]),
);
};
const mapDispatchToProps = {
......
......@@ -28,6 +28,16 @@ const elements = [
display_name: "LDAP Authentication",
type: "boolean",
},
{
key: "ldap-user-provisioning-enabled?",
value: null,
is_env_setting: false,
env_name: "MB_LDAP_USER_PROVISIONING_ENABLED",
display_name: "User Provisioning",
description:
"When we enable LDAP user provisioning, we automatically create a Metabase account on LDAP signin for users who\ndon't have one.",
default: true,
},
{
placeholder: "ldap.yourdomain.org",
key: "ldap-host",
......@@ -237,6 +247,7 @@ describe("SettingsLdapForm", () => {
});
const ATTRS = {
"ldap-user-provisioning-enabled?": false,
"ldap-host": "example.com",
"ldap-port": "123",
"ldap-security": "ssl",
......@@ -254,6 +265,7 @@ describe("SettingsLdapForm", () => {
"ldap-sync-admin-group": true,
};
userEvent.click(screen.getByLabelText(/User Provisioning/));
userEvent.type(
await screen.findByRole("textbox", { name: /LDAP Host/ }),
ATTRS["ldap-host"],
......
......@@ -10,6 +10,7 @@ export const prepareAnalyticsValue = setting =>
export const settingToFormField = setting => ({
name: setting.key,
label: setting.display_name,
description: setting.description,
placeholder: setting.is_env_setting
? t`Using ${setting.env_name}`
......
......@@ -10,19 +10,19 @@ interface SectionProps {
children: React.ReactNode;
}
function StandardSection({ title, children }: SectionProps) {
function StandardSection({ title, children, ...props }: SectionProps) {
return (
<section className="mb4">
<section className="mb4" {...props}>
{title && <h2 className="mb2">{title}</h2>}
{children}
</section>
);
}
function CollapsibleSection({ title, children }: SectionProps) {
function CollapsibleSection({ title, children, ...props }: SectionProps) {
const [isExpanded, { toggle: handleToggle }] = useToggle(false);
return (
<section className="mb4">
<section className="mb4" {...props}>
<CollapsibleSectionContent onClick={handleToggle}>
<DisclosureTriangle className="mr1" open={isExpanded} />
<h3>{title}</h3>
......
......@@ -34,6 +34,12 @@ PLUGIN_ADMIN_SETTINGS_UPDATES.push(
type: "boolean",
getHidden: () => true,
},
{
key: "ldap-user-provisioning-enabled?",
display_name: t`User Provisioning`,
description: t`When a user logs in via LDAP, create a Metabase account for them automatically if they don't have one.`,
type: "boolean",
},
{
key: "ldap-host",
display_name: t`LDAP Host`,
......
......@@ -104,14 +104,15 @@
{settings :map}
(api/check-superuser)
(let [ldap-settings (-> settings
(select-keys (keys ldap/mb-settings->ldap-details))
(assoc :ldap-port (when-let [^String ldap-port (not-empty (str (:ldap-port settings)))]
(Long/parseLong ldap-port)))
(update :ldap-password update-password-if-needed))
(update :ldap-password update-password-if-needed)
(dissoc :ldap-enabled))
ldap-details (set/rename-keys ldap-settings ldap/mb-settings->ldap-details)
results (ldap/test-ldap-connection ldap-details)]
(if (= :SUCCESS (:status results))
(t2/with-transaction [_conn]
;; We need to update the ldap settings before we update ldap-enabled, as the ldap-enabled setter tests the ldap settings
(setting/set-many! ldap-settings)
(setting/set-value-of-type! :boolean :ldap-enabled (boolean (:ldap-enabled settings))))
;; test failed, return result message
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment