web/admin: Unify the forms for providers between the ./admin/providers and ./admin/applications/wizard

## 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)
```

🎉
This commit is contained in:
Ken Sternberg
2024-10-29 15:06:32 -07:00
parent 5bd7cedaba
commit 807e2a9fb0
21 changed files with 567 additions and 645 deletions

View File

@ -10,7 +10,7 @@ export type LocalTypeCreate = TypeCreate & {
renderer: ProviderRenderer;
};
export const providerTypeRenderers = {
export const providerTypeRenderers: Record<string, () => TemplateResult> = {
oauth2provider: () =>
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
ldapprovider: () =>

View File

@ -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(

View File

@ -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<ProviderModelEnum, "11184809">;
@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<TransactionApplicationResponse | void> {
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}
</h1>
${this.renderErrors(this.errors)}
${this.commitState === errorState
? this.renderErrors(this.errors ?? {})
: nothing}
</div>
</div>
</div>

View File

@ -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(

View File

@ -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` <ak-wizard-title>${msg("Configure Proxy Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
${renderForm(this.wizard.provider ?? {}, this.wizard.errors.provider ?? [], {
mode: this.wizard.proxyMode,
mode: this.wizard.proxyMode ?? ProxyMode.Proxy,
onSetMode,
showHttpBasic: this.showHttpBasic,
onSetShowHttpBasic,

View File

@ -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";

View File

@ -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`
<ak-search-select
name=${ifDefined(this.name ?? undefined)}
.fetchObjects=${this.fetchObjects}
.renderElement=${renderElement}
.value=${renderValue}

View File

@ -134,7 +134,7 @@ export class FlowSearch<T extends Flow> 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}
>

View File

@ -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(
</ak-radio-input>
<ak-switch-input
name="openInNewTab"
name="mfaSupport"
label=${msg("Code-based MFA Support")}
?checked=${provider?.mfaSupport ?? true}
help=${mfaSupportHelp}
@ -108,7 +109,7 @@ export function renderForm(
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
.brandFlow=${brand.flowInvalidation}
.brandFlow=${brand?.flowInvalidation}
.errorMessages=${errors?.invalidationFlow ?? []}
required
></ak-branded-flow-search>

View File

@ -24,8 +24,8 @@ export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
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;
}

View File

@ -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<ProvxyModeValue>) => void;
export type SetMode = (ev: CustomEvent<ProxyModeValue>) => 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<ProxyProvider>) {
return html`<ak-text-input
name="basicAuthUserAttribute"
label=${msg("HTTP-Basic Username Key")}
@ -67,78 +72,59 @@ function renderModeSelector(mode: ProxyMode, onSet: SetMode) {
</ak-toggle-group>`;
}
function renderProxySettings(provider: ProxyProvider) {
function renderProxySettings(provider: Partial<ProxyProvider>, errors?: ValidationError) {
return html`<p class="pf-u-mb-xl">
${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.",
)}
</p>
<ak-form-element-horizontal label=${msg("External host")} required name="externalHost">
<input
type="text"
value="${ifDefined(provider?.externalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Internal host")} required name="internalHost">
<input
type="text"
value="${ifDefined(provider?.internalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Upstream host that the requests are forwarded to.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="internalHostSslValidation">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${provider?.internalHostSslValidation ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Internal host SSL Validation")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg("Validate SSL Certificates of upstream servers.")}
</p>
</ak-form-element-horizontal>`;
<ak-text-input
name="externalHost"
label=${msg("External host")}
value="${ifDefined(provider?.externalHost)}"
required
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
></ak-text-input>
<ak-text-input
name="internalHost"
label=${msg("Internal host")}
value="${ifDefined(provider?.internalHost)}"
required
.errorMessages=${errors?.internalHost ?? []}
help=${msg("Upstream host that the requests are forwarded to.")}
></ak-text-input>
<ak-switch-input
name="internalHostSslValidation"
label=${msg("Internal host SSL Validation")}
?checked=${provider?.internalHostSslValidation ?? true}
help=${msg("Validate SSL Certificates of upstream servers.")}
>
</ak-switch-input>`;
}
function renderForwardSingleSettings(provider: ProxyProvider) {
function renderForwardSingleSettings(provider: Partial<ProxyProvider>, errors?: ValidationError) {
return html`<p class="pf-u-mb-xl">
${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).",
)}
</p>
<ak-form-element-horizontal label=${msg("External host")} required name="externalHost">
<input
type="text"
value="${ifDefined(provider?.externalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
</p>
</ak-form-element-horizontal>`;
<ak-text-input
name="externalHost"
label=${msg("External host")}
value="${ifDefined(provider?.externalHost)}"
required
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
></ak-text-input>`;
}
function renderForwardDomainSettings(provider: ProxyProvider) {
function renderForwardDomainSettings(provider: Partial<ProxyProvider>, errors?: ValidationError) {
return html`<p class="pf-u-mb-xl">
${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.",
)}
</div>
<ak-form-element-horizontal label=${msg("Authentication URL")} required name="externalHost">
<input
type="text"
value="${provider?.externalHost ?? window.location.origin}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll authenticate at. The authentik core server should be reachable under this URL.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Cookie domain")} name="cookieDomain" required>
<input
type="text"
value="${ifDefined(provider?.cookieDomain)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${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'.",
)}
</p>
</ak-form-element-horizontal>`;
<ak-text-input
name="externalHost"
label=${msg("Authentication URL")}
value="${provider?.externalHost ?? window.location.origin}"
required
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll authenticate at. The authentik core server should be reachable under this URL.",
)}
></ak-text-input>
<ak-text-input
label=${msg("Cookie domain")}
name="cookieDomain"
value="${ifDefined(provider?.cookieDomain)}"
required
.errorMessages=${errors?.cookieDomain ?? []}
help=${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'.",
)}
></ak-text-input> `;
}
function renderSettings(provider: ProxyProvider, mode: ProxyMode) {
return match(mode)
type StrictProxyMode = Omit<ProxyMode, "11184809">;
function renderSettings(provider: Partial<ProxyProvider>, 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<ProxyProvider>,
errors: ValidationError,
provider: Partial<ProxyProvider> = {},
errors: ValidationError = {},
args: ProxyModeExtraArgs,
) {
const { mode, onSetMode, showHttpBasic, onSetShowHttpBasic } = args;
return html`
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(provider?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-text-input
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name ?? []}
required
></ak-text-input>
<ak-form-element-horizontal
label=${msg("Authorization flow")}
required
@ -225,15 +211,15 @@ export function renderForm(
<div class="pf-c-card__body">${renderModeSelector(mode, onSetMode)}</div>
<div class="pf-c-card__footer">${renderSettings(provider, mode)}</div>
</div>
<ak-form-element-horizontal label=${msg("Token validity")} name="accessTokenValidity">
<input
type="text"
value="${provider?.accessTokenValidity ?? "hours=24"}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">${msg("Configure how long tokens are valid for.")}</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-text-input
label=${msg("Token validity")}
name="accessTokenValidity"
value="${provider?.accessTokenValidity ?? "hours=24"}"
.errorMessages=${errors?.accessTokenValidity ?? []}
required
.help=${msg("Configure how long tokens are valid for.")}
></ak-text-input>
<ak-form-group>
<span slot="header">${msg("Advanced protocol settings")}</span>
@ -281,51 +267,27 @@ export function renderForm(
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="interceptHeaderAuth">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${provider?.interceptHeaderAuth ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Intercept header authentication")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="basicAuthEnabled">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${provider?.basicAuthEnabled ?? false}
@change=${onSetShowHttpBasic}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Send HTTP-Basic Authentication")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
)}
</p>
</ak-form-element-horizontal>
<ak-switch-input
name="interceptHeaderAuth"
label=${msg("Intercept header authentication")}
?checked=${provider?.interceptHeaderAuth ?? true}
help=${msg(
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
)}
>
</ak-switch-input>
<ak-switch-input
name="basicAuthEnabled"
label=${msg("Send HTTP-Basic Authentication")}
?checked=${provider?.basicAuthEnabled ?? false}
help=${msg(
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
)}
@change=${onSetShowHttpBasic}
>
</ak-switch-input>
${showHttpBasic ? renderHttpBasic(provider) : nothing}
<ak-form-element-horizontal label=${msg("Trusted OIDC Sources")} name="jwksSources">
<ak-dual-select-dynamic-selected

View File

@ -12,8 +12,10 @@ import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CurrentBrand,
FlowsInstancesListDesignationEnum,
PropertymappingsApi,
RadiusProvider,
RadiusProviderPropertyMapping,
ValidationError,
} from "@goauthentik/api";
@ -40,7 +42,7 @@ export function makeRadiusPropertyMappingsSelector(instanceMappings?: string[])
: ([_0, _1, _2, _]: DualSelectPair<RadiusProviderPropertyMapping>) => [];
}
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(
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="mfaSupport">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${provider?.mfaSupport ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Code-based MFA Support")}</span>
</label>
<p class="pf-c-form__helper-text">${mfaHelp}</p>
</ak-form-element-horizontal>
<ak-switch-input
name="mfaSupport"
label=${msg("Code-based MFA Support")}
?checked=${provider?.mfaSupport ?? true}
help=${mfaSupportHelp}
>
</ak-switch-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>

View File

@ -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";

View File

@ -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<SAMLProvider>) {
return html` <ak-switch-input
name="signAssertion"
label=${msg("Sign assertions")}
?checked=${provider?.signAssertion ?? true}
help=${msg("When enabled, the assertion element of the SAML response will be signed.")}
>
</ak-switch-input>
<ak-switch-input
name="signResponse"
label=${msg("Sign responses")}
?checked=${provider?.signResponse ?? false}
help=${msg("When enabled, the assertion element of the SAML response will be signed.")}
>
</ak-switch-input>`;
}
export function renderForm(
provider?: Partial<SAMLProvider>,
provider: Partial<SAMLProvider> = {},
errors: ValidationError,
setHasSigningKp: (ev: InputEvent) => void,
hasSigningKp: boolean,
) {
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(provider?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
return html` <ak-text-input
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
required
.errorMessages=${errors?.name ?? []}
></ak-text-input>
<ak-form-element-horizontal
name="authorizationFlow"
label=${msg("Authorization flow")}
required
name="authorizationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow}
required
.errorMessages=${errors?.authorizationFlow ?? []}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
@ -83,56 +112,38 @@ export function renderForm(
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("ACS URL")} ?required=${true} name="acsUrl">
<input
type="text"
value="${ifDefined(provider?.acsUrl)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Issuer")} ?required=${true} name="issuer">
<input
type="text"
value="${provider?.issuer || "authentik"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">${msg("Also known as EntityID.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
<ak-text-input
name="acsUrl"
label=${msg("ACS URL")}
value="${ifDefined(provider?.acsUrl)}"
required
.errorMessages=${errors?.acsUrl ?? []}
></ak-text-input>
<ak-text-input
label=${msg("Issuer")}
name="issuer"
value="${provider?.issuer || "authentik"}"
required
.errorMessages=${errors?.issuer ?? []}
help=${msg("Also known as EntityID.")}
></ak-text-input>
<ak-radio-input
label=${msg("Service Provider Binding")}
?required=${true}
name="spBinding"
required
.options=${serviceProviderBindingOptions}
.value=${provider?.spBinding}
help=${msg(
"Determines how authentik sends the response back to the Service Provider.",
)}
>
<ak-radio
.options=${[
{
label: msg("Redirect"),
value: SpBindingEnum.Redirect,
default: true,
},
{
label: msg("Post"),
value: SpBindingEnum.Post,
},
]}
.value=${provider?.spBinding}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${msg(
"Determines how authentik sends the response back to the Service Provider.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Audience")} name="audience">
<input
type="text"
value="${ifDefined(provider?.audience)}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
</ak-radio-input>
<ak-text-input
name="audience"
label=${msg("Audience")}
value="${ifDefined(provider?.audience)}"
.errorMessages=${errors?.audience ?? []}
></ak-text-input>
</div>
</ak-form-group>
@ -186,48 +197,8 @@ export function renderForm(
)}
</p>
</ak-form-element-horizontal>
${hasSigningKp
? html` <ak-form-element-horizontal name="signAssertion">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(provider?.signAssertion, true)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Sign assertions")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, the assertion element of the SAML response will be signed.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="signResponse">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(provider?.signResponse, false)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Sign responses")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, the assertion element of the SAML response will be signed.",
)}
</p>
</ak-form-element-horizontal>`
: nothing}
${hasSigningKp ? renderHasSigningKp(provider) : nothing}
<ak-form-element-horizontal
label=${msg("Verification Certificate")}
name="verificationKp"
@ -300,93 +271,60 @@ export function renderForm(
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Assertion valid not before")}
?required=${true}
<ak-text-input
name="assertionValidNotBefore"
>
<input
type="text"
value="${provider?.assertionValidNotBefore || "minutes=-5"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Configure the maximum allowed time drift for an assertion.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Assertion valid not on or after")}
?required=${true}
name="assertionValidNotOnOrAfter"
>
<input
type="text"
value="${provider?.assertionValidNotOnOrAfter || "minutes=5"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Assertion not valid on or after current time + this value.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Session valid not on or after")}
?required=${true}
name="sessionValidNotOnOrAfter"
>
<input
type="text"
value="${provider?.sessionValidNotOnOrAfter || "minutes=86400"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Session not valid on or after current time + this value.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Default relay state")}
name="defaultRelayState"
>
<input
type="text"
value="${provider?.defaultRelayState || ""}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${msg(
"When using IDP-initiated logins, the relay state will be set to this value.",
)}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
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.")}
></ak-text-input>
<ak-form-element-horizontal
label=${msg("Digest algorithm")}
?required=${true}
<ak-text-input
name="assertionValidNotOnOrAfter"
label=${msg("Assertion valid not on or after")}
value="${provider?.assertionValidNotOnOrAfter || "minutes=5"}"
required
.errorMessages=${errors?.assertionValidNotBefore ?? []}
help=${msg("Assertion not valid on or after current time + this value.")}
></ak-text-input>
<ak-text-input
name="sessionValidNotOnOrAfter"
label=${msg("Session valid not on or after")}
value="${provider?.sessionValidNotOnOrAfter || "minutes=86400"}"
required
.errorMessages=${errors?.sessionValidNotOnOrAfter ?? []}
help=${msg("Session not valid on or after current time + this value.")}
></ak-text-input>
<ak-text-input
name="defaultRelayState"
label=${msg("Default relay state")}
value="${provider?.defaultRelayState || ""}"
.errorMessages=${errors?.sessionValidNotOnOrAfter ?? []}
help=${msg(
"When using IDP-initiated logins, the relay state will be set to this value.",
)}
></ak-text-input>
<ak-radio-input
name="digestAlgorithm"
label=${msg("Digest algorithm")}
.options=${digestAlgorithmOptions}
.value=${provider?.digestAlgorithm}
required
>
<ak-radio
.options=${digestAlgorithmOptions}
.value=${provider?.digestAlgorithm}
>
</ak-radio>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Signature algorithm")}
?required=${true}
</ak-radio-input>
<ak-radio-input
name="signatureAlgorithm"
label=${msg("Signature algorithm")}
.options=${signatureAlgorithmOptions}
.value=${provider?.signatureAlgorithm}
required
>
<ak-radio
.options=${signatureAlgorithmOptions}
.value=${provider?.signatureAlgorithm}
>
</ak-radio>
</ak-form-element-horizontal>
</ak-radio-input>
</div>
</ak-form-group>`;
}

View File

@ -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<SCIMProvider>, errors: ValidationError = {}) {
return html`
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(provider?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-text-input
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name ?? []}
required
help=${msg("Method's display Name.")}
></ak-text-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("URL")} required name="url">
<input
type="text"
value="${first(provider?.url, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("SCIM base url, usually ends in /v2.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="verifyCertificates">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(provider?.verifyCertificates, true)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Verify SCIM server's certificates")}</span
>
</label>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Token")} required name="token">
<input
type="text"
value="${first(provider?.token, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Token to authenticate with. Currently only bearer authentication is supported.",
)}
</p>
</ak-form-element-horizontal>
<ak-text-input
name="url"
label=${msg("URL")}
value="${first(provider?.url, "")}"
.errorMessages=${errors?.url ?? []}
required
help=${msg("SCIM base url, usually ends in /v2.")}
></ak-text-input>
<ak-switch-input
name="verifyCertificates"
label=${msg("Verify SCIM server's certificates")}
?checked=${provider?.verifyCertificates ?? true}
>
</ak-switch-input>
<ak-text-input
name="token"
label=${msg("Token")}
value="${provider?.token ?? ""}"
.errorMessages=${errors?.token ?? []}
required
help=${msg(
"Token to authenticate with. Currently only bearer authentication is supported.",
)}
></ak-text-input>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="excludeUsersServiceAccount">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(provider?.excludeUsersServiceAccount, true)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Exclude service accounts")}</span>
</label>
</ak-form-element-horizontal>
<ak-switch-input
name="excludeUsersServiceAccount"
label=${msg("Exclude service accounts")}
?checked=${first(provider?.excludeUsersServiceAccount, true)}
>
</ak-switch-input>
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => {