Almost there!

This commit is contained in:
Ken Sternberg
2024-10-25 10:27:06 -07:00
parent 99af95b10c
commit c0814ad279
12 changed files with 401 additions and 501 deletions

View File

@ -36,7 +36,7 @@ export type LocalTypeCreate = TypeCreate & {
export const providerModelsList: LocalTypeCreate[] = [
{
formName: "oauth2provider",
name: msg("OAuth2/OIDC (Open Authorization/OpenID Connect)"),
name: msg("OAuth2/OpenID Provider"),
description: msg("Modern applications, APIs and Single-page applications."),
renderer: () =>
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
@ -50,7 +50,7 @@ export const providerModelsList: LocalTypeCreate[] = [
},
{
formName: "ldapprovider",
name: msg("LDAP (Lightweight Directory Access Protocol)"),
name: msg("LDAP Provider"),
description: msg(
"Provide an LDAP interface for applications and users to authenticate against.",
),
@ -127,7 +127,7 @@ export const providerModelsList: LocalTypeCreate[] = [
},
{
formName: "samlprovider",
name: msg("SAML (Security Assertion Markup Language)"),
name: msg("SAML Provider"),
description: msg("Configure SAML provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
@ -141,7 +141,7 @@ export const providerModelsList: LocalTypeCreate[] = [
},
{
formName: "radiusprovider",
name: msg("RADIUS (Remote Authentication Dial-In User Service)"),
name: msg("Radius Provider"),
description: msg("Configure RADIUS provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
@ -155,7 +155,7 @@ export const providerModelsList: LocalTypeCreate[] = [
},
{
formName: "scimprovider",
name: msg("SCIM (System for Cross-domain Identity Management)"),
name: msg("SCIM Provider"),
description: msg("Configure SCIM provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,

View File

@ -35,6 +35,7 @@ export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSumm
? html`<form class="pf-c-form pf-m-horizontal">
<ak-wizard-page-type-create
.types=${typesForWizard}
name="selectProviderType"
layout=${TypeCreateWizardPageLayouts.grid}
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
@select=${(ev: CustomEvent<LocalTypeCreate>) => {

View File

@ -1,7 +1,9 @@
import { renderForm } from "@goauthentik/admin/providers/oauth2/OAuth2ProviderFormForm.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { SourcesApi } from "@goauthentik/api";
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
@ -34,7 +36,15 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
const showClientSecretCallback = (show: boolean) => {
this.showClientSecret = show;
};
return renderForm(provider ?? {}, errors, this.showClientSecret, showClientSecretCallback);
return html` <ak-wizard-title>${msg("Configure LDAP Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
${renderForm(
provider ?? {},
errors,
this.showClientSecret,
showClientSecretCallback,
)}
</form>`;
}
}

View File

@ -1,4 +1,5 @@
import AkCryptoCertificateSearch from "@goauthentik/admin/common/ak-crypto-certificate-search";
import { renderForm } from "@goauthentik/admin/providers/saml/SAMLProviderFormForm.js";
import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js";

View File

@ -1,21 +1,8 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/common/ak-core-group-search";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-multi-select";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { PaginatedSCIMMappingList, PropertymappingsApi, type SCIMProvider } from "@goauthentik/api";
import { PaginatedSCIMMappingList, type SCIMProvider } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
@ -26,125 +13,15 @@ export class ApplicationWizardAuthenticationBySCIM extends BaseProviderPanel {
constructor() {
super();
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsProviderScimList({
ordering: "managed",
})
.then((propertyMappings: PaginatedSCIMMappingList) => {
this.propertyMappings = propertyMappings;
});
}
propertyMappingConfiguration(provider?: SCIMProvider) {
const propertyMappings = this.propertyMappings?.results ?? [];
const configuredMappings = (providerMappings: string[]) =>
propertyMappings.map((pm) => pm.pk).filter((pmpk) => providerMappings.includes(pmpk));
const managedMappings = (key: string) =>
propertyMappings
.filter((pm) => pm.managed === `goauthentik.io/providers/scim/${key}`)
.map((pm) => pm.pk);
const pmUserValues = provider?.propertyMappings
? configuredMappings(provider?.propertyMappings ?? [])
: managedMappings("user");
const pmGroupValues = provider?.propertyMappingsGroup
? configuredMappings(provider?.propertyMappingsGroup ?? [])
: managedMappings("group");
const propertyPairs = propertyMappings.map((pm) => [pm.pk, pm.name]);
return { pmUserValues, pmGroupValues, propertyPairs };
}
render() {
const provider = this.wizard.provider as SCIMProvider | undefined;
const errors = this.wizard.errors.provider;
const { pmUserValues, pmGroupValues, propertyPairs } =
this.propertyMappingConfiguration(provider);
return html`<ak-wizard-title>${msg("Configure SCIM Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
<ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name ?? []}
required
></ak-text-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="url"
label=${msg("URL")}
value="${first(provider?.url, "")}"
required
help=${msg("SCIM base url, usually ends in /v2.")}
.errorMessages=${errors?.url ?? []}
>
</ak-text-input>
<ak-text-input
name="token"
label=${msg("Token")}
value="${first(provider?.token, "")}"
.errorMessages=${errors?.token ?? []}
required
help=${msg(
"Token to authenticate with. Currently only bearer authentication is supported.",
)}
>
</ak-text-input>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-switch-input
name="excludeUsersServiceAccount"
?checked=${first(provider?.excludeUsersServiceAccount, true)}
label=${msg("Exclude service accounts")}
></ak-switch-input>
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
<ak-core-group-search
.group=${provider?.filterGroup}
></ak-core-group-search>
<p class="pf-c-form__helper-text">
${msg("Only sync users within the selected group.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group ?expanded=${true}>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-multi-select
label=${msg("User Property Mappings")}
name="propertyMappings"
.options=${propertyPairs}
.values=${pmUserValues}
.richhelp=${html`
<p class="pf-c-form__helper-text">
${msg("Property mappings used for user mapping.")}
</p>
`}
></ak-multi-select>
<ak-multi-select
label=${msg("Group Property Mappings")}
name="propertyMappingsGroup"
.options=${propertyPairs}
.values=${pmGroupValues}
.richhelp=${html`
<p class="pf-c-form__helper-text">
${msg("Property mappings used for group creation.")}
</p>
`}
></ak-multi-select>
</div>
</ak-form-group>
${renderForm(
(this.wizard.provider as SCIMProvider) ?? {},
this.wizard.errors.provider,
)}
</form>`;
}
}

View File

@ -102,7 +102,7 @@ export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
// prettier-ignore
return html`
<ak-toggle-group value=${this.mode} @ak-toggle=${setMode}>
<ak-toggle-group value=${this.mode} @ak-toggle=${setMode} data-ouid-component-name="proxy-type-toggle">
<option value=${ProxyMode.Proxy}>${msg("Proxy")}</option>
<option value=${ProxyMode.ForwardSingle}>${msg("Forward auth (single application)")}</option>
<option value=${ProxyMode.ForwardDomain}>${msg("Forward auth (domain level)")}</option>

View File

@ -1,53 +1,11 @@
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CoreApi,
CoreGroupsListRequest,
Group,
PropertymappingsApi,
ProvidersApi,
SCIMMapping,
SCIMProvider,
} from "@goauthentik/api";
import { ProvidersApi, SCIMProvider } from "@goauthentik/api";
export async function scimPropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderScimList({
ordering: "managed",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
export function makeSCIMPropertyMappingsSelector(
instanceMappings: string[] | undefined,
defaultSelected: string,
) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, mapping]: DualSelectPair<SCIMMapping>) =>
mapping?.managed === defaultSelected;
}
import { renderForm } from "./SCIMProviderFormForm.js";
@customElement("ak-provider-scim-form")
export class SCIMProviderFormPage extends BaseProviderForm<SCIMProvider> {
@ -70,156 +28,8 @@ export class SCIMProviderFormPage extends BaseProviderForm<SCIMProvider> {
}
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("URL")} ?required=${true} name="url">
<input
type="text"
value="${first(this.instance?.url, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("SCIM base url, usually ends in /v2.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="verifyCertificates">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.verifyCertificates, true)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Verify SCIM server's certificates")}</span
>
</label>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Token")}
?required=${true}
name="token"
>
<input
type="text"
value="${first(this.instance?.token, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Token to authenticate with. Currently only bearer authentication is supported.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group ?expanded=${true}>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="excludeUsersServiceAccount">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.excludeUsersServiceAccount, true)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Exclude service accounts")}</span
>
</label>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
ordering: "name",
includeUsers: false,
};
if (query !== undefined) {
args.search = query;
}
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(
args,
);
return groups.results;
}}
.renderElement=${(group: Group): string => {
return group.name;
}}
.value=${(group: Group | undefined): string | undefined => {
return group ? group.pk : undefined;
}}
.selected=${(group: Group): boolean => {
return group.pk === this.instance?.filterGroup;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg("Only sync users within the selected group.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group ?expanded=${true}>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="propertyMappings">
<ak-dual-select-dynamic-selected
.provider=${scimPropertyMappingsProvider}
.selector=${makeSCIMPropertyMappingsSelector(
this.instance?.propertyMappings,
"goauthentik.io/providers/scim/user",
)}
available-label=${msg("Available User Property Mappings")}
selected-label=${msg("Selected User Property Mappings")}
></ak-dual-select-dynamic-selected>
</select>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to user mapping.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Group Property Mappings")}
name="propertyMappingsGroup">
<ak-dual-select-dynamic-selected
.provider=${scimPropertyMappingsProvider}
.selector=${makeSCIMPropertyMappingsSelector(
this.instance?.propertyMappingsGroup,
"goauthentik.io/providers/scim/group",
)}
available-label=${msg("Available Group Property Mappings")}
selected-label=${msg("Selected Group Property Mappings")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to group creation.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
renderForm() {
return renderForm(this.instance ?? {}, []);
}
}

View File

@ -0,0 +1,196 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CoreApi,
CoreGroupsListRequest,
Group,
PropertymappingsApi,
SCIMMapping,
SCIMProvider,
} from "@goauthentik/api";
export async function scimPropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderScimList({
ordering: "managed",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
export function makeSCIMPropertyMappingsSelector(
instanceMappings: string[] | undefined,
defaultSelected: string,
) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, mapping]: DualSelectPair<SCIMMapping>) =>
mapping?.managed === defaultSelected;
}
export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationError = {}) {
return html`
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(provider?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("URL")} required name="url">
<input
type="text"
value="${first(provider?.url, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("SCIM base url, usually ends in /v2.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="verifyCertificates">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(provider?.verifyCertificates, true)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Verify SCIM server's certificates")}</span
>
</label>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Token")} required name="token">
<input
type="text"
value="${first(provider?.token, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Token to authenticate with. Currently only bearer authentication is supported.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="excludeUsersServiceAccount">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(provider?.excludeUsersServiceAccount, true)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Exclude service accounts")}</span>
</label>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
ordering: "name",
includeUsers: false,
};
if (query !== undefined) {
args.search = query;
}
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(args);
return groups.results;
}}
.renderElement=${(group: Group): string => {
return group.name;
}}
.value=${(group: Group | undefined): string | undefined => {
return group ? group.pk : undefined;
}}
.selected=${(group: Group): boolean => {
return group.pk === provider?.filterGroup;
}}
blankable
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg("Only sync users within the selected group.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${scimPropertyMappingsProvider}
.selector=${makeSCIMPropertyMappingsSelector(
provider?.propertyMappings,
"goauthentik.io/providers/scim/user",
)}
available-label=${msg("Available User Property Mappings")}
selected-label=${msg("Selected User Property Mappings")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to user mapping.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Group Property Mappings")}
name="propertyMappingsGroup"
>
<ak-dual-select-dynamic-selected
.provider=${scimPropertyMappingsProvider}
.selector=${makeSCIMPropertyMappingsSelector(
provider?.propertyMappingsGroup,
"goauthentik.io/providers/scim/group",
)}
available-label=${msg("Available Group Property Mappings")}
selected-label=${msg("Selected Group Property Mappings")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to group creation.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
`;
}

View File

@ -2,6 +2,11 @@ import { browser } from "@wdio/globals";
import { match } from "ts-pattern";
import { Key } from "webdriverio";
export async function doBlur(el: WebdriverIO.Element | ChainablePromiseElement) {
const element = await el;
browser.execute((element) => element.blur());
}
export async function setSearchSelect(name: string, value: string) {
const control = await (async () => {
try {
@ -38,12 +43,14 @@ export async function setSearchSelect(name: string, value: string) {
}
await (await button).click();
await browser.keys(Key.Tab);
await doBlur(control);
}
export async function setTextInput(name: string, value: string) {
const control = await $(`input[name="${name}"]`);
await control.scrollIntoView();
await control.setValue(value);
await doBlur(control);
}
export async function setRadio(name: string, value: string) {
@ -52,6 +59,7 @@ export async function setRadio(name: string, value: string) {
const item = await control.$(`label.*=${value}`).parentElement();
await item.scrollIntoView();
await item.click();
await doBlur(control);
}
export async function setTypeCreate(name: string, value: string | RegExp) {
@ -73,6 +81,7 @@ export async function setTypeCreate(name: string, value: string | RegExp) {
await card.scrollIntoView();
await card.click();
await doBlur(control);
}
export async function setFormGroup(name: string | RegExp, setting: "open" | "closed") {
@ -95,6 +104,7 @@ export async function setFormGroup(name: string | RegExp, setting: "open" | "clo
.with(["false", "open"], async () => await toggle.click())
.with(["true", "closed"], async () => await toggle.click())
.otherwise(async () => {});
await doBlur(formGroup);
}
export async function clickButton(name: string, ctx?: WebdriverIO.Element) {
@ -110,4 +120,30 @@ export async function clickButton(name: string, ctx?: WebdriverIO.Element) {
}
await button.scrollIntoView();
await button.click();
await doBlur(button);
}
const tap = <T>(a: T): T => {
console.log(a);
return a;
};
export async function clickToggleGroup(name: string, value: string | RegExp) {
const comparator =
typeof name === "string"
? (sample) => tap(sample) === tap(value)
: (sample) => value.test(sample);
const button = await (async () => {
for await (const button of $(`[data-ouid-component-name=${name}]`).$$(
".pf-c-toggle-group__button",
)) {
if (comparator(await button.$(".pf-c-toggle-group__text").getText())) {
return button;
}
}
})();
await button.scrollIntoView();
await button.click();
await doBlur(button);
}

View File

@ -9,29 +9,42 @@ import ApplicationWizardView from "../pageobjects/application-wizard.page.js";
import ApplicationsListPage from "../pageobjects/applications-list.page.js";
import { randomId } from "../utils/index.js";
import { login } from "../utils/login.js";
import { type TestSequence } from "./shared-sequences";
import {
simpleForwardAuthDomainProxyProviderForm,
simpleForwardAuthProxyProviderForm,
simpleLDAPProviderForm,
simpleOAuth2ProviderForm,
simpleProxyProviderForm,
simpleRadiusProviderForm,
simpleSAMLProviderForm,
simpleSCIMProviderForm,
} from "./shared-sequences.js";
async function reachTheProvider(title: string) {
const newPrefix = randomId();
const SUCCESS_MESSAGE = "Your application has been saved";
async function reachTheApplicationsPage() {
await ApplicationsListPage.logout();
await login();
await ApplicationsListPage.open();
await ApplicationsListPage.pause("ak-page-header");
await expect(await ApplicationsListPage.pageHeader()).toBeDisplayed();
await expect(await ApplicationsListPage.pageHeader()).toHaveText("Applications");
}
async function fillOutTheApplication(title: string) {
const newPrefix = randomId();
await (await ApplicationsListPage.startWizardButton()).click();
await (await ApplicationWizardView.wizardTitle()).waitForDisplayed();
await expect(await ApplicationWizardView.wizardTitle()).toHaveText("New application");
await (await ApplicationWizardView.app.name()).setValue(`${title} - ${newPrefix}`);
await (await ApplicationWizardView.app.uiSettings()).scrollIntoView();
await (await ApplicationWizardView.app.uiSettings()).click();
await (await ApplicationWizardView.app.launchUrl()).scrollIntoView();
await (await ApplicationWizardView.app.launchUrl()).setValue("http://example.goauthentik.io");
await (await ApplicationWizardView.nextButton()).click();
return await ApplicationWizardView.pause();
await ApplicationWizardView.pause();
}
async function getCommitMessage() {
@ -39,136 +52,45 @@ async function getCommitMessage() {
return await ApplicationWizardView.successMessage();
}
const SUCCESS_MESSAGE = "Your application has been saved";
const EXPLICIT_CONSENT = "default-provider-authorization-explicit-consent";
async function fillOutTheProviderAndCommit(provider: TestSequence) {
// The wizard automagically provides a name. If it doesn't, that's a bug.
const wizardProvider = provider.filter((p) => p.length < 2 || p[1] !== "name");
await $("ak-wizard-page-type-create").waitForDisplayed();
for await (const field of wizardProvider) {
const thefunc = field[0];
const args = field.slice(1);
console.log(`Running ${args.join(", ")}`);
// @ts-expect-error "This is a pretty alien call; I'm not surprised Typescript hates it."
await thefunc.apply($, args);
await browser.pause(1000);
}
describe("Configure Applications with the Application Wizard", () => {
it("Should configure a simple LDAP Application", async () => {
await reachTheProvider("New LDAP Application");
await $("ak-wizard-frame").$("footer button.pf-m-primary").click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
}
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.ldapProvider).scrollIntoView();
await (await ApplicationWizardView.ldapProvider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.ldap.setBindFlow("default-authentication-flow");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
async function itShouldConfigureApplicationsViaTheWizard(name: string, provider: TestSequence) {
it(`Should successfully configure an application with a ${name} provider`, async () => {
await reachTheApplicationsPage();
await fillOutTheApplication(name);
await fillOutTheProviderAndCommit(provider);
});
}
it("Should configure a simple Oauth2 Application", async () => {
await reachTheProvider("New Oauth2 Application");
const providers = [
["LDAP", simpleLDAPProviderForm],
["OAuth2", simpleOAuth2ProviderForm],
["Radius", simpleRadiusProviderForm],
["SAML", simpleSAMLProviderForm],
["SCIM", simpleSCIMProviderForm],
["Proxy", simpleProxyProviderForm],
["Forward Auth (single application)", simpleForwardAuthProxyProviderForm],
["Forward Auth (domain level)", simpleForwardAuthDomainProxyProviderForm],
];
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.oauth2Provider).scrollIntoView();
await (await ApplicationWizardView.oauth2Provider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.oauth.setAuthorizationFlow(EXPLICIT_CONSENT);
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple SAML Application", async () => {
await reachTheProvider("New SAML Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.samlProvider).scrollIntoView();
await (await ApplicationWizardView.samlProvider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.saml.setAuthorizationFlow(EXPLICIT_CONSENT);
await ApplicationWizardView.saml.acsUrl.setValue("http://example.com:8000/");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple SCIM Application", async () => {
await reachTheProvider("New SCIM Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.scimProvider).scrollIntoView();
await (await ApplicationWizardView.scimProvider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.scim.url.setValue("http://example.com:8000/");
await ApplicationWizardView.scim.token.setValue("a-very-basic-token");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple Radius Application", async () => {
await reachTheProvider("New Radius Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.radiusProvider).scrollIntoView();
await (await ApplicationWizardView.radiusProvider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.radius.setAuthenticationFlow("default-authentication-flow");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple Transparent Proxy Application", async () => {
await reachTheProvider("New Transparent Proxy Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.proxyProviderProxy).scrollIntoView();
await (await ApplicationWizardView.proxyProviderProxy).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.transparentProxy.setAuthorizationFlow(EXPLICIT_CONSENT);
await ApplicationWizardView.transparentProxy.externalHost.setValue(
"http://external.example.com",
);
await ApplicationWizardView.transparentProxy.internalHost.setValue(
"http://internal.example.com",
);
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple Forward Proxy Application", async () => {
await reachTheProvider("New Forward Proxy Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.proxyProviderForwardsingle).scrollIntoView();
await (await ApplicationWizardView.proxyProviderForwardsingle).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.forwardProxy.setAuthorizationFlow(EXPLICIT_CONSENT);
await ApplicationWizardView.forwardProxy.externalHost.setValue(
"http://external.example.com",
);
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
describe("Configuring Applications Via the Wizard", () => {
for (const [name, provider] of providers) {
itShouldConfigureApplicationsViaTheWizard(name, provider());
}
});

View File

@ -3,10 +3,16 @@ import { expect } from "@wdio/globals";
import ProviderWizardView from "../pageobjects/provider-wizard.page.js";
import ProvidersListPage from "../pageobjects/providers-list.page.js";
import { login } from "../utils/login.js";
import { type TestSequence } from "./shared-sequences";
import {
simpleForwardAuthDomainProxyProviderForm,
simpleForwardAuthProxyProviderForm,
simpleLDAPProviderForm,
simpleOAuth2ProviderForm,
simpleProxyProviderForm,
simpleRadiusProviderForm,
simpleSAMLProviderForm,
simpleSCIMProviderForm,
} from "./shared-sequences.js";
async function reachTheProvider() {
@ -36,56 +42,39 @@ const hasProviderSuccessMessage = async () =>
{ timeout: 1000, timeoutMsg: "Expected to see provider success message." },
);
type FieldDesc = [(..._: unknown) => Promise<void>, ...unknown];
async function fillOutFields(fields: FieldDesc[]) {
async function fillOutFields(fields: TestSequence) {
for (const field of fields) {
const thefunc = field[0];
const args = field.slice(1);
// @ts-expect-error "This is a pretty alien call; I'm not surprised Typescript hates it."
await thefunc.apply($, args);
}
}
describe("Configure Oauth2 Providers", () => {
it("Should configure a simple OAuth2 Provider", async () => {
async function itShouldConfigureASimpleProvider(name: string, provider: TestSequence) {
it(`Should successfully configure a ${name} provider`, async () => {
await reachTheProvider();
await $("ak-wizard-page-type-create").waitForDisplayed();
await fillOutFields(simpleOAuth2ProviderForm());
await fillOutFields(provider);
await ProviderWizardView.pause();
await ProviderWizardView.nextButton.click();
await hasProviderSuccessMessage();
});
});
}
describe("Configure LDAP Providers", () => {
it("Should configure a simple LDAP Provider", async () => {
await reachTheProvider();
await $("ak-wizard-page-type-create").waitForDisplayed();
await fillOutFields(simpleLDAPProviderForm());
await ProviderWizardView.pause();
await ProviderWizardView.nextButton.click();
await hasProviderSuccessMessage();
});
});
describe("Configuring Providers", () => {
const providers = [
["LDAP", simpleLDAPProviderForm],
["OAuth2", simpleOAuth2ProviderForm],
["Radius", simpleRadiusProviderForm],
["SAML", simpleSAMLProviderForm],
["SCIM", simpleSCIMProviderForm],
["Proxy", simpleProxyProviderForm],
["Forward Auth (single application)", simpleForwardAuthProxyProviderForm],
["Forward Auth (domain level)", simpleForwardAuthDomainProxyProviderForm],
];
describe("Configure Radius Providers", () => {
it("Should configure a simple Radius Provider", async () => {
await reachTheProvider();
await $("ak-wizard-page-type-create").waitForDisplayed();
await fillOutFields(simpleRadiusProviderForm());
await ProviderWizardView.pause();
await ProviderWizardView.nextButton.click();
await hasProviderSuccessMessage();
});
});
describe("Configure SAML Providers", () => {
it("Should configure a simple Radius Provider", async () => {
await reachTheProvider();
await $("ak-wizard-page-type-create").waitForDisplayed();
await fillOutFields(simpleRadiusProviderForm());
await ProviderWizardView.pause();
await ProviderWizardView.nextButton.click();
await hasProviderSuccessMessage();
});
for (const [name, provider] of providers) {
itShouldConfigureASimpleProvider(name, provider());
}
});

View File

@ -1,5 +1,6 @@
import {
clickButton,
clickToggleGroup,
setFormGroup,
setSearchSelect,
setTextInput,
@ -10,14 +11,26 @@ import { randomId } from "../utils/index.js";
const newObjectName = (prefix: string) => `${prefix} - ${randomId()}`;
export const simpleOAuth2ProviderForm = () => [
export type TestInteraction =
| [typeof clickButton, ...Parameters<typeof clickButton>]
| [typeof clickToggleGroup, ...Parameters<typeof clickToggleGroup>]
| [typeof setFormGroup, ...Parameters<typeof setFormGroup>]
| [typeof setSearchSelect, ...Parameters<typeof setSearchSelect>]
| [typeof setTextInput, ...Parameters<typeof setTextInput>]
| [typeof setTypeCreate, ...Parameters<typeof setTypeCreate>];
export type TestSequence = TestInteraction[];
export type TestProvider = () => TestSequence;
export const simpleOAuth2ProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Oauth2 Provider")],
[setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"],
];
export const simpleLDAPProviderForm = () => [
export const simpleLDAPProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "LDAP Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New LDAP Provider")],
@ -27,9 +40,54 @@ export const simpleLDAPProviderForm = () => [
[setSearchSelect, "invalidationFlow", "default-invalidation-flow"],
];
export const simpleRadiusProviderForm = () => [
export const simpleRadiusProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Radius Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Radius Provider")],
[setSearchSelect, "authorizationFlow", "default-authentication-flow"],
];
export const simpleSAMLProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "SAML Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New SAML Provider")],
[setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"],
[setTextInput, "acsUrl", "http://example.com:8000/"],
];
export const simpleSCIMProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "SCIM Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New SCIM Provider")],
[setTextInput, "url", "http://example.com:8000/"],
[setTextInput, "token", "insert-real-token-here"],
];
export const simpleProxyProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Proxy Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Proxy Provider")],
[setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"],
[clickToggleGroup, "proxy-type-toggle", "Proxy"],
[setTextInput, "externalHost", "http://example.com:8000/"],
[setTextInput, "internalHost", "http://example.com:8001/"],
];
export const simpleForwardAuthProxyProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Proxy Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Forward Auth Provider")],
[setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"],
[clickToggleGroup, "proxy-type-toggle", "Forward auth (single application)"],
[setTextInput, "externalHost", "http://example.com:8000/"],
];
export const simpleForwardAuthDomainProxyProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Proxy Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Forward Auth Domain Level Provider")],
[setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"],
[clickToggleGroup, "proxy-type-toggle", "Forward auth (domain level)"],
[setTextInput, "externalHost", "http://example.com:8000/"],
[setTextInput, "cookieDomain", "somedomain.tld"],
];