diff --git a/tests/wdio/.eslintrc.json b/tests/wdio/.eslintrc.json index 68900693d..d4095a2a3 100644 --- a/tests/wdio/.eslintrc.json +++ b/tests/wdio/.eslintrc.json @@ -15,6 +15,15 @@ "linebreak-style": ["error", "unix"], "quotes": ["error", "double", { "avoidEscape": true }], "semi": ["error", "always"], - "@typescript-eslint/ban-ts-comment": "off" + "@typescript-eslint/ban-ts-comment": "off", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ] } } diff --git a/tests/wdio/Makefile b/tests/wdio/Makefile index f5bdb0696..f45847bbe 100644 --- a/tests/wdio/Makefile +++ b/tests/wdio/Makefile @@ -36,3 +36,6 @@ test-good-login: node_modules admin-user ## Test that we can log into the serve test-bad-login: node_modules admin-user ## Test that bad usernames and passwords create appropriate error messages $(SPEC)/bad-logins.ts + +test-application-wizard: node_modules admin-user ## Test that the application wizard works as expected + $(SPEC)/new-application-by-wizard.ts diff --git a/tests/wdio/test/pageobjects/application-wizard.page.ts b/tests/wdio/test/pageobjects/application-wizard.page.ts new file mode 100644 index 000000000..2533a1abf --- /dev/null +++ b/tests/wdio/test/pageobjects/application-wizard.page.ts @@ -0,0 +1,75 @@ +import AdminPage from "./admin.page.js"; +import ApplicationForm from "./forms/application.form.js"; +import ForwardProxyForm from "./forms/forward-proxy.form.js"; +import LdapForm from "./forms/ldap.form.js"; +import OauthForm from "./forms/oauth.form.js"; +import RadiusForm from "./forms/radius.form.js"; +import SamlForm from "./forms/saml.form.js"; +import ScimForm from "./forms/scim.form.js"; +import TransparentProxyForm from "./forms/transparent-proxy.form.js"; +import { $ } from "@wdio/globals"; + +/** + * sub page containing specific selectors and methods for a specific page + */ + +class ApplicationWizardView extends AdminPage { + /** + * define selectors using getter methods + */ + + ldap = LdapForm; + oauth = OauthForm; + transparentProxy = TransparentProxyForm; + forwardProxy = ForwardProxyForm; + saml = SamlForm; + scim = ScimForm; + radius = RadiusForm; + app = ApplicationForm; + + get wizardTitle() { + return $(">>>ak-wizard-frame .pf-c-wizard__header h1.pf-c-title"); + } + + get providerList() { + return $(">>>ak-application-wizard-authentication-method-choice"); + } + + get nextButton() { + return $(">>>ak-wizard-frame footer button.pf-m-primary"); + } + + async getProviderType(type: string) { + return await this.providerList.$(`>>>input[value="${type}"]`); + } + + get successMessage() { + return $('>>>[data-commit-state="success"]'); + } +} + +type Pair = [string, string]; + +// Define a getter for each provider type in the radio button collection. + +const providerValues: Pair[] = [ + ["oauth2provider", "oauth2Provider"], + ["ldapprovider", "ldapProvider"], + ["proxyprovider-proxy", "proxyProviderProxy"], + ["proxyprovider-forwardsingle", "proxyProviderForwardsingle"], + ["radiusprovider", "radiusProvider"], + ["samlprovider", "samlProvider"], + ["scimprovider", "scimProvider"], +]; + +providerValues.forEach(([value, name]: Pair) => { + Object.defineProperties(ApplicationWizardView.prototype, { + [name]: { + get: function () { + return this.providerList.$(`>>>input[value="${value}"]`); + }, + }, + }); +}); + +export default new ApplicationWizardView(); diff --git a/tests/wdio/test/pageobjects/applications-list.page.ts b/tests/wdio/test/pageobjects/applications-list.page.ts new file mode 100644 index 000000000..0f3d93e03 --- /dev/null +++ b/tests/wdio/test/pageobjects/applications-list.page.ts @@ -0,0 +1,21 @@ +import AdminPage from "./admin.page.js"; +import { $ } from "@wdio/globals"; + +/** + * sub page containing specific selectors and methods for a specific page + */ +class ApplicationsListPage extends AdminPage { + /** + * define selectors using getter methods + */ + + get startWizardButton() { + return $('>>>ak-wizard-frame button[slot="trigger"]'); + } + + async open() { + return await super.open("if/admin/#/core/applications"); + } +} + +export default new ApplicationsListPage(); diff --git a/tests/wdio/test/pageobjects/forms/application.form.ts b/tests/wdio/test/pageobjects/forms/application.form.ts new file mode 100644 index 000000000..6f0d33217 --- /dev/null +++ b/tests/wdio/test/pageobjects/forms/application.form.ts @@ -0,0 +1,18 @@ +import Page from "../page.js"; +import { $ } from "@wdio/globals"; + +export class ApplicationForm extends Page { + get 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(); diff --git a/tests/wdio/test/pageobjects/forms/forward-proxy.form.ts b/tests/wdio/test/pageobjects/forms/forward-proxy.form.ts new file mode 100644 index 000000000..9d9a8bb50 --- /dev/null +++ b/tests/wdio/test/pageobjects/forms/forward-proxy.form.ts @@ -0,0 +1,18 @@ +import Page from "../page.js"; +import { $ } from "@wdio/globals"; + +export class ForwardProxyForm extends Page { + async setAuthorizationFlow(selector: string) { + await this.searchSelect( + '>>>ak-flow-search[name="authorizationFlow"] input[type="text"]', + "authorizationFlow", + `button*=${selector}`, + ); + } + + get externalHost() { + return $('>>>input[name="externalHost"]'); + } +} + +export default new ForwardProxyForm(); diff --git a/tests/wdio/test/pageobjects/forms/ldap.form.ts b/tests/wdio/test/pageobjects/forms/ldap.form.ts new file mode 100644 index 000000000..343fe583c --- /dev/null +++ b/tests/wdio/test/pageobjects/forms/ldap.form.ts @@ -0,0 +1,13 @@ +import Page from "../page.js"; + +export class LdapForm extends Page { + async setBindFlow(selector: string) { + await this.searchSelect( + '>>>ak-tenanted-flow-search[name="authorizationFlow"] input[type="text"]', + "authorizationFlow", + `button*=${selector}`, + ); + } +} + +export default new LdapForm(); diff --git a/tests/wdio/test/pageobjects/forms/radius.form.ts b/tests/wdio/test/pageobjects/forms/radius.form.ts new file mode 100644 index 000000000..591459866 --- /dev/null +++ b/tests/wdio/test/pageobjects/forms/radius.form.ts @@ -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(); diff --git a/tests/wdio/test/pageobjects/forms/saml.form.ts b/tests/wdio/test/pageobjects/forms/saml.form.ts new file mode 100644 index 000000000..4419e1cb5 --- /dev/null +++ b/tests/wdio/test/pageobjects/forms/saml.form.ts @@ -0,0 +1,18 @@ +import Page from "../page.js"; +import { $ } from "@wdio/globals"; + +export class SamlForm extends Page { + async setAuthorizationFlow(selector: string) { + await this.searchSelect( + '>>>ak-flow-search[name="authorizationFlow"] input[type="text"]', + "authorizationFlow", + `button*=${selector}`, + ); + } + + get acsUrl() { + return $('>>>input[name="acsUrl"]'); + } +} + +export default new SamlForm(); diff --git a/tests/wdio/test/pageobjects/forms/scim.form.ts b/tests/wdio/test/pageobjects/forms/scim.form.ts new file mode 100644 index 000000000..41a11356c --- /dev/null +++ b/tests/wdio/test/pageobjects/forms/scim.form.ts @@ -0,0 +1,13 @@ +import Page from "../page.js"; + +export class ScimForm extends Page { + get url() { + return $('>>>input[name="url"]'); + } + + get token() { + return $('>>>input[name="token"]'); + } +} + +export default new ScimForm(); diff --git a/tests/wdio/test/pageobjects/forms/transparent-proxy.form.ts b/tests/wdio/test/pageobjects/forms/transparent-proxy.form.ts new file mode 100644 index 000000000..505016946 --- /dev/null +++ b/tests/wdio/test/pageobjects/forms/transparent-proxy.form.ts @@ -0,0 +1,22 @@ +import Page from "../page.js"; +import { $ } from "@wdio/globals"; + +export class TransparentProxyForm extends Page { + async setAuthorizationFlow(selector: string) { + await this.searchSelect( + '>>>ak-flow-search[name="authorizationFlow"] input[type="text"]', + "authorizationFlow", + `button*=${selector}`, + ); + } + + get externalHost() { + return $('>>>input[name="externalHost"]'); + } + + get internalHost() { + return $('>>>input[name="internalHost"]'); + } +} + +export default new TransparentProxyForm(); diff --git a/tests/wdio/test/specs/new-application-by-wizard.ts b/tests/wdio/test/specs/new-application-by-wizard.ts new file mode 100644 index 000000000..6e4965779 --- /dev/null +++ b/tests/wdio/test/specs/new-application-by-wizard.ts @@ -0,0 +1,167 @@ +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 { expect } from "@wdio/globals"; + +async function reachTheProvider(title: string) { + const newPrefix = randomId(); + + await ApplicationsListPage.logout(); + await login(); + await ApplicationsListPage.open(); + await expect(await ApplicationsListPage.pageHeader).toHaveText("Applications"); + + await ApplicationsListPage.startWizardButton.click(); + await ApplicationWizardView.wizardTitle.waitForDisplayed(); + await expect(await ApplicationWizardView.wizardTitle).toHaveText("New application"); + + 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(); + return await ApplicationWizardView.pause(); +} + +async function getCommitMessage() { + await ApplicationWizardView.successMessage.waitForDisplayed(); + return await ApplicationWizardView.successMessage; +} + +const SUCCESS_MESSAGE = "Your application has been saved"; +const EXPLICIT_CONSENT = "default-provider-authorization-explicit-consent"; + +describe("Configure Applications with the Application Wizard", () => { + it("Should configure a simple LDAP Application", async () => { + await reachTheProvider("New LDAP Application"); + + await ApplicationWizardView.providerList.waitForDisplayed(); + await ApplicationWizardView.ldapProvider.scrollIntoView(); + await ApplicationWizardView.ldapProvider.click(); + + await ApplicationWizardView.nextButton.click(); + await ApplicationWizardView.pause(); + + await ApplicationWizardView.ldap.setBindFlow("default-authentication-flow"); + await ApplicationWizardView.nextButton.click(); + await ApplicationWizardView.pause(); + + await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE); + }); + + it("Should configure a simple Oauth2 Application", async () => { + await reachTheProvider("New Oauth2 Application"); + + await ApplicationWizardView.providerList.waitForDisplayed(); + await ApplicationWizardView.oauth2Provider.scrollIntoView(); + await ApplicationWizardView.oauth2Provider.click(); + + await ApplicationWizardView.nextButton.click(); + await ApplicationWizardView.pause(); + + await ApplicationWizardView.oauth.setAuthorizationFlow(EXPLICIT_CONSENT); + await ApplicationWizardView.nextButton.click(); + await ApplicationWizardView.pause(); + + await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE); + }); + + it("Should configure a simple SAML Application", async () => { + await reachTheProvider("New SAML Application"); + + await ApplicationWizardView.providerList.waitForDisplayed(); + await ApplicationWizardView.samlProvider.scrollIntoView(); + await ApplicationWizardView.samlProvider.click(); + + await ApplicationWizardView.nextButton.click(); + await ApplicationWizardView.pause(); + + await ApplicationWizardView.saml.setAuthorizationFlow(EXPLICIT_CONSENT); + await ApplicationWizardView.saml.acsUrl.setValue("http://example.com:8000/"); + await ApplicationWizardView.nextButton.click(); + await ApplicationWizardView.pause(); + + await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE); + }); + + it("Should configure a simple SCIM Application", async () => { + await reachTheProvider("New SCIM Application"); + + await ApplicationWizardView.providerList.waitForDisplayed(); + await ApplicationWizardView.scimProvider.scrollIntoView(); + await ApplicationWizardView.scimProvider.click(); + + 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 ApplicationWizardView.nextButton.click(); + await ApplicationWizardView.pause(); + + await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE); + }); + + 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(SUCCESS_MESSAGE); + }); + + it("Should configure a simple Transparent Proxy Application", async () => { + await reachTheProvider("New Transparent Proxy Application"); + + await ApplicationWizardView.providerList.waitForDisplayed(); + await ApplicationWizardView.proxyProviderProxy.scrollIntoView(); + await ApplicationWizardView.proxyProviderProxy.click(); + 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 ApplicationWizardView.nextButton.click(); + await ApplicationWizardView.pause(); + + await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE); + }); + + it("Should configure a simple Forward Proxy Application", async () => { + await reachTheProvider("New Forward Proxy Application"); + + await ApplicationWizardView.providerList.waitForDisplayed(); + await ApplicationWizardView.proxyProviderForwardsingle.scrollIntoView(); + await ApplicationWizardView.proxyProviderForwardsingle.click(); + await ApplicationWizardView.nextButton.click(); + await ApplicationWizardView.pause(); + + await ApplicationWizardView.forwardProxy.setAuthorizationFlow(EXPLICIT_CONSENT); + await ApplicationWizardView.forwardProxy.externalHost.setValue( + "http://external.example.com", + ); + + await ApplicationWizardView.nextButton.click(); + await ApplicationWizardView.pause(); + + await expect(getCommitMessage()).toHaveText(SUCCESS_MESSAGE); + }); +}); diff --git a/tests/wdio/wdio.conf.ts b/tests/wdio/wdio.conf.ts index bb7420636..525cfb00d 100644 --- a/tests/wdio/wdio.conf.ts +++ b/tests/wdio/wdio.conf.ts @@ -86,7 +86,7 @@ export const config: Options.Testrunner = { // Define all options that are relevant for the WebdriverIO instance here // // Level of logging verbosity: trace | debug | info | warn | error | silent - logLevel: "info", + logLevel: "warn", // // Set specific log levels per logger // loggers: @@ -157,4 +157,149 @@ export const config: Options.Testrunner = { ui: "bdd", timeout: 60000, }, + // + // ===== + // Hooks + // ===== + // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance + // it and to build services around it. You can either apply a single function or an array of + // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got + // resolved to continue. + /** + * Gets executed once before all workers get launched. + * @param {object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + */ + // onPrepare: function (config, capabilities) { + // }, + /** + * Gets executed before a worker process is spawned and can be used to initialise specific service + * for that worker as well as modify runtime environments in an async fashion. + * @param {string} cid capability id (e.g 0-0) + * @param {object} caps object containing capabilities for session that will be spawn in the worker + * @param {object} specs specs to be run in the worker process + * @param {object} args object that will be merged with the main configuration once worker is initialized + * @param {object} execArgv list of string arguments passed to the worker process + */ + // onWorkerStart: function (cid, caps, specs, args, execArgv) { + // }, + /** + * Gets executed just after a worker process has exited. + * @param {string} cid capability id (e.g 0-0) + * @param {number} exitCode 0 - success, 1 - fail + * @param {object} specs specs to be run in the worker process + * @param {number} retries number of retries used + */ + // onWorkerEnd: function (cid, exitCode, specs, retries) { + // }, + /** + * Gets executed just before initialising the webdriver session and test framework. It allows you + * to manipulate configurations depending on the capability or spec. + * @param {object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that are to be run + * @param {string} cid worker id (e.g. 0-0) + */ + // beforeSession: function (config, capabilities, specs, cid) { + // }, + /** + * Gets executed before test execution begins. At this point you can access to all global + * variables like `browser`. It is the perfect place to define custom commands. + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that are to be run + * @param {object} browser instance of created browser/device session + */ + before: function (_capabilities, _specs) {}, + /** + * Runs before a WebdriverIO command gets executed. + * @param {string} commandName hook command name + * @param {Array} args arguments that command would receive + */ + // beforeCommand: function (commandName, args) { + // }, + /** + * Hook that gets executed before the suite starts + * @param {object} suite suite details + */ + // beforeSuite: function (suite) { + // }, + /** + * Function to be executed before a test (in Mocha/Jasmine) starts. + */ + // beforeTest: function (test, context) { + // }, + /** + * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling + * beforeEach in Mocha) + */ + // beforeHook: function (test, context) { + // }, + /** + * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling + * afterEach in Mocha) + */ + // afterHook: function (test, context, { error, result, duration, passed, retries }) { + // }, + /** + * Function to be executed after a test (in Mocha/Jasmine only) + * @param {object} test test object + * @param {object} context scope object the test was executed with + * @param {Error} result.error error object in case the test fails, otherwise `undefined` + * @param {*} result.result return object of test function + * @param {number} result.duration duration of test + * @param {boolean} result.passed true if test has passed, otherwise false + * @param {object} result.retries information about spec related retries, e.g. `{ attempts: 0, limit: 0 }` + */ + // afterTest: function(test, context, { error, result, duration, passed, retries }) { + // }, + + /** + * Hook that gets executed after the suite has ended + * @param {object} suite suite details + */ + // afterSuite: function (suite) { + // }, + /** + * Runs after a WebdriverIO command gets executed + * @param {string} commandName hook command name + * @param {Array} args arguments that command would receive + * @param {number} result 0 - command success, 1 - command error + * @param {object} error error object if any + */ + // afterCommand: function (commandName, args, result, error) { + // }, + /** + * Gets executed after all tests are done. You still have access to all global variables from + * the test. + * @param {number} result 0 - test pass, 1 - test fail + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that ran + */ + // after: function (result, capabilities, specs) { + // }, + /** + * Gets executed right after terminating the webdriver session. + * @param {object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + * @param {Array.} specs List of spec file paths that ran + */ + // afterSession: function (config, capabilities, specs) { + // }, + /** + * Gets executed after all workers got shut down and the process is about to exit. An error + * thrown in the onComplete hook will result in the test run failing. + * @param {object} exitCode 0 - success, 1 - fail + * @param {object} config wdio configuration object + * @param {Array.} capabilities list of capabilities details + * @param {} results object containing test results + */ + // onComplete: function(exitCode, config, capabilities, results) { + // }, + /** + * Gets executed when a refresh happens. + * @param {string} oldSessionId session ID of the old session + * @param {string} newSessionId session ID of the new session + */ + // onReload: function(oldSessionId, newSessionId) { + // } }; diff --git a/web/src/admin/applications/ApplicationForm.ts b/web/src/admin/applications/ApplicationForm.ts index e6f5fae93..42489f9f3 100644 --- a/web/src/admin/applications/ApplicationForm.ts +++ b/web/src/admin/applications/ApplicationForm.ts @@ -1,7 +1,12 @@ import "@goauthentik/admin/applications/ProviderSelectModal"; import { iconHelperText } from "@goauthentik/admin/helperText"; import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config"; -import { first, groupBy } from "@goauthentik/common/utils"; +import { first } from "@goauthentik/common/utils"; +import "@goauthentik/components/ak-file-input"; +import "@goauthentik/components/ak-radio-input"; +import "@goauthentik/components/ak-switch-input"; +import "@goauthentik/components/ak-text-input"; +import "@goauthentik/components/ak-textarea-input"; import { rootInterface } from "@goauthentik/elements/Base"; import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/HorizontalFormElement"; @@ -23,12 +28,34 @@ import { CoreApi, PolicyEngineMode, Provider, - ProvidersAllListRequest, - ProvidersApi, } from "@goauthentik/api"; +import "./components/ak-backchannel-input"; +import "./components/ak-provider-search-input"; + +export const policyOptions = [ + { + label: "any", + value: PolicyEngineMode.Any, + default: true, + description: html`${msg("Any policy must match to grant access")}`, + }, + { + label: "all", + value: PolicyEngineMode.All, + description: html`${msg("All policies must match to grant access")}`, + }, +]; + @customElement("ak-application-form") export class ApplicationForm extends ModelForm { + constructor() { + super(); + this.handleConfirmBackchannelProviders = this.handleConfirmBackchannelProviders.bind(this); + this.makeRemoveBackchannelProviderHandler = + this.makeRemoveBackchannelProviderHandler.bind(this); + } + async loadInstance(pk: string): Promise { const app = await new CoreApi(DEFAULT_CONFIG).coreApplicationsRetrieve({ slug: pk, @@ -89,237 +116,137 @@ export class ApplicationForm extends ModelForm { return app; } + handleConfirmBackchannelProviders({ items }: { items: Provider[] }) { + this.backchannelProviders = items; + this.requestUpdate(); + return Promise.resolve(); + } + + makeRemoveBackchannelProviderHandler(provider: Provider) { + return () => { + const idx = this.backchannelProviders.indexOf(provider); + this.backchannelProviders.splice(idx, 1); + this.requestUpdate(); + }; + } + + handleClearIcon(ev: Event) { + ev.stopPropagation(); + if (!(ev instanceof InputEvent) || !ev.target) { + return; + } + this.clearIcon = !!(ev.target as HTMLInputElement).checked; + } + renderForm(): TemplateResult { - return html` - -

${msg("Application's display Name.")}

-
- - -

- ${msg("Internal application name, used in URLs.")} -

-
- - -

- ${msg( - "Optionally enter a group name. Applications with identical groups are shown grouped together.", - )} -

-
- - => { - const args: ProvidersAllListRequest = { - ordering: "name", - }; - if (query !== undefined) { - args.search = query; - } - const items = await new ProvidersApi(DEFAULT_CONFIG).providersAllList(args); - return items.results; - }} - .renderElement=${(item: Provider): string => { - return item.name; - }} - .value=${(item: Provider | undefined): number | undefined => { - return item?.pk; - }} - .groupBy=${(items: Provider[]) => { - return groupBy(items, (item) => item.verboseName); - }} - .selected=${(item: Provider): boolean => { - return this.instance?.provider === item.pk; - }} - ?blankable=${true} - > - -

- ${msg("Select a provider that this application should use.")} -

-
- - + + + + + `} > -
- { - this.backchannelProviders = items; - this.requestUpdate(); - return Promise.resolve(); - }} - > - - -
- - ${this.backchannelProviders.map((provider) => { - return html` { - const idx = this.backchannelProviders.indexOf(provider); - this.backchannelProviders.splice(idx, 1); - this.requestUpdate(); - }} - > - ${provider.name} - `; - })} - -
-
-

- ${msg( - "Select backchannel providers which augment the functionality of the main provider.", - )} -

-
- - + - - - + .options=${policyOptions} + .value=${this.instance?.policyEngineMode} + > ${msg("UI settings")}
- - -

- ${msg( - "If left empty, authentik will try to extract the launch URL based on the selected provider.", - )} -

-
- - -

- ${msg( - "If checked, the launch URL will open in a new browser tab or window from the user's application library.", - )} -

-
+ + + ${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanSaveMedia) - ? html` - - ${this.instance?.metaIcon - ? html` -

- ${msg("Currently set to:")} - ${this.instance?.metaIcon} -

- ` - : html``} -
+ ? html` ${this.instance?.metaIcon ? html` - - -

- ${msg("Delete currently set icon.")} -

