From 807e2a9fb0a60bec0224257afc3014cdba1ffeac Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Tue, 29 Oct 2024 15:06:32 -0700 Subject: [PATCH] web/admin: Unify the forms for providers between the ./admin/providers and ./admin/applications/wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What - For LDAP, OAuth2, Radius, SAML, SCIM, and Proxy providers, extract the literal form rendering component of each provider into a function. After all, that's what they are: they take input (the render state) and produce output (HTML with event handlers). - Rip out all of the forms in the wizard and replace them with ☝️ - Write E2E tests that exercise *all* of the components in *all* of the forms mentioned. See test results. These tests come in two flavors, "simple" (minimum amount needed to make the provider "pass" the backend's parsers) and "complete" (touches every legal field in the form according to the authentik `./schema.yml` file). As a result, every field is validated against the schema (although the schema is currently ported into the test by hand. - Fixed some serious bugginess in the way the wizard `commit` phase handles errors. ## Details ### Providers In some cases, I broke up the forms into smaller units: - Proxy, especially, with standalone units now for `renderHttpBasic`, `renderModeSelector`, `renderSettings`, and the differing modes) - SAML now has a `renderHasSigningKp` object, which makes that part of the code much more readable. I also extracted a few of static `options` collections into static const objects, so that the form object itself would be a bit more readable. ### Wizard Just ripped out all of the Provider forms. All of them. They weren't going to be needed in our glorious new future. Using the information provided by the `providerTypes` object, it was easy to extract all of the information that had once been in `ak-application-wizard-authentication-method-choice.choices`. The only thing left now is the renderers, one for each of the forms ripped out. Everything else is just gone. As a result, though, that's no longer a static list. It has to be derived from information sent via the API. So now it's in a context that's built when the wizard is initialized, and accessed by the `createTypes` pass as well as the specific provider. The error handling in the `commit` pass was just broken. I have improved it quite a bit, and now it actually displays helpful messages when things go wrong. ### Tests Wrote a simple test runner that iterates through a collection of fields, setting their values via field-type instructions contained in each line. For example, the "simple" OAuth2 Provider test looks like this: ``` export const simpleOAuth2ProviderForm: TestProvider = () => [ [setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"], [clickButton, "Next"], [setTextInput, "name", newObjectName("New Oauth2 Provider")], [setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/], ]; ``` Each control checks for the existence of the object, and in most cases its current `display`. (SearchSelect only checks existence, due to the oddness of the portaled popup.) Where a field can't reasonably be modified and still pass, we at least verify that the name provided in `schema.yml` corresponds to an existing, available control on the form or wizard panel. Combined with a routine for logging in and navigating to the Provider page, and another one to validate that a new and uniqute "Successfully Created Provider" notification appeared, this makes testing each provider a simple message of filling out the table of fields you want populated. Equally simple: these *exact same tests* can be incorporated into a wrapper for logging in, navigating to the Application page, and filling out an Application, and then a new and unique Provider for that Application, by Provider Type. As a special case, the Wizard variant checks the `TestSequence` object returned by the `TestProvider` function and removes the `name` field, since the Wizard pre-populates that automatically. As a result of this, the contents of `./web/src` has lost 1,504 lines of code. And results like these, where the behavior has been cross-checked three ways (the forms, the tests (and so the back-end), *and the schema* all agree on field names and behaviors, gives me much more confidence that the refactor works as expected: ``` [chrome 130.0.6723.70 mac #0-1] Running: chrome (v130.0.6723.70) on mac [chrome 130.0.6723.70 mac #0-1] Session ID: 039c70690eebc83ffbc2eef97043c774 [chrome 130.0.6723.70 mac #0-1] [chrome 130.0.6723.70 mac #0-1] » /tests/specs/providers.ts [chrome 130.0.6723.70 mac #0-1] Configuring Providers [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Simple LDAP provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Simple OAuth2 provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Simple Radius provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Simple SAML provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Simple SCIM provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Simple Proxy provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Simple Forward Auth (single application) provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Simple Forward Auth (domain level) provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Complete OAuth2 provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Complete LDAP provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Complete Radius provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Complete SAML provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Complete SCIM provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Complete Proxy provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Complete Forward Auth (single application) provider [chrome 130.0.6723.70 mac #0-1] ✓ Should successfully configure a Complete Forward Auth (domain level) provider [chrome 130.0.6723.70 mac #0-1] [chrome 130.0.6723.70 mac #0-1] 16 passing (1m 48.5s) ------------------------------------------------------------------ [chrome 130.0.6723.70 mac #0-2] Running: chrome (v130.0.6723.70) on mac [chrome 130.0.6723.70 mac #0-2] Session ID: 5a3ae12c851eff8fffd2686096759146 [chrome 130.0.6723.70 mac #0-2] [chrome 130.0.6723.70 mac #0-2] » /tests/specs/new-application-by-wizard.ts [chrome 130.0.6723.70 mac #0-2] Configuring Applications Via the Wizard [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Simple LDAP provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Simple OAuth2 provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Simple Radius provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Simple SAML provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Simple SCIM provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Simple Proxy provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Simple Forward Auth (single) provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Simple Forward Auth (domain) provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Complete OAuth2 provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Complete LDAP provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Complete Radius provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Complete SAML provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Complete SCIM provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Complete Proxy provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Complete Forward Auth (single) provider [chrome 130.0.6723.70 mac #0-2] ✓ Should successfully configure an application with a Complete Forward Auth (domain) provider [chrome 130.0.6723.70 mac #0-2] [chrome 130.0.6723.70 mac #0-2] 16 passing (2m 3s) ``` 🎉 --- ...rd-authentication-method-choice.choices.ts | 2 +- ...ion-wizard-authentication-method-choice.ts | 2 +- ...k-application-wizard-commit-application.ts | 34 +- ...pplication-wizard-authentication-method.ts | 4 +- ...wizard-authentication-for-reverse-proxy.ts | 6 +- ...ication-wizard-authentication-by-radius.ts | 1 - .../common/ak-crypto-certificate-search.ts | 5 +- .../admin/common/ak-flow-search/FlowSearch.ts | 2 +- .../providers/ldap/LDAPProviderFormForm.ts | 5 +- .../providers/proxy/ProxyProviderForm.ts | 4 +- .../providers/proxy/ProxyProviderFormForm.ts | 270 +++++++--------- .../radius/RadiusProviderFormForm.ts | 27 +- .../admin/providers/saml/SAMLProviderForm.ts | 1 + .../providers/saml/SAMLProviderFormForm.ts | 300 +++++++----------- .../providers/scim/SCIMProviderFormForm.ts | 107 +++---- web/tests/pageobjects/controls.ts | 247 +++++++++----- web/tests/pageobjects/page.ts | 4 +- web/tests/specs/new-application-by-wizard.ts | 37 ++- web/tests/specs/providers.ts | 42 ++- web/tests/specs/shared-sequences.ts | 93 ------ web/tests/utils/index.ts | 19 ++ 21 files changed, 567 insertions(+), 645 deletions(-) delete mode 100644 web/tests/specs/shared-sequences.ts diff --git a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts index c4b25a56c4..337787692d 100644 --- a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts +++ b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts @@ -10,7 +10,7 @@ export type LocalTypeCreate = TypeCreate & { renderer: ProviderRenderer; }; -export const providerTypeRenderers = { +export const providerTypeRenderers: Record TemplateResult> = { oauth2provider: () => html``, ldapprovider: () => diff --git a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts index f647b268d9..ef2dac6595 100644 --- a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts +++ b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts @@ -19,7 +19,7 @@ import type { LocalTypeCreate } from "./ak-application-wizard-authentication-met @customElement("ak-application-wizard-authentication-method-choice") export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSummary(BasePanel) { @consume({ context: applicationWizardProvidersContext }) - public providerModelsList: LocalTypeCreate[]; + public providerModelsList!: LocalTypeCreate[]; render() { const selectedTypes = this.providerModelsList.filter( diff --git a/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts b/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts index 1e032f2d7e..9eac8ae988 100644 --- a/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts +++ b/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts @@ -21,8 +21,10 @@ import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css"; import { type ApplicationRequest, CoreApi, + type ModelRequest, ProviderModelEnum, ProxyMode, + type ProxyProviderRequest, type TransactionApplicationRequest, type TransactionApplicationResponse, ValidationError, @@ -74,6 +76,8 @@ const successState: State = { icon: ["fa-check-circle", "pf-m-success"], }; +type StrictProviderModelEnum = Exclude; + @customElement("ak-application-wizard-commit-application") export class ApplicationWizardCommitApplication extends BasePanel { static get styles() { @@ -106,15 +110,18 @@ export class ApplicationWizardCommitApplication extends BasePanel { // Stringly-based API. Not the best, but it works. Just be aware that it is // stringly-based. - const providerModel = providerMap.get(this.wizard.providerModel); - const provider = this.wizard.provider; + + const providerModel = providerMap.get( + this.wizard.providerModel, + ) as StrictProviderModelEnum; + const provider = this.wizard.provider as ModelRequest; provider.providerModel = providerModel; - // Special case for providers. + // Special case for the Proxy provider. if (this.wizard.providerModel === "proxyprovider") { - provider.mode = this.wizard.proxyMode; - if (provider.model !== ProxyMode.ForwardDomain) { - provider.cookieDomain = ""; + (provider as ProxyProviderRequest).mode = this.wizard.proxyMode; + if ((provider as ProxyProviderRequest).mode !== ProxyMode.ForwardDomain) { + (provider as ProxyProviderRequest).cookieDomain = ""; } } @@ -132,6 +139,7 @@ export class ApplicationWizardCommitApplication extends BasePanel { data: TransactionApplicationRequest, ): Promise { this.errors = undefined; + this.commitState = idleState; new CoreApi(DEFAULT_CONFIG) .coreTransactionalApplicationsUpdate({ transactionApplicationRequest: data, @@ -144,11 +152,11 @@ export class ApplicationWizardCommitApplication extends BasePanel { }) // eslint-disable-next-line @typescript-eslint/no-explicit-any .catch(async (resolution: any) => { - const errors = await parseAPIError(resolution); + this.errors = await parseAPIError(resolution); this.dispatchWizardUpdate({ update: { ...this.wizard, - errors, + errors: this.errors, }, status: "failed", }); @@ -156,11 +164,7 @@ export class ApplicationWizardCommitApplication extends BasePanel { }); } - renderErrors(errors?: ValidationError) { - if (!errors) { - return nothing; - } - + renderErrors(errors: ValidationError) { const navTo = (step: number) => () => this.dispatchCustomEvent("ak-wizard-nav", { command: "goto", @@ -211,7 +215,9 @@ export class ApplicationWizardCommitApplication extends BasePanel { > ${this.commitState.label} - ${this.renderErrors(this.errors)} + ${this.commitState === errorState + ? this.renderErrors(this.errors ?? {}) + : nothing} diff --git a/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts b/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts index 8ec5344bb8..a9ba93fad6 100644 --- a/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts +++ b/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts @@ -3,7 +3,7 @@ import { customElement } from "@lit/reactive-element/decorators/custom-element.j import BasePanel from "../BasePanel"; import { applicationWizardProvidersContext } from "../ContextIdentity"; -import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices"; +import type { LocalTypeCreate } from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices"; import "./ldap/ak-application-wizard-authentication-by-ldap"; import "./oauth/ak-application-wizard-authentication-by-oauth"; import "./proxy/ak-application-wizard-authentication-for-reverse-proxy"; @@ -15,7 +15,7 @@ import "./scim/ak-application-wizard-authentication-by-scim"; @customElement("ak-application-wizard-authentication-method") export class ApplicationWizardApplicationDetails extends BasePanel { @consume({ context: applicationWizardProvidersContext }) - public providerModelsList: LocalTypeCreate[]; + public providerModelsList!: LocalTypeCreate[]; render() { const handler: LocalTypeCreate | undefined = this.providerModelsList.find( diff --git a/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-reverse-proxy.ts b/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-reverse-proxy.ts index 5487aa4802..1f111357de 100644 --- a/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-reverse-proxy.ts +++ b/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-reverse-proxy.ts @@ -1,5 +1,7 @@ import { ProxyModeValue, + type SetMode, + type SetShowHttpBasic, renderForm, } from "@goauthentik/admin/providers/proxy/ProxyProviderFormForm.js"; @@ -7,6 +9,8 @@ import { msg } from "@lit/localize"; import { customElement, state } from "@lit/reactive-element/decorators.js"; import { html } from "lit"; +import { ProxyMode } from "@goauthentik/api"; + import BaseProviderPanel from "../BaseProviderPanel.js"; @customElement("ak-application-wizard-authentication-for-reverse-proxy") @@ -35,7 +39,7 @@ export class AkReverseProxyApplicationWizardPage extends BaseProviderPanel { return html` ${msg("Configure Proxy Provider")}
${renderForm(this.wizard.provider ?? {}, this.wizard.errors.provider ?? [], { - mode: this.wizard.proxyMode, + mode: this.wizard.proxyMode ?? ProxyMode.Proxy, onSetMode, showHttpBasic: this.showHttpBasic, onSetShowHttpBasic, diff --git a/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts b/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts index 5df32e02d6..5b46db415b 100644 --- a/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts +++ b/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts @@ -2,7 +2,6 @@ import "@goauthentik/admin/applications/wizard/ak-wizard-title"; import "@goauthentik/admin/common/ak-crypto-certificate-search"; import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search"; import { renderForm } from "@goauthentik/admin/providers/radius/RadiusProviderFormForm.js"; -import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils"; import "@goauthentik/components/ak-text-input"; import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; import "@goauthentik/elements/forms/FormGroup"; diff --git a/web/src/admin/common/ak-crypto-certificate-search.ts b/web/src/admin/common/ak-crypto-certificate-search.ts index c222716821..45926c101e 100644 --- a/web/src/admin/common/ak-crypto-certificate-search.ts +++ b/web/src/admin/common/ak-crypto-certificate-search.ts @@ -5,8 +5,8 @@ import "@goauthentik/elements/forms/SearchSelect"; import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"; import { html } from "lit"; -import { customElement } from "lit/decorators.js"; -import { property, query } from "lit/decorators.js"; +import { customElement, property, query } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; import { CertificateKeyPair, @@ -114,6 +114,7 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement) render() { return html` extends CustomListenerElement(AKElement) .renderElement=${renderElement} .renderDescription=${renderDescription} .value=${getFlowValue} - name=${ifDefined(this.name)} + name=${ifDefined(this.name ?? undefined)} @ak-change=${this.handleSearchUpdate} ?blankable=${!this.required} > diff --git a/web/src/admin/providers/ldap/LDAPProviderFormForm.ts b/web/src/admin/providers/ldap/LDAPProviderFormForm.ts index d12cb43b05..6e436e5077 100644 --- a/web/src/admin/providers/ldap/LDAPProviderFormForm.ts +++ b/web/src/admin/providers/ldap/LDAPProviderFormForm.ts @@ -1,6 +1,7 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search"; import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search"; import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; +import "@goauthentik/components/ak-number-input"; import "@goauthentik/components/ak-radio-input"; import "@goauthentik/components/ak-text-input"; import "@goauthentik/components/ak-textarea-input"; @@ -72,7 +73,7 @@ export function renderForm( diff --git a/web/src/admin/providers/proxy/ProxyProviderForm.ts b/web/src/admin/providers/proxy/ProxyProviderForm.ts index 4cdee45371..31a2c1105b 100644 --- a/web/src/admin/providers/proxy/ProxyProviderForm.ts +++ b/web/src/admin/providers/proxy/ProxyProviderForm.ts @@ -24,8 +24,8 @@ export class ProxyProviderFormPage extends BaseProviderForm { const provider = await new ProvidersApi(DEFAULT_CONFIG).providersProxyRetrieve({ id: pk, }); - this.showHttpBasic = first(provider.basicAuthEnabled, true); - this.mode = first(provider.mode, ProxyMode.Proxy); + this.showHttpBasic = provider.basicAuthEnabled ?? true; + this.mode = provider.mode ?? ProxyMode.Proxy; return provider; } diff --git a/web/src/admin/providers/proxy/ProxyProviderFormForm.ts b/web/src/admin/providers/proxy/ProxyProviderFormForm.ts index f27ea019a4..6bc162e42e 100644 --- a/web/src/admin/providers/proxy/ProxyProviderFormForm.ts +++ b/web/src/admin/providers/proxy/ProxyProviderFormForm.ts @@ -16,7 +16,12 @@ import { msg } from "@lit/localize"; import { html, nothing } from "lit"; import { ifDefined } from "lit/directives/if-defined.js"; -import { FlowsInstancesListDesignationEnum, ProxyMode, ProxyProvider } from "@goauthentik/api"; +import { + FlowsInstancesListDesignationEnum, + ProxyMode, + ProxyProvider, + ValidationError, +} from "@goauthentik/api"; import { makeProxyPropertyMappingsSelector, @@ -24,7 +29,7 @@ import { } from "./ProxyProviderPropertyMappings.js"; export type ProxyModeValue = { value: ProxyMode }; -export type SetMode = (ev: CustomEvent) => void; +export type SetMode = (ev: CustomEvent) => void; export type SetShowHttpBasic = (ev: Event) => void; export interface ProxyModeExtraArgs { @@ -34,7 +39,7 @@ export interface ProxyModeExtraArgs { onSetShowHttpBasic: SetShowHttpBasic; } -function renderHttpBasic(provider: ProxyProvider) { +function renderHttpBasic(provider: Partial) { return html``; } -function renderProxySettings(provider: ProxyProvider) { +function renderProxySettings(provider: Partial, errors?: ValidationError) { return html`

${msg( "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.", )}

- - -

- ${msg( - "The external URL you'll access the application at. Include any non-standard port.", - )} -

-
- - -

- ${msg("Upstream host that the requests are forwarded to.")} -

-
- - -

- ${msg("Validate SSL Certificates of upstream servers.")} -

-
`; + + + + + `; } -function renderForwardSingleSettings(provider: ProxyProvider) { +function renderForwardSingleSettings(provider: Partial, errors?: ValidationError) { return html`

${msg( "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a managed outpost, this is done for you).", )}

- - -

- ${msg( - "The external URL you'll access the application at. Include any non-standard port.", - )} -

-
`; + `; } -function renderForwardDomainSettings(provider: ProxyProvider) { +function renderForwardDomainSettings(provider: Partial, errors?: ValidationError) { return html`

${msg( "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.", @@ -154,58 +140,58 @@ function renderForwardDomainSettings(provider: ProxyProvider) { "In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.", )} - - -

- ${msg( - "The external URL you'll authenticate at. The authentik core server should be reachable under this URL.", - )} -

- - - -

- ${msg( - "Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.", - )} -

-
`; + + + + `; } -function renderSettings(provider: ProxyProvider, mode: ProxyMode) { - return match(mode) +type StrictProxyMode = Omit; + +function renderSettings(provider: Partial, mode: ProxyMode) { + return match(mode as StrictProxyMode) .with(ProxyMode.Proxy, () => renderProxySettings(provider)) .with(ProxyMode.ForwardSingle, () => renderForwardSingleSettings(provider)) .with(ProxyMode.ForwardDomain, () => renderForwardDomainSettings(provider)) - .exhaustive(); + .otherwise(() => { + throw new Error("Unrecognized proxy mode"); + }); } export function renderForm( - provider?: Partial, - errors: ValidationError, + provider: Partial = {}, + errors: ValidationError = {}, args: ProxyModeExtraArgs, ) { const { mode, onSetMode, showHttpBasic, onSetShowHttpBasic } = args; return html` - - - + + ${renderModeSelector(mode, onSetMode)} - - -

${msg("Configure how long tokens are valid for.")}

- -
+ + ${msg("Advanced protocol settings")} @@ -281,51 +267,27 @@ export function renderForm( ${msg("Authentication settings")}
- - -

- ${msg( - "When enabled, authentik will intercept the Authorization header to authenticate the request.", - )} -

-
- - -

- ${msg( - "Send a custom HTTP-Basic Authentication header based on values from authentik.", - )} -

-
+ + + + + + ${showHttpBasic ? renderHttpBasic(provider) : nothing} ) => []; } -const mfaHelp = msg( +const mfaSupportHelp = msg( "When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.", ); @@ -85,22 +87,13 @@ export function renderForm(

${msg("Flow used for users to authenticate.")}

- - -

${mfaHelp}

-
+ + ${msg("Protocol settings")} diff --git a/web/src/admin/providers/saml/SAMLProviderForm.ts b/web/src/admin/providers/saml/SAMLProviderForm.ts index f54ead0c90..72525b2128 100644 --- a/web/src/admin/providers/saml/SAMLProviderForm.ts +++ b/web/src/admin/providers/saml/SAMLProviderForm.ts @@ -1,3 +1,4 @@ +import { type AkCryptoCertificateSearch } from "@goauthentik/admin/common/ak-crypto-certificate-search"; import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; diff --git a/web/src/admin/providers/saml/SAMLProviderFormForm.ts b/web/src/admin/providers/saml/SAMLProviderFormForm.ts index 760ddf24e8..ffa4ebc248 100644 --- a/web/src/admin/providers/saml/SAMLProviderFormForm.ts +++ b/web/src/admin/providers/saml/SAMLProviderFormForm.ts @@ -5,7 +5,6 @@ import { 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"; @@ -51,29 +50,59 @@ export function makeSAMLPropertyMappingsSelector(instanceMappings?: string[]) { mapping?.managed?.startsWith("goauthentik.io/providers/saml"); } +const serviceProviderBindingOptions = [ + { + label: msg("Redirect"), + value: SpBindingEnum.Redirect, + default: true, + }, + { + label: msg("Post"), + value: SpBindingEnum.Post, + }, +]; + +function renderHasSigningKp(provider?: Partial) { + return html` + + + + `; +} + export function renderForm( - provider?: Partial, + provider: Partial = {}, errors: ValidationError, setHasSigningKp: (ev: InputEvent) => void, hasSigningKp: boolean, ) { - return html` - - + return html`

${msg("Flow used when authorizing this provider.")} @@ -83,56 +112,38 @@ export function renderForm( ${msg("Protocol settings")}

- - - - - -

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

-
- + + - - -

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

-
- - - + +
@@ -186,48 +197,8 @@ export function renderForm( )}

- ${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} + ${hasSigningKp ? renderHasSigningKp(provider) : nothing} + - - -

- ${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.", - )} -

- -
+ label=${msg("Assertion valid not before")} + value="${provider?.assertionValidNotBefore || "minutes=-5"}" + required + .errorMessages=${errors?.assertionValidNotBefore ?? []} + help=${msg("Configure the maximum allowed time drift for an assertion.")} + > - + + + + + + - - - - + + - - - +
`; } diff --git a/web/src/admin/providers/scim/SCIMProviderFormForm.ts b/web/src/admin/providers/scim/SCIMProviderFormForm.ts index f6ea6bb434..229406ee69 100644 --- a/web/src/admin/providers/scim/SCIMProviderFormForm.ts +++ b/web/src/admin/providers/scim/SCIMProviderFormForm.ts @@ -18,6 +18,7 @@ import { PropertymappingsApi, SCIMMapping, SCIMProvider, + ValidationError, } from "@goauthentik/api"; export async function scimPropertyMappingsProvider(page = 1, search = "") { @@ -48,79 +49,55 @@ export function makeSCIMPropertyMappingsSelector( export function renderForm(provider?: Partial, errors: ValidationError = {}) { return html` - - - - + ${msg("Protocol settings")}
- - -

- ${msg("SCIM base url, usually ends in /v2.")} -

-
- - - - - -

- ${msg( - "Token to authenticate with. Currently only bearer authentication is supported.", - )} -

-
+ + + + + +
${msg("User filtering")}
- - - + + + => { diff --git a/web/tests/pageobjects/controls.ts b/web/tests/pageobjects/controls.ts index df70e00f54..50ac159ac6 100644 --- a/web/tests/pageobjects/controls.ts +++ b/web/tests/pageobjects/controls.ts @@ -4,90 +4,65 @@ import { Key } from "webdriverio"; export async function doBlur(el: WebdriverIO.Element | ChainablePromiseElement) { const element = await el; - browser.execute((element) => element.blur()); + browser.execute((element) => element.blur(), element); } -export async function setSearchSelect(name: string, value: string) { - const control = await (async () => { - try { - const control = await $(`ak-search-select[name="${name}"]`); - await control.waitForExist({ timeout: 500 }); - return control; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars - } catch (_e: any) { - const control = await $(`ak-search-selects-ez[name="${name}"]`); - return control; - } - })(); +export function tap(a: A) { + console.log("TAP:", a); + return a; +} - // Find the search select input control and activate it. - const view = await control.$("ak-search-select-view"); - const input = await view.$('input[type="text"]'); - await input.scrollIntoView(); - await input.click(); +const makeComparator = (value: string | RegExp) => + typeof value === "string" + ? (sample: string) => sample === value + : (sample: string) => value.test(sample); - // @ts-expect-error "Types break on shadow$$" +export async function checkIsPresent(name: string) { + await expect(await $(name)).toBeDisplayed(); +} + +export async function clickButton(name: string, ctx?: WebdriverIO.Element) { + const context = ctx ?? browser; const button = await (async () => { - for await (const button of $(`div[data-managed-for*="${name}"]`) - .$("ak-list-select") - .$$("button")) { - if ((await button.getText()).includes(value)) { + for await (const button of context.$$("button")) { + if ((await button.isDisplayed()) && (await button.getText()).indexOf(name) !== -1) { return button; } } })(); - // @ts-expect-error "TSC cannot tell if the `for` loop actually performs the assignment." - if (!button.isExisting()) { - throw new Error(`Expected to find an entry matching the spec ${value}`); + if (!(button && (await button.isDisplayed()))) { + throw new Error(`Unable to find button '${name}'`); } - await (await button).click(); - await browser.keys(Key.Tab); - await doBlur(control); + + await button.scrollIntoView(); + await button.click(); + await doBlur(button); } -export async function setTextInput(name: string, value: string) { - const control = await $(`input[name="${name}"]`); - await control.scrollIntoView(); - await control.setValue(value); - await doBlur(control); -} - -export async function setRadio(name: string, value: string) { - const control = await $(`ak-radio[name="${name}"]`); - await control.scrollIntoView(); - const item = await control.$(`label.*=${value}`).parentElement(); - await item.scrollIntoView(); - await item.click(); - await doBlur(control); -} - -export async function setTypeCreate(name: string, value: string | RegExp) { - const control = await $(`ak-wizard-page-type-create[name="${name}"]`); - await control.scrollIntoView(); - - const comparator = - typeof value === "string" ? (sample) => sample === value : (sample) => value.test(sample); - - const card = await (async () => { - for await (const card of $("ak-wizard-page-type-create").$$( - '[data-ouid-component-type="ak-type-create-grid-card"]', +export async function clickToggleGroup(name: string, value: string | RegExp) { + const comparator = makeComparator(value); + const button = await (async () => { + for await (const button of $(`[data-ouid-component-name=${name}]`).$$( + ".pf-c-toggle-group__button", )) { - if (comparator(await card.$(".pf-c-card__title").getText())) { - return card; + if (comparator(await button.$(".pf-c-toggle-group__text").getText())) { + return button; } } })(); - await card.scrollIntoView(); - await card.click(); - await doBlur(control); + if (!(button && (await button?.isDisplayed()))) { + throw new Error(`Unable to locate toggle button ${name}:${value.toString()}`); + } + + await button.scrollIntoView(); + await button.click(); + await doBlur(button); } export async function setFormGroup(name: string | RegExp, setting: "open" | "closed") { - const comparator = - typeof name === "string" ? (sample) => sample === name : (sample) => name.test(sample); - + const comparator = makeComparator(name); const formGroup = await (async () => { for await (const group of browser.$$("ak-form-group")) { // Delightfully, wizards may have slotted elements that *exist* but are not *attached*, @@ -103,6 +78,10 @@ export async function setFormGroup(name: string | RegExp, setting: "open" | "clo } })(); + if (!(formGroup && (await formGroup.isDisplayed()))) { + throw new Error(`Unable to find ak-form-group[name="${name}"]`); + } + await formGroup.scrollIntoView(); const toggle = await formGroup.$("div.pf-c-form__field-group-toggle-button button"); await match([await toggle.getAttribute("aria-expanded"), setting]) @@ -112,35 +91,135 @@ export async function setFormGroup(name: string | RegExp, setting: "open" | "clo await doBlur(formGroup); } -export async function clickButton(name: string, ctx?: WebdriverIO.Element) { - const context = ctx ?? browser; - const buttons = await context.$$("button"); - let button: WebdriverIO.Element; - for (const b of buttons) { - if (b.isDisplayed() && (await b.getText()).indexOf(name) !== -1) { - button = b; - break; +export async function setRadio(name: string, value: string | RegExp) { + const control = await $(`ak-radio[name="${name}"]`); + await control.scrollIntoView(); + + const comparator = makeComparator(value); + const item = await (async () => { + for await (const item of control.$$("div.pf-c-radio")) { + if (comparator(await item.$(".pf-c-radio__label").getText())) { + return item; + } } + })(); + + if (!(item && (await item.isDisplayed()))) { + throw new Error(`Unable to find a radio that matches ${name}:${value.toString()}`); } - await button.scrollIntoView(); - await button.click(); - await doBlur(button); + + await item.scrollIntoView(); + await item.click(); + await doBlur(control); } -export async function clickToggleGroup(name: string, value: string | RegExp) { - const comparator = - typeof name === "string" ? (sample) => sample === value : (sample) => value.test(sample); +export async function setSearchSelect(name: string, value: string | RegExp) { + const control = await (async () => { + try { + const control = await $(`ak-search-select[name="${name}"]`); + await control.waitForExist({ timeout: 500 }); + return control; + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars + } catch (_e: any) { + const control = await $(`ak-search-selects-ez[name="${name}"]`); + return control; + } + })(); + if (!(control && (await control.isExisting()))) { + throw new Error(`Unable to find an ak-search-select variant matching ${name}}`); + } + + // Find the search select input control and activate it. + const view = await control.$("ak-search-select-view"); + const input = await view.$('input[type="text"]'); + await input.scrollIntoView(); + await input.click(); + + const comparator = makeComparator(value); const button = await (async () => { - for await (const button of $(`[data-ouid-component-name=${name}]`).$$( - ".pf-c-toggle-group__button", - )) { - if (comparator(await button.$(".pf-c-toggle-group__text").getText())) { + for await (const button of $(`div[data-managed-for*="${name}"]`) + .$("ak-list-select") + .$$("button")) { + if (comparator(await button.getText())) { return button; } } })(); - await button.scrollIntoView(); - await button.click(); - await doBlur(button); + + if (!(button && (await button.isDisplayed()))) { + throw new Error( + `Unable to find an ak-search-select entry matching ${name}:${value.toString()}`, + ); + } + + await (await button).click(); + await browser.keys(Key.Tab); + await doBlur(control); } + +export async function setTextInput(name: string, value: string) { + const control = await $(`input[name="${name}"]`); + await control.scrollIntoView(); + await control.setValue(value); + await doBlur(control); +} + +export async function setTextareaInput(name: string, value: string) { + const control = await $(`textarea[name="${name}"]`); + await control.scrollIntoView(); + await control.setValue(value); + await doBlur(control); +} + +export async function setToggle(name: string, set: boolean) { + const toggle = await $(`input[name="${name}"]`); + await toggle.scrollIntoView(); + await expect(await toggle.getAttribute("type")).toBe("checkbox"); + const state = await toggle.isSelected(); + if (set !== state) { + const control = await (await toggle.parentElement()).$(".pf-c-switch__toggle"); + await control.click(); + await doBlur(control); + } +} + +export async function setTypeCreate(name: string, value: string | RegExp) { + const control = await $(`ak-wizard-page-type-create[name="${name}"]`); + await control.scrollIntoView(); + + const comparator = makeComparator(value); + const card = await (async () => { + for await (const card of $("ak-wizard-page-type-create").$$( + '[data-ouid-component-type="ak-type-create-grid-card"]', + )) { + if (comparator(await card.$(".pf-c-card__title").getText())) { + return card; + } + } + })(); + + if (!(card && (await card.isDisplayed()))) { + throw new Error(`Unable to locate radio card ${name}:${value.toString()}`); + } + + await card.scrollIntoView(); + await card.click(); + await doBlur(control); +} + +export type TestInteraction = + | [typeof checkIsPresent, ...Parameters] + | [typeof clickButton, ...Parameters] + | [typeof clickToggleGroup, ...Parameters] + | [typeof setFormGroup, ...Parameters] + | [typeof setRadio, ...Parameters] + | [typeof setSearchSelect, ...Parameters] + | [typeof setTextInput, ...Parameters] + | [typeof setTextareaInput, ...Parameters] + | [typeof setToggle, ...Parameters] + | [typeof setTypeCreate, ...Parameters]; + +export type TestSequence = TestInteraction[]; + +export type TestProvider = () => TestSequence; diff --git a/web/tests/pageobjects/page.ts b/web/tests/pageobjects/page.ts index ae225ce06c..63a26cd6c4 100644 --- a/web/tests/pageobjects/page.ts +++ b/web/tests/pageobjects/page.ts @@ -80,6 +80,7 @@ export default class Page { await $(`div[data-managed-for="${name}"]`).$("ak-list-select") ).shadow$$("button"); + let target: WebdriverIO.Element; // @ts-expect-error "Types break on shadow$$" for (const button of searchBlock) { if ((await button.getText()).includes(value)) { @@ -91,6 +92,7 @@ export default class Page { if (!target) { throw new Error(`Expected to find an entry matching the spec ${value}`); } + await (await target).click(); await browser.keys(Key.Tab); } @@ -121,7 +123,7 @@ export default class Page { const formGroup = await $(`ak-form-group span[slot="header"].*=${name}`).parentElement(); await formGroup.scrollIntoView(); const toggle = await formGroup.$("div.pf-c-form__field-group-toggle-button button"); - await match([toggle.getAttribute("expanded"), setting]) + await match([await toggle.getAttribute("expanded"), setting]) .with(["false", "open"], async () => await toggle.click()) .with(["true", "closed"], async () => await toggle.click()) .otherwise(async () => {}); diff --git a/web/tests/specs/new-application-by-wizard.ts b/web/tests/specs/new-application-by-wizard.ts index c636fda21f..50c3e5fd16 100644 --- a/web/tests/specs/new-application-by-wizard.ts +++ b/web/tests/specs/new-application-by-wizard.ts @@ -9,8 +9,15 @@ import ApplicationWizardView from "../pageobjects/application-wizard.page.js"; import ApplicationsListPage from "../pageobjects/applications-list.page.js"; import { randomId } from "../utils/index.js"; import { login } from "../utils/login.js"; -import { type TestSequence } from "./shared-sequences"; import { + completeForwardAuthDomainProxyProviderForm, + completeForwardAuthProxyProviderForm, + completeLDAPProviderForm, + completeOAuth2ProviderForm, + completeProxyProviderForm, + completeRadiusProviderForm, + completeSAMLProviderForm, + completeSCIMProviderForm, simpleForwardAuthDomainProxyProviderForm, simpleForwardAuthProxyProviderForm, simpleLDAPProviderForm, @@ -19,7 +26,8 @@ import { simpleRadiusProviderForm, simpleSAMLProviderForm, simpleSCIMProviderForm, -} from "./shared-sequences.js"; +} from "./provider-shared-sequences.js"; +import { type TestSequence } from "./shared-sequences"; const SUCCESS_MESSAGE = "Your application has been saved"; @@ -62,7 +70,6 @@ async function fillOutTheProviderAndCommit(provider: TestSequence) { console.log(`Running ${args.join(", ")}`); // @ts-expect-error "This is a pretty alien call; I'm not surprised Typescript hates it." await thefunc.apply($, args); - await browser.pause(1000); } await $("ak-wizard-frame").$("footer button.pf-m-primary").click(); @@ -79,14 +86,22 @@ async function itShouldConfigureApplicationsViaTheWizard(name: string, provider: } const providers = [ - ["LDAP", simpleLDAPProviderForm], - ["OAuth2", simpleOAuth2ProviderForm], - ["Radius", simpleRadiusProviderForm], - ["SAML", simpleSAMLProviderForm], - ["SCIM", simpleSCIMProviderForm], - ["Proxy", simpleProxyProviderForm], - ["Forward Auth (single application)", simpleForwardAuthProxyProviderForm], - ["Forward Auth (domain level)", simpleForwardAuthDomainProxyProviderForm], + ["Simple LDAP", simpleLDAPProviderForm], + ["Simple OAuth2", simpleOAuth2ProviderForm], + ["Simple Radius", simpleRadiusProviderForm], + ["Simple SAML", simpleSAMLProviderForm], + ["Simple SCIM", simpleSCIMProviderForm], + ["Simple Proxy", simpleProxyProviderForm], + ["Simple Forward Auth (single)", simpleForwardAuthProxyProviderForm], + ["Simple Forward Auth (domain)", simpleForwardAuthDomainProxyProviderForm], + ["Complete OAuth2", completeOAuth2ProviderForm], + ["Complete LDAP", completeLDAPProviderForm], + ["Complete Radius", completeRadiusProviderForm], + ["Complete SAML", completeSAMLProviderForm], + ["Complete SCIM", completeSCIMProviderForm], + ["Complete Proxy", completeProxyProviderForm], + ["Complete Forward Auth (single)", completeForwardAuthProxyProviderForm], + ["Complete Forward Auth (domain)", completeForwardAuthDomainProxyProviderForm], ]; describe("Configuring Applications Via the Wizard", () => { diff --git a/web/tests/specs/providers.ts b/web/tests/specs/providers.ts index 2e6299cd0c..5d25ed198d 100644 --- a/web/tests/specs/providers.ts +++ b/web/tests/specs/providers.ts @@ -1,10 +1,18 @@ import { expect } from "@wdio/globals"; +import { type TestProvider, type TestSequence } from "../pageobjects/controls"; import ProviderWizardView from "../pageobjects/provider-wizard.page.js"; import ProvidersListPage from "../pageobjects/providers-list.page.js"; import { login } from "../utils/login.js"; -import { type TestSequence } from "./shared-sequences"; import { + completeForwardAuthDomainProxyProviderForm, + completeForwardAuthProxyProviderForm, + completeLDAPProviderForm, + completeOAuth2ProviderForm, + completeProxyProviderForm, + completeRadiusProviderForm, + completeSAMLProviderForm, + completeSCIMProviderForm, simpleForwardAuthDomainProxyProviderForm, simpleForwardAuthProxyProviderForm, simpleLDAPProviderForm, @@ -13,7 +21,7 @@ import { simpleRadiusProviderForm, simpleSAMLProviderForm, simpleSCIMProviderForm, -} from "./shared-sequences.js"; +} from "./provider-shared-sequences.js"; async function reachTheProvider() { await ProvidersListPage.logout(); @@ -46,7 +54,7 @@ async function fillOutFields(fields: TestSequence) { for (const field of fields) { const thefunc = field[0]; const args = field.slice(1); - // @ts-expect-error "This is a pretty alien call; I'm not surprised Typescript hates it." + // @ts-expect-error "This is a pretty alien call, so I'm not surprised Typescript doesn't like it." await thefunc.apply($, args); } } @@ -62,16 +70,26 @@ async function itShouldConfigureASimpleProvider(name: string, provider: TestSequ }); } +type ProviderTest = [string, TestProvider]; + describe("Configuring Providers", () => { - const providers = [ - ["LDAP", simpleLDAPProviderForm], - ["OAuth2", simpleOAuth2ProviderForm], - ["Radius", simpleRadiusProviderForm], - ["SAML", simpleSAMLProviderForm], - ["SCIM", simpleSCIMProviderForm], - ["Proxy", simpleProxyProviderForm], - ["Forward Auth (single application)", simpleForwardAuthProxyProviderForm], - ["Forward Auth (domain level)", simpleForwardAuthDomainProxyProviderForm], + const providers: ProviderTest[] = [ + ["Simple LDAP", simpleLDAPProviderForm], + ["Simple OAuth2", simpleOAuth2ProviderForm], + ["Simple Radius", simpleRadiusProviderForm], + ["Simple SAML", simpleSAMLProviderForm], + ["Simple SCIM", simpleSCIMProviderForm], + ["Simple Proxy", simpleProxyProviderForm], + ["Simple Forward Auth (single application)", simpleForwardAuthProxyProviderForm], + ["Simple Forward Auth (domain level)", simpleForwardAuthDomainProxyProviderForm], + ["Complete OAuth2", completeOAuth2ProviderForm], + ["Complete LDAP", completeLDAPProviderForm], + ["Complete Radius", completeRadiusProviderForm], + ["Complete SAML", completeSAMLProviderForm], + ["Complete SCIM", completeSCIMProviderForm], + ["Complete Proxy", completeProxyProviderForm], + ["Complete Forward Auth (single application)", completeForwardAuthProxyProviderForm], + ["Complete Forward Auth (domain level)", completeForwardAuthDomainProxyProviderForm], ]; for (const [name, provider] of providers) { diff --git a/web/tests/specs/shared-sequences.ts b/web/tests/specs/shared-sequences.ts deleted file mode 100644 index 5dd8be8c4b..0000000000 --- a/web/tests/specs/shared-sequences.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - clickButton, - clickToggleGroup, - setFormGroup, - setSearchSelect, - setTextInput, - setTypeCreate, -} from "pageobjects/controls.js"; - -import { randomId } from "../utils/index.js"; - -const newObjectName = (prefix: string) => `${prefix} - ${randomId()}`; - -export type TestInteraction = - | [typeof clickButton, ...Parameters] - | [typeof clickToggleGroup, ...Parameters] - | [typeof setFormGroup, ...Parameters] - | [typeof setSearchSelect, ...Parameters] - | [typeof setTextInput, ...Parameters] - | [typeof setTypeCreate, ...Parameters]; - -export type TestSequence = TestInteraction[]; - -export type TestProvider = () => TestSequence; - -export const simpleOAuth2ProviderForm: TestProvider = () => [ - [setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"], - [clickButton, "Next"], - [setTextInput, "name", newObjectName("New Oauth2 Provider")], - [setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"], -]; - -export const simpleLDAPProviderForm: TestProvider = () => [ - [setTypeCreate, "selectProviderType", "LDAP Provider"], - [clickButton, "Next"], - [setTextInput, "name", newObjectName("New LDAP Provider")], - // This will never not weird me out. - [setFormGroup, /Flow settings/, "open"], - [setSearchSelect, "authorizationFlow", "default-authentication-flow"], - [setSearchSelect, "invalidationFlow", "default-invalidation-flow"], -]; - -export const simpleRadiusProviderForm: TestProvider = () => [ - [setTypeCreate, "selectProviderType", "Radius Provider"], - [clickButton, "Next"], - [setTextInput, "name", newObjectName("New Radius Provider")], - [setSearchSelect, "authorizationFlow", "default-authentication-flow"], -]; - -export const simpleSAMLProviderForm: TestProvider = () => [ - [setTypeCreate, "selectProviderType", "SAML Provider"], - [clickButton, "Next"], - [setTextInput, "name", newObjectName("New SAML Provider")], - [setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"], - [setTextInput, "acsUrl", "http://example.com:8000/"], -]; - -export const simpleSCIMProviderForm: TestProvider = () => [ - [setTypeCreate, "selectProviderType", "SCIM Provider"], - [clickButton, "Next"], - [setTextInput, "name", newObjectName("New SCIM Provider")], - [setTextInput, "url", "http://example.com:8000/"], - [setTextInput, "token", "insert-real-token-here"], -]; - -export const simpleProxyProviderForm: TestProvider = () => [ - [setTypeCreate, "selectProviderType", "Proxy Provider"], - [clickButton, "Next"], - [setTextInput, "name", newObjectName("New Proxy Provider")], - [setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"], - [clickToggleGroup, "proxy-type-toggle", "Proxy"], - [setTextInput, "externalHost", "http://example.com:8000/"], - [setTextInput, "internalHost", "http://example.com:8001/"], -]; - -export const simpleForwardAuthProxyProviderForm: TestProvider = () => [ - [setTypeCreate, "selectProviderType", "Proxy Provider"], - [clickButton, "Next"], - [setTextInput, "name", newObjectName("New Forward Auth Provider")], - [setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"], - [clickToggleGroup, "proxy-type-toggle", "Forward auth (single application)"], - [setTextInput, "externalHost", "http://example.com:8000/"], -]; - -export const simpleForwardAuthDomainProxyProviderForm: TestProvider = () => [ - [setTypeCreate, "selectProviderType", "Proxy Provider"], - [clickButton, "Next"], - [setTextInput, "name", newObjectName("New Forward Auth Domain Level Provider")], - [setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"], - [clickToggleGroup, "proxy-type-toggle", "Forward auth (domain level)"], - [setTextInput, "externalHost", "http://example.com:8000/"], - [setTextInput, "cookieDomain", "somedomain.tld"], -]; diff --git a/web/tests/utils/index.ts b/web/tests/utils/index.ts index 73e10a876d..edd10f3127 100644 --- a/web/tests/utils/index.ts +++ b/web/tests/utils/index.ts @@ -1,3 +1,22 @@ +// Taken from python's string module +export const ascii_lowercase = "abcdefghijklmnopqrstuvwxyz"; +export const ascii_uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +export const ascii_letters = ascii_lowercase + ascii_uppercase; +export const digits = "0123456789"; +export const hexdigits = digits + "abcdef" + "ABCDEF"; +export const octdigits = "01234567"; +export const punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; + +export function randomString(len: number, charset: string): string { + const chars = []; + const array = new Uint8Array(len); + globalThis.crypto.getRandomValues(array); + for (let index = 0; index < len; index++) { + chars.push(charset[Math.floor(charset.length * (array[index] / Math.pow(2, 8)))]); + } + return chars.join(""); +} + export function randomId() { let dt = new Date().getTime(); return "xxxxxxxx".replace(/x/g, (c) => {