From 99af95b10cfa397458b21919868ccebb16d361f9 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Thu, 24 Oct 2024 09:35:31 -0700 Subject: [PATCH] Committed harmony on SAML. Streamlined the tests even further. --- ...rd-authentication-by-saml-configuration.ts | 355 +-------------- .../admin/providers/saml/SAMLProviderForm.ts | 419 +----------------- .../providers/saml/SAMLProviderFormForm.ts | 394 ++++++++++++++++ web/tests/specs/providers.ts | 85 ++-- web/tests/specs/shared-sequences.ts | 35 ++ 5 files changed, 491 insertions(+), 797 deletions(-) create mode 100644 web/src/admin/providers/saml/SAMLProviderFormForm.ts create mode 100644 web/tests/specs/shared-sequences.ts diff --git a/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts b/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts index 61c1f6403d..9d419d8b07 100644 --- a/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts +++ b/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts @@ -1,363 +1,34 @@ -import "@goauthentik/admin/applications/wizard/ak-wizard-title"; -import "@goauthentik/admin/applications/wizard/ak-wizard-title"; -import "@goauthentik/admin/common/ak-crypto-certificate-search"; import AkCryptoCertificateSearch from "@goauthentik/admin/common/ak-crypto-certificate-search"; -import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search"; -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { first } from "@goauthentik/common/utils"; -import "@goauthentik/components/ak-multi-select"; -import "@goauthentik/components/ak-number-input"; -import "@goauthentik/components/ak-radio-input"; -import "@goauthentik/components/ak-switch-input"; -import "@goauthentik/components/ak-text-input"; -import "@goauthentik/elements/forms/FormGroup"; -import "@goauthentik/elements/forms/HorizontalFormElement"; import { msg } from "@lit/localize"; import { customElement, state } from "@lit/reactive-element/decorators.js"; -import { html, nothing } from "lit"; -import { ifDefined } from "lit/directives/if-defined.js"; +import { html } from "lit"; -import { - FlowsInstancesListDesignationEnum, - PaginatedSAMLPropertyMappingList, - PropertymappingsApi, - SAMLProvider, -} from "@goauthentik/api"; +import { SAMLProvider } from "@goauthentik/api"; import BaseProviderPanel from "../BaseProviderPanel"; -import { - digestAlgorithmOptions, - signatureAlgorithmOptions, - spBindingOptions, -} from "./SamlProviderOptions"; import "./saml-property-mappings-search"; @customElement("ak-application-wizard-authentication-by-saml-configuration") export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPanel { - @state() - propertyMappings?: PaginatedSAMLPropertyMappingList; - @state() hasSigningKp = false; - constructor() { - super(); - new PropertymappingsApi(DEFAULT_CONFIG) - .propertymappingsProviderSamlList({ - ordering: "saml_name", - }) - .then((propertyMappings: PaginatedSAMLPropertyMappingList) => { - this.propertyMappings = propertyMappings; - }); - } - - propertyMappingConfiguration(provider?: SAMLProvider) { - const propertyMappings = this.propertyMappings?.results ?? []; - - const configuredMappings = (providerMappings: string[]) => - propertyMappings.map((pm) => pm.pk).filter((pmpk) => providerMappings.includes(pmpk)); - - const managedMappings = () => - propertyMappings - .filter((pm) => (pm?.managed ?? "").startsWith("goauthentik.io/providers/saml")) - .map((pm) => pm.pk); - - const pmValues = provider?.propertyMappings - ? configuredMappings(provider?.propertyMappings ?? []) - : managedMappings(); - - const propertyPairs = propertyMappings.map((pm) => [pm.pk, pm.name]); - - return { pmValues, propertyPairs }; - } - render() { - const provider = this.wizard.provider as SAMLProvider | undefined; - const errors = this.wizard.errors.provider; - - const { pmValues, propertyPairs } = this.propertyMappingConfiguration(provider); + const setHasSigningKp = (ev: InputEvent) => { + const target = ev.target as AkCryptoCertificateSearch; + if (!target) return; + this.hasSigningKp = !!target.selectedKeypair; + }; return html` ${msg("Configure SAML Provider")}
- - - - -

- ${msg("Flow used when authorizing this provider.")} -

-
- - - ${msg("Protocol settings")} -
- - - - - - - - -
-
- - - ${msg("Advanced flow settings")} - - -

