web: package up horizontal elements into their own components (#7053)
* web: laying the groundwork for future expansion This commit is a hodge-podge of updates and changes to the web. Functional changes: - Makefile: Fixed a bug in the `help` section that prevented the WIDTH from being accurately calculated if `help` was included rather than in-lined. - ESLint: Modified the "unused vars" rule so that variables starting with an underline are not considered by the rule. This allows for elided variables in event handlers. It's not a perfect solution-- a better one would be to use Typescript's function-specialization typing, but there are too many places where we elide or ignore some variables in a function's usage that switching over to specialization would be a huge lift. - locale: It turns out, lit-locale does its own context management. We don't need to have a context at all in this space, and that's one less listener we need to attach t othe DOM. - ModalButton: A small thing, but using `nothing` instead of "html``" allows lit better control over rendering and reduces the number of actual renders of the page. - FormGroup: Provided a means to modify the aria-label, rather than stick with the just the word "Details." Specializing this field will both help users of screen readers in the future, and will allow test suites to find specific form groups now. - RadioButton: provide a more consistent interface to the RadioButton. First, we dispatch the events to the outside world, and we set the value locally so that the current `Form.ts` continues to behave as expected. We also prevent the "button lost value" event from propagating; this presents a unified select-like interface to users of the RadioButtonGroup. The current value semantics are preserved; other clients of the RadioButton do not see a change in behavior. - EventEmitter: If the custom event detail is *not* an object, do not use the object-like semantics for forwarding it; just send it as-is. - Comments: In the course of laying the groundwork for the application wizard, I throw a LOT of comments into the code, describing APIs, interfaces, class and function signatures, to better document the behavior inside and as signposts for future work. * web: permit arrays to be sent in custom events without interpolation. * actually use assignValue or rather serializeFieldRecursive Signed-off-by: Jens Langhammer <jens@goauthentik.io> * web: package up horizontal elements into their own components. This commit introduces a number of "components." Jens has this idiom: ``` <ak-form-element-horizontal label=${msg("Name")} name="name" ?required=${true}> <input type="text" value="${ifDefined(this.instance?.name)}" class="pf-c-form-control" required /> </ak-form-element-horizontal> ``` It's a very web-oriented idiom in that it's built out of two building blocks, the "element-horizontal" descriptor, and the input object itself. This idiom is repeated a lot throughout the code. As an alternative, let's wrap everything into an inheritable interface: ``` <ak-text-input name="name" label=${msg("Name")} value="${ifDefined(this.instance?.name)} required > </ak-text-input> ``` This preserves all the information of the above, makes it much clearer what kind of interaction we're having (sometimes the `type=` information in an input is lost or easily missed), and while it does require you know that there are provided components rather than the pair of layout-behavior as in the original it also gives the developer more precision over the look and feel of the components. *Right now* these components are placed into the LightDOM, as they are in the existing source code, because the Form handler has a need to be able to "peer into" the "element-horizontal" component to find the values of the input objects. In a future revision I hope to place the burden of type/value processing onto the input objects themselves such that the form handler will need only look for the `.value` of the associated input control. Other fixes: - update the FlowSearch() such that it actually emits an input event when its value changes. - Disable the storybook shortcuts; on Chrome, at least, they get confused with simple inputs - Fix an issue with precommit to not scan any Python with ESLint! :-) * web: provide storybook stories for the components This commit provides storybook stories for the ak-horizontal-element wrappers. A few bugs were found along the way, including one rather nasty one from Radio where we were still getting the "set/unset" pair in the wrong order, so I had to knuckle down and fix the event handler properly. * web: test oauth2 provider "guinea pig" for new components I used the Oauth2 provider page as my experiment in seeing if the horizontal-element wrappers could be used instead of the raw wrappers themselves, and I wanted to make sure a test existed that asserts that filling out THAT form in the ProvidersList and ProvidersForm didn't break anything. This commit updates the WDIO tests to do just that; the test is simple, but it does exercise the `name` field of the Provider, something not needed in the Wizard because it's set automatically based on the Application name, and it even asserts that the new Provider exists in the list of available Providers when it's done. * web: making sure ESlint and Prettier are happy * "fix" lint 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:
		
							
								
								
									
										104
									
								
								web/src/admin/common/ak-core-group-search.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								web/src/admin/common/ak-core-group-search.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,104 @@
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { AKElement } from "@goauthentik/elements/Base";
 | 
			
		||||
import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect";
 | 
			
		||||
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
 | 
			
		||||
 | 
			
		||||
import { html } from "lit";
 | 
			
		||||
import { customElement } from "lit/decorators.js";
 | 
			
		||||
import { property, query } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import { CoreApi, CoreGroupsListRequest, Group } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
async function fetchObjects(query?: string): Promise<Group[]> {
 | 
			
		||||
    const args: CoreGroupsListRequest = {
 | 
			
		||||
        ordering: "name",
 | 
			
		||||
    };
 | 
			
		||||
    if (query !== undefined) {
 | 
			
		||||
        args.search = query;
 | 
			
		||||
    }
 | 
			
		||||
    const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(args);
 | 
			
		||||
    return groups.results;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const renderElement = (group: Group): string => group.name;
 | 
			
		||||
 | 
			
		||||
const renderValue = (group: Group | undefined): string | undefined => group?.pk;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Core Group Search
 | 
			
		||||
 *
 | 
			
		||||
 * @element ak-core-group-search
 | 
			
		||||
 *
 | 
			
		||||
 * A wrapper around SearchSelect for the 8 search of groups used throughout our code
 | 
			
		||||
 * base.  This is one of those "If it's not error-free, at least it's localized to
 | 
			
		||||
 * one place" issues.
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@customElement("ak-core-group-search")
 | 
			
		||||
export class CoreGroupSearch extends CustomListenerElement(AKElement) {
 | 
			
		||||
    /**
 | 
			
		||||
     * The current group known to the caller.
 | 
			
		||||
     *
 | 
			
		||||
     * @attr
 | 
			
		||||
     */
 | 
			
		||||
    @property({ type: String, reflect: true })
 | 
			
		||||
    group?: string;
 | 
			
		||||
 | 
			
		||||
    @query("ak-search-select")
 | 
			
		||||
    search!: SearchSelect<Group>;
 | 
			
		||||
 | 
			
		||||
    @property({ type: String })
 | 
			
		||||
    name: string | null | undefined;
 | 
			
		||||
 | 
			
		||||
    selectedGroup?: Group;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        this.selected = this.selected.bind(this);
 | 
			
		||||
        this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
 | 
			
		||||
        this.addCustomListener("ak-change", this.handleSearchUpdate);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get value() {
 | 
			
		||||
        return this.selectedGroup ? renderValue(this.selectedGroup) : undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
        const horizontalContainer = this.closest("ak-form-element-horizontal[name]");
 | 
			
		||||
        if (!horizontalContainer) {
 | 
			
		||||
            throw new Error("This search can only be used in a named ak-form-element-horizontal");
 | 
			
		||||
        }
 | 
			
		||||
        const name = horizontalContainer.getAttribute("name");
 | 
			
		||||
        const myName = this.getAttribute("name");
 | 
			
		||||
        if (name !== null && name !== myName) {
 | 
			
		||||
            this.setAttribute("name", name);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    handleSearchUpdate(ev: CustomEvent) {
 | 
			
		||||
        ev.stopPropagation();
 | 
			
		||||
        this.selectedGroup = ev.detail.value;
 | 
			
		||||
        this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    selected(group: Group) {
 | 
			
		||||
        return this.group === group.pk;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        return html`
 | 
			
		||||
            <ak-search-select
 | 
			
		||||
                .fetchObjects=${fetchObjects}
 | 
			
		||||
                .renderElement=${renderElement}
 | 
			
		||||
                .value=${renderValue}
 | 
			
		||||
                .selected=${this.selected}
 | 
			
		||||
                ?blankable=${true}
 | 
			
		||||
            >
 | 
			
		||||
            </ak-search-select>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default CoreGroupSearch;
 | 
			
		||||
@ -80,6 +80,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
 | 
			
		||||
    handleSearchUpdate(ev: CustomEvent) {
 | 
			
		||||
        ev.stopPropagation();
 | 
			
		||||
        this.selectedFlow = ev.detail.value;
 | 
			
		||||
        this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fetchObjects(query?: string): Promise<Flow[]> {
 | 
			
		||||
 | 
			
		||||
@ -92,7 +92,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
 | 
			
		||||
                data.group = null;
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
        console.log(data);
 | 
			
		||||
 | 
			
		||||
        if (this.instance?.pk) {
 | 
			
		||||
            return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsUpdate({
 | 
			
		||||
                policyBindingUuid: this.instance.pk,
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,9 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search";
 | 
			
		||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
 | 
			
		||||
import "@goauthentik/components/ak-radio-input";
 | 
			
		||||
import "@goauthentik/components/ak-text-input";
 | 
			
		||||
import "@goauthentik/components/ak-textarea-input";
 | 
			
		||||
import "@goauthentik/elements/forms/FormGroup";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
 | 
			
		||||
@ -27,6 +30,91 @@ import {
 | 
			
		||||
    SubModeEnum,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
export const clientTypeOptions = [
 | 
			
		||||
    {
 | 
			
		||||
        label: msg("Confidential"),
 | 
			
		||||
        value: ClientTypeEnum.Confidential,
 | 
			
		||||
        default: true,
 | 
			
		||||
        description: html`${msg(
 | 
			
		||||
            "Confidential clients are capable of maintaining the confidentiality of their credentials such as client secrets",
 | 
			
		||||
        )}`,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        label: msg("Public"),
 | 
			
		||||
        value: ClientTypeEnum.Public,
 | 
			
		||||
        description: html`${msg(
 | 
			
		||||
            "Public clients are incapable of maintaining the confidentiality and should use methods like PKCE. ",
 | 
			
		||||
        )}`,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const subjectModeOptions = [
 | 
			
		||||
    {
 | 
			
		||||
        label: msg("Based on the User's hashed ID"),
 | 
			
		||||
        value: SubModeEnum.HashedUserId,
 | 
			
		||||
        default: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        label: msg("Based on the User's ID"),
 | 
			
		||||
        value: SubModeEnum.UserId,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        label: msg("Based on the User's UUID"),
 | 
			
		||||
        value: SubModeEnum.UserUuid,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        label: msg("Based on the User's username"),
 | 
			
		||||
        value: SubModeEnum.UserUsername,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        label: msg("Based on the User's Email"),
 | 
			
		||||
        value: SubModeEnum.UserEmail,
 | 
			
		||||
        description: html`${msg("This is recommended over the UPN mode.")}`,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        label: msg("Based on the User's UPN"),
 | 
			
		||||
        value: SubModeEnum.UserUpn,
 | 
			
		||||
        description: html`${msg(
 | 
			
		||||
            "Requires the user to have a 'upn' attribute set, and falls back to hashed user ID. Use this mode only if you have different UPN and Mail domains.",
 | 
			
		||||
        )}`,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const issuerModeOptions = [
 | 
			
		||||
    {
 | 
			
		||||
        label: msg("Each provider has a different issuer, based on the application slug"),
 | 
			
		||||
        value: IssuerModeEnum.PerProvider,
 | 
			
		||||
        default: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        label: msg("Same identifier is used for all providers"),
 | 
			
		||||
        value: IssuerModeEnum.Global,
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const redirectUriHelpMessages = [
 | 
			
		||||
    msg(
 | 
			
		||||
        "Valid redirect URLs after a successful authorization flow. Also specify any origins here for Implicit flows.",
 | 
			
		||||
    ),
 | 
			
		||||
    msg(
 | 
			
		||||
        "If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
 | 
			
		||||
    ),
 | 
			
		||||
    msg(
 | 
			
		||||
        'To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.',
 | 
			
		||||
    ),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const redirectUriHelp = html`${redirectUriHelpMessages.map(
 | 
			
		||||
    (m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
 | 
			
		||||
)}`;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Form page for OAuth2 Authentication Method
 | 
			
		||||
 *
 | 
			
		||||
 * @element ak-provider-oauth2-form
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@customElement("ak-provider-oauth2-form")
 | 
			
		||||
export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
 | 
			
		||||
    propertyMappings?: PaginatedScopeMappingList;
 | 
			
		||||
@ -77,22 +165,23 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderForm(): TemplateResult {
 | 
			
		||||
        const provider = this.instance;
 | 
			
		||||
 | 
			
		||||
        return html`<form class="pf-c-form pf-m-horizontal">
 | 
			
		||||
            <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
 | 
			
		||||
                <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    value="${ifDefined(this.instance?.name)}"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                    required
 | 
			
		||||
                />
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="name"
 | 
			
		||||
                label=${msg("Name")}
 | 
			
		||||
                value=${ifDefined(provider?.name)}
 | 
			
		||||
                required
 | 
			
		||||
            ></ak-text-input>
 | 
			
		||||
 | 
			
		||||
            <ak-form-element-horizontal
 | 
			
		||||
                label=${msg("Authentication flow")}
 | 
			
		||||
                name="authenticationFlow"
 | 
			
		||||
                label=${msg("Authentication flow")}
 | 
			
		||||
            >
 | 
			
		||||
                <ak-flow-search
 | 
			
		||||
                    flowType=${FlowsInstancesListDesignationEnum.Authentication}
 | 
			
		||||
                    .currentFlow=${this.instance?.authenticationFlow}
 | 
			
		||||
                    .currentFlow=${provider?.authenticationFlow}
 | 
			
		||||
                    required
 | 
			
		||||
                ></ak-flow-search>
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
@ -100,13 +189,13 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
            <ak-form-element-horizontal
 | 
			
		||||
                name="authorizationFlow"
 | 
			
		||||
                label=${msg("Authorization flow")}
 | 
			
		||||
                ?required=${true}
 | 
			
		||||
                name="authorizationFlow"
 | 
			
		||||
            >
 | 
			
		||||
                <ak-flow-search
 | 
			
		||||
                    flowType=${FlowsInstancesListDesignationEnum.Authorization}
 | 
			
		||||
                    .currentFlow=${this.instance?.authorizationFlow}
 | 
			
		||||
                    .currentFlow=${provider?.authorizationFlow}
 | 
			
		||||
                    required
 | 
			
		||||
                ></ak-flow-search>
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
@ -117,96 +206,50 @@ export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
 | 
			
		||||
            <ak-form-group .expanded=${true}>
 | 
			
		||||
                <span slot="header"> ${msg("Protocol settings")} </span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Client type")}
 | 
			
		||||
                        ?required=${true}
 | 
			
		||||
                    <ak-radio-input
 | 
			
		||||
                        name="clientType"
 | 
			
		||||
                        label=${msg("Client type")}
 | 
			
		||||
                        .value=${provider?.clientType}
 | 
			
		||||
                        required
 | 
			
		||||
                        @change=${(ev: CustomEvent<ClientTypeEnum>) => {
 | 
			
		||||
                            this.showClientSecret = ev.detail !== ClientTypeEnum.Public;
 | 
			
		||||
                        }}
 | 
			
		||||
                        .options=${clientTypeOptions}
 | 
			
		||||
                    >
 | 
			
		||||
                        <ak-radio
 | 
			
		||||
                            @change=${(ev: CustomEvent<ClientTypeEnum>) => {
 | 
			
		||||
                                if (ev.detail === ClientTypeEnum.Public) {
 | 
			
		||||
                                    this.showClientSecret = false;
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    this.showClientSecret = true;
 | 
			
		||||
                                }
 | 
			
		||||
                            }}
 | 
			
		||||
                            .options=${[
 | 
			
		||||
                                {
 | 
			
		||||
                                    label: msg("Confidential"),
 | 
			
		||||
                                    value: ClientTypeEnum.Confidential,
 | 
			
		||||
                                    default: true,
 | 
			
		||||
                                    description: html`${msg(
 | 
			
		||||
                                        "Confidential clients are capable of maintaining the confidentiality of their credentials such as client secrets",
 | 
			
		||||
                                    )}`,
 | 
			
		||||
                                },
 | 
			
		||||
                                {
 | 
			
		||||
                                    label: msg("Public"),
 | 
			
		||||
                                    value: ClientTypeEnum.Public,
 | 
			
		||||
                                    description: html`${msg(
 | 
			
		||||
                                        "Public clients are incapable of maintaining the confidentiality and should use methods like PKCE. ",
 | 
			
		||||
                                    )}`,
 | 
			
		||||
                                },
 | 
			
		||||
                            ]}
 | 
			
		||||
                            .value=${this.instance?.clientType}
 | 
			
		||||
                        >
 | 
			
		||||
                        </ak-radio>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Client ID")}
 | 
			
		||||
                        ?required=${true}
 | 
			
		||||
                    </ak-radio-input>
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="clientId"
 | 
			
		||||
                        label=${msg("Client ID")}
 | 
			
		||||
                        value="${first(
 | 
			
		||||
                            provider?.clientId,
 | 
			
		||||
                            randomString(40, ascii_letters + digits),
 | 
			
		||||
                        )}"
 | 
			
		||||
                        required
 | 
			
		||||
                    >
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="text"
 | 
			
		||||
                            value="${first(
 | 
			
		||||
                                this.instance?.clientId,
 | 
			
		||||
                                randomString(40, ascii_letters + digits),
 | 
			
		||||
                            )}"
 | 
			
		||||
                            class="pf-c-form-control"
 | 
			
		||||
                            required
 | 
			
		||||
                        />
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        ?hidden=${!this.showClientSecret}
 | 
			
		||||
                        label=${msg("Client Secret")}
 | 
			
		||||
                    </ak-text-input>
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="clientSecret"
 | 
			
		||||
                        label=${msg("Client Secret")}
 | 
			
		||||
                        value="${first(
 | 
			
		||||
                            provider?.clientSecret,
 | 
			
		||||
                            randomString(128, ascii_letters + digits),
 | 
			
		||||
                        )}"
 | 
			
		||||
                        ?hidden=${!this.showClientSecret}
 | 
			
		||||
                    >
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="text"
 | 
			
		||||
                            value="${first(
 | 
			
		||||
                                this.instance?.clientSecret,
 | 
			
		||||
                                randomString(128, ascii_letters + digits),
 | 
			
		||||
                            )}"
 | 
			
		||||
                            class="pf-c-form-control"
 | 
			
		||||
                        />
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Redirect URIs/Origins (RegEx)")}
 | 
			
		||||
                    </ak-text-input>
 | 
			
		||||
                    <ak-textarea-input
 | 
			
		||||
                        name="redirectUris"
 | 
			
		||||
                        label=${msg("Redirect URIs/Origins (RegEx)")}
 | 
			
		||||
                        .value=${provider?.redirectUris}
 | 
			
		||||
                        .bighelp=${redirectUriHelp}
 | 
			
		||||
                    >
 | 
			
		||||
                        <textarea class="pf-c-form-control">
 | 
			
		||||
