web: dual-select uses, part 2: dual-select harder (#9377)

* web: fix esbuild issue with style sheets

Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).

Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.

In standard mode, the following warning appears on the console when running a Flow:

```
Autofocus processing was blocked because a document already has a focused element.
```

In compatibility mode, the following **error** appears on the console when running a Flow:

```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
    at initDomMutationObservers (crawler-inject.js:1106:18)
    at crawler-inject.js:1114:24
    at Array.forEach (<anonymous>)
    at initDomMutationObservers (crawler-inject.js:1114:10)
    at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```

Despite this error, nothing seems to be broken and flows work as anticipated.

* web: replace multi-select with dual-select for all propertyMapping invocations

All of the uses of <select> to show propertyMappings have been replaced with an invocation to a
variant of dual select that allows for dynamic production of the "selected" list.  Instead of giving
a "selected" list of elements, a "selector" function is passed that can, given the elements listed
by the provider, generated the "selected" list dynamically.

This feature is required for propertyMappings because many of the propertyMappings have an alternative
"default selected" feature whereby an object with no property mappings is automatically granted some
by the `.managed` field of the property mapping.  The `DualSelectPair` type is now tragically
mis-named, as it it's now a 4-tuple, the fourth being whatever object or field is necessary to
figure out what the default value might be.  For example, the Oauth2PropertyMappingsSelector looks
like this:

```
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<ScopeMapping>) =>
              scope?.managed?.startsWith("goauthentik.io/providers/oauth2/scope-") &&
              scope?.managed !== "goauthentik.io/providers/oauth2/scope-offline_access";
}
```

If there are instanceMappings, we create a Set of them and just look up the pk for "is this
selected" as we generate the component.

If there is not, we look at the `scope` object itself (Oauth2PropertyMappings were called "scopes"
in the original source) and perform a token analysis.

It works well, is reasonably fast, and reasonably memory-friendly.

In the case of RAC, OAuth2, and ProxyProviders, I've also provided external definitions of the
MappingProvider and MappingSelector, so that they can be shared between the Provider and the
ApplicationWizard.

The algorithm for finding the "alternative (default) selections" was *different* between the two
instances of both Oauth and Proxy. I'm not marking this as "ready" until Jens (@BeryJu) and I can go
over why that might have been so, and decide if using a common implementation for both is the
correct thing to do.

Also, a lot of this is (still) cut-and-paste; the dual-select invocation, and the definitions of
Providers and Selectors have a bit of boilerplate that it just didn't make sense to try and abstract
away; the code is DAMP (Descriptive and Meaningful Phrases), and I can live with it.  Unfortunately,
that also points to the possibility of something being off; the wrong default token, or the wrong
phrase to describe the "Available" and "Selected" columns.  So this is not (yet) ready for a full
pull review.

On the other hand, if this passes muster and we're happy with it, there are 11 more places to put
DualSelect, four of which are pure cut-and-paste lookups of the PaginatedOauthSourceList, plus a
miscellany of Prompts, Sources, Stages, Roles, EventTransports and Policies.

Despite the churn, the difference between the two implementations is 438 lines removed, 231 lines
added, 121 lines new.  86 LOC deleted.  Could be better.  :-)

* web: make the ...Selector semantics uniform across the definition set.

* web: fix proxy property mapping default criteria

* web: restoring dropped message to user.

* Completed one.  Stashing momentarily.

* Ensuring the neccessary components are imported.

* I hate trying to coax MacOS into accepting case changes.

* Still trying to rename that thing.

* OAuth2 Sources multiple implementation completed.

* web: replace remaining multi-selects with dual-selects

This commit replaces the remaining multi-selects with their dual-select equivalents.

* web: fix problem with 'selector' overselecting

The 'selector' feature was overselecting, preventing items from
being removed from the "selected" list if they were part of the
host object.  This has the shortcoming that `default` items *must*
be in the first page of options from the server, or they probably
won't be registered.  Fortunately, that's currently the case.

