Files
authentik/web/src/admin/providers/saml/SAMLProviderViewPage.ts
Teffen Ellis b88d082947 web/a11y: Modals, Command Palette (Merge branch) (#17812)
* Use project relative paths.

* Fix tests.

* Fix types.

* Clean up admin imports.

* Move admin import.

* Remove or replace references to admin.

* Typo fix.

* Flesh out ak-modal, about modal.

* Flesh out lazy modal.

* Fix portal elements not using dialog scope.

* Fix url parameters, wizards.

* Fix invokers, lazy load.

* Fix theming.

* Add placeholders, help.

* Flesh out command palette.

Flesh out styles, command invokers.

Continue clean up.

Allow slotted content.

Flesh out.

* Flesh out edit invoker. Prep groups.

* Fix odd labeling, legacy situations.

* Prepare deprecation of table modal. Clean up serialization.

* Tidy types.

* Port provider select modal.

* Port member select form.

* Flesh out role modal. Fix loading state.

* Port user group form.

* Fix spellcheck.

* Fix dialog detection.

* Revise types.

* Port rac launch modal.

* Remove deprecated table modal.

* Consistent form action placement.

* Consistent casing.

* Consistent alignment.

* Use more appropriate description.

* Flesh out icon. Fix alignment, colors.

* Flesh out user search.

* Consistent save button.

* Clean up labels.

* Reduce warning noise.

* Clean up label.

* Use attribute e2e expects.

* Use directive. Fix lifecycle

* Fix frequent un-memoized entries.

* Fix up closedBy detection.

* Tidy alignment.

* Fix types, composition.

* Fix labels, tests.

* Fix up impersonation, labels.

* Flesh out. Fix refresh after submit.

* Flesh out basic modal test.

* Fix ARIA.

* Flesh out roles test.

* Revise selectors.

* Clean up selectors.

* Fix impersonation labels, form references.

* Fix messages appearing under modals.

* Ensure reason is parsed.

* Flesh out impersonation test.

* Flesh out impersonate test.

* Flesh out application tests. Clean up toolbar header, ARIA.

* Flesh out wizard test.

* Refine weight, order.

* Fix up initial values, selectors.

* Fix tests.

* Fix selector.
2026-03-25 06:07:29 +00:00

631 lines
28 KiB
TypeScript

import "#admin/providers/RelatedApplicationButton";
import "#admin/providers/saml/SAMLProviderForm";
import "#admin/rbac/ObjectPermissionsPage";
import "#admin/events/ObjectChangelog";
import "#elements/CodeMirror";
import "#elements/EmptyState";
import "#elements/Tabs";
import "#elements/buttons/ActionButton/index";
import "#elements/buttons/ModalButton";
import "#elements/buttons/SpinnerButton/index";
import { DEFAULT_CONFIG } from "#common/api/config";
import { EVENT_REFRESH } from "#common/constants";
import { MessageLevel } from "#common/messages";
import { AKElement } from "#elements/Base";
import { showMessage } from "#elements/messages/MessageContainer";
import { SlottedTemplateResult } from "#elements/types";
import renderDescriptionList from "#components/DescriptionList";
import {
CertificateKeyPair,
CoreApi,
CoreUsersListRequest,
CryptoApi,
ProvidersApi,
RbacPermissionsAssignedByRolesListModelEnum,
SAMLMetadata,
SAMLProvider,
User,
} from "@goauthentik/api";
import { msg } from "@lit/localize";
import { CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
export interface SAMLPreviewAttribute {
attributes: {
Name: string;
Value: string[];
}[];
nameID: string;
}
@customElement("ak-provider-saml-view")
export class SAMLProviderViewPage extends AKElement {
@property({ type: Number })
providerID?: number;
@state()
provider?: SAMLProvider;
@state()
preview?: SAMLPreviewAttribute;
@state()
metadata?: SAMLMetadata;
@state()
signer: CertificateKeyPair | null = null;
@state()
verifier: CertificateKeyPair | null = null;
@state()
previewUser?: User;
static styles: CSSResult[] = [
PFButton,
PFPage,
PFGrid,
PFContent,
PFCard,
PFList,
PFDescriptionList,
PFForm,
PFFormControl,
PFBanner,
];
constructor() {
super();
this.addEventListener(EVENT_REFRESH, () => {
if (!this.provider?.pk) return;
this.fetchProvider(this.provider.pk);
});
}
fetchPreview(): void {
new ProvidersApi(DEFAULT_CONFIG)
.providersSamlPreviewUserRetrieve({
id: this.provider?.pk || 0,
forUser: this.previewUser?.pk,
})
.then((preview) => {
this.preview = preview.preview as SAMLPreviewAttribute;
});
}
fetchCertificate(kpUuid: string) {
return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsRetrieve({ kpUuid });
}
fetchSigningCertificate(kpUuid: string) {
this.fetchCertificate(kpUuid).then((kp) => {
this.signer = kp;
this.requestUpdate("signer");
});
}
fetchVerificationCertificate(kpUuid: string) {
this.fetchCertificate(kpUuid).then((kp) => {
this.verifier = kp;
this.requestUpdate("verifier");
});
}
fetchProvider(id: number) {
new ProvidersApi(DEFAULT_CONFIG).providersSamlRetrieve({ id }).then((prov) => {
this.provider = prov;
// Clear existing signing certificate if the provider has none
if (!this.provider.signingKp) {
this.signer = null;
} else {
this.fetchSigningCertificate(this.provider.signingKp);
}
// Clear existing verification certificate if the provider has none
if (!this.provider.verificationKp) {
this.verifier = null;
} else {
this.fetchVerificationCertificate(this.provider.verificationKp);
}
});
}
willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("providerID") && this.providerID) {
this.fetchProvider(this.providerID);
}
}
renderRelatedObjects(): TemplateResult {
const relatedObjects = [];
if (this.provider?.assignedApplicationName) {
relatedObjects.push(
html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("Metadata")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<a
class="pf-c-button pf-m-primary"
target="_blank"
href=${ifDefined(this.provider?.urlDownloadMetadata)}
>
${msg("Download")}
</a>
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
if (!navigator.clipboard) {
return Promise.resolve(
showMessage({
level: MessageLevel.info,
message: this.provider?.urlDownloadMetadata || "",
}),
);
}
return navigator.clipboard.writeText(
this.provider?.urlDownloadMetadata || "",
);
}}
>
${msg("Copy download URL")}
</ak-action-button>
</div>
</dd>
</div>`,
);
}
if (this.signer) {
relatedObjects.push(
html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Download signing certificate")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<a
class="pf-c-button pf-m-primary"
href=${this.signer.certificateDownloadUrl}
>${msg("Download")}</a
>
</div>
</dd>
</div>`,
);
}
return html` <div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">${msg("Related objects")}</div>
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col">
${relatedObjects.length > 0 ? relatedObjects : html`-`}
</dl>
</div>
</div>`;
}
render(): SlottedTemplateResult {
if (!this.provider) {
return nothing;
}
return html`<main part="main">
<ak-tabs part="tabs">
<div
role="tabpanel"
tabindex="0"
slot="page-overview"
id="page-overview"
aria-label="${msg("Overview")}"
>
${this.renderTabOverview()}
</div>
${this.renderTabMetadata()}
<div
role="tabpanel"
tabindex="0"
slot="page-preview"
id="page-preview"
aria-label="${msg("Preview")}"
@activate=${() => {
this.fetchPreview();
}}
>
${this.renderTabPreview()}
</div>
<div
role="tabpanel"
tabindex="0"
slot="page-changelog"
id="page-changelog"
aria-label="${msg("Changelog")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-object-changelog
targetModelPk=${this.provider?.pk || ""}
targetModelName=${this.provider?.metaModelName || ""}
>
</ak-object-changelog>
</div>
</div>
</div>
<ak-rbac-object-permission-page
class="pf-c-page__main-section pf-m-no-padding-mobile"
role="tabpanel"
tabindex="0"
slot="page-permissions"
id="page-permissions"
aria-label="${msg("Permissions")}"
model=${RbacPermissionsAssignedByRolesListModelEnum.AuthentikProvidersSamlSamlprovider}
objectPk=${this.provider.pk}
></ak-rbac-object-permission-page>
</ak-tabs>
</main>`;
}
renderTabOverview(): SlottedTemplateResult {
if (!this.provider) {
return nothing;
}
return html`${
this.provider?.assignedApplicationName
? nothing
: html`<div slot="header" class="pf-c-banner pf-m-warning">
${msg("Warning: Provider is not used by an Application.")}
</div>`
}
<div class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter">
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-3-col-on-lg">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg("Name")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.provider.name}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Assigned to application")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ak-provider-related-application
.provider=${this.provider}
></ak-provider-related-application>
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg(
"ACS URL",
)}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.provider.acsUrl}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg(
"Audience",
)}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.provider.audience || "-"}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${msg(
"Issuer",
)}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.provider.issuer}
</div>
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__footer">
<ak-forms-modal>
<span slot="submit">${msg("Save Changes")}</span>
<span slot="header">${msg("Update SAML Provider")}</span>
<ak-provider-saml-form slot="form" .instancePk=${this.provider.pk || 0}>
</ak-provider-saml-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Edit")}
</button>
</ak-forms-modal>
</div>
</div>
${this.renderRelatedObjects()}
${
this.provider.assignedApplicationName
? html` <div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">${msg("SAML Configuration")}</div>
<div class="pf-c-card__body">
<form class="pf-c-form">
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${msg("EntityID/Issuer")}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${ifDefined(this.provider?.issuer)}"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${msg("SSO URL (Post)")}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${ifDefined(this.provider.urlSsoPost)}"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${msg("SSO URL (Redirect)")}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${ifDefined(this.provider.urlSsoRedirect)}"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${msg("SSO URL (IdP-initiated Login)")}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${ifDefined(this.provider.urlSsoInit)}"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${msg("SLO URL (Post)")}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${ifDefined(this.provider.urlSloPost)}"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${msg("SLO URL (Redirect)")}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${ifDefined(this.provider.urlSloRedirect)}"
/>
</div>
</form>
</div>
</div>`
: nothing
}
</div>
</div>`;
}
renderTabMetadata(): SlottedTemplateResult {
if (!this.provider) {
return nothing;
}
return html`
${this.provider.assignedApplicationName
? html` <div
role="tabpanel"
tabindex="0"
slot="page-metadata"
id="page-metadata"
aria-label="${msg("Metadata")}"
@activate=${() => {
new ProvidersApi(DEFAULT_CONFIG)
.providersSamlMetadataRetrieve({
id: this.provider?.pk || 0,
})
.then((metadata) => (this.metadata = metadata));
}}
>
<div
class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"
>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">${msg("SAML Metadata")}</div>
<div class="pf-c-card__body">
<a
class="pf-c-button pf-m-primary"
target="_blank"
href=${this.provider.urlDownloadMetadata}
>
${msg("Download")}
</a>
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
if (!navigator.clipboard) {
return Promise.resolve(
showMessage({
level: MessageLevel.info,
message:
this.provider?.urlDownloadMetadata || "",
}),
);
}
return navigator.clipboard.writeText(
this.provider?.urlDownloadMetadata || "",
);
}}
>
${msg("Copy download URL")}
</ak-action-button>
</div>
<div class="pf-c-card__footer">
<ak-codemirror
mode="xml"
readonly
value="${ifDefined(this.metadata?.metadata)}"
></ak-codemirror>
</div>
</div>
</div>
</div>`
: nothing}
`;
}
renderTabPreview(): SlottedTemplateResult {
if (!this.preview) {
return html`<ak-empty-state loading></ak-empty-state>`;
}
return html` <div
class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"
>
<div class="pf-c-card">
<div class="pf-c-card__title">${msg("Example SAML attributes")}</div>
<div class="pf-c-card__body">
${renderDescriptionList([
[
msg("Preview for user"),
html`
<ak-search-select
.fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = {
ordering: "username",
};
if (query !== undefined) {
args.search = query;
}
const users = await new CoreApi(
DEFAULT_CONFIG,
).coreUsersList(args);
return users.results;
}}
.renderElement=${(user: User): string => {
return user.username;
}}
.renderDescription=${(user: User): TemplateResult => {
return html`${user.name}`;
}}
.value=${(user: User | undefined): number | undefined => {
return user?.pk;
}}
.selected=${(user: User): boolean => {
return user.pk === this.previewUser?.pk;
}}
blankable
@ak-change=${(ev: CustomEvent) => {
this.previewUser = ev.detail.value;
this.fetchPreview();
}}
>
</ak-search-select>
`,
],
])}
</div>
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col-on-lg">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("NameID attribute")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.preview?.nameID}
</div>
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col-on-lg">
${this.preview?.attributes.map((attr) => {
return html` <div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${attr.Name}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ul class="pf-c-list">
${attr.Value.map((value) => {
return html` <li><pre>${value}</pre></li> `;
})}
</ul>
</div>
</dd>
</div>`;
})}
</dl>
</div>
</div>
</div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-provider-saml-view": SAMLProviderViewPage;
}
}