web: re-organise frontend and cleanup common code (#3572)

* fix repo in api client

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: re-organise files to match their interface

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* core: include version in script tags

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* cleanup maybe broken

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* revert rename

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: get rid of Client.ts

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* move more to common

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* more moving

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* format

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* unfuck files that vscode fucked, thanks

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* move more

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* finish moving (maybe)

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* ok more moving

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix more stuff that vs code destroyed

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* get rid "web" prefix for virtual package

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix locales

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* use custom base element

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix css file

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* don't run autoDetectLanguage when importing locale

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix circular dependencies

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: fix build

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L
2022-09-15 00:05:21 +02:00
committed by GitHub
parent 369440652c
commit 4a91a7d2e2
291 changed files with 2062 additions and 1921 deletions

View File

@ -0,0 +1,116 @@
import "@goauthentik/admin/providers/ProviderWizard";
import "@goauthentik/admin/providers/ldap/LDAPProviderForm";
import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
import "@goauthentik/admin/providers/saml/SAMLProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/forms/ProxyForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Provider, ProvidersApi } from "@goauthentik/api";
@customElement("ak-provider-list")
export class ProviderListPage extends TablePage<Provider> {
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return t`Providers`;
}
pageDescription(): string {
return t`Provide support for protocols like SAML and OAuth to assigned applications.`;
}
pageIcon(): string {
return "pf-icon pf-icon-integration";
}
checkbox = true;
@property()
order = "name";
async apiEndpoint(page: number): Promise<PaginatedResponse<Provider>> {
return new ProvidersApi(DEFAULT_CONFIG).providersAllList({
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn(t`Name`, "name"),
new TableColumn(t`Application`),
new TableColumn(t`Type`),
new TableColumn(t`Actions`),
];
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${t`Provider(s)`}
.objects=${this.selectedElements}
.usedBy=${(item: Provider) => {
return new ProvidersApi(DEFAULT_CONFIG).providersAllUsedByList({
id: item.pk,
});
}}
.delete=${(item: Provider) => {
return new ProvidersApi(DEFAULT_CONFIG).providersAllDestroy({
id: item.pk,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${t`Delete`}
</button>
</ak-forms-delete-bulk>`;
}
row(item: Provider): TemplateResult[] {
return [
html`<a href="#/core/providers/${item.pk}"> ${item.name} </a>`,
item.assignedApplicationName
? html`<i class="pf-icon pf-icon-ok pf-m-success"></i>
${t`Assigned to application `}
<a href="#/core/applications/${item.assignedApplicationSlug}"
>${item.assignedApplicationName}</a
>`
: html`<i class="pf-icon pf-icon-warning-triangle pf-m-warning"></i>
${t`Warning: Provider not assigned to any application.`}`,
html`${item.verboseName}`,
html`<ak-forms-modal>
<span slot="submit"> ${t`Update`} </span>
<span slot="header"> ${t`Update ${item.verboseName}`} </span>
<ak-proxy-form
slot="form"
.args=${{
instancePk: item.pk,
}}
type=${item.component}
>
</ak-proxy-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<i class="fas fa-edit"></i>
</button>
</ak-forms-modal>`,
];
}
renderObjectCreate(): TemplateResult {
return html`<ak-provider-wizard> </ak-provider-wizard> `;
}
}

View File

@ -0,0 +1,94 @@
import "@goauthentik/admin/providers/ldap/LDAPProviderViewPage";
import "@goauthentik/admin/providers/oauth2/OAuth2ProviderViewPage";
import "@goauthentik/admin/providers/proxy/ProxyProviderViewPage";
import "@goauthentik/admin/providers/saml/SAMLProviderViewPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/buttons/SpinnerButton";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import { Provider, ProvidersApi } from "@goauthentik/api";
@customElement("ak-provider-view")
export class ProviderViewPage extends AKElement {
@property({ type: Number })
set providerID(value: number) {
new ProvidersApi(DEFAULT_CONFIG)
.providersAllRetrieve({
id: value,
})
.then((prov) => (this.provider = prov));
}
@property({ attribute: false })
provider?: Provider;
static get styles(): CSSResult[] {
return [PFPage, AKGlobal];
}
renderProvider(): TemplateResult {
if (!this.provider) {
return html`<ak-empty-state ?loading=${true} ?fullHeight=${true}></ak-empty-state>`;
}
switch (this.provider?.component) {
case "ak-provider-saml-form":
return html`<ak-provider-saml-view
providerID=${ifDefined(this.provider.pk)}
></ak-provider-saml-view>`;
case "ak-provider-oauth2-form":
return html`<ak-provider-oauth2-view
providerID=${ifDefined(this.provider.pk)}
></ak-provider-oauth2-view>`;
case "ak-provider-proxy-form":
return html`<ak-provider-proxy-view
providerID=${ifDefined(this.provider.pk)}
></ak-provider-proxy-view>`;
case "ak-provider-ldap-form":
return html`<ak-provider-ldap-view
providerID=${ifDefined(this.provider.pk)}
></ak-provider-ldap-view>`;
default:
return html`<p>Invalid provider type ${this.provider?.component}</p>`;
}
}
render(): TemplateResult {
return html`<ak-page-header
icon="pf-icon pf-icon-integration"
header=${ifDefined(this.provider?.name)}
description=${ifDefined(this.provider?.verboseName)}
>
</ak-page-header>
<ak-tabs>
<section slot="page-overview" data-tab-title="${t`Overview`}">
${this.renderProvider()}
</section>
<section
slot="page-changelog"
data-tab-title="${t`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>
</section>
</ak-tabs>`;
}
}

View File

@ -0,0 +1,129 @@
import "@goauthentik/admin/providers/ldap/LDAPProviderForm";
import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
import "@goauthentik/admin/providers/saml/SAMLProviderForm";
import "@goauthentik/admin/providers/saml/SAMLProviderImportForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/ProxyForm";
import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
import "@goauthentik/elements/wizard/FormWizardPage";
import "@goauthentik/elements/wizard/Wizard";
import { WizardPage } from "@goauthentik/elements/wizard/WizardPage";
import { t } from "@lingui/macro";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { CSSResult, TemplateResult, html } from "lit";
import { property } from "lit/decorators.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFHint from "@patternfly/patternfly/components/Hint/hint.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { ProvidersApi, TypeCreate } from "@goauthentik/api";
@customElement("ak-provider-wizard-initial")
export class InitialProviderWizardPage extends WizardPage {
@property({ attribute: false })
providerTypes: TypeCreate[] = [];
static get styles(): CSSResult[] {
return [PFBase, PFForm, PFHint, PFButton, AKGlobal, PFRadio];
}
sidebarLabel = () => t`Select type`;
renderHint(): TemplateResult {
return html`<div class="pf-c-hint">
<div class="pf-c-hint__title">${t`Try the new application wizard`}</div>
<div class="pf-c-hint__body">
${t`The new application wizard greatly simplifies the steps required to create applications and providers.`}
</div>
<div class="pf-c-hint__footer">
<a
class="pf-c-button pf-m-link pf-m-inline"
href=${paramURL("/core/applications", {
createForm: true,
})}
>${t`Try it now`}</a
>
</div>
</div>
<br />`;
}
render(): TemplateResult {
return html` <form class="pf-c-form pf-m-horizontal">
${this.providerTypes.map((type) => {
return html`<div class="pf-c-radio">
<input
class="pf-c-radio__input"
type="radio"
name="type"
id=${type.component}
@change=${() => {
this.host.steps = ["initial", `type-${type.component}`];
this.host.isValid = true;
}}
/>
<label class="pf-c-radio__label" for=${type.component}>${type.name}</label>
<span class="pf-c-radio__description">${type.description}</span>
</div>`;
})}
</form>`;
}
}
@customElement("ak-provider-wizard")
export class ProviderWizard extends AKElement {
static get styles(): CSSResult[] {
return [PFBase, PFButton, AKGlobal, PFRadio];
}
@property()
createText = t`Create`;
@property({ attribute: false })
providerTypes: TypeCreate[] = [];
@property({ attribute: false })
finalHandler: () => Promise<void> = () => {
return Promise.resolve();
};
firstUpdated(): void {
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((types) => {
this.providerTypes = types;
});
}
render(): TemplateResult {
return html`
<ak-wizard
.steps=${["initial"]}
header=${t`New provider`}
description=${t`Create a new provider.`}
.finalHandler=${() => {
return this.finalHandler();
}}
>
<ak-provider-wizard-initial slot="initial" .providerTypes=${this.providerTypes}>
</ak-provider-wizard-initial>
${this.providerTypes.map((type) => {
return html`
<ak-wizard-page-form
slot=${`type-${type.component}`}
.sidebarLabel=${() => t`Create ${type.name}`}
>
<ak-proxy-form type=${type.component}></ak-proxy-form>
</ak-wizard-page-form>
`;
})}
<button slot="trigger" class="pf-c-button pf-m-primary">${this.createText}</button>
</ak-wizard>
`;
}
}

View File

@ -0,0 +1,38 @@
import "@goauthentik/admin/applications/ApplicationForm";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Spinner";
import "@goauthentik/elements/forms/ModalForm";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { Provider } from "@goauthentik/api";
@customElement("ak-provider-related-application")
export class RelatedApplicationButton extends AKElement {
static get styles(): CSSResult[] {
return [PFBase, PFButton];
}
@property({ attribute: false })
provider?: Provider;
render(): TemplateResult {
if (this.provider?.assignedApplicationSlug) {
return html`<a href="#/core/applications/${this.provider.assignedApplicationSlug}">
${this.provider.assignedApplicationName}
</a>`;
}
return html`<ak-forms-modal>
<span slot="submit"> ${t`Create`} </span>
<span slot="header"> ${t`Create Application`} </span>
<ak-application-form slot="form" .provider=${this.provider?.pk}> </ak-application-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button>
</ak-forms-modal>`;
}
}

View File

@ -0,0 +1,245 @@
import { DEFAULT_CONFIG, tenant } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
CoreApi,
CryptoApi,
FlowsApi,
FlowsInstancesListDesignationEnum,
LDAPAPIAccessMode,
LDAPProvider,
ProvidersApi,
} from "@goauthentik/api";
@customElement("ak-provider-ldap-form")
export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
loadInstance(pk: number): Promise<LDAPProvider> {
return new ProvidersApi(DEFAULT_CONFIG).providersLdapRetrieve({
id: pk,
});
}
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated provider.`;
} else {
return t`Successfully created provider.`;
}
}
send = (data: LDAPProvider): Promise<LDAPProvider> => {
if (this.instance) {
return new ProvidersApi(DEFAULT_CONFIG).providersLdapUpdate({
id: this.instance.pk || 0,
lDAPProviderRequest: data,
});
} else {
data.tlsServerName = "";
return new ProvidersApi(DEFAULT_CONFIG).providersLdapCreate({
lDAPProviderRequest: data,
});
}
};
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Bind flow`}
?required=${true}
name="authorizationFlow"
>
<select class="pf-c-form-control">
${until(
tenant().then((t) => {
return new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesList({
ordering: "slug",
designation: FlowsInstancesListDesignationEnum.Authentication,
})
.then((flows) => {
return flows.results.map((flow) => {
let selected = flow.pk === t.flowAuthentication;
if (this.instance?.authorizationFlow === flow.pk) {
selected = true;
}
return html`<option
value=${ifDefined(flow.pk)}
?selected=${selected}
>
${flow.name} (${flow.slug})
</option>`;
});
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`Flow used for users to authenticate. Currently only identification and password stages are supported.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Search group`} name="searchGroup">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.searchGroup === undefined}>
---------
</option>
${until(
new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then((groups) => {
return groups.results.map((group) => {
return html`<option
value=${ifDefined(group.pk)}
?selected=${this.instance?.searchGroup === group.pk}
>
${group.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`Users in the selected group can do search queries. If no group is selected, no LDAP Searches are allowed.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Bind mode`} name="bindMode">
<select class="pf-c-form-control">
<option
value="${LDAPAPIAccessMode.Cached}"
?selected=${this.instance?.bindMode === LDAPAPIAccessMode.Cached}
>
${t`Cached binding, flow is executed and session is cached in memory. Flow is executed when session expires.`}
</option>
<option
value="${LDAPAPIAccessMode.Direct}"
?selected=${this.instance?.bindMode === LDAPAPIAccessMode.Direct}
>
${t`Direct binding, always execute the configured bind flow to authenticate the user.`}
</option>
</select>
<p class="pf-c-form__helper-text">
${t`Configure how the outpost authenticates requests.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Search mode`} name="searchMode">
<select class="pf-c-form-control">
<option
value="${LDAPAPIAccessMode.Cached}"
?selected=${this.instance?.searchMode === LDAPAPIAccessMode.Cached}
>
${t`Cached querying, the outpost holds all users and groups in-memory and will refresh every 5 Minutes.`}
</option>
<option
value="${LDAPAPIAccessMode.Direct}"
?selected=${this.instance?.searchMode === LDAPAPIAccessMode.Direct}
>
${t`Direct querying, always returns the latest data, but slower than cached querying.`}
</option>
</select>
<p class="pf-c-form__helper-text">
${t`Configure how the outpost queries the core authentik server's users.`}
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${t`Base DN`} ?required=${true} name="baseDn">
<input
type="text"
value="${first(this.instance?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`LDAP DN under which bind requests and search requests can be made.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Certificate`} name="certificate">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.certificate === undefined}>
---------
</option>
${until(
new CryptoApi(DEFAULT_CONFIG)
.cryptoCertificatekeypairsList({
ordering: "name",
hasKey: true,
})
.then((keys) => {
return keys.results.map((key) => {
return html`<option
value=${ifDefined(key.pk)}
?selected=${this.instance?.certificate === key.pk}
>
${key.name}
</option>`;
});
}),
html`<option
value=${ifDefined(this.instance?.certificate || undefined)}
?selected=${this.instance?.certificate !== undefined}
>
${t`Loading...`}
</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`Due to protocol limitations, this certificate is only used when the outpost has a single provider.`}
</p>
<p class="pf-c-form__helper-text">
${t`If multiple providers share an outpost, a self-signed certificate is used.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`UID start number`}
?required=${true}
name="uidStartNumber"
>
<input
type="number"
value="${first(this.instance?.uidStartNumber, 2000)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`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`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`GID start number`}
?required=${true}
name="gidStartNumber"
>
<input
type="number"
value="${first(this.instance?.gidStartNumber, 4000)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`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`}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>`;
}
}

View File

@ -0,0 +1,210 @@
import "@goauthentik/admin/providers/RelatedApplicationButton";
import "@goauthentik/admin/providers/ldap/LDAPProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/events/ObjectChangelog";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { until } from "lit-html/directives/until.js";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
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";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { LDAPProvider, ProvidersApi } from "@goauthentik/api";
@customElement("ak-provider-ldap-view")
export class LDAPProviderViewPage extends AKElement {
@property()
set args(value: { [key: string]: number }) {
this.providerID = value.id;
}
@property({ type: Number })
set providerID(value: number) {
new ProvidersApi(DEFAULT_CONFIG)
.providersLdapRetrieve({
id: value,
})
.then((prov) => (this.provider = prov));
}
@property({ attribute: false })
provider?: LDAPProvider;
static get styles(): CSSResult[] {
return [
PFBase,
PFButton,
PFBanner,
PFForm,
PFFormControl,
PFList,
PFGrid,
PFPage,
PFContent,
PFCard,
PFDescriptionList,
AKGlobal,
];
}
constructor() {
super();
this.addEventListener(EVENT_REFRESH, () => {
if (!this.provider?.pk) return;
this.providerID = this.provider?.pk;
});
}
render(): TemplateResult {
if (!this.provider) {
return html``;
}
return html`${
this.provider?.assignedApplicationName
? html``
: html`<div slot="header" class="pf-c-banner pf-m-warning">
${t`Warning: Provider is not used by an Application.`}
</div>`
}
${
this.provider?.outpostSet.length < 1
? html`<div slot="header" class="pf-c-banner pf-m-warning">
${t`Warning: Provider is not used by any Outpost.`}
</div>`
: html``
}
<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">${t`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"
>${t`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"
>${t`Base DN`}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.provider.baseDn}
</div>
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__footer">
<ak-forms-modal>
<span slot="submit"> ${t`Update`} </span>
<span slot="header"> ${t`Update LDAP Provider`} </span>
<ak-provider-ldap-form slot="form" .instancePk=${this.provider.pk}>
</ak-provider-ldap-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${t`Edit`}
</button>
</ak-forms-modal>
</div>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">
${t`How to connect`}
</div>
<div class="pf-c-card__body">
<p>
${t`Connect to the LDAP Server on port 389:`}
</p>
<ul class="pf-c-list">
<li>${t`Check the IP of the Kubernetes service, or`}</li>
<li>${t`The Host IP of the docker host`}</li>
</ul>
<form class="pf-c-form">
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text">${t`Bind DN`}</span>
</label>
<!-- @ts-ignore -->
<input
class="pf-c-form-control"
readonly
type="text"
value=${until(
me().then((m) => {
return `cn=${
m.user.username
},ou=users,${this.provider?.baseDn?.toLowerCase()}`;
}),
)}
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text">${t`Bind Password`}</span>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="Your authentik password"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text">${t`Search base`}</span>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value=${ifDefined(this.provider?.baseDn?.toLowerCase())}
/>
</div>
</form>
</div>
</div>
</div>
</div>`;
}
}

View File

@ -0,0 +1,417 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first, randomString } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
ClientTypeEnum,
CryptoApi,
FlowsApi,
FlowsInstancesListDesignationEnum,
IssuerModeEnum,
OAuth2Provider,
PropertymappingsApi,
ProvidersApi,
SourcesApi,
SubModeEnum,
} from "@goauthentik/api";
@customElement("ak-provider-oauth2-form")
export class OAuth2ProviderFormPage extends ModelForm<OAuth2Provider, number> {
loadInstance(pk: number): Promise<OAuth2Provider> {
return new ProvidersApi(DEFAULT_CONFIG)
.providersOauth2Retrieve({
id: pk,
})
.then((provider) => {
this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential;
return provider;
});
}
@property({ type: Boolean })
showClientSecret = true;
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated provider.`;
} else {
return t`Successfully created provider.`;
}
}
send = (data: OAuth2Provider): Promise<OAuth2Provider> => {
if (this.instance) {
return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Update({
id: this.instance.pk || 0,
oAuth2ProviderRequest: data,
});
} else {
return new ProvidersApi(DEFAULT_CONFIG).providersOauth2Create({
oAuth2ProviderRequest: data,
});
}
};
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Authorization flow`}
?required=${true}
name="authorizationFlow"
>
<select class="pf-c-form-control">
${until(
new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesList({
ordering: "slug",
designation: FlowsInstancesListDesignationEnum.Authorization,
})
.then((flows) => {
return flows.results.map((flow) => {
return html`<option
value=${ifDefined(flow.pk)}
?selected=${this.instance?.authorizationFlow === flow.pk}
>
${flow.name} (${flow.slug})
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`Flow used when authorizing this provider.`}
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${t`Client type`}
?required=${true}
name="clientType"
>
<select
class="pf-c-form-control"
@change=${(ev: Event) => {
const target = ev.target as HTMLSelectElement;
if (target.selectedOptions[0].value === ClientTypeEnum.Public) {
this.showClientSecret = false;
} else {
this.showClientSecret = true;
}
}}
>
<option
value=${ClientTypeEnum.Confidential}
?selected=${this.instance?.clientType ===
ClientTypeEnum.Confidential}
>
${t`Confidential`}
</option>
<option
value=${ClientTypeEnum.Public}
?selected=${this.instance?.clientType === ClientTypeEnum.Public}
>
${t`Public`}
</option>
</select>
<p class="pf-c-form__helper-text">
${t`Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Client ID`}
?required=${true}
name="clientId"
>
<input
type="text"
value="${first(this.instance?.clientId, randomString(40))}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
?hidden=${!this.showClientSecret}
label=${t`Client Secret`}
name="clientSecret"
>
<input
type="text"
value="${first(this.instance?.clientSecret, randomString(128))}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Redirect URIs/Origins (RegEx)`}
name="redirectUris"
>
<textarea class="pf-c-form-control">
${this.instance?.redirectUris}</textarea
>
<p class="pf-c-form__helper-text">
${t`Valid redirect URLs after a successful authorization flow. Also specify any origins here for Implicit flows.`}
</p>
<p class="pf-c-form__helper-text">
${t`If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.`}
</p>
<p class="pf-c-form__helper-text">
${t`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-form-element-horizontal label=${t`Signing Key`} name="signingKey">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.signingKey === undefined}>
---------
</option>
${until(
new CryptoApi(DEFAULT_CONFIG)
.cryptoCertificatekeypairsList({
ordering: "name",
hasKey: true,
})
.then((keys) => {
return keys.results.map((key) => {
let selected = this.instance?.signingKey === key.pk;
if (!this.instance && keys.results.length === 1) {
selected = true;
}
return html`<option
value=${ifDefined(key.pk)}
?selected=${selected}
>
${key.name} (${key.privateKeyType?.toUpperCase()})
</option>`;
});
}),
html`<option
value=${ifDefined(this.instance?.signingKey || undefined)}
?selected=${this.instance?.signingKey !== undefined}
>
${t`Loading...`}
</option>`,
)}
</select>
<p class="pf-c-form__helper-text">${t`Key used to sign the tokens.`}</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${t`Advanced protocol settings`} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${t`Access token validity`}
?required=${true}
name="accessCodeValidity"
>
<input
type="text"
value="${first(this.instance?.accessCodeValidity, "minutes=1")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Configure how long access tokens are valid for.`}
</p>
<p class="pf-c-form__helper-text">
${t`If you are using an Implicit, client-side flow (where the token-endpoint isn't used), you probably want to increase this time.`}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Token validity`}
?required=${true}
name="tokenValidity"
>
<input
type="text"
value="${first(this.instance?.tokenValidity, "days=30")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Configure how long refresh tokens and their id_tokens are valid for.`}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Scopes`} name="propertyMappings">
<select class="pf-c-form-control" multiple>
${until(
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsScopeList({
ordering: "scope_name",
})
.then((scopes) => {
return scopes.results.map((scope) => {
let selected = false;
if (!this.instance?.propertyMappings) {
selected =
scope.managed?.startsWith(
"goauthentik.io/providers/oauth2/scope-",
) || false;
} else {
selected = Array.from(
this.instance?.propertyMappings,
).some((su) => {
return su == scope.pk;
});
}
return html`<option
value=${ifDefined(scope.pk)}
?selected=${selected}
>
${scope.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`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">
${t`Hold control/command to select multiple items.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Subject mode`}
?required=${true}
name="subMode"
>
<select class="pf-c-form-control">
<option
value="${SubModeEnum.HashedUserId}"
?selected=${this.instance?.subMode === SubModeEnum.HashedUserId}
>
${t`Based on the Hashed User ID`}
</option>
<option
value="${SubModeEnum.UserUsername}"
?selected=${this.instance?.subMode === SubModeEnum.UserUsername}
>
${t`Based on the username`}
</option>
<option
value="${SubModeEnum.UserEmail}"
?selected=${this.instance?.subMode === SubModeEnum.UserEmail}
>
${t`Based on the User's Email. This is recommended over the UPN method.`}
</option>
<option
value="${SubModeEnum.UserUpn}"
?selected=${this.instance?.subMode === SubModeEnum.UserUpn}
>
${t`Based on the User's UPN, only works if user has a 'upn' attribute set. Use this method only if you have different UPN and Mail domains.`}
</option>
</select>
<p class="pf-c-form__helper-text">
${t`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">
<div class="pf-c-check">
<input
type="checkbox"
class="pf-c-check__input"
?checked=${first(this.instance?.includeClaimsInIdToken, true)}
/>
<label class="pf-c-check__label">
${t`Include claims in id_token`}
</label>
</div>
<p class="pf-c-form__helper-text">
${t`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=${t`Issuer mode`}
?required=${true}
name="issuerMode"
>
<select class="pf-c-form-control">
<option
value="${IssuerModeEnum.PerProvider}"
?selected=${this.instance?.issuerMode ===
IssuerModeEnum.PerProvider}
>
${t`Each provider has a different issuer, based on the application slug.`}
</option>
<option
value="${IssuerModeEnum.Global}"
?selected=${this.instance?.issuerMode === IssuerModeEnum.Global}
>
${t`Same identifier is used for all providers`}
</option>
</select>
<p class="pf-c-form__helper-text">
${t`Configure how the issuer field of the ID Token should be filled.`}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${t`Machine-to-Machine authentication settings`}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${t`Trusted OIDC Sources`} name="jwksSources">
<select class="pf-c-form-control" multiple>
${until(
new SourcesApi(DEFAULT_CONFIG)
.sourcesOauthList({
ordering: "name",
})
.then((sources) => {
return sources.results.map((source) => {
const selected = (
this.instance?.jwksSources || []
).some((su) => {
return su == source.pk;
});
return html`<option
value=${source.pk}
?selected=${selected}
>
${source.name} (${source.slug})
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`Deprecated. Instead of using this field, configure the JWKS data/URL in Sources.`}
</p>
<p class="pf-c-form__helper-text">
${t`JWTs signed by certificates configured here can be used to authenticate to the provider.`}
</p>
<p class="pf-c-form__helper-text">
${t`Hold control/command to select multiple items.`}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>`;
}
}

View File

@ -0,0 +1,281 @@
import "@goauthentik/admin/providers/RelatedApplicationButton";
import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { convertToTitle } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/events/ObjectChangelog";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
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 PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { OAuth2Provider, OAuth2ProviderSetupURLs, ProvidersApi } from "@goauthentik/api";
@customElement("ak-provider-oauth2-view")
export class OAuth2ProviderViewPage extends AKElement {
@property({ type: Number })
set providerID(value: number) {
const api = new ProvidersApi(DEFAULT_CONFIG);
api.providersOauth2Retrieve({
id: value,
}).then((prov) => {
this.provider = prov;
});
api.providersOauth2SetupUrlsRetrieve({
id: value,
}).then((prov) => {
this.providerUrls = prov;
});
}
@property({ attribute: false })
provider?: OAuth2Provider;
@property({ attribute: false })
providerUrls?: OAuth2ProviderSetupURLs;
static get styles(): CSSResult[] {
return [
PFBase,
PFButton,
PFPage,
PFGrid,
PFContent,
PFCard,
PFDescriptionList,
PFForm,
PFFormControl,
PFBanner,
AKGlobal,
];
}
constructor() {
super();
this.addEventListener(EVENT_REFRESH, () => {
if (!this.provider?.pk) return;
this.providerID = this.provider?.pk;
});
}
render(): TemplateResult {
if (!this.provider) {
return html``;
}
return html` ${
this.provider?.assignedApplicationName
? html``
: html`<div slot="header" class="pf-c-banner pf-m-warning">
${t`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">
<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"
>${t`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"
>${t`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"
>${t`Client type`}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${convertToTitle(
this.provider.clientType || "",
)}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${t`Client ID`}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.provider.clientId}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${t`Redirect URIs`}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.provider.redirectUris}
</div>
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__footer">
<ak-forms-modal>
<span slot="submit"> ${t`Update`} </span>
<span slot="header"> ${t`Update OAuth2 Provider`} </span>
<ak-provider-oauth2-form
slot="form"
.instancePk=${this.provider.pk || 0}
>
</ak-provider-oauth2-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${t`Edit`}
</button>
</ak-forms-modal>
</div>
</div>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card">
<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"
>${t`OpenID Configuration URL`}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${this.providerUrls?.providerInfo || t`-`}"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${t`OpenID Configuration Issuer`}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${this.providerUrls?.issuer || t`-`}"
/>
</div>
<hr />
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${t`Authorize URL`}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${this.providerUrls?.authorize || t`-`}"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${t`Token URL`}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${this.providerUrls?.token || t`-`}"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${t`Userinfo URL`}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${this.providerUrls?.userInfo || t`-`}"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${t`Logout URL`}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${this.providerUrls?.logout || t`-`}"
/>
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"
>${t`JWKS URL`}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${this.providerUrls?.jwks || t`-`}"
/>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
</ak-tabs>`;
}
}

View File

@ -0,0 +1,449 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { t } from "@lingui/macro";
import { CSSResult, css } from "lit";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";
import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
import {
CryptoApi,
FlowsApi,
FlowsInstancesListDesignationEnum,
PropertymappingsApi,
ProvidersApi,
ProxyMode,
ProxyProvider,
} from "@goauthentik/api";
@customElement("ak-provider-proxy-form")
export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
static get styles(): CSSResult[] {
return super.styles.concat(
PFToggleGroup,
PFContent,
PFList,
PFSpacing,
css`
.pf-c-toggle-group {
justify-content: center;
}
`,
);
}
loadInstance(pk: number): Promise<ProxyProvider> {
return new ProvidersApi(DEFAULT_CONFIG)
.providersProxyRetrieve({
id: pk,
})
.then((provider) => {
this.showHttpBasic = first(provider.basicAuthEnabled, true);
this.mode = first(provider.mode, ProxyMode.Proxy);
return provider;
});
}
@property({ type: Boolean })
showHttpBasic = true;
@property({ attribute: false })
mode: ProxyMode = ProxyMode.Proxy;
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated provider.`;
} else {
return t`Successfully created provider.`;
}
}
send = (data: ProxyProvider): Promise<ProxyProvider> => {
data.mode = this.mode;
if (this.instance) {
return new ProvidersApi(DEFAULT_CONFIG).providersProxyUpdate({
id: this.instance.pk || 0,
proxyProviderRequest: data,
});
} else {
return new ProvidersApi(DEFAULT_CONFIG).providersProxyCreate({
proxyProviderRequest: data,
});
}
};
renderHttpBasic(): TemplateResult {
if (!this.showHttpBasic) {
return html``;
}
return html`<ak-form-element-horizontal
label=${t`HTTP-Basic Username Key`}
name="basicAuthUserAttribute"
>
<input
type="text"
value="${ifDefined(this.instance?.basicAuthUserAttribute)}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`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=${t`HTTP-Basic Password Key`}
name="basicAuthPasswordAttribute"
>
<input
type="text"
value="${ifDefined(this.instance?.basicAuthPasswordAttribute)}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`User/Group Attribute used for the password part of the HTTP-Basic Header.`}
</p>
</ak-form-element-horizontal>`;
}
renderModeSelector(): TemplateResult {
return html` <div class="pf-c-toggle-group__item">
<button
class="pf-c-toggle-group__button ${this.mode === ProxyMode.Proxy
? "pf-m-selected"
: ""}"
type="button"
@click=${() => {
this.mode = ProxyMode.Proxy;
}}
>
<span class="pf-c-toggle-group__text">${t`Proxy`}</span>
</button>
</div>
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
<div class="pf-c-toggle-group__item">
<button
class="pf-c-toggle-group__button ${this.mode === ProxyMode.ForwardSingle
? "pf-m-selected"
: ""}"
type="button"
@click=${() => {
this.mode = ProxyMode.ForwardSingle;
}}
>
<span class="pf-c-toggle-group__text"
>${t`Forward auth (single application)`}</span
>
</button>
</div>
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
<div class="pf-c-toggle-group__item">
<button
class="pf-c-toggle-group__button ${this.mode === ProxyMode.ForwardDomain
? "pf-m-selected"
: ""}"
type="button"
@click=${() => {
this.mode = ProxyMode.ForwardDomain;
}}
>
<span class="pf-c-toggle-group__text">${t`Forward auth (domain level)`}</span>
</button>
</div>`;
}
renderSettings(): TemplateResult {
switch (this.mode) {
case ProxyMode.Proxy:
return html`<p class="pf-u-mb-xl">
${t`This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.`}
</p>
<ak-form-element-horizontal
label=${t`External host`}
?required=${true}
name="externalHost"
>
<input
type="text"
value="${ifDefined(this.instance?.externalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`The external URL you'll access the application at. Include any non-standard port.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Internal host`}
?required=${true}
name="internalHost"
>
<input
type="text"
value="${ifDefined(this.instance?.internalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Upstream host that the requests are forwarded to.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="internalHostSslValidation">
<div class="pf-c-check">
<input
type="checkbox"
class="pf-c-check__input"
?checked=${first(this.instance?.internalHostSslValidation, true)}
/>
<label class="pf-c-check__label">
${t`Internal host SSL Validation`}
</label>
</div>
<p class="pf-c-form__helper-text">
${t`Validate SSL Certificates of upstream servers.`}
</p>
</ak-form-element-horizontal>`;
case ProxyMode.ForwardSingle:
return html`<p class="pf-u-mb-xl">
${t`Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you).`}
</p>
<ak-form-element-horizontal
label=${t`External host`}
?required=${true}
name="externalHost"
>
<input
type="text"
value="${ifDefined(this.instance?.externalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`The external URL you'll access the application at. Include any non-standard port.`}
</p>
</ak-form-element-horizontal>`;
case ProxyMode.ForwardDomain:
return html`<p class="pf-u-mb-xl">
${t`Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.`}
</p>
<div class="pf-u-mb-xl">
${t`An example setup can look like this:`}
<ul class="pf-c-list">
<li>${t`authentik running on auth.example.com`}</li>
<li>${t`app1 running on app1.example.com`}</li>
</ul>
${t`In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.`}
</div>
<ak-form-element-horizontal
label=${t`Authentication URL`}
?required=${true}
name="externalHost"
>
<input
type="text"
value="${first(this.instance?.externalHost, window.location.origin)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`The external URL you'll authenticate at. The authentik core server should be reachable under this URL.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Cookie domain`}
name="cookieDomain"
?required=${true}
>
<input
type="text"
value="${ifDefined(this.instance?.cookieDomain)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.`}
</p>
</ak-form-element-horizontal>`;
}
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Authorization flow`}
?required=${true}
name="authorizationFlow"
>
<select class="pf-c-form-control">
${until(
new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesList({
ordering: "slug",
designation: FlowsInstancesListDesignationEnum.Authorization,
})
.then((flows) => {
return flows.results.map((flow) => {
return html`<option
value=${ifDefined(flow.pk)}
?selected=${this.instance?.authorizationFlow === flow.pk}
>
${flow.name} (${flow.slug})
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`Flow used when authorizing this provider.`}
</p>
</ak-form-element-horizontal>
<div class="pf-c-card pf-m-selectable pf-m-selected">
<div class="pf-c-card__body">
<div class="pf-c-toggle-group">${this.renderModeSelector()}</div>
</div>
<div class="pf-c-card__footer">${this.renderSettings()}</div>
</div>
<ak-form-element-horizontal label=${t`Token validity`} name="tokenValidity">
<input
type="text"
value="${first(this.instance?.tokenValidity, "hours=24")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">${t`Configure how long tokens are valid for.`}</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-group>
<span slot="header">${t`Advanced protocol settings`}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${t`Certificate`} name="certificate">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.certificate === undefined}>
---------
</option>
${until(
new CryptoApi(DEFAULT_CONFIG)
.cryptoCertificatekeypairsList({
ordering: "name",
hasKey: true,
})
.then((keys) => {
return keys.results.map((key) => {
return html`<option
value=${ifDefined(key.pk)}
?selected=${this.instance?.certificate === key.pk}
>
${key.name}
</option>`;
});
}),
html`<option
value=${ifDefined(this.instance?.certificate || undefined)}
?selected=${this.instance?.certificate !== undefined}
>
${t`Loading...`}
</option>`,
)}
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Scopes`} name="propertyMappings">
<select class="pf-c-form-control" multiple>
${until(
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsScopeList({
ordering: "scope_name",
})
.then((scopes) => {
return scopes.results
.filter((scope) => {
return !scope.managed?.startsWith(
"goauthentik.io/providers",
);
})
.map((scope) => {
const selected = (
this.instance?.propertyMappings || []
).some((su) => {
return su == scope.pk;
});
return html`<option
value=${ifDefined(scope.pk)}
?selected=${selected}
>
${scope.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`Additional scope mappings, which are passed to the proxy.`}
</p>
<p class="pf-c-form__helper-text">
${t`Hold control/command to select multiple items.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label="${this.mode === ProxyMode.ForwardDomain
? t`Unauthenticated URLs`
: t`Unauthenticated Paths`}"
name="skipPathRegex"
>
<textarea class="pf-c-form-control">
${this.instance?.skipPathRegex}</textarea
>
<p class="pf-c-form__helper-text">
${t`Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.`}
</p>
<p class="pf-c-form__helper-text">
${t`When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="basicAuthEnabled">
<div class="pf-c-check">
<input
type="checkbox"
class="pf-c-check__input"
?checked=${first(this.instance?.basicAuthEnabled, false)}
@change=${(ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showHttpBasic = el.checked;
}}
/>
<label class="pf-c-check__label">
${t`Set HTTP-Basic Authentication`}
</label>
</div>
<p class="pf-c-form__helper-text">
${t`Set a custom HTTP-Basic Authentication header based on values from authentik.`}
</p>
</ak-form-element-horizontal>
${this.renderHttpBasic()}
</div>
</ak-form-group>
</form>`;
}
}

View File

@ -0,0 +1,313 @@
import "@goauthentik/admin/providers/RelatedApplicationButton";
import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { convertToSlug } from "@goauthentik/common/utils";
import MDCaddyStandalone from "@goauthentik/docs/providers/proxy/_caddy_standalone.md";
import MDNginxIngress from "@goauthentik/docs/providers/proxy/_nginx_ingress.md";
import MDNginxPM from "@goauthentik/docs/providers/proxy/_nginx_proxy_manager.md";
import MDNginxStandalone from "@goauthentik/docs/providers/proxy/_nginx_standalone.md";
import MDTraefikCompose from "@goauthentik/docs/providers/proxy/_traefik_compose.md";
import MDTraefikIngress from "@goauthentik/docs/providers/proxy/_traefik_ingress.md";
import MDTraefikStandalone from "@goauthentik/docs/providers/proxy/_traefik_standalone.md";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import { PFColor } from "@goauthentik/elements/Label";
import { MarkdownDocument } from "@goauthentik/elements/Markdown";
import "@goauthentik/elements/Markdown";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/events/ObjectChangelog";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
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 PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { ProvidersApi, ProxyMode, ProxyProvider } from "@goauthentik/api";
export function ModeToLabel(action?: ProxyMode): string {
if (!action) return "";
switch (action) {
case ProxyMode.Proxy:
return t`Proxy`;
case ProxyMode.ForwardSingle:
return t`Forward auth (single application)`;
case ProxyMode.ForwardDomain:
return t`Forward auth (domain-level)`;
}
}
export function isForward(mode: ProxyMode): boolean {
switch (mode) {
case ProxyMode.Proxy:
return false;
case ProxyMode.ForwardSingle:
case ProxyMode.ForwardDomain:
return true;
}
}
@customElement("ak-provider-proxy-view")
export class ProxyProviderViewPage extends AKElement {
@property()
set args(value: { [key: string]: number }) {
this.providerID = value.id;
}
@property({ type: Number })
set providerID(value: number) {
new ProvidersApi(DEFAULT_CONFIG)
.providersProxyRetrieve({
id: value,
})
.then((prov) => (this.provider = prov));
}
@property({ attribute: false })
provider?: ProxyProvider;
static get styles(): CSSResult[] {
return [
PFBase,
PFButton,
PFPage,
PFGrid,
PFContent,
PFForm,
PFFormControl,
PFCard,
PFDescriptionList,
PFBanner,
AKGlobal,
];
}
constructor() {
super();
this.addEventListener(EVENT_REFRESH, () => {
if (!this.provider?.pk) return;
this.providerID = this.provider?.pk;
});
}
renderConfigTemplate(markdown: MarkdownDocument): MarkdownDocument {
const extHost = new URL(this.provider?.externalHost || "http://a");
// See website/docs/providers/proxy/forward_auth.mdx
if (this.provider?.mode === ProxyMode.ForwardSingle) {
markdown.html = markdown.html
.replaceAll("authentik.company", window.location.hostname)
.replaceAll("outpost.company:9000", window.location.hostname)
.replaceAll("https://app.company", extHost.toString())
.replaceAll("app.company", extHost.hostname);
} else if (this.provider?.mode == ProxyMode.ForwardDomain) {
markdown.html = markdown.html
.replaceAll("authentik.company", window.location.hostname)
.replaceAll("outpost.company:9000", extHost.toString())
.replaceAll("https://app.company", extHost.toString())
.replaceAll("app.company", extHost.hostname);
}
return markdown;
}
renderConfig(): TemplateResult {
const serves = [
{
label: t`Nginx (Ingress)`,
md: MDNginxIngress,
},
{
label: t`Nginx (Proxy Manager)`,
md: MDNginxPM,
},
{
label: t`Nginx (standalone)`,
md: MDNginxStandalone,
},
{
label: t`Traefik (Ingress)`,
md: MDTraefikIngress,
},
{
label: t`Traefik (Compose)`,
md: MDTraefikCompose,
},
{
label: t`Traefik (Standalone)`,
md: MDTraefikStandalone,
},
{
label: t`Caddy (Standalone)`,
md: MDCaddyStandalone,
},
];
return html`<ak-tabs pageIdentifier="proxy-setup">
${serves.map((server) => {
return html`<section
slot="page-${convertToSlug(server.label)}"
data-tab-title="${server.label}"
class="pf-c-page__main-section pf-m-light pf-m-no-padding-mobile"
>
<ak-markdown .md=${this.renderConfigTemplate(server.md)}></ak-markdown>
</section>`;
})}</ak-tabs
>`;
}
render(): TemplateResult {
if (!this.provider) {
return html``;
}
return html`${this.provider?.assignedApplicationName
? html``
: html`<div slot="header" class="pf-c-banner pf-m-warning">
${t`Warning: Provider is not used by an Application.`}
</div>`}
${this.provider?.outpostSet.length < 1
? html`<div slot="header" class="pf-c-banner pf-m-warning">
${t`Warning: Provider is not used by any Outpost.`}
</div>`
: html``}
<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">${t`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"
>${t`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"
>${t`Internal Host`}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.provider.internalHost}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${t`External Host`}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<a target="_blank" href="${this.provider.externalHost}"
>${this.provider.externalHost}</a
>
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${t`Basic-Auth`}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ak-label
color=${this.provider.basicAuthEnabled
? PFColor.Green
: PFColor.Grey}
>
${this.provider.basicAuthEnabled ? t`Yes` : t`No`}
</ak-label>
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${t`Mode`}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${ModeToLabel(this.provider.mode || ProxyMode.Proxy)}
</div>
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__footer">
<ak-forms-modal>
<span slot="submit"> ${t`Update`} </span>
<span slot="header"> ${t`Update Proxy Provider`} </span>
<ak-provider-proxy-form
slot="form"
.instancePk=${this.provider.pk || 0}
>
</ak-provider-proxy-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${t`Edit`}
</button>
</ak-forms-modal>
</div>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">${t`Protocol Settings`}</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"
>${t`Allowed Redirect URIs`}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value=${this.provider.redirectUris}
/>
</div>
</form>
</div>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">${t`Setup`}</div>
<div class="pf-c-card__body">
${isForward(this.provider?.mode || ProxyMode.Proxy)
? html` ${this.renderConfig()} `
: html` <p>${t`No additional setup is required.`}</p> `}
</div>
</div>
</div>`;
}
}

View File

@ -0,0 +1,434 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import {
CryptoApi,
DigestAlgorithmEnum,
FlowsApi,
FlowsInstancesListDesignationEnum,
PropertymappingsApi,
ProvidersApi,
SAMLProvider,
SignatureAlgorithmEnum,
SpBindingEnum,
} from "@goauthentik/api";
@customElement("ak-provider-saml-form")
export class SAMLProviderFormPage extends ModelForm<SAMLProvider, number> {
loadInstance(pk: number): Promise<SAMLProvider> {
return new ProvidersApi(DEFAULT_CONFIG).providersSamlRetrieve({
id: pk,
});
}
getSuccessMessage(): string {
if (this.instance) {
return t`Successfully updated provider.`;
} else {
return t`Successfully created provider.`;
}
}
send = (data: SAMLProvider): Promise<SAMLProvider> => {
if (this.instance) {
return new ProvidersApi(DEFAULT_CONFIG).providersSamlUpdate({
id: this.instance.pk || 0,
sAMLProviderRequest: data,
});
} else {
return new ProvidersApi(DEFAULT_CONFIG).providersSamlCreate({
sAMLProviderRequest: data,
});
}
};
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Authorization flow`}
?required=${true}
name="authorizationFlow"
>
<select class="pf-c-form-control">
${until(
new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesList({
ordering: "slug",
designation: FlowsInstancesListDesignationEnum.Authorization,
})
.then((flows) => {
return flows.results.map((flow) => {
return html`<option
value=${ifDefined(flow.pk)}
?selected=${this.instance?.authorizationFlow === flow.pk}
>
${flow.name} (${flow.slug})
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`Flow used when authorizing this provider.`}
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${t`ACS URL`} ?required=${true} name="acsUrl">
<input
type="text"
value="${ifDefined(this.instance?.acsUrl)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Issuer`} ?required=${true} name="issuer">
<input
type="text"
value="${this.instance?.issuer || "authentik"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">${t`Also known as EntityID.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Service Provider Binding`}
?required=${true}
name="spBinding"
>
<select class="pf-c-form-control">
<option
value=${SpBindingEnum.Redirect}
?selected=${this.instance?.spBinding === SpBindingEnum.Redirect}
>
${t`Redirect`}
</option>
<option
value=${SpBindingEnum.Post}
?selected=${this.instance?.spBinding === SpBindingEnum.Post}
>
${t`Post`}
</option>
</select>
<p class="pf-c-form__helper-text">
${t`Determines how authentik sends the response back to the Service Provider.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Audience`} name="audience">
<input
type="text"
value="${ifDefined(this.instance?.audience)}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${t`Advanced protocol settings`} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${t`Signing Certificate`} name="signingKp">
<select class="pf-c-form-control">
<option value="" ?selected=${this.instance?.signingKp === undefined}>
---------
</option>
${until(
new CryptoApi(DEFAULT_CONFIG)
.cryptoCertificatekeypairsList({
ordering: "name",
hasKey: true,
})
.then((keys) => {
return keys.results.map((key) => {
return html`<option
value=${ifDefined(key.pk)}
?selected=${this.instance?.signingKp === key.pk}
>
${key.name}
</option>`;
});
}),
html`<option
value=${ifDefined(this.instance?.signingKp || undefined)}
?selected=${this.instance?.signingKp !== undefined}
>
${t`Loading...`}
</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`Certificate used to sign outgoing Responses going to the Service Provider.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Verification Certificate`}
name="verificationKp"
>
<select class="pf-c-form-control">
<option
value=""
?selected=${this.instance?.verificationKp === undefined}
>
---------
</option>
${until(
new CryptoApi(DEFAULT_CONFIG)
.cryptoCertificatekeypairsList({
ordering: "name",
})
.then((keys) => {
return keys.results.map((key) => {
return html`<option
value=${ifDefined(key.pk)}
?selected=${this.instance?.verificationKp ===
key.pk}
>
${key.name}
</option>`;
});
}),
html`<option
value=${ifDefined(this.instance?.verificationKp || undefined)}
?selected=${this.instance?.verificationKp !== undefined}
>
${t`Loading...`}
</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`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=${t`Property mappings`}
?required=${true}
name="propertyMappings"
>
<select class="pf-c-form-control" multiple>
${until(
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsSamlList({
ordering: "saml_name",
})
.then((mappings) => {
return mappings.results.map((mapping) => {
let selected = false;
if (!this.instance?.propertyMappings) {
selected =
mapping.managed?.startsWith(
"goauthentik.io/providers/saml",
) || false;
} else {
selected = Array.from(
this.instance?.propertyMappings,
).some((su) => {
return su == mapping.pk;
});
}
return html`<option
value=${ifDefined(mapping.pk)}
?selected=${selected}
>
${mapping.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`Hold control/command to select multiple items.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`NameID Property Mapping`}
name="nameIdMapping"
>
<select class="pf-c-form-control">
<option
value=""
?selected=${this.instance?.nameIdMapping === undefined}
>
---------
</option>
${until(
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsSamlList({
ordering: "saml_name",
})
.then((mappings) => {
return mappings.results.map((mapping) => {
return html`<option
value=${ifDefined(mapping.pk)}
?selected=${this.instance?.nameIdMapping ===
mapping.pk}
>
${mapping.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`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-form-element-horizontal
label=${t`Assertion valid not before`}
?required=${true}
name="assertionValidNotBefore"
>
<input
type="text"
value="${this.instance?.assertionValidNotBefore || "minutes=-5"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Configure the maximum allowed time drift for an assertion.`}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Assertion valid not on or after`}
?required=${true}
name="assertionValidNotOnOrAfter"
>
<input
type="text"
value="${this.instance?.assertionValidNotOnOrAfter || "minutes=5"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Assertion not valid on or after current time + this value.`}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Session valid not on or after`}
?required=${true}
name="sessionValidNotOnOrAfter"
>
<input
type="text"
value="${this.instance?.sessionValidNotOnOrAfter || "minutes=86400"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Session not valid on or after current time + this value.`}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Digest algorithm`}
?required=${true}
name="digestAlgorithm"
>
<select class="pf-c-form-control">
<option
value=${DigestAlgorithmEnum._200009Xmldsigsha1}
?selected=${this.instance?.digestAlgorithm ===
DigestAlgorithmEnum._200009Xmldsigsha1}
>
${t`SHA1`}
</option>
<option
value=${DigestAlgorithmEnum._200104Xmlencsha256}
?selected=${this.instance?.digestAlgorithm ===
DigestAlgorithmEnum._200104Xmlencsha256 ||
this.instance?.digestAlgorithm === undefined}
>
${t`SHA256`}
</option>
<option
value=${DigestAlgorithmEnum._200104XmldsigMoresha384}
?selected=${this.instance?.digestAlgorithm ===
DigestAlgorithmEnum._200104XmldsigMoresha384}
>
${t`SHA384`}
</option>
<option
value=${DigestAlgorithmEnum._200104Xmlencsha512}
?selected=${this.instance?.digestAlgorithm ===
DigestAlgorithmEnum._200104Xmlencsha512}
>
${t`SHA512`}
</option>
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Signature algorithm`}
?required=${true}
name="signatureAlgorithm"
>
<select class="pf-c-form-control">
<option
value=${SignatureAlgorithmEnum._200009XmldsigrsaSha1}
?selected=${this.instance?.signatureAlgorithm ===
SignatureAlgorithmEnum._200009XmldsigrsaSha1}
>
${t`RSA-SHA1`}
</option>
<option
value=${SignatureAlgorithmEnum._200104XmldsigMorersaSha256}
?selected=${this.instance?.signatureAlgorithm ===
SignatureAlgorithmEnum._200104XmldsigMorersaSha256 ||
this.instance?.signatureAlgorithm === undefined}
>
${t`RSA-SHA256`}
</option>
<option
value=${SignatureAlgorithmEnum._200104XmldsigMorersaSha384}
?selected=${this.instance?.signatureAlgorithm ===
SignatureAlgorithmEnum._200104XmldsigMorersaSha384}
>
${t`RSA-SHA384`}
</option>
<option
value=${SignatureAlgorithmEnum._200104XmldsigMorersaSha512}
?selected=${this.instance?.signatureAlgorithm ===
SignatureAlgorithmEnum._200104XmldsigMorersaSha512}
>
${t`RSA-SHA512`}
</option>
<option
value=${SignatureAlgorithmEnum._200009XmldsigdsaSha1}
?selected=${this.instance?.signatureAlgorithm ===
SignatureAlgorithmEnum._200009XmldsigdsaSha1}
>
${t`DSA-SHA1`}
</option>
</select>
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>`;
}
}

View File

@ -0,0 +1,75 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { SentryIgnoredError } from "@goauthentik/common/errors";
import { Form } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
import {
FlowsApi,
FlowsInstancesListDesignationEnum,
ProvidersApi,
SAMLProvider,
} from "@goauthentik/api";
@customElement("ak-provider-saml-import-form")
export class SAMLProviderImportForm extends Form<SAMLProvider> {
getSuccessMessage(): string {
return t`Successfully imported provider.`;
}
// eslint-disable-next-line
send = (data: SAMLProvider): Promise<void> => {
const file = this.getFormFiles()["metadata"];
if (!file) {
throw new SentryIgnoredError("No form data");
}
return new ProvidersApi(DEFAULT_CONFIG).providersSamlImportMetadataCreate({
file: file,
name: data.name,
authorizationFlow: data.authorizationFlow,
});
};
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
<input type="text" class="pf-c-form-control" required />
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Authorization flow`}
?required=${true}
name="authorizationFlow"
>
<select class="pf-c-form-control">
${until(
new FlowsApi(DEFAULT_CONFIG)
.flowsInstancesList({
ordering: "slug",
designation: FlowsInstancesListDesignationEnum.Authorization,
})
.then((flows) => {
return flows.results.map((flow) => {
return html`<option value=${flow.slug}>
${flow.name} (${flow.slug})
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<p class="pf-c-form__helper-text">
${t`Flow used when authorizing this provider.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Metadata`} name="metadata">
<input type="file" value="" class="pf-c-form-control" />
</ak-form-element-horizontal>
</form>`;
}
}

View File

@ -0,0 +1,296 @@
import "@goauthentik/admin/providers/RelatedApplicationButton";
import "@goauthentik/admin/providers/saml/SAMLProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/events/ObjectChangelog";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
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 PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { CryptoApi, ProvidersApi, SAMLProvider } from "@goauthentik/api";
@customElement("ak-provider-saml-view")
export class SAMLProviderViewPage extends AKElement {
@property()
set args(value: { [key: string]: number }) {
this.providerID = value.id;
}
@property({ type: Number })
set providerID(value: number) {
new ProvidersApi(DEFAULT_CONFIG)
.providersSamlRetrieve({
id: value,
})
.then((prov) => (this.provider = prov));
}
@property({ attribute: false })
provider?: SAMLProvider;
static get styles(): CSSResult[] {
return [
PFBase,
PFButton,
PFPage,
PFGrid,
PFContent,
PFCard,
PFDescriptionList,
PFForm,
PFFormControl,
PFBanner,
AKGlobal,
];
}
constructor() {
super();
this.addEventListener(EVENT_REFRESH, () => {
if (!this.provider?.pk) return;
this.providerID = this.provider?.pk;
});
}
async renderRelatedObjects(): Promise<TemplateResult> {
if (!this.provider?.signingKp) {
return Promise.resolve(html``);
}
const kp = await new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsRetrieve({
kpUuid: this.provider.signingKp,
});
return html` <div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">${t`Related objects`}</div>
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${t`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=${kp.certificateDownloadUrl}
>${t`Download`}</a
>
</div>
</dd>
</div>
</dl>
</div>
</div>`;
}
render(): TemplateResult {
if (!this.provider) {
return html``;
}
return html`${
this.provider?.assignedApplicationName
? html``
: html`<div slot="header" class="pf-c-banner pf-m-warning">
${t`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">${t`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"
>${t`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">${t`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">${t`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">${t`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"> ${t`Update`} </span>
<span slot="header"> ${t`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">
${t`Edit`}
</button>
</ak-forms-modal>
</div>
</div>
${until(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">${t`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"
>${t`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"
>${t`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"
>${t`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"
>${t`SSO URL (IdP-initiated Login)`}</span
>
</label>
<input
class="pf-c-form-control"
readonly
type="text"
value="${ifDefined(this.provider.urlSsoInit)}"
/>
</div>
</form>
</div>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">${t`SAML Metadata`}</div>
<div class="pf-c-card__body">
${until(
new ProvidersApi(DEFAULT_CONFIG)
.providersSamlMetadataRetrieve({
id: this.provider.pk || 0,
})
.then((m) => {
return html`<ak-codemirror
mode="xml"
?readOnly=${true}
value="${ifDefined(m.metadata)}"
></ak-codemirror>`;
}),
)}
</div>
<div class="pf-c-card__footer">
<a
class="pf-c-button pf-m-primary"
target="_blank"
href=${this.provider.urlDownloadMetadata}
>
${t`Download`}
</a>
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
return navigator.clipboard.writeText(
this.provider?.urlDownloadMetadata || "",
);
}}
>
${t`Copy download URL`}
</ak-action-button>
</div>
</div>`
: html``
}
</div>
</div>`;
}
}