${this.instance?.redirectUris}</textarea
 | 
			
		||||
                        >
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg(
 | 
			
		||||
                                "Valid redirect URLs after a successful authorization flow. Also specify any origins here for Implicit flows.",
 | 
			
		||||
                            )}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg(
 | 
			
		||||
                                "If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
 | 
			
		||||
                            )}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg(
 | 
			
		||||
                                'To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.',
 | 
			
		||||
                            )}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    </ak-textarea-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
 | 
			
		||||
                        <!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
 | 
			
		||||
                        <ak-crypto-certificate-search
 | 
			
		||||
                            certificate=${ifDefined(this.instance?.signingKey || "")}
 | 
			
		||||
                            ?singleton=${!this.instance}
 | 
			
		||||
                            certificate=${ifDefined(this.instance?.signingKey ?? undefined)}
 | 
			
		||||
                            singleton
 | 
			
		||||
                        ></ak-crypto-certificate-search>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">${msg("Key used to sign the tokens.")}</p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
@ -216,69 +259,53 @@ ${this.instance?.redirectUris}</textarea
 | 
			
		||||
            <ak-form-group>
 | 
			
		||||
                <span slot="header"> ${msg("Advanced protocol settings")} </span>
 | 
			
		||||
                <div slot="body" class="pf-c-form">
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Access code validity")}
 | 
			
		||||
                        ?required=${true}
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="accessCodeValidity"
 | 
			
		||||
                        label=${msg("Access code validity")}
 | 
			
		||||
                        required
 | 
			
		||||
                        value="${first(provider?.accessCodeValidity, "minutes=1")}"
 | 
			
		||||
                        .bighelp=${html`<p class="pf-c-form__helper-text">
 | 
			
		||||
                                ${msg("Configure how long access codes are valid for.")}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <ak-utils-time-delta-help></ak-utils-time-delta-help>`}
 | 
			
		||||
                    >
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="text"
 | 
			
		||||
                            value="${first(this.instance?.accessCodeValidity, "minutes=1")}"
 | 
			
		||||
                            class="pf-c-form-control"
 | 
			
		||||
                            required
 | 
			
		||||
                        />
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg("Configure how long access codes are valid for.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <ak-utils-time-delta-help></ak-utils-time-delta-help>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Access Token validity")}
 | 
			
		||||
                        ?required=${true}
 | 
			
		||||
                    </ak-text-input>
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="accessTokenValidity"
 | 
			
		||||
                        label=${msg("Access Token validity")}
 | 
			
		||||
                        value="${first(provider?.accessTokenValidity, "minutes=5")}"
 | 
			
		||||
                        required
 | 
			
		||||
                        .bighelp=${html` <p class="pf-c-form__helper-text">
 | 
			
		||||
                                ${msg("Configure how long access tokens are valid for.")}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <ak-utils-time-delta-help></ak-utils-time-delta-help>`}
 | 
			
		||||
                    >
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="text"
 | 
			
		||||
                            value="${first(this.instance?.accessTokenValidity, "minutes=5")}"
 | 
			
		||||
                            class="pf-c-form-control"
 | 
			
		||||
                            required
 | 
			
		||||
                        />
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg("Configure how long access tokens are valid for.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <ak-utils-time-delta-help></ak-utils-time-delta-help>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Refresh Token validity")}
 | 
			
		||||
                        ?required=${true}
 | 
			
		||||
                    </ak-text-input>
 | 
			
		||||
 | 
			
		||||
                    <ak-text-input
 | 
			
		||||
                        name="refreshTokenValidity"
 | 
			
		||||
                        label=${msg("Refresh Token validity")}
 | 
			
		||||
                        value="${first(provider?.refreshTokenValidity, "days=30")}"
 | 
			
		||||
                        ?required=${true}
 | 
			
		||||
                        .bighelp=${html` <p class="pf-c-form__helper-text">
 | 
			
		||||
                                ${msg("Configure how long refresh tokens are valid for.")}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <ak-utils-time-delta-help></ak-utils-time-delta-help>`}
 | 
			
		||||
                    >
 | 
			
		||||
                        <input
 | 
			
		||||
                            type="text"
 | 
			
		||||
                            value="${first(this.instance?.refreshTokenValidity, "days=30")}"
 | 
			
		||||
                            class="pf-c-form-control"
 | 
			
		||||
                            required
 | 
			
		||||
                        />
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg("Configure how long refresh tokens are valid for.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <ak-utils-time-delta-help></ak-utils-time-delta-help>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    </ak-text-input>
 | 
			
		||||
                    <ak-form-element-horizontal label=${msg("Scopes")} name="propertyMappings">
 | 
			
		||||
                        <select class="pf-c-form-control" multiple>
 | 
			
		||||
                            ${this.propertyMappings?.results.map((scope) => {
 | 
			
		||||
                                let selected = false;
 | 
			
		||||
                                if (!this.instance?.propertyMappings) {
 | 
			
		||||
                                if (!provider?.propertyMappings) {
 | 
			
		||||
                                    selected =
 | 
			
		||||
                                        scope.managed?.startsWith(
 | 
			
		||||
                                            "goauthentik.io/providers/oauth2/scope-",
 | 
			
		||||
                                        ) || false;
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    selected = Array.from(this.instance?.propertyMappings).some(
 | 
			
		||||
                                        (su) => {
 | 
			
		||||
                                            return su == scope.pk;
 | 
			
		||||
                                        },
 | 
			
		||||
                                    );
 | 
			
		||||
                                    selected = Array.from(provider?.propertyMappings).some((su) => {
 | 
			
		||||
                                        return su == scope.pk;
 | 
			
		||||
                                    });
 | 
			
		||||
                                }
 | 
			
		||||
                                return html`<option
 | 
			
		||||
                                    value=${ifDefined(scope.pk)}
 | 
			
		||||
@ -297,104 +324,35 @@ ${this.instance?.redirectUris}</textarea
 | 
			
		||||
                            ${msg("Hold control/command to select multiple items.")}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Subject mode")}
 | 
			
		||||
                        ?required=${true}
 | 
			
		||||
                    <ak-radio-input
 | 
			
		||||
                        name="subMode"
 | 
			
		||||
                        label=${msg("Subject mode")}
 | 
			
		||||
                        required
 | 
			
		||||
                        .options=${subjectModeOptions}
 | 
			
		||||
                        .value=${provider?.subMode}
 | 
			
		||||
                        help=${msg(
 | 
			
		||||
                            "Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
 | 
			
		||||
                        )}
 | 
			
		||||
                    >
 | 
			
		||||
                        <ak-radio
 | 
			
		||||
                            .options=${[
 | 
			
		||||
                                {
 | 
			
		||||
                                    label: msg("Based on the User's hashed ID"),
 | 
			
		||||
                                    value: SubModeEnum.HashedUserId,
 | 
			
		||||
                                    default: true,
 | 
			
		||||
                                },
 | 
			
		||||
                                {
 | 
			
		||||
                                    label: msg("Based on the User's ID"),
 | 
			
		||||
                                    value: SubModeEnum.UserId,
 | 
			
		||||
                                },
 | 
			
		||||
                                {
 | 
			
		||||
                                    label: msg("Based on the User's UUID"),
 | 
			
		||||
                                    value: SubModeEnum.UserUuid,
 | 
			
		||||
                                },
 | 
			
		||||
                                {
 | 
			
		||||
                                    label: msg("Based on the User's username"),
 | 
			
		||||
                                    value: SubModeEnum.UserUsername,
 | 
			
		||||
                                },
 | 
			
		||||
                                {
 | 
			
		||||
                                    label: msg("Based on the User's Email"),
 | 
			
		||||
                                    value: SubModeEnum.UserEmail,
 | 
			
		||||
                                    description: html`${msg(
 | 
			
		||||
                                        "This is recommended over the UPN mode.",
 | 
			
		||||
                                    )}`,
 | 
			
		||||
                                },
 | 
			
		||||
                                {
 | 
			
		||||
                                    label: msg("Based on the User's UPN"),
 | 
			
		||||
                                    value: SubModeEnum.UserUpn,
 | 
			
		||||
                                    description: html`${msg(
 | 
			
		||||
                                        "Requires the user to have a 'upn' attribute set, and falls back to hashed user ID. Use this mode only if you have different UPN and Mail domains.",
 | 
			
		||||
                                    )}`,
 | 
			
		||||
                                },
 | 
			
		||||
                            ]}
 | 
			
		||||
                            .value=${this.instance?.subMode}
 | 
			
		||||
                        >
 | 
			
		||||
                        </ak-radio>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg(
 | 
			
		||||
                                "Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
 | 
			
		||||
                            )}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    <ak-form-element-horizontal name="includeClaimsInIdToken">
 | 
			
		||||
                        <label class="pf-c-switch">
 | 
			
		||||
                            <input
 | 
			
		||||
                                class="pf-c-switch__input"
 | 
			
		||||
                                type="checkbox"
 | 
			
		||||
                                ?checked=${first(this.instance?.includeClaimsInIdToken, true)}
 | 
			
		||||
                            />
 | 
			
		||||
                            <span class="pf-c-switch__toggle">
 | 
			
		||||
                                <span class="pf-c-switch__toggle-icon">
 | 
			
		||||
                                    <i class="fas fa-check" aria-hidden="true"></i>
 | 
			
		||||
                                </span>
 | 
			
		||||
                            </span>
 | 
			
		||||
                            <span class="pf-c-switch__label"
 | 
			
		||||
                                >${msg("Include claims in id_token")}</span
 | 
			
		||||
                            >
 | 
			
		||||
                        </label>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg(
 | 
			
		||||
                                "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
 | 
			
		||||
                            )}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    <ak-form-element-horizontal
 | 
			
		||||
                        label=${msg("Issuer mode")}
 | 
			
		||||
                        ?required=${true}
 | 
			
		||||
                    </ak-radio-input>
 | 
			
		||||
                    <ak-switch-input name="includeClaimsInIdToken">
 | 
			
		||||
                        label=${msg("Include claims in id_token")}
 | 
			
		||||
                        ?checked=${first(provider?.includeClaimsInIdToken, true)}
 | 
			
		||||
                        help=${msg(
 | 
			
		||||
                            "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
 | 
			
		||||
                        )}></ak-switch-input
 | 
			
		||||
                    >
 | 
			
		||||
                    <ak-radio-input
 | 
			
		||||
                        name="issuerMode"
 | 
			
		||||
                        label=${msg("Issuer mode")}
 | 
			
		||||
                        required
 | 
			
		||||
                        .options=${issuerModeOptions}
 | 
			
		||||
                        .value=${provider?.issuerMode}
 | 
			
		||||
                        help=${msg(
 | 
			
		||||
                            "Configure how the issuer field of the ID Token should be filled.",
 | 
			
		||||
                        )}
 | 
			
		||||
                    >
 | 
			
		||||
                        <ak-radio
 | 
			
		||||
                            .options=${[
 | 
			
		||||
                                {
 | 
			
		||||
                                    label: msg(
 | 
			
		||||
                                        "Each provider has a different issuer, based on the application slug",
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    value: IssuerModeEnum.PerProvider,
 | 
			
		||||
                                    default: true,
 | 
			
		||||
                                },
 | 
			
		||||
                                {
 | 
			
		||||
                                    label: msg("Same identifier is used for all providers"),
 | 
			
		||||
                                    value: IssuerModeEnum.Global,
 | 
			
		||||
                                },
 | 
			
		||||
                            ]}
 | 
			
		||||
                            .value=${this.instance?.issuerMode}
 | 
			
		||||
                        >
 | 
			
		||||
                        </ak-radio>
 | 
			
		||||
                        <p class="pf-c-form__helper-text">
 | 
			
		||||
                            ${msg(
 | 
			
		||||
                                "Configure how the issuer field of the ID Token should be filled.",
 | 
			
		||||
                            )}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    </ak-form-element-horizontal>
 | 
			
		||||
                    </ak-radio-input>
 | 
			
		||||
                </div>
 | 
			
		||||
            </ak-form-group>
 | 
			
		||||
 | 
			
		||||
@ -407,7 +365,7 @@ ${this.instance?.redirectUris}</textarea
 | 
			
		||||
                    >
 | 
			
		||||
                        <select class="pf-c-form-control" multiple>
 | 
			
		||||
                            ${this.oauthSources?.results.map((source) => {
 | 
			
		||||
                                const selected = (this.instance?.jwksSources || []).some((su) => {
 | 
			
		||||
                                const selected = (provider?.jwksSources || []).some((su) => {
 | 
			
		||||
                                    return su == source.pk;
 | 
			
		||||
                                });
 | 
			
		||||
                                return html`<option value=${source.pk} ?selected=${selected}>
 | 
			
		||||
 | 
			
		||||
@ -10,8 +10,7 @@ import "@goauthentik/elements/forms/SearchSelect";
 | 
			
		||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { CSSResult } from "lit";
 | 
			
		||||
import { TemplateResult, html } from "lit";
 | 
			
		||||
import { CSSResult, TemplateResult, html } from "lit";
 | 
			
		||||
import { customElement, state } from "lit/decorators.js";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined.js";
 | 
			
		||||
 | 
			
		||||
@ -92,36 +91,25 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderHttpBasic(): TemplateResult {
 | 
			
		||||
        return html`<ak-form-element-horizontal
 | 
			
		||||
                label=${msg("HTTP-Basic Username Key")}
 | 
			
		||||
        return html`<ak-text-input
 | 
			
		||||
                name="basicAuthUserAttribute"
 | 
			
		||||
                label=${msg("HTTP-Basic Username Key")}
 | 
			
		||||
                value="${ifDefined(this.instance?.basicAuthUserAttribute)}"
 | 
			
		||||
                help=${msg(
 | 
			
		||||
                    "User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
 | 
			
		||||
                )}
 | 
			
		||||
            >
 | 
			
		||||
                <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    value="${ifDefined(this.instance?.basicAuthUserAttribute)}"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                />
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
                    ${msg(
 | 
			
		||||
                        "User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
 | 
			
		||||
                    )}
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>
 | 
			
		||||
            <ak-form-element-horizontal
 | 
			
		||||
                label=${msg("HTTP-Basic Password Key")}
 | 
			
		||||
            </ak-text-input>
 | 
			
		||||
 | 
			
		||||
            <ak-text-input
 | 
			
		||||
                name="basicAuthPasswordAttribute"
 | 
			
		||||
                label=${msg("HTTP-Basic Password Key")}
 | 
			
		||||
                value="${ifDefined(this.instance?.basicAuthPasswordAttribute)}"
 | 
			
		||||
                help=${msg(
 | 
			
		||||
                    "User/Group Attribute used for the password part of the HTTP-Basic Header.",
 | 
			
		||||
                )}
 | 
			
		||||
            >
 | 
			
		||||
                <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    value="${ifDefined(this.instance?.basicAuthPasswordAttribute)}"
 | 
			
		||||
                    class="pf-c-form-control"
 | 
			
		||||
                />
 | 
			
		||||
                <p class="pf-c-form__helper-text">
 | 
			
		||||
                    ${msg(
 | 
			
		||||
                        "User/Group Attribute used for the password part of the HTTP-Basic Header.",
 | 
			
		||||
                    )}
 | 
			
		||||
                </p>
 | 
			
		||||
            </ak-form-element-horizontal>`;
 | 
			
		||||
            </ak-text-input>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderModeSelector(): TemplateResult {
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user