-
+ ` : html``}` - : html` - -

${iconHelperText}

-
`} - - - - - - + : html` + `} + +
-
`; + + `; } } diff --git a/web/src/admin/applications/ApplicationListPage.ts b/web/src/admin/applications/ApplicationListPage.ts index b29ac0df5..c06236750 100644 --- a/web/src/admin/applications/ApplicationListPage.ts +++ b/web/src/admin/applications/ApplicationListPage.ts @@ -1,5 +1,4 @@ import "@goauthentik/admin/applications/ApplicationForm"; -import "@goauthentik/admin/applications/wizard/ApplicationWizard"; import { PFSize } from "@goauthentik/app/elements/Spinner"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { uiConfig } from "@goauthentik/common/ui/config"; @@ -10,6 +9,7 @@ import "@goauthentik/elements/buttons/SpinnerButton"; import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/ModalForm"; import { getURLParam } from "@goauthentik/elements/router/RouteMatch"; +// import { getURLParam } from "@goauthentik/elements/router/RouteMatch"; import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table"; import { TablePage } from "@goauthentik/elements/table/TablePage"; @@ -23,6 +23,8 @@ import PFCard from "@patternfly/patternfly/components/Card/card.css"; import { Application, CoreApi } from "@goauthentik/api"; +import "./ApplicationWizardHint"; + @customElement("ak-application-list") export class ApplicationListPage extends TablePage { searchEnabled(): boolean { @@ -33,7 +35,7 @@ export class ApplicationListPage extends TablePage { } pageDescription(): string { return msg( - "External Applications which use authentik as Identity-Provider, utilizing protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.", + "External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.", ); } pageIcon(): string { @@ -87,20 +89,27 @@ export class ApplicationListPage extends TablePage { ]; } + renderSectionBefore(): TemplateResult { + return html``; + } + renderSidebarAfter(): TemplateResult { // Rendering the wizard with .open here, as if we set the attribute in // renderObjectCreate() it'll open two wizards, since that function gets called twice - return html` -
-
-
- -
+ >*/ + + return html`
+
+
+
-
`; +
+
`; } renderToolbarSelected(): TemplateResult { diff --git a/web/src/admin/applications/ApplicationWizardHint.ts b/web/src/admin/applications/ApplicationWizardHint.ts index 9f0a22e88..288e18dbd 100644 --- a/web/src/admin/applications/ApplicationWizardHint.ts +++ b/web/src/admin/applications/ApplicationWizardHint.ts @@ -1,4 +1,4 @@ -import { MessageLevel } from "@goauthentik/common/messages"; +import "@goauthentik/admin/applications/wizard/ak-application-wizard"; import { ShowHintController, ShowHintControllerHost, @@ -6,18 +6,37 @@ import { import "@goauthentik/components/ak-hint/ak-hint"; import "@goauthentik/components/ak-hint/ak-hint-body"; import { AKElement } from "@goauthentik/elements/Base"; +import "@goauthentik/elements/Label"; import "@goauthentik/elements/buttons/ActionButton/ak-action-button"; -import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; +import { getURLParam } from "@goauthentik/elements/router/RouteMatch"; -import { html, nothing } from "lit"; +import { msg } from "@lit/localize"; +import { html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; +import { styleMap } from "lit/directives/style-map.js"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFLabel from "@patternfly/patternfly/components/Label/label.css"; import PFPage from "@patternfly/patternfly/components/Page/page.css"; +const closeButtonIcon = html``; + @customElement("ak-application-wizard-hint") export class AkApplicationWizardHint extends AKElement implements ShowHintControllerHost { static get styles() { - return [PFPage]; + return [PFButton, PFPage, PFLabel]; } @property({ type: Boolean, attribute: "show-hint" }) @@ -36,33 +55,60 @@ export class AkApplicationWizardHint extends AKElement implements ShowHintContro ); } + renderReminder() { + const sectionStyles = { + paddingBottom: "0", + marginBottom: "-0.5rem", + marginRight: "0.0625rem", + textAlign: "right", + }; + const textStyle = { maxWidth: "60ch" }; + + return html`
+ + + + ${msg("One hint, 'New Application Wizard', is currently hidden")} + + + + +
`; + } + renderHint() { return html`

- Authentik has a new Application Wizard that can configure both an - application and its authentication provider at the same time. - Learn more about the wizard here. + You can now configure both an application and its authentication provider at + the same time with our new Application Wizard. +

- { - showMessage({ - message: "This would have shown the wizard", - level: MessageLevel.success, - }); - }} - >Create with Wizard
+ + + ${this.showHintController.render()}
`; } render() { - return this.showHint || this.forceHint ? this.renderHint() : nothing; + return this.showHint || this.forceHint ? this.renderHint() : this.renderReminder(); } } diff --git a/web/src/admin/applications/components/ak-backchannel-input.ts b/web/src/admin/applications/components/ak-backchannel-input.ts new file mode 100644 index 000000000..2130ba10b --- /dev/null +++ b/web/src/admin/applications/components/ak-backchannel-input.ts @@ -0,0 +1,80 @@ +import "@goauthentik/admin/applications/ProviderSelectModal"; +import { AKElement } from "@goauthentik/elements/Base"; + +import { TemplateResult, html, nothing } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; +import { map } from "lit/directives/map.js"; + +import { Provider } from "@goauthentik/api"; + +@customElement("ak-backchannel-providers-input") +export class AkBackchannelProvidersInput extends AKElement { + // Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but + // we're not actually using that and, for the meantime, we need the form handlers to be able to + // find the children of this component. + // + // This field is so highly specialized that it would make more sense if we put the API and the + // fetcher here. + // + // TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the + // visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in + // general. + protected createRenderRoot() { + return this; + } + + @property({ type: String }) + name!: string; + + @property({ type: String }) + label = ""; + + @property({ type: Array }) + providers: Provider[] = []; + + @property({ type: Object }) + tooltip?: TemplateResult; + + @property({ attribute: false, type: Object }) + confirm!: ({ items }: { items: Provider[] }) => Promise; + + @property({ attribute: false, type: Object }) + remover!: (provider: Provider) => () => void; + + @property({ type: String }) + value = ""; + + @property({ type: Boolean }) + required = false; + + @property({ type: String }) + help = ""; + + render() { + const renderOneChip = (provider: Provider) => + html`${provider.name}`; + + return html` + +
+ + + +
+ ${map(this.providers, renderOneChip)} +
+
+ ${this.help ? html`

${this.help}

` : nothing} +
+ `; + } +} diff --git a/web/src/admin/applications/components/ak-provider-search-input.ts b/web/src/admin/applications/components/ak-provider-search-input.ts new file mode 100644 index 000000000..552cb0764 --- /dev/null +++ b/web/src/admin/applications/components/ak-provider-search-input.ts @@ -0,0 +1,80 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { groupBy } from "@goauthentik/common/utils"; +import { AKElement } from "@goauthentik/elements/Base"; +import "@goauthentik/elements/forms/SearchSelect"; + +import { html, nothing } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +import { Provider, ProvidersAllListRequest, ProvidersApi } from "@goauthentik/api"; + +const renderElement = (item: Provider) => item.name; +const renderValue = (item: Provider | undefined) => item?.pk; +const doGroupBy = (items: Provider[]) => groupBy(items, (item) => item.verboseName); + +async function fetch(query?: string) { + const args: ProvidersAllListRequest = { + ordering: "name", + }; + if (query !== undefined) { + args.search = query; + } + const items = await new ProvidersApi(DEFAULT_CONFIG).providersAllList(args); + return items.results; +} + +@customElement("ak-provider-search-input") +export class AkProviderInput extends AKElement { + // Render into the lightDOM. This effectively erases the shadowDOM nature of this component, but + // we're not actually using that and, for the meantime, we need the form handlers to be able to + // find the children of this component. + // + // TODO: This abstraction is wrong; it's putting *more* layers in as a way of managing the + // visual clutter and legibility issues of ak-form-elemental-horizontal and patternfly in + // general. + protected createRenderRoot() { + return this; + } + + @property({ type: String }) + name!: string; + + @property({ type: String }) + label = ""; + + @property({ type: Number }) + value?: number; + + @property({ type: Boolean }) + required = false; + + @property({ type: Boolean }) + blankable = false; + + @property({ type: String }) + help = ""; + + constructor() { + super(); + this.selected = this.selected.bind(this); + } + + selected(item: Provider) { + return this.value !== undefined && this.value === item.pk; + } + + render() { + return html` + + + ${this.help ? html`

${this.help}

` : nothing} +
`; + } +} diff --git a/web/src/admin/applications/wizard/ApplicationWizard.ts b/web/src/admin/applications/wizard/ApplicationWizard.ts deleted file mode 100644 index d8ceb9749..000000000 --- a/web/src/admin/applications/wizard/ApplicationWizard.ts +++ /dev/null @@ -1,64 +0,0 @@ -import "@goauthentik/admin/applications/wizard/InitialApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/TypeApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/ldap/TypeLDAPApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/link/TypeLinkApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthAPIApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/oauth/TypeOAuthImplicitApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/proxy/TypeProxyApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/saml/TypeSAMLApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/saml/TypeSAMLConfigApplicationWizardPage"; -import "@goauthentik/admin/applications/wizard/saml/TypeSAMLImportApplicationWizardPage"; -import { AKElement } from "@goauthentik/elements/Base"; -import "@goauthentik/elements/wizard/Wizard"; - -import { msg } from "@lit/localize"; -import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; -import { CSSResult, TemplateResult, html } from "lit"; -import { property } from "lit/decorators.js"; - -import PFButton from "@patternfly/patternfly/components/Button/button.css"; -import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; -import PFBase from "@patternfly/patternfly/patternfly-base.css"; - -@customElement("ak-application-wizard") -export class ApplicationWizard extends AKElement { - static get styles(): CSSResult[] { - return [PFBase, PFButton, PFRadio]; - } - - @property({ type: Boolean }) - open = false; - - @property() - createText = msg("Create"); - - @property({ type: Boolean }) - showButton = true; - - @property({ attribute: false }) - finalHandler: () => Promise = () => { - return Promise.resolve(); - }; - - render(): TemplateResult { - return html` - { - return this.finalHandler(); - }} - > - ${this.showButton - ? html`` - : html``} - - `; - } -} diff --git a/web/src/admin/applications/wizard/BasePanel.css.ts b/web/src/admin/applications/wizard/BasePanel.css.ts new file mode 100644 index 000000000..31479ed80 --- /dev/null +++ b/web/src/admin/applications/wizard/BasePanel.css.ts @@ -0,0 +1,28 @@ +import { css } from "lit"; + +import PFAlert from "@patternfly/patternfly/components/Alert/alert.css"; +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFCard from "@patternfly/patternfly/components/Card/card.css"; +import PFForm from "@patternfly/patternfly/components/Form/form.css"; +import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; +import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css"; +import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; +import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +export const styles = [ + PFBase, + PFCard, + PFButton, + PFForm, + PFAlert, + PFRadio, + PFInputGroup, + PFFormControl, + PFSwitch, + css` + select[multiple] { + height: 15em; + } + `, +]; diff --git a/web/src/admin/applications/wizard/BasePanel.ts b/web/src/admin/applications/wizard/BasePanel.ts new file mode 100644 index 000000000..a395fc4b3 --- /dev/null +++ b/web/src/admin/applications/wizard/BasePanel.ts @@ -0,0 +1,35 @@ +import { WizardPanel } from "@goauthentik/components/ak-wizard-main/types"; +import { AKElement } from "@goauthentik/elements/Base"; +import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; + +import { consume } from "@lit-labs/context"; +import { query } from "@lit/reactive-element/decorators.js"; + +import { styles as AwadStyles } from "./BasePanel.css"; + +import { applicationWizardContext } from "./ContextIdentity"; +import type { ApplicationWizardState, ApplicationWizardStateUpdate } from "./types"; + +export class ApplicationWizardPageBase + extends CustomEmitterElement(AKElement) + implements WizardPanel +{ + static get styles() { + return AwadStyles; + } + + @query("form") + form!: HTMLFormElement; + + rendered = false; + + @consume({ context: applicationWizardContext }) + public wizard!: ApplicationWizardState; + + // This used to be more complex; now it just establishes the event name. + dispatchWizardUpdate(update: ApplicationWizardStateUpdate) { + this.dispatchCustomEvent("ak-wizard-update", update); + } +} + +export default ApplicationWizardPageBase; diff --git a/web/src/admin/applications/wizard/ContextIdentity.ts b/web/src/admin/applications/wizard/ContextIdentity.ts new file mode 100644 index 000000000..f03f147a6 --- /dev/null +++ b/web/src/admin/applications/wizard/ContextIdentity.ts @@ -0,0 +1,9 @@ +import { createContext } from "@lit-labs/context"; + +import { ApplicationWizardState } from "./types"; + +export const applicationWizardContext = createContext( + Symbol("ak-application-wizard-state-context"), +); + +export default applicationWizardContext; diff --git a/web/src/admin/applications/wizard/InitialApplicationWizardPage.ts b/web/src/admin/applications/wizard/InitialApplicationWizardPage.ts deleted file mode 100644 index 1e4388e70..000000000 --- a/web/src/admin/applications/wizard/InitialApplicationWizardPage.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { convertToSlug } from "@goauthentik/common/utils"; -import { KeyUnknown } from "@goauthentik/elements/forms/Form"; -import "@goauthentik/elements/forms/FormGroup"; -import "@goauthentik/elements/forms/HorizontalFormElement"; -import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; - -import { msg } from "@lit/localize"; -import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; -import { TemplateResult, html } from "lit"; - -import { ApplicationRequest, CoreApi, Provider } from "@goauthentik/api"; - -@customElement("ak-application-wizard-initial") -export class InitialApplicationWizardPage extends WizardFormPage { - sidebarLabel = () => msg("Application details"); - - nextDataCallback = async (data: KeyUnknown): Promise => { - const name = data.name as string; - let slug = convertToSlug(name || ""); - // Check if an application with the generated slug already exists - const apps = await new CoreApi(DEFAULT_CONFIG).coreApplicationsList({ - search: slug, - }); - if (apps.results.filter((app) => app.slug == slug)) { - slug += "-1"; - } - this.host.state["slug"] = slug; - this.host.state["name"] = name; - this.host.addActionBefore(msg("Create application"), async (): Promise => { - const req: ApplicationRequest = { - name: name || "", - slug: slug, - metaPublisher: data.metaPublisher as string, - metaDescription: data.metaDescription as string, - }; - if ("provider" in this.host.state) { - req.provider = (this.host.state["provider"] as Provider).pk; - } - if ("link" in this.host.state) { - req.metaLaunchUrl = this.host.state["link"] as string; - } - this.host.state["app"] = await new CoreApi(DEFAULT_CONFIG).coreApplicationsCreate({ - applicationRequest: req, - }); - return true; - }); - return true; - }; - - renderForm(): TemplateResult { - return html` -
- - -

${msg("Application's display Name.")}

-
- - ${msg("Additional UI settings")} -
- - - - - - -
-
-
- `; - } -} diff --git a/web/src/admin/applications/wizard/TypeApplicationWizardPage.ts b/web/src/admin/applications/wizard/TypeApplicationWizardPage.ts deleted file mode 100644 index ff55cb67c..000000000 --- a/web/src/admin/applications/wizard/TypeApplicationWizardPage.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; - -import { msg } from "@lit/localize"; -import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; -import { CSSResult, TemplateResult, html } from "lit"; - -import PFButton from "@patternfly/patternfly/components/Button/button.css"; -import PFForm from "@patternfly/patternfly/components/Form/form.css"; -import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; -import PFBase from "@patternfly/patternfly/patternfly-base.css"; - -import { TypeCreate } from "@goauthentik/api"; - -@customElement("ak-application-wizard-type") -export class TypeApplicationWizardPage extends WizardPage { - applicationTypes: TypeCreate[] = [ - { - component: "ak-application-wizard-type-oauth", - name: msg("OAuth2/OIDC"), - description: msg("Modern applications, APIs and Single-page applications."), - modelName: "", - }, - { - component: "ak-application-wizard-type-saml", - name: msg("SAML"), - description: msg( - "XML-based SSO standard. Use this if your application only supports SAML.", - ), - modelName: "", - }, - { - component: "ak-application-wizard-type-proxy", - name: msg("Proxy"), - description: msg("Legacy applications which don't natively support SSO."), - modelName: "", - }, - { - component: "ak-application-wizard-type-ldap", - name: msg("LDAP"), - description: msg( - "Provide an LDAP interface for applications and users to authenticate against.", - ), - modelName: "", - }, - { - component: "ak-application-wizard-type-link", - name: msg("Link"), - description: msg( - "Provide an LDAP interface for applications and users to authenticate against.", - ), - modelName: "", - }, - ]; - - sidebarLabel = () => msg("Authentication method"); - - static get styles(): CSSResult[] { - return [PFBase, PFButton, PFForm, PFRadio]; - } - - render(): TemplateResult { - return html`
- ${this.applicationTypes.map((type) => { - return html`
- { - this.host.steps = [ - "ak-application-wizard-initial", - "ak-application-wizard-type", - type.component, - ]; - this.host.isValid = true; - }} - /> - - ${type.description} -
`; - })} -
`; - } -} diff --git a/web/src/admin/applications/wizard/ak-application-wizard.ts b/web/src/admin/applications/wizard/ak-application-wizard.ts new file mode 100644 index 000000000..d15570f13 --- /dev/null +++ b/web/src/admin/applications/wizard/ak-application-wizard.ts @@ -0,0 +1,124 @@ +import { merge } from "@goauthentik/common/merge"; +import { AkWizard } from "@goauthentik/components/ak-wizard-main/AkWizard"; +import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"; + +import { ContextProvider } from "@lit-labs/context"; +import { msg } from "@lit/localize"; +import { customElement, state } from "lit/decorators.js"; + +import applicationWizardContext from "./ContextIdentity"; +import { newSteps } from "./steps"; +import { + ApplicationStep, + ApplicationWizardState, + ApplicationWizardStateUpdate, + OneOfProvider, +} from "./types"; + +const freshWizardState = () => ({ + providerModel: "", + app: {}, + provider: {}, +}); + +@customElement("ak-application-wizard") +export class ApplicationWizard extends CustomListenerElement( + AkWizard, +) { + constructor() { + super(msg("Create With Wizard"), msg("New application"), msg("Create a new application")); + this.steps = newSteps(); + } + + /** + * We're going to be managing the content of the forms by percolating all of the data up to this + * class, which will ultimately transmit all of it to the server as a transaction. The + * WizardFramework doesn't know anything about the nature of the data itself; it just forwards + * valid updates to us. So here we maintain a state object *and* update it so all child + * components can access the wizard state. + * + */ + @state() + wizardState: ApplicationWizardState = freshWizardState(); + + wizardStateProvider = new ContextProvider(this, { + context: applicationWizardContext, + initialValue: this.wizardState, + }); + + /** + * One of our steps has multiple display variants, one for each type of service provider. We + * want to *preserve* a customer's decisions about different providers; never make someone "go + * back and type it all back in," even if it's probably rare that someone will chose one + * provider, realize it's the wrong one, and go back to chose a different one, *and then go + * back*. Nonetheless, strive to *never* lose customer input. + * + */ + providerCache: Map = new Map(); + + maybeProviderSwap(providerModel: string | undefined): boolean { + if ( + providerModel === undefined || + typeof providerModel !== "string" || + providerModel === this.wizardState.providerModel + ) { + return false; + } + + this.providerCache.set(this.wizardState.providerModel, this.wizardState.provider); + const prevProvider = this.providerCache.get(providerModel); + this.wizardState.provider = prevProvider ?? { + name: `Provider for ${this.wizardState.app.name}`, + }; + const method = this.steps.find(({ id }) => id === "provider-details"); + if (!method) { + throw new Error("Could not find Authentication Method page?"); + } + method.disabled = false; + return true; + } + + // And this is where all the special cases go... + handleUpdate(detail: ApplicationWizardStateUpdate) { + if (detail.status === "submitted") { + this.step.valid = true; + this.requestUpdate(); + return; + } + + this.step.valid = this.step.valid || detail.status === "valid"; + + const update = detail.update; + if (!update) { + return; + } + + if (this.maybeProviderSwap(update.providerModel)) { + this.requestUpdate(); + } + + this.wizardState = merge(this.wizardState, update) as ApplicationWizardState; + this.wizardStateProvider.setValue(this.wizardState); + this.requestUpdate(); + } + + close() { + this.steps = newSteps(); + this.currentStep = 0; + this.wizardState = freshWizardState(); + this.providerCache = new Map(); + this.wizardStateProvider.setValue(this.wizardState); + this.frame.value!.open = false; + } + + handleNav(stepId: number | undefined) { + if (stepId === undefined || this.steps[stepId] === undefined) { + throw new Error(`Attempt to navigate to undefined step: ${stepId}`); + } + if (stepId > this.currentStep && !this.step.valid) { + return; + } + this.currentStep = stepId; + this.requestUpdate(); + } +} diff --git a/web/src/admin/applications/wizard/application/ak-application-wizard-application-details.ts b/web/src/admin/applications/wizard/application/ak-application-wizard-application-details.ts new file mode 100644 index 000000000..13e439d76 --- /dev/null +++ b/web/src/admin/applications/wizard/application/ak-application-wizard-application-details.ts @@ -0,0 +1,101 @@ +import { policyOptions } from "@goauthentik/admin/applications/ApplicationForm"; +import { first } from "@goauthentik/common/utils"; +import "@goauthentik/components/ak-radio-input"; +import "@goauthentik/components/ak-slug-input"; +import "@goauthentik/components/ak-switch-input"; +import "@goauthentik/components/ak-text-input"; +import "@goauthentik/elements/forms/FormGroup"; +import "@goauthentik/elements/forms/FormGroup"; +import "@goauthentik/elements/forms/HorizontalFormElement"; + +import { msg } from "@lit/localize"; +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { TemplateResult, html } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import BasePanel from "../BasePanel"; + +@customElement("ak-application-wizard-application-details") +export class ApplicationWizardApplicationDetails extends BasePanel { + handleChange(ev: Event) { + if (!ev.target) { + console.warn(`Received event with no target: ${ev}`); + return; + } + + const target = ev.target as HTMLInputElement; + const value = target.type === "checkbox" ? target.checked : target.value; + this.dispatchWizardUpdate({ + update: { + app: { + [target.name]: value, + }, + }, + status: this.form.checkValidity() ? "valid" : "invalid", + }); + } + + validator() { + return this.form.reportValidity(); + } + + render(): TemplateResult { + return html`
+ + + + + + ${msg("UI Settings")} +
+ + + +
+
+
`; + } +} + +export default ApplicationWizardApplicationDetails; diff --git a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts new file mode 100644 index 000000000..16b3ce01a --- /dev/null +++ b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts @@ -0,0 +1,161 @@ +import { msg } from "@lit/localize"; +import { TemplateResult, html } from "lit"; + +import type { ProviderModelEnum as ProviderModelEnumType, TypeCreate } from "@goauthentik/api"; +import { ProviderModelEnum, ProxyMode } from "@goauthentik/api"; +import type { + LDAPProviderRequest, + ModelRequest, + OAuth2ProviderRequest, + ProxyProviderRequest, + RadiusProviderRequest, + SAMLProviderRequest, + SCIMProviderRequest, +} from "@goauthentik/api"; + +import { OneOfProvider } from "../types"; + +type ProviderRenderer = () => TemplateResult; + +type ModelConverter = (provider: OneOfProvider) => ModelRequest; + +type ProviderType = [ + string, + string, + string, + ProviderRenderer, + ProviderModelEnumType, + ModelConverter, +]; + +export type LocalTypeCreate = TypeCreate & { + formName: string; + modelName: ProviderModelEnumType; + converter: ModelConverter; +}; + +// prettier-ignore +const _providerModelsTable: ProviderType[] = [ + [ + "oauth2provider", + msg("OAuth2/OpenID"), + msg("Modern applications, APIs and Single-page applications."), + () => html``, + ProviderModelEnum.Oauth2Oauth2provider, + (provider: OneOfProvider) => ({ + providerModel: ProviderModelEnum.Oauth2Oauth2provider, + ...(provider as OAuth2ProviderRequest), + }), + ], + [ + "ldapprovider", + msg("LDAP"), + msg("Provide an LDAP interface for applications and users to authenticate against."), + () => html``, + ProviderModelEnum.LdapLdapprovider, + (provider: OneOfProvider) => ({ + providerModel: ProviderModelEnum.LdapLdapprovider, + ...(provider as LDAPProviderRequest), + }), + + ], + [ + "proxyprovider-proxy", + msg("Transparent Reverse Proxy"), + msg("For transparent reverse proxies with required authentication"), + () => html``, + ProviderModelEnum.ProxyProxyprovider, + (provider: OneOfProvider) => ({ + providerModel: ProviderModelEnum.ProxyProxyprovider, + ...(provider as ProxyProviderRequest), + mode: ProxyMode.Proxy, + }), + ], + [ + "proxyprovider-forwardsingle", + msg("Forward Auth Single Application"), + msg("For nginx's auth_request or traefix's forwardAuth"), + () => html``, + ProviderModelEnum.ProxyProxyprovider , + (provider: OneOfProvider) => ({ + providerModel: ProviderModelEnum.ProxyProxyprovider, + ...(provider as ProxyProviderRequest), + mode: ProxyMode.ForwardSingle, + }), + + ], + [ + "proxyprovider-forwarddomain", + msg("Forward Auth Domain Level"), + msg("For nginx's auth_request or traefix's forwardAuth per root domain"), + () => html``, + ProviderModelEnum.ProxyProxyprovider , + (provider: OneOfProvider) => ({ + providerModel: ProviderModelEnum.ProxyProxyprovider, + ...(provider as ProxyProviderRequest), + mode: ProxyMode.ForwardDomain, + }), + ], + [ + "samlprovider", + msg("SAML Configuration"), + msg("Configure SAML provider manually"), + () => html``, + ProviderModelEnum.SamlSamlprovider, + (provider: OneOfProvider) => ({ + providerModel: ProviderModelEnum.SamlSamlprovider, + ...(provider as SAMLProviderRequest), + }), + + ], + [ + "radiusprovider", + msg("RADIUS Configuration"), + msg("Configure RADIUS provider manually"), + () => html``, + ProviderModelEnum.RadiusRadiusprovider, + (provider: OneOfProvider) => ({ + providerModel: ProviderModelEnum.RadiusRadiusprovider, + ...(provider as RadiusProviderRequest), + }), + + ], + [ + "scimprovider", + msg("SCIM configuration"), + msg("Configure SCIM provider manually"), + () => html``, + ProviderModelEnum.ScimScimprovider, + (provider: OneOfProvider) => ({ + providerModel: ProviderModelEnum.ScimScimprovider, + ...(provider as SCIMProviderRequest), + }), + + ], +]; + +function mapProviders([ + formName, + name, + description, + _, + modelName, + converter, +]: ProviderType): LocalTypeCreate { + return { + formName, + name, + description, + component: "", + modelName, + converter, + }; +} + +export const providerModelsList = _providerModelsTable.map(mapProviders); + +export const providerRendererList = new Map( + _providerModelsTable.map(([modelName, _0, _1, renderer]) => [modelName, renderer]), +); + +export default providerModelsList; diff --git a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts new file mode 100644 index 000000000..593406d66 --- /dev/null +++ b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts @@ -0,0 +1,68 @@ +import "@goauthentik/components/ak-radio-input"; +import "@goauthentik/components/ak-switch-input"; +import "@goauthentik/components/ak-text-input"; +import "@goauthentik/elements/forms/FormGroup"; +import "@goauthentik/elements/forms/FormGroup"; +import "@goauthentik/elements/forms/HorizontalFormElement"; + +import { msg } from "@lit/localize"; +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { html } from "lit"; +import { map } from "lit/directives/map.js"; + +import BasePanel from "../BasePanel"; +import providerModelsList from "./ak-application-wizard-authentication-method-choice.choices"; +import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices"; + +@customElement("ak-application-wizard-authentication-method-choice") +export class ApplicationWizardAuthenticationMethodChoice extends BasePanel { + constructor() { + super(); + this.handleChoice = this.handleChoice.bind(this); + this.renderProvider = this.renderProvider.bind(this); + } + + handleChoice(ev: InputEvent) { + const target = ev.target as HTMLInputElement; + this.dispatchWizardUpdate({ + update: { providerModel: target.value }, + status: this.validator() ? "valid" : "invalid", + }); + } + + validator() { + const radios = Array.from(this.form.querySelectorAll('input[type="radio"]')); + const chosen = radios.find( + (radio: Element) => radio instanceof HTMLInputElement && radio.checked, + ); + return !!chosen; + } + + renderProvider(type: LocalTypeCreate) { + const method = this.wizard.providerModel; + + return html`
+ + + ${type.description} +
`; + } + + render() { + return providerModelsList.length > 0 + ? html`
+ ${map(providerModelsList, this.renderProvider)} +
` + : html``; + } +} + +export default ApplicationWizardAuthenticationMethodChoice; diff --git a/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts b/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts new file mode 100644 index 000000000..aa21bd073 --- /dev/null +++ b/web/src/admin/applications/wizard/commit/ak-application-wizard-commit-application.ts @@ -0,0 +1,202 @@ +import { EVENT_REFRESH } from "@goauthentik/app/common/constants"; +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import "@goauthentik/components/ak-radio-input"; +import "@goauthentik/components/ak-switch-input"; +import "@goauthentik/components/ak-text-input"; +import "@goauthentik/elements/forms/FormGroup"; +import "@goauthentik/elements/forms/FormGroup"; +import "@goauthentik/elements/forms/HorizontalFormElement"; + +import { msg } from "@lit/localize"; +import { customElement, state } from "@lit/reactive-element/decorators.js"; +import { TemplateResult, css, html, nothing } from "lit"; +import { classMap } from "lit/directives/class-map.js"; + +import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css"; +import PFProgressStepper from "@patternfly/patternfly/components/ProgressStepper/progress-stepper.css"; +import PFTitle from "@patternfly/patternfly/components/Title/title.css"; +import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css"; + +import { + ApplicationRequest, + CoreApi, + TransactionApplicationRequest, + TransactionApplicationResponse, +} from "@goauthentik/api"; +import type { ModelRequest } from "@goauthentik/api"; + +import BasePanel from "../BasePanel"; +import providerModelsList from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices"; + +function cleanApplication(app: Partial): ApplicationRequest { + return { + name: "", + slug: "", + ...app, + }; +} + +type ProviderModelType = Exclude; + +type State = { + state: "idle" | "running" | "error" | "success"; + label: string | TemplateResult; + icon: string[]; +}; + +const idleState: State = { + state: "idle", + label: "", + icon: ["fa-cogs", "pf-m-pending"], +}; + +const runningState: State = { + state: "running", + label: msg("Saving Application..."), + icon: ["fa-cogs", "pf-m-info"], +}; +const errorState: State = { + state: "error", + label: msg("Authentik was unable to save this application:"), + icon: ["fa-times-circle", "pf-m-danger"], +}; + +const successState: State = { + state: "success", + label: msg("Your application has been saved"), + icon: ["fa-check-circle", "pf-m-success"], +}; + +@customElement("ak-application-wizard-commit-application") +export class ApplicationWizardCommitApplication extends BasePanel { + static get styles() { + return [ + ...super.styles, + PFBullseye, + PFEmptyState, + PFTitle, + PFProgressStepper, + css` + .pf-c-title { + padding-bottom: var(--pf-global--spacer--md); + } + `, + ]; + } + + @state() + commitState: State = idleState; + + @state() + errors: string[] = []; + + response?: TransactionApplicationResponse; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + willUpdate(_changedProperties: Map) { + if (this.commitState === idleState) { + this.response = undefined; + this.commitState = runningState; + const providerModel = providerModelsList.find( + ({ formName }) => formName === this.wizard.providerModel, + ); + if (!providerModel) { + throw new Error( + `Could not determine provider model from user request: ${JSON.stringify( + this.wizard, + null, + 2, + )}`, + ); + } + + const request: TransactionApplicationRequest = { + providerModel: providerModel.modelName as ProviderModelType, + app: cleanApplication(this.wizard.app), + provider: providerModel.converter(this.wizard.provider), + }; + + this.send(request); + return; + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + decodeErrors(body: Record) { + const spaceify = (src: Record) => + Object.values(src).map((msg) => `\u00a0\u00a0\u00a0\u00a0${msg}`); + + let errs: string[] = []; + if (body["app"] !== undefined) { + errs = [...errs, msg("In the Application:"), ...spaceify(body["app"])]; + } + if (body["provider"] !== undefined) { + errs = [...errs, msg("In the Provider:"), ...spaceify(body["provider"])]; + } + console.log(body, errs); + return errs; + } + + async send( + data: TransactionApplicationRequest, + ): Promise { + this.errors = []; + + new CoreApi(DEFAULT_CONFIG) + .coreTransactionalApplicationsUpdate({ + transactionApplicationRequest: data, + }) + .then((response: TransactionApplicationResponse) => { + this.response = response; + this.dispatchCustomEvent(EVENT_REFRESH); + this.dispatchWizardUpdate({ status: "submitted" }); + this.commitState = successState; + }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .catch((resolution: any) => { + resolution.response.json().then( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (body: Record) => { + this.errors = this.decodeErrors(body); + }, + ); + this.commitState = errorState; + }); + } + + render(): TemplateResult { + const icon = classMap( + this.commitState.icon.reduce((acc, icon) => ({ ...acc, [icon]: true }), {}), + ); + + return html` +
+
+
+
+ +

+ ${this.commitState.label} +

+ ${this.errors.length > 0 + ? html`
    + ${this.errors.map( + (msg) => html`
  • ${msg}
  • `, + )} +
` + : nothing} +
+
+
+
+ `; + } +} + +export default ApplicationWizardCommitApplication; diff --git a/web/src/admin/applications/wizard/ldap/TypeLDAPApplicationWizardPage.ts b/web/src/admin/applications/wizard/ldap/TypeLDAPApplicationWizardPage.ts deleted file mode 100644 index 89c4a5ec8..000000000 --- a/web/src/admin/applications/wizard/ldap/TypeLDAPApplicationWizardPage.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { KeyUnknown } from "@goauthentik/elements/forms/Form"; -import "@goauthentik/elements/forms/HorizontalFormElement"; -import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; - -import { msg } from "@lit/localize"; -import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; -import { TemplateResult, html } from "lit"; - -import { - CoreApi, - FlowDesignationEnum, - FlowsApi, - LDAPProviderRequest, - ProvidersApi, - UserServiceAccountResponse, -} from "@goauthentik/api"; - -@customElement("ak-application-wizard-type-ldap") -export class TypeLDAPApplicationWizardPage extends WizardFormPage { - sidebarLabel = () => msg("LDAP details"); - - nextDataCallback = async (data: KeyUnknown): Promise => { - let name = this.host.state["name"] as string; - // Check if a provider with the name already exists - const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({ - search: name, - }); - if (providers.results.filter((provider) => provider.name == name)) { - name += "-1"; - } - this.host.addActionBefore(msg("Create service account"), async (): Promise => { - const serviceAccount = await new CoreApi(DEFAULT_CONFIG).coreUsersServiceAccountCreate({ - userServiceAccountRequest: { - name: name, - createGroup: true, - }, - }); - this.host.state["serviceAccount"] = serviceAccount; - return true; - }); - this.host.addActionBefore(msg("Create provider"), async (): Promise => { - // Get all flows and default to the implicit authorization - const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ - designation: FlowDesignationEnum.Authorization, - ordering: "slug", - }); - const serviceAccount = this.host.state["serviceAccount"] as UserServiceAccountResponse; - const req: LDAPProviderRequest = { - name: name, - authorizationFlow: flows.results[0].pk, - baseDn: data.baseDN as string, - searchGroup: serviceAccount.groupPk, - }; - const provider = await new ProvidersApi(DEFAULT_CONFIG).providersLdapCreate({ - lDAPProviderRequest: req, - }); - this.host.state["provider"] = provider; - return true; - }); - return true; - }; - - renderForm(): TemplateResult { - const domainParts = window.location.hostname.split("."); - const defaultBaseDN = domainParts.map((part) => `dc=${part}`).join(","); - return html`
- - - -
`; - } -} diff --git a/web/src/admin/applications/wizard/link/TypeLinkApplicationWizardPage.ts b/web/src/admin/applications/wizard/link/TypeLinkApplicationWizardPage.ts deleted file mode 100644 index c91133a45..000000000 --- a/web/src/admin/applications/wizard/link/TypeLinkApplicationWizardPage.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { KeyUnknown } from "@goauthentik/elements/forms/Form"; -import "@goauthentik/elements/forms/HorizontalFormElement"; -import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; - -import { msg } from "@lit/localize"; -import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; -import { TemplateResult, html } from "lit"; - -@customElement("ak-application-wizard-type-link") -export class TypeLinkApplicationWizardPage extends WizardFormPage { - sidebarLabel = () => msg("Application Link"); - - nextDataCallback = async (data: KeyUnknown): Promise => { - this.host.state["link"] = data.link; - return true; - }; - - renderForm(): TemplateResult { - return html` -
- - -

- ${msg("URL which will be opened when a user clicks on the application.")} -

-
-
- `; - } -} diff --git a/web/src/admin/applications/wizard/methods/BaseProviderPanel.ts b/web/src/admin/applications/wizard/methods/BaseProviderPanel.ts new file mode 100644 index 000000000..ce60213e1 --- /dev/null +++ b/web/src/admin/applications/wizard/methods/BaseProviderPanel.ts @@ -0,0 +1,26 @@ +import BasePanel from "../BasePanel"; + +export class ApplicationWizardProviderPageBase extends BasePanel { + handleChange(ev: InputEvent) { + if (!ev.target) { + console.warn(`Received event with no target: ${ev}`); + return; + } + const target = ev.target as HTMLInputElement; + const value = target.type === "checkbox" ? target.checked : target.value; + this.dispatchWizardUpdate({ + update: { + provider: { + [target.name]: value, + }, + }, + status: this.form.checkValidity() ? "valid" : "invalid", + }); + } + + validator() { + return this.form.reportValidity(); + } +} + +export default ApplicationWizardProviderPageBase; diff --git a/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts b/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts new file mode 100644 index 000000000..9b7e813bf --- /dev/null +++ b/web/src/admin/applications/wizard/methods/ak-application-wizard-authentication-method.ts @@ -0,0 +1,29 @@ +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; + +import BasePanel from "../BasePanel"; +import { providerRendererList } from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices"; +import "./ldap/ak-application-wizard-authentication-by-ldap"; +import "./oauth/ak-application-wizard-authentication-by-oauth"; +import "./proxy/ak-application-wizard-authentication-for-forward-domain-proxy"; +import "./proxy/ak-application-wizard-authentication-for-reverse-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 "./scim/ak-application-wizard-authentication-by-scim"; + +// prettier-ignore + +@customElement("ak-application-wizard-authentication-method") +export class ApplicationWizardApplicationDetails extends BasePanel { + render() { + const handler = providerRendererList.get(this.wizard.providerModel); + if (!handler) { + throw new Error( + "Unrecognized authentication method in ak-application-wizard-authentication-method", + ); + } + return handler(); + } +} + +export default ApplicationWizardApplicationDetails; diff --git a/web/src/admin/applications/wizard/methods/ldap/LDAPOptionsAndHelp.ts b/web/src/admin/applications/wizard/methods/ldap/LDAPOptionsAndHelp.ts new file mode 100644 index 000000000..5b2f1f483 --- /dev/null +++ b/web/src/admin/applications/wizard/methods/ldap/LDAPOptionsAndHelp.ts @@ -0,0 +1,64 @@ +import { msg } from "@lit/localize"; +import { html } from "lit"; + +import { LDAPAPIAccessMode } from "@goauthentik/api"; + +export const bindModeOptions = [ + { + label: msg("Cached binding"), + value: LDAPAPIAccessMode.Cached, + default: true, + description: html`${msg( + "Flow is executed and session is cached in memory. Flow is executed when session expires", + )}`, + }, + { + label: msg("Direct binding"), + value: LDAPAPIAccessMode.Direct, + description: html`${msg( + "Always execute the configured bind flow to authenticate the user", + )}`, + }, +]; + +export const searchModeOptions = [ + { + label: msg("Cached querying"), + value: LDAPAPIAccessMode.Cached, + default: true, + description: html`${msg( + "The outpost holds all users and groups in-memory and will refresh every 5 Minutes", + )}`, + }, + { + label: msg("Direct querying"), + value: LDAPAPIAccessMode.Direct, + description: html`${msg( + "Always returns the latest data, but slower than cached querying", + )}`, + }, +]; + +export const mfaSupportHelp = msg( + "When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.", +); + +export const groupHelp = msg( + "The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber", +); + +export const cryptoCertificateHelp = msg( + "The certificate for the above configured Base DN. As a fallback, the provider uses a self-signed certificate.", +); + +export const tlsServerNameHelp = msg( + "DNS name for which the above configured certificate should be used. The certificate cannot be detected based on the base DN, as the SSL/TLS negotiation happens before such data is exchanged.", +); + +export const uidStartNumberHelp = msg( + "The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber", +); + +export const gidStartNumberHelp = msg( + "The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber", +); diff --git a/web/src/admin/applications/wizard/methods/ldap/ak-application-wizard-authentication-by-ldap.ts b/web/src/admin/applications/wizard/methods/ldap/ak-application-wizard-authentication-by-ldap.ts new file mode 100644 index 000000000..2349af45b --- /dev/null +++ b/web/src/admin/applications/wizard/methods/ldap/ak-application-wizard-authentication-by-ldap.ts @@ -0,0 +1,146 @@ +import "@goauthentik/admin/common/ak-core-group-search"; +import "@goauthentik/admin/common/ak-crypto-certificate-search"; +import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search"; +import { first } from "@goauthentik/common/utils"; +import "@goauthentik/components/ak-number-input"; +import "@goauthentik/components/ak-radio-input"; +import "@goauthentik/components/ak-switch-input"; +import "@goauthentik/components/ak-text-input"; +import { 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/custom-element.js"; +import { html, nothing } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { FlowsInstancesListDesignationEnum } from "@goauthentik/api"; +import type { LDAPProvider } from "@goauthentik/api"; + +import BaseProviderPanel from "../BaseProviderPanel"; +import { + bindModeOptions, + cryptoCertificateHelp, + gidStartNumberHelp, + groupHelp, + mfaSupportHelp, + searchModeOptions, + tlsServerNameHelp, + uidStartNumberHelp, +} from "./LDAPOptionsAndHelp"; + +@customElement("ak-application-wizard-authentication-by-ldap") +export class ApplicationWizardApplicationDetails extends BaseProviderPanel { + render() { + const provider = this.wizard.provider as LDAPProvider | undefined; + + return html`
+ + + + +

${msg("Flow used for users to authenticate.")}

+
+ + + +

${groupHelp}

+
+ + + + + + + + + + + + ${msg("Protocol settings")} +
+ + + + + + +

${cryptoCertificateHelp}

+
+ + + + + + +
+
+
`; + } +} + +export default ApplicationWizardApplicationDetails; diff --git a/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts b/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts new file mode 100644 index 000000000..cf6ceb9de --- /dev/null +++ b/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts @@ -0,0 +1,301 @@ +import "@goauthentik/admin/common/ak-crypto-certificate-search"; +import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search"; +import { + clientTypeOptions, + issuerModeOptions, + redirectUriHelp, + subjectModeOptions, +} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm"; +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils"; +import "@goauthentik/components/ak-number-input"; +import "@goauthentik/components/ak-radio-input"; +import "@goauthentik/components/ak-switch-input"; +import "@goauthentik/components/ak-text-input"; +import "@goauthentik/components/ak-textarea-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, nothing } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { + ClientTypeEnum, + FlowsInstancesListDesignationEnum, + PropertymappingsApi, + SourcesApi, +} from "@goauthentik/api"; +import type { + OAuth2Provider, + PaginatedOAuthSourceList, + PaginatedScopeMappingList, +} from "@goauthentik/api"; + +import BaseProviderPanel from "../BaseProviderPanel"; + +@customElement("ak-application-wizard-authentication-by-oauth") +export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel { + @state() + showClientSecret = false; + + @state() + propertyMappings?: PaginatedScopeMappingList; + + @state() + oauthSources?: PaginatedOAuthSourceList; + + constructor() { + super(); + new PropertymappingsApi(DEFAULT_CONFIG) + .propertymappingsScopeList({ + ordering: "scope_name", + }) + .then((propertyMappings: PaginatedScopeMappingList) => { + this.propertyMappings = propertyMappings; + }); + + new SourcesApi(DEFAULT_CONFIG) + .sourcesOauthList({ + ordering: "name", + hasJwks: true, + }) + .then((oauthSources: PaginatedOAuthSourceList) => { + this.oauthSources = oauthSources; + }); + } + + render() { + const provider = this.wizard.provider as OAuth2Provider | undefined; + + return html`
+ + + + +

+ ${msg("Flow used when a user access this provider and is not authenticated.")} +

+
+ + + +

+ ${msg("Flow used when authorizing this provider.")} +

+
+ + + ${msg("Protocol settings")} +
+ ) => { + this.showClientSecret = ev.detail !== ClientTypeEnum.Public; + }} + .options=${clientTypeOptions} + > + + + + + + + + + + + + + + +