* fix a

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix b

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* migrate new providers

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove old incorrect help message

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix incorrect copy paste

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix status label for gorups

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Ken Sternberg
2024-08-22 02:59:03 -07:00
committed by GitHub
parent 85eb104966
commit 3de78ebb09
23 changed files with 350 additions and 361 deletions

View File

@ -0,0 +1,30 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:lit/recommended",
"plugin:custom-elements/recommended",
"plugin:storybook/recommended",
"plugin:sonarjs/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["@typescript-eslint", "lit", "custom-elements", "sonarjs"],
"rules": {
"indent": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double", { "avoidEscape": true }],
"semi": ["error", "always"],
"@typescript-eslint/ban-ts-comment": "off",
"sonarjs/cognitive-complexity": ["warn", 9],
"sonarjs/no-duplicate-string": "off",
"sonarjs/no-nested-template-literals": "off"
}
}

View File

@ -1,16 +1,17 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import {
makeOAuth2PropertyMappingsSelector,
oauth2PropertyMappingsProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2PropertyMappings.js";
import {
clientTypeOptions,
issuerModeOptions,
redirectUriHelp,
subjectModeOptions,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
import {
makeOAuth2PropertyMappingsSelector,
oauth2PropertyMappingsProvider,
} from "@goauthentik/admin/providers/oauth2/Oauth2PropertyMappings.js";
import { oauth2SourcesProvider } 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";
import "@goauthentik/components/ak-number-input";
@ -262,24 +263,17 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
name="jwksSources"
.errorMessages=${errors?.jwksSources ?? []}
>
<select class="pf-c-form-control" multiple>
${this.oauthSources?.results.map((source) => {
const selected = (provider?.jwksSources || []).some((su) => {
return su == source.pk;
});
return html`<option value=${source.pk} ?selected=${selected}>
${source.name} (${source.slug})
</option>`;
})}
</select>
<ak-dual-select-provider
.provider=${oauth2SourcesProvider}
.selected=${provider?.jwksSources}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-provider>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>

View File

@ -1,4 +1,5 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import { oauth2SourcesProvider } from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import {
makeProxyPropertyMappingsSelector,
proxyPropertyMappingsProvider,
@ -10,6 +11,7 @@ import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
@ -226,26 +228,17 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
name="jwksSources"
.errorMessages=${errors?.jwksSources ?? []}
>
<select class="pf-c-form-control" multiple>
${this.oauthSources?.results.map((source) => {
const selected = (this.instance?.jwksSources || []).some(
(su) => {
return su == source.pk;
},
);
return html`<option value=${source.pk} ?selected=${selected}>
${source.name} (${source.slug})
</option>`;
})}
</select>
<ak-dual-select-provider
.provider=${oauth2SourcesProvider}
.selected=${this.instance?.jwksSources}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-provider>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>

View File

@ -266,11 +266,8 @@ export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPane
.options=${propertyPairs}
.values=${pmValues}
.richhelp=${html` <p class="pf-c-form__helper-text">
${msg("Property mappings used for user mapping.")}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>`}
${msg("Property mappings used for user mapping.")}
</p>`}
></ak-multi-select>
<ak-form-element-horizontal

View File

@ -126,24 +126,22 @@ export class ApplicationWizardAuthenticationBySCIM extends BaseProviderPanel {
name="propertyMappings"
.options=${propertyPairs}
.values=${pmUserValues}
.richhelp=${html` <p class="pf-c-form__helper-text">
.richhelp=${html`
<p class="pf-c-form__helper-text">
${msg("Property mappings used for user mapping.")}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>`}
`}
></ak-multi-select>
<ak-multi-select
label=${msg("Group Property Mappings")}
name="propertyMappingsGroup"
.options=${propertyPairs}
.values=${pmGroupValues}
.richhelp=${html` <p class="pf-c-form__helper-text">
.richhelp=${html`
<p class="pf-c-form__helper-text">
${msg("Property mappings used for group creation.")}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>`}
`}
></ak-multi-select>
</div>
</ak-form-group>

View File

@ -20,6 +20,20 @@ import {
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,
});
return {
pagination: eventTransports.pagination,
options: eventTransports.results.map((transport) => [transport.pk, transport.name]),
};
}
@customElement("ak-event-rule-form")
export class RuleForm extends ModelForm<NotificationRule, string> {
eventTransports?: PaginatedNotificationTransportList;
@ -100,24 +114,17 @@ export class RuleForm extends ModelForm<NotificationRule, string> {
?required=${true}
name="transports"
>
<select class="pf-c-form-control" multiple>
${this.eventTransports?.results.map((transport) => {
const selected = Array.from(this.instance?.transports || []).some((su) => {
return su == transport.pk;
});
return html`<option value=${ifDefined(transport.pk)} ?selected=${selected}>
${transport.name}
</option>`;
})}
</select>
<ak-dual-select-provider
.provider=${eventTransportsProvider}
.selected=${this.instance?.transports}
available-label="${msg("Available Transports")}"
selected-label="${msg("Selected Transports")}"
></ak-dual-select-provider>
<p class="pf-c-form__helper-text">
${msg(
"Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Severity")} ?required=${true} name="severity">
<ak-radio

View File

@ -79,7 +79,7 @@ export class GroupListPage extends TablePage<Group> {
html`<a href="#/identity/groups/${item.pk}">${item.name}</a>`,
html`${item.parentName || msg("-")}`,
html`${Array.from(item.users || []).length}`,
html`<ak-label type="info" ?good=${item.isSuperuser}></ak-label>`,
html`<ak-status-label type="info" ?good=${item.isSuperuser}></ak-status-label>`,
html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Group")} </span>

View File

@ -2,7 +2,7 @@ import "@goauthentik/admin/applications/ApplicationWizardHint";
import "@goauthentik/admin/providers/ProviderWizard";
import "@goauthentik/admin/providers/google_workspace/GoogleWorkspaceProviderForm";
import "@goauthentik/admin/providers/ldap/LDAPProviderForm";
import "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderViewPage";
import "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderForm";
import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
import "@goauthentik/admin/providers/rac/RACProviderForm";

View File

@ -1,8 +1,14 @@
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import {
googleWorkspacePropertyMappingsProvider,
makeGoogleWorkspacePropertyMappingsSelector,
} from "@goauthentik/admin/providers/google_workspace/GoogleWorkspaceProviderPropertyMappings";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
@ -19,8 +25,6 @@ import {
GoogleWorkspaceProvider,
Group,
OutgoingSyncDeleteAction,
PaginatedGoogleWorkspaceProviderMappingList,
PropertymappingsApi,
ProvidersApi,
} from "@goauthentik/api";
@ -32,16 +36,6 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
});
}
async load(): Promise<void> {
this.propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderGoogleWorkspaceList({
ordering: "managed",
});
}
propertyMappings?: PaginatedGoogleWorkspaceProviderMappingList;
async send(data: GoogleWorkspaceProvider): Promise<GoogleWorkspaceProvider> {
if (this.instance) {
return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceUpdate({
@ -229,68 +223,35 @@ export class GoogleWorkspaceProviderFormPage extends BaseProviderForm<GoogleWork
label=${msg("User Property Mappings")}
name="propertyMappings"
>
<select class="pf-c-form-control" multiple>
${this.propertyMappings?.results.map((mapping) => {
let selected = false;
if (!this.instance?.propertyMappings) {
selected =
mapping.managed ===
"goauthentik.io/providers/google_workspace/user" ||
false;
} else {
selected = Array.from(this.instance?.propertyMappings).some(
(su) => {
return su == mapping.pk;
},
);
}
return html`<option
value=${ifDefined(mapping.pk)}
?selected=${selected}
>
${mapping.name}
</option>`;
})}
</select>
<ak-dual-select-dynamic-selected
.provider=${googleWorkspacePropertyMappingsProvider}
.selector=${makeGoogleWorkspacePropertyMappingsSelector(
this.instance?.propertyMappings,
"goauthentik.io/providers/google_workspace/user",
)}
available-label=${msg("Available Property Mappings")}
selected-label=${msg("Selected Property Mappings")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to user mapping.")}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Group Property Mappings")}
name="propertyMappingsGroup"
>
<select class="pf-c-form-control" multiple>
${this.propertyMappings?.results.map((mapping) => {
let selected = false;
if (!this.instance?.propertyMappingsGroup) {
selected =
mapping.managed ===
"goauthentik.io/providers/google_workspace/group";
} else {
selected = Array.from(
this.instance?.propertyMappingsGroup,
).some((su) => {
return su == mapping.pk;
});
}
return html`<option
value=${ifDefined(mapping.pk)}
?selected=${selected}
>
${mapping.name}
</option>`;
})}
</select>
<ak-dual-select-dynamic-selected
.provider=${googleWorkspacePropertyMappingsProvider}
.selector=${makeGoogleWorkspacePropertyMappingsSelector(
this.instance?.propertyMappingsGroup,
"goauthentik.io/providers/google_workspace/group",
)}
available-label=${msg("Available Property Mappings")}
selected-label=${msg("Selected Property Mappings")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to group creation.")}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;

View File

@ -0,0 +1,30 @@
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<ScopeMapping>) =>
scope?.managed === defaultSelection;
}

View File

@ -1,6 +1,12 @@
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import {
makeMicrosoftEntraPropertyMappingsSelector,
microsoftEntraPropertyMappingsProvider,
} from "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderPropertyMappings";
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 "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
@ -17,8 +23,6 @@ import {
Group,
MicrosoftEntraProvider,
OutgoingSyncDeleteAction,
PaginatedMicrosoftEntraProviderMappingList,
PropertymappingsApi,
ProvidersApi,
} from "@goauthentik/api";
@ -30,16 +34,6 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
});
}
async load(): Promise<void> {
this.propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderMicrosoftEntraList({
ordering: "managed",
});
}
propertyMappings?: PaginatedMicrosoftEntraProviderMappingList;
async send(data: MicrosoftEntraProvider): Promise<MicrosoftEntraProvider> {
if (this.instance) {
return new ProvidersApi(DEFAULT_CONFIG).providersMicrosoftEntraUpdate({
@ -218,68 +212,35 @@ export class MicrosoftEntraProviderFormPage extends BaseProviderForm<MicrosoftEn
label=${msg("User Property Mappings")}
name="propertyMappings"
>
<select class="pf-c-form-control" multiple>
${this.propertyMappings?.results.map((mapping) => {
let selected = false;
if (!this.instance?.propertyMappings) {
selected =
mapping.managed ===
"goauthentik.io/providers/microsoft_entra/user" ||
false;
} else {
selected = Array.from(this.instance?.propertyMappings).some(
(su) => {
return su == mapping.pk;
},
);
}
return html`<option
value=${ifDefined(mapping.pk)}
?selected=${selected}
>
${mapping.name}
</option>`;
})}
</select>
<ak-dual-select-dynamic-selected
.provider=${microsoftEntraPropertyMappingsProvider}
.selector=${makeMicrosoftEntraPropertyMappingsSelector(
this.instance?.propertyMappings,
"goauthentik.io/providers/microsoft_entra/user",
)}
available-label=${msg("Available Property Mappings")}
selected-label=${msg("Selected Property Mappings")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to user mapping.")}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Group Property Mappings")}
name="propertyMappingsGroup"
>
<select class="pf-c-form-control" multiple>
${this.propertyMappings?.results.map((mapping) => {
let selected = false;
if (!this.instance?.propertyMappingsGroup) {
selected =
mapping.managed ===
"goauthentik.io/providers/microsoft_entra/group";
} else {
selected = Array.from(
this.instance?.propertyMappingsGroup,
).some((su) => {
return su == mapping.pk;
});
}
return html`<option
value=${ifDefined(mapping.pk)}
?selected=${selected}
>
${mapping.name}
</option>`;
})}
</select>
<ak-dual-select-dynamic-selected
.provider=${microsoftEntraPropertyMappingsProvider}
.selector=${makeMicrosoftEntraPropertyMappingsSelector(
this.instance?.propertyMappingsGroup,
"goauthentik.io/providers/microsoft_entra/group",
)}
available-label=${msg("Available Property Mappings")}
selected-label=${msg("Selected Property Mappings")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to group creation.")}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;

View File

@ -0,0 +1,30 @@
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 microsoftEntraPropertyMappingsProvider(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((scope) => [scope.pk, scope.name, scope.name, scope]),
};
}
export function makeMicrosoftEntraPropertyMappingsSelector(
instanceMappings: string[] | undefined,
defaultSelection: string,
) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, scope]: DualSelectPair<ScopeMapping>) =>
scope?.managed === defaultSelection;
}

View File

@ -1,4 +1,4 @@
import "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderFormPage";
import "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderForm";
import "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderGroupList";
import "@goauthentik/admin/providers/microsoft_entra/MicrosoftEntraProviderUserList";
import "@goauthentik/admin/rbac/ObjectPermissionsPage";

View File

@ -7,6 +7,7 @@ import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
@ -23,16 +24,15 @@ import {
FlowsInstancesListDesignationEnum,
IssuerModeEnum,
OAuth2Provider,
PaginatedOAuthSourceList,
ProvidersApi,
SourcesApi,
SubModeEnum,
} from "@goauthentik/api";
import {
makeOAuth2PropertyMappingsSelector,
oauth2PropertyMappingsProvider,
} from "./Oauth2PropertyMappings.js";
} from "./OAuth2PropertyMappings.js";
import { oauth2SourcesProvider } from "./OAuth2Sources.js";
export const clientTypeOptions = [
{
@ -127,8 +127,6 @@ export const redirectUriHelp = html`${redirectUriHelpMessages.map(
@customElement("ak-provider-oauth2-form")
export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
oauthSources?: PaginatedOAuthSourceList;
@state()
showClientSecret = true;
@ -140,13 +138,6 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
return provider;
}
async load(): Promise<void> {
this.oauthSources = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthList({
ordering: "name",
hasJwks: true,
});
}
async send(data: OAuth2Provider): Promise<OAuth2Provider> {
if (this.instance) {
return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Update({
@ -344,24 +335,17 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
label=${msg("Trusted OIDC Sources")}
name="jwksSources"
>
<select class="pf-c-form-control" multiple>
${this.oauthSources?.results.map((source) => {
const selected = (provider?.jwksSources || []).some((su) => {
return su == source.pk;
});
return html`<option value=${source.pk} ?selected=${selected}>
${source.name} (${source.slug})
</option>`;
})}
</select>
<ak-dual-select-provider
.provider=${oauth2SourcesProvider}
.selected=${provider?.jwksSources}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-provider>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;

View File

@ -0,0 +1,21 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { SourcesApi } from "@goauthentik/api";
export async function oauth2SourcesProvider(page = 1, search = "") {
const oauthSources = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthList({
ordering: "name",
hasJwks: true,
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: oauthSources.pagination,
options: oauthSources.results.map((source) => [
source.pk,
`${source.name} (${source.slug})`,
]),
};
}

View File

@ -1,10 +1,12 @@
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 { oauth2SourcesProvider } from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
@ -21,11 +23,9 @@ import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
import {
FlowsInstancesListDesignationEnum,
PaginatedOAuthSourceList,
ProvidersApi,
ProxyMode,
ProxyProvider,
SourcesApi,
} from "@goauthentik/api";
import {
@ -48,15 +48,6 @@ export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
return provider;
}
async load(): Promise<void> {
this.oauthSources = await new SourcesApi(DEFAULT_CONFIG).sourcesOauthList({
ordering: "name",
hasJwks: true,
});
}
oauthSources?: PaginatedOAuthSourceList;
@state()
showHttpBasic = true;
@ -412,24 +403,17 @@ ${this.instance?.skipPathRegex}</textarea
label=${msg("Trusted OIDC Sources")}
name="jwksSources"
>
<select class="pf-c-form-control" multiple>
${this.oauthSources?.results.map((source) => {
const selected = (this.instance?.jwksSources || []).some((su) => {
return su == source.pk;
});
return html`<option value=${source.pk} ?selected=${selected}>
${source.name} (${source.slug})
</option>`;
})}
</select>
<ak-dual-select-provider
.provider=${oauth2SourcesProvider}
.selected=${this.instance?.jwksSources}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-provider>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;

View File

@ -159,9 +159,6 @@ export class RadiusProviderFormPage extends WithBrandConfig(BaseProviderForm<Rad
available-label=${msg("Available Property Mappings")}
selected-label=${msg("Selected Property Mappings")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;

View File

@ -34,6 +34,7 @@ async function propertyMappingsProvider(page = 1, search = "") {
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),

View File

@ -13,6 +13,8 @@ import {
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
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";
@ -194,9 +196,6 @@ export class PlexSourceForm extends WithCapabilitiesConfig(BaseSourceForm<PlexSo
"Select which server a user has to be a member of to be allowed to authenticate.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>`;
}

View File

@ -3,7 +3,6 @@ import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticat
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/Alert";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
import { DataProvision } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
@ -23,6 +22,35 @@ import {
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})`]),
};
}
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),
};
}
@customElement("ak-stage-authenticator-validate-form")
export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorValidateStage> {
async loadInstance(pk: string): Promise<AuthenticatorValidateStage> {
@ -177,21 +205,14 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
label=${msg("Configuration stages")}
name="configurationStages"
>
<select class="pf-c-form-control" multiple>
${this.stages?.results.map((stage) => {
const selected = Array.from(
this.instance?.configurationStages || [],
).some((su) => {
return su == stage.pk;
});
return html`<option
value=${ifDefined(stage.pk)}
?selected=${selected}
>
${stage.name} (${stage.verboseName})
</option>`;
})}
</select>
<ak-dual-select-provider
.provider=${stagesProvider}
.selected=${Array.from(
this.instance?.configurationStages ?? [],
)}
available-label="${msg("Available Stages")}"
selected-label="${msg("Selected Stages")}"
></ak-dual-select-provider>
<p class="pf-c-form__helper-text">
${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.",
@ -242,19 +263,7 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
name="webauthnAllowedDeviceTypes"
>
<ak-dual-select-provider
.provider=${(page: number, search?: string): Promise<DataProvision> => {
return new StagesApi(DEFAULT_CONFIG)
.stagesAuthenticatorWebauthnDeviceTypesList({
page: page,
search: search,
})
.then((results) => {
return {
pagination: results.pagination,
options: results.results.map(deviceTypeRestrictionPair),
};
});
}}
.provider=${authenticatorWebauthnDeviceTypesListProvider}
.selected=${(this.instance?.webauthnAllowedDeviceTypesObj ?? []).map(
deviceTypeRestrictionPair,
)}

View File

@ -3,6 +3,8 @@ import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
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";
@ -15,7 +17,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
import {
FlowsInstancesListDesignationEnum,
IdentificationStage,
PaginatedSourceList,
Source,
SourcesApi,
Stage,
StagesApi,
@ -23,6 +25,31 @@ 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]),
};
}
async function makeSourcesSelector(instanceSources: string[] | undefined) {
const localSources = instanceSources ? new Set(instanceSources) : undefined;
return localSources
? ([pk, _]: DualSelectPair) => localSources.has(pk)
: ([_0, _1, _2, source]: DualSelectPair<Source>) =>
source !== undefined && source.component === "";
}
@customElement("ak-stage-identification-form")
export class IdentificationStageForm extends BaseStageForm<IdentificationStage> {
static get styles() {
@ -42,14 +69,6 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
});
}
async load(): Promise<void> {
this.sources = await new SourcesApi(DEFAULT_CONFIG).sourcesAllList({
ordering: "slug",
});
}
sources?: PaginatedSourceList;
async send(data: IdentificationStage): Promise<IdentificationStage> {
if (this.instance) {
return new StagesApi(DEFAULT_CONFIG).stagesIdentificationUpdate({
@ -213,33 +232,17 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
?required=${true}
name="sources"
>
<select class="pf-c-form-control" multiple>
${this.sources?.results
.filter((source) => {
return source.component !== "";
})
.map((source) => {
const selected = Array.from(this.instance?.sources || []).some(
(su) => {
return su == source.pk;
},
);
return html`<option
value=${ifDefined(source.pk)}
?selected=${selected}
>
${source.name}
</option>`;
})}
</select>
<ak-dual-select-provider-dynamic-selected
.provider=${sourcesProvider}
.selected=${makeSourcesSelector(this.instance?.sources)}
available-label="${msg("Available Stages")}"
selected-label="${msg("Selected Stages")}"
></ak-dual-select-provider-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"Select sources should be shown for users to authenticate with. This only affects web-based sources, not LDAP.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="showSourceLabels">
<label class="pf-c-switch">

View File

@ -7,17 +7,45 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/ModalForm";
import { msg, str } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { TemplateResult, html, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
PaginatedPolicyList,
PaginatedPromptList,
PoliciesApi,
PromptStage,
StagesApi,
} from "@goauthentik/api";
import { PoliciesApi, PromptStage, StagesApi } from "@goauthentik/api";
async function promptsProvider(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,
str`${prompt.name} ("${prompt.fieldKey}", of type ${prompt.type})`,
]),
};
}
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})`,
]),
};
}
@customElement("ak-stage-prompt-form")
export class PromptStageForm extends BaseStageForm<PromptStage> {
@ -27,18 +55,6 @@ export class PromptStageForm extends BaseStageForm<PromptStage> {
});
}
async load(): Promise<void> {
this.prompts = await new StagesApi(DEFAULT_CONFIG).stagesPromptPromptsList({
ordering: "field_name",
});
this.policies = await new PoliciesApi(DEFAULT_CONFIG).policiesAllList({
ordering: "name",
});
}
prompts?: PaginatedPromptList;
policies?: PaginatedPolicyList;
async send(data: PromptStage): Promise<PromptStage> {
if (this.instance) {
return new StagesApi(DEFAULT_CONFIG).stagesPromptStagesUpdate({
@ -74,26 +90,12 @@ export class PromptStageForm extends BaseStageForm<PromptStage> {
?required=${true}
name="fields"
>
<select name="users" class="pf-c-form-control" multiple>
${this.prompts?.results.map((prompt) => {
const selected = Array.from(this.instance?.fields || []).some(
(su) => {
return su == prompt.pk;
},
);
return html`<option
value=${ifDefined(prompt.pk)}
?selected=${selected}
>
${msg(
str`${prompt.name} ("${prompt.fieldKey}", of type ${prompt.type})`,
)}
</option>`;
})}
</select>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
<ak-dual-select-provider
.provider=${promptsProvider}
.selected=${this.instance?.fields}
available-label="${msg("Available Fields")}"
selected-label="${msg("Selected Fields")}"
></ak-dual-select-provider>
${this.instance
? html`<ak-forms-modal size=${PFSize.XLarge}>
<span slot="submit"> ${msg("Create")} </span>
@ -107,35 +109,23 @@ export class PromptStageForm extends BaseStageForm<PromptStage> {
${msg("Create")}
</button>
</ak-forms-modal>`
: html``}
: nothing}
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Validation Policies")}
name="validationPolicies"
>
<select name="users" class="pf-c-form-control" multiple>
${this.policies?.results.map((policy) => {
const selected = Array.from(
this.instance?.validationPolicies || [],
).some((su) => {
return su == policy.pk;
});
return html`<option
value=${ifDefined(policy.pk)}
?selected=${selected}
>
${msg(str`${policy.name} (${policy.verboseName})`)}
</option>`;
})}
</select>
<ak-dual-select-provider
.provider=${policiesProvider}
.selected=${this.instance?.validationPolicies}
available-label="${msg("Available Fields")}"
selected-label="${msg("Selected Fields")}"
></ak-dual-select-provider>
<p class="pf-c-form__helper-text">
${msg(
"Selected policies are executed when the stage is submitted to validate the data.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;