Skip to content

Commit

Permalink
Merge pull request #58 from metabase/master
Browse files Browse the repository at this point in the history
Fork Sync: Update from parent repository
  • Loading branch information
github-actions[bot] authored Dec 8, 2023
2 parents 6b30dc2 + 177a6b3 commit 52c33bc
Show file tree
Hide file tree
Showing 8 changed files with 556 additions and 123 deletions.
28 changes: 17 additions & 11 deletions e2e/test/scenarios/admin-2/sso/ldap.cy.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,22 +91,28 @@ describe(
it("shouldn't be possible to save a non-numeric port (#13313)", () => {
cy.visit("/admin/settings/authentication/ldap");

cy.findByLabelText("LDAP Port").parent().parent().as("portSection");

enterLdapSettings();
enterLdapPort("asd");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("That's not a valid port number").should("exist");
cy.get("@portSection")
.findByText("That's not a valid port number")
.should("exist");

enterLdapPort("21.3");
cy.button("Save and enable").click();
cy.wait("@updateLdapSettings");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText('For input string: "21.3"').should("exist");
cy.get("@portSection")
.findByText("That's not a valid port number")
.should("exist");

enterLdapPort("389 ");
cy.get("@portSection")
.findByText("That's not a valid port number")
.should("not.exist");

enterLdapPort("123 ");
cy.button("Save failed").click();
cy.button("Save and enable").click();
cy.wait("@updateLdapSettings");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText('For input string: "123 "').should("exist");
cy.findByText("Success").should("exist");
});

it("should allow user login on OSS when LDAP is enabled", () => {
Expand Down Expand Up @@ -211,9 +217,9 @@ const enterLdapPort = value => {
};

const enterLdapSettings = () => {
typeAndBlurUsingLabel("LDAP Host", "localhost");
typeAndBlurUsingLabel(/LDAP Host/, "localhost");
typeAndBlurUsingLabel("LDAP Port", "389");
typeAndBlurUsingLabel("Username or DN", "cn=admin,dc=example,dc=org");
typeAndBlurUsingLabel("Password", "adminpass");
typeAndBlurUsingLabel("User search base", "ou=users,dc=example,dc=org");
typeAndBlurUsingLabel(/User search base/, "ou=users,dc=example,dc=org");
};
7 changes: 0 additions & 7 deletions enterprise/frontend/src/metabase-enterprise/auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,6 @@ if (hasPremiumFeature("sso_ldap")) {
key: "ldap-group-membership-filter",
display_name: t`Group membership filter`,
type: "string",
validations: [
value =>
(value.match(/\(/g) || []).length !==
(value.match(/\)/g) || []).length
? t`Check your parentheses`
: null,
],
},
{
key: "ldap-sync-admin-group",
Expand Down

This file was deleted.

This file was deleted.

214 changes: 214 additions & 0 deletions frontend/src/metabase/admin/settings/components/SettingsLdapForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { useCallback, useMemo } from "react";
import { t } from "ttag";
import _ from "underscore";
import { connect } from "react-redux";
import * as Yup from "yup";
import type { TestConfig } from "yup";

import { updateLdapSettings } from "metabase/admin/settings/settings";

import { Stack, Group, Radio } from "metabase/ui";
import {
Form,
FormErrorMessage,
FormProvider,
FormRadioGroup,
FormSubmitButton,
FormSwitch,
FormTextInput,
} from "metabase/forms";
import Breadcrumbs from "metabase/components/Breadcrumbs";
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";

const testParentheses: TestConfig<string | null | undefined> = {
name: "test-parentheses",
message: "Check your parentheses",
test: value =>
(value?.match(/\(/g) || []).length === (value?.match(/\)/g) || []).length,
};

const testPort: TestConfig<string | null | undefined> = {
name: "test-port",
message: "That's not a valid port number",
test: value => Boolean((value || "").trim().match(/^\d*$/)),
};

const LDAP_SCHEMA = Yup.object({
"ldap-port": Yup.string().nullable().test(testPort),
"ldap-user-filter": Yup.string().nullable().test(testParentheses),
"ldap-group-membership-filter": Yup.string().nullable().test(testParentheses),
});

export type SettingValues = { [key: string]: SettingValue };

type LdapFormSettingElement = Omit<SettingElement, "key"> & {
key: string; // ensuring key is required
is_env_setting?: boolean;
env_name?: string;
default?: any;
};

type Props = {
elements: LdapFormSettingElement[];
settingValues: SettingValues;
onSubmit: (values: SettingValues) => void;
};

export const SettingsLdapFormView = ({
elements = [],
settingValues,
onSubmit,
}: Props) => {
const isEnabled = settingValues["ldap-enabled"];

const settings = useMemo(() => {
return _.indexBy(elements, "key");
}, [elements]);

const fields = useMemo(() => {
return _.mapObject(settings, setting => ({
name: setting.key,
label: setting.display_name,
description: setting.description,
placeholder: setting.is_env_setting
? t`Using ${setting.env_name}`
: setting.placeholder || setting.default,
required: setting.required,
autoFocus: setting.autoFocus,
}));
}, [settings]);

const attributeValues = useMemo(() => {
return getAttributeValues(settingValues);
}, [settingValues]);

const handleSubmit = useCallback(
values => {
return onSubmit({
...values,
"ldap-port": values["ldap-port"]?.trim(),
"ldap-enabled": true,
});
},
[onSubmit],
);

return (
<FormProvider
initialValues={attributeValues}
onSubmit={handleSubmit}
validationSchema={LDAP_SCHEMA}
enableReinitialize
>
{({ dirty }) => (
<Form m="0 1rem" maw="32.5rem">
<Breadcrumbs
className="mb3"
crumbs={[
[t`Authentication`, "/admin/settings/authentication"],
[t`LDAP`],
]}
/>
<FormSection title={"Server Settings"}>
<Stack spacing="md">
<FormTextInput {...fields["ldap-host"]} />
<FormTextInput {...fields["ldap-port"]} />
<FormRadioGroup {...fields["ldap-security"]}>
<Group mt={"xs"}>
<Radio value="none" label="None" />
<Radio value="ssl" label="SSL" />
<Radio value="starttls" label="StartTLS" />
</Group>
</FormRadioGroup>
<FormTextInput {...fields["ldap-bind-dn"]} />
<FormTextInput {...fields["ldap-password"]} type="password" />
</Stack>
</FormSection>
<FormSection title={"User Schema"}>
<Stack spacing="md">
<FormTextInput {...fields["ldap-user-base"]} />
<FormTextInput {...fields["ldap-user-filter"]} />
</Stack>
</FormSection>
<FormSection title={"Attributes"} collapsible>
<Stack spacing="md">
<FormTextInput {...fields["ldap-attribute-email"]} />
<FormTextInput {...fields["ldap-attribute-firstname"]} />
<FormTextInput {...fields["ldap-attribute-lastname"]} />
</Stack>
</FormSection>
<FormSection title={"Group Schema"}>
<Stack spacing={"md"}>
<GroupMappingsWidget
isFormik
setting={{ key: "ldap-group-sync" }}
onChange={handleSubmit}
settingValues={settingValues}
mappingSetting="ldap-group-mappings"
groupHeading={t`Group Name`}
groupPlaceholder={t`Group Name`}
/>
<FormTextInput {...fields["ldap-group-base"]} />
{"ldap-group-membership-filter" in fields &&
"ldap-group-membership-filter" in settingValues && (
<FormTextInput {...fields["ldap-group-membership-filter"]} />
)}
{"ldap-sync-admin-group" in fields &&
"ldap-sync-admin-group" in settingValues && (
<FormSwitch {...fields["ldap-sync-admin-group"]} />
)}
</Stack>
</FormSection>
<Stack align="start" spacing="1rem" mb="1rem">
<FormErrorMessage />
<FormSubmitButton
disabled={!dirty}
label={isEnabled ? t`Save changes` : t`Save and enable`}
variant="filled"
/>
</Stack>
</Form>
)}
</FormProvider>
);
};

const LDAP_ATTRS = [
// Server Settings
"ldap-host",
"ldap-port",
"ldap-security",
"ldap-bind-dn",
"ldap-password",

// User Schema
"ldap-user-base",
"ldap-user-filter",

// Attributes
"ldap-attribute-email",
"ldap-attribute-firstname",
"ldap-attribute-lastname",

// Group Schema
"ldap-group-sync",
"ldap-group-base",
"ldap-group-membership-filter",
"ldap-sync-admin-group",
];

const getAttributeValues = (values: SettingValues) => {
return Object.fromEntries(LDAP_ATTRS.map(key => [key, values[key]]));
};

const mapDispatchToProps = {
onSubmit: updateLdapSettings,
};

export const SettingsLdapForm = connect(
null,
mapDispatchToProps,
)(SettingsLdapFormView);
Loading

0 comments on commit 52c33bc

Please sign in to comment.