From 89dad07a660cab8115ffa5fb6ff9489cf02c771e Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Wed, 23 Oct 2024 14:17:30 -0700 Subject: [PATCH 1/7] web: Add InvalidationFlow to Radius Provider dialogues ## What - Bugfix: adds the InvalidationFlow to the Radius Provider dialogues - Repairs: `{"invalidation_flow":["This field is required."]}` message, which was *not* propagated to the Notification. - Nitpick: Pretties `?foo=${true}` expressions: `s/\?([^=]+)=\$\{true\}/\1/` ## Note Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current dialogues at the moment. --- ...ication-wizard-authentication-by-radius.ts | 20 +++++++++++ .../providers/radius/RadiusProviderForm.ts | 36 +++++++++++++++---- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts b/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts index c7d6bd0f0e80..fca1666d813e 100644 --- a/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts +++ b/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts @@ -75,6 +75,26 @@ export class ApplicationWizardAuthenticationByRadius extends WithBrandConfig(Bas > + + ${msg("Advanced flow settings")} +
+ + +

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

+
+
`; } } diff --git a/web/src/admin/providers/radius/RadiusProviderForm.ts b/web/src/admin/providers/radius/RadiusProviderForm.ts index 9beb0e115fc1..d3280d8013c7 100644 --- a/web/src/admin/providers/radius/RadiusProviderForm.ts +++ b/web/src/admin/providers/radius/RadiusProviderForm.ts @@ -1,3 +1,5 @@ +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"; @@ -70,7 +72,8 @@ export class RadiusProviderFormPage extends WithBrandConfig(BaseProviderForm + return html` + - + ${msg("Protocol settings")}
-
`; +
+ + ${msg("Advanced flow settings")} +
+ + +

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

+
+
+ `; } } From 852c9969ce831847b5c541ba5af2410d1359f323 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Fri, 15 Nov 2024 10:19:04 -0800 Subject: [PATCH 2/7] Start of dual select revision process. --- .../AuthenticatorValidateStageForm.ts | 101 ++++------------ .../AuthenticatorValidateStageFormHelpers.ts | 50 ++++++++ .../identification/IdentificationStageForm.ts | 112 ++++-------------- .../IdentificationStageFormHelpers.ts | 41 +++++++ .../admin/stages/prompt/PromptStageForm.ts | 94 +++------------ .../stages/prompt/PromptStageFormHelpers.ts | 72 +++++++++++ ...k-dual-select-dynamic-selected-provider.ts | 8 +- 7 files changed, 230 insertions(+), 248 deletions(-) create mode 100644 web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageFormHelpers.ts create mode 100644 web/src/admin/stages/identification/IdentificationStageFormHelpers.ts create mode 100644 web/src/admin/stages/prompt/PromptStageFormHelpers.ts diff --git a/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageForm.ts b/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageForm.ts index 16c94119733b..bcebf8f27406 100644 --- a/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +++ b/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageForm.ts @@ -4,7 +4,6 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import "@goauthentik/elements/Alert"; import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js"; import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider"; -import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types"; import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/Radio"; @@ -20,47 +19,15 @@ import { DeviceClassesEnum, NotConfiguredActionEnum, PaginatedStageList, - Stage, StagesApi, UserVerificationEnum, } from "@goauthentik/api"; -async function stagesProvider(page = 1, search = "") { - const stages = await new StagesApi(DEFAULT_CONFIG).stagesAllList({ - ordering: "name", - pageSize: 20, - search: search.trim(), - page, - }); - - return { - pagination: stages.pagination, - options: stages.results.map((stage) => [stage.pk, `${stage.name} (${stage.verboseName})`]), - }; -} - -export function makeStageSelector(instanceStages: string[] | undefined) { - const localStages = instanceStages ? new Set(instanceStages) : undefined; - - return localStages - ? ([pk, _]: DualSelectPair) => localStages.has(pk) - : ([_0, _1, _2, stage]: DualSelectPair) => stage !== undefined; -} - -async function authenticatorWebauthnDeviceTypesListProvider(page = 1, search = "") { - const devicetypes = await new StagesApi( - DEFAULT_CONFIG, - ).stagesAuthenticatorWebauthnDeviceTypesList({ - pageSize: 20, - search: search.trim(), - page, - }); - - return { - pagination: devicetypes.pagination, - options: devicetypes.results.map(deviceTypeRestrictionPair), - }; -} +import { + authenticatorWebauthnDeviceTypesListProvider, + stagesProvider, + stagesSelector, +} from "./AuthenticatorValidateStageFormHelpers.js"; @customElement("ak-stage-authenticator-validate-form") export class AuthenticatorValidateStageForm extends BaseStageForm { @@ -68,8 +35,7 @@ export class AuthenticatorValidateStageForm extends BaseStageForm ${msg( - "Stage used to validate any authenticator. This stage should be used during authentication or authorization flows.", + "Stage used to validate any authenticator. This stage should be used during authentication or authorization flows." )} - + ${msg("Stage-specific settings")}
- + authenticator[0]) - .filter((name) => - this.isDeviceClassSelected(name as DeviceClassesEnum), - )} + .filter((name) => this.isDeviceClassSelected(name as DeviceClassesEnum))} >

${msg("Device classes which can be used to authenticate.")} @@ -163,7 +118,7 @@ export class AuthenticatorValidateStageForm extends BaseStageForm

${msg( - "If the user has successfully authenticated with a device in the classes listed above within this configured duration, this stage will be skipped.", + "If the user has successfully authenticated with a device in the classes listed above within this configured duration, this stage will be skipped." )}