- ${msg( - "Flow used when a user access this provider and is not authenticated.", - )} -

-
- - -

- ${msg("Flow used when logging out of this provider.")} -

-
- -
- - ${msg("Advanced protocol settings")} -
- - { - const target = ev.target as AkCryptoCertificateSearch; - if (!target) return; - this.hasSigningKp = !!target.selectedKeypair; - }} - > -

- ${msg( - "Certificate used to sign outgoing Responses going to the Service Provider.", - )} -

-
- ${ - this.hasSigningKp - ? html` - -

- ${msg( - "When enabled, the assertion element of the SAML response will be signed.", - )} -

-
- - -

- ${msg( - "When enabled, the assertion element of the SAML response will be signed.", - )} -

-
` - : nothing - } - - - -

- ${msg( - "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.", - )} -

-
- - - -

- ${msg( - "When selected, encrypted assertions will be decrypted using this keypair.", - )} -

-
- - - ${msg("Property mappings used for user mapping.")} -

`} - >
- - - -

- ${msg( - "Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.", - )} -

-
- - - - - - - - - - - - -
-
+ ${renderForm( + (this.wizard.provider as SAMLProvider) ?? {}, + this.wizard.errors.provider, + setHasSigningKp, + this.hasSigningKp, + )}
`; } } diff --git a/web/src/admin/providers/saml/SAMLProviderForm.ts b/web/src/admin/providers/saml/SAMLProviderForm.ts index ef35d2960b..f54ead0c90 100644 --- a/web/src/admin/providers/saml/SAMLProviderForm.ts +++ b/web/src/admin/providers/saml/SAMLProviderForm.ts @@ -1,58 +1,11 @@ -import { - digestAlgorithmOptions, - signatureAlgorithmOptions, -} from "@goauthentik/admin/applications/wizard/methods/saml/SamlProviderOptions"; -import "@goauthentik/admin/common/ak-crypto-certificate-search"; -import AkCryptoCertificateSearch from "@goauthentik/admin/common/ak-crypto-certificate-search"; -import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { first } from "@goauthentik/common/utils"; -import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js"; -import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; -import "@goauthentik/elements/forms/FormGroup"; -import "@goauthentik/elements/forms/HorizontalFormElement"; -import "@goauthentik/elements/forms/Radio"; -import "@goauthentik/elements/forms/SearchSelect"; -import "@goauthentik/elements/utils/TimeDeltaHelp"; -import { msg } from "@lit/localize"; -import { TemplateResult, html, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; -import { ifDefined } from "lit/directives/if-defined.js"; -import { - FlowsInstancesListDesignationEnum, - PropertymappingsApi, - PropertymappingsProviderSamlListRequest, - ProvidersApi, - SAMLPropertyMapping, - SAMLProvider, - SpBindingEnum, -} from "@goauthentik/api"; +import { ProvidersApi, SAMLProvider } from "@goauthentik/api"; -export async function samlPropertyMappingsProvider(page = 1, search = "") { - const propertyMappings = await new PropertymappingsApi( - DEFAULT_CONFIG, - ).propertymappingsProviderSamlList({ - ordering: "saml_name", - pageSize: 20, - search: search.trim(), - page, - }); - return { - pagination: propertyMappings.pagination, - options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]), - }; -} - -export function makeSAMLPropertyMappingsSelector(instanceMappings?: string[]) { - const localMappings = instanceMappings ? new Set(instanceMappings) : undefined; - return localMappings - ? ([pk, _]: DualSelectPair) => localMappings.has(pk) - : ([_0, _1, _2, mapping]: DualSelectPair) => - mapping?.managed?.startsWith("goauthentik.io/providers/saml"); -} +import { renderForm } from "./SAMLProviderFormForm.js"; @customElement("ak-provider-saml-form") export class SAMLProviderFormPage extends BaseProviderForm { @@ -80,368 +33,14 @@ export class SAMLProviderFormPage extends BaseProviderForm { } } - renderForm(): TemplateResult { - return html` - - - - -

- ${msg("Flow used when authorizing this provider.")} -

