web: add radius to application wizard

This commit continues the application wizard buildout.  In this commit are the following changes:

- Fixed a width-setting bug in the Makefile `make help` feature (i.e "automate that stuff!")
- Added Radius to the list of providers we can offer via the wizard
- Added `launchUrl` and `UI Settings` to features of the application page the wizard can find
- Changed 'SAML Manual Configuration' to just say "SAML Configuration"
- Modified `ak-form-group` to take and honor the `aria-label` property (which in turn makes it
  easier to target specific forms with unit testing)
- Reduced the log level for wdio to 'warn'; 'info' was super-spammy and not helpful.  It can be put
  back with `--logLevel info` from the command line.
This commit is contained in:
Ken Sternberg 2023-09-27 15:30:56 -07:00
parent f2ba927d34
commit c05ff4cca1
12 changed files with 172 additions and 10 deletions

View file

@ -28,10 +28,13 @@ CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \
all: lint-fix lint test gen web ## Lint, build, and test everything all: lint-fix lint test gen web ## Lint, build, and test everything
HELP_WIDTH := $(shell grep -h '^[a-z][^ ]*:.*\#\#' $(MAKEFILE_LIST) 2>/dev/null | \
cut -d':' -f1 | awk '{printf "%d\n", length}' | sort -rn | head -1)
help: ## Show this help help: ## Show this help
@echo "\nSpecify a command. The choices are:\n" @echo "\nSpecify a command. The choices are:\n"
@grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ @grep -Eh '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-24s\033[m %s\n", $$1, $$2}' | \ awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-$(HELP_WIDTH)s \033[m %s\n", $$1, $$2}' | \
sort sort
@echo "" @echo ""

View file

@ -6,6 +6,7 @@ import TransparentProxyForm from "./forms/transparent-proxy.form.js";
import ForwardProxyForm from "./forms/forward-proxy.form.js"; import ForwardProxyForm from "./forms/forward-proxy.form.js";
import SamlForm from "./forms/saml.form.js"; import SamlForm from "./forms/saml.form.js";
import ScimForm from "./forms/scim.form.js"; import ScimForm from "./forms/scim.form.js";
import RadiusForm from "./forms/radius.form.js";
import { $ } from "@wdio/globals"; import { $ } from "@wdio/globals";
/** /**
@ -23,6 +24,7 @@ class ApplicationWizardView extends AdminPage {
forwardProxy = ForwardProxyForm; forwardProxy = ForwardProxyForm;
saml = SamlForm; saml = SamlForm;
scim = ScimForm; scim = ScimForm;
radius = RadiusForm;
app = ApplicationForm; app = ApplicationForm;
get wizardTitle() { get wizardTitle() {

View file

@ -5,6 +5,14 @@ export class ApplicationForm extends Page {
get name() { get name() {
return $('>>>ak-form-element-horizontal input[name="name"]'); return $('>>>ak-form-element-horizontal input[name="name"]');
} }
get uiSettings() {
return $('>>>ak-form-group button[aria-label="UI Settings"]');
}
get launchUrl() {
return $('>>>input[name="metaLaunchUrl"]');
}
} }
export default new ApplicationForm(); export default new ApplicationForm();

View file

@ -0,0 +1,13 @@
import Page from "../page.js";
export class RadiusForm extends Page {
async setAuthenticationFlow(selector: string) {
await this.searchSelect(
'>>>ak-tenanted-flow-search[name="authorizationFlow"] input[type="text"]',
"authorizationFlow",
`button*=${selector}`
);
}
}
export default new RadiusForm();

View file

@ -4,6 +4,8 @@ import { randomId } from "../utils/index.js";
import { login } from "../utils/login.js"; import { login } from "../utils/login.js";
import { expect } from "@wdio/globals"; import { expect } from "@wdio/globals";
async function reachTheProvider(title: string) { async function reachTheProvider(title: string) {
const newPrefix = randomId(); const newPrefix = randomId();
@ -17,6 +19,11 @@ async function reachTheProvider(title: string) {
await expect(await ApplicationWizardView.wizardTitle).toHaveText("New application"); await expect(await ApplicationWizardView.wizardTitle).toHaveText("New application");
await ApplicationWizardView.app.name.setValue(`${title} - ${newPrefix}`); await ApplicationWizardView.app.name.setValue(`${title} - ${newPrefix}`);
await ApplicationWizardView.app.uiSettings.scrollIntoView();
await ApplicationWizardView.app.uiSettings.click();
await ApplicationWizardView.app.launchUrl.scrollIntoView();
await ApplicationWizardView.app.launchUrl.setValue('http://example.goauthentik.io');
await ApplicationWizardView.nextButton.click(); await ApplicationWizardView.nextButton.click();
return await ApplicationWizardView.pause(); return await ApplicationWizardView.pause();
} }
@ -29,11 +36,16 @@ async function getCommitMessage() {
describe("Configure Applications with the Application Wizard", () => { describe("Configure Applications with the Application Wizard", () => {
it("Should configure a simple LDAP Application", async () => { it("Should configure a simple LDAP Application", async () => {
await reachTheProvider("New LDAP Application"); await reachTheProvider("New LDAP Application");
await ApplicationWizardView.providerList.waitForDisplayed(); await ApplicationWizardView.providerList.waitForDisplayed();
await ApplicationWizardView.ldapProvider.scrollIntoView();
await ApplicationWizardView.ldapProvider.click(); await ApplicationWizardView.ldapProvider.click();
await ApplicationWizardView.nextButton.click(); await ApplicationWizardView.nextButton.click();
await ApplicationWizardView.pause(); await ApplicationWizardView.pause();
@ -50,7 +62,9 @@ describe("Configure Applications with the Application Wizard", () => {
await reachTheProvider("New Oauth2 Application"); await reachTheProvider("New Oauth2 Application");
await ApplicationWizardView.providerList.waitForDisplayed(); await ApplicationWizardView.providerList.waitForDisplayed();
await ApplicationWizardView.oauth2Provider.scrollIntoView();
await ApplicationWizardView.oauth2Provider.click(); await ApplicationWizardView.oauth2Provider.click();
await ApplicationWizardView.nextButton.click(); await ApplicationWizardView.nextButton.click();
await ApplicationWizardView.pause(); await ApplicationWizardView.pause();
@ -69,7 +83,9 @@ describe("Configure Applications with the Application Wizard", () => {
await reachTheProvider("New SAML Application"); await reachTheProvider("New SAML Application");
await ApplicationWizardView.providerList.waitForDisplayed(); await ApplicationWizardView.providerList.waitForDisplayed();
await ApplicationWizardView.samlProvider.scrollIntoView();
await ApplicationWizardView.samlProvider.click(); await ApplicationWizardView.samlProvider.click();
await ApplicationWizardView.nextButton.click(); await ApplicationWizardView.nextButton.click();
await ApplicationWizardView.pause(); await ApplicationWizardView.pause();
@ -89,7 +105,9 @@ describe("Configure Applications with the Application Wizard", () => {
await reachTheProvider("New SCIM Application"); await reachTheProvider("New SCIM Application");
await ApplicationWizardView.providerList.waitForDisplayed(); await ApplicationWizardView.providerList.waitForDisplayed();
await ApplicationWizardView.scimProvider.scrollIntoView();
await ApplicationWizardView.scimProvider.click(); await ApplicationWizardView.scimProvider.click();
await ApplicationWizardView.nextButton.click(); await ApplicationWizardView.nextButton.click();
await ApplicationWizardView.pause(); await ApplicationWizardView.pause();
@ -103,10 +121,32 @@ describe("Configure Applications with the Application Wizard", () => {
); );
}); });
it("Should configure a simple Radius Application", async () => {
await reachTheProvider("New Radius Application");
await ApplicationWizardView.providerList.waitForDisplayed();
await ApplicationWizardView.radiusProvider.scrollIntoView();
await ApplicationWizardView.radiusProvider.click();
await ApplicationWizardView.nextButton.click();
await ApplicationWizardView.pause();
await ApplicationWizardView.radius.setAuthenticationFlow(
"default-authentication-flow"
);
await ApplicationWizardView.nextButton.click();
await ApplicationWizardView.pause();
await expect(getCommitMessage()).toHaveText(
"Your application has been saved"
);
});
it("Should configure a simple Transparent Proxy Application", async () => { it("Should configure a simple Transparent Proxy Application", async () => {
await reachTheProvider("New Transparent Proxy Application"); await reachTheProvider("New Transparent Proxy Application");
await ApplicationWizardView.providerList.waitForDisplayed(); await ApplicationWizardView.providerList.waitForDisplayed();
await ApplicationWizardView.proxyProviderProxy.scrollIntoView();
await ApplicationWizardView.proxyProviderProxy.click(); await ApplicationWizardView.proxyProviderProxy.click();
await ApplicationWizardView.nextButton.click(); await ApplicationWizardView.nextButton.click();
await ApplicationWizardView.pause(); await ApplicationWizardView.pause();
@ -129,6 +169,7 @@ describe("Configure Applications with the Application Wizard", () => {
await reachTheProvider("New Forward Proxy Application"); await reachTheProvider("New Forward Proxy Application");
await ApplicationWizardView.providerList.waitForDisplayed(); await ApplicationWizardView.providerList.waitForDisplayed();
await ApplicationWizardView.proxyProviderForwardsingle.scrollIntoView();
await ApplicationWizardView.proxyProviderForwardsingle.click(); await ApplicationWizardView.proxyProviderForwardsingle.click();
await ApplicationWizardView.nextButton.click(); await ApplicationWizardView.nextButton.click();
await ApplicationWizardView.pause(); await ApplicationWizardView.pause();

View file

@ -1,4 +1,5 @@
import type { Options } from "@wdio/types"; import type { Options } from "@wdio/types";
import { browser } from "@wdio/globals";
export const config: Options.Testrunner = { export const config: Options.Testrunner = {
// //
@ -86,7 +87,7 @@ export const config: Options.Testrunner = {
// Define all options that are relevant for the WebdriverIO instance here // Define all options that are relevant for the WebdriverIO instance here
// //
// Level of logging verbosity: trace | debug | info | warn | error | silent // Level of logging verbosity: trace | debug | info | warn | error | silent
logLevel: "info", logLevel: "warn",
// //
// Set specific log levels per logger // Set specific log levels per logger
// loggers: // loggers:
@ -209,8 +210,8 @@ export const config: Options.Testrunner = {
* @param {Array.<String>} specs List of spec file paths that are to be run * @param {Array.<String>} specs List of spec file paths that are to be run
* @param {object} browser instance of created browser/device session * @param {object} browser instance of created browser/device session
*/ */
// before: function (capabilities, specs) { before: function (capabilities, specs) {
// }, },
/** /**
* Runs before a WebdriverIO command gets executed. * Runs before a WebdriverIO command gets executed.
* @param {string} commandName hook command name * @param {string} commandName hook command name

View file

@ -72,8 +72,8 @@ export class ApplicationWizardApplicationDetails extends BasePanel {
.options=${policyOptions} .options=${policyOptions}
.value=${this.wizard.app?.policyEngineMode} .value=${this.wizard.app?.policyEngineMode}
></ak-radio-input> ></ak-radio-input>
<ak-form-group> <ak-form-group aria-label="UI Settings">
<span slot="header"> ${msg("UI settings")} </span> <span slot="header"> ${msg("UI Settings")} </span>
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">
<ak-text-input <ak-text-input
name="metaLaunchUrl" name="metaLaunchUrl"

View file

@ -8,6 +8,7 @@ import type {
ModelRequest, ModelRequest,
OAuth2ProviderRequest, OAuth2ProviderRequest,
ProxyProviderRequest, ProxyProviderRequest,
RadiusProviderRequest,
SAMLProviderRequest, SAMLProviderRequest,
SCIMProviderRequest, SCIMProviderRequest,
} from "@goauthentik/api"; } from "@goauthentik/api";
@ -59,11 +60,18 @@ const _providerModelsTable: ProviderType[] = [
], ],
[ [
"samlprovider", "samlprovider",
msg("SAML Manual configuration"), msg("SAML Configuration"),
msg("Configure SAML provider manually"), msg("Configure SAML provider manually"),
() => html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`, () => html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
ProviderModelEnum.SamlSamlprovider ProviderModelEnum.SamlSamlprovider
], ],
[
"radiusprovider",
msg("RADIUS Configuration"),
msg("Configure RADIUS provider manually"),
() => html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
ProviderModelEnum.RadiusRadiusprovider
],
[ [
"scimprovider", "scimprovider",
msg("SCIM Manual configuration"), msg("SCIM Manual configuration"),
@ -109,6 +117,13 @@ const converters = new Map<ProviderModelEnumType, ModelConverter>([
...(provider as SCIMProviderRequest), ...(provider as SCIMProviderRequest),
}), }),
], ],
[
ProviderModelEnum.RadiusRadiusprovider,
(provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.RadiusRadiusprovider,
...(provider as RadiusProviderRequest),
}),
],
]); ]);
// Contract enforcement // Contract enforcement

View file

@ -177,7 +177,9 @@ export class ApplicationWizardCommitApplication extends BasePanel {
} }
render(): TemplateResult { render(): TemplateResult {
const icon = classMap(this.commitState.icon.reduce((acc, icon) => ({ ...acc, [icon]: true }), {})); const icon = classMap(
this.commitState.icon.reduce((acc, icon) => ({ ...acc, [icon]: true }), {}),
);
return html` return html`
<div> <div>

View file

@ -6,6 +6,7 @@ import "./ldap/ak-application-wizard-authentication-by-ldap";
import "./oauth/ak-application-wizard-authentication-by-oauth"; import "./oauth/ak-application-wizard-authentication-by-oauth";
import "./proxy/ak-application-wizard-authentication-for-reverse-proxy"; import "./proxy/ak-application-wizard-authentication-for-reverse-proxy";
import "./proxy/ak-application-wizard-authentication-for-single-forward-proxy"; import "./proxy/ak-application-wizard-authentication-for-single-forward-proxy";
import "./radius/ak-application-wizard-authentication-by-radius";
import "./saml/ak-application-wizard-authentication-by-saml-configuration"; import "./saml/ak-application-wizard-authentication-by-saml-configuration";
import "./scim/ak-application-wizard-authentication-by-scim"; import "./scim/ak-application-wizard-authentication-by-scim";

View file

@ -0,0 +1,73 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-text-input";
import { rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { FlowsInstancesListDesignationEnum, RadiusProvider } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
@customElement("ak-application-wizard-authentication-by-radius")
export class ApplicationWizardAuthenticationByRadius extends BaseProviderPanel {
render() {
const provider = this.wizard.provider as RadiusProvider | undefined;
return html`<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
<ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
required
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Authentication flow")}
?required=${true}
name="authorizationFlow"
>
<ak-tenanted-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow}
.tenantFlow=${rootInterface()?.tenant?.flowAuthentication}
required
></ak-tenanted-flow-search>
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="sharedSecret"
label=${msg("Shared secret")}
value=${first(
provider?.sharedSecret,
randomString(128, ascii_letters + digits),
)}
required
></ak-text-input>
<ak-text-input
name="clientNetworks"
label=${msg("Client Networks")}
value=${first(provider?.clientNetworks, "0.0.0.0/0, ::/0")}
required
help=${msg(`List of CIDRs (comma-seperated) that clients can connect from. A more specific
CIDR will match before a looser one. Clients connecting from a non-specified CIDR
will be dropped.`)}
></ak-text-input>
</div>
</ak-form-group>
</form>`;
}
}
export default ApplicationWizardAuthenticationByRadius;

View file

@ -25,6 +25,9 @@ export class FormGroup extends AKElement {
@property({ type: Boolean, reflect: true }) @property({ type: Boolean, reflect: true })
expanded = false; expanded = false;
@property({ type: String, attribute: "aria-label", reflect: true })
ariaLabel = "Details";
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
PFBase, PFBase,
@ -47,7 +50,7 @@ export class FormGroup extends AKElement {
class="pf-c-button pf-m-plain" class="pf-c-button pf-m-plain"
type="button" type="button"
aria-expanded="${this.expanded}" aria-expanded="${this.expanded}"
aria-label="Details" aria-label=${this.ariaLabel}
@click=${() => { @click=${() => {
this.expanded = !this.expanded; this.expanded = !this.expanded;
}} }}