@@ -177,10 +132,7 @@ export class AuthenticatorValidateStageForm extends BaseStageForm { const target = ev.target as HTMLSelectElement; - if ( - target.selectedOptions[0].value === - NotConfiguredActionEnum.Configure - ) { + if (target.selectedOptions[0].value === NotConfiguredActionEnum.Configure) { this.showConfigurationStages = true; } else { this.showConfigurationStages = false; @@ -189,22 +141,19 @@ export class AuthenticatorValidateStageForm extends BaseStageForm @@ -218,20 +167,18 @@ export class AuthenticatorValidateStageForm extends BaseStageForm

${msg( - "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again.", + "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." )}

${msg( - "When multiple stages are selected, the user can choose which one they want to enroll.", + "When multiple stages are selected, the user can choose which one they want to enroll." )}

@@ -255,9 +202,7 @@ export class AuthenticatorValidateStageForm extends BaseStageForm

${msg( - "Optionally restrict which WebAuthn device types may be used. When no device types are selected, all devices are allowed.", + "Optionally restrict which WebAuthn device types may be used. When no device types are selected, all devices are allowed." )}

${ /* TODO: Remove this after 2024.6..or maybe later? */ - msg( - "This restriction only applies to devices created in authentik 2024.4 or later.", - ) + msg("This restriction only applies to devices created in authentik 2024.4 or later.") }
diff --git a/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageFormHelpers.ts b/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageFormHelpers.ts new file mode 100644 index 000000000000..465de339287b --- /dev/null +++ b/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageFormHelpers.ts @@ -0,0 +1,50 @@ +import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils"; +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; + +import { StagesApi } from "@goauthentik/api"; + +const stageToSelect = (stage: Stage) => [stage.pk, `${stage.name} (${stage.verboseName})`]; + +export async function stagesProvider(page = 1, search = "") { + const stages = await new StagesApi(DEFAULT_CONFIG).stagesAllList({ + ordering: "name", + pageSize: 20, + search: search.trim(), + page, + }); + + return { + pagination: stages.pagination, + options: stages.results.map(stageToSelect), + }; +} + +export function stagesSelector(instanceStages: string[] | undefined) { + if (!instanceStages) { + return async (stages: DualSelectPair) => + stages.filter(([_0, _1, _2, stage]: DualSelectPair) => stage !== undefined); + } + return async () => { + const stagesApi = new StagesApi(DEFAULT_CONFIG); + const stages = await Promise.allSettled( + instanceStages.map((instanceId) => stagesApi.stagesAllRetrieve({ stageUuid: instanceId })) + ); + return stages + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(stageToSelect); + }; +} + +export async function authenticatorWebauthnDeviceTypesListProvider(page = 1, search = "") { + const devicetypes = await new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorWebauthnDeviceTypesList({ + pageSize: 20, + search: search.trim(), + page, + }); + + return { + pagination: devicetypes.pagination, + options: devicetypes.results.map(deviceTypeRestrictionPair), + }; +} diff --git a/web/src/admin/stages/identification/IdentificationStageForm.ts b/web/src/admin/stages/identification/IdentificationStageForm.ts index 7a20af84d612..f65c9785a3f1 100644 --- a/web/src/admin/stages/identification/IdentificationStageForm.ts +++ b/web/src/admin/stages/identification/IdentificationStageForm.ts @@ -4,7 +4,6 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { first, groupBy } from "@goauthentik/common/utils"; import "@goauthentik/elements/ak-checkbox-group/ak-checkbox-group.js"; 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/SearchSelect"; @@ -17,8 +16,6 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { FlowsInstancesListDesignationEnum, IdentificationStage, - Source, - SourcesApi, Stage, StagesApi, StagesCaptchaListRequest, @@ -26,31 +23,7 @@ import { UserFieldsEnum, } from "@goauthentik/api"; -async function sourcesProvider(page = 1, search = "") { - const sources = await new SourcesApi(DEFAULT_CONFIG).sourcesAllList({ - ordering: "slug", - pageSize: 20, - search: search.trim(), - page, - }); - - return { - pagination: sources.pagination, - options: sources.results - .filter((source) => source.component !== "") - .map((source) => [source.pk, source.name, source.name, source]), - }; -} - -function makeSourcesSelector(instanceSources: string[] | undefined) { - const localSources = instanceSources ? new Set(instanceSources) : undefined; - - return localSources - ? ([pk, _]: DualSelectPair) => localSources.has(pk) - : // Creating a new instance, auto-select built-in source only when no other sources exist - ([_0, _1, _2, source]: DualSelectPair) => - source !== undefined && source.component === ""; -} +import { sourcesProvider, sourcesSelector } from "./IdentificationStageFormHelpers.js"; @customElement("ak-stage-identification-form") export class IdentificationStageForm extends BaseStageForm { @@ -99,16 +72,9 @@ export class IdentificationStageForm extends BaseStageForm { name: UserFieldsEnum.Upn, label: msg("UPN") }, ]; - return html` - ${msg("Let the user identify themselves with their username or Email address.")} - + return html` ${msg("Let the user identify themselves with their username or Email address.")} - + ${msg("Stage-specific settings")} @@ -123,7 +89,7 @@ export class IdentificationStageForm extends BaseStageForm >

${msg( - "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources.", + "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources." )}

@@ -136,23 +102,19 @@ export class IdentificationStageForm extends BaseStageForm if (query !== undefined) { args.search = query; } - const stages = await new StagesApi( - DEFAULT_CONFIG, - ).stagesPasswordList(args); + const stages = await new StagesApi(DEFAULT_CONFIG).stagesPasswordList(args); return stages.results; }} - .groupBy=${(items: Stage[]) => - groupBy(items, (stage) => stage.verboseNamePlural)} + .groupBy=${(items: Stage[]) => groupBy(items, (stage) => stage.verboseNamePlural)} .renderElement=${(stage: Stage): string => stage.name} .value=${(stage: Stage | undefined): string | undefined => stage?.pk} - .selected=${(stage: Stage): boolean => - stage.pk === this.instance?.passwordStage} + .selected=${(stage: Stage): boolean => stage.pk === this.instance?.passwordStage} blankable >

${msg( - "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks.", + "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." )}

@@ -165,23 +127,19 @@ export class IdentificationStageForm extends BaseStageForm if (query !== undefined) { args.search = query; } - const stages = await new StagesApi( - DEFAULT_CONFIG, - ).stagesCaptchaList(args); + const stages = await new StagesApi(DEFAULT_CONFIG).stagesCaptchaList(args); return stages.results; }} - .groupBy=${(items: Stage[]) => - groupBy(items, (stage) => stage.verboseNamePlural)} + .groupBy=${(items: Stage[]) => groupBy(items, (stage) => stage.verboseNamePlural)} .renderElement=${(stage: Stage): string => stage.name} .value=${(stage: Stage | undefined): string | undefined => stage?.pk} - .selected=${(stage: Stage): boolean => - stage.pk === this.instance?.captchaStage} + .selected=${(stage: Stage): boolean => stage.pk === this.instance?.captchaStage} blankable >

${msg( - "When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage.", + "When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage." )}

@@ -197,14 +155,10 @@ export class IdentificationStageForm extends BaseStageForm - ${msg("Case insensitive matching")} + ${msg("Case insensitive matching")}

- ${msg( - "When enabled, user fields are matched regardless of their casing.", - )} + ${msg("When enabled, user fields are matched regardless of their casing.")}

@@ -222,9 +176,7 @@ export class IdentificationStageForm extends BaseStageForm ${msg("Pretend user exists")}

- ${msg( - "When enabled, the stage will always accept the given user identifier and continue.", - )} + ${msg("When enabled, the stage will always accept the given user identifier and continue.")}

@@ -243,7 +195,7 @@ export class IdentificationStageForm extends BaseStageForm

${msg( - "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown.", + "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown." )}

@@ -252,20 +204,16 @@ export class IdentificationStageForm extends BaseStageForm ${msg("Source settings")}
- +

${msg( - "Select sources should be shown for users to authenticate with. This only affects web-based sources, not LDAP.", + "Select sources should be shown for users to authenticate with. This only affects web-based sources, not LDAP." )}

@@ -285,7 +233,7 @@ export class IdentificationStageForm extends BaseStageForm

${msg( - "By default, only icons are shown for sources. Enable this to show their full names.", + "By default, only icons are shown for sources. Enable this to show their full names." )}

@@ -294,33 +242,25 @@ export class IdentificationStageForm extends BaseStageForm ${msg("Flow settings")}
- +

${msg( - "Optional passwordless flow, which is linked at the bottom of the page. When configured, users can use this flow to authenticate with a WebAuthn authenticator, without entering any details.", + "Optional passwordless flow, which is linked at the bottom of the page. When configured, users can use this flow to authenticate with a WebAuthn authenticator, without entering any details." )}

- +

- ${msg( - "Optional enrollment flow, which is linked at the bottom of the page.", - )} + ${msg("Optional enrollment flow, which is linked at the bottom of the page.")}

@@ -329,9 +269,7 @@ export class IdentificationStageForm extends BaseStageForm .currentFlow=${this.instance?.recoveryFlow} >

- ${msg( - "Optional recovery flow, which is linked at the bottom of the page.", - )} + ${msg("Optional recovery flow, which is linked at the bottom of the page.")}

diff --git a/web/src/admin/stages/identification/IdentificationStageFormHelpers.ts b/web/src/admin/stages/identification/IdentificationStageFormHelpers.ts new file mode 100644 index 000000000000..1190110fb0b4 --- /dev/null +++ b/web/src/admin/stages/identification/IdentificationStageFormHelpers.ts @@ -0,0 +1,41 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; + +import { + Source, + SourcesApi, +} from "@goauthentik/api"; + +const sourceToSelect = (source: Source) => [source.pk, source.name, source.name, source]; + +export async function sourcesProvider(page = 1, search = "") { + const sources = await new SourcesApi(DEFAULT_CONFIG).sourcesAllList({ + ordering: "slug", + pageSize: 20, + search: search.trim(), + page, + }); + + return { + pagination: sources.pagination, + options: sources.results + .filter((source) => source.component !== "") + .map(sourceToSelect) + }; +} + +export function sourcesSelector(instanceSources: string[] | undefined) { + if (!instanceSources) { + return async (sources: DualSelectPair) => + sources.filter(([_0, _1, _2, source]: DualSelectPair) => source !== undefined); + } + return async () => { + const sourcesApi = new SourcesApi(DEFAULT_CONFIG); + const sources = await Promise.allSettled( + instanceSources.map((instanceId) => sourcesApi.sourcesAllRetrieve({ sourceUuid: instanceId })) + ); + return sources + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(sourceToSelect) + }; +} diff --git a/web/src/admin/stages/prompt/PromptStageForm.ts b/web/src/admin/stages/prompt/PromptStageForm.ts index 2fb9e6e2b63f..1de30218965c 100644 --- a/web/src/admin/stages/prompt/PromptStageForm.ts +++ b/web/src/admin/stages/prompt/PromptStageForm.ts @@ -3,67 +3,23 @@ import "@goauthentik/admin/stages/prompt/PromptForm"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { PFSize } from "@goauthentik/common/enums"; 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/ModalForm"; -import { msg, str } from "@lit/localize"; +import { msg } from "@lit/localize"; import { TemplateResult, html, nothing } from "lit"; import { customElement } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; -import { PoliciesApi, Policy, Prompt, PromptStage, StagesApi } from "@goauthentik/api"; +import { PromptStage, StagesApi } from "@goauthentik/api"; -async function promptFieldsProvider(page = 1, search = "") { - const prompts = await new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsList({ - ordering: "field_name", - pageSize: 20, - search: search.trim(), - page, - }); - - return { - pagination: prompts.pagination, - options: prompts.results.map((prompt) => [ - prompt.pk, - msg(str`${prompt.name} ("${prompt.fieldKey}", of type ${prompt.type})`), - ]), - }; -} - -function makeFieldSelector(instanceFields: string[] | undefined) { - const localFields = instanceFields ? new Set(instanceFields) : undefined; - - return localFields - ? ([pk, _]: DualSelectPair) => localFields.has(pk) - : ([_0, _1, _2, prompt]: DualSelectPair) => prompt !== undefined; -} - -async function policiesProvider(page = 1, search = "") { - const policies = await new PoliciesApi(DEFAULT_CONFIG).policiesAllList({ - ordering: "name", - pageSize: 20, - search: search.trim(), - page, - }); - - return { - pagination: policies.pagination, - options: policies.results.map((policy) => [ - policy.pk, - `${policy.name} (${policy.verboseName})`, - ]), - }; -} - -function makePoliciesSelector(instancePolicies: string[] | undefined) { - const localPolicies = instancePolicies ? new Set(instancePolicies) : undefined; - - return localPolicies - ? ([pk, _]: DualSelectPair) => localPolicies.has(pk) - : ([_0, _1, _2, policy]: DualSelectPair) => policy !== undefined; -} +import { + policiesProvider, + policiesSelector, + promptFieldsProvider, + promptFieldsSelector, +} from "./PromptStageFormHelpers.js"; @customElement("ak-stage-prompt-form") export class PromptStageForm extends BaseStageForm { @@ -89,28 +45,19 @@ export class PromptStageForm extends BaseStageForm { renderForm(): TemplateResult { return html` ${msg( - "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable.", + "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable." )} - + ${msg("Stage-specific settings")}
- + @@ -119,30 +66,21 @@ export class PromptStageForm extends BaseStageForm { ${msg("Create")} ${msg("Create Prompt")} - ` : nothing} - +

- ${msg( - "Selected policies are executed when the stage is submitted to validate the data.", - )} + ${msg("Selected policies are executed when the stage is submitted to validate the data.")}

diff --git a/web/src/admin/stages/prompt/PromptStageFormHelpers.ts b/web/src/admin/stages/prompt/PromptStageFormHelpers.ts new file mode 100644 index 000000000000..da28afe61f13 --- /dev/null +++ b/web/src/admin/stages/prompt/PromptStageFormHelpers.ts @@ -0,0 +1,72 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; + +import { msg, str } from "@lit/localize"; + +import { PoliciesApi, Policy, Prompt, StagesApi } from "@goauthentik/api"; + +export async function promptFieldsProvider(page = 1, search = "") { + const prompts = await new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsList({ + ordering: "field_name", + pageSize: 20, + search: search.trim(), + page, + }); + + return { + pagination: prompts.pagination, + options: prompts.results.map((prompt) => [ + prompt.pk, + msg(str`${prompt.name} ("${prompt.fieldKey}", of type ${prompt.type})`), + ]), + }; +} + +export function promptFieldsSelector(instanceFields: string[] | undefined) { + if (!instanceFields) { + return async (options: DualSelectPair) => + options.filter(([_0, _1, _2, prompt]: DualSelectPair) => prompt !== undefined); + } + return async () => { + const stages = new StagesApi(DEFAULT_CONFIG); + const prompts = await Promise.allSettled( + instanceFields.map((instanceId) => stages.stagesPromptPromptsRetrieve({ promptUuid: instanceId })) + ); + return prompts + .filter((p) => p.status === "fulfilled") + .map((p) => p.value) + .map((p) => [p.pk, msg(str`${p.name} ("${p.fieldKey}", of type ${p.type})`), p.name, p]); + }; +} + +export async function policiesProvider(page = 1, search = "") { + const policies = await new PoliciesApi(DEFAULT_CONFIG).policiesAllList({ + ordering: "name", + pageSize: 20, + search: search.trim(), + page, + }); + + return { + pagination: policies.pagination, + options: policies.results.map((policy) => [policy.pk, `${policy.name} (${policy.verboseName})`]), + }; +} + +export function policiesSelector(instancePolicies: string[] | undefined) { + if (!instancePolicies) { + return async (options: DualSelectPair) => + options.filter(([_0, _1, _2, policy]: DualSelectPair) => policy !== undefined); + } + + return async () => { + const policy = new PoliciesApi(DEFAULT_CONFIG); + const policies = await Promise.allSettled( + instancePolicies.map((instanceId) => policy.policiesAllRetrieve({ policyUuid: instanceId })) + ); + return policies + .filter((p) => p.status === "fulfilled") + .map((p) => p.value) + .map((p) => [p.pk, `${p.name} (${p.verbose_name})`, p.name, p]); + }; +} diff --git a/web/src/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.ts b/web/src/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.ts index 59037776ef2d..2a4d24872834 100644 --- a/web/src/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.ts +++ b/web/src/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.ts @@ -23,7 +23,7 @@ export class AkDualSelectDynamic extends AkDualSelectProvider { * @attr */ @property({ attribute: false }) - selector: ([key, _]: DualSelectPair) => boolean = ([_key, _]) => false; + selector: (_: DualSelectPair[]) => Promise = async (_) => Promise.resolve([]); private firstUpdateHasRun = false; @@ -33,9 +33,9 @@ export class AkDualSelectDynamic extends AkDualSelectProvider { // the selected list with the contents derived from the selector. if (!this.firstUpdateHasRun && this.options.length > 0) { this.firstUpdateHasRun = true; - this.selected = Array.from( - new Set([...this.selected, ...this.options.filter(this.selector)]), - ); + this.selector(this.options).then((selected) => { + this.selected = selected; + }); } } From 667cbf2910dc61852ed8b8025ddebb3421848c2a Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Fri, 15 Nov 2024 12:57:13 -0800 Subject: [PATCH 3/7] Progress. --- web/src/admin/events/RuleFormHelpers.ts | 42 ++++++++++ .../AuthenticatorValidateStageForm.ts | 58 +++++++++---- .../AuthenticatorValidateStageFormHelpers.ts | 15 ++-- .../identification/IdentificationStageForm.ts | 81 +++++++++++++------ .../IdentificationStageFormHelpers.ts | 20 +++-- .../admin/stages/prompt/PromptStageForm.ts | 30 +++++-- .../stages/prompt/PromptStageFormHelpers.ts | 32 +++++--- 7 files changed, 205 insertions(+), 73 deletions(-) create mode 100644 web/src/admin/events/RuleFormHelpers.ts diff --git a/web/src/admin/events/RuleFormHelpers.ts b/web/src/admin/events/RuleFormHelpers.ts new file mode 100644 index 000000000000..f941dc0182c4 --- /dev/null +++ b/web/src/admin/events/RuleFormHelpers.ts @@ -0,0 +1,42 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types"; + +import { EventsApi, NotificationTransport } from "@goauthentik/api"; + +const transportToSelect = (transport: NotificationTransport) => [transport.pk, transport.name]; + +export async function eventTransportsProvider(page = 1, search = "") { + const eventTransports = await new EventsApi(DEFAULT_CONFIG).eventsTransportsList({ + ordering: "name", + pageSize: 20, + search: search.trim(), + page, + }); + + return { + pagination: eventTransports.pagination, + options: eventTransports.results.map(transportToSelect), + }; +} + +export function eventTransportsSelector(instanceTransports: string[] | undefined) { + if (!instanceTransports) { + return async (transports: DualSelectPair[]) => + transports.filter( + ([_0, _1, _2, stage]: DualSelectPair) => stage !== undefined, + ); + } + + return async () => { + const transportsApi = new EventsApi(DEFAULT_CONFIG); + const transports = await Promise.allSettled( + instanceTransports.map((instanceId) => + transportsApi.eventsTransportsRetrieve({ uuid: instanceId }), + ), + ); + return transports + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(transportToSelect); + }; +} diff --git a/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageForm.ts b/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageForm.ts index bcebf8f27406..7fed4be5ccb6 100644 --- a/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +++ b/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageForm.ts @@ -35,7 +35,8 @@ export class AuthenticatorValidateStageForm extends BaseStageForm ${msg( - "Stage used to validate any authenticator. This stage should be used during authentication or authorization flows." + "Stage used to validate any authenticator. This stage should be used during authentication or authorization flows.", )} - + ${msg("Stage-specific settings")}
- + authenticator[0]) - .filter((name) => this.isDeviceClassSelected(name as DeviceClassesEnum))} + .filter((name) => + this.isDeviceClassSelected(name as DeviceClassesEnum), + )} >

${msg("Device classes which can be used to authenticate.")} @@ -118,7 +130,7 @@ export class AuthenticatorValidateStageForm extends BaseStageForm

${msg( - "If the user has successfully authenticated with a device in the classes listed above within this configured duration, this stage will be skipped." + "If the user has successfully authenticated with a device in the classes listed above within this configured duration, this stage will be skipped.", )}

@@ -132,7 +144,10 @@ export class AuthenticatorValidateStageForm extends BaseStageForm { const target = ev.target as HTMLSelectElement; - if (target.selectedOptions[0].value === NotConfiguredActionEnum.Configure) { + if ( + target.selectedOptions[0].value === + NotConfiguredActionEnum.Configure + ) { this.showConfigurationStages = true; } else { this.showConfigurationStages = false; @@ -141,19 +156,22 @@ export class AuthenticatorValidateStageForm extends BaseStageForm @@ -167,18 +185,20 @@ export class AuthenticatorValidateStageForm extends BaseStageForm

${msg( - "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." + "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again.", )}

${msg( - "When multiple stages are selected, the user can choose which one they want to enroll." + "When multiple stages are selected, the user can choose which one they want to enroll.", )}

@@ -202,7 +222,9 @@ export class AuthenticatorValidateStageForm extends BaseStageForm

${msg( - "Optionally restrict which WebAuthn device types may be used. When no device types are selected, all devices are allowed." + "Optionally restrict which WebAuthn device types may be used. When no device types are selected, all devices are allowed.", )}

${ /* TODO: Remove this after 2024.6..or maybe later? */ - msg("This restriction only applies to devices created in authentik 2024.4 or later.") + msg( + "This restriction only applies to devices created in authentik 2024.4 or later.", + ) }
diff --git a/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageFormHelpers.ts b/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageFormHelpers.ts index 465de339287b..205e9fd0b612 100644 --- a/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageFormHelpers.ts +++ b/web/src/admin/stages/authenticator_validate/AuthenticatorValidateStageFormHelpers.ts @@ -1,7 +1,8 @@ import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; -import { StagesApi } from "@goauthentik/api"; +import { Stage, StagesApi } from "@goauthentik/api"; const stageToSelect = (stage: Stage) => [stage.pk, `${stage.name} (${stage.verboseName})`]; @@ -21,13 +22,15 @@ export async function stagesProvider(page = 1, search = "") { export function stagesSelector(instanceStages: string[] | undefined) { if (!instanceStages) { - return async (stages: DualSelectPair) => - stages.filter(([_0, _1, _2, stage]: DualSelectPair) => stage !== undefined); + return async (stages: DualSelectPair[]) => + stages.filter(([_0, _1, _2, stage]: DualSelectPair) => stage !== undefined); } return async () => { const stagesApi = new StagesApi(DEFAULT_CONFIG); const stages = await Promise.allSettled( - instanceStages.map((instanceId) => stagesApi.stagesAllRetrieve({ stageUuid: instanceId })) + instanceStages.map((instanceId) => + stagesApi.stagesAllRetrieve({ stageUuid: instanceId }), + ), ); return stages .filter((s) => s.status === "fulfilled") @@ -37,7 +40,9 @@ export function stagesSelector(instanceStages: string[] | undefined) { } export async function authenticatorWebauthnDeviceTypesListProvider(page = 1, search = "") { - const devicetypes = await new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorWebauthnDeviceTypesList({ + const devicetypes = await new StagesApi( + DEFAULT_CONFIG, + ).stagesAuthenticatorWebauthnDeviceTypesList({ pageSize: 20, search: search.trim(), page, diff --git a/web/src/admin/stages/identification/IdentificationStageForm.ts b/web/src/admin/stages/identification/IdentificationStageForm.ts index f65c9785a3f1..bc2445ddbb2c 100644 --- a/web/src/admin/stages/identification/IdentificationStageForm.ts +++ b/web/src/admin/stages/identification/IdentificationStageForm.ts @@ -72,9 +72,16 @@ export class IdentificationStageForm extends BaseStageForm { name: UserFieldsEnum.Upn, label: msg("UPN") }, ]; - return html` ${msg("Let the user identify themselves with their username or Email address.")} + return html` + ${msg("Let the user identify themselves with their username or Email address.")} + - + ${msg("Stage-specific settings")} @@ -89,7 +96,7 @@ export class IdentificationStageForm extends BaseStageForm >

${msg( - "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources." + "Fields a user can identify themselves with. If no fields are selected, the user will only be able to use sources.", )}

@@ -102,19 +109,23 @@ export class IdentificationStageForm extends BaseStageForm if (query !== undefined) { args.search = query; } - const stages = await new StagesApi(DEFAULT_CONFIG).stagesPasswordList(args); + const stages = await new StagesApi( + DEFAULT_CONFIG, + ).stagesPasswordList(args); return stages.results; }} - .groupBy=${(items: Stage[]) => groupBy(items, (stage) => stage.verboseNamePlural)} + .groupBy=${(items: Stage[]) => + groupBy(items, (stage) => stage.verboseNamePlural)} .renderElement=${(stage: Stage): string => stage.name} .value=${(stage: Stage | undefined): string | undefined => stage?.pk} - .selected=${(stage: Stage): boolean => stage.pk === this.instance?.passwordStage} + .selected=${(stage: Stage): boolean => + stage.pk === this.instance?.passwordStage} blankable >

${msg( - "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." + "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks.", )}

@@ -127,19 +138,23 @@ export class IdentificationStageForm extends BaseStageForm if (query !== undefined) { args.search = query; } - const stages = await new StagesApi(DEFAULT_CONFIG).stagesCaptchaList(args); + const stages = await new StagesApi( + DEFAULT_CONFIG, + ).stagesCaptchaList(args); return stages.results; }} - .groupBy=${(items: Stage[]) => groupBy(items, (stage) => stage.verboseNamePlural)} + .groupBy=${(items: Stage[]) => + groupBy(items, (stage) => stage.verboseNamePlural)} .renderElement=${(stage: Stage): string => stage.name} .value=${(stage: Stage | undefined): string | undefined => stage?.pk} - .selected=${(stage: Stage): boolean => stage.pk === this.instance?.captchaStage} + .selected=${(stage: Stage): boolean => + stage.pk === this.instance?.captchaStage} blankable >

${msg( - "When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage." + "When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage.", )}

@@ -155,10 +170,14 @@ export class IdentificationStageForm extends BaseStageForm - ${msg("Case insensitive matching")} + ${msg("Case insensitive matching")}

- ${msg("When enabled, user fields are matched regardless of their casing.")} + ${msg( + "When enabled, user fields are matched regardless of their casing.", + )}

@@ -176,7 +195,9 @@ export class IdentificationStageForm extends BaseStageForm ${msg("Pretend user exists")}

- ${msg("When enabled, the stage will always accept the given user identifier and continue.")} + ${msg( + "When enabled, the stage will always accept the given user identifier and continue.", + )}

@@ -195,7 +216,7 @@ export class IdentificationStageForm extends BaseStageForm

${msg( - "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown." + "When a valid username/email has been entered, and this option is enabled, the user's username and avatar will be shown. Otherwise, the text that the user entered will be shown.", )}

@@ -204,7 +225,11 @@ export class IdentificationStageForm extends BaseStageForm ${msg("Source settings")}
- + >

${msg( - "Select sources should be shown for users to authenticate with. This only affects web-based sources, not LDAP." + "Select sources should be shown for users to authenticate with. This only affects web-based sources, not LDAP.", )}

@@ -233,7 +258,7 @@ export class IdentificationStageForm extends BaseStageForm

${msg( - "By default, only icons are shown for sources. Enable this to show their full names." + "By default, only icons are shown for sources. Enable this to show their full names.", )}

@@ -242,25 +267,33 @@ export class IdentificationStageForm extends BaseStageForm ${msg("Flow settings")}
- +

${msg( - "Optional passwordless flow, which is linked at the bottom of the page. When configured, users can use this flow to authenticate with a WebAuthn authenticator, without entering any details." + "Optional passwordless flow, which is linked at the bottom of the page. When configured, users can use this flow to authenticate with a WebAuthn authenticator, without entering any details.", )}

- +

- ${msg("Optional enrollment flow, which is linked at the bottom of the page.")} + ${msg( + "Optional enrollment flow, which is linked at the bottom of the page.", + )}

@@ -269,7 +302,9 @@ export class IdentificationStageForm extends BaseStageForm .currentFlow=${this.instance?.recoveryFlow} >

- ${msg("Optional recovery flow, which is linked at the bottom of the page.")} + ${msg( + "Optional recovery flow, which is linked at the bottom of the page.", + )}

diff --git a/web/src/admin/stages/identification/IdentificationStageFormHelpers.ts b/web/src/admin/stages/identification/IdentificationStageFormHelpers.ts index 1190110fb0b4..a6a31590486b 100644 --- a/web/src/admin/stages/identification/IdentificationStageFormHelpers.ts +++ b/web/src/admin/stages/identification/IdentificationStageFormHelpers.ts @@ -1,9 +1,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; -import { - Source, - SourcesApi, -} from "@goauthentik/api"; +import { Source, SourcesApi } from "@goauthentik/api"; const sourceToSelect = (source: Source) => [source.pk, source.name, source.name, source]; @@ -17,25 +15,25 @@ export async function sourcesProvider(page = 1, search = "") { return { pagination: sources.pagination, - options: sources.results - .filter((source) => source.component !== "") - .map(sourceToSelect) + options: sources.results.filter((source) => source.component !== "").map(sourceToSelect), }; } export function sourcesSelector(instanceSources: string[] | undefined) { if (!instanceSources) { - return async (sources: DualSelectPair) => - sources.filter(([_0, _1, _2, source]: DualSelectPair) => source !== undefined); + return async (sources: DualSelectPair[]) => + sources.filter(([_0, _1, _2, source]: DualSelectPair) => source !== undefined); } return async () => { const sourcesApi = new SourcesApi(DEFAULT_CONFIG); const sources = await Promise.allSettled( - instanceSources.map((instanceId) => sourcesApi.sourcesAllRetrieve({ sourceUuid: instanceId })) + instanceSources.map((instanceId) => + sourcesApi.sourcesAllRetrieve({ slug: instanceId }), + ), ); return sources .filter((s) => s.status === "fulfilled") .map((s) => s.value) - .map(sourceToSelect) + .map(sourceToSelect); }; } diff --git a/web/src/admin/stages/prompt/PromptStageForm.ts b/web/src/admin/stages/prompt/PromptStageForm.ts index 1de30218965c..845f12d73d1b 100644 --- a/web/src/admin/stages/prompt/PromptStageForm.ts +++ b/web/src/admin/stages/prompt/PromptStageForm.ts @@ -45,16 +45,25 @@ export class PromptStageForm extends BaseStageForm { renderForm(): TemplateResult { return html` ${msg( - "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable." + "Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable.", )} - + ${msg("Stage-specific settings")}
- + { ${msg("Create")} ${msg("Create Prompt")} - ` : nothing} - + { selected-label="${msg("Selected Fields")}" >

- ${msg("Selected policies are executed when the stage is submitted to validate the data.")} + ${msg( + "Selected policies are executed when the stage is submitted to validate the data.", + )}

diff --git a/web/src/admin/stages/prompt/PromptStageFormHelpers.ts b/web/src/admin/stages/prompt/PromptStageFormHelpers.ts index da28afe61f13..602f408b695b 100644 --- a/web/src/admin/stages/prompt/PromptStageFormHelpers.ts +++ b/web/src/admin/stages/prompt/PromptStageFormHelpers.ts @@ -5,6 +5,13 @@ import { msg, str } from "@lit/localize"; import { PoliciesApi, Policy, Prompt, StagesApi } from "@goauthentik/api"; +const promptToSelect = (p: Prompt) => [ + p.pk, + msg(str`${p.name} ("${p.fieldKey}", of type ${p.type})`), + p.name, + p, +]; + export async function promptFieldsProvider(page = 1, search = "") { const prompts = await new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsList({ ordering: "field_name", @@ -15,30 +22,31 @@ export async function promptFieldsProvider(page = 1, search = "") { return { pagination: prompts.pagination, - options: prompts.results.map((prompt) => [ - prompt.pk, - msg(str`${prompt.name} ("${prompt.fieldKey}", of type ${prompt.type})`), - ]), + options: prompts.results.map(promptToSelect), }; } export function promptFieldsSelector(instanceFields: string[] | undefined) { if (!instanceFields) { - return async (options: DualSelectPair) => + return async (options: DualSelectPair[]) => options.filter(([_0, _1, _2, prompt]: DualSelectPair) => prompt !== undefined); } return async () => { const stages = new StagesApi(DEFAULT_CONFIG); const prompts = await Promise.allSettled( - instanceFields.map((instanceId) => stages.stagesPromptPromptsRetrieve({ promptUuid: instanceId })) + instanceFields.map((instanceId) => + stages.stagesPromptPromptsRetrieve({ promptUuid: instanceId }), + ), ); return prompts .filter((p) => p.status === "fulfilled") .map((p) => p.value) - .map((p) => [p.pk, msg(str`${p.name} ("${p.fieldKey}", of type ${p.type})`), p.name, p]); + .map(promptToSelect); }; } +const policyToSelect = (p: Policy) => [p.pk, `${p.name} (${p.verboseName})`, p.name, p]; + export async function policiesProvider(page = 1, search = "") { const policies = await new PoliciesApi(DEFAULT_CONFIG).policiesAllList({ ordering: "name", @@ -49,24 +57,26 @@ export async function policiesProvider(page = 1, search = "") { return { pagination: policies.pagination, - options: policies.results.map((policy) => [policy.pk, `${policy.name} (${policy.verboseName})`]), + options: policies.results.map(policyToSelect), }; } export function policiesSelector(instancePolicies: string[] | undefined) { if (!instancePolicies) { - return async (options: DualSelectPair) => + return async (options: DualSelectPair[]) => options.filter(([_0, _1, _2, policy]: DualSelectPair) => policy !== undefined); } return async () => { const policy = new PoliciesApi(DEFAULT_CONFIG); const policies = await Promise.allSettled( - instancePolicies.map((instanceId) => policy.policiesAllRetrieve({ policyUuid: instanceId })) + instancePolicies.map((instanceId) => + policy.policiesAllRetrieve({ policyUuid: instanceId }), + ), ); return policies .filter((p) => p.status === "fulfilled") .map((p) => p.value) - .map((p) => [p.pk, `${p.name} (${p.verbose_name})`, p.name, p]); + .map(policyToSelect); }; } From 07a10007cc949e4c6a0207dbd2bd4fb1936e6ce7 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Fri, 15 Nov 2024 12:59:31 -0800 Subject: [PATCH 4/7] Made the RuleFormHelper's dualselect conform. --- web/src/admin/events/RuleForm.ts | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/web/src/admin/events/RuleForm.ts b/web/src/admin/events/RuleForm.ts index 24ec962f4d7c..7d89033649c6 100644 --- a/web/src/admin/events/RuleForm.ts +++ b/web/src/admin/events/RuleForm.ts @@ -1,7 +1,6 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { severityToLabel } from "@goauthentik/common/labels"; import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js"; -import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types"; import "@goauthentik/elements/forms/HorizontalFormElement"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import "@goauthentik/elements/forms/Radio"; @@ -18,32 +17,12 @@ import { EventsApi, Group, NotificationRule, - NotificationTransport, PaginatedNotificationTransportList, SeverityEnum, } from "@goauthentik/api"; -async function eventTransportsProvider(page = 1, search = "") { - const eventTransports = await new EventsApi(DEFAULT_CONFIG).eventsTransportsList({ - ordering: "name", - pageSize: 20, - search: search.trim(), - page, - }); +import { eventTransportsProvider, eventTransportsSelector } from "./RuleFormHelpers.js"; - return { - pagination: eventTransports.pagination, - options: eventTransports.results.map((transport) => [transport.pk, transport.name]), - }; -} - -export function makeTransportSelector(instanceTransports: string[] | undefined) { - const localTransports = instanceTransports ? new Set(instanceTransports) : undefined; - - return localTransports - ? ([pk, _]: DualSelectPair) => localTransports.has(pk) - : ([_0, _1, _2, stage]: DualSelectPair) => stage !== undefined; -} @customElement("ak-event-rule-form") export class RuleForm extends ModelForm { eventTransports?: PaginatedNotificationTransportList; @@ -126,7 +105,7 @@ export class RuleForm extends ModelForm { > From c419efe328edf5ac1dbf706f29b09e03ea615396 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Fri, 15 Nov 2024 13:54:28 -0800 Subject: [PATCH 5/7] Providers and Selectors harmonized for sources. --- .../sources/kerberos/KerberosSourceForm.ts | 32 ++----------- .../kerberos/KerberosSourceFormHelpers.ts | 46 +++++++++++++++++++ web/src/admin/sources/ldap/LDAPSourceForm.ts | 32 ++----------- .../sources/ldap/LDAPSourceFormHelpers.ts | 46 +++++++++++++++++++ .../admin/sources/oauth/OAuthSourceForm.ts | 29 ++---------- .../sources/oauth/OAuthSourceFormHelpers.ts | 44 ++++++++++++++++++ web/src/admin/sources/plex/PlexSourceForm.ts | 29 ++---------- .../sources/plex/PlexSourceFormHelpers.ts | 42 +++++++++++++++++ web/src/admin/sources/saml/SAMLSourceForm.ts | 29 ++---------- .../sources/saml/SAMLSourceFormHelpers.ts | 42 +++++++++++++++++ web/src/admin/sources/scim/SCIMSourceForm.ts | 35 ++------------ .../sources/scim/SCIMSourceFormHelpers.ts | 42 +++++++++++++++++ 12 files changed, 281 insertions(+), 167 deletions(-) create mode 100644 web/src/admin/sources/kerberos/KerberosSourceFormHelpers.ts create mode 100644 web/src/admin/sources/ldap/LDAPSourceFormHelpers.ts create mode 100644 web/src/admin/sources/oauth/OAuthSourceFormHelpers.ts create mode 100644 web/src/admin/sources/plex/PlexSourceFormHelpers.ts create mode 100644 web/src/admin/sources/saml/SAMLSourceFormHelpers.ts create mode 100644 web/src/admin/sources/scim/SCIMSourceFormHelpers.ts diff --git a/web/src/admin/sources/kerberos/KerberosSourceForm.ts b/web/src/admin/sources/kerberos/KerberosSourceForm.ts index a388c9ef33c6..e7d8c2ef1b2b 100644 --- a/web/src/admin/sources/kerberos/KerberosSourceForm.ts +++ b/web/src/admin/sources/kerberos/KerberosSourceForm.ts @@ -15,7 +15,6 @@ import { WithCapabilitiesConfig, } from "@goauthentik/elements/Interface/capabilitiesProvider"; 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/SearchSelect"; @@ -29,37 +28,12 @@ import { FlowsInstancesListDesignationEnum, GroupMatchingModeEnum, KerberosSource, - KerberosSourcePropertyMapping, KerberosSourceRequest, - PropertymappingsApi, SourcesApi, UserMatchingModeEnum, } from "@goauthentik/api"; -async function propertyMappingsProvider(page = 1, search = "") { - const propertyMappings = await new PropertymappingsApi( - DEFAULT_CONFIG, - ).propertymappingsSourceKerberosList({ - ordering: "managed", - pageSize: 20, - search: search.trim(), - page, - }); - - return { - pagination: propertyMappings.pagination, - options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]), - }; -} - -function makePropertyMappingsSelector(object: string, instanceMappings?: string[]) { - const localMappings = instanceMappings ? new Set(instanceMappings) : undefined; - return localMappings - ? ([pk, _]: DualSelectPair) => localMappings.has(pk) - : ([_0, _1, _2, mapping]: DualSelectPair) => - object == "user" && - mapping?.managed?.startsWith("goauthentik.io/sources/kerberos/user/default/"); -} +import { propertyMappingsProvider, propertyMappingsSelector } from "./KerberosSourceFormHelpers.js"; @customElement("ak-source-kerberos-form") export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm) { @@ -323,7 +297,7 @@ export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm [m.pk, m.name, m.name, m]; + +export async function propertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsSourceKerberosList({ + ordering: "managed", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map(mappingToSelect), + }; +} + +export function propertyMappingsSelector(object: string, instanceMappings?: string[]) { + if (!instanceMappings) { + return async (mappings: DualSelectPair[]) => + mappings.filter( + ([_0, _1, _2, mapping]: DualSelectPair) => + object == "user" && + mapping?.managed?.startsWith("goauthentik.io/sources/kerberos/user/default/"), + ); + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsSourceKerberosRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} diff --git a/web/src/admin/sources/ldap/LDAPSourceForm.ts b/web/src/admin/sources/ldap/LDAPSourceForm.ts index ecca7d4a7df5..0905e192ce78 100644 --- a/web/src/admin/sources/ldap/LDAPSourceForm.ts +++ b/web/src/admin/sources/ldap/LDAPSourceForm.ts @@ -4,7 +4,6 @@ import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm"; 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/SearchSelect"; @@ -19,36 +18,11 @@ import { CoreGroupsListRequest, Group, LDAPSource, - LDAPSourcePropertyMapping, LDAPSourceRequest, - PropertymappingsApi, SourcesApi, } from "@goauthentik/api"; -async function propertyMappingsProvider(page = 1, search = "") { - const propertyMappings = await new PropertymappingsApi( - DEFAULT_CONFIG, - ).propertymappingsSourceLdapList({ - ordering: "managed", - pageSize: 20, - search: search.trim(), - page, - }); - - return { - pagination: propertyMappings.pagination, - options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]), - }; -} - -function makePropertyMappingsSelector(instanceMappings?: string[]) { - const localMappings = instanceMappings ? new Set(instanceMappings) : undefined; - return localMappings - ? ([pk, _]: DualSelectPair) => localMappings.has(pk) - : ([_0, _1, _2, mapping]: DualSelectPair) => - mapping?.managed?.startsWith("goauthentik.io/sources/ldap/default") || - mapping?.managed?.startsWith("goauthentik.io/sources/ldap/ms"); -} +import { propertyMappingsProvider, propertyMappingsSelector } from "./LDAPSourceFormHelpers.js"; @customElement("ak-source-ldap-form") export class LDAPSourceForm extends BaseSourceForm { @@ -296,7 +270,7 @@ export class LDAPSourceForm extends BaseSourceForm { > { > [m.pk, m.name, m.name, m]; + +export async function propertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsSourceLdapList({ + ordering: "managed", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map(mappingToSelect), + }; +} + +export function propertyMappingsSelector(instanceMappings?: string[]) { + if (!instanceMappings) { + return async (transports: DualSelectPair[]) => + transports.filter( + ([_0, _1, _2, mapping]: DualSelectPair) => + mapping?.managed?.startsWith("goauthentik.io/sources/ldap/default") || + mapping?.managed?.startsWith("goauthentik.io/sources/ldap/ms"), + ); + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsSourceLdapRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} diff --git a/web/src/admin/sources/oauth/OAuthSourceForm.ts b/web/src/admin/sources/oauth/OAuthSourceForm.ts index a1b7b2ce082a..04fce8b888b5 100644 --- a/web/src/admin/sources/oauth/OAuthSourceForm.ts +++ b/web/src/admin/sources/oauth/OAuthSourceForm.ts @@ -14,7 +14,6 @@ import { WithCapabilitiesConfig, } from "@goauthentik/elements/Interface/capabilitiesProvider"; 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/SearchSelect"; @@ -28,36 +27,14 @@ import { FlowsInstancesListDesignationEnum, GroupMatchingModeEnum, OAuthSource, - OAuthSourcePropertyMapping, OAuthSourceRequest, - PropertymappingsApi, ProviderTypeEnum, SourceType, SourcesApi, UserMatchingModeEnum, } from "@goauthentik/api"; -async function propertyMappingsProvider(page = 1, search = "") { - const propertyMappings = await new PropertymappingsApi( - DEFAULT_CONFIG, - ).propertymappingsSourceOauthList({ - ordering: "managed", - pageSize: 20, - search: search.trim(), - page, - }); - return { - pagination: propertyMappings.pagination, - options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]), - }; -} - -function makePropertyMappingsSelector(instanceMappings?: string[]) { - const localMappings = instanceMappings ? new Set(instanceMappings) : undefined; - return localMappings - ? ([pk, _]: DualSelectPair) => localMappings.has(pk) - : ([_0, _1, _2, _]: DualSelectPair) => false; -} +import { propertyMappingsProvider, propertyMappingsSelector } from "./OAuthSourceFormHelpers.js"; @customElement("ak-source-oauth-form") export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm) { @@ -467,7 +444,7 @@ export class OAuthSourceForm extends WithCapabilitiesConfig(BaseSourceForm [m.pk, m.name, m.name, m]; + +export async function propertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsSourceOauthList({ + ordering: "managed", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map(mappingToSelect), + }; +} + +export function propertyMappingsSelector(instanceMappings?: string[]) { + if (!instanceMappings) { + return async (mappings: DualSelectPair[]) => + mappings.filter( + ([_0, _1, _2, _3]: DualSelectPair) => false, + ); + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsSourceOauthRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} diff --git a/web/src/admin/sources/plex/PlexSourceForm.ts b/web/src/admin/sources/plex/PlexSourceForm.ts index 347954b76a5e..08f9c30ccd39 100644 --- a/web/src/admin/sources/plex/PlexSourceForm.ts +++ b/web/src/admin/sources/plex/PlexSourceForm.ts @@ -15,7 +15,6 @@ import { import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.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 { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/SearchSelect"; @@ -29,33 +28,11 @@ import { FlowsInstancesListDesignationEnum, GroupMatchingModeEnum, PlexSource, - PlexSourcePropertyMapping, - PropertymappingsApi, SourcesApi, UserMatchingModeEnum, } from "@goauthentik/api"; -async function propertyMappingsProvider(page = 1, search = "") { - const propertyMappings = await new PropertymappingsApi( - DEFAULT_CONFIG, - ).propertymappingsSourcePlexList({ - ordering: "managed", - pageSize: 20, - search: search.trim(), - page, - }); - return { - pagination: propertyMappings.pagination, - options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]), - }; -} - -function makePropertyMappingsSelector(instanceMappings?: string[]) { - const localMappings = instanceMappings ? new Set(instanceMappings) : undefined; - return localMappings - ? ([pk, _]: DualSelectPair) => localMappings.has(pk) - : ([_0, _1, _2, _]: DualSelectPair) => false; -} +import { propertyMappingsProvider, propertyMappingsSelector } from "./PlexSourceFormHelpers.js"; @customElement("ak-source-plex-form") export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm) { @@ -420,7 +397,7 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm [m.pk, m.name, m.name, m]; + +export async function propertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsSourcePlexList({ + ordering: "managed", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map(mappingToSelect), + }; +} + +export function propertyMappingsSelector(instanceMappings?: string[]) { + if (!instanceMappings) { + return async (mappings: DualSelectPair[]) => + mappings.filter(([_0, _1, _2, _3]: DualSelectPair) => false); + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsSourcePlexRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} diff --git a/web/src/admin/sources/saml/SAMLSourceForm.ts b/web/src/admin/sources/saml/SAMLSourceForm.ts index 599928c613c4..dc1dfc09105e 100644 --- a/web/src/admin/sources/saml/SAMLSourceForm.ts +++ b/web/src/admin/sources/saml/SAMLSourceForm.ts @@ -13,7 +13,6 @@ import { WithCapabilitiesConfig, } from "@goauthentik/elements/Interface/capabilitiesProvider"; 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"; @@ -30,35 +29,13 @@ import { FlowsInstancesListDesignationEnum, GroupMatchingModeEnum, NameIdPolicyEnum, - PropertymappingsApi, SAMLSource, - SAMLSourcePropertyMapping, SignatureAlgorithmEnum, SourcesApi, UserMatchingModeEnum, } from "@goauthentik/api"; -async function propertyMappingsProvider(page = 1, search = "") { - const propertyMappings = await new PropertymappingsApi( - DEFAULT_CONFIG, - ).propertymappingsSourceSamlList({ - ordering: "managed", - pageSize: 20, - search: search.trim(), - page, - }); - return { - pagination: propertyMappings.pagination, - options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]), - }; -} - -function makePropertyMappingsSelector(instanceMappings?: string[]) { - const localMappings = instanceMappings ? new Set(instanceMappings) : undefined; - return localMappings - ? ([pk, _]: DualSelectPair) => localMappings.has(pk) - : ([_0, _1, _2, _]: DualSelectPair) => false; -} +import { propertyMappingsProvider, propertyMappingsSelector } from "./SAMLSourceFormHelpers.js"; @customElement("ak-source-saml-form") export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm) { @@ -532,7 +509,7 @@ export class SAMLSourceForm extends WithCapabilitiesConfig(BaseSourceForm [m.pk, m.name, m.name, m]; + +export async function propertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsSourceSamlList({ + ordering: "managed", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map(mappingToSelect), + }; +} + +export function propertyMappingsSelector(instanceMappings?: string[]) { + if (!instanceMappings) { + return async (mappings: DualSelectPair[]) => + mappings.filter(([_0, _1, _2, _4]: DualSelectPair) => false); + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsSourceSamlRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} diff --git a/web/src/admin/sources/scim/SCIMSourceForm.ts b/web/src/admin/sources/scim/SCIMSourceForm.ts index c156d2166394..61546f6cdb4f 100644 --- a/web/src/admin/sources/scim/SCIMSourceForm.ts +++ b/web/src/admin/sources/scim/SCIMSourceForm.ts @@ -3,7 +3,6 @@ import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm"; 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"; @@ -12,35 +11,9 @@ import { TemplateResult, html } from "lit"; import { customElement } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; -import { - PropertymappingsApi, - SCIMSource, - SCIMSourcePropertyMapping, - SCIMSourceRequest, - SourcesApi, -} from "@goauthentik/api"; +import { SCIMSource, SCIMSourceRequest, SourcesApi } from "@goauthentik/api"; -async function propertyMappingsProvider(page = 1, search = "") { - const propertyMappings = await new PropertymappingsApi( - DEFAULT_CONFIG, - ).propertymappingsSourceScimList({ - ordering: "managed", - pageSize: 20, - search: search.trim(), - page, - }); - return { - pagination: propertyMappings.pagination, - options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]), - }; -} - -function makePropertyMappingsSelector(instanceMappings?: string[]) { - const localMappings = instanceMappings ? new Set(instanceMappings) : undefined; - return localMappings - ? ([pk, _]: DualSelectPair) => localMappings.has(pk) - : ([_0, _1, _2, _]: DualSelectPair) => false; -} +import { propertyMappingsProvider, propertyMappingsSelector } from "./SCIMSourceFormHelpers.js"; @customElement("ak-source-scim-form") export class SCIMSourceForm extends BaseSourceForm { @@ -104,7 +77,7 @@ export class SCIMSourceForm extends BaseSourceForm { > { > [m.pk, m.name, m.name, m]; + +export async function propertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsSourceScimList({ + ordering: "managed", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map(mappingToSelect), + }; +} + +export function propertyMappingsSelector(instanceMappings?: string[]) { + if (!instanceMappings) { + return async (mappings: DualSelectPair[]) => + mappings.filter(([_0, _1, _2, _3]: DualSelectPair) => false); + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsSourceScimRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} From 3f578961d459fe24291b1071824d8796202625fb Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Fri, 15 Nov 2024 15:53:21 -0800 Subject: [PATCH 6/7] web/bugfix/dual-select-full-options # What - Replaces the dual-select "selected" list mechanism with a more comprehensive (if computationally expensive) version that is correct. # How In the previous iteration, each dual select controller gets a *provider* and a *selector*; the latter keeps the keys of all the objects a specific instance may have, and marks those objects as "selected" when they appear in the dual-selects "selected" panel. In order to distinguish between "selected on the existing instance" and "selected by the user," the *selector* only runs at construction time, creating a unified "selected" list; this is standard and allows for a uniform experience of adding and deleting items. Unfortunately, this means that the "selected" items, because their displays are crafted bespoke, are only chosen from those available at construction. If there are selected items later in the paginated collection, they will not be marked as selected. This defeats the purpose of having a paginated multi-select! The correct way to do this is to retrieve every item pased to the *selector* and use the same algorithm to craft the views in both windows. For every instance of Dual Select with dynamic selection, the *provider* and *selector* have been put in a separate file (usually suffixed as a `*FormHelper.ts` file); the algorithm by which an item is crafted for use by DualSelect has been broken out into a small function (usually named `*toSelect()`). The *provider* works as before. The *selector* takes every instance key passed to it and runs a `Promise.allSettled(...*Retrieve({ uuid: instanceId }))` on them, mapping them onto the `selected` collection using the same `*toSelect()`, so they resemble the possibilities in every way. # Lessons This exercise emphasizes just how much sheer *repetition* the Django REST API creates on the client side. Every Helper file is a copy-pasta of a sibling, with only a few minor changes: - How the objects are turned into displays for DualSelect - The type and calls being used; - The field on which retrival is defined - The defaulting rule. There are 19 `*FormHelper` files, and each one is 50 lines long. That's 950 lines of code. Of those 950 lines of code, 874 of those lines are *complete duplicates* of those in the other FormHelper files. Only 76 lines are unique. This language really needs macros. That, or I need to seriously level up my Typescript and figure out how to make this whole thing a lot smarter. --- ...lication-wizard-authentication-by-oauth.ts | 18 +++---- .../proxy/AuthenticationByProxyPage.ts | 14 ++--- ...plication-wizard-authentication-for-rac.ts | 12 ++--- .../GoogleWorkspaceProviderForm.ts | 14 ++--- .../GoogleWorkspaceProviderFormHelpers.ts | 48 +++++++++++++++++ ...GoogleWorkspaceProviderPropertyMappings.ts | 30 ----------- .../providers/ldap/LDAPProviderFormHelpers.ts | 46 +++++++++++++++++ .../MicrosoftEntraProviderForm.ts | 14 ++--- .../MicrosoftEntraProviderFormHelpers.ts | 48 +++++++++++++++++ .../oauth2/OAuth2PropertyMappings.ts | 33 ------------ .../providers/oauth2/OAuth2ProviderForm.ts | 15 ++---- .../oauth2/OAuth2ProviderFormHelpers.ts | 51 +++++++++++++++++++ .../admin/providers/oauth2/OAuth2Sources.ts | 37 ++++++++++---- .../providers/proxy/ProxyProviderForm.ts | 15 ++---- .../proxy/ProxyProviderFormHelpers.ts | 45 ++++++++++++++++ .../proxy/ProxyProviderPropertyMappings.ts | 27 ---------- web/src/admin/providers/rac/EndpointForm.ts | 9 ++-- .../providers/rac/RACPropertyMappings.ts | 24 --------- .../admin/providers/rac/RACProviderForm.ts | 11 ++-- .../providers/rac/RACProviderFormHelpers.ts | 41 +++++++++++++++ .../providers/radius/RadiusProviderForm.ts | 37 ++------------ .../radius/RadiusProviderFormHelpers.ts | 41 +++++++++++++++ .../admin/providers/saml/SAMLProviderForm.ts | 30 ++--------- .../providers/saml/SAMLProviderFormHelpers.ts | 44 ++++++++++++++++ .../admin/providers/scim/SCIMProviderForm.ts | 37 ++------------ .../providers/scim/SCIMProviderFormHelpers.ts | 48 +++++++++++++++++ 26 files changed, 502 insertions(+), 287 deletions(-) create mode 100644 web/src/admin/providers/google_workspace/GoogleWorkspaceProviderFormHelpers.ts delete mode 100644 web/src/admin/providers/google_workspace/GoogleWorkspaceProviderPropertyMappings.ts create mode 100644 web/src/admin/providers/ldap/LDAPProviderFormHelpers.ts create mode 100644 web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderFormHelpers.ts delete mode 100644 web/src/admin/providers/oauth2/OAuth2PropertyMappings.ts create mode 100644 web/src/admin/providers/oauth2/OAuth2ProviderFormHelpers.ts create mode 100644 web/src/admin/providers/proxy/ProxyProviderFormHelpers.ts delete mode 100644 web/src/admin/providers/proxy/ProxyProviderPropertyMappings.ts delete mode 100644 web/src/admin/providers/rac/RACPropertyMappings.ts create mode 100644 web/src/admin/providers/rac/RACProviderFormHelpers.ts create mode 100644 web/src/admin/providers/radius/RadiusProviderFormHelpers.ts create mode 100644 web/src/admin/providers/saml/SAMLProviderFormHelpers.ts create mode 100644 web/src/admin/providers/scim/SCIMProviderFormHelpers.ts diff --git a/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts b/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts index db1d6517f828..adeee893d474 100644 --- a/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts +++ b/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts @@ -1,10 +1,6 @@ import "@goauthentik/admin/applications/wizard/ak-wizard-title"; import "@goauthentik/admin/common/ak-crypto-certificate-search"; import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search"; -import { - makeOAuth2PropertyMappingsSelector, - oauth2PropertyMappingsProvider, -} from "@goauthentik/admin/providers/oauth2/OAuth2PropertyMappings.js"; import { clientTypeOptions, issuerModeOptions, @@ -12,8 +8,12 @@ import { subjectModeOptions, } from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm"; import { - makeSourceSelector, + propertyMappingsProvider, + propertyMappingsSelector, +} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderFormHelpers.js"; +import { oauth2SourcesProvider, + oauth2SourcesSelector, } from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils"; @@ -229,10 +229,8 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel { .errorMessages=${errors?.propertyMappings ?? []} > @@ -286,7 +284,7 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel { > diff --git a/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts b/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts index 867efbd0b383..6219beaa0aa8 100644 --- a/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts +++ b/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts @@ -1,12 +1,12 @@ import "@goauthentik/admin/applications/wizard/ak-wizard-title"; import { - makeSourceSelector, oauth2SourcesProvider, + oauth2SourcesSelector, } from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js"; import { - makeProxyPropertyMappingsSelector, - proxyPropertyMappingsProvider, -} from "@goauthentik/admin/providers/proxy/ProxyProviderPropertyMappings.js"; + propertyMappingsProvider, + propertyMappingsSelector, +} from "@goauthentik/admin/providers/proxy/ProxyProviderFormHelpers.js"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { first } from "@goauthentik/common/utils"; import "@goauthentik/components/ak-switch-input"; @@ -147,8 +147,8 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel { name="propertyMappings" > diff --git a/web/src/admin/applications/wizard/methods/rac/ak-application-wizard-authentication-for-rac.ts b/web/src/admin/applications/wizard/methods/rac/ak-application-wizard-authentication-for-rac.ts index 27b7a2fb7ed0..15ab15fb910b 100644 --- a/web/src/admin/applications/wizard/methods/rac/ak-application-wizard-authentication-for-rac.ts +++ b/web/src/admin/applications/wizard/methods/rac/ak-application-wizard-authentication-for-rac.ts @@ -1,9 +1,9 @@ import "@goauthentik/admin/applications/wizard/ak-wizard-title"; import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; import { - makeRACPropertyMappingsSelector, - racPropertyMappingsProvider, -} from "@goauthentik/admin/providers/rac/RACPropertyMappings.js"; + propertyMappingsProvider, + propertyMappingsSelector, +} from "@goauthentik/admin/providers/rac/RACProviderFormHelpers.js"; import "@goauthentik/components/ak-text-input"; import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js"; @@ -70,10 +70,8 @@ export class ApplicationWizardAuthenticationByRAC extends BaseProviderPanel { name="propertyMappings" > diff --git a/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderForm.ts b/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderForm.ts index 86f2598db0ac..952685355abf 100644 --- a/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderForm.ts +++ b/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderForm.ts @@ -1,8 +1,8 @@ import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"; import { - googleWorkspacePropertyMappingsProvider, - makeGoogleWorkspacePropertyMappingsSelector, -} from "@goauthentik/admin/providers/google_workspace/GoogleWorkspaceProviderPropertyMappings"; + propertyMappingsProvider, + propertyMappingsSelector, +} from "@goauthentik/admin/providers/google_workspace/GoogleWorkspaceProviderFormHelpers.js"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { first } from "@goauthentik/common/utils"; import "@goauthentik/elements/CodeMirror"; @@ -224,8 +224,8 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm [m.pk, m.name, m.name, m]; + +export async function propertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsProviderGoogleWorkspaceList({ + ordering: "managed", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map(mappingToSelect), + }; +} + +export function propertyMappingsSelector( + instanceMappings: string[] | undefined, + defaultSelection: string, +) { + if (!instanceMappings) { + return async (mappings: DualSelectPair[]) => + mappings.filter( + ([_0, _1, _2, mapping]: DualSelectPair) => + mapping?.managed === defaultSelection, + ); + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsProviderGoogleWorkspaceRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} diff --git a/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderPropertyMappings.ts b/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderPropertyMappings.ts deleted file mode 100644 index 36996885b98f..000000000000 --- a/web/src/admin/providers/google_workspace/GoogleWorkspaceProviderPropertyMappings.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; - -import { PropertymappingsApi, ScopeMapping } from "@goauthentik/api"; - -export async function googleWorkspacePropertyMappingsProvider(page = 1, search = "") { - const propertyMappings = await new PropertymappingsApi( - DEFAULT_CONFIG, - ).propertymappingsProviderGoogleWorkspaceList({ - ordering: "managed", - pageSize: 20, - search: search.trim(), - page, - }); - return { - pagination: propertyMappings.pagination, - options: propertyMappings.results.map((scope) => [scope.pk, scope.name, scope.name, scope]), - }; -} - -export function makeGoogleWorkspacePropertyMappingsSelector( - instanceMappings: string[] | undefined, - defaultSelection: string, -) { - const localMappings = instanceMappings ? new Set(instanceMappings) : undefined; - return localMappings - ? ([pk, _]: DualSelectPair) => localMappings.has(pk) - : ([_0, _1, _2, scope]: DualSelectPair) => - scope?.managed === defaultSelection; -} diff --git a/web/src/admin/providers/ldap/LDAPProviderFormHelpers.ts b/web/src/admin/providers/ldap/LDAPProviderFormHelpers.ts new file mode 100644 index 000000000000..08c66e423db6 --- /dev/null +++ b/web/src/admin/providers/ldap/LDAPProviderFormHelpers.ts @@ -0,0 +1,46 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; + +import { LDAPSourcePropertyMapping, PropertymappingsApi } from "@goauthentik/api"; + +const mappingToSelect = (m: LDAPSourcePropertyMapping) => [m.pk, m.name, m.name, m]; + +export async function propertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsSourceLdapList({ + ordering: "managed", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map(mappingToSelect), + }; +} + +export function propertyMappingsSelector(instanceMappings?: string[]) { + if (!instanceMappings) { + return async (transports: DualSelectPair[]) => + transports.filter( + ([_0, _1, _2, mapping]: DualSelectPair) => + mapping?.managed?.startsWith("goauthentik.io/sources/ldap/default") || + mapping?.managed?.startsWith("goauthentik.io/sources/ldap/ms"), + ); + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsSourceLdapRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} diff --git a/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderForm.ts b/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderForm.ts index de2eb396ebc8..523af5cc22d0 100644 --- a/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderForm.ts +++ b/web/src/admin/providers/microsoft_entra/MicrosoftEntraProviderForm.ts @@ -1,8 +1,8 @@ import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"; import { - makeMicrosoftEntraPropertyMappingsSelector, - microsoftEntraPropertyMappingsProvider, -} from "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderPropertyMappings"; + propertyMappingsProvider, + propertyMappingsSelector, +} from "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderFormHelpers.js"; 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"; @@ -213,8 +213,8 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm [m.pk, m.name, m.name, m]; + +export async function propertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsProviderMicrosoftEntraList({ + ordering: "managed", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map(mappingToSelect), + }; +} + +export function propertyMappingsSelector( + instanceMappings: string[] | undefined, + defaultSelection: string, +) { + if (!instanceMappings) { + return async (mappings: DualSelectPair[]) => + mappings.filter( + ([_0, _1, _2, mapping]: DualSelectPair) => + mapping?.managed === defaultSelection, + ); + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsProviderMicrosoftEntraRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} diff --git a/web/src/admin/providers/oauth2/OAuth2PropertyMappings.ts b/web/src/admin/providers/oauth2/OAuth2PropertyMappings.ts deleted file mode 100644 index 2c4cc45e5bd3..000000000000 --- a/web/src/admin/providers/oauth2/OAuth2PropertyMappings.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; - -import { PropertymappingsApi, ScopeMapping } from "@goauthentik/api"; - -export const defaultScopes = [ - "goauthentik.io/providers/oauth2/scope-openid", - "goauthentik.io/providers/oauth2/scope-email", - "goauthentik.io/providers/oauth2/scope-profile", -]; - -export async function oauth2PropertyMappingsProvider(page = 1, search = "") { - const propertyMappings = await new PropertymappingsApi( - DEFAULT_CONFIG, - ).propertymappingsProviderScopeList({ - ordering: "scope_name", - pageSize: 20, - search: search.trim(), - page, - }); - return { - pagination: propertyMappings.pagination, - options: propertyMappings.results.map((scope) => [scope.pk, scope.name, scope.name, scope]), - }; -} - -export function makeOAuth2PropertyMappingsSelector(instanceMappings: string[] | undefined) { - const localMappings = instanceMappings ? new Set(instanceMappings) : undefined; - return localMappings - ? ([pk, _]: DualSelectPair) => localMappings.has(pk) - : ([_0, _1, _2, scope]: DualSelectPair) => - scope?.managed && defaultScopes.includes(scope?.managed); -} diff --git a/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts b/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts index dd4d8b2a5579..b18a4fddc8f7 100644 --- a/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts +++ b/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts @@ -28,11 +28,8 @@ import { SubModeEnum, } from "@goauthentik/api"; -import { - makeOAuth2PropertyMappingsSelector, - oauth2PropertyMappingsProvider, -} from "./OAuth2PropertyMappings.js"; -import { makeSourceSelector, oauth2SourcesProvider } from "./OAuth2Sources.js"; +import { propertyMappingsProvider, propertyMappingsSelector } from "./OAuth2ProviderFormHelpers.js"; +import { oauth2SourcesProvider, oauth2SourcesSelector } from "./OAuth2Sources.js"; export const clientTypeOptions = [ { @@ -305,10 +302,8 @@ export class OAuth2ProviderFormPage extends BaseProviderForm { @@ -361,7 +356,7 @@ export class OAuth2ProviderFormPage extends BaseProviderForm { > diff --git a/web/src/admin/providers/oauth2/OAuth2ProviderFormHelpers.ts b/web/src/admin/providers/oauth2/OAuth2ProviderFormHelpers.ts new file mode 100644 index 000000000000..676f42091b08 --- /dev/null +++ b/web/src/admin/providers/oauth2/OAuth2ProviderFormHelpers.ts @@ -0,0 +1,51 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; + +import { PropertymappingsApi, ScopeMapping } from "@goauthentik/api"; + +export const defaultScopes = [ + "goauthentik.io/providers/oauth2/scope-openid", + "goauthentik.io/providers/oauth2/scope-email", + "goauthentik.io/providers/oauth2/scope-profile", +]; + +const mappingToSelect = (s: ScopeMapping) => [s.pk, s.name, s.name, s]; + +export async function propertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsProviderScopeList({ + ordering: "scope_name", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map(mappingToSelect), + }; +} + +export function propertyMappingsSelector(instanceMappings?: string[]) { + if (!instanceMappings) { + return async (mappings: DualSelectPair[]) => + mappings.filter( + ([_0, _1, _2, scope]: DualSelectPair) => + scope?.managed && defaultScopes.includes(scope?.managed), + ); + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsProviderScopeRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} diff --git a/web/src/admin/providers/oauth2/OAuth2Sources.ts b/web/src/admin/providers/oauth2/OAuth2Sources.ts index f6ecde10f2ea..69743223ecaa 100644 --- a/web/src/admin/providers/oauth2/OAuth2Sources.ts +++ b/web/src/admin/providers/oauth2/OAuth2Sources.ts @@ -3,6 +3,13 @@ import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types"; import { OAuthSource, SourcesApi } from "@goauthentik/api"; +const sourceToSelect = (source: OAuthSource) => [ + source.pk, + `${source.name} (${source.slug})`, + source.name, + source, +]; + export async function oauth2SourcesProvider(page = 1, search = "") { const oauthSources = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthList({ ordering: "name", @@ -14,17 +21,29 @@ export async function oauth2SourcesProvider(page = 1, search = "") { return { pagination: oauthSources.pagination, - options: oauthSources.results.map((source) => [ - source.pk, - `${source.name} (${source.slug})`, - ]), + options: oauthSources.results.map(sourceToSelect), }; } -export function makeSourceSelector(instanceSources: string[] | undefined) { - const localSources = instanceSources ? new Set(instanceSources) : undefined; +export function oauth2SourcesSelector(instanceMappings?: string[]) { + if (!instanceMappings) { + return async (mappings: DualSelectPair[]) => + mappings.filter( + ([_0, _1, _2, source]: DualSelectPair) => source !== undefined, + ); + } + + return async () => { + const oauthSources = new SourcesApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + oauthSources.sourcesOauthRetrieve({ slug: instanceId }), + ), + ); - return localSources - ? ([pk, _]: DualSelectPair) => localSources.has(pk) - : ([_0, _1, _2, prompt]: DualSelectPair) => prompt !== undefined; + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(sourceToSelect); + }; } diff --git a/web/src/admin/providers/proxy/ProxyProviderForm.ts b/web/src/admin/providers/proxy/ProxyProviderForm.ts index 86c34969f171..aa93ea08adbf 100644 --- a/web/src/admin/providers/proxy/ProxyProviderForm.ts +++ b/web/src/admin/providers/proxy/ProxyProviderForm.ts @@ -2,8 +2,8 @@ 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, + oauth2SourcesSelector, } from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { first } from "@goauthentik/common/utils"; @@ -30,10 +30,7 @@ import { ProxyProvider, } from "@goauthentik/api"; -import { - makeProxyPropertyMappingsSelector, - proxyPropertyMappingsProvider, -} from "./ProxyProviderPropertyMappings.js"; +import { propertyMappingsProvider, propertyMappingsSelector } from "./ProxyProviderFormHelpers.js"; @customElement("ak-provider-proxy-form") export class ProxyProviderFormPage extends BaseProviderForm { @@ -302,10 +299,8 @@ export class ProxyProviderFormPage extends BaseProviderForm { name="propertyMappings" > @@ -394,7 +389,7 @@ ${this.instance?.skipPathRegex} diff --git a/web/src/admin/providers/proxy/ProxyProviderFormHelpers.ts b/web/src/admin/providers/proxy/ProxyProviderFormHelpers.ts new file mode 100644 index 000000000000..fa805d7ec4ea --- /dev/null +++ b/web/src/admin/providers/proxy/ProxyProviderFormHelpers.ts @@ -0,0 +1,45 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; + +import { PropertymappingsApi, ScopeMapping } from "@goauthentik/api"; + +const mappingToSelect = (s: ScopeMapping) => [s.pk, s.name, s.name, s]; + +export async function propertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsProviderScopeList({ + ordering: "scope_name", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map(mappingToSelect), + }; +} + +export function propertyMappingsSelector(instanceMappings?: string[]) { + if (!instanceMappings) { + return async (mappings: DualSelectPair[]) => + mappings.filter( + ([_0, _1, _2, scope]: DualSelectPair) => + !(scope?.managed ?? "").startsWith("goauthentik.io/providers"), + ); + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsProviderScopeRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} diff --git a/web/src/admin/providers/proxy/ProxyProviderPropertyMappings.ts b/web/src/admin/providers/proxy/ProxyProviderPropertyMappings.ts deleted file mode 100644 index d90cc8ea71a2..000000000000 --- a/web/src/admin/providers/proxy/ProxyProviderPropertyMappings.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; - -import { PropertymappingsApi, ScopeMapping } from "@goauthentik/api"; - -export async function proxyPropertyMappingsProvider(page = 1, search = "") { - const propertyMappings = await new PropertymappingsApi( - DEFAULT_CONFIG, - ).propertymappingsProviderScopeList({ - ordering: "scope_name", - pageSize: 20, - search: search.trim(), - page, - }); - return { - pagination: propertyMappings.pagination, - options: propertyMappings.results.map((scope) => [scope.pk, scope.name, scope.name, scope]), - }; -} - -export function makeProxyPropertyMappingsSelector(mappings?: string[]) { - const localMappings = mappings ? new Set(mappings) : undefined; - return localMappings - ? ([pk, _]: DualSelectPair) => localMappings.has(pk) - : ([_0, _1, _2, scope]: DualSelectPair) => - !(scope?.managed ?? "").startsWith("goauthentik.io/providers"); -} diff --git a/web/src/admin/providers/rac/EndpointForm.ts b/web/src/admin/providers/rac/EndpointForm.ts index b464c0aac073..2c039b85fdd3 100644 --- a/web/src/admin/providers/rac/EndpointForm.ts +++ b/web/src/admin/providers/rac/EndpointForm.ts @@ -15,10 +15,7 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { AuthModeEnum, Endpoint, ProtocolEnum, RacApi } from "@goauthentik/api"; -import { - makeRACPropertyMappingsSelector, - racPropertyMappingsProvider, -} from "./RACPropertyMappings.js"; +import { propertyMappingsProvider, propertyMappingsSelector } from "./RACProviderFormHelpers.js"; @customElement("ak-rac-endpoint-form") export class EndpointForm extends ModelForm { @@ -114,8 +111,8 @@ export class EndpointForm extends ModelForm { diff --git a/web/src/admin/providers/rac/RACPropertyMappings.ts b/web/src/admin/providers/rac/RACPropertyMappings.ts deleted file mode 100644 index 1b26c74041e8..000000000000 --- a/web/src/admin/providers/rac/RACPropertyMappings.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; - -import { PropertymappingsApi } from "@goauthentik/api"; - -export async function racPropertyMappingsProvider(page = 1, search = "") { - const propertyMappings = await new PropertymappingsApi( - DEFAULT_CONFIG, - ).propertymappingsProviderRacList({ - ordering: "name", - pageSize: 20, - search: search.trim(), - page, - }); - return { - pagination: propertyMappings.pagination, - options: propertyMappings.results.map((mapping) => [mapping.pk, mapping.name]), - }; -} - -export function makeRACPropertyMappingsSelector(instanceMappings?: string[]) { - const localMappings = new Set(instanceMappings ?? []); - return ([pk, _]: DualSelectPair) => localMappings.has(pk); -} diff --git a/web/src/admin/providers/rac/RACProviderForm.ts b/web/src/admin/providers/rac/RACProviderForm.ts index e7d3f9428510..73b4c88c82f2 100644 --- a/web/src/admin/providers/rac/RACProviderForm.ts +++ b/web/src/admin/providers/rac/RACProviderForm.ts @@ -19,10 +19,7 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { FlowsInstancesListDesignationEnum, ProvidersApi, RACProvider } from "@goauthentik/api"; -import { - makeRACPropertyMappingsSelector, - racPropertyMappingsProvider, -} from "./RACPropertyMappings.js"; +import { propertyMappingsProvider, propertyMappingsSelector } from "./RACProviderFormHelpers.js"; @customElement("ak-provider-rac-form") export class RACProviderFormPage extends ModelForm { @@ -127,10 +124,8 @@ export class RACProviderFormPage extends ModelForm { name="propertyMappings" > diff --git a/web/src/admin/providers/rac/RACProviderFormHelpers.ts b/web/src/admin/providers/rac/RACProviderFormHelpers.ts new file mode 100644 index 000000000000..3da5619cd109 --- /dev/null +++ b/web/src/admin/providers/rac/RACProviderFormHelpers.ts @@ -0,0 +1,41 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; + +import { PropertymappingsApi, RACPropertyMapping } from "@goauthentik/api"; + +const mappingToSelect = (m: RACPropertyMapping) => [m.pk, m.name, m.name, m]; + +export async function propertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsProviderRacList({ + ordering: "name", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map(mappingToSelect), + }; +} + +export function propertyMappingsSelector(instanceMappings?: string[]) { + if (!instanceMappings) { + return async (_mappings: DualSelectPair[]) => []; + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsProviderRacRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} diff --git a/web/src/admin/providers/radius/RadiusProviderForm.ts b/web/src/admin/providers/radius/RadiusProviderForm.ts index a1d7aeacf951..b5f7cf921338 100644 --- a/web/src/admin/providers/radius/RadiusProviderForm.ts +++ b/web/src/admin/providers/radius/RadiusProviderForm.ts @@ -4,7 +4,6 @@ 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"; @@ -14,35 +13,9 @@ 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 { FlowsInstancesListDesignationEnum, 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) => []; -} +import { propertyMappingsProvider, propertyMappingsSelector } from "./RadiusProviderFormHelpers.js"; @customElement("ak-provider-radius-form") export class RadiusProviderFormPage extends WithBrandConfig(BaseProviderForm) { @@ -155,10 +128,8 @@ export class RadiusProviderFormPage extends WithBrandConfig(BaseProviderForm diff --git a/web/src/admin/providers/radius/RadiusProviderFormHelpers.ts b/web/src/admin/providers/radius/RadiusProviderFormHelpers.ts new file mode 100644 index 000000000000..33843d220156 --- /dev/null +++ b/web/src/admin/providers/radius/RadiusProviderFormHelpers.ts @@ -0,0 +1,41 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; + +import { PropertymappingsApi, RadiusProviderPropertyMapping } from "@goauthentik/api"; + +const mappingToSelect = (m: RadiusProviderPropertyMapping) => [m.pk, m.name, m.name, m]; + +export async function propertyMappingsProvider(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(mappingToSelect), + }; +} + +export function propertyMappingsSelector(instanceMappings?: string[]) { + if (!instanceMappings) { + return async (_mappings: DualSelectPair[]) => []; + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsProviderRadiusRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} diff --git a/web/src/admin/providers/saml/SAMLProviderForm.ts b/web/src/admin/providers/saml/SAMLProviderForm.ts index ef35d2960b3f..6917c9ae9c63 100644 --- a/web/src/admin/providers/saml/SAMLProviderForm.ts +++ b/web/src/admin/providers/saml/SAMLProviderForm.ts @@ -9,7 +9,6 @@ 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"; @@ -31,28 +30,7 @@ import { SpBindingEnum, } from "@goauthentik/api"; -export async function samlPropertyMappingsProvider(page = 1, search = "") { - const propertyMappings = await new PropertymappingsApi( - DEFAULT_CONFIG, - ).propertymappingsProviderSamlList({ - ordering: "saml_name", - pageSize: 20, - search: search.trim(), - page, - }); - return { - pagination: propertyMappings.pagination, - options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]), - }; -} - -export function makeSAMLPropertyMappingsSelector(instanceMappings?: string[]) { - const localMappings = instanceMappings ? new Set(instanceMappings) : undefined; - return localMappings - ? ([pk, _]: DualSelectPair) => localMappings.has(pk) - : ([_0, _1, _2, mapping]: DualSelectPair) => - mapping?.managed?.startsWith("goauthentik.io/providers/saml"); -} +import { propertyMappingsProvider, propertyMappingsSelector } from "./SAMLProviderFormHelpers.js"; @customElement("ak-provider-saml-form") export class SAMLProviderFormPage extends BaseProviderForm { @@ -303,10 +281,8 @@ export class SAMLProviderFormPage extends BaseProviderForm { name="propertyMappings" > diff --git a/web/src/admin/providers/saml/SAMLProviderFormHelpers.ts b/web/src/admin/providers/saml/SAMLProviderFormHelpers.ts new file mode 100644 index 000000000000..54e8c789e254 --- /dev/null +++ b/web/src/admin/providers/saml/SAMLProviderFormHelpers.ts @@ -0,0 +1,44 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; + +import { PropertymappingsApi, SAMLPropertyMapping } from "@goauthentik/api"; + +const mappingToSelect = (m: SAMLPropertyMapping) => [m.pk, m.name, m.name, m]; + +export async function propertyMappingsProvider(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(mappingToSelect), + }; +} + +export function propertyMappingsSelector(instanceMappings?: string[]) { + if (!instanceMappings) { + return async (mappings: DualSelectPair[]) => + mappings.filter(([_0, _1, _2, mapping]: DualSelectPair) => + mapping?.managed?.startsWith("goauthentik.io/providers/saml"), + ); + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsProviderSamlRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} diff --git a/web/src/admin/providers/scim/SCIMProviderForm.ts b/web/src/admin/providers/scim/SCIMProviderForm.ts index 5bc15c2bf8cf..ff2074f2986c 100644 --- a/web/src/admin/providers/scim/SCIMProviderForm.ts +++ b/web/src/admin/providers/scim/SCIMProviderForm.ts @@ -2,7 +2,6 @@ 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"; @@ -17,37 +16,11 @@ import { CoreApi, CoreGroupsListRequest, Group, - PropertymappingsApi, ProvidersApi, - SCIMMapping, 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) => - mapping?.managed === defaultSelected; -} +import { propertyMappingsProvider, propertyMappingsSelector } from "./SCIMProviderFormHelpers.js"; @customElement("ak-provider-scim-form") export class SCIMProviderFormPage extends BaseProviderForm { @@ -189,8 +162,8 @@ export class SCIMProviderFormPage extends BaseProviderForm { label=${msg("User Property Mappings")} name="propertyMappings"> { label=${msg("Group Property Mappings")} name="propertyMappingsGroup"> [m.pk, m.name, m.name, m]; + +export async function propertyMappingsProvider(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(mappingToSelect), + }; +} + +export function propertyMappingsSelector( + instanceMappings: string[] | undefined, + defaultSelected: string, +) { + if (!instanceMappings) { + return async (mappings: DualSelectPair[]) => + mappings.filter( + ([_0, _1, _2, mapping]: DualSelectPair) => + mapping?.managed === defaultSelected, + ); + } + + return async () => { + const pm = new PropertymappingsApi(DEFAULT_CONFIG); + const mappings = await Promise.allSettled( + instanceMappings.map((instanceId) => + pm.propertymappingsProviderScimRetrieve({ pmUuid: instanceId }), + ), + ); + + return mappings + .filter((s) => s.status === "fulfilled") + .map((s) => s.value) + .map(mappingToSelect); + }; +} From e3db6060c86ce14ebf7da304dc722522c51ef838 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 27 Nov 2024 16:03:50 +0100 Subject: [PATCH 7/7] order fields by field_key and order Signed-off-by: Jens Langhammer --- web/src/admin/stages/prompt/PromptStageFormHelpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/admin/stages/prompt/PromptStageFormHelpers.ts b/web/src/admin/stages/prompt/PromptStageFormHelpers.ts index 602f408b695b..379649864a20 100644 --- a/web/src/admin/stages/prompt/PromptStageFormHelpers.ts +++ b/web/src/admin/stages/prompt/PromptStageFormHelpers.ts @@ -14,7 +14,7 @@ const promptToSelect = (p: Prompt) => [ export async function promptFieldsProvider(page = 1, search = "") { const prompts = await new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsList({ - ordering: "field_name", + ordering: "field_name,order", pageSize: 20, search: search.trim(), page,