-
+ renderForm() { + const setHasSigningKp = (ev: InputEvent) => { + const target = ev.target as AkCryptoCertificateSearch; + if (!target) return; + this.hasSigningKp = !!target.selectedKeypair; + }; - - ${msg("Protocol settings")} -
- - - - - -

${msg("Also known as EntityID.")}

-
- - - -

- ${msg( - "Determines how authentik sends the response back to the Service Provider.", - )} -

-
- - - -
-
- - - ${msg("Advanced flow settings")} -
- - -

- ${msg( - "Flow used when a user access this provider and is not authenticated.", - )} -

-
- - -

- ${msg("Flow used when logging out of this provider.")} -

-
-
-
- - - ${msg("Advanced protocol settings")} -
- - { - const target = ev.target as AkCryptoCertificateSearch; - if (!target) return; - this.hasSigningKp = !!target.selectedKeypair; - }} - > -

- ${msg( - "Certificate used to sign outgoing Responses going to the Service Provider.", - )} -

-
- ${this.hasSigningKp - ? html` - -

- ${msg( - "When enabled, the assertion element of the SAML response will be signed.", - )} -

-
- - -

- ${msg( - "When enabled, the assertion element of the SAML response will be signed.", - )} -

-
` - : nothing} - - -

- ${msg( - "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.", - )} -

-
- - -

- ${msg( - "When selected, assertions will be encrypted using this keypair.", - )} -

-
- - - - - => { - const args: PropertymappingsProviderSamlListRequest = { - ordering: "saml_name", - }; - if (query !== undefined) { - args.search = query; - } - const items = await new PropertymappingsApi( - DEFAULT_CONFIG, - ).propertymappingsProviderSamlList(args); - return items.results; - }} - .renderElement=${(item: SAMLPropertyMapping): string => { - return item.name; - }} - .value=${( - item: SAMLPropertyMapping | undefined, - ): string | undefined => { - return item?.pk; - }} - .selected=${(item: SAMLPropertyMapping): boolean => { - return this.instance?.nameIdMapping === item.pk; - }} - ?blankable=${true} - > - -

- ${msg( - "Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.", - )} -

-
- - - -

- ${msg("Configure the maximum allowed time drift for an assertion.")} -

- -
- - -

- ${msg("Assertion not valid on or after current time + this value.")} -

- -
- - -

- ${msg("Session not valid on or after current time + this value.")} -

- -
- - -

- ${msg( - "When using IDP-initiated logins, the relay state will be set to this value.", - )} -

- -
- - - - - - - - - -
-
`; + return renderForm(this.instance ?? {}, [], setHasSigningKp, this.hasSigningKp); } } diff --git a/web/src/admin/providers/saml/SAMLProviderFormForm.ts b/web/src/admin/providers/saml/SAMLProviderFormForm.ts new file mode 100644 index 0000000000..5fe9d9b873 --- /dev/null +++ b/web/src/admin/providers/saml/SAMLProviderFormForm.ts @@ -0,0 +1,394 @@ +import { + digestAlgorithmOptions, + signatureAlgorithmOptions, +} from "@goauthentik/admin/applications/wizard/methods/saml/SamlProviderOptions"; +import "@goauthentik/admin/common/ak-crypto-certificate-search"; +import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { first } from "@goauthentik/common/utils"; +import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js"; +import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; +import "@goauthentik/elements/forms/FormGroup"; +import "@goauthentik/elements/forms/HorizontalFormElement"; +import "@goauthentik/elements/forms/Radio"; +import "@goauthentik/elements/forms/SearchSelect"; +import "@goauthentik/elements/utils/TimeDeltaHelp"; + +import { msg } from "@lit/localize"; +import { html, nothing } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { + FlowsInstancesListDesignationEnum, + PropertymappingsApi, + PropertymappingsProviderSamlListRequest, + SAMLPropertyMapping, + SAMLProvider, + SpBindingEnum, + ValidationError, +} from "@goauthentik/api"; + +export async function samlPropertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsProviderSamlList({ + ordering: "saml_name", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]), + }; +} + +export function makeSAMLPropertyMappingsSelector(instanceMappings?: string[]) { + const localMappings = instanceMappings ? new Set(instanceMappings) : undefined; + return localMappings + ? ([pk, _]: DualSelectPair) => localMappings.has(pk) + : ([_0, _1, _2, mapping]: DualSelectPair) => + mapping?.managed?.startsWith("goauthentik.io/providers/saml"); +} + +export function renderForm( + provider?: Partial, + errors: ValidationError, + setHasSigningKp: (ev: InputEvent) => void, + hasSigningKp: boolean, +) { + return html` + + + + +