${msg("Key used to sign the tokens.")}

+
+
+
+ + + ${msg("Advanced protocol settings")} +
+ + ${msg("Configure how long access codes are valid for.")} +

+ `} + > +
+ + + ${msg("Configure how long access tokens are valid for.")} +

+ `} + > +
+ + + ${msg("Configure how long refresh tokens are valid for.")} +

+ `} + > +
+ + + +

+ ${msg( + "Select which scopes can be used by the client. The client still has to specify the scope to access the data.", + )} +

+

+ ${msg("Hold control/command to select multiple items.")} +

+
+ + + + + label=${msg("Include claims in id_token")} + ?checked=${first(provider?.includeClaimsInIdToken, true)} + help=${msg( + "Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.", + )}> + + +
+
+ + + ${msg("Machine-to-Machine authentication settings")} +
+ + +

+ ${msg( + "JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.", + )} +

+

+ ${msg("Hold control/command to select multiple items.")} +

+
+
+
+
`; + } +} + +export default ApplicationWizardAuthenticationByOauth; diff --git a/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts b/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts new file mode 100644 index 000000000..4b32fe2d7 --- /dev/null +++ b/web/src/admin/applications/wizard/methods/proxy/AuthenticationByProxyPage.ts @@ -0,0 +1,255 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { first } from "@goauthentik/common/utils"; +import "@goauthentik/components/ak-switch-input"; +import "@goauthentik/components/ak-text-input"; +import "@goauthentik/components/ak-textarea-input"; +import "@goauthentik/components/ak-toggle-group"; +import "@goauthentik/elements/forms/HorizontalFormElement"; + +import { msg } from "@lit/localize"; +import { state } from "@lit/reactive-element/decorators.js"; +import { TemplateResult, html, nothing } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { + FlowsInstancesListDesignationEnum, + PaginatedOAuthSourceList, + PaginatedScopeMappingList, + PropertymappingsApi, + ProxyMode, + ProxyProvider, + SourcesApi, +} from "@goauthentik/api"; + +import BaseProviderPanel from "../BaseProviderPanel"; + +type MaybeTemplateResult = TemplateResult | typeof nothing; + +export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel { + constructor() { + super(); + new PropertymappingsApi(DEFAULT_CONFIG) + .propertymappingsScopeList({ ordering: "scope_name" }) + .then((propertyMappings: PaginatedScopeMappingList) => { + this.propertyMappings = propertyMappings; + }); + + new SourcesApi(DEFAULT_CONFIG) + .sourcesOauthList({ + ordering: "name", + hasJwks: true, + }) + .then((oauthSources: PaginatedOAuthSourceList) => { + this.oauthSources = oauthSources; + }); + } + + propertyMappings?: PaginatedScopeMappingList; + oauthSources?: PaginatedOAuthSourceList; + + @state() + showHttpBasic = true; + + @state() + mode: ProxyMode = ProxyMode.Proxy; + + get instance(): ProxyProvider | undefined { + return this.wizard.provider as ProxyProvider; + } + + renderModeDescription(): MaybeTemplateResult { + return nothing; + } + + renderProxyMode() { + return html`

This space intentionally left blank

`; + } + + renderHttpBasic(): TemplateResult { + return html` + + + + `; + } + + render() { + return html`
+ ${this.renderModeDescription()} + + + + +

+ ${msg("Flow used when a user access this provider and is not authenticated.")} +

+
+ + + +

+ ${msg("Flow used when authorizing this provider.")} +

+
+ + ${this.renderProxyMode()} + + + + + ${msg("Advanced protocol settings")} +
+ + + + + + +

+ ${msg("Additional scope mappings, which are passed to the proxy.")} +

+

+ ${msg("Hold control/command to select multiple items.")} +

+
+ + + ${msg( + "Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.", + )} +

+

+ ${msg( + "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.", + )} +

`} + > +
+
+
+ + ${msg("Authentication settings")} +
+ + + { + const el = ev.target as HTMLInputElement; + this.showHttpBasic = el.checked; + }} + label=${msg("Send HTTP-Basic Authentication")} + help=${msg( + "Send a custom HTTP-Basic Authentication header based on values from authentik.", + )} + > + + ${this.showHttpBasic ? this.renderHttpBasic() : html``} + + + +

+ ${msg( + "JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.", + )} +

+

+ ${msg("Hold control/command to select multiple items.")} +

+
+
+
+
`; + } +} + +export default AkTypeProxyApplicationWizardPage; diff --git a/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-forward-domain-proxy.ts b/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-forward-domain-proxy.ts new file mode 100644 index 000000000..e794cd7d4 --- /dev/null +++ b/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-forward-domain-proxy.ts @@ -0,0 +1,55 @@ +import "@goauthentik/components/ak-text-input"; + +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 AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage"; + +@customElement("ak-application-wizard-authentication-for-forward-proxy-domain") +export class AkForwardDomainProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage { + renderModeDescription() { + return html`

+ ${msg( + "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.", + )} +

+
+ ${msg("An example setup can look like this:")} +
    +
  • ${msg("authentik running on auth.example.com")}
  • +
  • ${msg("app1 running on app1.example.com")}
  • +
+ ${msg( + "In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.", + )} +
`; + } + + renderProxyMode() { + return html` + + + + `; + } +} + +export default AkForwardDomainProxyApplicationWizardPage; diff --git a/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-reverse-proxy.ts b/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-reverse-proxy.ts new file mode 100644 index 000000000..b82d1e538 --- /dev/null +++ b/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-reverse-proxy.ts @@ -0,0 +1,49 @@ +import { first } from "@goauthentik/common/utils"; +import "@goauthentik/components/ak-switch-input"; +import "@goauthentik/components/ak-text-input"; + +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 AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage"; + +@customElement("ak-application-wizard-authentication-for-reverse-proxy") +export class AkReverseProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage { + renderModeDescription() { + return html`

+ ${msg( + "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.", + )} +

`; + } + + renderProxyMode() { + return html` + + + `; + } +} + +export default AkReverseProxyApplicationWizardPage; diff --git a/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-single-forward-proxy.ts b/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-single-forward-proxy.ts new file mode 100644 index 000000000..0840c698f --- /dev/null +++ b/web/src/admin/applications/wizard/methods/proxy/ak-application-wizard-authentication-for-single-forward-proxy.ts @@ -0,0 +1,36 @@ +import "@goauthentik/components/ak-text-input"; + +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 AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage"; + +@customElement("ak-application-wizard-authentication-for-single-forward-proxy") +export class AkForwardSingleProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage { + renderModeDescription() { + return html`

