diff --git a/web/src/admin/common/ak-crypto-certificate-search.ts b/web/src/admin/common/ak-crypto-certificate-search.ts new file mode 100644 index 000000000..3612b722b --- /dev/null +++ b/web/src/admin/common/ak-crypto-certificate-search.ts @@ -0,0 +1,129 @@ +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 { + CertificateKeyPair, + CryptoApi, + CryptoCertificatekeypairsListRequest, +} from "@goauthentik/api"; + +const renderElement = (item: CertificateKeyPair): string => item.name; + +const renderValue = (item: CertificateKeyPair | undefined): string | undefined => item?.pk; + +/** + * Cryptographic Certificate Search + * + * @element ak-crypto-certificate-search + * + * A wrapper around SearchSelect for the many searches of cryptographic key-pairs used throughout our + * code base. This is another one of those "If it's not error-free, at least it's localized to one + * place" issues. + * + */ + +@customElement("ak-crypto-certificate-search") +export class CryptoCertificateSearch extends CustomListenerElement(AKElement) { + @property({ type: String, reflect: true }) + certificate?: string; + + @query("ak-search-select") + search!: SearchSelect; + + @property({ type: String }) + name: string | null | undefined; + + /** + * Set to `true` if you want to find pairs that don't have a valid key. Of our 14 searches, 11 + * require the key, 3 do not (as of 2023-08-01). + * + * @attr + */ + @property({ type: Boolean, attribute: "nokey" }) + noKey = false; + + /** + * Set this to true if, should there be only one certificate available, you want the system to + * use it by default. + * + * @attr + */ + @property({ type: Boolean, attribute: "singleton" }) + singleton = false; + + selectedKeypair?: CertificateKeyPair; + + constructor() { + super(); + this.selected = this.selected.bind(this); + this.fetchObjects = this.fetchObjects.bind(this); + this.handleSearchUpdate = this.handleSearchUpdate.bind(this); + this.addCustomListener("ak-change", this.handleSearchUpdate); + } + + get value() { + return this.selectedKeypair ? renderValue(this.selectedKeypair) : 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.selectedKeypair = ev.detail.value; + this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true })); + } + + async fetchObjects(query?: string): Promise { + const args: CryptoCertificatekeypairsListRequest = { + ordering: "name", + hasKey: !this.noKey, + includeDetails: false, + }; + if (query !== undefined) { + args.search = query; + } + const certificates = await new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsList( + args, + ); + return certificates.results; + } + + selected(item: CertificateKeyPair, items: CertificateKeyPair[]) { + return ( + (this.singleton && !this.certificate && items.length === 1) || + (!!this.certificate && this.certificate === item.pk) + ); + } + + render() { + return html` + + + `; + } +} + +export default CryptoCertificateSearch; diff --git a/web/src/admin/outposts/ServiceConnectionDockerForm.ts b/web/src/admin/outposts/ServiceConnectionDockerForm.ts index c0b1c499d..c6840d50d 100644 --- a/web/src/admin/outposts/ServiceConnectionDockerForm.ts +++ b/web/src/admin/outposts/ServiceConnectionDockerForm.ts @@ -1,3 +1,4 @@ +import "@goauthentik/admin/common/ak-crypto-certificate-search"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { first } from "@goauthentik/common/utils"; import "@goauthentik/elements/forms/HorizontalFormElement"; @@ -9,13 +10,7 @@ import { TemplateResult, html } from "lit"; import { customElement } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; -import { - CertificateKeyPair, - CryptoApi, - CryptoCertificatekeypairsListRequest, - DockerServiceConnection, - OutpostsApi, -} from "@goauthentik/api"; +import { DockerServiceConnection, OutpostsApi } from "@goauthentik/api"; @customElement("ak-service-connection-docker-form") export class ServiceConnectionDockerForm extends ModelForm { @@ -93,33 +88,9 @@ export class ServiceConnectionDockerForm extends ModelForm - => { - const args: CryptoCertificatekeypairsListRequest = { - ordering: "name", - hasKey: true, - includeDetails: false, - }; - if (query !== undefined) { - args.search = query; - } - const certificates = await new CryptoApi( - DEFAULT_CONFIG, - ).cryptoCertificatekeypairsList(args); - return certificates.results; - }} - .renderElement=${(item: CertificateKeyPair): string => { - return item.name; - }} - .value=${(item: CertificateKeyPair | undefined): string | undefined => { - return item?.pk; - }} - .selected=${(item: CertificateKeyPair): boolean => { - return this.instance?.tlsVerification === item.pk; - }} - ?blankable=${true} - > - +

