Compare commits
20 Commits
enterprise
...
web/update
Author | SHA1 | Date | |
---|---|---|---|
76b9add6b4 | |||
d76e3c8023 | |||
fb40ee72a5 | |||
24f52252ba | |||
26ceb3d6c9 | |||
f756be2ece | |||
4bfd06e034 | |||
107dff39af | |||
401850c5e2 | |||
11bc9b8041 | |||
807e2a9fb0 | |||
5bd7cedaba | |||
c0814ad279 | |||
99af95b10c | |||
a36cc820bd | |||
6ff260df01 | |||
e497dbc314 | |||
f9f849574b | |||
4439b298bd | |||
4af6ecf629 |
1620
web/package-lock.json
generated
1620
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -135,6 +135,7 @@
|
||||
"storybook:build": "wireit",
|
||||
"storybook:build-import-map": "wireit",
|
||||
"test": "wireit",
|
||||
"test:e2e": "wireit",
|
||||
"test:e2e:watch": "wireit",
|
||||
"test:watch": "wireit",
|
||||
"tsc": "wireit",
|
||||
@ -321,11 +322,24 @@
|
||||
},
|
||||
"test": {
|
||||
"command": "wdio ./wdio.conf.ts --logLevel=warn",
|
||||
"dependencies": [
|
||||
"build"
|
||||
],
|
||||
"env": {
|
||||
"CI": "true",
|
||||
"TS_NODE_PROJECT": "tsconfig.test.json"
|
||||
}
|
||||
},
|
||||
"test:e2e": {
|
||||
"command": "wdio run ./tests/wdio.conf.ts",
|
||||
"dependencies": [
|
||||
"build"
|
||||
],
|
||||
"env": {
|
||||
"CI": "true",
|
||||
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
|
||||
}
|
||||
},
|
||||
"test:e2e:watch": {
|
||||
"command": "wdio run ./tests/wdio.conf.ts",
|
||||
"dependencies": [
|
||||
|
@ -29,7 +29,7 @@ export class ApplicationWizardPageBase
|
||||
return AwadStyles;
|
||||
}
|
||||
|
||||
@consume({ context: applicationWizardContext })
|
||||
@consume({ context: applicationWizardContext, subscribe: true })
|
||||
public wizard!: ApplicationWizardState;
|
||||
|
||||
@query("form")
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { createContext } from "@lit/context";
|
||||
|
||||
import { LocalTypeCreate } from "./auth-method-choice/ak-application-wizard-authentication-method-choice.choices.js";
|
||||
import { ApplicationWizardState } from "./types";
|
||||
|
||||
export const applicationWizardContext = createContext<ApplicationWizardState>(
|
||||
Symbol("ak-application-wizard-state-context"),
|
||||
);
|
||||
|
||||
export const applicationWizardProvidersContext = createContext<LocalTypeCreate[]>(
|
||||
Symbol("ak-application-wizard-providers-context"),
|
||||
);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { AkWizard } from "@goauthentik/components/ak-wizard-main/AkWizard";
|
||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
|
||||
|
||||
@ -5,7 +6,10 @@ import { ContextProvider } from "@lit/context";
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import { applicationWizardContext } from "./ContextIdentity";
|
||||
import { ProvidersApi, ProxyMode } from "@goauthentik/api";
|
||||
|
||||
import { applicationWizardContext, applicationWizardProvidersContext } from "./ContextIdentity";
|
||||
import { providerTypeRenderers } from "./auth-method-choice/ak-application-wizard-authentication-method-choice.choices.js";
|
||||
import { newSteps } from "./steps";
|
||||
import {
|
||||
ApplicationStep,
|
||||
@ -19,6 +23,7 @@ const freshWizardState = (): ApplicationWizardState => ({
|
||||
app: {},
|
||||
provider: {},
|
||||
errors: {},
|
||||
proxyMode: ProxyMode.Proxy,
|
||||
});
|
||||
|
||||
@customElement("ak-application-wizard")
|
||||
@ -46,6 +51,11 @@ export class ApplicationWizard extends CustomListenerElement(
|
||||
initialValue: this.wizardState,
|
||||
});
|
||||
|
||||
wizardProviderProvider = new ContextProvider(this, {
|
||||
context: applicationWizardProvidersContext,
|
||||
initialValue: [],
|
||||
});
|
||||
|
||||
/**
|
||||
* One of our steps has multiple display variants, one for each type of service provider. We
|
||||
* want to *preserve* a customer's decisions about different providers; never make someone "go
|
||||
@ -56,6 +66,21 @@ export class ApplicationWizard extends CustomListenerElement(
|
||||
*/
|
||||
providerCache: Map<string, OneOfProvider> = new Map();
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => {
|
||||
const wizardReadyProviders = Object.keys(providerTypeRenderers);
|
||||
this.wizardProviderProvider.setValue(
|
||||
providerTypes
|
||||
.filter((providerType) => wizardReadyProviders.includes(providerType.modelName))
|
||||
.map((providerType) => ({
|
||||
...providerType,
|
||||
renderer: providerTypeRenderers[providerType.modelName],
|
||||
})),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// And this is where all the special cases go...
|
||||
handleUpdate(detail: ApplicationWizardStateUpdate) {
|
||||
if (detail.status === "submitted") {
|
||||
|
@ -1,176 +1,28 @@
|
||||
import "@goauthentik/admin/common/ak-license-notice";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import type { ProviderModelEnum as ProviderModelEnumType, TypeCreate } from "@goauthentik/api";
|
||||
import { ProviderModelEnum, ProxyMode } from "@goauthentik/api";
|
||||
import type {
|
||||
LDAPProviderRequest,
|
||||
ModelRequest,
|
||||
OAuth2ProviderRequest,
|
||||
ProxyProviderRequest,
|
||||
RACProviderRequest,
|
||||
RadiusProviderRequest,
|
||||
SAMLProviderRequest,
|
||||
SCIMProviderRequest,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { OneOfProvider } from "../types";
|
||||
import type { TypeCreate } from "@goauthentik/api";
|
||||
|
||||
type ProviderRenderer = () => TemplateResult;
|
||||
|
||||
type ModelConverter = (provider: OneOfProvider) => ModelRequest;
|
||||
|
||||
type ProviderNoteProvider = () => TemplateResult | undefined;
|
||||
type ProviderNote = ProviderNoteProvider | undefined;
|
||||
|
||||
export type LocalTypeCreate = TypeCreate & {
|
||||
formName: string;
|
||||
modelName: ProviderModelEnumType;
|
||||
converter: ModelConverter;
|
||||
note?: ProviderNote;
|
||||
renderer: ProviderRenderer;
|
||||
};
|
||||
|
||||
export const providerModelsList: LocalTypeCreate[] = [
|
||||
{
|
||||
formName: "oauth2provider",
|
||||
name: msg("OAuth2/OIDC (Open Authorization/OpenID Connect)"),
|
||||
description: msg("Modern applications, APIs and Single-page applications."),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
|
||||
modelName: ProviderModelEnum.Oauth2Oauth2provider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.Oauth2Oauth2provider,
|
||||
...(provider as OAuth2ProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/openidconnect.svg",
|
||||
},
|
||||
{
|
||||
formName: "ldapprovider",
|
||||
name: msg("LDAP (Lightweight Directory Access Protocol)"),
|
||||
description: msg(
|
||||
"Provide an LDAP interface for applications and users to authenticate against.",
|
||||
),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
|
||||
modelName: ProviderModelEnum.LdapLdapprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.LdapLdapprovider,
|
||||
...(provider as LDAPProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/ldap.png",
|
||||
},
|
||||
{
|
||||
formName: "proxyprovider-proxy",
|
||||
name: msg("Transparent Reverse Proxy"),
|
||||
description: msg("For transparent reverse proxies with required authentication"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
|
||||
modelName: ProviderModelEnum.ProxyProxyprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.ProxyProxyprovider,
|
||||
...(provider as ProxyProviderRequest),
|
||||
mode: ProxyMode.Proxy,
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
formName: "proxyprovider-forwardsingle",
|
||||
name: msg("Forward Auth (Single Application)"),
|
||||
description: msg("For nginx's auth_request or traefik's forwardAuth"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>`,
|
||||
modelName: ProviderModelEnum.ProxyProxyprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.ProxyProxyprovider,
|
||||
...(provider as ProxyProviderRequest),
|
||||
mode: ProxyMode.ForwardSingle,
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
formName: "proxyprovider-forwarddomain",
|
||||
name: msg("Forward Auth (Domain Level)"),
|
||||
description: msg("For nginx's auth_request or traefik's forwardAuth per root domain"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-for-forward-proxy-domain></ak-application-wizard-authentication-for-forward-proxy-domain>`,
|
||||
modelName: ProviderModelEnum.ProxyProxyprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.ProxyProxyprovider,
|
||||
...(provider as ProxyProviderRequest),
|
||||
mode: ProxyMode.ForwardDomain,
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/proxy.svg",
|
||||
},
|
||||
{
|
||||
formName: "racprovider",
|
||||
name: msg("Remote Access Provider"),
|
||||
description: msg("Remotely access computers/servers via RDP/SSH/VNC"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
|
||||
modelName: ProviderModelEnum.RacRacprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.RacRacprovider,
|
||||
...(provider as RACProviderRequest),
|
||||
}),
|
||||
note: () => html`<ak-license-notice></ak-license-notice>`,
|
||||
requiresEnterprise: true,
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/rac.svg",
|
||||
},
|
||||
{
|
||||
formName: "samlprovider",
|
||||
name: msg("SAML (Security Assertion Markup Language)"),
|
||||
description: msg("Configure SAML provider manually"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
|
||||
modelName: ProviderModelEnum.SamlSamlprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.SamlSamlprovider,
|
||||
...(provider as SAMLProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/saml.png",
|
||||
},
|
||||
{
|
||||
formName: "radiusprovider",
|
||||
name: msg("RADIUS (Remote Authentication Dial-In User Service)"),
|
||||
description: msg("Configure RADIUS provider manually"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
|
||||
modelName: ProviderModelEnum.RadiusRadiusprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.RadiusRadiusprovider,
|
||||
...(provider as RadiusProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/radius.svg",
|
||||
},
|
||||
{
|
||||
formName: "scimprovider",
|
||||
name: msg("SCIM (System for Cross-domain Identity Management)"),
|
||||
description: msg("Configure SCIM provider manually"),
|
||||
renderer: () =>
|
||||
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
|
||||
modelName: ProviderModelEnum.ScimScimprovider,
|
||||
converter: (provider: OneOfProvider) => ({
|
||||
providerModel: ProviderModelEnum.ScimScimprovider,
|
||||
...(provider as SCIMProviderRequest),
|
||||
}),
|
||||
component: "",
|
||||
iconUrl: "/static/authentik/sources/scim.png",
|
||||
},
|
||||
];
|
||||
|
||||
export const providerRendererList = new Map<string, ProviderRenderer>(
|
||||
providerModelsList.map((tc) => [tc.formName, tc.renderer]),
|
||||
);
|
||||
|
||||
export default providerModelsList;
|
||||
export const providerTypeRenderers: Record<string, () => TemplateResult> = {
|
||||
oauth2provider: () =>
|
||||
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
|
||||
ldapprovider: () =>
|
||||
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
|
||||
proxyprovider: () =>
|
||||
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
|
||||
racprovider: () =>
|
||||
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
|
||||
samlprovider: () =>
|
||||
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
|
||||
radiusprovider: () =>
|
||||
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
|
||||
scimprovider: () =>
|
||||
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
|
||||
};
|
||||
|
@ -7,41 +7,37 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
|
||||
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage";
|
||||
|
||||
import { consume } from "@lit/context";
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { html } from "lit";
|
||||
|
||||
import BasePanel from "../BasePanel";
|
||||
import { applicationWizardProvidersContext } from "../ContextIdentity";
|
||||
import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices";
|
||||
import providerModelsList from "./ak-application-wizard-authentication-method-choice.choices";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-method-choice")
|
||||
export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSummary(BasePanel) {
|
||||
@consume({ context: applicationWizardProvidersContext })
|
||||
public providerModelsList!: LocalTypeCreate[];
|
||||
|
||||
render() {
|
||||
const selectedTypes = providerModelsList.filter(
|
||||
(t) => t.formName === this.wizard.providerModel,
|
||||
const selectedTypes = this.providerModelsList.filter(
|
||||
(t) => t.modelName === this.wizard.providerModel,
|
||||
);
|
||||
|
||||
// As a hack, the Application wizard has separate provider paths for our three types of
|
||||
// proxy providers. This patch swaps the form we want to be directed to on page 3 from the
|
||||
// modelName to the formName, so we get the right one. This information isn't modified
|
||||
// or forwarded, so the proxy-plus-subtype is correctly mapped on submission.
|
||||
const typesForWizard = providerModelsList.map((provider) => ({
|
||||
...provider,
|
||||
modelName: provider.formName,
|
||||
}));
|
||||
|
||||
return providerModelsList.length > 0
|
||||
return this.providerModelsList.length > 0
|
||||
? html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-wizard-page-type-create
|
||||
.types=${typesForWizard}
|
||||
.types=${this.providerModelsList}
|
||||
name="selectProviderType"
|
||||
layout=${TypeCreateWizardPageLayouts.grid}
|
||||
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
|
||||
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
|
||||
this.dispatchWizardUpdate({
|
||||
update: {
|
||||
...this.wizard,
|
||||
providerModel: ev.detail.formName,
|
||||
providerModel: ev.detail.modelName,
|
||||
errors: {},
|
||||
},
|
||||
status: this.valid ? "valid" : "invalid",
|
||||
|
@ -22,6 +22,9 @@ import {
|
||||
type ApplicationRequest,
|
||||
CoreApi,
|
||||
type ModelRequest,
|
||||
ProviderModelEnum,
|
||||
ProxyMode,
|
||||
type ProxyProviderRequest,
|
||||
type TransactionApplicationRequest,
|
||||
type TransactionApplicationResponse,
|
||||
ValidationError,
|
||||
@ -29,7 +32,6 @@ import {
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import BasePanel from "../BasePanel";
|
||||
import providerModelsList from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
|
||||
|
||||
function cleanApplication(app: Partial<ApplicationRequest>): ApplicationRequest {
|
||||
return {
|
||||
@ -39,14 +41,19 @@ function cleanApplication(app: Partial<ApplicationRequest>): ApplicationRequest
|
||||
};
|
||||
}
|
||||
|
||||
type ProviderModelType = Exclude<ModelRequest["providerModel"], "11184809">;
|
||||
|
||||
type State = {
|
||||
state: "idle" | "running" | "error" | "success";
|
||||
label: string | TemplateResult;
|
||||
icon: string[];
|
||||
};
|
||||
|
||||
const providerMap: Map<string, string> = Object.values(ProviderModelEnum)
|
||||
.filter((value) => /^authentik_providers_/.test(value) && /provider$/.test(value))
|
||||
.reduce((acc: Map<string, string>, value) => {
|
||||
acc.set(value.split(".")[1], value);
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
const idleState: State = {
|
||||
state: "idle",
|
||||
label: "",
|
||||
@ -70,6 +77,7 @@ const successState: State = {
|
||||
icon: ["fa-check-circle", "pf-m-success"],
|
||||
};
|
||||
|
||||
type StrictProviderModelEnum = Exclude<ProviderModelEnum, "11184809">;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const isValidationError = (v: any): v is ValidationError => instanceOfValidationError(v);
|
||||
|
||||
@ -102,19 +110,28 @@ export class ApplicationWizardCommitApplication extends BasePanel {
|
||||
if (this.commitState === idleState) {
|
||||
this.response = undefined;
|
||||
this.commitState = runningState;
|
||||
const providerModel = providerModelsList.find(
|
||||
({ formName }) => formName === this.wizard.providerModel,
|
||||
);
|
||||
if (!providerModel) {
|
||||
throw new Error(
|
||||
`Could not determine provider model from user request: ${JSON.stringify(this.wizard, null, 2)}`,
|
||||
);
|
||||
|
||||
// Stringly-based API. Not the best, but it works. Just be aware that it is
|
||||
// stringly-based.
|
||||
|
||||
const providerModel = providerMap.get(
|
||||
this.wizard.providerModel,
|
||||
) as StrictProviderModelEnum;
|
||||
const provider = this.wizard.provider as ModelRequest;
|
||||
provider.providerModel = providerModel;
|
||||
|
||||
// Special case for the Proxy provider.
|
||||
if (this.wizard.providerModel === "proxyprovider") {
|
||||
(provider as ProxyProviderRequest).mode = this.wizard.proxyMode;
|
||||
if ((provider as ProxyProviderRequest).mode !== ProxyMode.ForwardDomain) {
|
||||
(provider as ProxyProviderRequest).cookieDomain = "";
|
||||
}
|
||||
}
|
||||
|
||||
const request: TransactionApplicationRequest = {
|
||||
providerModel: providerModel.modelName as ProviderModelType,
|
||||
app: cleanApplication(this.wizard.app),
|
||||
provider: providerModel.converter(this.wizard.provider),
|
||||
providerModel,
|
||||
provider,
|
||||
};
|
||||
|
||||
this.send(request);
|
||||
@ -125,6 +142,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
|
||||
data: TransactionApplicationRequest,
|
||||
): Promise<TransactionApplicationResponse | void> {
|
||||
this.errors = undefined;
|
||||
this.commitState = idleState;
|
||||
new CoreApi(DEFAULT_CONFIG)
|
||||
.coreTransactionalApplicationsUpdate({
|
||||
transactionApplicationRequest: data,
|
||||
@ -138,6 +156,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.catch(async (resolution: any) => {
|
||||
const errors = await parseAPIError(resolution);
|
||||
console.log(errors);
|
||||
|
||||
// THIS is a really gross special case; if the user is duplicating the name of an
|
||||
// existing provider, the error appears on the `app` (!) error object. We have to
|
||||
@ -164,11 +183,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",
|
||||
@ -219,7 +234,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>
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
|
||||
import BasePanel from "../BasePanel";
|
||||
import { providerRendererList } from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
|
||||
import { applicationWizardProvidersContext } from "../ContextIdentity";
|
||||
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-forward-domain-proxy";
|
||||
import "./proxy/ak-application-wizard-authentication-for-reverse-proxy";
|
||||
import "./proxy/ak-application-wizard-authentication-for-single-forward-proxy";
|
||||
import "./rac/ak-application-wizard-authentication-for-rac";
|
||||
import "./radius/ak-application-wizard-authentication-by-radius";
|
||||
import "./saml/ak-application-wizard-authentication-by-saml-configuration";
|
||||
@ -14,14 +14,19 @@ 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[];
|
||||
|
||||
render() {
|
||||
const handler = providerRendererList.get(this.wizard.providerModel);
|
||||
const handler: LocalTypeCreate | undefined = this.providerModelsList.find(
|
||||
({ modelName }) => modelName === this.wizard.providerModel,
|
||||
);
|
||||
if (!handler) {
|
||||
throw new Error(
|
||||
"Unrecognized authentication method in ak-application-wizard-authentication-method",
|
||||
);
|
||||
}
|
||||
return handler();
|
||||
return handler.renderer();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,33 +1,14 @@
|
||||
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 { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-number-input";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import { renderForm } from "@goauthentik/admin/providers/ldap/LDAPProviderFormForm.js";
|
||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { html } from "lit";
|
||||
|
||||
import { FlowsInstancesListDesignationEnum } from "@goauthentik/api";
|
||||
import type { LDAPProvider } from "@goauthentik/api";
|
||||
|
||||
import BaseProviderPanel from "../BaseProviderPanel";
|
||||
import {
|
||||
bindModeOptions,
|
||||
cryptoCertificateHelp,
|
||||
gidStartNumberHelp,
|
||||
mfaSupportHelp,
|
||||
searchModeOptions,
|
||||
tlsServerNameHelp,
|
||||
uidStartNumberHelp,
|
||||
} from "./LDAPOptionsAndHelp";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-by-ldap")
|
||||
export class ApplicationWizardApplicationDetails extends WithBrandConfig(BaseProviderPanel) {
|
||||
@ -37,129 +18,7 @@ export class ApplicationWizardApplicationDetails extends WithBrandConfig(BasePro
|
||||
|
||||
return html` <ak-wizard-title>${msg("Configure LDAP Provider")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<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-element-horizontal
|
||||
label=${msg("Bind flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
.errorMessages=${errors?.authorizationFlow ?? []}
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
.brandFlow=${this.brand.flowAuthentication}
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used for users to authenticate.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Unbind flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
.brandFlow=${this.brand.flowInvalidation}
|
||||
defaultFlowSlug="default-invalidation-flow"
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Flow used for unbinding users.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-radio-input
|
||||
label=${msg("Bind mode")}
|
||||
name="bindMode"
|
||||
.options=${bindModeOptions}
|
||||
.value=${provider?.bindMode}
|
||||
help=${msg("Configure how the outpost authenticates requests.")}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-radio-input
|
||||
label=${msg("Search mode")}
|
||||
name="searchMode"
|
||||
.options=${searchModeOptions}
|
||||
.value=${provider?.searchMode}
|
||||
help=${msg(
|
||||
"Configure how the outpost queries the core authentik server's users.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-switch-input
|
||||
name="mfaSupport"
|
||||
label=${msg("Code-based MFA Support")}
|
||||
?checked=${provider?.mfaSupport ?? true}
|
||||
help=${mfaSupportHelp}
|
||||
>
|
||||
</ak-switch-input>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="baseDn"
|
||||
label=${msg("Base DN")}
|
||||
required
|
||||
value="${first(provider?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}"
|
||||
.errorMessages=${errors?.baseDn ?? []}
|
||||
help=${msg(
|
||||
"LDAP DN under which bind requests and search requests can be made.",
|
||||
)}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Certificate")}
|
||||
name="certificate"
|
||||
.errorMessages=${errors?.certificate ?? []}
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.certificate ?? nothing)}
|
||||
name="certificate"
|
||||
>
|
||||
</ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">${cryptoCertificateHelp}</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-text-input
|
||||
label=${msg("TLS Server name")}
|
||||
name="tlsServerName"
|
||||
value="${first(provider?.tlsServerName, "")}"
|
||||
.errorMessages=${errors?.tlsServerName ?? []}
|
||||
help=${tlsServerNameHelp}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-number-input
|
||||
label=${msg("UID start number")}
|
||||
required
|
||||
name="uidStartNumber"
|
||||
value="${first(provider?.uidStartNumber, 2000)}"
|
||||
.errorMessages=${errors?.uidStartNumber ?? []}
|
||||
help=${uidStartNumberHelp}
|
||||
></ak-number-input>
|
||||
|
||||
<ak-number-input
|
||||
label=${msg("GID start number")}
|
||||
required
|
||||
name="gidStartNumber"
|
||||
value="${first(provider?.gidStartNumber, 4000)}"
|
||||
.errorMessages=${errors?.gidStartNumber ?? []}
|
||||
help=${gidStartNumberHelp}
|
||||
></ak-number-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
${renderForm(provider, errors, this.brand)}
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +1,11 @@
|
||||
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 {
|
||||
makeOAuth2PropertyMappingsSelector,
|
||||
oauth2PropertyMappingsProvider,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2PropertyMappings.js";
|
||||
import {
|
||||
clientTypeOptions,
|
||||
issuerModeOptions,
|
||||
redirectUriHelp,
|
||||
subjectModeOptions,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
|
||||
import {
|
||||
IRedirectURIInput,
|
||||
akOAuthRedirectURIInput,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
|
||||
import {
|
||||
makeSourceSelector,
|
||||
oauth2SourcesProvider,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
|
||||
import { renderForm } from "@goauthentik/admin/providers/oauth2/OAuth2ProviderFormForm.js";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-number-input";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/components/ak-textarea-input";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement, state } from "@lit/reactive-element/decorators.js";
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { html } from "lit";
|
||||
|
||||
import {
|
||||
ClientTypeEnum,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
MatchingModeEnum,
|
||||
RedirectURI,
|
||||
SourcesApi,
|
||||
} from "@goauthentik/api";
|
||||
import { SourcesApi } from "@goauthentik/api";
|
||||
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
|
||||
|
||||
import BaseProviderPanel from "../BaseProviderPanel";
|
||||
@ -69,258 +33,17 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
|
||||
render() {
|
||||
const provider = this.wizard.provider as OAuth2Provider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
return html`<ak-wizard-title>${msg("Configure OAuth2/OpenId Provider")}</ak-wizard-title>
|
||||
const showClientSecretCallback = (show: boolean) => {
|
||||
this.showClientSecret = show;
|
||||
};
|
||||
return html` <ak-wizard-title>${msg("Configure OAuth2 Provider")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
label=${msg("Name")}
|
||||
value=${ifDefined(provider?.name)}
|
||||
.errorMessages=${errors?.name ?? []}
|
||||
required
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
name="authorizationFlow"
|
||||
label=${msg("Authorization flow")}
|
||||
.errorMessages=${errors?.authorizationFlow ?? []}
|
||||
?required=${true}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-radio-input
|
||||
name="clientType"
|
||||
label=${msg("Client type")}
|
||||
.value=${provider?.clientType}
|
||||
required
|
||||
@change=${(ev: CustomEvent<{ value: ClientTypeEnum }>) => {
|
||||
this.showClientSecret = ev.detail.value !== ClientTypeEnum.Public;
|
||||
}}
|
||||
.options=${clientTypeOptions}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-text-input
|
||||
name="clientId"
|
||||
label=${msg("Client ID")}
|
||||
value=${provider?.clientId ?? randomString(40, ascii_letters + digits)}
|
||||
.errorMessages=${errors?.clientId ?? []}
|
||||
required
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="clientSecret"
|
||||
label=${msg("Client Secret")}
|
||||
value=${provider?.clientSecret ??
|
||||
randomString(128, ascii_letters + digits)}
|
||||
.errorMessages=${errors?.clientSecret ?? []}
|
||||
?hidden=${!this.showClientSecret}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Redirect URIs/Origins")}
|
||||
required
|
||||
name="redirectUris"
|
||||
>
|
||||
<ak-array-input
|
||||
.items=${[]}
|
||||
.newItem=${() => ({
|
||||
matchingMode: MatchingModeEnum.Strict,
|
||||
url: "",
|
||||
})}
|
||||
.row=${(f?: RedirectURI) =>
|
||||
akOAuthRedirectURIInput({
|
||||
".redirectURI": f,
|
||||
"style": "width: 100%",
|
||||
"name": "oauth2-redirect-uri",
|
||||
} as unknown as IRedirectURIInput)}
|
||||
>
|
||||
</ak-array-input>
|
||||
${redirectUriHelp}
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Signing Key")}
|
||||
name="signingKey"
|
||||
.errorMessages=${errors?.signingKey ?? []}
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.signingKey ?? nothing)}
|
||||
name="certificate"
|
||||
singleton
|
||||
>
|
||||
</ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Key used to sign the tokens.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
name="authenticationFlow"
|
||||
label=${msg("Authentication flow")}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
defaultFlowSlug="default-provider-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="accessCodeValidity"
|
||||
label=${msg("Access code validity")}
|
||||
required
|
||||
value="${first(provider?.accessCodeValidity, "minutes=1")}"
|
||||
.errorMessages=${errors?.accessCodeValidity ?? []}
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg("Configure how long access codes are valid for.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="accessTokenValidity"
|
||||
label=${msg("Access Token validity")}
|
||||
value="${first(provider?.accessTokenValidity, "minutes=5")}"
|
||||
required
|
||||
.errorMessages=${errors?.accessTokenValidity ?? []}
|
||||
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||
${msg("Configure how long access tokens are valid for.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="refreshTokenValidity"
|
||||
label=${msg("Refresh Token validity")}
|
||||
value="${first(provider?.refreshTokenValidity, "days=30")}"
|
||||
.errorMessages=${errors?.refreshTokenValidity ?? []}
|
||||
?required=${true}
|
||||
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||
${msg("Configure how long refresh tokens are valid for.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Scopes")}
|
||||
name="propertyMappings"
|
||||
.errorMessages=${errors?.propertyMappings ?? []}
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${oauth2PropertyMappingsProvider}
|
||||
.selector=${makeOAuth2PropertyMappingsSelector(
|
||||
provider?.propertyMappings,
|
||||
)}
|
||||
available-label=${msg("Available Scopes")}
|
||||
selected-label=${msg("Selected Scopes")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Select which scopes can be used by the client. The client still has to specify the scope to access the data.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-radio-input
|
||||
name="subMode"
|
||||
label=${msg("Subject mode")}
|
||||
required
|
||||
.options=${subjectModeOptions}
|
||||
.value=${provider?.subMode}
|
||||
help=${msg(
|
||||
"Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
<ak-switch-input
|
||||
name="includeClaimsInIdToken"
|
||||
label=${msg("Include claims in id_token")}
|
||||
?checked=${first(provider?.includeClaimsInIdToken, true)}
|
||||
help=${msg(
|
||||
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
|
||||
)}
|
||||
></ak-switch-input>
|
||||
<ak-radio-input
|
||||
name="issuerMode"
|
||||
label=${msg("Issuer mode")}
|
||||
required
|
||||
.options=${issuerModeOptions}
|
||||
.value=${provider?.issuerMode}
|
||||
help=${msg(
|
||||
"Configure how the issuer field of the ID Token should be filled.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header">${msg("Machine-to-Machine authentication settings")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Trusted OIDC Sources")}
|
||||
name="jwksSources"
|
||||
.errorMessages=${errors?.jwksSources ?? []}
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${oauth2SourcesProvider}
|
||||
.selector=${makeSourceSelector(provider?.jwksSources)}
|
||||
available-label=${msg("Available Sources")}
|
||||
selected-label=${msg("Selected Sources")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
${renderForm(
|
||||
provider ?? {},
|
||||
errors,
|
||||
this.showClientSecret,
|
||||
showClientSecretCallback,
|
||||
)}
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
@ -1,267 +0,0 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import {
|
||||
makeSourceSelector,
|
||||
oauth2SourcesProvider,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
|
||||
import {
|
||||
makeProxyPropertyMappingsSelector,
|
||||
proxyPropertyMappingsProvider,
|
||||
} from "@goauthentik/admin/providers/proxy/ProxyProviderPropertyMappings.js";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/components/ak-textarea-input";
|
||||
import "@goauthentik/components/ak-toggle-group";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { state } from "@lit/reactive-element/decorators.js";
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
PaginatedOAuthSourceList,
|
||||
PaginatedScopeMappingList,
|
||||
ProxyMode,
|
||||
ProxyProvider,
|
||||
SourcesApi,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import BaseProviderPanel from "../BaseProviderPanel";
|
||||
|
||||
type MaybeTemplateResult = TemplateResult | typeof nothing;
|
||||
|
||||
export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
|
||||
constructor() {
|
||||
super();
|
||||
new SourcesApi(DEFAULT_CONFIG)
|
||||
.sourcesOauthList({
|
||||
ordering: "name",
|
||||
hasJwks: true,
|
||||
})
|
||||
.then((oauthSources: PaginatedOAuthSourceList) => {
|
||||
this.oauthSources = oauthSources;
|
||||
});
|
||||
}
|
||||
|
||||
propertyMappings?: PaginatedScopeMappingList;
|
||||
oauthSources?: PaginatedOAuthSourceList;
|
||||
|
||||
@state()
|
||||
showHttpBasic = true;
|
||||
|
||||
@state()
|
||||
mode: ProxyMode = ProxyMode.Proxy;
|
||||
|
||||
get instance(): ProxyProvider | undefined {
|
||||
return this.wizard.provider as ProxyProvider;
|
||||
}
|
||||
|
||||
renderModeDescription(): MaybeTemplateResult {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
renderProxyMode(): TemplateResult {
|
||||
throw new Error("Must be implemented in a child class.");
|
||||
}
|
||||
|
||||
renderHttpBasic() {
|
||||
return html`<ak-text-input
|
||||
name="basicAuthUserAttribute"
|
||||
label=${msg("HTTP-Basic Username Key")}
|
||||
value="${ifDefined(this.instance?.basicAuthUserAttribute)}"
|
||||
help=${msg(
|
||||
"User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
|
||||
)}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="basicAuthPasswordAttribute"
|
||||
label=${msg("HTTP-Basic Password Key")}
|
||||
value="${ifDefined(this.instance?.basicAuthPasswordAttribute)}"
|
||||
help=${msg(
|
||||
"User/Group Attribute used for the password part of the HTTP-Basic Header.",
|
||||
)}
|
||||
>
|
||||
</ak-text-input>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
return html` <ak-wizard-title>${msg("Configure Proxy Provider")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
${this.renderModeDescription()}
|
||||
<ak-text-input
|
||||
name="name"
|
||||
value=${ifDefined(this.instance?.name)}
|
||||
required
|
||||
.errorMessages=${errors?.name ?? []}
|
||||
label=${msg("Name")}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
required
|
||||
name="authorizationFlow"
|
||||
.errorMessages=${errors?.authorizationFlow ?? []}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
${this.renderProxyMode()}
|
||||
|
||||
<ak-text-input
|
||||
name="accessTokenValidity"
|
||||
value=${first(this.instance?.accessTokenValidity, "hours=24")}
|
||||
label=${msg("Token validity")}
|
||||
help=${msg("Configure how long tokens are valid for.")}
|
||||
.errorMessages=${errors?.accessTokenValidity ?? []}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header">${msg("Advanced protocol settings")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Certificate")}
|
||||
name="certificate"
|
||||
.errorMessages=${errors?.certificate ?? []}
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(this.instance?.certificate ?? undefined)}
|
||||
></ak-crypto-certificate-search>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Additional scopes")}
|
||||
name="propertyMappings"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${proxyPropertyMappingsProvider}
|
||||
.selector=${makeProxyPropertyMappingsSelector(
|
||||
this.instance?.propertyMappings,
|
||||
)}
|
||||
available-label="${msg("Available Scopes")}"
|
||||
selected-label="${msg("Selected Scopes")}"
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Additional scope mappings, which are passed to the proxy.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-textarea-input
|
||||
name="skipPathRegex"
|
||||
label=${this.mode === ProxyMode.ForwardDomain
|
||||
? msg("Unauthenticated URLs")
|
||||
: msg("Unauthenticated Paths")}
|
||||
value=${ifDefined(this.instance?.skipPathRegex)}
|
||||
.errorMessages=${errors?.skipPathRegex ?? []}
|
||||
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.",
|
||||
)}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.",
|
||||
)}
|
||||
</p>`}
|
||||
>
|
||||
</ak-textarea-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
name="authenticationFlow"
|
||||
label=${msg("Authentication flow")}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${this.instance?.invalidationFlow}
|
||||
defaultFlowSlug="default-provider-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header">${msg("Authentication settings")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-switch-input
|
||||
name="interceptHeaderAuth"
|
||||
?checked=${first(this.instance?.interceptHeaderAuth, true)}
|
||||
label=${msg("Intercept header authentication")}
|
||||
help=${msg(
|
||||
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
|
||||
)}
|
||||
></ak-switch-input>
|
||||
|
||||
<ak-switch-input
|
||||
name="basicAuthEnabled"
|
||||
?checked=${first(this.instance?.basicAuthEnabled, false)}
|
||||
@change=${(ev: Event) => {
|
||||
const el = ev.target as HTMLInputElement;
|
||||
this.showHttpBasic = el.checked;
|
||||
}}
|
||||
label=${msg("Send HTTP-Basic Authentication")}
|
||||
help=${msg(
|
||||
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
|
||||
)}
|
||||
></ak-switch-input>
|
||||
|
||||
${this.showHttpBasic ? this.renderHttpBasic() : html``}
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Trusted OIDC Sources")}
|
||||
name="jwksSources"
|
||||
.errorMessages=${errors?.jwksSources ?? []}
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${oauth2SourcesProvider}
|
||||
.selector=${makeSourceSelector(this.instance?.jwksSources)}
|
||||
available-label=${msg("Available Sources")}
|
||||
selected-label=${msg("Selected Sources")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkTypeProxyApplicationWizardPage;
|
@ -1,74 +0,0 @@
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators.js";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
|
||||
import { ProxyProvider } from "@goauthentik/api";
|
||||
|
||||
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-for-forward-proxy-domain")
|
||||
export class AkForwardDomainProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
|
||||
static get styles() {
|
||||
return super.styles.concat(PFList);
|
||||
}
|
||||
|
||||
renderModeDescription() {
|
||||
return html`<p>
|
||||
${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.",
|
||||
)}
|
||||
</p>
|
||||
<div>
|
||||
${msg("An example setup can look like this:")}
|
||||
<ul class="pf-c-list">
|
||||
<li>${msg("authentik running on auth.example.com")}</li>
|
||||
<li>${msg("app1 running on app1.example.com")}</li>
|
||||
</ul>
|
||||
${msg(
|
||||
"In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.",
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
renderProxyMode() {
|
||||
const provider = this.wizard.provider as ProxyProvider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
return html`
|
||||
<ak-text-input
|
||||
name="externalHost"
|
||||
label=${msg("External host")}
|
||||
value=${ifDefined(provider?.externalHost)}
|
||||
.errorMessages=${errors?.externalHost ?? []}
|
||||
required
|
||||
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
|
||||
name="cookieDomain"
|
||||
label=${msg("Cookie domain")}
|
||||
value="${ifDefined(provider?.cookieDomain)}"
|
||||
.errorMessages=${errors?.cookieDomain ?? []}
|
||||
required
|
||||
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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkForwardDomainProxyApplicationWizardPage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-authentication-for-forward-proxy-domain": AkForwardDomainProxyApplicationWizardPage;
|
||||
}
|
||||
}
|
@ -1,55 +1,50 @@
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import {
|
||||
ProxyModeValue,
|
||||
type SetMode,
|
||||
type SetShowHttpBasic,
|
||||
renderForm,
|
||||
} from "@goauthentik/admin/providers/proxy/ProxyProviderFormForm.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators.js";
|
||||
import { customElement, state } from "@lit/reactive-element/decorators.js";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { ProxyProvider } from "@goauthentik/api";
|
||||
import { ProxyMode } from "@goauthentik/api";
|
||||
|
||||
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
|
||||
import BaseProviderPanel from "../BaseProviderPanel.js";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-for-reverse-proxy")
|
||||
export class AkReverseProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
|
||||
renderModeDescription() {
|
||||
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>`;
|
||||
}
|
||||
export class AkReverseProxyApplicationWizardPage extends BaseProviderPanel {
|
||||
@state()
|
||||
showHttpBasic = true;
|
||||
|
||||
renderProxyMode() {
|
||||
const provider = this.wizard.provider as ProxyProvider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
render() {
|
||||
const onSetMode: SetMode = (ev: CustomEvent<ProxyModeValue>) => {
|
||||
this.dispatchWizardUpdate({
|
||||
update: {
|
||||
...this.wizard,
|
||||
proxyMode: ev.detail.value,
|
||||
},
|
||||
});
|
||||
// We deliberately chose not to make the forms "controlled," but we do need this form to
|
||||
// respond immediately to a state change in the wizard.
|
||||
window.setTimeout(() => this.requestUpdate(), 0);
|
||||
};
|
||||
|
||||
return html` <ak-text-input
|
||||
name="externalHost"
|
||||
value=${ifDefined(provider?.externalHost)}
|
||||
required
|
||||
label=${msg("External host")}
|
||||
.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"
|
||||
value=${ifDefined(provider?.internalHost)}
|
||||
.errorMessages=${errors?.internalHost ?? []}
|
||||
required
|
||||
label=${msg("Internal host")}
|
||||
help=${msg("Upstream host that the requests are forwarded to.")}
|
||||
></ak-text-input>
|
||||
<ak-switch-input
|
||||
name="internalHostSslValidation"
|
||||
?checked=${first(provider?.internalHostSslValidation, true)}
|
||||
label=${msg("Internal host SSL Validation")}
|
||||
help=${msg("Validate SSL Certificates of upstream servers.")}
|
||||
>
|
||||
</ak-switch-input>`;
|
||||
const onSetShowHttpBasic: SetShowHttpBasic = (ev: Event) => {
|
||||
const el = ev.target as HTMLInputElement;
|
||||
this.showHttpBasic = el.checked;
|
||||
};
|
||||
|
||||
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 ?? ProxyMode.Proxy,
|
||||
onSetMode,
|
||||
showHttpBasic: this.showHttpBasic,
|
||||
onSetShowHttpBasic,
|
||||
})}
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,48 +0,0 @@
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators.js";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { ProxyProvider } from "@goauthentik/api";
|
||||
|
||||
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-for-single-forward-proxy")
|
||||
export class AkForwardSingleProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
|
||||
renderModeDescription() {
|
||||
return html`<p class="pf-u-mb-xl">
|
||||
${msg(
|
||||
html`Use this provider with nginx's <code>auth_request</code> or traefik's
|
||||
<code>forwardAuth</code>. Each application/domain needs its own provider.
|
||||
Additionally, on each domain, <code>/outpost.goauthentik.io</code> must be
|
||||
routed to the outpost (when using a managed outpost, this is done for you).`,
|
||||
)}
|
||||
</p>`;
|
||||
}
|
||||
|
||||
renderProxyMode() {
|
||||
const provider = this.wizard.provider as ProxyProvider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
return html`<ak-text-input
|
||||
name="externalHost"
|
||||
value=${ifDefined(provider?.externalHost)}
|
||||
required
|
||||
label=${msg("External host")}
|
||||
.errorMessages=${errors?.externalHost ?? []}
|
||||
help=${msg(
|
||||
"The external URL you'll access the application at. Include any non-standard port.",
|
||||
)}
|
||||
></ak-text-input>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkForwardSingleProxyApplicationWizardPage;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-application-wizard-authentication-for-single-forward-proxy": AkForwardSingleProxyApplicationWizardPage;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
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 { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||
import { renderForm } from "@goauthentik/admin/providers/radius/RadiusProviderFormForm.js";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
@ -10,91 +10,21 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators.js";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { FlowsInstancesListDesignationEnum, RadiusProvider } from "@goauthentik/api";
|
||||
import { RadiusProvider } from "@goauthentik/api";
|
||||
|
||||
import BaseProviderPanel from "../BaseProviderPanel";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-by-radius")
|
||||
export class ApplicationWizardAuthenticationByRadius extends WithBrandConfig(BaseProviderPanel) {
|
||||
render() {
|
||||
const provider = this.wizard.provider as RadiusProvider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
return html`<ak-wizard-title>${msg("Configure Radius Provider")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
label=${msg("Name")}
|
||||
value=${ifDefined(provider?.name)}
|
||||
.errorMessages=${errors?.name ?? []}
|
||||
required
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
.errorMessages=${errors?.authorizationFlow ?? []}
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
.brandFlow=${this.brand.flowAuthentication}
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used for users to authenticate.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group expanded>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="sharedSecret"
|
||||
label=${msg("Shared secret")}
|
||||
.errorMessages=${errors?.sharedSecret ?? []}
|
||||
value=${first(
|
||||
provider?.sharedSecret,
|
||||
randomString(128, ascii_letters + digits),
|
||||
)}
|
||||
required
|
||||
></ak-text-input>
|
||||
<ak-text-input
|
||||
name="clientNetworks"
|
||||
label=${msg("Client Networks")}
|
||||
value=${first(provider?.clientNetworks, "0.0.0.0/0, ::/0")}
|
||||
.errorMessages=${errors?.clientNetworks ?? []}
|
||||
required
|
||||
help=${msg(`List of CIDRs (comma-seperated) that clients can connect from. A more specific
|
||||
CIDR will match before a looser one. Clients connecting from a non-specified CIDR
|
||||
will be dropped.`)}
|
||||
></ak-text-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
defaultFlowSlug="default-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div></ak-form-group
|
||||
>
|
||||
${renderForm(
|
||||
this.wizard.provider as RadiusProvider | undefined,
|
||||
this.wizard.errors.provider,
|
||||
this.brand,
|
||||
)}
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
@ -1,356 +1,35 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import AkCryptoCertificateSearch from "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-multi-select";
|
||||
import "@goauthentik/components/ak-number-input";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { renderForm } from "@goauthentik/admin/providers/saml/SAMLProviderFormForm.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement, state } from "@lit/reactive-element/decorators.js";
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { html } from "lit";
|
||||
|
||||
import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
PaginatedSAMLPropertyMappingList,
|
||||
PropertymappingsApi,
|
||||
SAMLProvider,
|
||||
} from "@goauthentik/api";
|
||||
import { SAMLProvider } from "@goauthentik/api";
|
||||
|
||||
import BaseProviderPanel from "../BaseProviderPanel";
|
||||
import {
|
||||
digestAlgorithmOptions,
|
||||
signatureAlgorithmOptions,
|
||||
spBindingOptions,
|
||||
} from "./SamlProviderOptions";
|
||||
import "./saml-property-mappings-search";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-by-saml-configuration")
|
||||
export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPanel {
|
||||
@state()
|
||||
propertyMappings?: PaginatedSAMLPropertyMappingList;
|
||||
|
||||
@state()
|
||||
hasSigningKp = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
new PropertymappingsApi(DEFAULT_CONFIG)
|
||||
.propertymappingsProviderSamlList({
|
||||
ordering: "saml_name",
|
||||
})
|
||||
.then((propertyMappings: PaginatedSAMLPropertyMappingList) => {
|
||||
this.propertyMappings = propertyMappings;
|
||||
});
|
||||
}
|
||||
|
||||
propertyMappingConfiguration(provider?: SAMLProvider) {
|
||||
const propertyMappings = this.propertyMappings?.results ?? [];
|
||||
|
||||
const configuredMappings = (providerMappings: string[]) =>
|
||||
propertyMappings.map((pm) => pm.pk).filter((pmpk) => providerMappings.includes(pmpk));
|
||||
|
||||
const managedMappings = () =>
|
||||
propertyMappings
|
||||
.filter((pm) => (pm?.managed ?? "").startsWith("goauthentik.io/providers/saml"))
|
||||
.map((pm) => pm.pk);
|
||||
|
||||
const pmValues = provider?.propertyMappings
|
||||
? configuredMappings(provider?.propertyMappings ?? [])
|
||||
: managedMappings();
|
||||
|
||||
const propertyPairs = propertyMappings.map((pm) => [pm.pk, pm.name]);
|
||||
|
||||
return { pmValues, propertyPairs };
|
||||
}
|
||||
|
||||
render() {
|
||||
const provider = this.wizard.provider as SAMLProvider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
const { pmValues, propertyPairs } = this.propertyMappingConfiguration(provider);
|
||||
const setHasSigningKp = (ev: InputEvent) => {
|
||||
const target = ev.target as AkCryptoCertificateSearch;
|
||||
if (!target) return;
|
||||
this.hasSigningKp = !!target.selectedKeypair;
|
||||
};
|
||||
|
||||
return html` <ak-wizard-title>${msg("Configure SAML Provider")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
value=${ifDefined(provider?.name)}
|
||||
required
|
||||
label=${msg("Name")}
|
||||
.errorMessages=${errors?.name ?? []}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
.errorMessages=${errors?.authorizationFlow ?? []}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="acsUrl"
|
||||
value=${ifDefined(provider?.acsUrl)}
|
||||
required
|
||||
label=${msg("ACS URL")}
|
||||
.errorMessages=${errors?.acsUrl ?? []}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="issuer"
|
||||
value=${provider?.issuer || "authentik"}
|
||||
required
|
||||
label=${msg("Issuer")}
|
||||
help=${msg("Also known as EntityID.")}
|
||||
.errorMessages=${errors?.issuer ?? []}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-radio-input
|
||||
name="spBinding"
|
||||
label=${msg("Service Provider Binding")}
|
||||
required
|
||||
.options=${spBindingOptions}
|
||||
.value=${provider?.spBinding}
|
||||
help=${msg(
|
||||
"Determines how authentik sends the response back to the Service Provider.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-text-input
|
||||
name="audience"
|
||||
value=${ifDefined(provider?.audience)}
|
||||
label=${msg("Audience")}
|
||||
.errorMessages=${errors?.audience ?? []}
|
||||
></ak-text-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
name="authenticationFlow"
|
||||
label=${msg("Authentication flow")}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
defaultFlowSlug="default-provider-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Signing Certificate")}
|
||||
name="signingKp"
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.signingKp ?? undefined)}
|
||||
@input=${(ev: InputEvent) => {
|
||||
const target = ev.target as AkCryptoCertificateSearch;
|
||||
if (!target) return;
|
||||
this.hasSigningKp = !!target.selectedKeypair;
|
||||
}}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Certificate used to sign outgoing Responses going to the Service Provider.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
${this.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}
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Verification Certificate")}
|
||||
name="verificationKp"
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.verificationKp ?? undefined)}
|
||||
nokey
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Encryption Certificate")}
|
||||
name="encryptionKp"
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.encryptionKp ?? undefined)}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When selected, encrypted assertions will be decrypted using this keypair.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-multi-select
|
||||
label=${msg("Property Mappings")}
|
||||
name="propertyMappings"
|
||||
.options=${propertyPairs}
|
||||
.values=${pmValues}
|
||||
.richhelp=${html` <p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings used for user mapping.")}
|
||||
</p>`}
|
||||
></ak-multi-select>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("NameID Property Mapping")}
|
||||
name="nameIdMapping"
|
||||
>
|
||||
<ak-saml-property-mapping-search
|
||||
name="nameIdMapping"
|
||||
propertymapping=${ifDefined(provider?.nameIdMapping ?? undefined)}
|
||||
></ak-saml-property-mapping-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-text-input
|
||||
name="assertionValidNotBefore"
|
||||
value=${provider?.assertionValidNotBefore || "minutes=-5"}
|
||||
required
|
||||
label=${msg("Assertion valid not before")}
|
||||
help=${msg(
|
||||
"Configure the maximum allowed time drift for an assertion.",
|
||||
)}
|
||||
.errorMessages=${errors?.assertionValidNotBefore ?? []}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="assertionValidNotOnOrAfter"
|
||||
value=${provider?.assertionValidNotOnOrAfter || "minutes=5"}
|
||||
required
|
||||
label=${msg("Assertion valid not on or after")}
|
||||
help=${msg(
|
||||
"Assertion not valid on or after current time + this value.",
|
||||
)}
|
||||
.errorMessages=${errors?.assertionValidNotOnOrAfter ?? []}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="sessionValidNotOnOrAfter"
|
||||
value=${provider?.sessionValidNotOnOrAfter || "minutes=86400"}
|
||||
required
|
||||
label=${msg("Session valid not on or after")}
|
||||
help=${msg("Session not valid on or after current time + this value.")}
|
||||
.errorMessages=${errors?.sessionValidNotOnOrAfter ?? []}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-radio-input
|
||||
name="digestAlgorithm"
|
||||
label=${msg("Digest algorithm")}
|
||||
required
|
||||
.options=${digestAlgorithmOptions}
|
||||
.value=${provider?.digestAlgorithm}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-radio-input
|
||||
name="signatureAlgorithm"
|
||||
label=${msg("Signature algorithm")}
|
||||
required
|
||||
.options=${signatureAlgorithmOptions}
|
||||
.value=${provider?.signatureAlgorithm}
|
||||
>
|
||||
</ak-radio-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
${renderForm(
|
||||
(this.wizard.provider as SAMLProvider) ?? {},
|
||||
this.wizard.errors.provider,
|
||||
setHasSigningKp,
|
||||
this.hasSigningKp,
|
||||
)}
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,10 @@
|
||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
||||
import "@goauthentik/admin/common/ak-core-group-search";
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-multi-select";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { renderForm } from "@goauthentik/admin/providers/scim/SCIMProviderFormForm.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement, state } from "@lit/reactive-element/decorators.js";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { PaginatedSCIMMappingList, PropertymappingsApi, type SCIMProvider } from "@goauthentik/api";
|
||||
import { PaginatedSCIMMappingList, type SCIMProvider } from "@goauthentik/api";
|
||||
|
||||
import BaseProviderPanel from "../BaseProviderPanel";
|
||||
|
||||
@ -26,125 +15,15 @@ export class ApplicationWizardAuthenticationBySCIM extends BaseProviderPanel {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
new PropertymappingsApi(DEFAULT_CONFIG)
|
||||
.propertymappingsProviderScimList({
|
||||
ordering: "managed",
|
||||
})
|
||||
.then((propertyMappings: PaginatedSCIMMappingList) => {
|
||||
this.propertyMappings = propertyMappings;
|
||||
});
|
||||
}
|
||||
|
||||
propertyMappingConfiguration(provider?: SCIMProvider) {
|
||||
const propertyMappings = this.propertyMappings?.results ?? [];
|
||||
|
||||
const configuredMappings = (providerMappings: string[]) =>
|
||||
propertyMappings.map((pm) => pm.pk).filter((pmpk) => providerMappings.includes(pmpk));
|
||||
|
||||
const managedMappings = (key: string) =>
|
||||
propertyMappings
|
||||
.filter((pm) => pm.managed === `goauthentik.io/providers/scim/${key}`)
|
||||
.map((pm) => pm.pk);
|
||||
|
||||
const pmUserValues = provider?.propertyMappings
|
||||
? configuredMappings(provider?.propertyMappings ?? [])
|
||||
: managedMappings("user");
|
||||
|
||||
const pmGroupValues = provider?.propertyMappingsGroup
|
||||
? configuredMappings(provider?.propertyMappingsGroup ?? [])
|
||||
: managedMappings("group");
|
||||
|
||||
const propertyPairs = propertyMappings.map((pm) => [pm.pk, pm.name]);
|
||||
|
||||
return { pmUserValues, pmGroupValues, propertyPairs };
|
||||
}
|
||||
|
||||
render() {
|
||||
const provider = this.wizard.provider as SCIMProvider | undefined;
|
||||
const errors = this.wizard.errors.provider;
|
||||
|
||||
const { pmUserValues, pmGroupValues, propertyPairs } =
|
||||
this.propertyMappingConfiguration(provider);
|
||||
|
||||
return html`<ak-wizard-title>${msg("Configure SCIM Provider")}</ak-wizard-title>
|
||||
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
label=${msg("Name")}
|
||||
value=${ifDefined(provider?.name)}
|
||||
.errorMessages=${errors?.name ?? []}
|
||||
required
|
||||
></ak-text-input>
|
||||
<ak-form-group expanded>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="url"
|
||||
label=${msg("URL")}
|
||||
value="${first(provider?.url, "")}"
|
||||
required
|
||||
help=${msg("SCIM base url, usually ends in /v2.")}
|
||||
.errorMessages=${errors?.url ?? []}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-text-input
|
||||
name="token"
|
||||
label=${msg("Token")}
|
||||
value="${first(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-switch-input
|
||||
name="excludeUsersServiceAccount"
|
||||
?checked=${first(provider?.excludeUsersServiceAccount, true)}
|
||||
label=${msg("Exclude service accounts")}
|
||||
></ak-switch-input>
|
||||
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
|
||||
<ak-core-group-search
|
||||
.group=${provider?.filterGroup}
|
||||
></ak-core-group-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Only sync users within the selected group.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group ?expanded=${true}>
|
||||
<span slot="header"> ${msg("Attribute mapping")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-multi-select
|
||||
label=${msg("User Property Mappings")}
|
||||
name="propertyMappings"
|
||||
.options=${propertyPairs}
|
||||
.values=${pmUserValues}
|
||||
.richhelp=${html`
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings used for user mapping.")}
|
||||
</p>
|
||||
`}
|
||||
></ak-multi-select>
|
||||
<ak-multi-select
|
||||
label=${msg("Group Property Mappings")}
|
||||
name="propertyMappingsGroup"
|
||||
.options=${propertyPairs}
|
||||
.values=${pmGroupValues}
|
||||
.richhelp=${html`
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings used for group creation.")}
|
||||
</p>
|
||||
`}
|
||||
></ak-multi-select>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
${renderForm(
|
||||
(this.wizard.provider as SCIMProvider) ?? {},
|
||||
this.wizard.errors.provider,
|
||||
)}
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
type LDAPProviderRequest,
|
||||
type OAuth2ProviderRequest,
|
||||
type ProvidersSamlImportMetadataCreateRequest,
|
||||
ProxyMode,
|
||||
type ProxyProviderRequest,
|
||||
type RACProviderRequest,
|
||||
type RadiusProviderRequest,
|
||||
@ -27,6 +28,7 @@ export interface ApplicationWizardState {
|
||||
providerModel: string;
|
||||
app: Partial<ApplicationRequest>;
|
||||
provider: OneOfProvider;
|
||||
proxyMode?: ProxyMode;
|
||||
errors: ValidationError;
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
|
@ -7,6 +7,7 @@ import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"
|
||||
|
||||
import { html } from "lit";
|
||||
import { property, query } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { FlowsApi, FlowsInstancesListDesignationEnum } from "@goauthentik/api";
|
||||
import type { Flow, FlowsInstancesListRequest } from "@goauthentik/api";
|
||||
@ -133,7 +134,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
|
||||
.renderElement=${renderElement}
|
||||
.renderDescription=${renderDescription}
|
||||
.value=${getFlowValue}
|
||||
.name=${this.name}
|
||||
name=${ifDefined(this.name ?? undefined)}
|
||||
@ak-change=${this.handleSearchUpdate}
|
||||
?blankable=${!this.required}
|
||||
>
|
||||
|
@ -43,8 +43,11 @@ export class ProviderWizard extends AKElement {
|
||||
@query("ak-wizard")
|
||||
wizard?: Wizard;
|
||||
|
||||
async firstUpdated(): Promise<void> {
|
||||
this.providerTypes = await new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList();
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => {
|
||||
this.providerTypes = providerTypes;
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
@ -58,6 +61,7 @@ export class ProviderWizard extends AKElement {
|
||||
}}
|
||||
>
|
||||
<ak-wizard-page-type-create
|
||||
name="selectProviderType"
|
||||
slot="initial"
|
||||
layout=${TypeCreateWizardPageLayouts.grid}
|
||||
.types=${this.providerTypes}
|
||||
|
@ -2,24 +2,17 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
LDAPAPIAccessMode,
|
||||
LDAPProvider,
|
||||
ProvidersApi,
|
||||
} from "@goauthentik/api";
|
||||
import { LDAPProvider, ProvidersApi } from "@goauthentik/api";
|
||||
|
||||
import { renderForm } from "./LDAPProviderFormForm.js";
|
||||
|
||||
@customElement("ak-provider-ldap-form")
|
||||
export class LDAPProviderFormPage extends WithBrandConfig(BaseProviderForm<LDAPProvider>) {
|
||||
@ -42,212 +35,8 @@ export class LDAPProviderFormPage extends WithBrandConfig(BaseProviderForm<LDAPP
|
||||
}
|
||||
}
|
||||
|
||||
// All Provider objects have an Authorization flow, but not all providers have an Authentication
|
||||
// flow. LDAP needs only one field, but it is not an Authorization field, it is an
|
||||
// Authentication field. So, yeah, we're using the authorization field to store the
|
||||
// authentication information, which is why the ak-branded-flow-search call down there looks so
|
||||
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
|
||||
// field of the target Provider.
|
||||
renderForm(): TemplateResult {
|
||||
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Bind mode")} name="bindMode">
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
label: msg("Cached binding"),
|
||||
value: LDAPAPIAccessMode.Cached,
|
||||
default: true,
|
||||
description: html`${msg(
|
||||
"Flow is executed and session is cached in memory. Flow is executed when session expires",
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
label: msg("Direct binding"),
|
||||
value: LDAPAPIAccessMode.Direct,
|
||||
description: html`${msg(
|
||||
"Always execute the configured bind flow to authenticate the user",
|
||||
)}`,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.bindMode}
|
||||
>
|
||||
</ak-radio>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Configure how the outpost authenticates requests.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Search mode")} name="searchMode">
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
label: msg("Cached querying"),
|
||||
value: LDAPAPIAccessMode.Cached,
|
||||
default: true,
|
||||
description: html`${msg(
|
||||
"The outpost holds all users and groups in-memory and will refresh every 5 Minutes",
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
label: msg("Direct querying"),
|
||||
value: LDAPAPIAccessMode.Direct,
|
||||
description: html`${msg(
|
||||
"Always returns the latest data, but slower than cached querying",
|
||||
)}`,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.searchMode}
|
||||
>
|
||||
</ak-radio>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Configure how the outpost queries the core authentik server's users.")}
|
||||
</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=${first(this.instance?.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">
|
||||
${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.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group expanded>
|
||||
<span slot="header"> ${msg("Flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Bind flow")}
|
||||
name="authorizationFlow"
|
||||
required
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
.brandFlow=${this.brand?.flowAuthentication}
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used for users to authenticate.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Unbind flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${this.instance?.invalidationFlow}
|
||||
.brandFlow=${this.brand.flowInvalidation}
|
||||
defaultFlowSlug="default-invalidation-flow"
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used for unbinding users.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<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("Base DN")}
|
||||
?required=${true}
|
||||
name="baseDn"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"LDAP DN under which bind requests and search requests can be made.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
|
||||
<ak-crypto-certificate-search
|
||||
.certificate=${this.instance?.certificate}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"The certificate for the above configured Base DN. As a fallback, the provider uses a self-signed certificate.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("TLS Server name")}
|
||||
name="tlsServerName"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.tlsServerName, "")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"DNS name for which the above configured certificate should be used. The certificate cannot be detected based on the base DN, as the SSL/TLS negotiation happens before such data is exchanged.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("UID start number")}
|
||||
?required=${true}
|
||||
name="uidStartNumber"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.uidStartNumber, 2000)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("GID start number")}
|
||||
?required=${true}
|
||||
name="gidStartNumber"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
value="${first(this.instance?.gidStartNumber, 4000)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
renderForm() {
|
||||
return renderForm(this.instance ?? {}, [], this.brand);
|
||||
}
|
||||
}
|
||||
|
||||
|
178
web/src/admin/providers/ldap/LDAPProviderFormForm.ts
Normal file
178
web/src/admin/providers/ldap/LDAPProviderFormForm.ts
Normal file
@ -0,0 +1,178 @@
|
||||
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";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
CurrentBrand,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
LDAPProvider,
|
||||
ValidationError,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import {
|
||||
bindModeOptions,
|
||||
cryptoCertificateHelp,
|
||||
gidStartNumberHelp,
|
||||
mfaSupportHelp,
|
||||
searchModeOptions,
|
||||
tlsServerNameHelp,
|
||||
uidStartNumberHelp,
|
||||
} from "./LDAPOptionsAndHelp.js";
|
||||
|
||||
// All Provider objects have an Authorization flow, but not all providers have an Authentication
|
||||
// flow. LDAP needs only one field, but it is not an Authorization field, it is an Authentication
|
||||
// field. So, yeah, we're using the authorization field to store the authentication information,
|
||||
// which is why the ak-branded-flow-search call down there looks so weird-- we're looking up
|
||||
// Authentication flows, but we're storing them in the Authorization field of the target Provider.
|
||||
|
||||
export function renderForm(
|
||||
provider?: Partial<LDAPProvider>,
|
||||
errors: ValidationError = {},
|
||||
brand?: CurrentBrand,
|
||||
) {
|
||||
return html`
|
||||
<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-radio-input
|
||||
label=${msg("Bind mode")}
|
||||
name="bindMode"
|
||||
.options=${bindModeOptions}
|
||||
.value=${provider?.bindMode}
|
||||
help=${msg("Configure how the outpost authenticates requests.")}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-radio-input
|
||||
label=${msg("Search mode")}
|
||||
name="searchMode"
|
||||
.options=${searchModeOptions}
|
||||
.value=${provider?.searchMode}
|
||||
help=${msg("Configure how the outpost queries the core authentik server's users.")}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<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("Flow settings")} </span>
|
||||
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Bind flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
.errorMessages=${errors?.authorizationFlow ?? []}
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
.brandFlow=${brand?.flowAuthentication}
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used for users to authenticate.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Unbind flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
.brandFlow=${brand?.flowInvalidation}
|
||||
defaultFlowSlug="default-invalidation-flow"
|
||||
.errorMessages=${errors?.invalidationFlow ?? []}
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Flow used for unbinding users.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group expanded>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="baseDn"
|
||||
label=${msg("Base DN")}
|
||||
required
|
||||
value="${provider?.baseDn ?? "DC=ldap,DC=goauthentik,DC=io"}"
|
||||
.errorMessages=${errors?.baseDn ?? []}
|
||||
help=${msg(
|
||||
"LDAP DN under which bind requests and search requests can be made.",
|
||||
)}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Certificate")}
|
||||
name="certificate"
|
||||
.errorMessages=${errors?.certificate ?? []}
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.certificate ?? nothing)}
|
||||
name="certificate"
|
||||
>
|
||||
</ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">${cryptoCertificateHelp}</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-text-input
|
||||
label=${msg("TLS Server name")}
|
||||
name="tlsServerName"
|
||||
value="${provider?.tlsServerName ?? ""}"
|
||||
.errorMessages=${errors?.tlsServerName ?? []}
|
||||
help=${tlsServerNameHelp}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-number-input
|
||||
label=${msg("UID start number")}
|
||||
required
|
||||
name="uidStartNumber"
|
||||
value="${provider?.uidStartNumber ?? 2000}"
|
||||
.errorMessages=${errors?.uidStartNumber ?? []}
|
||||
help=${uidStartNumberHelp}
|
||||
></ak-number-input>
|
||||
|
||||
<ak-number-input
|
||||
label=${msg("GID start number")}
|
||||
required
|
||||
name="gidStartNumber"
|
||||
value="${provider?.gidStartNumber ?? 4000}"
|
||||
.errorMessages=${errors?.gidStartNumber ?? []}
|
||||
help=${gidStartNumberHelp}
|
||||
></ak-number-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
}
|
@ -1,12 +1,5 @@
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
||||
import {
|
||||
IRedirectURIInput,
|
||||
akOAuthRedirectURIInput,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/components/ak-textarea-input";
|
||||
@ -19,105 +12,12 @@ import "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, css, html } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
ClientTypeEnum,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
IssuerModeEnum,
|
||||
MatchingModeEnum,
|
||||
OAuth2Provider,
|
||||
ProvidersApi,
|
||||
RedirectURI,
|
||||
SubModeEnum,
|
||||
} from "@goauthentik/api";
|
||||
import { ClientTypeEnum, OAuth2Provider, ProvidersApi } from "@goauthentik/api";
|
||||
|
||||
import {
|
||||
makeOAuth2PropertyMappingsSelector,
|
||||
oauth2PropertyMappingsProvider,
|
||||
} from "./OAuth2PropertyMappings.js";
|
||||
import { makeSourceSelector, oauth2SourcesProvider } from "./OAuth2Sources.js";
|
||||
|
||||
export const clientTypeOptions = [
|
||||
{
|
||||
label: msg("Confidential"),
|
||||
value: ClientTypeEnum.Confidential,
|
||||
default: true,
|
||||
description: html`${msg(
|
||||
"Confidential clients are capable of maintaining the confidentiality of their credentials such as client secrets",
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
label: msg("Public"),
|
||||
value: ClientTypeEnum.Public,
|
||||
description: html`${msg(
|
||||
"Public clients are incapable of maintaining the confidentiality and should use methods like PKCE. ",
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
|
||||
export const subjectModeOptions = [
|
||||
{
|
||||
label: msg("Based on the User's hashed ID"),
|
||||
value: SubModeEnum.HashedUserId,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: msg("Based on the User's ID"),
|
||||
value: SubModeEnum.UserId,
|
||||
},
|
||||
{
|
||||
label: msg("Based on the User's UUID"),
|
||||
value: SubModeEnum.UserUuid,
|
||||
},
|
||||
{
|
||||
label: msg("Based on the User's username"),
|
||||
value: SubModeEnum.UserUsername,
|
||||
},
|
||||
{
|
||||
label: msg("Based on the User's Email"),
|
||||
value: SubModeEnum.UserEmail,
|
||||
description: html`${msg("This is recommended over the UPN mode.")}`,
|
||||
},
|
||||
{
|
||||
label: msg("Based on the User's UPN"),
|
||||
value: SubModeEnum.UserUpn,
|
||||
description: html`${msg(
|
||||
"Requires the user to have a 'upn' attribute set, and falls back to hashed user ID. Use this mode only if you have different UPN and Mail domains.",
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
|
||||
export const issuerModeOptions = [
|
||||
{
|
||||
label: msg("Each provider has a different issuer, based on the application slug"),
|
||||
value: IssuerModeEnum.PerProvider,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: msg("Same identifier is used for all providers"),
|
||||
value: IssuerModeEnum.Global,
|
||||
},
|
||||
];
|
||||
|
||||
const redirectUriHelpMessages = [
|
||||
msg(
|
||||
"Valid redirect URIs after a successful authorization flow. Also specify any origins here for Implicit flows.",
|
||||
),
|
||||
msg(
|
||||
"If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
|
||||
),
|
||||
msg(
|
||||
'To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.',
|
||||
),
|
||||
];
|
||||
|
||||
export const redirectUriHelp = html`${redirectUriHelpMessages.map(
|
||||
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
|
||||
)}`;
|
||||
import { renderForm } from "./OAuth2ProviderFormForm.js";
|
||||
|
||||
/**
|
||||
* Form page for OAuth2 Authentication Method
|
||||
@ -131,9 +31,6 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
|
||||
@state()
|
||||
showClientSecret = true;
|
||||
|
||||
@state()
|
||||
redirectUris: RedirectURI[] = [];
|
||||
|
||||
static get styles() {
|
||||
return super.styles.concat(css`
|
||||
ak-array-input {
|
||||
@ -147,7 +44,6 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
|
||||
id: pk,
|
||||
});
|
||||
this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential;
|
||||
this.redirectUris = provider.redirectUris;
|
||||
return provider;
|
||||
}
|
||||
|
||||
@ -164,245 +60,11 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
|
||||
}
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
const provider = this.instance;
|
||||
|
||||
return html` <ak-text-input
|
||||
name="name"
|
||||
label=${msg("Name")}
|
||||
value=${ifDefined(provider?.name)}
|
||||
required
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
name="authorizationFlow"
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-group expanded>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-radio-input
|
||||
name="clientType"
|
||||
label=${msg("Client type")}
|
||||
.value=${provider?.clientType}
|
||||
required
|
||||
@change=${(ev: CustomEvent<{ value: ClientTypeEnum }>) => {
|
||||
this.showClientSecret = ev.detail.value !== ClientTypeEnum.Public;
|
||||
}}
|
||||
.options=${clientTypeOptions}
|
||||
>
|
||||
</ak-radio-input>
|
||||
<ak-text-input
|
||||
name="clientId"
|
||||
label=${msg("Client ID")}
|
||||
value="${first(
|
||||
provider?.clientId,
|
||||
randomString(40, ascii_letters + digits),
|
||||
)}"
|
||||
required
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-text-input
|
||||
name="clientSecret"
|
||||
label=${msg("Client Secret")}
|
||||
value="${first(
|
||||
provider?.clientSecret,
|
||||
randomString(128, ascii_letters + digits),
|
||||
)}"
|
||||
?hidden=${!this.showClientSecret}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Redirect URIs/Origins")}
|
||||
required
|
||||
name="redirectUris"
|
||||
>
|
||||
<ak-array-input
|
||||
.items=${this.instance?.redirectUris ?? []}
|
||||
.newItem=${() => ({ matchingMode: MatchingModeEnum.Strict, url: "" })}
|
||||
.row=${(f?: RedirectURI) =>
|
||||
akOAuthRedirectURIInput({
|
||||
".redirectURI": f,
|
||||
"style": "width: 100%",
|
||||
"name": "oauth2-redirect-uri",
|
||||
} as unknown as IRedirectURIInput)}
|
||||
>
|
||||
</ak-array-input>
|
||||
${redirectUriHelp}
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
|
||||
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(this.instance?.signingKey ?? undefined)}
|
||||
singleton
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Key used to sign the tokens.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Encryption Key")} name="encryptionKey">
|
||||
<!-- NOTE: 'null' cast to 'undefined' on encryptionKey to satisfy Lit requirements -->
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(this.instance?.encryptionKey ?? undefined)}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Key used to encrypt the tokens.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
name="authenticationFlow"
|
||||
label=${msg("Authentication flow")}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
defaultFlowSlug="default-provider-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="accessCodeValidity"
|
||||
label=${msg("Access code validity")}
|
||||
required
|
||||
value="${first(provider?.accessCodeValidity, "minutes=1")}"
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg("Configure how long access codes are valid for.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-text-input
|
||||
name="accessTokenValidity"
|
||||
label=${msg("Access Token validity")}
|
||||
value="${first(provider?.accessTokenValidity, "minutes=5")}"
|
||||
required
|
||||
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||
${msg("Configure how long access tokens are valid for.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="refreshTokenValidity"
|
||||
label=${msg("Refresh Token validity")}
|
||||
value="${first(provider?.refreshTokenValidity, "days=30")}"
|
||||
?required=${true}
|
||||
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||
${msg("Configure how long refresh tokens are valid for.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-form-element-horizontal label=${msg("Scopes")} name="propertyMappings">
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${oauth2PropertyMappingsProvider}
|
||||
.selector=${makeOAuth2PropertyMappingsSelector(
|
||||
provider?.propertyMappings,
|
||||
)}
|
||||
available-label=${msg("Available Scopes")}
|
||||
selected-label=${msg("Selected Scopes")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Select which scopes can be used by the client. The client still has to specify the scope to access the data.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-radio-input
|
||||
name="subMode"
|
||||
label=${msg("Subject mode")}
|
||||
required
|
||||
.options=${subjectModeOptions}
|
||||
.value=${provider?.subMode}
|
||||
help=${msg(
|
||||
"Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
<ak-switch-input
|
||||
name="includeClaimsInIdToken"
|
||||
label=${msg("Include claims in id_token")}
|
||||
?checked=${first(provider?.includeClaimsInIdToken, true)}
|
||||
help=${msg(
|
||||
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
|
||||
)}
|
||||
></ak-switch-input>
|
||||
<ak-radio-input
|
||||
name="issuerMode"
|
||||
label=${msg("Issuer mode")}
|
||||
required
|
||||
.options=${issuerModeOptions}
|
||||
.value=${provider?.issuerMode}
|
||||
help=${msg(
|
||||
"Configure how the issuer field of the ID Token should be filled.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header">${msg("Machine-to-Machine authentication settings")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Trusted OIDC Sources")}
|
||||
name="jwksSources"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${oauth2SourcesProvider}
|
||||
.selector=${makeSourceSelector(provider?.jwksSources)}
|
||||
available-label=${msg("Available Sources")}
|
||||
selected-label=${msg("Selected Sources")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
renderForm() {
|
||||
const showClientSecretCallback = (show: boolean) => {
|
||||
this.showClientSecret = show;
|
||||
};
|
||||
return renderForm(this.instance ?? {}, [], this.showClientSecret, showClientSecretCallback);
|
||||
}
|
||||
}
|
||||
|
||||
|
350
web/src/admin/providers/oauth2/OAuth2ProviderFormForm.ts
Normal file
350
web/src/admin/providers/oauth2/OAuth2ProviderFormForm.ts
Normal file
@ -0,0 +1,350 @@
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import {
|
||||
IRedirectURIInput,
|
||||
akOAuthRedirectURIInput,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
|
||||
import { ascii_letters, digits, randomString } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/components/ak-textarea-input";
|
||||
import "@goauthentik/elements/ak-array-input.js";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
ClientTypeEnum,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
IssuerModeEnum,
|
||||
MatchingModeEnum,
|
||||
OAuth2Provider,
|
||||
RedirectURI,
|
||||
SubModeEnum,
|
||||
ValidationError,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import {
|
||||
makeOAuth2PropertyMappingsSelector,
|
||||
oauth2PropertyMappingsProvider,
|
||||
} from "./OAuth2PropertyMappings.js";
|
||||
import { makeSourceSelector, oauth2SourcesProvider } from "./OAuth2Sources.js";
|
||||
|
||||
export const clientTypeOptions = [
|
||||
{
|
||||
label: msg("Confidential"),
|
||||
value: ClientTypeEnum.Confidential,
|
||||
default: true,
|
||||
description: html`${msg(
|
||||
"Confidential clients are capable of maintaining the confidentiality of their credentials such as client secrets",
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
label: msg("Public"),
|
||||
value: ClientTypeEnum.Public,
|
||||
description: html`${msg(
|
||||
"Public clients are incapable of maintaining the confidentiality and should use methods like PKCE. ",
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
|
||||
export const subjectModeOptions = [
|
||||
{
|
||||
label: msg("Based on the User's hashed ID"),
|
||||
value: SubModeEnum.HashedUserId,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: msg("Based on the User's ID"),
|
||||
value: SubModeEnum.UserId,
|
||||
},
|
||||
{
|
||||
label: msg("Based on the User's UUID"),
|
||||
value: SubModeEnum.UserUuid,
|
||||
},
|
||||
{
|
||||
label: msg("Based on the User's username"),
|
||||
value: SubModeEnum.UserUsername,
|
||||
},
|
||||
{
|
||||
label: msg("Based on the User's Email"),
|
||||
value: SubModeEnum.UserEmail,
|
||||
description: html`${msg("This is recommended over the UPN mode.")}`,
|
||||
},
|
||||
{
|
||||
label: msg("Based on the User's UPN"),
|
||||
value: SubModeEnum.UserUpn,
|
||||
description: html`${msg(
|
||||
"Requires the user to have a 'upn' attribute set, and falls back to hashed user ID. Use this mode only if you have different UPN and Mail domains.",
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
|
||||
export const issuerModeOptions = [
|
||||
{
|
||||
label: msg("Each provider has a different issuer, based on the application slug"),
|
||||
value: IssuerModeEnum.PerProvider,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: msg("Same identifier is used for all providers"),
|
||||
value: IssuerModeEnum.Global,
|
||||
},
|
||||
];
|
||||
|
||||
const redirectUriHelpMessages = [
|
||||
msg(
|
||||
"Valid redirect URIs after a successful authorization flow. Also specify any origins here for Implicit flows.",
|
||||
),
|
||||
msg(
|
||||
"If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
|
||||
),
|
||||
msg(
|
||||
'To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.',
|
||||
),
|
||||
];
|
||||
|
||||
export const redirectUriHelp = html`${redirectUriHelpMessages.map(
|
||||
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
|
||||
)}`;
|
||||
|
||||
type ShowClientSecret = (show: boolean) => void;
|
||||
const defaultShowClientSecret: ShowClientSecret = (_show) => undefined;
|
||||
|
||||
export function renderForm(
|
||||
provider: Partial<OAuth2Provider>,
|
||||
errors: ValidationError,
|
||||
showClientSecret = false,
|
||||
showClientSecretCallback: ShowClientSecret = defaultShowClientSecret,
|
||||
) {
|
||||
return html` <ak-text-input
|
||||
name="name"
|
||||
label=${msg("Name")}
|
||||
value=${ifDefined(provider?.name)}
|
||||
required
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
name="authorizationFlow"
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-group expanded>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-radio-input
|
||||
name="clientType"
|
||||
label=${msg("Client type")}
|
||||
.value=${provider?.clientType}
|
||||
required
|
||||
@change=${(ev: CustomEvent<{ value: ClientTypeEnum }>) => {
|
||||
showClientSecretCallback(ev.detail.value !== ClientTypeEnum.Public);
|
||||
}}
|
||||
.options=${clientTypeOptions}
|
||||
>
|
||||
</ak-radio-input>
|
||||
<ak-text-input
|
||||
name="clientId"
|
||||
label=${msg("Client ID")}
|
||||
value="${provider?.clientId ?? randomString(40, ascii_letters + digits)}"
|
||||
required
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-text-input
|
||||
name="clientSecret"
|
||||
label=${msg("Client Secret")}
|
||||
value="${provider?.clientSecret ?? randomString(128, ascii_letters + digits)}"
|
||||
?hidden=${!showClientSecret}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Redirect URIs/Origins")}
|
||||
required
|
||||
name="redirectUris"
|
||||
>
|
||||
<ak-array-input
|
||||
name="redirectUris"
|
||||
.items=${provider?.redirectUris ?? []}
|
||||
.newItem=${() => ({ matchingMode: MatchingModeEnum.Strict, url: "" })}
|
||||
.row=${(f?: RedirectURI) =>
|
||||
akOAuthRedirectURIInput({
|
||||
".redirectURI": f,
|
||||
"style": "width: 100%",
|
||||
"name": "oauth2-redirect-uri",
|
||||
} as unknown as IRedirectURIInput)}
|
||||
>
|
||||
</ak-array-input>
|
||||
${redirectUriHelp}
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
|
||||
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.signingKey ?? undefined)}
|
||||
singleton
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Key used to sign the tokens.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Encryption Key")} name="encryptionKey">
|
||||
<!-- NOTE: 'null' cast to 'undefined' on encryptionKey to satisfy Lit requirements -->
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.encryptionKey ?? undefined)}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Key used to encrypt the tokens.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
name="authenticationFlow"
|
||||
label=${msg("Authentication flow")}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
defaultFlowSlug="default-provider-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="accessCodeValidity"
|
||||
label=${msg("Access code validity")}
|
||||
required
|
||||
value="${provider?.accessCodeValidity ?? "minutes=1"}"
|
||||
.bighelp=${html`<p class="pf-c-form__helper-text">
|
||||
${msg("Configure how long access codes are valid for.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-text-input
|
||||
name="accessTokenValidity"
|
||||
label=${msg("Access Token validity")}
|
||||
value="${provider?.accessTokenValidity ?? "minutes=5"}"
|
||||
required
|
||||
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||
${msg("Configure how long access tokens are valid for.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="refreshTokenValidity"
|
||||
label=${msg("Refresh Token validity")}
|
||||
value="${provider?.refreshTokenValidity ?? "days=30"}"
|
||||
?required=${true}
|
||||
.bighelp=${html` <p class="pf-c-form__helper-text">
|
||||
${msg("Configure how long refresh tokens are valid for.")}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
<ak-form-element-horizontal label=${msg("Scopes")} name="propertyMappings">
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${oauth2PropertyMappingsProvider}
|
||||
.selector=${makeOAuth2PropertyMappingsSelector(provider?.propertyMappings)}
|
||||
available-label=${msg("Available Scopes")}
|
||||
selected-label=${msg("Selected Scopes")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Select which scopes can be used by the client. The client still has to specify the scope to access the data.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-radio-input
|
||||
name="subMode"
|
||||
label=${msg("Subject mode")}
|
||||
required
|
||||
.options=${subjectModeOptions}
|
||||
.value=${provider?.subMode}
|
||||
help=${msg(
|
||||
"Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
<ak-switch-input
|
||||
name="includeClaimsInIdToken"
|
||||
label=${msg("Include claims in id_token")}
|
||||
?checked=${provider?.includeClaimsInIdToken ?? true}
|
||||
help=${msg(
|
||||
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
|
||||
)}
|
||||
></ak-switch-input>
|
||||
<ak-radio-input
|
||||
name="issuerMode"
|
||||
label=${msg("Issuer mode")}
|
||||
required
|
||||
.options=${issuerModeOptions}
|
||||
.value=${provider?.issuerMode}
|
||||
help=${msg("Configure how the issuer field of the ID Token should be filled.")}
|
||||
>
|
||||
</ak-radio-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header">${msg("Machine-to-Machine authentication settings")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal label=${msg("Trusted OIDC Sources")} name="jwksSources">
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${oauth2SourcesProvider}
|
||||
.selector=${makeSourceSelector(provider?.jwksSources)}
|
||||
available-label=${msg("Available Sources")}
|
||||
selected-label=${msg("Selected Sources")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
}
|
@ -1,39 +1,18 @@
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
||||
import {
|
||||
makeSourceSelector,
|
||||
oauth2SourcesProvider,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-toggle-group";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { CSSResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
|
||||
|
||||
import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
ProvidersApi,
|
||||
ProxyMode,
|
||||
ProxyProvider,
|
||||
} from "@goauthentik/api";
|
||||
import { ProvidersApi, ProxyMode, ProxyProvider } from "@goauthentik/api";
|
||||
|
||||
import {
|
||||
makeProxyPropertyMappingsSelector,
|
||||
proxyPropertyMappingsProvider,
|
||||
} from "./ProxyProviderPropertyMappings.js";
|
||||
import { SetMode, SetShowHttpBasic, renderForm } from "./ProxyProviderFormForm.js";
|
||||
|
||||
@customElement("ak-provider-proxy-form")
|
||||
export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
|
||||
@ -45,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;
|
||||
}
|
||||
|
||||
@ -73,376 +52,22 @@ export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
|
||||
}
|
||||
}
|
||||
|
||||
renderHttpBasic(): TemplateResult {
|
||||
return html`<ak-text-input
|
||||
name="basicAuthUserAttribute"
|
||||
label=${msg("HTTP-Basic Username Key")}
|
||||
value="${ifDefined(this.instance?.basicAuthUserAttribute)}"
|
||||
help=${msg(
|
||||
"User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
|
||||
)}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="basicAuthPasswordAttribute"
|
||||
label=${msg("HTTP-Basic Password Key")}
|
||||
value="${ifDefined(this.instance?.basicAuthPasswordAttribute)}"
|
||||
help=${msg(
|
||||
"User/Group Attribute used for the password part of the HTTP-Basic Header.",
|
||||
)}
|
||||
>
|
||||
</ak-text-input>`;
|
||||
}
|
||||
|
||||
renderModeSelector(): TemplateResult {
|
||||
const setMode = (ev: CustomEvent<{ value: ProxyMode }>) => {
|
||||
renderForm() {
|
||||
const onSetMode: SetMode = (ev) => {
|
||||
this.mode = ev.detail.value;
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
<ak-toggle-group value=${this.mode} @ak-toggle=${setMode}>
|
||||
<option value=${ProxyMode.Proxy}>${msg("Proxy")}</option>
|
||||
<option value=${ProxyMode.ForwardSingle}>${msg("Forward auth (single application)")}</option>
|
||||
<option value=${ProxyMode.ForwardDomain}>${msg("Forward auth (domain level)")}</option>
|
||||
</ak-toggle-group>
|
||||
`;
|
||||
}
|
||||
const onSetShowHttpBasic: SetShowHttpBasic = (ev: Event) => {
|
||||
const el = ev.target as HTMLInputElement;
|
||||
this.showHttpBasic = el.checked;
|
||||
};
|
||||
|
||||
renderSettings(): TemplateResult {
|
||||
switch (this.mode) {
|
||||
case ProxyMode.Proxy:
|
||||
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=${true}
|
||||
name="externalHost"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.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=${true}
|
||||
name="internalHost"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.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=${first(this.instance?.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>`;
|
||||
case ProxyMode.ForwardSingle:
|
||||
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=${true}
|
||||
name="externalHost"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.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>`;
|
||||
case ProxyMode.ForwardDomain:
|
||||
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.",
|
||||
)}
|
||||
</p>
|
||||
<div class="pf-u-mb-xl">
|
||||
${msg("An example setup can look like this:")}
|
||||
<ul class="pf-c-list">
|
||||
<li>${msg("authentik running on auth.example.com")}</li>
|
||||
<li>${msg("app1 running on app1.example.com")}</li>
|
||||
</ul>
|
||||
${msg(
|
||||
"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=${true}
|
||||
name="externalHost"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.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=${true}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.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>`;
|
||||
case ProxyMode.UnknownDefaultOpenApi:
|
||||
return html`<p>${msg("Unknown proxy mode")}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`
|
||||
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
required
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<div class="pf-c-card pf-m-selectable pf-m-selected">
|
||||
<div class="pf-c-card__body">${this.renderModeSelector()}</div>
|
||||
<div class="pf-c-card__footer">${this.renderSettings()}</div>
|
||||
</div>
|
||||
<ak-form-element-horizontal label=${msg("Token validity")} name="accessTokenValidity">
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.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-form-group>
|
||||
<span slot="header">${msg("Advanced protocol settings")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
|
||||
<ak-crypto-certificate-search
|
||||
.certificate=${this.instance?.certificate}
|
||||
></ak-crypto-certificate-search>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Additional scopes")}
|
||||
name="propertyMappings"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${proxyPropertyMappingsProvider}
|
||||
.selector=${makeProxyPropertyMappingsSelector(
|
||||
this.instance?.propertyMappings,
|
||||
)}
|
||||
available-label="${msg("Available Scopes")}"
|
||||
selected-label="${msg("Selected Scopes")}"
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Additional scope mappings, which are passed to the proxy.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label="${this.mode === ProxyMode.ForwardDomain
|
||||
? msg("Unauthenticated URLs")
|
||||
: msg("Unauthenticated Paths")}"
|
||||
name="skipPathRegex"
|
||||
>
|
||||
<textarea class="pf-c-form-control">
|
||||
${this.instance?.skipPathRegex}</textarea
|
||||
>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.",
|
||||
)}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<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=${first(this.instance?.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=${first(this.instance?.basicAuthEnabled, false)}
|
||||
@change=${(ev: Event) => {
|
||||
const el = ev.target as HTMLInputElement;
|
||||
this.showHttpBasic = el.checked;
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
${this.showHttpBasic ? this.renderHttpBasic() : html``}
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Trusted OIDC Sources")}
|
||||
name="jwksSources"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${oauth2SourcesProvider}
|
||||
.selector=${makeSourceSelector(this.instance?.jwksSources)}
|
||||
available-label=${msg("Available Sources")}
|
||||
selected-label=${msg("Selected Sources")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
?required=${false}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${this.instance?.invalidationFlow}
|
||||
defaultFlowSlug="default-provider-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
return renderForm(this.instance ?? {}, [], {
|
||||
mode: this.mode,
|
||||
onSetMode,
|
||||
showHttpBasic: this.showHttpBasic,
|
||||
onSetShowHttpBasic,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
343
web/src/admin/providers/proxy/ProxyProviderFormForm.ts
Normal file
343
web/src/admin/providers/proxy/ProxyProviderFormForm.ts
Normal file
@ -0,0 +1,343 @@
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import {
|
||||
makeSourceSelector,
|
||||
oauth2SourcesProvider,
|
||||
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
|
||||
import "@goauthentik/components/ak-toggle-group";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||
import { match } from "ts-pattern";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
ProxyMode,
|
||||
ProxyProvider,
|
||||
ValidationError,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import {
|
||||
makeProxyPropertyMappingsSelector,
|
||||
proxyPropertyMappingsProvider,
|
||||
} from "./ProxyProviderPropertyMappings.js";
|
||||
|
||||
export type ProxyModeValue = { value: ProxyMode };
|
||||
export type SetMode = (ev: CustomEvent<ProxyModeValue>) => void;
|
||||
export type SetShowHttpBasic = (ev: Event) => void;
|
||||
|
||||
export interface ProxyModeExtraArgs {
|
||||
mode: ProxyMode;
|
||||
onSetMode: SetMode;
|
||||
showHttpBasic: boolean;
|
||||
onSetShowHttpBasic: SetShowHttpBasic;
|
||||
}
|
||||
|
||||
function renderHttpBasic(provider: Partial<ProxyProvider>) {
|
||||
return html`<ak-text-input
|
||||
name="basicAuthUserAttribute"
|
||||
label=${msg("HTTP-Basic Username Key")}
|
||||
value="${ifDefined(provider?.basicAuthUserAttribute)}"
|
||||
help=${msg(
|
||||
"User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
|
||||
)}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="basicAuthPasswordAttribute"
|
||||
label=${msg("HTTP-Basic Password Key")}
|
||||
value="${ifDefined(provider?.basicAuthPasswordAttribute)}"
|
||||
help=${msg("User/Group Attribute used for the password part of the HTTP-Basic Header.")}
|
||||
>
|
||||
</ak-text-input>`;
|
||||
}
|
||||
|
||||
function renderModeSelector(mode: ProxyMode, onSet: SetMode) {
|
||||
// prettier-ignore
|
||||
return html` <ak-toggle-group
|
||||
value=${mode}
|
||||
@ak-toggle=${onSet}
|
||||
data-ouid-component-name="proxy-type-toggle"
|
||||
>
|
||||
<option value=${ProxyMode.Proxy}>${msg("Proxy")}</option>
|
||||
<option value=${ProxyMode.ForwardSingle}>${msg("Forward auth (single application)")}</option>
|
||||
<option value=${ProxyMode.ForwardDomain}>${msg("Forward auth (domain level)")}</option>
|
||||
</ak-toggle-group>`;
|
||||
}
|
||||
|
||||
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-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: 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-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: 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.",
|
||||
)}
|
||||
</p>
|
||||
<div class="pf-u-mb-xl">
|
||||
${msg("An example setup can look like this:")}
|
||||
<ul class="pf-c-list">
|
||||
<li>${msg("authentik running on auth.example.com")}</li>
|
||||
<li>${msg("app1 running on app1.example.com")}</li>
|
||||
</ul>
|
||||
${msg(
|
||||
"In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.",
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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> `;
|
||||
}
|
||||
|
||||
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))
|
||||
.otherwise(() => {
|
||||
throw new Error("Unrecognized proxy mode");
|
||||
});
|
||||
}
|
||||
|
||||
export function renderForm(
|
||||
provider: Partial<ProxyProvider> = {},
|
||||
errors: ValidationError = {},
|
||||
args: ProxyModeExtraArgs,
|
||||
) {
|
||||
const { mode, onSetMode, showHttpBasic, onSetShowHttpBasic } = args;
|
||||
|
||||
return html`
|
||||
<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
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<div class="pf-c-card pf-m-selectable pf-m-selected">
|
||||
<div class="pf-c-card__body">${renderModeSelector(mode, onSetMode)}</div>
|
||||
<div class="pf-c-card__footer">${renderSettings(provider, mode)}</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
|
||||
<ak-crypto-certificate-search
|
||||
.certificate=${provider?.certificate}
|
||||
></ak-crypto-certificate-search>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Additional scopes")}
|
||||
name="propertyMappings"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${proxyPropertyMappingsProvider}
|
||||
.selector=${makeProxyPropertyMappingsSelector(provider?.propertyMappings)}
|
||||
available-label="${msg("Available Scopes")}"
|
||||
selected-label="${msg("Selected Scopes")}"
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Additional scope mappings, which are passed to the proxy.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label="${mode === ProxyMode.ForwardDomain
|
||||
? msg("Unauthenticated URLs")
|
||||
: msg("Unauthenticated Paths")}"
|
||||
name="skipPathRegex"
|
||||
>
|
||||
<textarea class="pf-c-form-control">${provider?.skipPathRegex}</textarea>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.",
|
||||
)}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header">${msg("Authentication settings")}</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<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
|
||||
.provider=${oauth2SourcesProvider}
|
||||
.selector=${makeSourceSelector(provider?.jwksSources)}
|
||||
available-label=${msg("Available Sources")}
|
||||
selected-label=${msg("Selected Sources")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
defaultFlowSlug="default-provider-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
}
|
@ -1,48 +1,12 @@
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { ifDefined } from "lit-html/directives/if-defined.js";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
PropertymappingsApi,
|
||||
ProvidersApi,
|
||||
RadiusProvider,
|
||||
RadiusProviderPropertyMapping,
|
||||
} from "@goauthentik/api";
|
||||
import { ProvidersApi, RadiusProvider } from "@goauthentik/api";
|
||||
|
||||
export async function radiusPropertyMappingsProvider(page = 1, search = "") {
|
||||
const propertyMappings = await new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsProviderRadiusList({
|
||||
ordering: "name",
|
||||
pageSize: 20,
|
||||
search: search.trim(),
|
||||
page,
|
||||
});
|
||||
return {
|
||||
pagination: propertyMappings.pagination,
|
||||
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
|
||||
};
|
||||
}
|
||||
|
||||
export function makeRadiusPropertyMappingsSelector(instanceMappings?: string[]) {
|
||||
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
|
||||
return localMappings
|
||||
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
|
||||
: ([_0, _1, _2, _]: DualSelectPair<RadiusProviderPropertyMapping>) => [];
|
||||
}
|
||||
import { renderForm } from "./RadiusProviderFormForm.js";
|
||||
|
||||
@customElement("ak-provider-radius-form")
|
||||
export class RadiusProviderFormPage extends WithBrandConfig(BaseProviderForm<RadiusProvider>) {
|
||||
@ -65,127 +29,8 @@ export class RadiusProviderFormPage extends WithBrandConfig(BaseProviderForm<Rad
|
||||
}
|
||||
}
|
||||
|
||||
// All Provider objects have an Authorization flow, but not all providers have an Authentication
|
||||
// flow. Radius needs only one field, but it is not the Authorization field, it is an
|
||||
// Authentication field. So, yeah, we're using the authorization field to store the
|
||||
// authentication information, which is why the ak-branded-flow-search call down there looks so
|
||||
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
|
||||
// field of the target Provider.
|
||||
renderForm(): TemplateResult {
|
||||
return html`
|
||||
<ak-form-element-horizontal label=${msg("Name")} required name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
required
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
.brandFlow=${this.brand?.flowAuthentication}
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<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=${first(this.instance?.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">
|
||||
${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.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group expanded>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Shared secret")}
|
||||
required
|
||||
name="sharedSecret"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(
|
||||
this.instance?.sharedSecret,
|
||||
randomString(128, ascii_letters + digits),
|
||||
)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Client Networks")}
|
||||
required
|
||||
name="clientNetworks"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.clientNetworks, "0.0.0.0/0, ::/0")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(`List of CIDRs (comma-seperated) that clients can connect from. A more specific
|
||||
CIDR will match before a looser one. Clients connecting from a non-specified CIDR
|
||||
will be dropped.`)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Property mappings")}
|
||||
name="propertyMappings"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${radiusPropertyMappingsProvider}
|
||||
.selector=${makeRadiusPropertyMappingsSelector(
|
||||
this.instance?.propertyMappings,
|
||||
)}
|
||||
available-label=${msg("Available Property Mappings")}
|
||||
selected-label=${msg("Selected Property Mappings")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${this.instance?.invalidationFlow}
|
||||
defaultFlowSlug="default-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div></ak-form-group
|
||||
>
|
||||
`;
|
||||
renderForm() {
|
||||
return renderForm(this.instance ?? {}, [], this.brand);
|
||||
}
|
||||
}
|
||||
|
||||
|
154
web/src/admin/providers/radius/RadiusProviderFormForm.ts
Normal file
154
web/src/admin/providers/radius/RadiusProviderFormForm.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
|
||||
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
CurrentBrand,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
PropertymappingsApi,
|
||||
RadiusProvider,
|
||||
RadiusProviderPropertyMapping,
|
||||
ValidationError,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
export async function radiusPropertyMappingsProvider(page = 1, search = "") {
|
||||
const propertyMappings = await new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsProviderRadiusList({
|
||||
ordering: "name",
|
||||
pageSize: 20,
|
||||
search: search.trim(),
|
||||
page,
|
||||
});
|
||||
return {
|
||||
pagination: propertyMappings.pagination,
|
||||
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
|
||||
};
|
||||
}
|
||||
|
||||
export function makeRadiusPropertyMappingsSelector(instanceMappings?: string[]) {
|
||||
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
|
||||
return localMappings
|
||||
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
|
||||
: ([_0, _1, _2, _]: DualSelectPair<RadiusProviderPropertyMapping>) => [];
|
||||
}
|
||||
|
||||
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.",
|
||||
);
|
||||
|
||||
const clientNetworksHelp = msg(
|
||||
"List of CIDRs (comma-seperated) that clients can connect from. A more specific CIDR will match before a looser one. Clients connecting from a non-specified CIDR will be dropped.",
|
||||
);
|
||||
|
||||
// All Provider objects have an Authorization flow, but not all providers have an Authentication
|
||||
// flow. Radius needs only one field, but it is not the Authorization field, it is an
|
||||
// Authentication field. So, yeah, we're using the authorization field to store the
|
||||
// authentication information, which is why the ak-branded-flow-search call down there looks so
|
||||
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
|
||||
// field of the target Provider.
|
||||
|
||||
export function renderForm(
|
||||
provider?: Partial<RadiusProvider>,
|
||||
errors: ValidationError = {},
|
||||
brand?: CurrentBrand,
|
||||
) {
|
||||
return html`
|
||||
<ak-text-input
|
||||
name="name"
|
||||
label=${msg("Name")}
|
||||
value=${ifDefined(provider?.name)}
|
||||
.errorMessages=${errors?.name ?? []}
|
||||
required
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
.errorMessages=${errors?.authorizationFlow ?? []}
|
||||
>
|
||||
<ak-branded-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
.brandFlow=${brand?.flowAuthentication}
|
||||
required
|
||||
></ak-branded-flow-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</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>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="sharedSecret"
|
||||
label=${msg("Shared secret")}
|
||||
.errorMessages=${errors?.sharedSecret ?? []}
|
||||
value=${first(
|
||||
provider?.sharedSecret,
|
||||
randomString(128, ascii_letters + digits),
|
||||
)}
|
||||
required
|
||||
></ak-text-input>
|
||||
<ak-text-input
|
||||
name="clientNetworks"
|
||||
label=${msg("Client Networks")}
|
||||
value=${first(provider?.clientNetworks, "0.0.0.0/0, ::/0")}
|
||||
.errorMessages=${errors?.clientNetworks ?? []}
|
||||
required
|
||||
help=${clientNetworksHelp}
|
||||
></ak-text-input>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Property mappings")}
|
||||
name="propertyMappings"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${radiusPropertyMappingsProvider}
|
||||
.selector=${makeRadiusPropertyMappingsSelector(provider?.propertyMappings)}
|
||||
available-label=${msg("Available Property Mappings")}
|
||||
selected-label=${msg("Selected Property Mappings")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
.errorMessages=${errors?.invalidationFlow ?? []}
|
||||
defaultFlowSlug="default-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div></ak-form-group
|
||||
>
|
||||
`;
|
||||
}
|
@ -1,58 +1,12 @@
|
||||
import {
|
||||
digestAlgorithmOptions,
|
||||
signatureAlgorithmOptions,
|
||||
} from "@goauthentik/admin/applications/wizard/methods/saml/SamlProviderOptions";
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import AkCryptoCertificateSearch from "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { 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";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
PropertymappingsApi,
|
||||
PropertymappingsProviderSamlListRequest,
|
||||
ProvidersApi,
|
||||
SAMLPropertyMapping,
|
||||
SAMLProvider,
|
||||
SpBindingEnum,
|
||||
} from "@goauthentik/api";
|
||||
import { ProvidersApi, SAMLProvider } from "@goauthentik/api";
|
||||
|
||||
export async function samlPropertyMappingsProvider(page = 1, search = "") {
|
||||
const propertyMappings = await new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsProviderSamlList({
|
||||
ordering: "saml_name",
|
||||
pageSize: 20,
|
||||
search: search.trim(),
|
||||
page,
|
||||
});
|
||||
return {
|
||||
pagination: propertyMappings.pagination,
|
||||
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
|
||||
};
|
||||
}
|
||||
|
||||
export function makeSAMLPropertyMappingsSelector(instanceMappings?: string[]) {
|
||||
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
|
||||
return localMappings
|
||||
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
|
||||
: ([_0, _1, _2, mapping]: DualSelectPair<SAMLPropertyMapping>) =>
|
||||
mapping?.managed?.startsWith("goauthentik.io/providers/saml");
|
||||
}
|
||||
import { renderForm } from "./SAMLProviderFormForm.js";
|
||||
|
||||
@customElement("ak-provider-saml-form")
|
||||
export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
|
||||
@ -80,368 +34,14 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
|
||||
}
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
required
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
renderForm() {
|
||||
const setHasSigningKp = (ev: InputEvent) => {
|
||||
const target = ev.target as AkCryptoCertificateSearch;
|
||||
if (!target) return;
|
||||
this.hasSigningKp = !!target.selectedKeypair;
|
||||
};
|
||||
|
||||
<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(this.instance?.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="${this.instance?.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
|
||||
label=${msg("Service Provider Binding")}
|
||||
?required=${true}
|
||||
name="spBinding"
|
||||
>
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
label: msg("Redirect"),
|
||||
value: SpBindingEnum.Redirect,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: msg("Post"),
|
||||
value: SpBindingEnum.Post,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.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(this.instance?.audience)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
?required=${false}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${this.instance?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${this.instance?.invalidationFlow}
|
||||
defaultFlowSlug="default-provider-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Signing Certificate")}
|
||||
name="signingKp"
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
.certificate=${this.instance?.signingKp}
|
||||
@input=${(ev: InputEvent) => {
|
||||
const target = ev.target as AkCryptoCertificateSearch;
|
||||
if (!target) return;
|
||||
this.hasSigningKp = !!target.selectedKeypair;
|
||||
}}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Certificate used to sign outgoing Responses going to the Service Provider.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
${this.hasSigningKp
|
||||
? html` <ak-form-element-horizontal name="signAssertion">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(this.instance?.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(this.instance?.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}
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Verification Certificate")}
|
||||
name="verificationKp"
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
.certificate=${this.instance?.verificationKp}
|
||||
nokey
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Encryption Certificate")}
|
||||
name="encryptionKp"
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
.certificate=${this.instance?.encryptionKp}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When selected, assertions will be encrypted using this keypair.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Property mappings")}
|
||||
name="propertyMappings"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${samlPropertyMappingsProvider}
|
||||
.selector=${makeSAMLPropertyMappingsSelector(
|
||||
this.instance?.propertyMappings,
|
||||
)}
|
||||
available-label=${msg("Available User Property Mappings")}
|
||||
selected-label=${msg("Selected User Property Mappings")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("NameID Property Mapping")}
|
||||
name="nameIdMapping"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (
|
||||
query?: string,
|
||||
): Promise<SAMLPropertyMapping[]> => {
|
||||
const args: PropertymappingsProviderSamlListRequest = {
|
||||
ordering: "saml_name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const items = await new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsProviderSamlList(args);
|
||||
return items.results;
|
||||
}}
|
||||
.renderElement=${(item: SAMLPropertyMapping): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(
|
||||
item: SAMLPropertyMapping | undefined,
|
||||
): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(item: SAMLPropertyMapping): boolean => {
|
||||
return this.instance?.nameIdMapping === item.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Assertion valid not before")}
|
||||
?required=${true}
|
||||
name="assertionValidNotBefore"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${this.instance?.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="${this.instance?.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="${this.instance?.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")}
|
||||
?required=${true}
|
||||
name="defaultRelayState"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${this.instance?.defaultRelayState || ""}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<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>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Digest algorithm")}
|
||||
?required=${true}
|
||||
name="digestAlgorithm"
|
||||
>
|
||||
<ak-radio
|
||||
.options=${digestAlgorithmOptions}
|
||||
.value=${this.instance?.digestAlgorithm}
|
||||
>
|
||||
</ak-radio>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Signature algorithm")}
|
||||
?required=${true}
|
||||
name="signatureAlgorithm"
|
||||
>
|
||||
<ak-radio
|
||||
.options=${signatureAlgorithmOptions}
|
||||
.value=${this.instance?.signatureAlgorithm}
|
||||
>
|
||||
</ak-radio>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
return renderForm(this.instance ?? {}, [], setHasSigningKp, this.hasSigningKp);
|
||||
}
|
||||
}
|
||||
|
||||
|
330
web/src/admin/providers/saml/SAMLProviderFormForm.ts
Normal file
330
web/src/admin/providers/saml/SAMLProviderFormForm.ts
Normal file
@ -0,0 +1,330 @@
|
||||
import {
|
||||
digestAlgorithmOptions,
|
||||
signatureAlgorithmOptions,
|
||||
} from "@goauthentik/admin/applications/wizard/methods/saml/SamlProviderOptions";
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
PropertymappingsApi,
|
||||
PropertymappingsProviderSamlListRequest,
|
||||
SAMLPropertyMapping,
|
||||
SAMLProvider,
|
||||
SpBindingEnum,
|
||||
ValidationError,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
export async function samlPropertyMappingsProvider(page = 1, search = "") {
|
||||
const propertyMappings = await new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsProviderSamlList({
|
||||
ordering: "saml_name",
|
||||
pageSize: 20,
|
||||
search: search.trim(),
|
||||
page,
|
||||
});
|
||||
return {
|
||||
pagination: propertyMappings.pagination,
|
||||
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
|
||||
};
|
||||
}
|
||||
|
||||
export function makeSAMLPropertyMappingsSelector(instanceMappings?: string[]) {
|
||||
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
|
||||
return localMappings
|
||||
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
|
||||
: ([_0, _1, _2, mapping]: DualSelectPair<SAMLPropertyMapping>) =>
|
||||
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> = {},
|
||||
errors: ValidationError,
|
||||
setHasSigningKp: (ev: InputEvent) => void,
|
||||
hasSigningKp: boolean,
|
||||
) {
|
||||
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
|
||||
>
|
||||
<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.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<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")}
|
||||
name="spBinding"
|
||||
required
|
||||
.options=${serviceProviderBindingOptions}
|
||||
.value=${provider?.spBinding}
|
||||
help=${msg(
|
||||
"Determines how authentik sends the response back to the Service Provider.",
|
||||
)}
|
||||
>
|
||||
</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>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced flow settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
?required=${false}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authenticationFlow}
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Flow used when a user access this provider and is not authenticated.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Invalidation flow")}
|
||||
name="invalidationFlow"
|
||||
required
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||
.currentFlow=${provider?.invalidationFlow}
|
||||
defaultFlowSlug="default-provider-invalidation-flow"
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when logging out of this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal label=${msg("Signing Certificate")} name="signingKp">
|
||||
<ak-crypto-certificate-search
|
||||
.certificate=${provider?.signingKp}
|
||||
@input=${setHasSigningKp}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Certificate used to sign outgoing Responses going to the Service Provider.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
${hasSigningKp ? renderHasSigningKp(provider) : nothing}
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Verification Certificate")}
|
||||
name="verificationKp"
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
.certificate=${provider?.verificationKp}
|
||||
nokey
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Encryption Certificate")}
|
||||
name="encryptionKp"
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
.certificate=${provider?.encryptionKp}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("When selected, assertions will be encrypted using this keypair.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Property mappings")}
|
||||
name="propertyMappings"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${samlPropertyMappingsProvider}
|
||||
.selector=${makeSAMLPropertyMappingsSelector(provider?.propertyMappings)}
|
||||
available-label=${msg("Available User Property Mappings")}
|
||||
selected-label=${msg("Selected User Property Mappings")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("NameID Property Mapping")}
|
||||
name="nameIdMapping"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<SAMLPropertyMapping[]> => {
|
||||
const args: PropertymappingsProviderSamlListRequest = {
|
||||
ordering: "saml_name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const items = await new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsProviderSamlList(args);
|
||||
return items.results;
|
||||
}}
|
||||
.renderElement=${(item: SAMLPropertyMapping): string => {
|
||||
return item.name;
|
||||
}}
|
||||
.value=${(item: SAMLPropertyMapping | undefined): string | undefined => {
|
||||
return item?.pk;
|
||||
}}
|
||||
.selected=${(item: SAMLPropertyMapping): boolean => {
|
||||
return provider?.nameIdMapping === item.pk;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-text-input
|
||||
name="assertionValidNotBefore"
|
||||
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-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-input>
|
||||
|
||||
<ak-radio-input
|
||||
name="signatureAlgorithm"
|
||||
label=${msg("Signature algorithm")}
|
||||
.options=${signatureAlgorithmOptions}
|
||||
.value=${provider?.signatureAlgorithm}
|
||||
required
|
||||
>
|
||||
</ak-radio-input>
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
}
|
@ -1,53 +1,11 @@
|
||||
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
CoreGroupsListRequest,
|
||||
Group,
|
||||
PropertymappingsApi,
|
||||
ProvidersApi,
|
||||
SCIMMapping,
|
||||
SCIMProvider,
|
||||
} from "@goauthentik/api";
|
||||
import { ProvidersApi, SCIMProvider } from "@goauthentik/api";
|
||||
|
||||
export async function scimPropertyMappingsProvider(page = 1, search = "") {
|
||||
const propertyMappings = await new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsProviderScimList({
|
||||
ordering: "managed",
|
||||
pageSize: 20,
|
||||
search: search.trim(),
|
||||
page,
|
||||
});
|
||||
return {
|
||||
pagination: propertyMappings.pagination,
|
||||
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
|
||||
};
|
||||
}
|
||||
|
||||
export function makeSCIMPropertyMappingsSelector(
|
||||
instanceMappings: string[] | undefined,
|
||||
defaultSelected: string,
|
||||
) {
|
||||
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
|
||||
return localMappings
|
||||
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
|
||||
: ([_0, _1, _2, mapping]: DualSelectPair<SCIMMapping>) =>
|
||||
mapping?.managed === defaultSelected;
|
||||
}
|
||||
import { renderForm } from "./SCIMProviderFormForm.js";
|
||||
|
||||
@customElement("ak-provider-scim-form")
|
||||
export class SCIMProviderFormPage extends BaseProviderForm<SCIMProvider> {
|
||||
@ -70,156 +28,8 @@ export class SCIMProviderFormPage extends BaseProviderForm<SCIMProvider> {
|
||||
}
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<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("URL")} ?required=${true} name="url">
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.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(this.instance?.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=${true}
|
||||
name="token"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.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>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group ?expanded=${true}>
|
||||
<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(this.instance?.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-form-element-horizontal label=${msg("Group")} name="filterGroup">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Group[]> => {
|
||||
const args: CoreGroupsListRequest = {
|
||||
ordering: "name",
|
||||
includeUsers: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(
|
||||
args,
|
||||
);
|
||||
return groups.results;
|
||||
}}
|
||||
.renderElement=${(group: Group): string => {
|
||||
return group.name;
|
||||
}}
|
||||
.value=${(group: Group | undefined): string | undefined => {
|
||||
return group ? group.pk : undefined;
|
||||
}}
|
||||
.selected=${(group: Group): boolean => {
|
||||
return group.pk === this.instance?.filterGroup;
|
||||
}}
|
||||
?blankable=${true}
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Only sync users within the selected group.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group ?expanded=${true}>
|
||||
<span slot="header"> ${msg("Attribute mapping")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("User Property Mappings")}
|
||||
name="propertyMappings">
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${scimPropertyMappingsProvider}
|
||||
.selector=${makeSCIMPropertyMappingsSelector(
|
||||
this.instance?.propertyMappings,
|
||||
"goauthentik.io/providers/scim/user",
|
||||
)}
|
||||
available-label=${msg("Available User Property Mappings")}
|
||||
selected-label=${msg("Selected User Property Mappings")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings used to user mapping.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Group Property Mappings")}
|
||||
name="propertyMappingsGroup">
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${scimPropertyMappingsProvider}
|
||||
.selector=${makeSCIMPropertyMappingsSelector(
|
||||
this.instance?.propertyMappingsGroup,
|
||||
"goauthentik.io/providers/scim/group",
|
||||
)}
|
||||
available-label=${msg("Available Group Property Mappings")}
|
||||
selected-label=${msg("Selected Group Property Mappings")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings used to group creation.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>`;
|
||||
renderForm() {
|
||||
return renderForm(this.instance ?? {}, []);
|
||||
}
|
||||
}
|
||||
|
||||
|
173
web/src/admin/providers/scim/SCIMProviderFormForm.ts
Normal file
173
web/src/admin/providers/scim/SCIMProviderFormForm.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
|
||||
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
CoreGroupsListRequest,
|
||||
Group,
|
||||
PropertymappingsApi,
|
||||
SCIMMapping,
|
||||
SCIMProvider,
|
||||
ValidationError,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
export async function scimPropertyMappingsProvider(page = 1, search = "") {
|
||||
const propertyMappings = await new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsProviderScimList({
|
||||
ordering: "managed",
|
||||
pageSize: 20,
|
||||
search: search.trim(),
|
||||
page,
|
||||
});
|
||||
return {
|
||||
pagination: propertyMappings.pagination,
|
||||
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
|
||||
};
|
||||
}
|
||||
|
||||
export function makeSCIMPropertyMappingsSelector(
|
||||
instanceMappings: string[] | undefined,
|
||||
defaultSelected: string,
|
||||
) {
|
||||
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
|
||||
return localMappings
|
||||
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
|
||||
: ([_0, _1, _2, mapping]: DualSelectPair<SCIMMapping>) =>
|
||||
mapping?.managed === defaultSelected;
|
||||
}
|
||||
|
||||
export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationError = {}) {
|
||||
return html`
|
||||
<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-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-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[]> => {
|
||||
const args: CoreGroupsListRequest = {
|
||||
ordering: "name",
|
||||
includeUsers: false,
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(args);
|
||||
return groups.results;
|
||||
}}
|
||||
.renderElement=${(group: Group): string => {
|
||||
return group.name;
|
||||
}}
|
||||
.value=${(group: Group | undefined): string | undefined => {
|
||||
return group ? group.pk : undefined;
|
||||
}}
|
||||
.selected=${(group: Group): boolean => {
|
||||
return group.pk === provider?.filterGroup;
|
||||
}}
|
||||
blankable
|
||||
>
|
||||
</ak-search-select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Only sync users within the selected group.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group expanded>
|
||||
<span slot="header"> ${msg("Attribute mapping")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("User Property Mappings")}
|
||||
name="propertyMappings"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${scimPropertyMappingsProvider}
|
||||
.selector=${makeSCIMPropertyMappingsSelector(
|
||||
provider?.propertyMappings,
|
||||
"goauthentik.io/providers/scim/user",
|
||||
)}
|
||||
available-label=${msg("Available User Property Mappings")}
|
||||
selected-label=${msg("Selected User Property Mappings")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings used to user mapping.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Group Property Mappings")}
|
||||
name="propertyMappingsGroup"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${scimPropertyMappingsProvider}
|
||||
.selector=${makeSCIMPropertyMappingsSelector(
|
||||
provider?.propertyMappingsGroup,
|
||||
"goauthentik.io/providers/scim/group",
|
||||
)}
|
||||
available-label=${msg("Available Group Property Mappings")}
|
||||
selected-label=${msg("Selected Group Property Mappings")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Property mappings used to group creation.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
}
|
@ -44,37 +44,43 @@ export class FormGroup extends AKElement {
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<div class="pf-c-form__field-group ${this.expanded ? "pf-m-expanded" : ""}">
|
||||
<div class="pf-c-form__field-group-toggle">
|
||||
<div class="pf-c-form__field-group-toggle-button">
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
type="button"
|
||||
aria-expanded="${this.expanded}"
|
||||
aria-label=${this.ariaLabel}
|
||||
@click=${() => {
|
||||
this.expanded = !this.expanded;
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-form__field-group-toggle-icon">
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
return html` <div class="pf-c-form">
|
||||
<div class="pf-c-form__field-group ${this.expanded ? "pf-m-expanded" : ""}">
|
||||
<div class="pf-c-form__field-group-toggle">
|
||||
<div class="pf-c-form__field-group-toggle-button">
|
||||
<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
type="button"
|
||||
aria-expanded="${this.expanded}"
|
||||
aria-label=${this.ariaLabel}
|
||||
@click=${() => {
|
||||
this.expanded = !this.expanded;
|
||||
}}
|
||||
>
|
||||
<span class="pf-c-form__field-group-toggle-icon">
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-form__field-group-header">
|
||||
<div class="pf-c-form__field-group-header-main">
|
||||
<div class="pf-c-form__field-group-header-title">
|
||||
<div class="pf-c-form__field-group-header-title-text">
|
||||
<slot name="header"></slot>
|
||||
<div class="pf-c-form__field-group-header">
|
||||
<div class="pf-c-form__field-group-header-main">
|
||||
<div class="pf-c-form__field-group-header-title">
|
||||
<div class="pf-c-form__field-group-header-title-text">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-form__field-group-header-description">
|
||||
<slot name="description"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-form__field-group-header-description">
|
||||
<slot name="description"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<slot
|
||||
?hidden=${!this.expanded}
|
||||
class="pf-c-form__field-group-body"
|
||||
name="body"
|
||||
></slot>
|
||||
</div>
|
||||
<slot ?hidden=${!this.expanded} class="pf-c-form__field-group-body" name="body"></slot>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
225
web/tests/pageobjects/controls.ts
Normal file
225
web/tests/pageobjects/controls.ts
Normal file
@ -0,0 +1,225 @@
|
||||
import { browser } from "@wdio/globals";
|
||||
import { match } from "ts-pattern";
|
||||
import { Key } from "webdriverio";
|
||||
|
||||
export async function doBlur(el: WebdriverIO.Element | ChainablePromiseElement) {
|
||||
const element = await el;
|
||||
browser.execute((element) => element.blur(), element);
|
||||
}
|
||||
|
||||
export function tap<A>(a: A) {
|
||||
console.log("TAP:", a);
|
||||
return a;
|
||||
}
|
||||
|
||||
const makeComparator = (value: string | RegExp) =>
|
||||
typeof value === "string"
|
||||
? (sample: string) => sample === value
|
||||
: (sample: string) => value.test(sample);
|
||||
|
||||
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 context.$$("button")) {
|
||||
if ((await button.isDisplayed()) && (await button.getText()).indexOf(name) !== -1) {
|
||||
return button;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
if (!(button && (await button.isDisplayed()))) {
|
||||
throw new Error(`Unable to find button '${name}'`);
|
||||
}
|
||||
|
||||
await button.scrollIntoView();
|
||||
await button.click();
|
||||
await doBlur(button);
|
||||
}
|
||||
|
||||
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 button.$(".pf-c-toggle-group__text").getText())) {
|
||||
return button;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
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 = 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*,
|
||||
// and this can break the damn tests.
|
||||
if (!(await group.isDisplayed())) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
comparator(await group.$("div.pf-c-form__field-group-header-title-text").getText())
|
||||
) {
|
||||
return group;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
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])
|
||||
.with(["false", "open"], async () => await toggle.click())
|
||||
.with(["true", "closed"], async () => await toggle.click())
|
||||
.otherwise(async () => {});
|
||||
await doBlur(formGroup);
|
||||
}
|
||||
|
||||
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 item.scrollIntoView();
|
||||
await item.click();
|
||||
await doBlur(control);
|
||||
}
|
||||
|
||||
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 $(`div[data-managed-for*="${name}"]`)
|
||||
.$("ak-list-select")
|
||||
.$$("button")) {
|
||||
if (comparator(await button.getText())) {
|
||||
return 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 checkIsPresent>]
|
||||
| [typeof clickButton, ...Parameters<typeof clickButton>]
|
||||
| [typeof clickToggleGroup, ...Parameters<typeof clickToggleGroup>]
|
||||
| [typeof setFormGroup, ...Parameters<typeof setFormGroup>]
|
||||
| [typeof setRadio, ...Parameters<typeof setRadio>]
|
||||
| [typeof setSearchSelect, ...Parameters<typeof setSearchSelect>]
|
||||
| [typeof setTextInput, ...Parameters<typeof setTextInput>]
|
||||
| [typeof setTextareaInput, ...Parameters<typeof setTextareaInput>]
|
||||
| [typeof setToggle, ...Parameters<typeof setToggle>]
|
||||
| [typeof setTypeCreate, ...Parameters<typeof setTypeCreate>];
|
||||
|
||||
export type TestSequence = TestInteraction[];
|
||||
|
||||
export type TestProvider = () => TestSequence;
|
@ -1,4 +1,5 @@
|
||||
import { browser } from "@wdio/globals";
|
||||
import { match } from "ts-pattern";
|
||||
import { Key } from "webdriverio";
|
||||
|
||||
const CLICK_TIME_DELAY = 250;
|
||||
@ -7,6 +8,7 @@ const CLICK_TIME_DELAY = 250;
|
||||
* Main page object containing all methods, selectors and functionality that is shared across all
|
||||
* page objects
|
||||
*/
|
||||
|
||||
export default class Page {
|
||||
/**
|
||||
* Opens a sub page of the page
|
||||
@ -31,7 +33,6 @@ export default class Page {
|
||||
* why it would be hard to simplify this further (`flow` vs `tentanted-flow` vs a straight-up
|
||||
* SearchSelect each have different a `searchSelector`).
|
||||
*/
|
||||
|
||||
async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) {
|
||||
const inputBind = await $(searchSelector);
|
||||
const inputMain = await inputBind.$('input[type="text"]');
|
||||
@ -55,6 +56,79 @@ export default class Page {
|
||||
await browser.keys(Key.Tab);
|
||||
}
|
||||
|
||||
async 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;
|
||||
}
|
||||
})();
|
||||
|
||||
// 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();
|
||||
|
||||
// Weirdly necessary because it's portals!
|
||||
const searchBlock = await (
|
||||
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)) {
|
||||
target = button;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// @ts-expect-error "TSC cannot tell if the `for` loop actually performs the assignment."
|
||||
if (!target) {
|
||||
throw new Error(`Expected to find an entry matching the spec ${value}`);
|
||||
}
|
||||
|
||||
await (await target).click();
|
||||
await browser.keys(Key.Tab);
|
||||
}
|
||||
|
||||
async setTextInput(name: string, value: string) {
|
||||
const control = await $(`input[name="${name}"}`);
|
||||
await control.scrollIntoView();
|
||||
await control.setValue(value);
|
||||
}
|
||||
|
||||
async 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();
|
||||
}
|
||||
|
||||
async setTypeCreate(name: string, value: string) {
|
||||
const control = await $(`ak-wizard-page-type-create[name="${name}"]`);
|
||||
await control.scrollIntoView();
|
||||
const selection = await $(`.pf-c-card__.*=${value}`);
|
||||
await selection.scrollIntoView();
|
||||
await selection.click();
|
||||
}
|
||||
|
||||
async setFormGroup(name: string, setting: "open" | "closed") {
|
||||
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([await toggle.getAttribute("expanded"), setting])
|
||||
.with(["false", "open"], async () => await toggle.click())
|
||||
.with(["true", "closed"], async () => await toggle.click())
|
||||
.otherwise(async () => {});
|
||||
}
|
||||
|
||||
public async logout() {
|
||||
await browser.url("http://localhost:9000/flows/-/default/invalidation/");
|
||||
return await this.pause();
|
||||
|
@ -9,29 +9,50 @@ 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 {
|
||||
completeForwardAuthDomainProxyProviderForm,
|
||||
completeForwardAuthProxyProviderForm,
|
||||
completeLDAPProviderForm,
|
||||
completeOAuth2ProviderForm,
|
||||
completeProxyProviderForm,
|
||||
completeRadiusProviderForm,
|
||||
completeSAMLProviderForm,
|
||||
completeSCIMProviderForm,
|
||||
simpleForwardAuthDomainProxyProviderForm,
|
||||
simpleForwardAuthProxyProviderForm,
|
||||
simpleLDAPProviderForm,
|
||||
simpleOAuth2ProviderForm,
|
||||
simpleProxyProviderForm,
|
||||
simpleRadiusProviderForm,
|
||||
simpleSAMLProviderForm,
|
||||
simpleSCIMProviderForm,
|
||||
} from "./provider-shared-sequences.js";
|
||||
import { type TestSequence } from "./shared-sequences";
|
||||
|
||||
async function reachTheProvider(title: string) {
|
||||
const newPrefix = randomId();
|
||||
const SUCCESS_MESSAGE = "Your application has been saved";
|
||||
|
||||
async function reachTheApplicationsPage() {
|
||||
await ApplicationsListPage.logout();
|
||||
await login();
|
||||
await ApplicationsListPage.open();
|
||||
await ApplicationsListPage.pause("ak-page-header");
|
||||
await expect(await ApplicationsListPage.pageHeader()).toBeDisplayed();
|
||||
await expect(await ApplicationsListPage.pageHeader()).toHaveText("Applications");
|
||||
}
|
||||
|
||||
async function fillOutTheApplication(title: string) {
|
||||
const newPrefix = randomId();
|
||||
|
||||
await (await ApplicationsListPage.startWizardButton()).click();
|
||||
await (await ApplicationWizardView.wizardTitle()).waitForDisplayed();
|
||||
await expect(await ApplicationWizardView.wizardTitle()).toHaveText("New application");
|
||||
|
||||
await (await ApplicationWizardView.app.name()).setValue(`${title} - ${newPrefix}`);
|
||||
await (await ApplicationWizardView.app.uiSettings()).scrollIntoView();
|
||||
await (await ApplicationWizardView.app.uiSettings()).click();
|
||||
await (await ApplicationWizardView.app.launchUrl()).scrollIntoView();
|
||||
await (await ApplicationWizardView.app.launchUrl()).setValue("http://example.goauthentik.io");
|
||||
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
return await ApplicationWizardView.pause();
|
||||
await ApplicationWizardView.pause();
|
||||
}
|
||||
|
||||
async function getCommitMessage() {
|
||||
@ -39,136 +60,51 @@ async function getCommitMessage() {
|
||||
return await ApplicationWizardView.successMessage();
|
||||
}
|
||||
|
||||
const SUCCESS_MESSAGE = "Your application has been saved";
|
||||
const EXPLICIT_CONSENT = "default-provider-authorization-explicit-consent";
|
||||
async function fillOutTheProviderAndCommit(provider: TestSequence) {
|
||||
// The wizard automagically provides a name. If it doesn't, that's a bug.
|
||||
const wizardProvider = provider.filter((p) => p.length < 2 || p[1] !== "name");
|
||||
await $("ak-wizard-page-type-create").waitForDisplayed();
|
||||
for await (const field of wizardProvider) {
|
||||
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."
|
||||
await thefunc.apply($, args);
|
||||
}
|
||||
|
||||
describe("Configure Applications with the Application Wizard", () => {
|
||||
it("Should configure a simple LDAP Application", async () => {
|
||||
await reachTheProvider("New LDAP Application");
|
||||
await $("ak-wizard-frame").$("footer button.pf-m-primary").click();
|
||||
await ApplicationWizardView.pause();
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
}
|
||||
|
||||
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||
await (await ApplicationWizardView.ldapProvider).scrollIntoView();
|
||||
await (await ApplicationWizardView.ldapProvider).click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await ApplicationWizardView.ldap.setBindFlow("default-authentication-flow");
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
async function itShouldConfigureApplicationsViaTheWizard(name: string, provider: TestSequence) {
|
||||
it(`Should successfully configure an application with a ${name} provider`, async () => {
|
||||
await reachTheApplicationsPage();
|
||||
await fillOutTheApplication(name);
|
||||
await fillOutTheProviderAndCommit(provider);
|
||||
});
|
||||
}
|
||||
|
||||
it("Should configure a simple Oauth2 Application", async () => {
|
||||
await reachTheProvider("New Oauth2 Application");
|
||||
const providers = [
|
||||
["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],
|
||||
];
|
||||
|
||||
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||
await (await ApplicationWizardView.oauth2Provider).scrollIntoView();
|
||||
await (await ApplicationWizardView.oauth2Provider).click();
|
||||
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await ApplicationWizardView.oauth.setAuthorizationFlow(EXPLICIT_CONSENT);
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
});
|
||||
|
||||
it("Should configure a simple SAML Application", async () => {
|
||||
await reachTheProvider("New SAML Application");
|
||||
|
||||
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||
await (await ApplicationWizardView.samlProvider).scrollIntoView();
|
||||
await (await ApplicationWizardView.samlProvider).click();
|
||||
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await ApplicationWizardView.saml.setAuthorizationFlow(EXPLICIT_CONSENT);
|
||||
await ApplicationWizardView.saml.acsUrl.setValue("http://example.com:8000/");
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
});
|
||||
|
||||
it("Should configure a simple SCIM Application", async () => {
|
||||
await reachTheProvider("New SCIM Application");
|
||||
|
||||
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||
await (await ApplicationWizardView.scimProvider).scrollIntoView();
|
||||
await (await ApplicationWizardView.scimProvider).click();
|
||||
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await ApplicationWizardView.scim.url.setValue("http://example.com:8000/");
|
||||
await ApplicationWizardView.scim.token.setValue("a-very-basic-token");
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
});
|
||||
|
||||
it("Should configure a simple Radius Application", async () => {
|
||||
await reachTheProvider("New Radius Application");
|
||||
|
||||
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||
await (await ApplicationWizardView.radiusProvider).scrollIntoView();
|
||||
await (await ApplicationWizardView.radiusProvider).click();
|
||||
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await ApplicationWizardView.radius.setAuthenticationFlow("default-authentication-flow");
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
});
|
||||
|
||||
it("Should configure a simple Transparent Proxy Application", async () => {
|
||||
await reachTheProvider("New Transparent Proxy Application");
|
||||
|
||||
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||
await (await ApplicationWizardView.proxyProviderProxy).scrollIntoView();
|
||||
await (await ApplicationWizardView.proxyProviderProxy).click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await ApplicationWizardView.transparentProxy.setAuthorizationFlow(EXPLICIT_CONSENT);
|
||||
await ApplicationWizardView.transparentProxy.externalHost.setValue(
|
||||
"http://external.example.com",
|
||||
);
|
||||
await ApplicationWizardView.transparentProxy.internalHost.setValue(
|
||||
"http://internal.example.com",
|
||||
);
|
||||
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
});
|
||||
|
||||
it("Should configure a simple Forward Proxy Application", async () => {
|
||||
await reachTheProvider("New Forward Proxy Application");
|
||||
|
||||
await (await ApplicationWizardView.providerList()).waitForDisplayed();
|
||||
await (await ApplicationWizardView.proxyProviderForwardsingle).scrollIntoView();
|
||||
await (await ApplicationWizardView.proxyProviderForwardsingle).click();
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await ApplicationWizardView.forwardProxy.setAuthorizationFlow(EXPLICIT_CONSENT);
|
||||
await ApplicationWizardView.forwardProxy.externalHost.setValue(
|
||||
"http://external.example.com",
|
||||
);
|
||||
|
||||
await (await ApplicationWizardView.nextButton()).click();
|
||||
await ApplicationWizardView.pause();
|
||||
|
||||
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
|
||||
});
|
||||
describe("Configuring Applications Via the Wizard", () => {
|
||||
for (const [name, provider] of providers) {
|
||||
itShouldConfigureApplicationsViaTheWizard(name, provider());
|
||||
}
|
||||
});
|
||||
|
@ -1,47 +0,0 @@
|
||||
import { expect } from "@wdio/globals";
|
||||
|
||||
import ProviderWizardView from "../pageobjects/provider-wizard.page.js";
|
||||
import ProvidersListPage from "../pageobjects/providers-list.page.js";
|
||||
import { randomId } from "../utils/index.js";
|
||||
import { login } from "../utils/login.js";
|
||||
|
||||
async function reachTheProvider() {
|
||||
await ProvidersListPage.logout();
|
||||
await login();
|
||||
await ProvidersListPage.open();
|
||||
await expect(await ProvidersListPage.pageHeader()).toHaveText("Providers");
|
||||
|
||||
await ProvidersListPage.startWizardButton.click();
|
||||
await ProviderWizardView.wizardTitle.waitForDisplayed();
|
||||
await expect(await ProviderWizardView.wizardTitle).toHaveText("New provider");
|
||||
}
|
||||
|
||||
describe("Configure Oauth2 Providers", () => {
|
||||
it("Should configure a simple LDAP Application", async () => {
|
||||
const newProviderName = `New OAuth2 Provider - ${randomId()}`;
|
||||
|
||||
await reachTheProvider();
|
||||
|
||||
await $("ak-wizard-page-type-create").waitForDisplayed();
|
||||
await $('div[data-ouid-component-name="oauth2provider"]').scrollIntoView();
|
||||
await $('div[data-ouid-component-name="oauth2provider"]').click();
|
||||
await ProviderWizardView.nextButton.click();
|
||||
await ProviderWizardView.pause();
|
||||
|
||||
return await $('ak-form-element-horizontal[name="name"]').$("input");
|
||||
await ProviderWizardView.oauth.setAuthorizationFlow(
|
||||
"default-provider-authorization-explicit-consent",
|
||||
);
|
||||
await ProviderWizardView.nextButton.click();
|
||||
await ProviderWizardView.pause();
|
||||
|
||||
await ProvidersListPage.searchInput.setValue(newProviderName);
|
||||
await ProvidersListPage.clickSearchButton();
|
||||
await ProvidersListPage.pause();
|
||||
|
||||
const newProvider = await ProvidersListPage.findProviderRow();
|
||||
await newProvider.waitForDisplayed();
|
||||
expect(newProvider).toExist();
|
||||
expect(await newProvider.getText()).toHaveText(newProviderName);
|
||||
});
|
||||
});
|
323
web/tests/specs/provider-shared-sequences.ts
Normal file
323
web/tests/specs/provider-shared-sequences.ts
Normal file
@ -0,0 +1,323 @@
|
||||
import {
|
||||
type TestProvider,
|
||||
type TestSequence,
|
||||
checkIsPresent,
|
||||
clickButton,
|
||||
clickToggleGroup,
|
||||
setFormGroup,
|
||||
setRadio,
|
||||
setSearchSelect,
|
||||
setTextInput,
|
||||
setTextareaInput,
|
||||
setToggle,
|
||||
setTypeCreate,
|
||||
} from "pageobjects/controls.js";
|
||||
|
||||
import { ascii_letters, digits, randomString } from "../utils";
|
||||
import { randomId } from "../utils/index.js";
|
||||
|
||||
const newObjectName = (prefix: string) => `${prefix} - ${randomId()}`;
|
||||
|
||||
// components.schemas.OAuth2ProviderRequest
|
||||
//
|
||||
// - name
|
||||
// - authentication_flow
|
||||
// - authorization_flow
|
||||
// - invalidation_flow
|
||||
// - property_mappings
|
||||
// - client_type
|
||||
// - client_id
|
||||
// - client_secret
|
||||
// - access_code_validity
|
||||
// - access_token_validity
|
||||
// - refresh_token_validity
|
||||
// - include_claims_in_id_token
|
||||
// - signing_key
|
||||
// - encryption_key
|
||||
// - redirect_uris
|
||||
// - sub_mode
|
||||
// - issuer_mode
|
||||
// - jwks_sources
|
||||
//
|
||||
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 completeOAuth2ProviderForm: TestProvider = () => [
|
||||
[setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"],
|
||||
[clickButton, "Next"],
|
||||
[setTextInput, "name", newObjectName("New Oauth2 Provider")],
|
||||
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
|
||||
[setFormGroup, /Protocol settings/, "open"],
|
||||
[setRadio, "clientType", "Public"],
|
||||
// Switch back so we can make sure `clientSecret` is available.
|
||||
[setRadio, "clientType", "Confidential"],
|
||||
[checkIsPresent, '[name="clientId"]'],
|
||||
[checkIsPresent, '[name="clientSecret"]'],
|
||||
[setSearchSelect, "signingKey", /authentik Self-signed Certificate/],
|
||||
[setSearchSelect, "encryptionKey", /authentik Self-signed Certificate/],
|
||||
[setFormGroup, /Advanced flow settings/, "open"],
|
||||
[setSearchSelect, "authenticationFlow", /default-source-authentication/],
|
||||
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
|
||||
[setFormGroup, /Advanced protocol settings/, "open"],
|
||||
[setTextInput, "accessCodeValidity", "minutes=2"],
|
||||
[setTextInput, "accessTokenValidity", "minutes=10"],
|
||||
[setTextInput, "refreshTokenValidity", "days=40"],
|
||||
[setToggle, "includeClaimsInIdToken", false],
|
||||
[checkIsPresent, '[name="redirectUris"]'],
|
||||
[setRadio, "subMode", "Based on the User's username"],
|
||||
[setRadio, "issuerMode", "Same identifier is used for all providers"],
|
||||
[setFormGroup, /Machine-to-Machine authentication settings/, "open"],
|
||||
[checkIsPresent, '[name="jwksSources"]'],
|
||||
];
|
||||
|
||||
// components.schemas.LDAPProviderRequest
|
||||
//
|
||||
// - name
|
||||
// - authentication_flow
|
||||
// - authorization_flow
|
||||
// - invalidation_flow
|
||||
// - base_dn
|
||||
// - certificate
|
||||
// - tls_server_name
|
||||
// - uid_start_number
|
||||
// - gid_start_number
|
||||
// - search_mode
|
||||
// - bind_mode
|
||||
// - mfa_support
|
||||
//
|
||||
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 completeLDAPProviderForm: TestProvider = () => [
|
||||
[setTypeCreate, "selectProviderType", "LDAP Provider"],
|
||||
[clickButton, "Next"],
|
||||
[setTextInput, "name", newObjectName("New LDAP Provider")],
|
||||
[setFormGroup, /Flow settings/, "open"],
|
||||
[setFormGroup, /Protocol settings/, "open"],
|
||||
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
|
||||
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
|
||||
[setTextInput, "baseDn", "DC=ldap-2,DC=goauthentik,DC=io"],
|
||||
[setSearchSelect, "certificate", /authentik Self-signed Certificate/],
|
||||
[checkIsPresent, '[name="tlsServerName"]'],
|
||||
[setTextInput, "uidStartNumber", "2001"],
|
||||
[setTextInput, "gidStartNumber", "4001"],
|
||||
[setRadio, "searchMode", "Direct querying"],
|
||||
[setRadio, "bindMode", "Direct binding"],
|
||||
[setToggle, "mfaSupport", false],
|
||||
];
|
||||
|
||||
export const simpleRadiusProviderForm: TestProvider = () => [
|
||||
[setTypeCreate, "selectProviderType", "Radius Provider"],
|
||||
[clickButton, "Next"],
|
||||
[setTextInput, "name", newObjectName("New Radius Provider")],
|
||||
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
|
||||
];
|
||||
|
||||
export const completeRadiusProviderForm: TestProvider = () => [
|
||||
[setTypeCreate, "selectProviderType", "Radius Provider"],
|
||||
[clickButton, "Next"],
|
||||
[setTextInput, "name", newObjectName("New Radius Provider")],
|
||||
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
|
||||
[setFormGroup, /Advanced flow settings/, "open"],
|
||||
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
|
||||
[setFormGroup, /Protocol settings/, "open"],
|
||||
[setToggle, "mfaSupport", false],
|
||||
[setTextInput, "clientNetworks", ""],
|
||||
[setTextInput, "clientNetworks", "0.0.0.0/0, ::/0"],
|
||||
[setTextInput, "sharedSecret", randomString(128, ascii_letters + digits)],
|
||||
[checkIsPresent, '[name="propertyMappings"]'],
|
||||
];
|
||||
|
||||
// provider_components.schemas.SAMLProviderRequest.yml
|
||||
//
|
||||
// - name
|
||||
// - authentication_flow
|
||||
// - authorization_flow
|
||||
// - invalidation_flow
|
||||
// - property_mappings
|
||||
// - acs_url
|
||||
// - audience
|
||||
// - issuer
|
||||
// - assertion_valid_not_before
|
||||
// - assertion_valid_not_on_or_after
|
||||
// - session_valid_not_on_or_after
|
||||
// - name_id_mapping
|
||||
// - digest_algorithm
|
||||
// - signature_algorithm
|
||||
// - signing_kp
|
||||
// - verification_kp
|
||||
// - encryption_kp
|
||||
// - sign_assertion
|
||||
// - sign_response
|
||||
// - sp_binding
|
||||
// - default_relay_state
|
||||
//
|
||||
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 completeSAMLProviderForm: 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/"],
|
||||
[setTextInput, "issuer", "someone-else"],
|
||||
[setRadio, "spBinding", "Post"],
|
||||
[setTextInput, "audience", ""],
|
||||
[setFormGroup, /Advanced flow settings/, "open"],
|
||||
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
|
||||
[setSearchSelect, "authenticationFlow", /default-source-authentication/],
|
||||
[setFormGroup, /Advanced protocol settings/, "open"],
|
||||
[checkIsPresent, '[name="propertyMappings"]'],
|
||||
[setSearchSelect, "signingKp", /authentik Self-signed Certificate/],
|
||||
[setSearchSelect, "verificationKp", /authentik Self-signed Certificate/],
|
||||
[setSearchSelect, "encryptionKp", /authentik Self-signed Certificate/],
|
||||
[setSearchSelect, "nameIdMapping", /authentik default SAML Mapping. Username/],
|
||||
[setTextInput, "assertionValidNotBefore", "minutes=-10"],
|
||||
[setTextInput, "assertionValidNotOnOrAfter", "minutes=10"],
|
||||
[setTextInput, "sessionValidNotOnOrAfter", "minutes=172800"],
|
||||
[checkIsPresent, '[name="defaultRelayState"]'],
|
||||
[setRadio, "digestAlgorithm", "SHA512"],
|
||||
[setRadio, "signatureAlgorithm", "RSA-SHA512"],
|
||||
// These are only available after the signingKp is defined.
|
||||
[setToggle, "signAssertion", true],
|
||||
[setToggle, "signResponse", true],
|
||||
];
|
||||
|
||||
// provider_components.schemas.SCIMProviderRequest.yml
|
||||
//
|
||||
// - name
|
||||
// - property_mappings
|
||||
// - property_mappings_group
|
||||
// - url
|
||||
// - verify_certificates
|
||||
// - token
|
||||
// - exclude_users_service_account
|
||||
// - filter_group
|
||||
//
|
||||
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 completeSCIMProviderForm: TestProvider = () => [
|
||||
[setTypeCreate, "selectProviderType", "SCIM Provider"],
|
||||
[clickButton, "Next"],
|
||||
[setTextInput, "name", newObjectName("New SCIM Provider")],
|
||||
[setTextInput, "url", "http://example.com:8000/"],
|
||||
[setToggle, "verifyCertificates", false],
|
||||
[setTextInput, "token", "insert-real-token-here"],
|
||||
[setFormGroup, /Protocol settings/, "open"],
|
||||
[setFormGroup, /User filtering/, "open"],
|
||||
[setToggle, "excludeUsersServiceAccount", false],
|
||||
[setSearchSelect, "filterGroup", /authentik Admins/],
|
||||
[setFormGroup, /Attribute mapping/, "open"],
|
||||
[checkIsPresent, '[name="propertyMappings"]'],
|
||||
[checkIsPresent, '[name="propertyMappingsGroup"]'],
|
||||
];
|
||||
|
||||
// provider_components.schemas.ProxyProviderRequest.yml
|
||||
//
|
||||
// - name
|
||||
// - authentication_flow
|
||||
// - authorization_flow
|
||||
// - invalidation_flow
|
||||
// - property_mappings
|
||||
// - internal_host
|
||||
// - external_host
|
||||
// - internal_host_ssl_validation
|
||||
// - certificate
|
||||
// - skip_path_regex
|
||||
// - basic_auth_enabled
|
||||
// - basic_auth_password_attribute
|
||||
// - basic_auth_user_attribute
|
||||
// - mode
|
||||
// - intercept_header_auth
|
||||
// - cookie_domain
|
||||
// - jwks_sources
|
||||
// - access_token_validity
|
||||
// - refresh_token_validity
|
||||
// - refresh_token_validity is not handled in any of our forms. On purpose.
|
||||
// - internal_host_ssl_validation
|
||||
// - only on ProxyMode
|
||||
|
||||
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"],
|
||||
];
|
||||
|
||||
const proxyModeCompletions: TestSequence = [
|
||||
[setTextInput, "accessTokenValidity", "hours=36"],
|
||||
[setFormGroup, /Advanced protocol settings/, "open"],
|
||||
[setSearchSelect, "certificate", /authentik Self-signed Certificate/],
|
||||
[checkIsPresent, '[name="propertyMappings"]'],
|
||||
[setTextareaInput, "skipPathRegex", "."],
|
||||
[setFormGroup, /Authentication settings/, "open"],
|
||||
[setToggle, "interceptHeaderAuth", false],
|
||||
[setToggle, "basicAuthEnabled", true],
|
||||
[setTextInput, "basicAuthUserAttribute", "authorized-user"],
|
||||
[setTextInput, "basicAuthPasswordAttribute", "authorized-user-password"],
|
||||
[setFormGroup, /Advanced flow settings/, "open"],
|
||||
[setSearchSelect, "authenticationFlow", /default-source-authentication/],
|
||||
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
|
||||
[checkIsPresent, '[name="jwksSources"]'],
|
||||
];
|
||||
|
||||
export const completeProxyProviderForm: TestProvider = () => [
|
||||
...simpleProxyProviderForm(),
|
||||
[setToggle, "internalHostSslValidation", false],
|
||||
...proxyModeCompletions,
|
||||
];
|
||||
|
||||
export const completeForwardAuthProxyProviderForm: TestProvider = () => [
|
||||
...simpleForwardAuthProxyProviderForm(),
|
||||
...proxyModeCompletions,
|
||||
];
|
||||
|
||||
export const completeForwardAuthDomainProxyProviderForm: TestProvider = () => [
|
||||
...simpleForwardAuthProxyProviderForm(),
|
||||
...proxyModeCompletions,
|
||||
];
|
98
web/tests/specs/providers.ts
Normal file
98
web/tests/specs/providers.ts
Normal file
@ -0,0 +1,98 @@
|
||||
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 {
|
||||
completeForwardAuthDomainProxyProviderForm,
|
||||
completeForwardAuthProxyProviderForm,
|
||||
completeLDAPProviderForm,
|
||||
completeOAuth2ProviderForm,
|
||||
completeProxyProviderForm,
|
||||
completeRadiusProviderForm,
|
||||
completeSAMLProviderForm,
|
||||
completeSCIMProviderForm,
|
||||
simpleForwardAuthDomainProxyProviderForm,
|
||||
simpleForwardAuthProxyProviderForm,
|
||||
simpleLDAPProviderForm,
|
||||
simpleOAuth2ProviderForm,
|
||||
simpleProxyProviderForm,
|
||||
simpleRadiusProviderForm,
|
||||
simpleSAMLProviderForm,
|
||||
simpleSCIMProviderForm,
|
||||
} from "./provider-shared-sequences.js";
|
||||
|
||||
async function reachTheProvider() {
|
||||
await ProvidersListPage.logout();
|
||||
await login();
|
||||
await ProvidersListPage.open();
|
||||
await expect(await ProvidersListPage.pageHeader()).toHaveText("Providers");
|
||||
await expect(await containedMessages()).not.toContain("Successfully created provider.");
|
||||
|
||||
await ProvidersListPage.startWizardButton.click();
|
||||
await ProviderWizardView.wizardTitle.waitForDisplayed();
|
||||
await expect(await ProviderWizardView.wizardTitle).toHaveText("New provider");
|
||||
}
|
||||
|
||||
const containedMessages = async () =>
|
||||
await (async () => {
|
||||
const messages = [];
|
||||
for await (const alert of $("ak-message-container").$$("ak-message")) {
|
||||
messages.push(await alert.$("p.pf-c-alert__title").getText());
|
||||
}
|
||||
return messages;
|
||||
})();
|
||||
|
||||
const hasProviderSuccessMessage = async () =>
|
||||
await browser.waitUntil(
|
||||
async () => (await containedMessages()).includes("Successfully created provider."),
|
||||
{ timeout: 1000, timeoutMsg: "Expected to see provider success message." },
|
||||
);
|
||||
|
||||
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, so I'm not surprised Typescript doesn't like it."
|
||||
await thefunc.apply($, args);
|
||||
}
|
||||
}
|
||||
|
||||
async function itShouldConfigureASimpleProvider(name: string, provider: TestSequence) {
|
||||
it(`Should successfully configure a ${name} provider`, async () => {
|
||||
await reachTheProvider();
|
||||
await $("ak-wizard-page-type-create").waitForDisplayed();
|
||||
await fillOutFields(provider);
|
||||
await ProviderWizardView.pause();
|
||||
await ProviderWizardView.nextButton.click();
|
||||
await hasProviderSuccessMessage();
|
||||
});
|
||||
}
|
||||
|
||||
type ProviderTest = [string, TestProvider];
|
||||
|
||||
describe("Configuring Providers", () => {
|
||||
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) {
|
||||
itShouldConfigureASimpleProvider(name, provider());
|
||||
}
|
||||
});
|
@ -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) => {
|
||||
|
Reference in New Issue
Block a user