+ ${msg( + html`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 managed outpost, this is done for you).`, + )} +

`; + } + + renderProxyMode() { + return html``; + } +} + +export default AkForwardSingleProxyApplicationWizardPage; diff --git a/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts b/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts new file mode 100644 index 000000000..d107eab0f --- /dev/null +++ b/web/src/admin/applications/wizard/methods/radius/ak-application-wizard-authentication-by-radius.ts @@ -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`
+ + + + + +

${msg("Flow used for users to authenticate.")}

+
+ + + ${msg("Protocol settings")} +
+ + +
+
+
`; + } +} + +export default ApplicationWizardAuthenticationByRadius; diff --git a/web/src/admin/applications/wizard/methods/saml/SamlProviderOptions.ts b/web/src/admin/applications/wizard/methods/saml/SamlProviderOptions.ts new file mode 100644 index 000000000..a1b6d44a2 --- /dev/null +++ b/web/src/admin/applications/wizard/methods/saml/SamlProviderOptions.ts @@ -0,0 +1,33 @@ +import { msg } from "@lit/localize"; + +import { DigestAlgorithmEnum, SignatureAlgorithmEnum, SpBindingEnum } from "@goauthentik/api"; + +type Option = [string, T, boolean?]; + +function toOptions(options: Option[]) { + return options.map(([label, value, isDefault]: Option) => ({ + label, + value, + default: isDefault ?? false, + })); +} + +export const spBindingOptions = toOptions([ + [msg("Redirect"), SpBindingEnum.Redirect, true], + [msg("Post"), SpBindingEnum.Post], +]); + +export const digestAlgorithmOptions = toOptions([ + ["SHA1", DigestAlgorithmEnum._200009Xmldsigsha1], + ["SHA256", DigestAlgorithmEnum._200104Xmlencsha256, true], + ["SHA384", DigestAlgorithmEnum._200104XmldsigMoresha384], + ["SHA512", DigestAlgorithmEnum._200104Xmlencsha512], +]); + +export const signatureAlgorithmOptions = toOptions([ + ["RSA-SHA1", SignatureAlgorithmEnum._200009XmldsigrsaSha1], + ["RSA-SHA256", SignatureAlgorithmEnum._200104XmldsigMorersaSha256, true], + ["RSA-SHA384", SignatureAlgorithmEnum._200104XmldsigMorersaSha384], + ["RSA-SHA512", SignatureAlgorithmEnum._200104XmldsigMorersaSha512], + ["DSA-SHA1", SignatureAlgorithmEnum._200009XmldsigdsaSha1], +]); diff --git a/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts b/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts new file mode 100644 index 000000000..f25c995ef --- /dev/null +++ b/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts @@ -0,0 +1,250 @@ +import "@goauthentik/admin/common/ak-core-group-search"; +import "@goauthentik/admin/common/ak-crypto-certificate-search"; +import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search"; +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import "@goauthentik/components/ak-number-input"; +import "@goauthentik/components/ak-radio-input"; +import "@goauthentik/components/ak-switch-input"; +import "@goauthentik/components/ak-text-input"; +import "@goauthentik/elements/forms/FormGroup"; +import "@goauthentik/elements/forms/HorizontalFormElement"; + +import { msg } from "@lit/localize"; +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { html } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { + FlowsInstancesListDesignationEnum, + PaginatedSAMLPropertyMappingList, + PropertymappingsApi, + SAMLProvider, +} from "@goauthentik/api"; + +import BaseProviderPanel from "../BaseProviderPanel"; +import { + digestAlgorithmOptions, + signatureAlgorithmOptions, + spBindingOptions, +} from "./SamlProviderOptions"; + +@customElement("ak-application-wizard-authentication-by-saml-configuration") +export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPanel { + propertyMappings?: PaginatedSAMLPropertyMappingList; + + constructor() { + super(); + new PropertymappingsApi(DEFAULT_CONFIG) + .propertymappingsSamlList({ + ordering: "saml_name", + }) + .then((propertyMappings: PaginatedSAMLPropertyMappingList) => { + this.propertyMappings = propertyMappings; + }); + } + + render() { + const provider = this.wizard.provider as SAMLProvider | undefined; + + return html`
+ + + + +

+ ${msg("Flow used when a user access this provider and is not authenticated.")} +

+
+ + + +

+ ${msg("Flow used when authorizing this provider.")} +

+
+ + + ${msg("Protocol settings")} +
+ + + + + + + + +
+
+ + + ${msg("Advanced protocol settings")} +
+ + +

+ ${msg( + "Certificate used to sign outgoing Responses going to the Service Provider.", + )} +

+
+ + + +

+ ${msg( + "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.", + )} +

+
+ + + +

+ ${msg("Hold control/command to select multiple items.")} +

+
+ + + +

+ ${msg( + "Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.", + )} +

+
+ + + + + + + + + + + + +
+
+
`; + } +} + +export default ApplicationWizardProviderSamlConfiguration; diff --git a/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-import.ts b/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-import.ts new file mode 100644 index 000000000..924aead76 --- /dev/null +++ b/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-import.ts @@ -0,0 +1,81 @@ +import "@goauthentik/admin/common/ak-flow-search/ak-flow-search-no-default"; +import "@goauthentik/components/ak-file-input"; +import { AkFileInput } from "@goauthentik/components/ak-file-input"; +import "@goauthentik/components/ak-text-input"; +import "@goauthentik/elements/forms/HorizontalFormElement"; + +import { msg } from "@lit/localize"; +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { html } from "lit"; +import { query } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { + FlowsInstancesListDesignationEnum, + ProvidersSamlImportMetadataCreateRequest, +} from "@goauthentik/api"; + +import BaseProviderPanel from "../BaseProviderPanel"; + +@customElement("ak-application-wizard-authentication-by-saml-import") +export class ApplicationWizardProviderSamlImport extends BaseProviderPanel { + @query('ak-file-input[name="metadata"]') + fileInput!: AkFileInput; + + handleChange(ev: InputEvent) { + if (!ev.target) { + console.warn(`Received event with no target: ${ev}`); + return; + } + const target = ev.target as HTMLInputElement; + if (target.type === "file") { + const file = this.fileInput.files?.[0] ?? null; + if (file) { + this.dispatchWizardUpdate({ + update: { + provider: { + file, + }, + }, + status: this.form.checkValidity() ? "valid" : "invalid", + }); + } + return; + } + super.handleChange(ev); + } + + render() { + const provider = this.wizard.provider as + | ProvidersSamlImportMetadataCreateRequest + | undefined; + + return html`
+ + + + +

+ ${msg("Flow used when authorizing this provider.")} +

