From 611a24200d0c268e996ca3d7f25c54ee6c3b291d Mon Sep 17 00:00:00 2001 From: Dominic R Date: Sun, 13 Sep 2026 20:42:52 -0400 Subject: [PATCH 1/4] web/admin: use managed secrets in consumer forms --- .../connectors/fleet/FleetConnectorForm.ts | 17 +++-- .../gdtc/GoogleChromeConnectorForm.ts | 25 ++++---- web/src/admin/events/TransportForm.ts | 19 +++--- .../ServiceConnectionKubernetesForm.ts | 25 ++++---- .../GoogleWorkspaceProviderForm.ts | 25 ++++---- .../MicrosoftEntraProviderForm.ts | 20 +++--- .../oauth2/OAuth2ProviderFormForm.ts | 27 ++++---- .../radius/RadiusProviderFormForm.ts | 30 ++++----- .../providers/scim/SCIMProviderFormForm.ts | 24 +++---- .../sources/kerberos/KerberosSourceForm.ts | 58 +++++++++-------- web/src/admin/sources/ldap/LDAPSourceForm.ts | 15 +++-- .../admin/sources/oauth/OAuthSourceForm.ts | 18 +++--- web/src/admin/sources/plex/PlexSourceForm.ts | 61 ++++++++++++++++-- .../sources/telegram/TelegramSourceForm.ts | 19 +++--- .../AuthenticatorDuoStageForm.ts | 31 +++++---- .../AuthenticatorEmailStageForm.ts | 15 +++-- .../AuthenticatorEndpointGDTCStageForm.ts | 25 ++++---- .../AuthenticatorSMSStageForm.ts | 64 +++++++++++-------- .../admin/stages/captcha/CaptchaStageForm.ts | 18 +++--- web/src/admin/stages/captcha/shared.ts | 2 +- web/src/admin/stages/email/EmailStageForm.ts | 15 +++-- 21 files changed, 325 insertions(+), 228 deletions(-) diff --git a/web/src/admin/endpoints/connectors/fleet/FleetConnectorForm.ts b/web/src/admin/endpoints/connectors/fleet/FleetConnectorForm.ts index c2e4f80f8893..f5f726971593 100644 --- a/web/src/admin/endpoints/connectors/fleet/FleetConnectorForm.ts +++ b/web/src/admin/endpoints/connectors/fleet/FleetConnectorForm.ts @@ -1,4 +1,4 @@ -import "#components/ak-secret-text-input"; +import "#components/ak-secret-search-input"; import "#components/ak-switch-input"; import "#components/ak-text-input"; import "#elements/forms/FormGroup"; @@ -6,6 +6,7 @@ import "#elements/forms/FormGroup"; import { aki } from "#common/api/client"; import { ModelForm } from "#elements/forms/ModelForm"; +import { ifPresent } from "#elements/utils/attributes"; import { EndpointsApi, FleetConnector, FleetConnectorRequest } from "@goauthentik/api"; @@ -62,13 +63,15 @@ export class FleetConnectorForm extends ModelForm { input-hint="code" > - + value=${ifPresent(this.instance?.secret ?? undefined)} + required + help=${msg("Token used to authenticate against the Fleet server.", { + id: "connector.fleet.form.secret.description", + })} + >
- - -

- ${msg("Google Cloud credentials file.")} -

-
+ help=${msg( + "Select a secret containing the Google Cloud credentials JSON file.", + { id: "google.credentials.description" }, + )} + >
`; } diff --git a/web/src/admin/events/TransportForm.ts b/web/src/admin/events/TransportForm.ts index 44fd6a51722c..f42a24005737 100644 --- a/web/src/admin/events/TransportForm.ts +++ b/web/src/admin/events/TransportForm.ts @@ -1,4 +1,4 @@ -import "#components/ak-hidden-text-input"; +import "#components/ak-secret-search-input"; import "#components/ak-switch-input"; import "#components/ak-text-input"; import "#elements/forms/HorizontalFormElement"; @@ -9,6 +9,7 @@ import "#admin/common/ak-crypto-certificate-search"; import { aki } from "#common/api/client"; import { ModelForm } from "#elements/forms/ModelForm"; +import { ifPresent } from "#elements/utils/attributes"; import { EventsApi, @@ -139,15 +140,17 @@ export class TransportForm extends ModelForm { > - - + ?hidden=${!this.showWebhook} + help=${msg("URL the notification is sent to.", { + id: "event.transport.form.secret.description", + })} + > - - - -

- ${msg("Set custom attributes using YAML or JSON.")} -

-
+
- - -

- ${msg("Google Cloud credentials file.")} -

-
+ help=${msg( + "Select a secret containing the Google Cloud credentials JSON file.", + { id: "google.credentials.description" }, + )} + > - - + value=${ifPresent(this.instance?.secret ?? undefined)} + required + help=${msg("Client secret for the app registration.", { + id: "provider.microsoft-entra.form.secret.description", + })} + > - - + >
- + value=${ifPresent(provider.secret)} + blankable + help=${provider.pk + ? msg("Secret between clients and server to hash packets.", { + id: "provider.radius.form.secret.description.edit", + }) + : msg( + "Secret between clients and server to hash packets. Leave empty to create one for this provider.", + { id: "provider.radius.form.secret.description.create" }, + )} + > , errors: ValidationError = {}) { - return html`) { + return html``; + value=${ifPresent(provider?.secret ?? undefined)} + blankable + help=${msg("Token to authenticate with.", { + id: "provider.scim.form.secret.description", + })} + >`; } export function renderAuthOAuth(provider?: Partial, _errors: ValidationError = {}) { @@ -91,7 +93,7 @@ export function renderAuth(provider?: Partial, errors: ValidationE switch (provider?.authMode) { default: case SCIMAuthenticationModeEnum.Token: - return renderAuthToken(provider, errors); + return renderAuthToken(provider); case SCIMAuthenticationModeEnum.Oauth: case SCIMAuthenticationModeEnum.OauthInteractive: return renderAuthOAuth(provider, errors); diff --git a/web/src/admin/sources/kerberos/KerberosSourceForm.ts b/web/src/admin/sources/kerberos/KerberosSourceForm.ts index a024a49f8ae1..ca16bdbcbe40 100644 --- a/web/src/admin/sources/kerberos/KerberosSourceForm.ts +++ b/web/src/admin/sources/kerberos/KerberosSourceForm.ts @@ -1,6 +1,5 @@ import "#admin/common/ak-flow-search/ak-source-flow-search"; -import "#components/ak-secret-text-input"; -import "#components/ak-secret-textarea-input"; +import "#components/ak-secret-search-input"; import "#components/ak-slug-input"; import "#components/ak-text-input"; import "#components/ak-radio-input"; @@ -17,6 +16,7 @@ import { propertyMappingsProvider, propertyMappingsSelector } from "./KerberosSo import { aki } from "#common/api/client"; import { RadioOption } from "#elements/forms/Radio"; +import { ifPresent } from "#elements/utils/attributes"; import { iconHelperText, placeholderHelperText } from "#admin/helperText"; import { BaseSourceForm } from "#admin/sources/BaseSourceForm"; @@ -248,30 +248,34 @@ export class KerberosSourceForm extends BaseSourceForm { value=${ifDefined(this.instance?.syncPrincipal)} help=${msg("Principal used to authenticate to the KDC for syncing.")} > - - + - + + >
@@ -284,22 +288,24 @@ export class KerberosSourceForm extends BaseSourceForm { "Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain", )} > - - + + >
diff --git a/web/src/admin/sources/ldap/LDAPSourceForm.ts b/web/src/admin/sources/ldap/LDAPSourceForm.ts index d0ae4afe1343..f25b45ed4fc7 100644 --- a/web/src/admin/sources/ldap/LDAPSourceForm.ts +++ b/web/src/admin/sources/ldap/LDAPSourceForm.ts @@ -1,5 +1,5 @@ import "#admin/common/ak-crypto-certificate-search"; -import "#components/ak-secret-text-input"; +import "#components/ak-secret-search-input"; import "#components/ak-slug-input"; import "#components/ak-radio-input"; import "#components/ak-switch-input"; @@ -13,6 +13,7 @@ import { propertyMappingsProvider, propertyMappingsSelector } from "./LDAPSource import { aki } from "#common/api/client"; import { RadioOption } from "#elements/forms/Radio"; +import { ifPresent } from "#elements/utils/attributes"; import { placeholderHelperText } from "#admin/helperText"; import { BaseSourceForm } from "#admin/sources/BaseSourceForm"; @@ -196,11 +197,15 @@ export class LDAPSourceForm extends BaseSourceForm { class="pf-c-form-control" />
- + value=${ifPresent(this.instance?.secret)} + blankable + help=${msg("Password used to bind to the LDAP server.", { + id: "source.ldap.form.secret.description", + })} + > { />

${msg("Also known as Client ID.")}

- + value=${ifPresent(this.instance?.secret)} + required + help=${msg("Also known as Client Secret.", { + id: "source.oauth.form.secret.description", + })} + > { const source = await aki(SourcesApi).sourcesPlexRetrieve({ slug: pk, }); - this.plexToken = source.plexToken; - this.loadServers(); + if (source.secret) { + try { + const { value } = await aki(SecretsApi).secretsSecretsViewValueRetrieve({ + secretUuid: source.secret, + }); + this.plexToken = value; + this.initialToken = value; + await this.loadServers(); + } catch (error) { + await showAPIErrorMessage(error); + } + } return source; } @property() plexToken?: string; + private initialToken?: string; + @property({ attribute: false }) plexResources?: PlexResource[]; @@ -59,7 +74,16 @@ export class PlexSourceForm extends BaseSourceForm { } async send(data: PlexSource): Promise { - data.plexToken = this.plexToken || ""; + if (!this.plexResources && this.instance) { + data.allowedServers = this.instance.allowedServers; + data.allowFriends = this.instance.allowFriends; + } + if (this.instance?.secret) { + data.secret = this.instance.secret; + } + if (this.plexToken && this.plexToken !== this.initialToken) { + data.secret = await this.saveTokenSecret(data.name || this.instance?.name || "Plex"); + } if (this.instance?.pk) { return aki(SourcesApi).sourcesPlexUpdate({ slug: this.instance.slug, @@ -72,6 +96,29 @@ export class PlexSourceForm extends BaseSourceForm { }); } + // A secret created for a not-yet-saved source, so a retry after a failed source + // save reuses it instead of leaving an orphan and creating another. + private createdSecretPk?: string; + + private async saveTokenSecret(sourceName: string): Promise { + const target = this.instance?.secret ?? this.createdSecretPk; + if (target) { + await aki(SecretsApi).secretsSecretsPartialUpdate({ + secretUuid: target, + patchedSecretRequest: { value: this.plexToken }, + }); + return target; + } + const secret = await aki(SecretsApi).secretsSecretsCreate({ + secretRequest: { + name: `${sourceName} Plex token (${crypto.randomUUID()})`, + value: this.plexToken, + }, + }); + this.createdSecretPk = secret.pk; + return secret.pk; + } + async doAuth(): Promise { const authInfo = await PlexAPIClient.getPin(this.instance?.clientId || ""); const authWindow = await popupCenterScreen(authInfo.authUrl, "plex auth", 550, 700); @@ -86,11 +133,15 @@ export class PlexSourceForm extends BaseSourceForm { if (!this.plexToken) { return; } - this.plexResources = await new PlexAPIClient(this.plexToken).getServers(); + try { + this.plexResources = await new PlexAPIClient(this.plexToken).getServers(); + } catch (error) { + await showAPIErrorMessage(error); + } } renderSettings(): TemplateResult { - if (!this.plexToken) { + if (!this.plexResources) { return html`