+ ${msg("Flow used when authorizing this provider.")} +

+
+ + + ${msg("Protocol settings")} +
+ + + + + +

${msg("Also known as EntityID.")}

+
+ + + +

+ ${msg( + "Determines how authentik sends the response back to the Service Provider.", + )} +

+
+ + + +
+
+ + + ${msg("Advanced flow settings")} +
+ + +

+ ${msg( + "Flow used when a user access this provider and is not authenticated.", + )} +

+
+ + +

+ ${msg("Flow used when logging out of this provider.")} +

+
+
+
+ + + ${msg("Advanced protocol settings")} +
+ + +

+ ${msg( + "Certificate used to sign outgoing Responses going to the Service Provider.", + )} +

+
+ ${hasSigningKp + ? html` + +

+ ${msg( + "When enabled, the assertion element of the SAML response will be signed.", + )} +

+
+ + +

+ ${msg( + "When enabled, the assertion element of the SAML response will be signed.", + )} +

+
` + : nothing} + + +

+ ${msg( + "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.", + )} +

+
+ + +

+ ${msg("When selected, assertions will be encrypted using this keypair.")} +

+
+ + + + + => { + const args: PropertymappingsProviderSamlListRequest = { + ordering: "saml_name", + }; + if (query !== undefined) { + args.search = query; + } + const items = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsProviderSamlList(args); + return items.results; + }} + .renderElement=${(item: SAMLPropertyMapping): string => { + return item.name; + }} + .value=${(item: SAMLPropertyMapping | undefined): string | undefined => { + return item?.pk; + }} + .selected=${(item: SAMLPropertyMapping): boolean => { + return provider?.nameIdMapping === item.pk; + }} + ?blankable=${true} + > + +

+ ${msg( + "Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.", + )} +

+
+ + + +

+ ${msg("Configure the maximum allowed time drift for an assertion.")} +

+ +
+ + +

+ ${msg("Assertion not valid on or after current time + this value.")} +

+ +
+ + +

+ ${msg("Session not valid on or after current time + this value.")} +

+ +
+ + +

+ ${msg( + "When using IDP-initiated logins, the relay state will be set to this value.", + )} +