+
+ + +
`; + } +} + +export default ApplicationWizardProviderSamlImport; diff --git a/web/src/admin/applications/wizard/methods/saml/saml-property-mappings-search.ts b/web/src/admin/applications/wizard/methods/saml/saml-property-mappings-search.ts new file mode 100644 index 000000000..27b1d53ab --- /dev/null +++ b/web/src/admin/applications/wizard/methods/saml/saml-property-mappings-search.ts @@ -0,0 +1,112 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { AKElement } from "@goauthentik/elements/Base"; +import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect"; +import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"; + +import { html } from "lit"; +import { customElement } from "lit/decorators.js"; +import { property, query } from "lit/decorators.js"; + +import { + PropertymappingsApi, + PropertymappingsSamlListRequest, + SAMLPropertyMapping, +} from "@goauthentik/api"; + +async function fetchObjects(query?: string): Promise { + const args: PropertymappingsSamlListRequest = { + ordering: "saml_name", + }; + if (query !== undefined) { + args.search = query; + } + const items = await new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSamlList(args); + return items.results; +} + +function renderElement(item: SAMLPropertyMapping): string { + return item.name; +} + +function renderValue(item: SAMLPropertyMapping | undefined): string | undefined { + return item?.pk; +} + +/** + * SAML Property Mapping Search + * + * @element ak-saml-property-mapping-search + * + * A wrapper around SearchSelect for the SAML Property Search. It's a unique search, but for the + * purpose of the form all you need to know is that it is being searched and selected. Let's put the + * how somewhere else. + * + */ + +@customElement("ak-saml-property-mapping-search") +export class SAMLPropertyMappingSearch extends CustomListenerElement(AKElement) { + /** + * The current property mapping known to the caller. + * + * @attr + */ + @property({ type: String, reflect: true, attribute: "propertymapping" }) + propertyMapping?: string; + + @query("ak-search-select") + search!: SearchSelect; + + @property({ type: String }) + name: string | null | undefined; + + selectedPropertyMapping?: SAMLPropertyMapping; + + constructor() { + super(); + this.selected = this.selected.bind(this); + this.handleSearchUpdate = this.handleSearchUpdate.bind(this); + this.addCustomListener("ak-change", this.handleSearchUpdate); + } + + get value() { + return this.selectedPropertyMapping ? renderValue(this.selectedPropertyMapping) : undefined; + } + + connectedCallback() { + super.connectedCallback(); + const horizontalContainer = this.closest("ak-form-element-horizontal[name]"); + if (!horizontalContainer) { + throw new Error("This search can only be used in a named ak-form-element-horizontal"); + } + const name = horizontalContainer.getAttribute("name"); + const myName = this.getAttribute("name"); + if (name !== null && name !== myName) { + this.setAttribute("name", name); + } + } + + handleSearchUpdate(ev: CustomEvent) { + ev.stopPropagation(); + this.selectedPropertyMapping = ev.detail.value; + this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true })); + } + + selected(item: SAMLPropertyMapping): boolean { + return this.propertyMapping === item.pk; + } + + render() { + return html` + + + `; + } +} + +export default SAMLPropertyMappingSearch; diff --git a/web/src/admin/applications/wizard/methods/scim/ak-application-wizard-authentication-by-scim.ts b/web/src/admin/applications/wizard/methods/scim/ak-application-wizard-authentication-by-scim.ts new file mode 100644 index 000000000..493c740d1 --- /dev/null +++ b/web/src/admin/applications/wizard/methods/scim/ak-application-wizard-authentication-by-scim.ts @@ -0,0 +1,189 @@ +import "@goauthentik/admin/common/ak-crypto-certificate-search"; +import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search"; +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { first } from "@goauthentik/common/utils"; +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 { + CoreApi, + CoreGroupsListRequest, + type Group, + PaginatedSCIMMappingList, + PropertymappingsApi, + type SCIMProvider, +} from "@goauthentik/api"; + +import BaseProviderPanel from "../BaseProviderPanel"; + +@customElement("ak-application-wizard-authentication-by-scim") +export class ApplicationWizardAuthenticationBySCIM extends BaseProviderPanel { + @state() + propertyMappings?: PaginatedSCIMMappingList; + + constructor() { + super(); + new PropertymappingsApi(DEFAULT_CONFIG) + .propertymappingsScopeList({ + ordering: "scope_name", + }) + .then((propertyMappings: PaginatedSCIMMappingList) => { + this.propertyMappings = propertyMappings; + }); + } + + render() { + const provider = this.wizard.provider as SCIMProvider | undefined; + + return html`
+ + + ${msg("Protocol settings")} +
+ + + + +
+
+ + ${msg("User filtering")} +
+ + + => { + const args: CoreGroupsListRequest = { + ordering: "name", + }; + 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=${true} + > + +

+ ${msg("Only sync users within the selected group.")} +

+
+
+
+ + ${msg("Attribute mapping")} +
+ + +

+ ${msg("Property mappings used to user mapping.")} +

+

+ ${msg("Hold control/command to select multiple items.")} +

+
+ + +

+ ${msg("Property mappings used to group creation.")} +

+

+ ${msg("Hold control/command to select multiple items.")} +

+
+
+
+
`; + } +} + +export default ApplicationWizardAuthenticationBySCIM; diff --git a/web/src/admin/applications/wizard/oauth/TypeOAuthAPIApplicationWizardPage.ts b/web/src/admin/applications/wizard/oauth/TypeOAuthAPIApplicationWizardPage.ts deleted file mode 100644 index a2e08c2aa..000000000 --- a/web/src/admin/applications/wizard/oauth/TypeOAuthAPIApplicationWizardPage.ts +++ /dev/null @@ -1,35 +0,0 @@ -import "@goauthentik/elements/forms/HorizontalFormElement"; -import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; - -import { msg } from "@lit/localize"; -import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; -import { CSSResult, TemplateResult, html } from "lit"; - -import PFButton from "@patternfly/patternfly/components/Button/button.css"; -import PFForm from "@patternfly/patternfly/components/Form/form.css"; -import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; -import PFBase from "@patternfly/patternfly/patternfly-base.css"; - -@customElement("ak-application-wizard-type-oauth-api") -export class TypeOAuthAPIApplicationWizardPage extends WizardPage { - static get styles(): CSSResult[] { - return [PFBase, PFButton, PFForm, PFRadio]; - } - - sidebarLabel = () => msg("Method details"); - - render(): TemplateResult { - return html`
-

- ${msg( - "This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically.", - )} -

-

- ${msg( - "By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password.", - )} -

-
`; - } -} diff --git a/web/src/admin/applications/wizard/oauth/TypeOAuthApplicationWizardPage.ts b/web/src/admin/applications/wizard/oauth/TypeOAuthApplicationWizardPage.ts deleted file mode 100644 index bb19272e9..000000000 --- a/web/src/admin/applications/wizard/oauth/TypeOAuthApplicationWizardPage.ts +++ /dev/null @@ -1,84 +0,0 @@ -import "@goauthentik/elements/forms/HorizontalFormElement"; -import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; - -import { msg } from "@lit/localize"; -import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; -import { CSSResult, TemplateResult, html } from "lit"; - -import PFButton from "@patternfly/patternfly/components/Button/button.css"; -import PFForm from "@patternfly/patternfly/components/Form/form.css"; -import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; -import PFBase from "@patternfly/patternfly/patternfly-base.css"; - -import { TypeCreate } from "@goauthentik/api"; - -@customElement("ak-application-wizard-type-oauth") -export class TypeOAuthApplicationWizardPage extends WizardPage { - applicationTypes: TypeCreate[] = [ - { - component: "ak-application-wizard-type-oauth-code", - name: msg("Web application"), - description: msg( - "Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP)", - ), - modelName: "", - }, - { - component: "ak-application-wizard-type-oauth-implicit", - name: msg("Single-page applications"), - description: msg( - "Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue)", - ), - modelName: "", - }, - { - component: "ak-application-wizard-type-oauth-implicit", - name: msg("Native application"), - description: msg( - "Applications which redirect users to a non-web callback (for example, Android, iOS)", - ), - modelName: "", - }, - { - component: "ak-application-wizard-type-oauth-api", - name: msg("API"), - description: msg( - "Authentication without user interaction, or machine-to-machine authentication.", - ), - modelName: "", - }, - ]; - - static get styles(): CSSResult[] { - return [PFBase, PFButton, PFForm, PFRadio]; - } - - sidebarLabel = () => msg("Application type"); - - render(): TemplateResult { - return html`
- ${this.applicationTypes.map((type) => { - return html`
- { - this.host.steps = [ - "ak-application-wizard-initial", - "ak-application-wizard-type", - "ak-application-wizard-type-oauth", - type.component, - ]; - this.host.state["oauth-type"] = type.component; - this.host.isValid = true; - }} - /> - - ${type.description} -
`; - })} -
`; - } -} diff --git a/web/src/admin/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage.ts b/web/src/admin/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage.ts deleted file mode 100644 index 458def24b..000000000 --- a/web/src/admin/applications/wizard/oauth/TypeOAuthCodeApplicationWizardPage.ts +++ /dev/null @@ -1,57 +0,0 @@ -import "@goauthentik/admin/common/ak-flow-search/ak-flow-search-no-default"; -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { KeyUnknown } from "@goauthentik/elements/forms/Form"; -import "@goauthentik/elements/forms/HorizontalFormElement"; -import "@goauthentik/elements/forms/SearchSelect"; -import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; -import "@goauthentik/elements/wizard/WizardFormPage"; - -import { msg } from "@lit/localize"; -import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; -import { TemplateResult, html } from "lit"; - -import { - ClientTypeEnum, - FlowsInstancesListDesignationEnum, - OAuth2ProviderRequest, - ProvidersApi, -} from "@goauthentik/api"; - -@customElement("ak-application-wizard-type-oauth-code") -export class TypeOAuthCodeApplicationWizardPage extends WizardFormPage { - sidebarLabel = () => msg("Method details"); - - nextDataCallback = async (data: KeyUnknown): Promise => { - this.host.addActionBefore(msg("Create provider"), async (): Promise => { - const req: OAuth2ProviderRequest = { - name: this.host.state["name"] as string, - clientType: ClientTypeEnum.Confidential, - authorizationFlow: data.authorizationFlow as string, - }; - const provider = await new ProvidersApi(DEFAULT_CONFIG).providersOauth2Create({ - oAuth2ProviderRequest: req, - }); - this.host.state["provider"] = provider; - return true; - }); - return true; - }; - - renderForm(): TemplateResult { - return html`
- - -

- ${msg("Flow used when users access this application.")} -

-
-
`; - } -} diff --git a/web/src/admin/applications/wizard/oauth/TypeOAuthImplicitApplicationWizardPage.ts b/web/src/admin/applications/wizard/oauth/TypeOAuthImplicitApplicationWizardPage.ts deleted file mode 100644 index c1fa0f3cf..000000000 --- a/web/src/admin/applications/wizard/oauth/TypeOAuthImplicitApplicationWizardPage.ts +++ /dev/null @@ -1,15 +0,0 @@ -import "@goauthentik/elements/forms/HorizontalFormElement"; -import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; - -import { msg } from "@lit/localize"; -import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; -import { TemplateResult, html } from "lit"; - -@customElement("ak-application-wizard-type-oauth-implicit") -export class TypeOAuthImplicitApplicationWizardPage extends WizardFormPage { - sidebarLabel = () => msg("Method details"); - - render(): TemplateResult { - return html`
some stuff idk
`; - } -} diff --git a/web/src/admin/applications/wizard/proxy/TypeProxyApplicationWizardPage.ts b/web/src/admin/applications/wizard/proxy/TypeProxyApplicationWizardPage.ts deleted file mode 100644 index 43db7c56c..000000000 --- a/web/src/admin/applications/wizard/proxy/TypeProxyApplicationWizardPage.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { KeyUnknown } from "@goauthentik/elements/forms/Form"; -import "@goauthentik/elements/forms/HorizontalFormElement"; -import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; - -import { msg } from "@lit/localize"; -import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; -import { TemplateResult, html } from "lit"; - -import { - FlowDesignationEnum, - FlowsApi, - ProvidersApi, - ProxyProviderRequest, -} from "@goauthentik/api"; - -@customElement("ak-application-wizard-type-proxy") -export class TypeProxyApplicationWizardPage extends WizardFormPage { - sidebarLabel = () => msg("Proxy details"); - - nextDataCallback = async (data: KeyUnknown): Promise => { - let name = this.host.state["name"] as string; - // Check if a provider with the name already exists - const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({ - search: name, - }); - if (providers.results.filter((provider) => provider.name == name)) { - name += "-1"; - } - this.host.addActionBefore(msg("Create provider"), async (): Promise => { - // Get all flows and default to the implicit authorization - const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ - designation: FlowDesignationEnum.Authorization, - ordering: "slug", - }); - const req: ProxyProviderRequest = { - name: name, - authorizationFlow: flows.results[0].pk, - externalHost: data.externalHost as string, - }; - const provider = await new ProvidersApi(DEFAULT_CONFIG).providersProxyCreate({ - proxyProviderRequest: req, - }); - this.host.state["provider"] = provider; - return true; - }); - return true; - }; - - renderForm(): TemplateResult { - return html`
- - -

- ${msg("External domain you will be accessing the domain from.")} -

-
-
`; - } -} diff --git a/web/src/admin/applications/wizard/saml/TypeSAMLApplicationWizardPage.ts b/web/src/admin/applications/wizard/saml/TypeSAMLApplicationWizardPage.ts deleted file mode 100644 index 2ef4d6972..000000000 --- a/web/src/admin/applications/wizard/saml/TypeSAMLApplicationWizardPage.ts +++ /dev/null @@ -1,66 +0,0 @@ -import "@goauthentik/elements/forms/HorizontalFormElement"; -import { WizardPage } from "@goauthentik/elements/wizard/WizardPage"; - -import { msg } from "@lit/localize"; -import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; -import { CSSResult, TemplateResult, html } from "lit"; - -import PFButton from "@patternfly/patternfly/components/Button/button.css"; -import PFForm from "@patternfly/patternfly/components/Form/form.css"; -import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; -import PFBase from "@patternfly/patternfly/patternfly-base.css"; - -import { TypeCreate } from "@goauthentik/api"; - -@customElement("ak-application-wizard-type-saml") -export class TypeOAuthApplicationWizardPage extends WizardPage { - applicationTypes: TypeCreate[] = [ - { - component: "ak-application-wizard-type-saml-import", - name: msg("Import SAML Metadata"), - description: msg( - "Import the metadata document of the applicaation you want to configure.", - ), - modelName: "", - }, - { - component: "ak-application-wizard-type-saml-config", - name: msg("Manual configuration"), - description: msg("Manually configure SAML"), - modelName: "", - }, - ]; - - static get styles(): CSSResult[] { - return [PFBase, PFButton, PFForm, PFRadio]; - } - - sidebarLabel = () => msg("Application type"); - - render(): TemplateResult { - return html`
- ${this.applicationTypes.map((type) => { - return html`
- { - this.host.steps = [ - "ak-application-wizard-initial", - "ak-application-wizard-type", - "ak-application-wizard-type-saml", - type.component, - ]; - this.host.state["saml-type"] = type.component; - this.host.isValid = true; - }} - /> - - ${type.description} -
`; - })} -
`; - } -} diff --git a/web/src/admin/applications/wizard/saml/TypeSAMLConfigApplicationWizardPage.ts b/web/src/admin/applications/wizard/saml/TypeSAMLConfigApplicationWizardPage.ts deleted file mode 100644 index ad269ac63..000000000 --- a/web/src/admin/applications/wizard/saml/TypeSAMLConfigApplicationWizardPage.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { KeyUnknown } from "@goauthentik/elements/forms/Form"; -import "@goauthentik/elements/forms/HorizontalFormElement"; -import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; - -import { msg } from "@lit/localize"; -import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; -import { TemplateResult, html } from "lit"; - -import { FlowDesignationEnum, FlowsApi, ProvidersApi, SAMLProviderRequest } from "@goauthentik/api"; - -@customElement("ak-application-wizard-type-saml-config") -export class TypeSAMLApplicationWizardPage extends WizardFormPage { - sidebarLabel = () => msg("SAML details"); - - nextDataCallback = async (data: KeyUnknown): Promise => { - let name = this.host.state["name"] as string; - // Check if a provider with the name already exists - const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({ - search: name, - }); - if (providers.results.filter((provider) => provider.name == name)) { - name += "-1"; - } - this.host.addActionBefore(msg("Create provider"), async (): Promise => { - // Get all flows and default to the implicit authorization - const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ - designation: FlowDesignationEnum.Authorization, - ordering: "slug", - }); - const req: SAMLProviderRequest = { - name: name, - authorizationFlow: flows.results[0].pk, - acsUrl: data.acsUrl as string, - }; - const provider = await new ProvidersApi(DEFAULT_CONFIG).providersSamlCreate({ - sAMLProviderRequest: req, - }); - this.host.state["provider"] = provider; - return true; - }); - return true; - }; - - renderForm(): TemplateResult { - return html`
- - -

- ${msg( - "URL that authentik will redirect back to after successful authentication.", - )} -

-
-
`; - } -} diff --git a/web/src/admin/applications/wizard/saml/TypeSAMLImportApplicationWizardPage.ts b/web/src/admin/applications/wizard/saml/TypeSAMLImportApplicationWizardPage.ts deleted file mode 100644 index c3ddfda79..000000000 --- a/web/src/admin/applications/wizard/saml/TypeSAMLImportApplicationWizardPage.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { KeyUnknown } from "@goauthentik/elements/forms/Form"; -import "@goauthentik/elements/forms/HorizontalFormElement"; -import { WizardFormPage } from "@goauthentik/elements/wizard/WizardFormPage"; - -import { msg } from "@lit/localize"; -import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; -import { TemplateResult, html } from "lit"; - -import { - FlowDesignationEnum, - FlowsApi, - ProvidersApi, - ProvidersSamlImportMetadataCreateRequest, -} from "@goauthentik/api"; - -@customElement("ak-application-wizard-type-saml-import") -export class TypeSAMLImportApplicationWizardPage extends WizardFormPage { - sidebarLabel = () => msg("Import SAML metadata"); - - nextDataCallback = async (data: KeyUnknown): Promise => { - let name = this.host.state["name"] as string; - // Check if a provider with the name already exists - const providers = await new ProvidersApi(DEFAULT_CONFIG).providersAllList({ - search: name, - }); - if (providers.results.filter((provider) => provider.name == name)) { - name += "-1"; - } - this.host.addActionBefore(msg("Create provider"), async (): Promise => { - // Get all flows and default to the implicit authorization - const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({ - designation: FlowDesignationEnum.Authorization, - ordering: "slug", - }); - const req: ProvidersSamlImportMetadataCreateRequest = { - name: name, - authorizationFlow: flows.results[0].slug, - file: data["metadata"] as Blob, - }; - const provider = await new ProvidersApi( - DEFAULT_CONFIG, - ).providersSamlImportMetadataCreate(req); - this.host.state["provider"] = provider; - return true; - }); - return true; - }; - - renderForm(): TemplateResult { - return html`
- - - -
`; - } -} diff --git a/web/src/admin/applications/wizard/steps.ts b/web/src/admin/applications/wizard/steps.ts new file mode 100644 index 000000000..451367bf8 --- /dev/null +++ b/web/src/admin/applications/wizard/steps.ts @@ -0,0 +1,82 @@ +import { + BackStep, + CancelWizard, + CloseWizard, + DisabledNextStep, + NextStep, + SubmitStep, +} from "@goauthentik/components/ak-wizard-main/commonWizardButtons"; + +import { html } from "lit"; + +import "./application/ak-application-wizard-application-details"; +import "./auth-method-choice/ak-application-wizard-authentication-method-choice"; +import "./commit/ak-application-wizard-commit-application"; +import "./methods/ak-application-wizard-authentication-method"; +import { ApplicationStep as ApplicationStepType } from "./types"; + +class ApplicationStep implements ApplicationStepType { + id = "application"; + label = "Application Details"; + disabled = false; + valid = false; + get buttons() { + return [this.valid ? NextStep : DisabledNextStep, CancelWizard]; + } + render() { + return html``; + } +} + +class ProviderMethodStep implements ApplicationStepType { + id = "provider-method"; + label = "Provider Type"; + disabled = false; + valid = false; + + get buttons() { + return [BackStep, this.valid ? NextStep : DisabledNextStep, CancelWizard]; + } + + render() { + // prettier-ignore + return html` `; + } +} + +class ProviderStepDetails implements ApplicationStepType { + id = "provider-details"; + label = "Provider Configuration"; + disabled = true; + valid = false; + get buttons() { + return [BackStep, this.valid ? SubmitStep : DisabledNextStep, CancelWizard]; + } + + render() { + return html``; + } +} + +class SubmitApplicationStep implements ApplicationStepType { + id = "submit"; + label = "Submit Application"; + disabled = true; + valid = false; + + get buttons() { + return this.valid ? [CloseWizard] : [BackStep, CancelWizard]; + } + + render() { + return html``; + } +} + +export const newSteps = (): ApplicationStep[] => [ + new ApplicationStep(), + new ProviderMethodStep(), + new ProviderStepDetails(), + new SubmitApplicationStep(), +]; diff --git a/web/src/admin/applications/wizard/stories/ak-application-context-display-for-test.ts b/web/src/admin/applications/wizard/stories/ak-application-context-display-for-test.ts new file mode 100644 index 000000000..fa418199e --- /dev/null +++ b/web/src/admin/applications/wizard/stories/ak-application-context-display-for-test.ts @@ -0,0 +1,18 @@ +import { consume } from "@lit-labs/context"; +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { state } from "@lit/reactive-element/decorators/state.js"; +import { LitElement, html } from "lit"; + +import applicationWizardContext from "../ContextIdentity"; +import type { ApplicationWizardState } from "../types"; + +@customElement("ak-application-context-display-for-test") +export class ApplicationContextDisplayForTest extends LitElement { + @consume({ context: applicationWizardContext, subscribe: true }) + @state() + private wizard!: ApplicationWizardState; + + render() { + return html`
${JSON.stringify(this.wizard, null, 2)}
`; + } +} diff --git a/web/src/admin/applications/wizard/stories/ak-application-wizard-main.stories.ts b/web/src/admin/applications/wizard/stories/ak-application-wizard-main.stories.ts new file mode 100644 index 000000000..d0e7d8aec --- /dev/null +++ b/web/src/admin/applications/wizard/stories/ak-application-wizard-main.stories.ts @@ -0,0 +1,54 @@ +import { Meta } from "@storybook/web-components"; + +import { TemplateResult, html } from "lit"; + +import { ApplicationWizard } from "../ak-application-wizard"; +import "../ak-application-wizard"; +import { mockData } from "./mockData"; + +const metadata: Meta = { + title: "Elements / Application Wizard Implementation / Main", + component: "ak-application-wizard", + parameters: { + docs: { + description: { + component: "The first page of the application wizard", + }, + }, + mockData, + }, +}; + +const LIGHT = "pf-t-light"; +function injectTheme() { + setTimeout(() => { + if (!document.body.classList.contains(LIGHT)) { + document.body.classList.add(LIGHT); + } + }); +} + +export default metadata; + +const container = (testItem: TemplateResult) => { + injectTheme(); + return html`
+ + ${testItem} +
`; +}; + +export const MainPage = () => { + return container(html` + +
+ + `); +}; diff --git a/web/src/admin/applications/wizard/stories/mockData.ts b/web/src/admin/applications/wizard/stories/mockData.ts new file mode 100644 index 000000000..3bd5be087 --- /dev/null +++ b/web/src/admin/applications/wizard/stories/mockData.ts @@ -0,0 +1,62 @@ +import { + dummyAuthenticationFlowsSearch, + dummyAuthorizationFlowsSearch, + dummyCoreGroupsSearch, + dummyCryptoCertsSearch, + dummyHasJwks, + dummyPropertyMappings, + dummyProviderTypesList, + dummySAMLProviderMappings, +} from "./samples"; + +export const mockData = [ + { + url: "/api/v3/providers/all/types/", + method: "GET", + status: 200, + response: dummyProviderTypesList, + }, + { + url: "/api/v3/core/groups/?ordering=name", + method: "GET", + status: 200, + response: dummyCoreGroupsSearch, + }, + + { + url: "/api/v3/crypto/certificatekeypairs/?has_key=true&include_details=false&ordering=name", + method: "GET", + status: 200, + response: dummyCryptoCertsSearch, + }, + { + url: "/api/v3/flows/instances/?designation=authentication&ordering=slug", + method: "GET", + status: 200, + response: dummyAuthenticationFlowsSearch, + }, + { + url: "/api/v3/flows/instances/?designation=authorization&ordering=slug", + method: "GET", + status: 200, + response: dummyAuthorizationFlowsSearch, + }, + { + url: "/api/v3/propertymappings/scope/?ordering=scope_name", + method: "GET", + status: 200, + response: dummyPropertyMappings, + }, + { + url: "/api/v3/sources/oauth/?has_jwks=true&ordering=name", + method: "GET", + status: 200, + response: dummyHasJwks, + }, + { + url: "/api/v3/propertymappings/saml/?ordering=saml_name", + method: "GET", + status: 200, + response: dummySAMLProviderMappings, + }, +]; diff --git a/web/src/admin/applications/wizard/stories/samples.ts b/web/src/admin/applications/wizard/stories/samples.ts new file mode 100644 index 000000000..27b5867aa --- /dev/null +++ b/web/src/admin/applications/wizard/stories/samples.ts @@ -0,0 +1,375 @@ +export const dummyCryptoCertsSearch = { + pagination: { + next: 0, + previous: 0, + count: 1, + current: 1, + total_pages: 1, + start_index: 1, + end_index: 1, + }, + results: [ + { + pk: "63efd1b8-6c39-4f65-8157-9a406cb37447", + name: "authentik Self-signed Certificate", + fingerprint_sha256: null, + fingerprint_sha1: null, + cert_expiry: null, + cert_subject: null, + private_key_available: true, + private_key_type: null, + certificate_download_url: + "/api/v3/crypto/certificatekeypairs/63efd1b8-6c39-4f65-8157-9a406cb37447/view_certificate/?download", + private_key_download_url: + "/api/v3/crypto/certificatekeypairs/63efd1b8-6c39-4f65-8157-9a406cb37447/view_private_key/?download", + managed: null, + }, + ], +}; + +export const dummyAuthenticationFlowsSearch = { + pagination: { + next: 0, + previous: 0, + count: 2, + current: 1, + total_pages: 1, + start_index: 1, + end_index: 2, + }, + results: [ + { + pk: "2594b1a0-f234-4965-8b93-a8631a55bd5c", + policybindingmodel_ptr_id: "0bc529a6-dcd0-4ba8-8fef-5702348832f9", + name: "Welcome to authentik!", + slug: "default-authentication-flow", + title: "Welcome to authentik!", + designation: "authentication", + background: "/static/dist/assets/images/flow_background.jpg", + stages: [ + "bad9fbce-fb86-4ba4-8124-e7a1d8c147f3", + "1da1f272-a76e-4112-be95-f02421fca1d4", + "945cd956-6670-4dfa-ab3a-2a72dd3051a7", + "0fc1fc5c-b928-4d99-a892-9ae48de089f5", + ], + policies: [], + cache_count: 0, + policy_engine_mode: "any", + compatibility_mode: false, + export_url: "/api/v3/flows/instances/default-authentication-flow/export/", + layout: "stacked", + denied_action: "message_continue", + authentication: "none", + }, + { + pk: "3526dbd1-b50e-4553-bada-fbe7b3c2f660", + policybindingmodel_ptr_id: "cde67954-b78a-4fe9-830e-c2aba07a724a", + name: "Welcome to authentik!", + slug: "default-source-authentication", + title: "Welcome to authentik!", + designation: "authentication", + background: "/static/dist/assets/images/flow_background.jpg", + stages: ["3713b252-cee3-4acb-a02f-083f26459fff"], + policies: ["f42a4c7f-6586-4b14-9325-a832127ba295"], + cache_count: 0, + policy_engine_mode: "any", + compatibility_mode: false, + export_url: "/api/v3/flows/instances/default-source-authentication/export/", + layout: "stacked", + denied_action: "message_continue", + authentication: "require_unauthenticated", + }, + ], +}; + +export const dummyAuthorizationFlowsSearch = { + pagination: { + next: 0, + previous: 0, + count: 2, + current: 1, + total_pages: 1, + start_index: 1, + end_index: 2, + }, + results: [ + { + pk: "9e01f011-8b3f-43d6-bedf-c29be5f3a428", + policybindingmodel_ptr_id: "14179ef8-2726-4027-9e2f-dc99185199bf", + name: "Authorize Application", + slug: "default-provider-authorization-explicit-consent", + title: "Redirecting to %(app)s", + designation: "authorization", + background: "/static/dist/assets/images/flow_background.jpg", + stages: ["ed5f015f-82b9-450f-addf-1e9d21d8dda3"], + policies: [], + cache_count: 0, + policy_engine_mode: "any", + compatibility_mode: false, + export_url: + "/api/v3/flows/instances/default-provider-authorization-explicit-consent/export/", + layout: "stacked", + denied_action: "message_continue", + authentication: "require_authenticated", + }, + { + pk: "06f11ee3-cbe3-456d-81df-fae4c0a62951", + policybindingmodel_ptr_id: "686e6539-8b9f-473e-9f54-e05cc207dd2a", + name: "Authorize Application", + slug: "default-provider-authorization-implicit-consent", + title: "Redirecting to %(app)s", + designation: "authorization", + background: "/static/dist/assets/images/flow_background.jpg", + stages: [], + policies: [], + cache_count: 0, + policy_engine_mode: "any", + compatibility_mode: false, + export_url: + "/api/v3/flows/instances/default-provider-authorization-implicit-consent/export/", + layout: "stacked", + denied_action: "message_continue", + authentication: "require_authenticated", + }, + ], +}; + +export const dummyCoreGroupsSearch = { + pagination: { + next: 0, + previous: 0, + count: 1, + current: 1, + total_pages: 1, + start_index: 1, + end_index: 1, + }, + results: [ + { + pk: "67543d37-0ee2-4a4c-b020-9e735a8b5178", + num_pk: 13734, + name: "authentik Admins", + is_superuser: true, + parent: null, + users: [1], + attributes: {}, + users_obj: [ + { + pk: 1, + username: "akadmin", + name: "authentik Default Admin", + is_active: true, + last_login: "2023-07-03T16:08:11.196942Z", + email: "ken@goauthentik.io", + attributes: { + settings: { + locale: "en", + }, + }, + uid: "6dedc98b3fdd0f9afdc705e9d577d61127d89f1d91ea2f90f0b9a353615fb8f2", + }, + ], + }, + ], +}; + +export const dummyPropertyMappings = { + pagination: { + next: 0, + previous: 0, + count: 4, + current: 1, + total_pages: 1, + start_index: 1, + end_index: 4, + }, + results: [ + { + pk: "30d87af7-9d9d-4292-873e-a52145ba4bcb", + managed: "goauthentik.io/providers/proxy/scope-proxy", + name: "authentik default OAuth Mapping: Proxy outpost", + expression: + '# This mapping is used by the authentik proxy. It passes extra user attributes,\n# which are used for example for the HTTP-Basic Authentication mapping.\nreturn {\n "ak_proxy": {\n "user_attributes": request.user.group_attributes(request),\n "is_superuser": request.user.is_superuser,\n }\n}', + component: "ak-property-mapping-scope-form", + verbose_name: "Scope Mapping", + verbose_name_plural: "Scope Mappings", + meta_model_name: "authentik_providers_oauth2.scopemapping", + scope_name: "ak_proxy", + description: "authentik Proxy - User information", + }, + { + pk: "3e3751ed-a24c-4f47-a051-e2e05b5cd306", + managed: "goauthentik.io/providers/oauth2/scope-email", + name: "authentik default OAuth Mapping: OpenID 'email'", + expression: 'return {\n "email": request.user.email,\n "email_verified": True\n}', + component: "ak-property-mapping-scope-form", + verbose_name: "Scope Mapping", + verbose_name_plural: "Scope Mappings", + meta_model_name: "authentik_providers_oauth2.scopemapping", + scope_name: "email", + description: "Email address", + }, + { + pk: "81c5e330-d8a0-45cd-9cad-e6a49a9c428f", + managed: "goauthentik.io/providers/oauth2/scope-openid", + name: "authentik default OAuth Mapping: OpenID 'openid'", + expression: + "# This scope is required by the OpenID-spec, and must as such exist in authentik.\n# The scope by itself does not grant any information\nreturn {}", + component: "ak-property-mapping-scope-form", + verbose_name: "Scope Mapping", + verbose_name_plural: "Scope Mappings", + meta_model_name: "authentik_providers_oauth2.scopemapping", + scope_name: "openid", + description: "", + }, + { + pk: "7ad9cd6f-bcc8-425d-b7c2-c7c4592a1b36", + managed: "goauthentik.io/providers/oauth2/scope-profile", + name: "authentik default OAuth Mapping: OpenID 'profile'", + expression: + 'return {\n # Because authentik only saves the user\'s full name, and has no concept of first and last names,\n # the full name is used as given name.\n # You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")`\n "name": request.user.name,\n "given_name": request.user.name,\n "preferred_username": request.user.username,\n "nickname": request.user.username,\n # groups is not part of the official userinfo schema, but is a quasi-standard\n "groups": [group.name for group in request.user.ak_groups.all()],\n}', + component: "ak-property-mapping-scope-form", + verbose_name: "Scope Mapping", + verbose_name_plural: "Scope Mappings", + meta_model_name: "authentik_providers_oauth2.scopemapping", + scope_name: "profile", + description: "General Profile Information", + }, + ], +}; + +export const dummyHasJwks = { + pagination: { + next: 0, + previous: 0, + count: 0, + current: 1, + total_pages: 1, + start_index: 0, + end_index: 0, + }, + results: [], +}; + +export const dummySAMLProviderMappings = { + pagination: { + next: 0, + previous: 0, + count: 7, + current: 1, + total_pages: 1, + start_index: 1, + end_index: 7, + }, + results: [ + { + pk: "9f1f23b7-1956-4daa-b08b-338cab9b3953", + managed: "goauthentik.io/providers/saml/uid", + name: "authentik default SAML Mapping: User ID", + expression: "return request.user.pk", + component: "ak-property-mapping-saml-form", + verbose_name: "SAML Property Mapping", + verbose_name_plural: "SAML Property Mappings", + meta_model_name: "authentik_providers_saml.samlpropertymapping", + saml_name: "http://schemas.goauthentik.io/2021/02/saml/uid", + friendly_name: null, + }, + { + pk: "801b6328-bb0b-4ec6-b52c-f3dc7bb6ec7f", + managed: "goauthentik.io/providers/saml/username", + name: "authentik default SAML Mapping: Username", + expression: "return request.user.username", + component: "ak-property-mapping-saml-form", + verbose_name: "SAML Property Mapping", + verbose_name_plural: "SAML Property Mappings", + meta_model_name: "authentik_providers_saml.samlpropertymapping", + saml_name: "http://schemas.goauthentik.io/2021/02/saml/username", + friendly_name: null, + }, + { + pk: "27c4d370-658d-4acf-9f61-cfa6dd020b11", + managed: "goauthentik.io/providers/saml/ms-windowsaccountname", + name: "authentik default SAML Mapping: WindowsAccountname (Username)", + expression: "return request.user.username", + component: "ak-property-mapping-saml-form", + verbose_name: "SAML Property Mapping", + verbose_name_plural: "SAML Property Mappings", + meta_model_name: "authentik_providers_saml.samlpropertymapping", + saml_name: "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", + friendly_name: null, + }, + { + pk: "757b185b-1c21-42b4-a2ee-04d6f7b655b3", + managed: "goauthentik.io/providers/saml/groups", + name: "authentik default SAML Mapping: Groups", + expression: "for group in request.user.ak_groups.all():\n yield group.name", + component: "ak-property-mapping-saml-form", + verbose_name: "SAML Property Mapping", + verbose_name_plural: "SAML Property Mappings", + meta_model_name: "authentik_providers_saml.samlpropertymapping", + saml_name: "http://schemas.xmlsoap.org/claims/Group", + friendly_name: null, + }, + { + pk: "de67cee7-7c56-4c1d-9466-9ad0e0105092", + managed: "goauthentik.io/providers/saml/email", + name: "authentik default SAML Mapping: Email", + expression: "return request.user.email", + component: "ak-property-mapping-saml-form", + verbose_name: "SAML Property Mapping", + verbose_name_plural: "SAML Property Mappings", + meta_model_name: "authentik_providers_saml.samlpropertymapping", + saml_name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", + friendly_name: null, + }, + { + pk: "42a936a5-11a9-4442-8748-ec27a8ab9546", + managed: "goauthentik.io/providers/saml/name", + name: "authentik default SAML Mapping: Name", + expression: "return request.user.name", + component: "ak-property-mapping-saml-form", + verbose_name: "SAML Property Mapping", + verbose_name_plural: "SAML Property Mappings", + meta_model_name: "authentik_providers_saml.samlpropertymapping", + saml_name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", + friendly_name: null, + }, + { + pk: "06bee8f0-e5b4-4ce8-959a-308ba0769917", + managed: "goauthentik.io/providers/saml/upn", + name: "authentik default SAML Mapping: UPN", + expression: "return request.user.attributes.get('upn', request.user.email)", + component: "ak-property-mapping-saml-form", + verbose_name: "SAML Property Mapping", + verbose_name_plural: "SAML Property Mappings", + meta_model_name: "authentik_providers_saml.samlpropertymapping", + saml_name: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", + friendly_name: null, + }, + ], +}; + +// prettier-ignore +export const dummyProviderTypesList = [ + ["LDAP Provider", "ldapprovider", + "Allow applications to authenticate against authentik's users using LDAP.", + ], + ["OAuth2/OpenID Provider", "oauth2provider", + "OAuth2 Provider for generic OAuth and OpenID Connect Applications.", + ], + ["Proxy Provider", "proxyprovider", + "Protect applications that don't support any of the other\n Protocols by using a Reverse-Proxy.", + ], + ["Radius Provider", "radiusprovider", + "Allow applications to authenticate against authentik's users using Radius.", + ], + ["SAML Provider", "samlprovider", + "SAML 2.0 Endpoint for applications which support SAML.", + ], + ["SCIM Provider", "scimprovider", + "SCIM 2.0 provider to create users and groups in external applications", + ], + ["SAML Provider from Metadata", "", + "Create a SAML Provider by importing its Metadata.", + ], +].map(([name, model_name, description]) => ({ name, description, model_name })); diff --git a/web/src/admin/applications/wizard/types.ts b/web/src/admin/applications/wizard/types.ts new file mode 100644 index 000000000..0ebe7aa8a --- /dev/null +++ b/web/src/admin/applications/wizard/types.ts @@ -0,0 +1,39 @@ +import { type WizardStep } from "@goauthentik/components/ak-wizard-main/types"; + +import { + type ApplicationRequest, + type LDAPProviderRequest, + type OAuth2ProviderRequest, + type ProvidersSamlImportMetadataCreateRequest, + type ProxyProviderRequest, + type RadiusProviderRequest, + type SAMLProviderRequest, + type SCIMProviderRequest, +} from "@goauthentik/api"; + +export type OneOfProvider = + | Partial + | Partial + | Partial + | Partial + | Partial + | Partial + | Partial; + +export interface ApplicationWizardState { + providerModel: string; + app: Partial; + provider: OneOfProvider; +} + +type StatusType = "invalid" | "valid" | "submitted" | "failed"; + +export type ApplicationWizardStateUpdate = { + update?: Partial; + status?: StatusType; +}; + +export type ApplicationStep = WizardStep & { + id: string; + valid: boolean; +}; diff --git a/web/src/admin/providers/ProviderListPage.ts b/web/src/admin/providers/ProviderListPage.ts index c82985815..7dbab4f16 100644 --- a/web/src/admin/providers/ProviderListPage.ts +++ b/web/src/admin/providers/ProviderListPage.ts @@ -1,3 +1,4 @@ +import "@goauthentik/admin/applications/ApplicationWizardHint"; import "@goauthentik/admin/providers/ProviderWizard"; import "@goauthentik/admin/providers/ldap/LDAPProviderForm"; import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm"; @@ -60,6 +61,10 @@ export class ProviderListPage extends TablePage { ]; } + renderSectionBefore(): TemplateResult { + return html``; + } + renderToolbarSelected(): TemplateResult { const disabled = this.selectedElements.length < 1; return html`(value: T) { + return Object.prototype.toString.call(value); +} + +// Creates a deep clone for each value +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function cloneDescriptorValue(value: any) { + // Arrays + if (objectType(value) === "[object Array]") { + const array = []; + for (let v of value) { + v = cloneDescriptorValue(v); + array.push(v); + } + return array; + } + + // Objects + if (objectType(value) === "[object Object]") { + const obj = {}; + const props = Object.keys(value); + for (const prop of props) { + const descriptor = Object.getOwnPropertyDescriptor(value, prop); + if (!descriptor) { + continue; + } + + if (descriptor.value) { + descriptor.value = cloneDescriptorValue(descriptor.value); + } + Object.defineProperty(obj, prop, descriptor); + } + return obj; + } + + // Other Types of Objects + if (objectType(value) === "[object Date]") { + return new Date(value.getTime()); + } + + if (objectType(value) === "[object Map]") { + const map = new Map(); + for (const entry of value) { + map.set(entry[0], cloneDescriptorValue(entry[1])); + } + return map; + } + + if (objectType(value) === "[object Set]") { + const set = new Set(); + for (const entry of value.entries()) { + set.add(cloneDescriptorValue(entry[0])); + } + return set; + } + + // Types we don't need to clone or cannot clone. + // Examples: + // - Primitives don't need to clone + // - Functions cannot clone + return value; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function _merge(output: Record, input: Record) { + const props = Object.keys(input); + + for (const prop of props) { + // Prevents Prototype Pollution + if (prop === "__proto__") continue; + + const descriptor = Object.getOwnPropertyDescriptor(input, prop); + if (!descriptor) { + continue; + } + + const value = descriptor.value; + if (value) descriptor.value = cloneDescriptorValue(value); + + // If don't have prop => Define property + // [ken@goauthentik] Using `hasOwn` is preferable over + // the basic identity test, according to Typescript. + if (!Object.hasOwn(output, prop)) { + Object.defineProperty(output, prop, descriptor); + continue; + } + + // If have prop, but type is not object => Overwrite by redefining property + if (typeof output[prop] !== "object") { + Object.defineProperty(output, prop, descriptor); + continue; + } + + // If have prop, but type is Object => Concat the arrays together. + if (objectType(descriptor.value) === "[object Array]") { + output[prop] = output[prop].concat(descriptor.value); + continue; + } + + // If have prop, but type is Object => Merge. + _merge(output[prop], descriptor.value); + } +} + +export function merge(...sources: Array) { + const result = {}; + for (const source of sources) { + _merge(result, source); + } + return result; +} + +export default merge; diff --git a/web/src/components/ak-hint/ShowHintController.ts b/web/src/components/ak-hint/ShowHintController.ts index b3eb1393c..d4f4ed0ae 100644 --- a/web/src/components/ak-hint/ShowHintController.ts +++ b/web/src/components/ak-hint/ShowHintController.ts @@ -27,18 +27,27 @@ export class ShowHintController implements ReactiveController { constructor(host: ShowHintControllerHost, hintToken: string) { (this.host = host).addController(this); this.hintToken = hintToken; - this.hideTheHint = this.hideTheHint.bind(this); + this.hide = this.hide.bind(this); + this.show = this.show.bind(this); } - hideTheHint() { + setTheHint(state: boolean = false) { window?.localStorage.setItem( LOCALSTORAGE_AUTHENTIK_KEY, JSON.stringify({ ...getCurrentStorageValue(), - [this.hintToken]: false, + [this.hintToken]: state, }), ); - this.host.showHint = false; + this.host.showHint = state; + } + + hide() { + this.setTheHint(false); + } + + show() { + this.setTheHint(true); } hostConnected() { @@ -54,7 +63,7 @@ export class ShowHintController implements ReactiveController { render() { return html`
- ${msg( + ${msg( "Don't show this message again.", )}
+ extends AKElement + implements ReactiveControllerHost +{ + // prettier-ignore + static get styles() { return [PFBase, PFButton]; } + + @state() + steps: Step[] = []; + + @state() + currentStep = 0; + + /** + * A reference to the frame. Since the frame implements and inherits from ModalButton, + * you will need either a reference to or query to the frame in order to call + * `.close()` on it. + */ + frame: Ref = createRef(); + + get step() { + return this.steps[this.currentStep]; + } + + prompt = msg("Create"); + + header: string; + + description?: string; + + wizard: AkWizardController; + + constructor(prompt: string, header: string, description?: string) { + super(); + this.header = header; + this.prompt = prompt; + this.description = description; + this.wizard = new AkWizardController(this); + } + + /** + * Derive the labels used by the frame's Breadcrumbs display. + */ + get stepLabels(): WizardStepLabel[] { + let disabled = false; + return this.steps.map((step, index) => { + disabled = disabled || step.disabled; + return { + label: step.label, + active: index === this.currentStep, + index, + disabled, + }; + }); + } + + /** + * You should still consider overriding this if you need to consider details like "Is the step + * requested valid?" + */ + handleNav(stepId: number | undefined) { + if (stepId === undefined || this.steps[stepId] === undefined) { + throw new Error(`Attempt to navigate to undefined step: ${stepId}`); + } + this.currentStep = stepId; + this.requestUpdate(); + } + + close() { + throw new Error("This function must be overridden in the child class."); + } + + /** + * This is where all the business logic and special cases go. The Wizard Controller intercepts + * updates tagged `ak-wizard-update` and forwards the event content here. Business logic about + * "is the current step valid?" and "should the Next button be made enabled" are controlled + * here. (Any step implementing WizardStep can do it anyhow it pleases, putting "is the current + * form valid" and so forth into the step object itself.) + */ + handleUpdate(_detail: D) { + throw new Error("This function must be overridden in the child class."); + } + + render() { + return html` + + + + `; + } +} diff --git a/web/src/components/ak-wizard-main/AkWizardController.ts b/web/src/components/ak-wizard-main/AkWizardController.ts new file mode 100644 index 000000000..f5c2a23d3 --- /dev/null +++ b/web/src/components/ak-wizard-main/AkWizardController.ts @@ -0,0 +1,104 @@ +import { type ReactiveController } from "lit"; + +import { type AkWizard, type WizardNavCommand } from "./types"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isCustomEvent = (v: any): v is CustomEvent => + v instanceof CustomEvent && "detail" in v; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isNavEvent = (v: any): v is CustomEvent => + isCustomEvent(v) && "command" in v.detail; + +/** + * AkWizardController + * + * A ReactiveController that plugs into any wizard and provides a somewhat more convenient API for + * interacting with that wizard. It expects three different events from the wizard frame, each of + * which has a corresponding method that then forwards the necessary information to the host: + * + * - nav: A request to navigate to different step. Calls the host's `handleNav()` with the requested + step number. + * - update: A request to update the content of the current step. Forwarded to the host's + * `handleUpdate()` method. + * - close: A request to end the wizard interaction. Forwarded to the host's `close()` method. + * + */ + +export class AkWizardController implements ReactiveController { + private host: AkWizard; + + constructor(host: AkWizard) { + this.host = host; + this.handleNavRequest = this.handleNavRequest.bind(this); + this.handleUpdateRequest = this.handleUpdateRequest.bind(this); + host.addController(this); + } + + get maxStep() { + return this.host.steps.length - 1; + } + + get nextStep() { + return this.host.currentStep < this.maxStep ? this.host.currentStep + 1 : undefined; + } + + get backStep() { + return this.host.currentStep > 0 ? this.host.currentStep - 1 : undefined; + } + + get step() { + return this.host.steps[this.host.currentStep]; + } + + hostConnected() { + this.host.addEventListener("ak-wizard-nav", this.handleNavRequest); + this.host.addEventListener("ak-wizard-update", this.handleUpdateRequest); + this.host.addEventListener("ak-wizard-closed", this.handleCloseRequest); + } + + hostDisconnected() { + this.host.removeEventListener("ak-wizard-nav", this.handleNavRequest); + this.host.removeEventListener("ak-wizard-update", this.handleUpdateRequest); + this.host.removeEventListener("ak-wizard-closed", this.handleCloseRequest); + } + + handleNavRequest(event: Event) { + if (!isNavEvent(event)) { + throw new Error(`Unexpected event received by nav handler: ${event}`); + } + + if (event.detail.command === "close") { + this.host.close(); + return; + } + + const navigate = (): number | undefined => { + switch (event.detail.command) { + case "next": + return this.nextStep; + case "back": + return this.backStep; + case "goto": + return event.detail.step; + default: + throw new Error( + `Unrecognized command passed to ak-wizard-controller:handleNavRequest: ${event.detail.command}`, + ); + } + }; + + this.host.handleNav(navigate()); + } + + handleUpdateRequest(event: Event) { + if (!isCustomEvent(event)) { + throw new Error(`Unexpected event received by nav handler: ${event}`); + } + this.host.handleUpdate(event.detail); + } + + handleCloseRequest() { + this.host.close(); + } +} diff --git a/web/src/components/ak-wizard-main/ak-wizard-frame.ts b/web/src/components/ak-wizard-main/ak-wizard-frame.ts new file mode 100644 index 000000000..a0f320095 --- /dev/null +++ b/web/src/components/ak-wizard-main/ak-wizard-frame.ts @@ -0,0 +1,201 @@ +import { ModalButton } from "@goauthentik/elements/buttons/ModalButton"; +import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; + +import { msg } from "@lit/localize"; +import { customElement, property, query } from "@lit/reactive-element/decorators.js"; +import { TemplateResult, html, nothing } from "lit"; +import { classMap } from "lit/directives/class-map.js"; +import { map } from "lit/directives/map.js"; + +import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css"; + +import { type WizardButton, WizardStepLabel } from "./types"; + +/** + * AKWizardFrame is the main container for displaying Wizard pages. + * + * AKWizardFrame is one component of a Wizard development environment. It provides the header, + * titled navigation sidebar, and bottom row button bar. It takes its cues about what to render from + * two data structure, `this.steps: WizardStep[]`, which lists all the current steps *in order* and + * doesn't care otherwise about their structure, and `this.currentStep: WizardStep` which must be a + * _reference_ to a member of `this.steps`. + * + * @element ak-wizard-frame + * + * @slot - Where the form itself should go + * + * @fires ak-wizard-nav - Tell the orchestrator what page the user wishes to move to. + * + */ + +@customElement("ak-wizard-frame") +export class AkWizardFrame extends CustomEmitterElement(ModalButton) { + static get styles() { + return [...super.styles, PFWizard]; + } + + /** + * The text for the title of the wizard + */ + @property() + header?: string; + + /** + * The text for a descriptive subtitle for the wizard + */ + @property() + description?: string; + + /** + * The labels for all current steps, including their availability + */ + @property({ attribute: false, type: Array }) + stepLabels!: WizardStepLabel[]; + + /** + * What buttons to Show + */ + @property({ attribute: false, type: Array }) + buttons: WizardButton[] = []; + + /** + * Show the [Cancel] icon and offer the [Cancel] button + */ + @property({ type: Boolean, attribute: "can-cancel" }) + canCancel = false; + + /** + * The form renderer, passed as a function + */ + @property({ type: Object }) + form!: () => TemplateResult; + + @query("#main-content *:first-child") + content!: HTMLElement; + + constructor() { + super(); + this.renderButtons = this.renderButtons.bind(this); + } + + renderModalInner() { + // prettier-ignore + return html`
+ ${this.renderHeader()} +
+
+ ${this.renderNavigation()} + ${this.renderMainSection()} +
+ ${this.renderFooter()} +
+
`; + } + + renderHeader() { + return html`
+ ${this.canCancel ? this.renderHeaderCancelIcon() : nothing} +

${this.header}

+

${this.description}

+
`; + } + + renderHeaderCancelIcon() { + return html``; + } + + renderNavigation() { + return html``; + } + + renderNavigationStep(step: WizardStepLabel) { + const buttonClasses = { + "pf-c-wizard__nav-link": true, + "pf-m-current": step.active, + }; + + return html` +
  • + +
  • + `; + } + + // This is where the panel is shown. We expect the panel to get its information from an + // independent context. + renderMainSection() { + return html`
    +
    ${this.form()}
    +
    `; + } + + renderFooter() { + return html` +
    ${map(this.buttons, this.renderButtons)}
    + `; + } + + renderButtons([label, command]: WizardButton) { + switch (command.command) { + case "next": + return this.renderButton(label, "pf-m-primary", command.command); + case "back": + return this.renderButton(label, "pf-m-secondary", command.command); + case "close": + return this.renderLink(label, "pf-m-link"); + default: + throw new Error(`Button type not understood: ${command} for ${label}`); + } + } + + renderButton(label: string, classname: string, command: string) { + const buttonClasses = { "pf-c-button": true, [classname]: true }; + return html``; + } + + renderLink(label: string, classname: string) { + const buttonClasses = { "pf-c-button": true, [classname]: true }; + return html``; + } +} + +export default AkWizardFrame; diff --git a/web/src/components/ak-wizard-main/commonWizardButtons.ts b/web/src/components/ak-wizard-main/commonWizardButtons.ts new file mode 100644 index 000000000..e9b531f36 --- /dev/null +++ b/web/src/components/ak-wizard-main/commonWizardButtons.ts @@ -0,0 +1,15 @@ +import { msg } from "@lit/localize"; + +import { WizardButton } from "./types"; + +export const NextStep: WizardButton = [msg("Next"), { command: "next" }]; + +export const BackStep: WizardButton = [msg("Back"), { command: "back" }]; + +export const SubmitStep: WizardButton = [msg("Submit"), { command: "next" }]; + +export const CancelWizard: WizardButton = [msg("Cancel"), { command: "close" }]; + +export const CloseWizard: WizardButton = [msg("Close"), { command: "close" }]; + +export const DisabledNextStep: WizardButton = [msg("Next"), { command: "next" }, true]; diff --git a/web/src/components/ak-wizard-main/stories/ak-demo-wizard.ts b/web/src/components/ak-wizard-main/stories/ak-demo-wizard.ts new file mode 100644 index 000000000..30322d666 --- /dev/null +++ b/web/src/components/ak-wizard-main/stories/ak-demo-wizard.ts @@ -0,0 +1,47 @@ +import type { WizardStep } from "@goauthentik/components/ak-wizard-main/types"; + +import { msg } from "@lit/localize"; +import { customElement } from "@lit/reactive-element/decorators/custom-element.js"; +import { html } from "lit"; + +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFRadio from "@patternfly/patternfly/components/Radio/radio.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import { AkWizard } from "../AkWizard"; +import { BackStep, CancelWizard, CloseWizard, NextStep } from "../commonWizardButtons"; + +type WizardStateUpdate = { + message: string; +}; + +const dummySteps: WizardStep[] = [ + { + label: "Test Step1", + render: () => html`

    This space intentionally left blank today

    `, + disabled: false, + buttons: [NextStep, CancelWizard], + }, + { + label: "Test Step 2", + render: () => html`

    This space also intentionally left blank

    `, + disabled: false, + buttons: [BackStep, CloseWizard], + }, +]; + +@customElement("ak-demo-wizard") +export class ApplicationWizard extends AkWizard { + static get styles() { + return [PFBase, PFButton, PFRadio]; + } + + constructor() { + super(msg("Open Wizard"), msg("Demo Wizard"), msg("Run the demo wizard")); + this.steps = [...dummySteps]; + } + + close() { + this.frame.value!.open = false; + } +} diff --git a/web/src/components/ak-wizard-main/stories/ak-wizard-main.stories.ts b/web/src/components/ak-wizard-main/stories/ak-wizard-main.stories.ts new file mode 100644 index 000000000..0de141dc2 --- /dev/null +++ b/web/src/components/ak-wizard-main/stories/ak-wizard-main.stories.ts @@ -0,0 +1,40 @@ +import type { WizardStep } from "@goauthentik/components/ak-wizard-main/types"; +import "@goauthentik/elements/messages/MessageContainer"; +import { Meta } from "@storybook/web-components"; + +import { TemplateResult, html } from "lit"; + +import { AkWizard } from "../AkWizard"; +import "./ak-demo-wizard"; + +const metadata: Meta> = { + title: "Components / Wizard / Basic", + component: "ak-wizard-main", + parameters: { + docs: { + description: { + component: "A container for our wizard.", + }, + }, + }, +}; + +export default metadata; + +const container = (testItem: TemplateResult) => + html`
    + + + ${testItem} +
    `; + +export const OnePageWizard = () => { + return container(html` `); +}; diff --git a/web/src/components/ak-wizard-main/types.ts b/web/src/components/ak-wizard-main/types.ts new file mode 100644 index 000000000..bd29ca89b --- /dev/null +++ b/web/src/components/ak-wizard-main/types.ts @@ -0,0 +1,79 @@ +import { type LitElement, type ReactiveControllerHost, type TemplateResult } from "lit"; + +/** These are the navigation commands that the frame will send up to the controller. In the + * accompanying file, `./commonWizardButtons.ts`, you'll find a variety of Next, Back, Close, + * Cancel, and Submit buttons that can be used to send these, but these commands are also + * used by the breadcrumbs to hop around the wizard (if the wizard client so chooses to allow), + */ + +export type WizardNavCommand = + | { command: "next" } + | { command: "back" } + | { command: "close" } + | { command: "goto"; step: number }; + +/** + * The pattern for buttons being passed to the wizard. See `./commonWizardButtons.ts` for + * example implementations. The details are: Label, Command, and Disabled. + */ +export type WizardButton = [string, WizardNavCommand, boolean?]; + +/** + * Objects of this type are produced by the Controller, and are used in the Breadcrumbs to + * indicate the name of the step, whether or not it is the current step ("active"), and + * whether or not it is disabled. It is up to WizardClients to ensure that a step is + * not both "active" and "disabled". + */ + +export type WizardStepLabel = { + label: string; + index: number; + active: boolean; + disabled: boolean; +}; + +type LitControllerHost = ReactiveControllerHost & LitElement; + +export interface AkWizard extends LitControllerHost { + // Every wizard must provide a list of the steps to show. This list can change, but if it does, + // note that the *first* page must never change, and it's the responsibility of the developer to + // ensure that if the list changes that the currentStep points to the right place. + steps: WizardStep[]; + + // The index of the current step; + currentStep: number; + + // An accessor to the current step; + step: WizardStep; + + // Handle pressing the "close," "cancel," or "done" buttons. + close: () => void; + + // When a navigation event such as "next," "back," or "go to" (from the breadcrumbs) occurs. + handleNav: (_1: number | undefined) => void; + + // When a notification that the data on the live form has changed. + handleUpdate: (_1: D) => void; +} + +export interface WizardStep { + // The name of the step, as shown in the navigation. + label: string; + + // A function which returns the html for rendering the actual content of the step, its form and + // such. + render: () => TemplateResult; + + // A collection of buttons, in render order, that are to be shown in the button bar. If you can, + // always lead with the [Back] button and ensure it's in the same place every time. The + // controller's current behavior is to call the host's `handleNav()` command with the index of + // the requested step, or undefined if the command is nonsensical. + buttons: WizardButton[]; + + // If this step is "disabled," the prior step's next button will be disabled. + disabled: boolean; +} + +export interface WizardPanel extends HTMLElement { + validator?: () => boolean; +} diff --git a/web/xliff/de.xlf b/web/xliff/de.xlf index 6efa19454..732e38e11 100644 --- a/web/xliff/de.xlf +++ b/web/xliff/de.xlf @@ -1404,10 +1404,6 @@ Slug Slug - - Internal application name, used in URLs. - Interner Applikationsname, wird in URLs verwendet. - Optionally enter a group name. Applications with identical groups are shown grouped together. Geben Sie optional einen Gruppennamen ein. Anwendungen in gleicher Gruppe werden gruppiert angezeigt. @@ -1419,9 +1415,6 @@ Select a provider that this application should use. - - Backchannel providers - Select backchannel providers which augment the functionality of the main provider. @@ -1696,9 +1689,6 @@ NameID attribute - - SCIM provider is in preview. - Warning: Provider is not assigned to an application as backchannel provider. @@ -1712,32 +1702,9 @@ Run sync again Synchronisation erneut ausführen - - Application details - - - Create application - - - Additional UI settings - Weitere UI-Einstellungen - - - OAuth2/OIDC - Modern applications, APIs and Single-page applications. - - SAML - SAML - - - XML-based SSO standard. Use this if your application only supports SAML. - - - Legacy applications which don't natively support SSO. - LDAP LDAP @@ -1745,108 +1712,9 @@ Provide an LDAP interface for applications and users to authenticate against. - - Link - Link - - - Authentication method - - - LDAP details - LDAP-Details - - - Create service account - - - Create provider - Anbieter erstellen - - - Application Link - - - URL which will be opened when a user clicks on the application. - - - Method details - - - This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically. - - - By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password. - - - Web application - - - Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP) - - - Single-page applications - - - Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue) - - - Native application - - - Applications which redirect users to a non-web callback (for example, Android, iOS) - - - API - API - - - Authentication without user interaction, or machine-to-machine authentication. - - - Application type - - - Flow used when users access this application. - - - Proxy details - Proxy-Details - - - External domain - - - External domain you will be accessing the domain from. - - - Import SAML Metadata - - - Import the metadata document of the applicaation you want to configure. - - - Manual configuration - - - Manually configure SAML - - - SAML details - SAML-Details - - - URL that authentik will redirect back to after successful authentication. - - - Import SAML metadata - New application - - Create a new application. - Applications Anwendungen @@ -5933,6 +5801,76 @@ Bindings to groups/users are checked against the user of the event. Stage used to configure a WebAuthn authenticator (i.e. Yubikey, FaceID/Windows Hello). +<<<<<<< HEAD + + Internal application name used in URLs. + + + Submit + + + UI Settings + + + OAuth2/OpenID + + + Transparent Reverse Proxy + + + For transparent reverse proxies with required authentication + + + Forward Auth Single Application + + + For nginx's auth_request or traefix's forwardAuth + + + Forward Auth Domain Level + + + For nginx's auth_request or traefix's forwardAuth per root domain + + + Configure SAML provider manually + + + RADIUS Configuration + + + Configure RADIUS provider manually + + + SCIM configuration + + + Configure SCIM provider manually + + + Saving Application... + + + Authentik was unable to save this application: + + + Your application has been saved + + + In the Application: + + + In the Provider: + + + Method's display Name. + + + 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 managed outpost, this is done for you). + Custom attributes @@ -6043,6 +5981,9 @@ Bindings to groups/users are checked against the user of the event. Role Info + + + Pseudolocale (for testing) diff --git a/web/xliff/en.xlf b/web/xliff/en.xlf index 0e80e8b12..7cf38b6b7 100644 --- a/web/xliff/en.xlf +++ b/web/xliff/en.xlf @@ -1475,10 +1475,6 @@ Slug Slug - - Internal application name, used in URLs. - Internal application name, used in URLs. - Optionally enter a group name. Applications with identical groups are shown grouped together. Optionally enter a group name. Applications with identical groups are shown grouped together. @@ -1491,10 +1487,6 @@ Select a provider that this application should use. Select a provider that this application should use. - - Backchannel providers - Backchannel providers - Select backchannel providers which augment the functionality of the main provider. Select backchannel providers which augment the functionality of the main provider. @@ -1787,10 +1779,6 @@ NameID attribute NameID attribute - - SCIM provider is in preview. - SCIM provider is in preview. - Warning: Provider is not assigned to an application as backchannel provider. Warning: Provider is not assigned to an application as backchannel provider. @@ -1807,38 +1795,10 @@ Run sync again Run sync again - - Application details - Application details - - - Create application - Create application - - - Additional UI settings - Additional UI settings - - - OAuth2/OIDC - OAuth2/OIDC - Modern applications, APIs and Single-page applications. Modern applications, APIs and Single-page applications. - - SAML - SAML - - - XML-based SSO standard. Use this if your application only supports SAML. - XML-based SSO standard. Use this if your application only supports SAML. - - - Legacy applications which don't natively support SSO. - Legacy applications which don't natively support SSO. - LDAP LDAP @@ -1847,134 +1807,10 @@ Provide an LDAP interface for applications and users to authenticate against. Provide an LDAP interface for applications and users to authenticate against. - - Link - Link - - - Authentication method - Authentication method - - - LDAP details - LDAP details - - - Create service account - Create service account - - - Create provider - Create provider - - - Application Link - Application Link - - - URL which will be opened when a user clicks on the application. - URL which will be opened when a user clicks on the application. - - - Method details - Method details - - - This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically. - This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically. - - - By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password. - By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password. - - - Web application - Web application - - - Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP) - Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP) - - - Single-page applications - Single-page applications - - - Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue) - Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue) - - - Native application - Native application - - - Applications which redirect users to a non-web callback (for example, Android, iOS) - Applications which redirect users to a non-web callback (for example, Android, iOS) - - - API - API - - - Authentication without user interaction, or machine-to-machine authentication. - Authentication without user interaction, or machine-to-machine authentication. - - - Application type - Application type - - - Flow used when users access this application. - Flow used when users access this application. - - - Proxy details - Proxy details - - - External domain - External domain - - - External domain you will be accessing the domain from. - External domain you will be accessing the domain from. - - - Import SAML Metadata - Import SAML Metadata - - - Import the metadata document of the applicaation you want to configure. - Import the metadata document of the applicaation you want to configure. - - - Manual configuration - Manual configuration - - - Manually configure SAML - Manually configure SAML - - - SAML details - SAML details - - - URL that authentik will redirect back to after successful authentication. - URL that authentik will redirect back to after successful authentication. - - - Import SAML metadata - Import SAML metadata - New application New application - - Create a new application. - Create a new application. - Applications Applications @@ -6247,6 +6083,76 @@ Bindings to groups/users are checked against the user of the event. Stage used to configure a WebAuthn authenticator (i.e. Yubikey, FaceID/Windows Hello). +<<<<<<< HEAD + + Internal application name used in URLs. + + + Submit + + + UI Settings + + + OAuth2/OpenID + + + Transparent Reverse Proxy + + + For transparent reverse proxies with required authentication + + + Forward Auth Single Application + + + For nginx's auth_request or traefix's forwardAuth + + + Forward Auth Domain Level + + + For nginx's auth_request or traefix's forwardAuth per root domain + + + Configure SAML provider manually + + + RADIUS Configuration + + + Configure RADIUS provider manually + + + SCIM configuration + + + Configure SCIM provider manually + + + Saving Application... + + + Authentik was unable to save this application: + + + Your application has been saved + + + In the Application: + + + In the Provider: + + + Method's display Name. + + + 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 managed outpost, this is done for you). + Custom attributes @@ -6357,6 +6263,9 @@ Bindings to groups/users are checked against the user of the event. Role Info + + + Pseudolocale (for testing) diff --git a/web/xliff/es.xlf b/web/xliff/es.xlf index d9fc14013..308666c0a 100644 --- a/web/xliff/es.xlf +++ b/web/xliff/es.xlf @@ -1378,10 +1378,6 @@ Slug babosa - - Internal application name, used in URLs. - Nombre de la aplicación interna, utilizado en las URL. - Optionally enter a group name. Applications with identical groups are shown grouped together. @@ -1392,9 +1388,6 @@ Select a provider that this application should use. - - Backchannel providers - Select backchannel providers which augment the functionality of the main provider. @@ -1668,9 +1661,6 @@ NameID attribute - - SCIM provider is in preview. - Warning: Provider is not assigned to an application as backchannel provider. @@ -1684,30 +1674,9 @@ Run sync again Vuelve a ejecutar la sincronización - - Application details - - - Create application - - - Additional UI settings - - - OAuth2/OIDC - Modern applications, APIs and Single-page applications. - - SAML - - - XML-based SSO standard. Use this if your application only supports SAML. - - - Legacy applications which don't natively support SSO. - LDAP LDAP @@ -1715,103 +1684,9 @@ Provide an LDAP interface for applications and users to authenticate against. - - Link - - - Authentication method - - - LDAP details - - - Create service account - - - Create provider - Crear proveedor - - - Application Link - - - URL which will be opened when a user clicks on the application. - - - Method details - - - This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically. - - - By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password. - - - Web application - - - Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP) - - - Single-page applications - - - Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue) - - - Native application - - - Applications which redirect users to a non-web callback (for example, Android, iOS) - - - API - - - Authentication without user interaction, or machine-to-machine authentication. - - - Application type - - - Flow used when users access this application. - - - Proxy details - - - External domain - - - External domain you will be accessing the domain from. - - - Import SAML Metadata - - - Import the metadata document of the applicaation you want to configure. - - - Manual configuration - - - Manually configure SAML - - - SAML details - - - URL that authentik will redirect back to after successful authentication. - - - Import SAML metadata - New application - - Create a new application. - Applications Aplicaciones @@ -5841,6 +5716,76 @@ Bindings to groups/users are checked against the user of the event. Stage used to configure a WebAuthn authenticator (i.e. Yubikey, FaceID/Windows Hello). +<<<<<<< HEAD + + Internal application name used in URLs. + + + Submit + + + UI Settings + + + OAuth2/OpenID + + + Transparent Reverse Proxy + + + For transparent reverse proxies with required authentication + + + Forward Auth Single Application + + + For nginx's auth_request or traefix's forwardAuth + + + Forward Auth Domain Level + + + For nginx's auth_request or traefix's forwardAuth per root domain + + + Configure SAML provider manually + + + RADIUS Configuration + + + Configure RADIUS provider manually + + + SCIM configuration + + + Configure SCIM provider manually + + + Saving Application... + + + Authentik was unable to save this application: + + + Your application has been saved + + + In the Application: + + + In the Provider: + + + Method's display Name. + + + 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 managed outpost, this is done for you). + Custom attributes @@ -5951,6 +5896,9 @@ Bindings to groups/users are checked against the user of the event. Role Info + + + Pseudolocale (for testing) diff --git a/web/xliff/fr.xlf b/web/xliff/fr.xlf index d4f94ea4c..15eb5b863 100644 --- a/web/xliff/fr.xlf +++ b/web/xliff/fr.xlf @@ -1,4 +1,4 @@ - + @@ -613,9 +613,9 @@ Il y a jour(s) - The URL "" was not found. - L'URL " - " n'a pas été trouvée. + The URL "" was not found. + L'URL " + " n'a pas été trouvée. @@ -1067,8 +1067,8 @@ Il y a jour(s) - To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have. - Pour permettre n'importe quelle URI de redirection, définissez cette valeur sur ".*". Soyez conscient des possibles implications de sécurité que cela peut avoir. + To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have. + Pour permettre n'importe quelle URI de redirection, définissez cette valeur sur ".*". Soyez conscient des possibles implications de sécurité que cela peut avoir. @@ -1640,7 +1640,7 @@ Il y a jour(s) Token to authenticate with. Currently only bearer authentication is supported. - Jeton d'authentification à utiliser. Actuellement, seule l'authentification "bearer authentication" est prise en charge. + Jeton d'authentification à utiliser. Actuellement, seule l'authentification "bearer authentication" est prise en charge. @@ -1808,8 +1808,8 @@ Il y a jour(s) - Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test". - Entrez une URL complète, un chemin relatif ou utilisez 'fa://fa-test' pour utiliser l'icône Font Awesome "fa-test". + Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test". + Entrez une URL complète, un chemin relatif ou utilisez 'fa://fa-test' pour utiliser l'icône Font Awesome "fa-test". @@ -1836,11 +1836,6 @@ Il y a jour(s) Slug Slug - - - Internal application name, used in URLs. - Nom de l'application interne, utilisé dans les URLs. - Optionally enter a group name. Applications with identical groups are shown grouped together. @@ -1856,11 +1851,6 @@ Il y a jour(s) Select a provider that this application should use. Sélectionnez un fournisseur que cette application doit utiliser. - - - Backchannel providers - Fournisseurs backchannel - Select backchannel providers which augment the functionality of the main provider. @@ -2226,11 +2216,6 @@ Il y a jour(s) NameID attribute Attribut NameID - - - SCIM provider is in preview. - Le fournisseur SCIM est en aperçu. - Warning: Provider is not assigned to an application as backchannel provider. @@ -2251,46 +2236,11 @@ Il y a jour(s) Run sync again Relancer la synchro - - - Application details - Détails de l'application - - - - Create application - Créer une application - - - - Additional UI settings - Paramètres d'interface additionnels - - - - OAuth2/OIDC - OAuth2/OIDC - Modern applications, APIs and Single-page applications. Applications modernes, API et applications à page unique. - - - SAML - SAML - - - - XML-based SSO standard. Use this if your application only supports SAML. - Norme SSO basée sur XML. Utilisez cette option si votre application ne soutient que SAML. - - - - Legacy applications which don't natively support SSO. - Les applications anciennes qui ne supportent pas nativement un SSO. - LDAP @@ -2301,166 +2251,11 @@ Il y a jour(s) Provide an LDAP interface for applications and users to authenticate against. Fournir une interface LDAP permettant aux applications et aux utilisateurs de s'authentifier. - - - Link - Lien - - - - Authentication method - Méthode d'authentification - - - - LDAP details - Détails LDAP - - - - Create service account - Créer un compte de service - - - - Create provider - Créer un fournisseur - - - - Application Link - Lien de l’application - - - - URL which will be opened when a user clicks on the application. - URL qui sera ouverte lorsqu'un utilisateur clique sur l'application. - - - - Method details - Détails de la méthode - - - - This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically. - Cette configuration peut être utilisée pour s'authentifier auprès d'authentik avec d'autres API ou de manière programmatique. - - - - By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password. - Par défaut, tous les comptes de services peuvent se connecter à cette application, tant qu'ils ont un jeton valide du type app-password. - - - - Web application - Application Web - - - - Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP) - Applications qui s’occupent de l’authentification côté serveur (par exemple Python, Go, Rust, Java, PHP) - - - - Single-page applications - Applications à page unique - - - - Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue) - Applications à page unique qui gèrent l'authentification dans le navigateur (par exemple, Javascript, Angular, React, Vue). - - - - Native application - Application native - - - - Applications which redirect users to a non-web callback (for example, Android, iOS) - Applications qui redirigent les utilisateurs vers un callback non web (par exemple, Android, iOS) - - - - API - API - - - - Authentication without user interaction, or machine-to-machine authentication. - Authentification sans interaction avec l'utilisateur, ou authentification de machine à machine. - - - - Application type - Type d’application - - - - Flow used when users access this application. - Flux utilisé lorsque les utilisateurs accèdent à cette application. - - - - Proxy details - Détails du Proxy - - - - External domain - Domaine externe - - - - External domain you will be accessing the domain from. - Domaine externe à partir duquel vous accéderez au domaine. - - - - Import SAML Metadata - Importer des métadonnées SAML - - - - Import the metadata document of the applicaation you want to configure. - Importez le document de métadonnées de l'application que vous souhaitez configurer. - - - - Manual configuration - Configuration manuelle - - - - Manually configure SAML - Configurer SAML manuellement - - - - SAML details - Détails SAML - - - - URL that authentik will redirect back to after successful authentication. - URL vers laquelle authentik redirigera après une authentification réussie. - - - - Import SAML metadata - Importer des métadonnées SAML - New application Nouvelle application - - - Create a new application. - Créer une nouvelle application. - Applications @@ -3142,7 +2937,7 @@ doesn't pass when either or both of the selected options are equal or above the To use SSL instead, use 'ldaps://' and disable this option. - Pour utiliser SSL à la base, utilisez "ldaps://" et désactviez cette option. + Pour utiliser SSL à la base, utilisez "ldaps://" et désactviez cette option. @@ -3231,8 +3026,8 @@ doesn't pass when either or both of the selected options are equal or above the - Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...' - Champ qui contient les membres d'un groupe. Si vous utilisez le champ "memberUid", la valeur est censée contenir un nom distinctif relatif, par exemple 'memberUid=un-utilisateur' au lieu de 'memberUid=cn=un-utilisateur,ou=groups,...' + Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...' + Champ qui contient les membres d'un groupe. Si vous utilisez le champ "memberUid", la valeur est censée contenir un nom distinctif relatif, par exemple 'memberUid=un-utilisateur' au lieu de 'memberUid=cn=un-utilisateur,ou=groups,...' @@ -3527,7 +3322,7 @@ doesn't pass when either or both of the selected options are equal or above the Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format 'transient', and the user doesn't log out manually. - Moment où les utilisateurs temporaires doivent être supprimés. Cela ne s'applique que si votre IDP utilise le format NameID "transient" et que l'utilisateur ne se déconnecte pas manuellement. + Moment où les utilisateurs temporaires doivent être supprimés. Cela ne s'applique que si votre IDP utilise le format NameID "transient" et que l'utilisateur ne se déconnecte pas manuellement. @@ -3695,7 +3490,7 @@ doesn't pass when either or both of the selected options are equal or above the Optionally set the 'FriendlyName' value of the Assertion attribute. - Indiquer la valeur "FriendlyName" de l'attribut d'assertion (optionnel) + Indiquer la valeur "FriendlyName" de l'attribut d'assertion (optionnel) @@ -4024,8 +3819,8 @@ doesn't pass when either or both of the selected options are equal or above the - When using an external logging solution for archiving, this can be set to "minutes=5". - En cas d'utilisation d'une solution de journalisation externe pour l'archivage, cette valeur peut être fixée à "minutes=5". + When using an external logging solution for archiving, this can be set to "minutes=5". + En cas d'utilisation d'une solution de journalisation externe pour l'archivage, cette valeur peut être fixée à "minutes=5". @@ -4034,8 +3829,8 @@ doesn't pass when either or both of the selected options are equal or above the - Format: "weeks=3;days=2;hours=3,seconds=2". - Format : "weeks=3;days=2;hours=3,seconds=2". + Format: "weeks=3;days=2;hours=3,seconds=2". + Format : "weeks=3;days=2;hours=3,seconds=2". @@ -4231,10 +4026,10 @@ doesn't pass when either or both of the selected options are equal or above the - Are you sure you want to update ""? + Are you sure you want to update ""? Êtes-vous sûr de vouloir mettre à jour - " - " ? + " + " ? @@ -5330,8 +5125,8 @@ doesn't pass when either or both of the selected options are equal or above the - A "roaming" authenticator, like a YubiKey - Un authentificateur "itinérant", comme une YubiKey + A "roaming" authenticator, like a YubiKey + Un authentificateur "itinérant", comme une YubiKey @@ -5656,7 +5451,7 @@ doesn't pass when either or both of the selected options are equal or above the Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable. - Afficher des champs de saisie arbitraires à l'utilisateur, par exemple pendant l'inscription. Les données sont enregistrées dans le contexte du flux sous la variable "prompt_data". + Afficher des champs de saisie arbitraires à l'utilisateur, par exemple pendant l'inscription. Les données sont enregistrées dans le contexte du flux sous la variable "prompt_data". @@ -5665,10 +5460,10 @@ doesn't pass when either or both of the selected options are equal or above the - ("", of type ) + ("", of type ) - (" - ", de type + (" + ", de type ) @@ -5717,8 +5512,8 @@ doesn't pass when either or both of the selected options are equal or above the - If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here. - Si défini à une durée supérieure à 0, l'utilisateur aura la possibilité de choisir de "rester connecté", ce qui prolongera sa session jusqu'à la durée spécifiée ici. + If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here. + Si défini à une durée supérieure à 0, l'utilisateur aura la possibilité de choisir de "rester connecté", ce qui prolongera sa session jusqu'à la durée spécifiée ici. @@ -6502,7 +6297,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system. - Peut être au format "unix://" pour une connexion à un service docker local, "ssh://" pour une connexion via SSH, ou "https://:2376" pour une connexion à un système distant. + Peut être au format "unix://" pour une connexion à un service docker local, "ssh://" pour une connexion via SSH, ou "https://:2376" pour une connexion à un système distant. @@ -7809,7 +7604,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti 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 managed outpost, this is done for you). - Utilisez ce fournisseur avec l'option "auth_request" de Nginx ou "forwardAuth" de Traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, "/outpost.goauthentik.io" doit être routé vers le poste avancé (lorsque vous utilisez un poste avancé géré, cela est fait pour vous). + Utilisez ce fournisseur avec l'option "auth_request" de Nginx ou "forwardAuth" de Traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, "/outpost.goauthentik.io" doit être routé vers le poste avancé (lorsque vous utilisez un poste avancé géré, cela est fait pour vous). Default relay state @@ -7827,6 +7622,76 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti Stage used to configure a WebAuthn authenticator (i.e. Yubikey, FaceID/Windows Hello). Étape de configuration d'un authentificateur WebAuthn (Yubikey, FaceID/Windows Hello). +<<<<<<< HEAD + + Internal application name used in URLs. + + + Submit + + + UI Settings + + + OAuth2/OpenID + + + Transparent Reverse Proxy + + + For transparent reverse proxies with required authentication + + + Forward Auth Single Application + + + For nginx's auth_request or traefix's forwardAuth + + + Forward Auth Domain Level + + + For nginx's auth_request or traefix's forwardAuth per root domain + + + Configure SAML provider manually + + + RADIUS Configuration + + + Configure RADIUS provider manually + + + SCIM configuration + + + Configure SCIM provider manually + + + Saving Application... + + + Authentik was unable to save this application: + + + Your application has been saved + + + In the Application: + + + In the Provider: + + + Method's display Name. + + + 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 managed outpost, this is done for you). + Custom attributes Attributs personnalisés @@ -7974,7 +7839,10 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti Role Info Informations du rôle + + + Pseudolocale (for testing) - \ No newline at end of file + diff --git a/web/xliff/pl.xlf b/web/xliff/pl.xlf index 0f43daabe..62823319b 100644 --- a/web/xliff/pl.xlf +++ b/web/xliff/pl.xlf @@ -1418,10 +1418,6 @@ Slug Ślimak - - Internal application name, used in URLs. - Wewnętrzna nazwa aplikacji, używana w adresach URL. - Optionally enter a group name. Applications with identical groups are shown grouped together. Opcjonalnie wprowadź nazwę grupy. Aplikacje z identycznymi grupami są wyświetlane razem. @@ -1433,9 +1429,6 @@ Select a provider that this application should use. - - Backchannel providers - Select backchannel providers which augment the functionality of the main provider. @@ -1722,9 +1715,6 @@ NameID attribute Atrybut NameID - - SCIM provider is in preview. - Warning: Provider is not assigned to an application as backchannel provider. @@ -1738,38 +1728,10 @@ Run sync again Uruchom ponownie synchronizację - - Application details - Szczegóły aplikacji - - - Create application - Utwórz aplikację - - - Additional UI settings - Dodatkowe ustawienia interfejsu użytkownika - - - OAuth2/OIDC - OAuth2/OIDC - Modern applications, APIs and Single-page applications. Nowoczesne aplikacje, API i aplikacje jednostronicowe. - - SAML - SAML - - - XML-based SSO standard. Use this if your application only supports SAML. - Standard SSO oparty na XML. Użyj tego, jeśli Twoja aplikacja obsługuje tylko SAML. - - - Legacy applications which don't natively support SSO. - Starsze aplikacje, które nie obsługują natywnego logowania jednokrotnego. - LDAP LDAP @@ -1777,128 +1739,10 @@ Provide an LDAP interface for applications and users to authenticate against. - - Link - Link - - - Authentication method - Metoda Uwierzytelnienia - - - LDAP details - Szczegóły LDAP - - - Create service account - Utwórz konto usługi - - - Create provider - Utwórz dostawcę - - - Application Link - Link do aplikacji - - - URL which will be opened when a user clicks on the application. - - - Method details - Szczegóły metody - - - This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically. - - - By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password. - - - Web application - Aplikacja internetowa - - - Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP) - Aplikacje obsługujące uwierzytelnianie po stronie serwera (na przykład Python, Go, Rust, Java, PHP) - - - Single-page applications - Aplikacje jednostronicowe - - - Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue) - - - Native application - Natywna aplikacja - - - Applications which redirect users to a non-web callback (for example, Android, iOS) - Aplikacje, które przekierowują użytkowników do nie internetowych callback (na przykład Android, iOS) - - - API - API - - - Authentication without user interaction, or machine-to-machine authentication. - Uwierzytelnianie bez interakcji użytkownika lub uwierzytelnianie między maszynami. - - - Application type - Typ aplikacji - - - Flow used when users access this application. - Przepływ używany, gdy użytkownicy uzyskują dostęp do tej aplikacji. - - - Proxy details - Dane proxy - - - External domain - Zewnętrzna domena - - - External domain you will be accessing the domain from. - Domena zewnętrzna, z której będziesz uzyskiwać dostęp do domeny. - - - Import SAML Metadata - Importuj Metadane SAML - - - Import the metadata document of the applicaation you want to configure. - - - Manual configuration - Ręczna konfiguracja - - - Manually configure SAML - Ręcznie skonfiguruj SAML - - - SAML details - Szczegóły SAML - - - URL that authentik will redirect back to after successful authentication. - - - Import SAML metadata - Importuj metadane SAML - New application Nowa aplikacja - - Create a new application. - Utwórz nową aplikację - Applications Aplikacje @@ -6080,6 +5924,76 @@ Bindings to groups/users are checked against the user of the event. Stage used to configure a WebAuthn authenticator (i.e. Yubikey, FaceID/Windows Hello). +<<<<<<< HEAD + + Internal application name used in URLs. + + + Submit + + + UI Settings + + + OAuth2/OpenID + + + Transparent Reverse Proxy + + + For transparent reverse proxies with required authentication + + + Forward Auth Single Application + + + For nginx's auth_request or traefix's forwardAuth + + + Forward Auth Domain Level + + + For nginx's auth_request or traefix's forwardAuth per root domain + + + Configure SAML provider manually + + + RADIUS Configuration + + + Configure RADIUS provider manually + + + SCIM configuration + + + Configure SCIM provider manually + + + Saving Application... + + + Authentik was unable to save this application: + + + Your application has been saved + + + In the Application: + + + In the Provider: + + + Method's display Name. + + + 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 managed outpost, this is done for you). + Custom attributes @@ -6190,6 +6104,9 @@ Bindings to groups/users are checked against the user of the event. Role Info + + + Pseudolocale (for testing) diff --git a/web/xliff/pseudo-LOCALE.xlf b/web/xliff/pseudo-LOCALE.xlf index c4fa2d82f..3c033e526 100644 --- a/web/xliff/pseudo-LOCALE.xlf +++ b/web/xliff/pseudo-LOCALE.xlf @@ -1817,11 +1817,6 @@ Slug Śĺũĝ - - - Internal application name, used in URLs. - Ĩńţēŕńàĺ àƥƥĺĩćàţĩōń ńàmē, ũśēď ĩń ŨŔĹś. - Optionally enter a group name. Applications with identical groups are shown grouped together. @@ -1837,11 +1832,6 @@ Select a provider that this application should use. Śēĺēćţ à ƥŕōvĩďēŕ ţĥàţ ţĥĩś àƥƥĺĩćàţĩōń śĥōũĺď ũśē. - - - Backchannel providers - ßàćķćĥàńńēĺ ƥŕōvĩďēŕś - Select backchannel providers which augment the functionality of the main provider. @@ -2206,7 +2196,6 @@ NameID attribute ŃàmēĨĎ àţţŕĩƀũţē - Warning: Provider is not assigned to an application as backchannel provider. @@ -2227,46 +2216,11 @@ Run sync again Ŕũń śŷńć àĝàĩń - - - Application details - Àƥƥĺĩćàţĩōń ďēţàĩĺś - - - - Create application - Ćŕēàţē àƥƥĺĩćàţĩōń - - - - Additional UI settings - Àďďĩţĩōńàĺ ŨĨ śēţţĩńĝś - - - - OAuth2/OIDC - ŌÀũţĥ2/ŌĨĎĆ - Modern applications, APIs and Single-page applications. Mōďēŕń àƥƥĺĩćàţĩōńś, ÀƤĨś àńď Śĩńĝĺē-ƥàĝē àƥƥĺĩćàţĩōńś. - - - SAML - ŚÀMĹ - - - - XML-based SSO standard. Use this if your application only supports SAML. - XMĹ-ƀàśēď ŚŚŌ śţàńďàŕď. Ũśē ţĥĩś ĩƒ ŷōũŕ àƥƥĺĩćàţĩōń ōńĺŷ śũƥƥōŕţś ŚÀMĹ. - - - - Legacy applications which don't natively support SSO. - Ĺēĝàćŷ àƥƥĺĩćàţĩōńś ŵĥĩćĥ ďōń'ţ ńàţĩvēĺŷ śũƥƥōŕţ ŚŚŌ. - LDAP @@ -2277,166 +2231,11 @@ Provide an LDAP interface for applications and users to authenticate against. Ƥŕōvĩďē àń ĹĎÀƤ ĩńţēŕƒàćē ƒōŕ àƥƥĺĩćàţĩōńś àńď ũśēŕś ţō àũţĥēńţĩćàţē àĝàĩńśţ. - - - Link - Ĺĩńķ - - - - Authentication method - Àũţĥēńţĩćàţĩōń mēţĥōď - - - - LDAP details - ĹĎÀƤ ďēţàĩĺś - - - - Create service account - Ćŕēàţē śēŕvĩćē àććōũńţ - - - - Create provider - Ćŕēàţē ƥŕōvĩďēŕ - - - - Application Link - Àƥƥĺĩćàţĩōń Ĺĩńķ - - - - URL which will be opened when a user clicks on the application. - ŨŔĹ ŵĥĩćĥ ŵĩĺĺ ƀē ōƥēńēď ŵĥēń à ũśēŕ ćĺĩćķś ōń ţĥē àƥƥĺĩćàţĩōń. - - - - Method details - Mēţĥōď ďēţàĩĺś - - - - This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically. - Ţĥĩś ćōńƒĩĝũŕàţĩōń ćàń ƀē ũśēď ţō àũţĥēńţĩćàţē ţō àũţĥēńţĩķ ŵĩţĥ ōţĥēŕ ÀƤĨś ōţĥēŕ ōţĥēŕŵĩśē ƥŕōĝŕàmmàţĩćàĺĺŷ. - - - - By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password. - ßŷ ďēƒàũĺţ, àĺĺ śēŕvĩćē àććōũńţś ćàń àũţĥēńţĩćàţē àś ţĥĩś àƥƥĺĩćàţĩōń, àś ĺōńĝ àś ţĥēŷ ĥàvē à vàĺĩď ţōķēń ōƒ ţĥē ţŷƥē àƥƥ-ƥàśśŵōŕď. - - - - Web application - Ŵēƀ àƥƥĺĩćàţĩōń - - - - Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP) - Àƥƥĺĩćàţĩōńś ŵĥĩćĥ ĥàńďĺē ţĥē àũţĥēńţĩćàţĩōń śēŕvēŕ-śĩďē (ƒōŕ ēxàmƥĺē, Ƥŷţĥōń, Ĝō, Ŕũśţ, ĵàvà, ƤĤƤ) - - - - Single-page applications - Śĩńĝĺē-ƥàĝē àƥƥĺĩćàţĩōńś - - - - Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue) - Śĩńĝĺē-ƥàĝē àƥƥĺĩćàţĩōńś ŵĥĩćĥ ĥàńďĺē àũţĥēńţĩćàţĩōń ĩń ţĥē ƀŕōŵśēŕ (ƒōŕ ēxàmƥĺē, ĵàvàśćŕĩƥţ, Àńĝũĺàŕ, Ŕēàćţ, Vũē) - - - - Native application - Ńàţĩvē àƥƥĺĩćàţĩōń - - - - Applications which redirect users to a non-web callback (for example, Android, iOS) - Àƥƥĺĩćàţĩōńś ŵĥĩćĥ ŕēďĩŕēćţ ũśēŕś ţō à ńōń-ŵēƀ ćàĺĺƀàćķ (ƒōŕ ēxàmƥĺē, Àńďŕōĩď, ĩŌŚ) - - - - API - ÀƤĨ - - - - Authentication without user interaction, or machine-to-machine authentication. - Àũţĥēńţĩćàţĩōń ŵĩţĥōũţ ũśēŕ ĩńţēŕàćţĩōń, ōŕ màćĥĩńē-ţō-màćĥĩńē àũţĥēńţĩćàţĩōń. - - - - Application type - Àƥƥĺĩćàţĩōń ţŷƥē - - - - Flow used when users access this application. - Ƒĺōŵ ũśēď ŵĥēń ũśēŕś àććēśś ţĥĩś àƥƥĺĩćàţĩōń. - - - - Proxy details - Ƥŕōxŷ ďēţàĩĺś - - - - External domain - Ēxţēŕńàĺ ďōmàĩń - - - - External domain you will be accessing the domain from. - Ēxţēŕńàĺ ďōmàĩń ŷōũ ŵĩĺĺ ƀē àććēśśĩńĝ ţĥē ďōmàĩń ƒŕōm. - - - - Import SAML Metadata - Ĩmƥōŕţ ŚÀMĹ Mēţàďàţà - - - - Import the metadata document of the applicaation you want to configure. - Ĩmƥōŕţ ţĥē mēţàďàţà ďōćũmēńţ ōƒ ţĥē àƥƥĺĩćààţĩōń ŷōũ ŵàńţ ţō ćōńƒĩĝũŕē. - - - - Manual configuration - Màńũàĺ ćōńƒĩĝũŕàţĩōń - - - - Manually configure SAML - Màńũàĺĺŷ ćōńƒĩĝũŕē ŚÀMĹ - - - - SAML details - ŚÀMĹ ďēţàĩĺś - - - - URL that authentik will redirect back to after successful authentication. - ŨŔĹ ţĥàţ àũţĥēńţĩķ ŵĩĺĺ ŕēďĩŕēćţ ƀàćķ ţō àƒţēŕ śũććēśśƒũĺ àũţĥēńţĩćàţĩōń. - - - - Import SAML metadata - Ĩmƥōŕţ ŚÀMĹ mēţàďàţà - New application Ńēŵ àƥƥĺĩćàţĩōń - - - Create a new application. - Ćŕēàţē à ńēŵ àƥƥĺĩćàţĩōń. - Applications @@ -7762,6 +7561,101 @@ Bindings to groups/users are checked against the user of the event. Stage used to configure a WebAuthn authenticator (i.e. Yubikey, FaceID/Windows Hello). Śţàĝē ũśēď ţō ćōńƒĩĝũŕē à ŴēƀÀũţĥń àũţĥēńţĩćàţōŕ (ĩ.ē. Ŷũƀĩķēŷ, ƑàćēĨĎ/Ŵĩńďōŵś Ĥēĺĺō). +<<<<<<< HEAD + + Internal application name used in URLs. + Ĩńţēŕńàĺ àƥƥĺĩćàţĩōń ńàmē ũśēď ĩń ŨŔĹś. + + + Submit + Śũƀmĩţ + + + UI Settings + ŨĨ Śēţţĩńĝś + + + OAuth2/OpenID + ŌÀũţĥ2/ŌƥēńĨĎ + + + Transparent Reverse Proxy + Ţŕàńśƥàŕēńţ Ŕēvēŕśē Ƥŕōxŷ + + + For transparent reverse proxies with required authentication + Ƒōŕ ţŕàńśƥàŕēńţ ŕēvēŕśē ƥŕōxĩēś ŵĩţĥ ŕēǫũĩŕēď àũţĥēńţĩćàţĩōń + + + Forward Auth Single Application + Ƒōŕŵàŕď Àũţĥ Śĩńĝĺē Àƥƥĺĩćàţĩōń + + + For nginx's auth_request or traefix's forwardAuth + Ƒōŕ ńĝĩńx'ś àũţĥ_ŕēǫũēśţ ōŕ ţŕàēƒĩx'ś ƒōŕŵàŕďÀũţĥ + + + Forward Auth Domain Level + Ƒōŕŵàŕď Àũţĥ Ďōmàĩń Ĺēvēĺ + + + For nginx's auth_request or traefix's forwardAuth per root domain + Ƒōŕ ńĝĩńx'ś àũţĥ_ŕēǫũēśţ ōŕ ţŕàēƒĩx'ś ƒōŕŵàŕďÀũţĥ ƥēŕ ŕōōţ ďōmàĩń + + + Configure SAML provider manually + Ćōńƒĩĝũŕē ŚÀMĹ ƥŕōvĩďēŕ màńũàĺĺŷ + + + RADIUS Configuration + ŔÀĎĨŨŚ Ćōńƒĩĝũŕàţĩōń + + + Configure RADIUS provider manually + Ćōńƒĩĝũŕē ŔÀĎĨŨŚ ƥŕōvĩďēŕ màńũàĺĺŷ + + + SCIM configuration + ŚĆĨM ćōńƒĩĝũŕàţĩōń + + + Configure SCIM provider manually + Ćōńƒĩĝũŕē ŚĆĨM ƥŕōvĩďēŕ màńũàĺĺŷ + + + Saving Application... + Śàvĩńĝ Àƥƥĺĩćàţĩōń... + + + Authentik was unable to save this application: + Àũţĥēńţĩķ ŵàś ũńàƀĺē ţō śàvē ţĥĩś àƥƥĺĩćàţĩōń: + + + Your application has been saved + Ŷōũŕ àƥƥĺĩćàţĩōń ĥàś ƀēēń śàvēď + + + In the Application: + Ĩń ţĥē Àƥƥĺĩćàţĩōń: + + + In the Provider: + Ĩń ţĥē Ƥŕōvĩďēŕ: + + + Method's display Name. + Mēţĥōď'ś ďĩśƥĺàŷ Ńàmē. + + + 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 managed outpost, this is done for you). + Ũśē ţĥĩś ƥŕōvĩďēŕ ŵĩţĥ ńĝĩńx'ś àũţĥ_ŕēǫũēśţ ōŕ ţŕàēƒĩķ'ś + ƒōŕŵàŕďÀũţĥ. Ēàćĥ àƥƥĺĩćàţĩōń/ďōmàĩń ńēēďś ĩţś ōŵń ƥŕōvĩďēŕ. + Àďďĩţĩōńàĺĺŷ, ōń ēàćĥ ďōmàĩń, /ōũţƥōśţ.ĝōàũţĥēńţĩķ.ĩō mũśţ ƀē + ŕōũţēď ţō ţĥē ōũţƥōśţ (ŵĥēń ũśĩńĝ à màńàĝēď ōũţƥōśţ, ţĥĩś ĩś ďōńē ƒōŕ ŷōũ). + Custom attributes Ćũśţōm àţţŕĩƀũţēś @@ -7776,109 +7670,142 @@ Bindings to groups/users are checked against the user of the event. Failed to fetch + Ƒàĩĺēď ţō ƒēţćĥ Failed to fetch data. + Ƒàĩĺēď ţō ƒēţćĥ ďàţà. Successfully assigned permission. + Śũććēśśƒũĺĺŷ àśśĩĝńēď ƥēŕmĩśśĩōń. Role + Ŕōĺē Assign + Àśśĩĝń Assign permission to role + Àśśĩĝń ƥēŕmĩśśĩōń ţō ŕōĺē Assign to new role + Àśśĩĝń ţō ńēŵ ŕōĺē Directly assigned + Ďĩŕēćţĺŷ àśśĩĝńēď Assign permission to user + Àśśĩĝń ƥēŕmĩśśĩōń ţō ũśēŕ Assign to new user + Àśśĩĝń ţō ńēŵ ũśēŕ User Object Permissions + Ũśēŕ ŌƀĴēćţ Ƥēŕmĩśśĩōńś Role Object Permissions + Ŕōĺē ŌƀĴēćţ Ƥēŕmĩśśĩōńś Roles + Ŕōĺēś Select roles to grant this groups' users' permissions from the selected roles. + Śēĺēćţ ŕōĺēś ţō ĝŕàńţ ţĥĩś ĝŕōũƥś' ũśēŕś' ƥēŕmĩśśĩōńś ƒŕōm ţĥē śēĺēćţēď ŕōĺēś. Update Permissions + Ũƥďàţē Ƥēŕmĩśśĩōńś Editing is disabled for managed tokens + Ēďĩţĩńĝ ĩś ďĩśàƀĺēď ƒōŕ màńàĝēď ţōķēńś Select permissions to grant + Śēĺēćţ ƥēŕmĩśśĩōńś ţō ĝŕàńţ Permissions to add + Ƥēŕmĩśśĩōńś ţō àďď Select permissions + Śēĺēćţ ƥēŕmĩśśĩōńś Assign permission + Àśśĩĝń ƥēŕmĩśśĩōń Permission(s) + Ƥēŕmĩśśĩōń(ś) Permission + Ƥēŕmĩśśĩōń User doesn't have view permission so description cannot be retrieved. + Ũśēŕ ďōēśń'ţ ĥàvē vĩēŵ ƥēŕmĩśśĩōń śō ďēśćŕĩƥţĩōń ćàńńōţ ƀē ŕēţŕĩēvēď. Assigned permissions + Àśśĩĝńēď ƥēŕmĩśśĩōńś Assigned global permissions + Àśśĩĝńēď ĝĺōƀàĺ ƥēŕmĩśśĩōńś Assigned object permissions + Àśśĩĝńēď ōƀĴēćţ ƥēŕmĩśśĩōńś Successfully updated role. + Śũććēśśƒũĺĺŷ ũƥďàţēď ŕōĺē. Successfully created role. + Śũććēśśƒũĺĺŷ ćŕēàţēď ŕōĺē. Manage roles which grant permissions to objects within authentik. + Màńàĝē ŕōĺēś ŵĥĩćĥ ĝŕàńţ ƥēŕmĩśśĩōńś ţō ōƀĴēćţś ŵĩţĥĩń àũţĥēńţĩķ. Role(s) + Ŕōĺē(ś) Update Role + Ũƥďàţē Ŕōĺē Create Role + Ćŕēàţē Ŕōĺē Role doesn't have view permission so description cannot be retrieved. + Ŕōĺē ďōēśń'ţ ĥàvē vĩēŵ ƥēŕmĩśśĩōń śō ďēśćŕĩƥţĩōń ćàńńōţ ƀē ŕēţŕĩēvēď. Role + Ŕōĺē Role Info + Ŕōĺē Ĩńƒō - - - + diff --git a/web/xliff/tr.xlf b/web/xliff/tr.xlf index a2d79415f..0d8e200a7 100644 --- a/web/xliff/tr.xlf +++ b/web/xliff/tr.xlf @@ -1377,10 +1377,6 @@ Slug Kısa İsim - - Internal application name, used in URLs. - URL'lerde kullanılan dahili uygulama adı. - Optionally enter a group name. Applications with identical groups are shown grouped together. @@ -1391,9 +1387,6 @@ Select a provider that this application should use. - - Backchannel providers - Select backchannel providers which augment the functionality of the main provider. @@ -1667,9 +1660,6 @@ NameID attribute - - SCIM provider is in preview. - Warning: Provider is not assigned to an application as backchannel provider. @@ -1683,30 +1673,9 @@ Run sync again Eşzamanlamayı tekrar çalıştır - - Application details - - - Create application - - - Additional UI settings - - - OAuth2/OIDC - Modern applications, APIs and Single-page applications. - - SAML - - - XML-based SSO standard. Use this if your application only supports SAML. - - - Legacy applications which don't natively support SSO. - LDAP LDAP @@ -1714,103 +1683,9 @@ Provide an LDAP interface for applications and users to authenticate against. - - Link - - - Authentication method - - - LDAP details - - - Create service account - - - Create provider - Sağlayıcı oluştur - - - Application Link - - - URL which will be opened when a user clicks on the application. - - - Method details - - - This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically. - - - By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password. - - - Web application - - - Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP) - - - Single-page applications - - - Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue) - - - Native application - - - Applications which redirect users to a non-web callback (for example, Android, iOS) - - - API - - - Authentication without user interaction, or machine-to-machine authentication. - - - Application type - - - Flow used when users access this application. - - - Proxy details - - - External domain - - - External domain you will be accessing the domain from. - - - Import SAML Metadata - - - Import the metadata document of the applicaation you want to configure. - - - Manual configuration - - - Manually configure SAML - - - SAML details - - - URL that authentik will redirect back to after successful authentication. - - - Import SAML metadata - New application - - Create a new application. - Applications Uygulamalar @@ -5834,6 +5709,76 @@ Bindings to groups/users are checked against the user of the event. Stage used to configure a WebAuthn authenticator (i.e. Yubikey, FaceID/Windows Hello). +<<<<<<< HEAD + + Internal application name used in URLs. + + + Submit + + + UI Settings + + + OAuth2/OpenID + + + Transparent Reverse Proxy + + + For transparent reverse proxies with required authentication + + + Forward Auth Single Application + + + For nginx's auth_request or traefix's forwardAuth + + + Forward Auth Domain Level + + + For nginx's auth_request or traefix's forwardAuth per root domain + + + Configure SAML provider manually + + + RADIUS Configuration + + + Configure RADIUS provider manually + + + SCIM configuration + + + Configure SCIM provider manually + + + Saving Application... + + + Authentik was unable to save this application: + + + Your application has been saved + + + In the Application: + + + In the Provider: + + + Method's display Name. + + + 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 managed outpost, this is done for you). + Custom attributes @@ -5944,6 +5889,9 @@ Bindings to groups/users are checked against the user of the event. Role Info + + + Pseudolocale (for testing) diff --git a/web/xliff/zh-Hans.xlf b/web/xliff/zh-Hans.xlf index cf3cfab8d..7c0e0c445 100644 --- a/web/xliff/zh-Hans.xlf +++ b/web/xliff/zh-Hans.xlf @@ -1837,11 +1837,6 @@ Slug Slug - - - Internal application name, used in URLs. - 应用的内部名称,在 URL 中使用。 - Optionally enter a group name. Applications with identical groups are shown grouped together. @@ -1857,11 +1852,6 @@ Select a provider that this application should use. 选择此应用应该使用的提供程序。 - - - Backchannel providers - 反向通道提供程序 - Select backchannel providers which augment the functionality of the main provider. @@ -2227,11 +2217,6 @@ NameID attribute NameID 属性 - - - SCIM provider is in preview. - SCIM 提供程序处于预览状态。 - Warning: Provider is not assigned to an application as backchannel provider. @@ -2252,46 +2237,11 @@ Run sync again 再次运行同步 - - - Application details - 应用程序详情 - - - - Create application - 创建应用程序 - - - - Additional UI settings - 其他界面设置 - - - - OAuth2/OIDC - OAuth2/OIDC - Modern applications, APIs and Single-page applications. 现代应用程序、API 与单页应用程序。 - - - SAML - SAML - - - - XML-based SSO standard. Use this if your application only supports SAML. - 基于 XML 的 SSO 标准。如果您的应用程序仅支持 SAML 则应使用。 - - - - Legacy applications which don't natively support SSO. - 不原生支持 SSO 的传统应用程序。 - LDAP @@ -2302,166 +2252,11 @@ Provide an LDAP interface for applications and users to authenticate against. 为应用程序和用户提供 LDAP 接口以进行身份​​验证。 - - - Link - 链接 - - - - Authentication method - 身份验证方法 - - - - LDAP details - LDAP 详情 - - - - Create service account - 创建服务账户 - - - - Create provider - 创建提供程序 - - - - Application Link - 应用程序链接 - - - - URL which will be opened when a user clicks on the application. - 用户点击应用程序时将打开的 URL。 - - - - Method details - 方法详情 - - - - This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically. - 此配置可用于通过其他 API 或以编程方式处理 authentik 身份验证。 - - - - By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password. - 默认情况下,所有服务账户都可以作为此应用程序进行身份验证,只要它们拥有 app-password 类型的有效令牌。 - - - - Web application - Web 应用程序 - - - - Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP) - 在服务端处理身份验证的应用程序(例如 Python、Go、Rust、Java、PHP) - - - - Single-page applications - 单页应用程序 - - - - Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue) - 在浏览器内处理身份验证的单页应用程序(例如 Javascript、Angular、React、Vue) - - - - Native application - 原生应用程序 - - - - Applications which redirect users to a non-web callback (for example, Android, iOS) - 重定向用户到非 Web 回调的应用程序(例如 Android、iOS) - - - - API - API - - - - Authentication without user interaction, or machine-to-machine authentication. - 无需用户操作的身份验证,或 M2M(机器到机器)身份验证。 - - - - Application type - 应用程序类型 - - - - Flow used when users access this application. - 用户访问此应用程序时使用的流程。 - - - - Proxy details - 代理详情 - - - - External domain - 外部域名 - - - - External domain you will be accessing the domain from. - 您将从此外部域名访问域名。 - - - - Import SAML Metadata - 导入 SAML 元数据 - - - - Import the metadata document of the applicaation you want to configure. - 导入您要配置的应用程序的元数据文档。 - - - - Manual configuration - 手动配置 - - - - Manually configure SAML - 手动配置 SAML - - - - SAML details - SAML 详情 - - - - URL that authentik will redirect back to after successful authentication. - 身份验证成功后,authentik 将重定向回的 URL。 - - - - Import SAML metadata - 导入 SAML 元数据 - New application 新应用程序 - - - Create a new application. - 创建一个新应用程序。 - Applications @@ -7829,6 +7624,76 @@ Bindings to groups/users are checked against the user of the event. Stage used to configure a WebAuthn authenticator (i.e. Yubikey, FaceID/Windows Hello). 用来配置 WebAuthn 身份验证器(即 Yubikey、FaceID/Windows Hello)的阶段。 +<<<<<<< HEAD + + Internal application name used in URLs. + + + Submit + + + UI Settings + + + OAuth2/OpenID + + + Transparent Reverse Proxy + + + For transparent reverse proxies with required authentication + + + Forward Auth Single Application + + + For nginx's auth_request or traefix's forwardAuth + + + Forward Auth Domain Level + + + For nginx's auth_request or traefix's forwardAuth per root domain + + + Configure SAML provider manually + + + RADIUS Configuration + + + Configure RADIUS provider manually + + + SCIM configuration + + + Configure SCIM provider manually + + + Saving Application... + + + Authentik was unable to save this application: + + + Your application has been saved + + + In the Application: + + + In the Provider: + + + Method's display Name. + + + 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 managed outpost, this is done for you). + Custom attributes 自定义属性 @@ -7941,6 +7806,9 @@ Bindings to groups/users are checked against the user of the event. Role Info + + + Pseudolocale (for testing) diff --git a/web/xliff/zh-Hant.xlf b/web/xliff/zh-Hant.xlf index 8cfcc6044..9db16c2f6 100644 --- a/web/xliff/zh-Hant.xlf +++ b/web/xliff/zh-Hant.xlf @@ -1390,10 +1390,6 @@ Slug Slug - - Internal application name, used in URLs. - 应用的内部名称,在URL中使用。 - Optionally enter a group name. Applications with identical groups are shown grouped together. 输入可选的分组名称。分组相同的应用程序会显示在一起。 @@ -1405,9 +1401,6 @@ Select a provider that this application should use. - - Backchannel providers - Select backchannel providers which augment the functionality of the main provider. @@ -1681,9 +1674,6 @@ NameID attribute - - SCIM provider is in preview. - Warning: Provider is not assigned to an application as backchannel provider. @@ -1697,31 +1687,9 @@ Run sync again 再次运行同步 - - Application details - - - Create application - - - Additional UI settings - - - OAuth2/OIDC - Modern applications, APIs and Single-page applications. - - SAML - SAML - - - XML-based SSO standard. Use this if your application only supports SAML. - - - Legacy applications which don't natively support SSO. - LDAP LDAP @@ -1729,106 +1697,9 @@ Provide an LDAP interface for applications and users to authenticate against. - - Link - - - Authentication method - - - LDAP details - LDAP 详情 - - - Create service account - - - Create provider - 创建提供商 - - - Application Link - - - URL which will be opened when a user clicks on the application. - - - Method details - - - This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically. - - - By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password. - - - Web application - - - Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP) - - - Single-page applications - - - Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue) - - - Native application - - - Applications which redirect users to a non-web callback (for example, Android, iOS) - - - API - - - Authentication without user interaction, or machine-to-machine authentication. - - - Application type - - - Flow used when users access this application. - - - Proxy details - 代理详情 - - - External domain - - - External domain you will be accessing the domain from. - - - Import SAML Metadata - - - Import the metadata document of the applicaation you want to configure. - - - Manual configuration - - - Manually configure SAML - - - SAML details - SAML 详情 - - - URL that authentik will redirect back to after successful authentication. - - - Import SAML metadata - New application - - Create a new application. - Applications 应用程序 @@ -5886,6 +5757,76 @@ Bindings to groups/users are checked against the user of the event. Stage used to configure a WebAuthn authenticator (i.e. Yubikey, FaceID/Windows Hello). +<<<<<<< HEAD + + Internal application name used in URLs. + + + Submit + + + UI Settings + + + OAuth2/OpenID + + + Transparent Reverse Proxy + + + For transparent reverse proxies with required authentication + + + Forward Auth Single Application + + + For nginx's auth_request or traefix's forwardAuth + + + Forward Auth Domain Level + + + For nginx's auth_request or traefix's forwardAuth per root domain + + + Configure SAML provider manually + + + RADIUS Configuration + + + Configure RADIUS provider manually + + + SCIM configuration + + + Configure SCIM provider manually + + + Saving Application... + + + Authentik was unable to save this application: + + + Your application has been saved + + + In the Application: + + + In the Provider: + + + Method's display Name. + + + 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 managed outpost, this is done for you). + Custom attributes @@ -5996,6 +5937,9 @@ Bindings to groups/users are checked against the user of the event. Role Info + + + Pseudolocale (for testing) diff --git a/web/xliff/zh_TW.xlf b/web/xliff/zh_TW.xlf index 8f95191e6..52e66eca3 100644 --- a/web/xliff/zh_TW.xlf +++ b/web/xliff/zh_TW.xlf @@ -1390,10 +1390,6 @@ Slug Slug - - Internal application name, used in URLs. - 应用的内部名称,在URL中使用。 - Optionally enter a group name. Applications with identical groups are shown grouped together. 输入可选的分组名称。分组相同的应用程序会显示在一起。 @@ -1405,9 +1401,6 @@ Select a provider that this application should use. - - Backchannel providers - Select backchannel providers which augment the functionality of the main provider. @@ -1681,9 +1674,6 @@ NameID attribute - - SCIM provider is in preview. - Warning: Provider is not assigned to an application as backchannel provider. @@ -1697,31 +1687,9 @@ Run sync again 再次运行同步 - - Application details - - - Create application - - - Additional UI settings - - - OAuth2/OIDC - Modern applications, APIs and Single-page applications. - - SAML - SAML - - - XML-based SSO standard. Use this if your application only supports SAML. - - - Legacy applications which don't natively support SSO. - LDAP LDAP @@ -1729,106 +1697,9 @@ Provide an LDAP interface for applications and users to authenticate against. - - Link - - - Authentication method - - - LDAP details - LDAP 详情 - - - Create service account - - - Create provider - 创建提供商 - - - Application Link - - - URL which will be opened when a user clicks on the application. - - - Method details - - - This configuration can be used to authenticate to authentik with other APIs other otherwise programmatically. - - - By default, all service accounts can authenticate as this application, as long as they have a valid token of the type app-password. - - - Web application - - - Applications which handle the authentication server-side (for example, Python, Go, Rust, Java, PHP) - - - Single-page applications - - - Single-page applications which handle authentication in the browser (for example, Javascript, Angular, React, Vue) - - - Native application - - - Applications which redirect users to a non-web callback (for example, Android, iOS) - - - API - - - Authentication without user interaction, or machine-to-machine authentication. - - - Application type - - - Flow used when users access this application. - - - Proxy details - 代理详情 - - - External domain - - - External domain you will be accessing the domain from. - - - Import SAML Metadata - - - Import the metadata document of the applicaation you want to configure. - - - Manual configuration - - - Manually configure SAML - - - SAML details - SAML 详情 - - - URL that authentik will redirect back to after successful authentication. - - - Import SAML metadata - New application - - Create a new application. - Applications 应用程序 @@ -5885,6 +5756,76 @@ Bindings to groups/users are checked against the user of the event. Stage used to configure a WebAuthn authenticator (i.e. Yubikey, FaceID/Windows Hello). +<<<<<<< HEAD + + Internal application name used in URLs. + + + Submit + + + UI Settings + + + OAuth2/OpenID + + + Transparent Reverse Proxy + + + For transparent reverse proxies with required authentication + + + Forward Auth Single Application + + + For nginx's auth_request or traefix's forwardAuth + + + Forward Auth Domain Level + + + For nginx's auth_request or traefix's forwardAuth per root domain + + + Configure SAML provider manually + + + RADIUS Configuration + + + Configure RADIUS provider manually + + + SCIM configuration + + + Configure SCIM provider manually + + + Saving Application... + + + Authentik was unable to save this application: + + + Your application has been saved + + + In the Application: + + + In the Provider: + + + Method's display Name. + + + 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 managed outpost, this is done for you). + Custom attributes @@ -5995,6 +5936,9 @@ Bindings to groups/users are checked against the user of the event. Role Info + + + Pseudolocale (for testing)