${msg( "CA which the endpoint's Certificate is verified against. Can be left empty for no validation.", @@ -130,33 +101,9 @@ export class ServiceConnectionDockerForm extends ModelForm - => { - const args: CryptoCertificatekeypairsListRequest = { - ordering: "name", - hasKey: true, - includeDetails: false, - }; - if (query !== undefined) { - args.search = query; - } - const certificates = await new CryptoApi( - DEFAULT_CONFIG, - ).cryptoCertificatekeypairsList(args); - return certificates.results; - }} - .renderElement=${(item: CertificateKeyPair): string => { - return item.name; - }} - .value=${(item: CertificateKeyPair | undefined): string | undefined => { - return item?.pk; - }} - .selected=${(item: CertificateKeyPair): boolean => { - return this.instance?.tlsAuthentication === item.pk; - }} - ?blankable=${true} - > - +

${msg( "Certificate/Key used for authentication. Can be left empty for no authentication.", diff --git a/web/src/admin/providers/ldap/LDAPProviderForm.ts b/web/src/admin/providers/ldap/LDAPProviderForm.ts index c7adc1002..d89902f6b 100644 --- a/web/src/admin/providers/ldap/LDAPProviderForm.ts +++ b/web/src/admin/providers/ldap/LDAPProviderForm.ts @@ -1,3 +1,4 @@ +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"; @@ -14,11 +15,8 @@ import { customElement } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { - CertificateKeyPair, CoreApi, CoreGroupsListRequest, - CryptoApi, - CryptoCertificatekeypairsListRequest, FlowsInstancesListDesignationEnum, Group, LDAPAPIAccessMode, @@ -208,35 +206,9 @@ export class LDAPProviderFormPage extends ModelForm {

- => { - const args: CryptoCertificatekeypairsListRequest = { - ordering: "name", - hasKey: true, - includeDetails: false, - }; - if (query !== undefined) { - args.search = query; - } - const certificates = await new CryptoApi( - DEFAULT_CONFIG, - ).cryptoCertificatekeypairsList(args); - return certificates.results; - }} - .renderElement=${(item: CertificateKeyPair): string => { - return item.name; - }} - .value=${(item: CertificateKeyPair | undefined): string | undefined => { - return item?.pk; - }} - .selected=${(item: CertificateKeyPair): boolean => { - return item.pk === this.instance?.certificate; - }} - ?blankable=${true} - > - +

${msg( "The certificate for the above configured Base DN. As a fallback, the provider uses a self-signed certificate.", diff --git a/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts b/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts index 8e9bdbb02..1a335846d 100644 --- a/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts +++ b/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts @@ -1,3 +1,4 @@ +import "@goauthentik/admin/common/ak-crypto-certificate-search"; import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils"; @@ -14,10 +15,7 @@ import { customElement, state } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { - CertificateKeyPair, ClientTypeEnum, - CryptoApi, - CryptoCertificatekeypairsListRequest, FlowsInstancesListDesignationEnum, IssuerModeEnum, OAuth2Provider, @@ -206,42 +204,10 @@ ${this.instance?.redirectUris} - => { - const args: CryptoCertificatekeypairsListRequest = { - ordering: "name", - hasKey: true, - includeDetails: false, - }; - if (query !== undefined) { - args.search = query; - } - const certificates = await new CryptoApi( - DEFAULT_CONFIG, - ).cryptoCertificatekeypairsList(args); - return certificates.results; - }} - .renderElement=${(item: CertificateKeyPair): string => { - return item.name; - }} - .value=${(item: CertificateKeyPair | undefined): string | undefined => { - return item?.pk; - }} - .selected=${( - item: CertificateKeyPair, - items: CertificateKeyPair[], - ): boolean => { - let selected = this.instance?.signingKey === item.pk; - if (!this.instance && items.length === 1) { - selected = true; - } - return selected; - }} - ?blankable=${true} - > - +

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

diff --git a/web/src/admin/providers/proxy/ProxyProviderForm.ts b/web/src/admin/providers/proxy/ProxyProviderForm.ts index d352c0336..89bbc61af 100644 --- a/web/src/admin/providers/proxy/ProxyProviderForm.ts +++ b/web/src/admin/providers/proxy/ProxyProviderForm.ts @@ -1,3 +1,4 @@ +import "@goauthentik/admin/common/ak-crypto-certificate-search"; import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { first } from "@goauthentik/common/utils"; @@ -19,9 +20,6 @@ import PFList from "@patternfly/patternfly/components/List/list.css"; import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css"; import { - CertificateKeyPair, - CryptoApi, - CryptoCertificatekeypairsListRequest, FlowsInstancesListDesignationEnum, PaginatedOAuthSourceList, PaginatedScopeMappingList, @@ -338,35 +336,9 @@ export class ProxyProviderFormPage extends ModelForm { ${msg("Advanced protocol settings")}
- => { - const args: CryptoCertificatekeypairsListRequest = { - ordering: "name", - hasKey: true, - includeDetails: false, - }; - if (query !== undefined) { - args.search = query; - } - const certificates = await new CryptoApi( - DEFAULT_CONFIG, - ).cryptoCertificatekeypairsList(args); - return certificates.results; - }} - .renderElement=${(item: CertificateKeyPair): string => { - return item.name; - }} - .value=${(item: CertificateKeyPair | undefined): string | undefined => { - return item?.pk; - }} - .selected=${(item: CertificateKeyPair): boolean => { - return item.pk === this.instance?.certificate; - }} - ?blankable=${true} - > - + { label=${msg("Signing Certificate")} name="signingKp" > - => { - const args: CryptoCertificatekeypairsListRequest = { - ordering: "name", - hasKey: true, - includeDetails: false, - }; - if (query !== undefined) { - args.search = query; - } - const certificates = await new CryptoApi( - DEFAULT_CONFIG, - ).cryptoCertificatekeypairsList(args); - return certificates.results; - }} - .renderElement=${(item: CertificateKeyPair): string => { - return item.name; - }} - .value=${(item: CertificateKeyPair | undefined): string | undefined => { - return item?.pk; - }} - .selected=${(item: CertificateKeyPair): boolean => { - return item.pk === this.instance?.signingKp; - }} - ?blankable=${true} - > - +

${msg( "Certificate used to sign outgoing Responses going to the Service Provider.", @@ -216,41 +188,16 @@ export class SAMLProviderFormPage extends ModelForm { label=${msg("Verification Certificate")} name="verificationKp" > - => { - const args: CryptoCertificatekeypairsListRequest = { - ordering: "name", - includeDetails: false, - }; - if (query !== undefined) { - args.search = query; - } - const certificates = await new CryptoApi( - DEFAULT_CONFIG, - ).cryptoCertificatekeypairsList(args); - return certificates.results; - }} - .renderElement=${(item: CertificateKeyPair): string => { - return item.name; - }} - .value=${(item: CertificateKeyPair | undefined): string | undefined => { - return item?.pk; - }} - .selected=${(item: CertificateKeyPair): boolean => { - return item.pk === this.instance?.verificationKp; - }} - ?blankable=${true} - > - +

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

- { label=${msg("TLS Verification Certificate")} name="peerCertificate" > - => { - const args: CryptoCertificatekeypairsListRequest = { - ordering: "name", - includeDetails: false, - }; - if (query !== undefined) { - args.search = query; - } - const certificates = await new CryptoApi( - DEFAULT_CONFIG, - ).cryptoCertificatekeypairsList(args); - return certificates.results; - }} - .renderElement=${(item: CertificateKeyPair): string => { - return item.name; - }} - .value=${(item: CertificateKeyPair | undefined): string | undefined => { - return item?.pk; - }} - .selected=${(item: CertificateKeyPair): boolean => { - return item.pk === this.instance?.peerCertificate; - }} - ?blankable=${true} - > - +

${msg( "When connecting to an LDAP Server with TLS, certificates are not checked by default. Specify a keypair to validate the remote certificate.", @@ -246,35 +220,9 @@ export class LDAPSourceForm extends ModelForm { label=${msg("TLS Client authentication certificate")} name="clientCertificate" > - => { - const args: CryptoCertificatekeypairsListRequest = { - ordering: "name", - hasKey: true, - includeDetails: false, - }; - if (query !== undefined) { - args.search = query; - } - const certificates = await new CryptoApi( - DEFAULT_CONFIG, - ).cryptoCertificatekeypairsList(args); - return certificates.results; - }} - .renderElement=${(item: CertificateKeyPair): string => { - return item.name; - }} - .value=${(item: CertificateKeyPair | undefined): string | undefined => { - return item?.pk; - }} - .selected=${(item: CertificateKeyPair): boolean => { - return item.pk === this.instance?.clientCertificate; - }} - ?blankable=${true} - > - +

${msg( "Client certificate keypair to authenticate against the LDAP Server's Certificate.", diff --git a/web/src/admin/sources/saml/SAMLSourceForm.ts b/web/src/admin/sources/saml/SAMLSourceForm.ts index 86e7e3bb8..ac168ae5b 100644 --- a/web/src/admin/sources/saml/SAMLSourceForm.ts +++ b/web/src/admin/sources/saml/SAMLSourceForm.ts @@ -1,3 +1,4 @@ +import "@goauthentik/admin/common/ak-crypto-certificate-search"; import "@goauthentik/admin/common/ak-flow-search/ak-source-flow-search"; import { iconHelperText, placeholderHelperText } from "@goauthentik/admin/helperText"; import { UserMatchingModeToLabel } from "@goauthentik/admin/sources/oauth/utils"; @@ -18,9 +19,6 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { BindingTypeEnum, CapabilitiesEnum, - CertificateKeyPair, - CryptoApi, - CryptoCertificatekeypairsListRequest, DigestAlgorithmEnum, FlowsInstancesListDesignationEnum, NameIdPolicyEnum, @@ -274,35 +272,9 @@ export class SAMLSourceForm extends ModelForm { - => { - const args: CryptoCertificatekeypairsListRequest = { - ordering: "name", - hasKey: true, - includeDetails: false, - }; - if (query !== undefined) { - args.search = query; - } - const certificates = await new CryptoApi( - DEFAULT_CONFIG, - ).cryptoCertificatekeypairsList(args); - return certificates.results; - }} - .renderElement=${(item: CertificateKeyPair): string => { - return item.name; - }} - .value=${(item: CertificateKeyPair | undefined): string | undefined => { - return item?.pk; - }} - .selected=${(item: CertificateKeyPair): boolean => { - return item.pk === this.instance?.signingKp; - }} - ?blankable=${true} - > - +

${msg( "Keypair which is used to sign outgoing requests. Leave empty to disable signing.", @@ -313,34 +285,10 @@ export class SAMLSourceForm extends ModelForm { label=${msg("Verification Certificate")} name="verificationKp" > - => { - const args: CryptoCertificatekeypairsListRequest = { - ordering: "name", - includeDetails: false, - }; - if (query !== undefined) { - args.search = query; - } - const certificates = await new CryptoApi( - DEFAULT_CONFIG, - ).cryptoCertificatekeypairsList(args); - return certificates.results; - }} - .renderElement=${(item: CertificateKeyPair): string => { - return item.name; - }} - .value=${(item: CertificateKeyPair | undefined): string | undefined => { - return item?.pk; - }} - .selected=${(item: CertificateKeyPair): boolean => { - return item.pk === this.instance?.verificationKp; - }} - ?blankable=${true} - > - +

${msg( "When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.", diff --git a/web/src/admin/tenants/TenantForm.ts b/web/src/admin/tenants/TenantForm.ts index 34420b367..9a3b88d61 100644 --- a/web/src/admin/tenants/TenantForm.ts +++ b/web/src/admin/tenants/TenantForm.ts @@ -1,3 +1,4 @@ +import "@goauthentik/admin/common/ak-crypto-certificate-search"; import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { first } from "@goauthentik/common/utils"; @@ -13,14 +14,7 @@ import { msg } from "@lit/localize"; import { TemplateResult, html } from "lit"; import { customElement } from "lit/decorators.js"; -import { - CertificateKeyPair, - CoreApi, - CryptoApi, - CryptoCertificatekeypairsListRequest, - FlowsInstancesListDesignationEnum, - Tenant, -} from "@goauthentik/api"; +import { CoreApi, FlowsInstancesListDesignationEnum, Tenant } from "@goauthentik/api"; @customElement("ak-tenant-form") export class TenantForm extends ModelForm { @@ -236,35 +230,9 @@ export class TenantForm extends ModelForm { label=${msg("Web Certificate")} name="webCertificate" > - => { - const args: CryptoCertificatekeypairsListRequest = { - ordering: "name", - hasKey: true, - includeDetails: false, - }; - if (query !== undefined) { - args.search = query; - } - const certificates = await new CryptoApi( - DEFAULT_CONFIG, - ).cryptoCertificatekeypairsList(args); - return certificates.results; - }} - .renderElement=${(item: CertificateKeyPair): string => { - return item.name; - }} - .value=${(item: CertificateKeyPair | undefined): string | undefined => { - return item?.pk; - }} - .selected=${(item: CertificateKeyPair): boolean => { - return item.pk === this.instance?.webCertificate; - }} - ?blankable=${true} - > - +