web: SAML Manual Configuration
Added a 'design document' that just kinda describes what I'm trying to do, in case I don't get this done by Friday Aug 11, 2023. I had two tables doing the same thing, so I merged them and then wrote a few map/filters to specialize them for those two use cases. Along the way I had to fiddle with the ESLint settings so that underscore-prefixed unused variables would be ignored. I cleaned up the visual appeal of the forms in the LDAP application. I was copy/pasting the "handleProviderEvent" function, so I pulled it out into ApplicationWizardProviderPageBase. Not so much a matter of abstraction as just disliking that kind of duplication; it served no purpose.
This commit is contained in:
parent
67b7371026
commit
ae66297196
|
@ -0,0 +1,20 @@
|
|||
import ApplicationWizardPageBase from "./ApplicationWizardPageBase";
|
||||
|
||||
export class ApplicationWizardProviderPageBase extends ApplicationWizardPageBase {
|
||||
handleChange(ev: InputEvent) {
|
||||
if (!ev.target) {
|
||||
console.warn(`Received event with no target: ${ev}`);
|
||||
return;
|
||||
}
|
||||
const target = ev.target as HTMLInputElement;
|
||||
const value = target.type === "checkbox" ? target.checked : target.value;
|
||||
this.dispatchWizardUpdate({
|
||||
provider: {
|
||||
...this.wizard.provider,
|
||||
[target.name]: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardProviderPageBase;
|
|
@ -1,35 +1,70 @@
|
|||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import type { TypeCreate } from "@goauthentik/api";
|
||||
|
||||
type ProviderType = [string, string, string] | [string, string, string, ProviderType[]];
|
||||
type ProviderRenderer = () => TemplateResult;
|
||||
|
||||
type ProviderOption = TypeCreate & {
|
||||
children?: TypeCreate[];
|
||||
};
|
||||
type ProviderType = [string, string, string, ProviderRenderer];
|
||||
|
||||
// prettier-ignore
|
||||
const _providerTypesTable: ProviderType[] = [
|
||||
["oauth2provider", msg("OAuth2/OpenID"), msg("Modern applications, APIs and Single-page applications.")],
|
||||
["ldapprovider", msg("LDAP"), msg("Provide an LDAP interface for applications and users to authenticate against.")],
|
||||
["proxyprovider-proxy", msg("Transparent Reverse Proxy"), msg("For transparent reverse proxies with required authentication")],
|
||||
["proxyprovider-forwardsingle", msg("Forward Single Proxy"), msg("For nginx's auth_request or traefix's forwardAuth")],
|
||||
["radiusprovider", msg("Radius"), msg("Allow applications to authenticate against authentik's users using Radius.")],
|
||||
["samlprovider-manual", msg("SAML Manual configuration"), msg("Configure SAML provider manually")],
|
||||
["samlprovider-import", msg("SAML Import Configuration"), msg("Create a SAML provider by importing its metadata")],
|
||||
["scimprovider", msg("SCIM Provider"), msg("SCIM 2.0 provider to create users and groups in external applications")]
|
||||
[
|
||||
"oauth2provider",
|
||||
msg("OAuth2/OpenID"),
|
||||
msg("Modern applications, APIs and Single-page applications."),
|
||||
() => html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`
|
||||
],
|
||||
|
||||
[
|
||||
"ldapprovider",
|
||||
msg("LDAP"),
|
||||
msg("Provide an LDAP interface for applications and users to authenticate against."),
|
||||
() => html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`
|
||||
],
|
||||
|
||||
[
|
||||
"proxyprovider-proxy",
|
||||
msg("Transparent Reverse Proxy"),
|
||||
msg("For transparent reverse proxies with required authentication"),
|
||||
() => html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`
|
||||
],
|
||||
|
||||
[
|
||||
"proxyprovider-forwardsingle",
|
||||
msg("Forward Single Proxy"),
|
||||
msg("For nginx's auth_request or traefix's forwardAuth"),
|
||||
() => html`<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>`
|
||||
],
|
||||
|
||||
[
|
||||
"samlprovider-manual",
|
||||
msg("SAML Manual configuration"),
|
||||
msg("Configure SAML provider manually"),
|
||||
() => html`<p>Under construction</p>`
|
||||
],
|
||||
|
||||
[
|
||||
"samlprovider-import",
|
||||
msg("SAML Import Configuration"),
|
||||
msg("Create a SAML provider by importing its metadata"),
|
||||
() => html`<p>Under construction</p>`
|
||||
],
|
||||
];
|
||||
|
||||
function mapProviders([modelName, name, description, children]: ProviderType): ProviderOption {
|
||||
function mapProviders([modelName, name, description]: ProviderType): TypeCreate {
|
||||
return {
|
||||
modelName,
|
||||
name,
|
||||
description,
|
||||
component: "",
|
||||
...(children ? { children: children.map(mapProviders) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export const providerTypesList = _providerTypesTable.map(mapProviders);
|
||||
|
||||
export const providerRendererList = new Map<string, ProviderRenderer>(
|
||||
_providerTypesTable.map(([modelName, _0, _1, renderer]) => [modelName, renderer]),
|
||||
);
|
||||
|
||||
export default providerTypesList;
|
||||
|
|
|
@ -1,27 +1,18 @@
|
|||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import ApplicationWizardPageBase from "./ApplicationWizardPageBase";
|
||||
import { providerRendererList } from "./ak-application-wizard-authentication-method-choice.choices";
|
||||
import "./ldap/ak-application-wizard-authentication-by-ldap";
|
||||
import "./oauth/ak-application-wizard-authentication-by-oauth";
|
||||
import "./proxy/ak-application-wizard-authentication-for-reverse-proxy";
|
||||
import "./proxy/ak-application-wizard-authentication-for-single-forward-proxy";
|
||||
|
||||
// prettier-ignore
|
||||
const handlers = new Map<string, () => TemplateResult>([
|
||||
["ldapprovider`", () => html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`],
|
||||
["oauth2provider", () => html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`],
|
||||
["proxyprovider-proxy", () => html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`],
|
||||
["proxyprovider-forwardsingle", () => html`<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>`],
|
||||
["radiusprovider", () => html`<p>Under construction</p>`],
|
||||
["samlprovider", () => html`<p>Under construction</p>`],
|
||||
["scimprovider", () => html`<p>Under construction</p>`],
|
||||
]);
|
||||
|
||||
@customElement("ak-application-wizard-authentication-method")
|
||||
export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBase {
|
||||
render() {
|
||||
const handler = handlers.get(this.wizard.providerType);
|
||||
const handler = providerRendererList.get(this.wizard.providerType);
|
||||
if (!handler) {
|
||||
throw new Error(
|
||||
"Unrecognized authentication method in ak-application-wizard-authentication-method",
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
The design of the wizard is actually very simple. There is an orchestrator in the Context object;
|
||||
it takes messages from the current page and grants permissions to proceed based on the content of
|
||||
the Context object after a message.
|
||||
|
||||
The fields of the Context object are:
|
||||
|
||||
```Javascript
|
||||
{
|
||||
step: number // The page currently being visited
|
||||
providerType: The provider type chosen in step 2. Dictates which view to show in step 3
|
||||
application: // The data collected from the ApplicationDetails page
|
||||
provider: // the data collected from the ProviderDetails page.
|
||||
|
||||
|
||||
```
|
||||
|
||||
The orchestrator leans on the per-page forms to tell it when a page is "valid enough to proceed".
|
||||
|
||||
When it reaches the last page, the transaction is triggered. If there are errors, the user is
|
||||
invited to "go back to the page where the error occurred" and try again.
|
|
@ -0,0 +1,64 @@
|
|||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
|
||||
import { LDAPAPIAccessMode } from "@goauthentik/api";
|
||||
|
||||
export const bindModeOptions = [
|
||||
{
|
||||
label: msg("Cached binding"),
|
||||
value: LDAPAPIAccessMode.Cached,
|
||||
default: true,
|
||||
description: html`${msg(
|
||||
"Flow is executed and session is cached in memory. Flow is executed when session expires",
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
label: msg("Direct binding"),
|
||||
value: LDAPAPIAccessMode.Direct,
|
||||
description: html`${msg(
|
||||
"Always execute the configured bind flow to authenticate the user",
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
|
||||
export const searchModeOptions = [
|
||||
{
|
||||
label: msg("Cached querying"),
|
||||
value: LDAPAPIAccessMode.Cached,
|
||||
default: true,
|
||||
description: html`${msg(
|
||||
"The outpost holds all users and groups in-memory and will refresh every 5 Minutes",
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
label: msg("Direct querying"),
|
||||
value: LDAPAPIAccessMode.Direct,
|
||||
description: html`${msg(
|
||||
"Always returns the latest data, but slower than cached querying",
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
|
||||
export const mfaSupportHelp = msg(
|
||||
"When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
|
||||
);
|
||||
|
||||
export const groupHelp = msg(
|
||||
"The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
|
||||
);
|
||||
|
||||
export const cryptoCertificateHelp = msg(
|
||||
"The certificate for the above configured Base DN. As a fallback, the provider uses a self-signed certificate.",
|
||||
);
|
||||
|
||||
export const tlsServerNameHelp = msg(
|
||||
"DNS name for which the above configured certificate should be used. The certificate cannot be detected based on the base DN, as the SSL/TLS negotiation happens before such data is exchanged.",
|
||||
);
|
||||
|
||||
export const uidStartNumberHelp = msg(
|
||||
"The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber",
|
||||
);
|
||||
|
||||
export const gidStartNumberHelp = msg(
|
||||
"The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
|
||||
);
|
|
@ -1,73 +0,0 @@
|
|||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { TemplateResult, html } from "lit";
|
||||
|
||||
import {
|
||||
CoreApi,
|
||||
FlowDesignationEnum,
|
||||
FlowsApi,
|
||||
LDAPProviderRequest,
|
||||
ProvidersApi,
|
||||
UserServiceAccountResponse,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-application-wizard-type-ldap")
|
||||
export class TypeLDAPApplicationWizardPage extends WizardFormPage {
|
||||
sidebarLabel = () => msg("LDAP details");
|
||||
|
||||
nextDataCallback = async (data: KeyUnknown): Promise<boolean> => {
|
||||
let name = this.host.state["name"] as string;
|
||||
// Check if a provider with the name already exists
|
||||
const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({
|
||||
search: name,
|
||||
});
|
||||
if (providers.results.filter((provider) => provider.name == name)) {
|
||||
name += "-1";
|
||||
}
|
||||
this.host.addActionBefore(msg("Create service account"), async (): Promise<boolean> => {
|
||||
const serviceAccount = await new CoreApi(DEFAULT_CONFIG).coreUsersServiceAccountCreate({
|
||||
userServiceAccountRequest: {
|
||||
name: name,
|
||||
createGroup: true,
|
||||
},
|
||||
});
|
||||
this.host.state["serviceAccount"] = serviceAccount;
|
||||
return true;
|
||||
});
|
||||
this.host.addActionBefore(msg("Create provider"), async (): Promise<boolean> => {
|
||||
// Get all flows and default to the implicit authorization
|
||||
const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
|
||||
designation: FlowDesignationEnum.Authorization,
|
||||
ordering: "slug",
|
||||
});
|
||||
const serviceAccount = this.host.state["serviceAccount"] as UserServiceAccountResponse;
|
||||
const req: LDAPProviderRequest = {
|
||||
name: name,
|
||||
authorizationFlow: flows.results[0].pk,
|
||||
baseDn: data.baseDN as string,
|
||||
searchGroup: serviceAccount.groupPk,
|
||||
};
|
||||
const provider = await new ProvidersApi(DEFAULT_CONFIG).providersLdapCreate({
|
||||
lDAPProviderRequest: req,
|
||||
});
|
||||
this.host.state["provider"] = provider;
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
const domainParts = window.location.hostname.split(".");
|
||||
const defaultBaseDN = domainParts.map((part) => `dc=${part}`).join(",");
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${msg("Base DN")} name="baseDN" ?required=${true}>
|
||||
<input type="text" value="${defaultBaseDN}" class="pf-c-form-control" required />
|
||||
</ak-form-element-horizontal>
|
||||
</form> `;
|
||||
}
|
||||
}
|
|
@ -15,76 +15,26 @@ import { customElement } from "@lit/reactive-element/decorators/custom-element.j
|
|||
import { html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import { FlowsInstancesListDesignationEnum, LDAPAPIAccessMode } from "@goauthentik/api";
|
||||
import { FlowsInstancesListDesignationEnum } from "@goauthentik/api";
|
||||
import type { LDAPProvider } from "@goauthentik/api";
|
||||
|
||||
import ApplicationWizardPageBase from "../ApplicationWizardPageBase";
|
||||
|
||||
const bindModeOptions = [
|
||||
{
|
||||
label: msg("Cached binding"),
|
||||
value: LDAPAPIAccessMode.Cached,
|
||||
default: true,
|
||||
description: html`${msg(
|
||||
"Flow is executed and session is cached in memory. Flow is executed when session expires",
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
label: msg("Direct binding"),
|
||||
value: LDAPAPIAccessMode.Direct,
|
||||
description: html`${msg(
|
||||
"Always execute the configured bind flow to authenticate the user",
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
|
||||
const searchModeOptions = [
|
||||
{
|
||||
label: msg("Cached querying"),
|
||||
value: LDAPAPIAccessMode.Cached,
|
||||
default: true,
|
||||
description: html`${msg(
|
||||
"The outpost holds all users and groups in-memory and will refresh every 5 Minutes",
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
label: msg("Direct querying"),
|
||||
value: LDAPAPIAccessMode.Direct,
|
||||
description: html`${msg(
|
||||
"Always returns the latest data, but slower than cached querying",
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
|
||||
const groupHelp = msg(
|
||||
"Users in the selected group can do search queries. If no group is selected, no LDAP Searches are allowed.",
|
||||
);
|
||||
|
||||
const mfaSupportHelp = msg(
|
||||
"When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
|
||||
);
|
||||
import ApplicationWizardProviderPageBase from "../ApplicationWizardProviderPageBase";
|
||||
import {
|
||||
bindModeOptions,
|
||||
cryptoCertificateHelp,
|
||||
gidStartNumberHelp,
|
||||
groupHelp,
|
||||
mfaSupportHelp,
|
||||
searchModeOptions,
|
||||
tlsServerNameHelp,
|
||||
uidStartNumberHelp,
|
||||
} from "./LDAPOptionsAndHelp";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-by-ldap")
|
||||
export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBase {
|
||||
handleChange(ev: InputEvent) {
|
||||
if (!ev.target) {
|
||||
console.warn(`Received event with no target: ${ev}`);
|
||||
return;
|
||||
}
|
||||
const target = ev.target as HTMLInputElement;
|
||||
const value = target.type === "checkbox" ? target.checked : target.value;
|
||||
this.dispatchWizardUpdate({
|
||||
provider: {
|
||||
...this.wizard.provider,
|
||||
[target.name]: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export class ApplicationWizardApplicationDetails extends ApplicationWizardProviderPageBase {
|
||||
render() {
|
||||
const provider = this.wizard.provider as LDAPProvider | undefined;
|
||||
|
||||
// prettier-ignore
|
||||
return html` <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
|
@ -149,12 +99,9 @@ export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBa
|
|||
name="baseDn"
|
||||
label=${msg("Base DN")}
|
||||
required
|
||||
value="${first(
|
||||
provider?.baseDn,
|
||||
"DC=ldap,DC=goauthentik,DC=io"
|
||||
)}"
|
||||
value="${first(provider?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}"
|
||||
help=${msg(
|
||||
"LDAP DN under which bind requests and search requests can be made."
|
||||
"LDAP DN under which bind requests and search requests can be made.",
|
||||
)}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
@ -165,11 +112,7 @@ export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBa
|
|||
name="certificate"
|
||||
>
|
||||
</ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"The certificate for the above configured Base DN. As a fallback, the provider uses a self-signed certificate."
|
||||
)}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">${cryptoCertificateHelp}</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-text-input
|
||||
|
@ -177,9 +120,7 @@ export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBa
|
|||
required
|
||||
name="tlsServerName"
|
||||
value="${first(provider?.tlsServerName, "")}"
|
||||
help=${msg(
|
||||
"DNS name for which the above configured certificate should be used. The certificate cannot be detected based on the base DN, as the SSL/TLS negotiation happens before such data is exchanged."
|
||||
)}
|
||||
help=${tlsServerNameHelp}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-number-input
|
||||
|
@ -187,9 +128,7 @@ export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBa
|
|||
required
|
||||
name="uidStartNumber"
|
||||
value="${first(provider?.uidStartNumber, 2000)}"
|
||||
help=${msg(
|
||||
"The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber"
|
||||
)}
|
||||
help=${uidStartNumberHelp}
|
||||
></ak-number-input>
|
||||
|
||||
<ak-number-input
|
||||
|
@ -197,9 +136,7 @@ export class ApplicationWizardApplicationDetails extends ApplicationWizardPageBa
|
|||
required
|
||||
name="gidStartNumber"
|
||||
value="${first(provider?.gidStartNumber, 4000)}"
|
||||
help=${msg(
|
||||
"The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber"
|
||||
)}
|
||||
help=${gidStartNumberHelp}
|
||||
></ak-number-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
|
|
@ -33,10 +33,10 @@ import type {
|
|||
PaginatedScopeMappingList,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import ApplicationWizardPageBase from "../ApplicationWizardPageBase";
|
||||
import ApplicationWizardProviderPageBase from "../ApplicationWizardProviderPageBase";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-by-oauth")
|
||||
export class ApplicationWizardAuthenticationByOauth extends ApplicationWizardPageBase {
|
||||
export class ApplicationWizardAuthenticationByOauth extends ApplicationWizardProviderPageBase {
|
||||
@state()
|
||||
showClientSecret = false;
|
||||
|
||||
|
@ -66,25 +66,9 @@ export class ApplicationWizardAuthenticationByOauth extends ApplicationWizardPag
|
|||
});
|
||||
}
|
||||
|
||||
handleChange(ev: InputEvent) {
|
||||
if (!ev.target) {
|
||||
console.warn(`Received event with no target: ${ev}`);
|
||||
return;
|
||||
}
|
||||
const target = ev.target as HTMLInputElement;
|
||||
const value = target.type === "checkbox" ? target.checked : target.value;
|
||||
this.dispatchWizardUpdate({
|
||||
provider: {
|
||||
...this.wizard.provider,
|
||||
[target.name]: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const provider = this.wizard.provider as OAuth2Provider | undefined;
|
||||
|
||||
// prettier-ignore
|
||||
return html`<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
|
@ -106,6 +90,7 @@ export class ApplicationWizardAuthenticationByOauth extends ApplicationWizardPag
|
|||
${msg("Flow used when a user access this provider and is not authenticated.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
name="authorizationFlow"
|
||||
label=${msg("Authorization flow")}
|
||||
|
@ -135,26 +120,29 @@ export class ApplicationWizardAuthenticationByOauth extends ApplicationWizardPag
|
|||
.options=${clientTypeOptions}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-text-input
|
||||
name="clientId"
|
||||
label=${msg("Client ID")}
|
||||
value="${first(
|
||||
provider?.clientId,
|
||||
randomString(40, ascii_letters + digits)
|
||||
randomString(40, ascii_letters + digits),
|
||||
)}"
|
||||
required
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="clientSecret"
|
||||
label=${msg("Client Secret")}
|
||||
value="${first(
|
||||
provider?.clientSecret,
|
||||
randomString(128, ascii_letters + digits)
|
||||
randomString(128, ascii_letters + digits),
|
||||
)}"
|
||||
?hidden=${!this.showClientSecret}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-textarea-input
|
||||
name="redirectUris"
|
||||
label=${msg("Redirect URIs/Origins (RegEx)")}
|
||||
|
@ -189,6 +177,7 @@ export class ApplicationWizardAuthenticationByOauth extends ApplicationWizardPag
|
|||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="accessTokenValidity"
|
||||
label=${msg("Access Token validity")}
|
||||
|
@ -212,6 +201,7 @@ export class ApplicationWizardAuthenticationByOauth extends ApplicationWizardPag
|
|||
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
|
||||
>
|
||||
</ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal label=${msg("Scopes")} name="propertyMappings">
|
||||
<select class="pf-c-form-control" multiple>
|
||||
${this.propertyMappings?.results.map((scope) => {
|
||||
|
@ -219,14 +209,12 @@ export class ApplicationWizardAuthenticationByOauth extends ApplicationWizardPag
|
|||
if (!provider?.propertyMappings) {
|
||||
selected =
|
||||
scope.managed?.startsWith(
|
||||
"goauthentik.io/providers/oauth2/scope-"
|
||||
"goauthentik.io/providers/oauth2/scope-",
|
||||
) || false;
|
||||
} else {
|
||||
selected = Array.from(provider?.propertyMappings).some(
|
||||
(su) => {
|
||||
selected = Array.from(provider?.propertyMappings).some((su) => {
|
||||
return su == scope.pk;
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
return html`<option
|
||||
value=${ifDefined(scope.pk)}
|
||||
|
@ -238,13 +226,14 @@ export class ApplicationWizardAuthenticationByOauth extends ApplicationWizardPag
|
|||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Select which scopes can be used by the client. The client still has to specify the scope to access the data."
|
||||
"Select which scopes can be used by the client. The client still has to specify the scope to access the data.",
|
||||
)}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Hold control/command to select multiple items.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-radio-input
|
||||
name="subMode"
|
||||
label=${msg("Subject mode")}
|
||||
|
@ -252,7 +241,7 @@ export class ApplicationWizardAuthenticationByOauth extends ApplicationWizardPag
|
|||
.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."
|
||||
"Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
@ -260,7 +249,7 @@ export class ApplicationWizardAuthenticationByOauth extends ApplicationWizardPag
|
|||
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."
|
||||
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
|
||||
)}></ak-switch-input
|
||||
>
|
||||
<ak-radio-input
|
||||
|
@ -270,7 +259,7 @@ export class ApplicationWizardAuthenticationByOauth extends ApplicationWizardPag
|
|||
.options=${issuerModeOptions}
|
||||
.value=${provider?.issuerMode}
|
||||
help=${msg(
|
||||
"Configure how the issuer field of the ID Token should be filled."
|
||||
"Configure how the issuer field of the ID Token should be filled.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
@ -296,7 +285,7 @@ export class ApplicationWizardAuthenticationByOauth extends ApplicationWizardPag
|
|||
</select>
|
||||
<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."
|
||||
"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">
|
||||
|
|
|
@ -21,11 +21,11 @@ import {
|
|||
SourcesApi,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import ApplicationWizardPageBase from "../ApplicationWizardPageBase";
|
||||
import ApplicationWizardProviderPageBase from "../ApplicationWizardProviderPageBase";
|
||||
|
||||
type MaybeTemplateResult = TemplateResult | typeof nothing;
|
||||
|
||||
export class AkTypeProxyApplicationWizardPage extends ApplicationWizardPageBase {
|
||||
export class AkTypeProxyApplicationWizardPage extends ApplicationWizardProviderPageBase {
|
||||
constructor() {
|
||||
super();
|
||||
new PropertymappingsApi(DEFAULT_CONFIG)
|
||||
|
@ -44,21 +44,6 @@ export class AkTypeProxyApplicationWizardPage extends ApplicationWizardPageBase
|
|||
});
|
||||
}
|
||||
|
||||
handleChange(ev: InputEvent) {
|
||||
if (!ev.target) {
|
||||
console.warn(`Received event with no target: ${ev}`);
|
||||
return;
|
||||
}
|
||||
const target = ev.target as HTMLInputElement;
|
||||
const value = target.type === "checkbox" ? target.checked : target.value;
|
||||
this.dispatchWizardUpdate({
|
||||
provider: {
|
||||
...this.wizard.provider,
|
||||
[target.name]: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
propertyMappings?: PaginatedScopeMappingList;
|
||||
oauthSources?: PaginatedOAuthSourceList;
|
||||
|
||||
|
@ -111,6 +96,7 @@ export class AkTypeProxyApplicationWizardPage extends ApplicationWizardPageBase
|
|||
required
|
||||
label=${msg("Name")}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
?required=${false}
|
||||
|
@ -125,6 +111,7 @@ export class AkTypeProxyApplicationWizardPage extends ApplicationWizardPageBase
|
|||
${msg("Flow used when a user access this provider and is not authenticated.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
|
@ -157,6 +144,7 @@ export class AkTypeProxyApplicationWizardPage extends ApplicationWizardPageBase
|
|||
certificate=${ifDefined(this.instance?.certificate ?? undefined)}
|
||||
></ak-crypto-certificate-search>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Additional scopes")}
|
||||
name="propertyMappings"
|
||||
|
@ -187,6 +175,7 @@ export class AkTypeProxyApplicationWizardPage extends ApplicationWizardPageBase
|
|||
${msg("Hold control/command to select multiple items.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-textarea-input
|
||||
name="skipPathRegex"
|
||||
label=${this.mode === ProxyMode.ForwardDomain
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import { msg } from "@lit/localize";
|
||||
|
||||
import { DigestAlgorithmEnum, SignatureAlgorithmEnum, SpBindingEnum } from "@goauthentik/api";
|
||||
|
||||
export const spBindingOptions = [
|
||||
{
|
||||
label: msg("Redirect"),
|
||||
value: SpBindingEnum.Redirect,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: msg("Post"),
|
||||
value: SpBindingEnum.Post,
|
||||
},
|
||||
];
|
||||
|
||||
export const digestAlgorithmOptions = [
|
||||
{
|
||||
label: "SHA1",
|
||||
value: DigestAlgorithmEnum._200009Xmldsigsha1,
|
||||
},
|
||||
{
|
||||
label: "SHA256",
|
||||
value: DigestAlgorithmEnum._200104Xmlencsha256,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: "SHA384",
|
||||
value: DigestAlgorithmEnum._200104XmldsigMoresha384,
|
||||
},
|
||||
{
|
||||
label: "SHA512",
|
||||
value: DigestAlgorithmEnum._200104Xmlencsha512,
|
||||
},
|
||||
];
|
||||
|
||||
export const signatureAlgorithmOptions = [
|
||||
{
|
||||
label: "RSA-SHA1",
|
||||
value: SignatureAlgorithmEnum._200009XmldsigrsaSha1,
|
||||
},
|
||||
{
|
||||
label: "RSA-SHA256",
|
||||
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha256,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: "RSA-SHA384",
|
||||
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha384,
|
||||
},
|
||||
{
|
||||
label: "RSA-SHA512",
|
||||
value: SignatureAlgorithmEnum._200104XmldsigMorersaSha512,
|
||||
},
|
||||
{
|
||||
label: "DSA-SHA1",
|
||||
value: SignatureAlgorithmEnum._200009XmldsigdsaSha1,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,250 @@
|
|||
import "@goauthentik/admin/common/ak-core-group-search";
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-number-input";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/components/ak-switch-input";
|
||||
import "@goauthentik/components/ak-text-input";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
|
||||
import { html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
PaginatedSAMLPropertyMappingList,
|
||||
PropertymappingsApi,
|
||||
SAMLProvider,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import ApplicationWizardProviderPageBase from "../ApplicationWizardProviderPageBase";
|
||||
import {
|
||||
digestAlgorithmOptions,
|
||||
signatureAlgorithmOptions,
|
||||
spBindingOptions,
|
||||
} from "./SamlProviderOptions";
|
||||
|
||||
@customElement("ak-application-wizard-authentication-by-saml-configuration")
|
||||
export class ApplicationWizardProviderSamlConfiguration extends ApplicationWizardProviderPageBase {
|
||||
propertyMappings?: PaginatedSAMLPropertyMappingList;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
new PropertymappingsApi(DEFAULT_CONFIG)
|
||||
.propertymappingsSamlList({
|
||||
ordering: "saml_name",
|
||||
})
|
||||
.then((propertyMappings: PaginatedSAMLPropertyMappingList) => {
|
||||
this.propertyMappings = propertyMappings;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const provider = this.wizard.provider as SAMLProvider | undefined;
|
||||
|
||||
return html` <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
||||
<ak-text-input
|
||||
name="name"
|
||||
value=${ifDefined(provider?.name)}
|
||||
required
|
||||
label=${msg("Name")}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authentication flow")}
|
||||
?required=${false}
|
||||
name="authenticationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
||||
.currentFlow=${provider?.authenticationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when a user access this provider and is not authenticated.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
name="authorizationFlow"
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${provider?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-text-input
|
||||
name="acsUrl"
|
||||
value=${ifDefined(provider?.acsUrl)}
|
||||
required
|
||||
label=${msg("ACS URL")}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="issuer"
|
||||
value=${provider?.issuer || "authentik"}
|
||||
required
|
||||
label=${msg("Issuer")}
|
||||
help=${msg("Also known as EntityID.")}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-radio-input
|
||||
name="spBinding"
|
||||
label=${msg("Service Provider Binding")}
|
||||
required
|
||||
.options=${spBindingOptions}
|
||||
.value=${provider?.spBinding}
|
||||
help=${msg(
|
||||
"Determines how authentik sends the response back to the Service Provider.",
|
||||
)}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-text-input
|
||||
name="audience"
|
||||
value=${ifDefined(provider?.audience)}
|
||||
label=${msg("Audience")}
|
||||
></ak-text-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced protocol settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Signing Certificate")}
|
||||
name="signingKp"
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.signingKp ?? undefined)}
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Certificate used to sign outgoing Responses going to the Service Provider.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Verification Certificate")}
|
||||
name="verificationKp"
|
||||
>
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.verificationKp ?? undefined)}
|
||||
nokey
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Property mappings")}
|
||||
?required=${true}
|
||||
name="propertyMappings"
|
||||
>
|
||||
<select class="pf-c-form-control" multiple>
|
||||
${this.propertyMappings?.results.map((mapping) => {
|
||||
let selected = false;
|
||||
if (!provider?.propertyMappings) {
|
||||
selected =
|
||||
mapping.managed?.startsWith(
|
||||
"goauthentik.io/providers/saml",
|
||||
) || false;
|
||||
} else {
|
||||
selected = Array.from(provider?.propertyMappings).some((su) => {
|
||||
return su == mapping.pk;
|
||||
});
|
||||
}
|
||||
return html`<option
|
||||
value=${ifDefined(mapping.pk)}
|
||||
?selected=${selected}
|
||||
>
|
||||
${mapping.name}
|
||||
</option>`;
|
||||
})}
|
||||
</select>
|
||||
<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("NameID Property Mapping")}
|
||||
name="nameIdMapping"
|
||||
>
|
||||
<ak-saml-property-mapping-search
|
||||
name="nameIdMapping"
|
||||
propertymapping=${ifDefined(provider?.nameIdMapping ?? undefined)}
|
||||
></ak-saml-property-mapping-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-text-input
|
||||
name="assertionValidNotBefore"
|
||||
value=${provider?.assertionValidNotBefore || "minutes=-5"}
|
||||
required
|
||||
label=${msg("Assertion valid not before")}
|
||||
help=${msg("Configure the maximum allowed time drift for an assertion.")}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="assertionValidNotOnOrAfter"
|
||||
value=${provider?.assertionValidNotOnOrAfter || "minutes=5"}
|
||||
required
|
||||
label=${msg("Assertion valid not on or after")}
|
||||
help=${msg("Assertion not valid on or after current time + this value.")}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-text-input
|
||||
name="sessionValidNotOnOrAfter"
|
||||
value=${provider?.sessionValidNotOnOrAfter || "minutes=86400"}
|
||||
required
|
||||
label=${msg("Session valid not on or after")}
|
||||
help=${msg("Session not valid on or after current time + this value.")}
|
||||
></ak-text-input>
|
||||
|
||||
<ak-radio-input
|
||||
name="digestAlgorithm"
|
||||
label=${msg("Digest algorithm")}
|
||||
required
|
||||
.options=${digestAlgorithmOptions}
|
||||
.value=${provider?.digestAlgorithm}
|
||||
>
|
||||
</ak-radio-input>
|
||||
|
||||
<ak-radio-input
|
||||
name="signatureAlgorithm"
|
||||
label=${msg("Signature algorithm")}
|
||||
required
|
||||
.options=${signatureAlgorithmOptions}
|
||||
.value=${provider?.signatureAlgorithm}
|
||||
>
|
||||
</ak-radio-input>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default ApplicationWizardProviderSamlConfiguration;
|
|
@ -0,0 +1,112 @@
|
|||
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 {
|
||||
PropertymappingsApi,
|
||||
PropertymappingsSamlListRequest,
|
||||
SAMLPropertyMapping,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
async function fetchObjects(query?: string): Promise<SAMLPropertyMapping[]> {
|
||||
const args: PropertymappingsSamlListRequest = {
|
||||
ordering: "saml_name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const items = await new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSamlList(args);
|
||||
return items.results;
|
||||
}
|
||||
|
||||
function renderElement(item: SAMLPropertyMapping): string {
|
||||
return item.name;
|
||||
}
|
||||
|
||||
function renderValue(item: SAMLPropertyMapping | undefined): string | undefined {
|
||||
return item?.pk;
|
||||
}
|
||||
|
||||
/**
|
||||
* SAML Property Mapping Search
|
||||
*
|
||||
* @element ak-saml-property-mapping-search
|
||||
*
|
||||
* A wrapper around SearchSelect for the SAML Property Search. It's a unique search, but for the
|
||||
* purpose of the form all you need to know is that it is being searched and selected. Let's put the
|
||||
* how somewhere else.
|
||||
*
|
||||
*/
|
||||
|
||||
@customElement("ak-saml-property-mapping-search")
|
||||
export class SAMLPropertyMappingSearch extends CustomListenerElement(AKElement) {
|
||||
/**
|
||||
* The current property mapping known to the caller.
|
||||
*
|
||||
* @attr
|
||||
*/
|
||||
@property({ type: String, reflect: true, attribute: "propertymapping" })
|
||||
propertyMapping?: string;
|
||||
|
||||
@query("ak-search-select")
|
||||
search!: SearchSelect<SAMLPropertyMapping>;
|
||||
|
||||
@property({ type: String })
|
||||
name: string | null | undefined;
|
||||
|
||||
selectedPropertyMapping?: SAMLPropertyMapping;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.selected = this.selected.bind(this);
|
||||
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
|
||||
this.addCustomListener("ak-change", this.handleSearchUpdate);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.selectedPropertyMapping ? renderValue(this.selectedPropertyMapping) : 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.selectedPropertyMapping = ev.detail.value;
|
||||
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
selected(item: SAMLPropertyMapping): boolean {
|
||||
return this.propertyMapping === item.pk;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ak-search-select
|
||||
.fetchObjects=${fetchObjects}
|
||||
.renderElement=${renderElement}
|
||||
.value=${renderValue}
|
||||
.selected=${this.selected}
|
||||
blankable
|
||||
>
|
||||
</ak-search-select>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default SAMLPropertyMappingSearch;
|
Reference in New Issue