+ +
+ + + + + + + + + +
+
`; +} diff --git a/web/tests/specs/providers.ts b/web/tests/specs/providers.ts index 8582eae4a2..4a69453835 100644 --- a/web/tests/specs/providers.ts +++ b/web/tests/specs/providers.ts @@ -1,28 +1,43 @@ import { expect } from "@wdio/globals"; -import { - clickButton, - setFormGroup, - setSearchSelect, - setTextInput, - setTypeCreate, -} from "pageobjects/controls.js"; import ProviderWizardView from "../pageobjects/provider-wizard.page.js"; import ProvidersListPage from "../pageobjects/providers-list.page.js"; -import { randomId } from "../utils/index.js"; import { login } from "../utils/login.js"; +import { + simpleLDAPProviderForm, + simpleOAuth2ProviderForm, + simpleRadiusProviderForm, +} from "./shared-sequences.js"; async function reachTheProvider() { await ProvidersListPage.logout(); await login(); await ProvidersListPage.open(); await expect(await ProvidersListPage.pageHeader()).toHaveText("Providers"); + await expect(await containedMessages()).not.toContain("Successfully created provider."); await ProvidersListPage.startWizardButton.click(); await ProviderWizardView.wizardTitle.waitForDisplayed(); await expect(await ProviderWizardView.wizardTitle).toHaveText("New provider"); } +const containedMessages = async () => + await (async () => { + const messages = []; + for await (const alert of $("ak-message-container").$$("ak-message")) { + messages.push(await alert.$("p.pf-c-alert__title").getText()); + } + return messages; + })(); + +const hasProviderSuccessMessage = async () => + await browser.waitUntil( + async () => (await containedMessages()).includes("Successfully created provider."), + { timeout: 1000, timeoutMsg: "Expected to see provider success message." }, + ); + +type FieldDesc = [(..._: unknown) => Promise, ...unknown]; + async function fillOutFields(fields: FieldDesc[]) { for (const field of fields) { const thefunc = field[0]; @@ -33,64 +48,44 @@ async function fillOutFields(fields: FieldDesc[]) { describe("Configure Oauth2 Providers", () => { it("Should configure a simple OAuth2 Provider", async () => { - const newProviderName = `New OAuth2 Provider - ${randomId()}`; - await reachTheProvider(); - await $("ak-wizard-page-type-create").waitForDisplayed(); - - // prettier-ignore - await fillOutFields([ - [setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"], - [clickButton, "Next"], - [setTextInput, "name", newProviderName], - [setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"], - ]); - + await fillOutFields(simpleOAuth2ProviderForm()); await ProviderWizardView.pause(); await ProviderWizardView.nextButton.click(); + await hasProviderSuccessMessage(); }); }); describe("Configure LDAP Providers", () => { it("Should configure a simple LDAP Provider", async () => { - const newProviderName = `New LDAP Provider - ${randomId()}`; - await reachTheProvider(); await $("ak-wizard-page-type-create").waitForDisplayed(); - - // prettier-ignore - await fillOutFields([ - [setTypeCreate, "selectProviderType", "LDAP Provider"], - [clickButton, "Next"], - [setTextInput, "name", newProviderName], - [setFormGroup, /Flow settings/, "open"], - // This will never not weird me out. - [setSearchSelect, "authorizationFlow", "default-authentication-flow"], - [setSearchSelect, "invalidationFlow", "default-invalidation-flow"], - ]); - + await fillOutFields(simpleLDAPProviderForm()); await ProviderWizardView.pause(); await ProviderWizardView.nextButton.click(); + await hasProviderSuccessMessage(); }); }); describe("Configure Radius Providers", () => { it("Should configure a simple Radius Provider", async () => { - const newProviderName = `New Radius Provider - ${randomId()}`; - await reachTheProvider(); await $("ak-wizard-page-type-create").waitForDisplayed(); - - // prettier-ignore - await fillOutFields([ - [setTypeCreate, "selectProviderType", "Radius Provider"], - [clickButton, "Next"], - [setTextInput, "name", newProviderName], - [setSearchSelect, "authorizationFlow", "default-authentication-flow"], - ]); - + await fillOutFields(simpleRadiusProviderForm()); await ProviderWizardView.pause(); await ProviderWizardView.nextButton.click(); + await hasProviderSuccessMessage(); + }); +}); + +describe("Configure SAML Providers", () => { + it("Should configure a simple Radius Provider", async () => { + await reachTheProvider(); + await $("ak-wizard-page-type-create").waitForDisplayed(); + await fillOutFields(simpleRadiusProviderForm()); + await ProviderWizardView.pause(); + await ProviderWizardView.nextButton.click(); + await hasProviderSuccessMessage(); }); }); diff --git a/web/tests/specs/shared-sequences.ts b/web/tests/specs/shared-sequences.ts new file mode 100644 index 0000000000..77504672ed --- /dev/null +++ b/web/tests/specs/shared-sequences.ts @@ -0,0 +1,35 @@ +import { + clickButton, + setFormGroup, + setSearchSelect, + setTextInput, + setTypeCreate, +} from "pageobjects/controls.js"; + +import { randomId } from "../utils/index.js"; + +const newObjectName = (prefix: string) => `${prefix} - ${randomId()}`; + +export const simpleOAuth2ProviderForm = () => [ + [setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"], + [clickButton, "Next"], + [setTextInput, "name", newObjectName("New Oauth2 Provider")], + [setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"], +]; + +export const simpleLDAPProviderForm = () => [ + [setTypeCreate, "selectProviderType", "LDAP Provider"], + [clickButton, "Next"], + [setTextInput, "name", newObjectName("New LDAP Provider")], + // This will never not weird me out. + [setSearchSelect, "authorizationFlow", "default-authentication-flow"], + [setFormGroup, /Flow settings/, "open"], + [setSearchSelect, "invalidationFlow", "default-invalidation-flow"], +]; + +export const simpleRadiusProviderForm = () => [ + [setTypeCreate, "selectProviderType", "Radius Provider"], + [clickButton, "Next"], + [setTextInput, "name", newObjectName("New Radius Provider")], + [setSearchSelect, "authorizationFlow", "default-authentication-flow"], +];