From 911f8e49cc55ad0ba984a7864faa693ae17c36a2 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Fri, 4 Aug 2023 11:04:25 -0700 Subject: [PATCH 1/2] web: Replace ad-hoc search for CryptoCertificateKeyPairs with ak-crypto-certeficate-search This commit replaces various ad-hoc implementations of `search-select` for CryptoCertificateKeyPairs with a web component that encapsulates all of the needed behavior and exposes a single API. The results are: Lots of visual clutter is eliminated. A single search of: ```HTML => { 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} > ``` Now looks like: ```HTML ``` There are three searches that do not require there to be a valid key with the certificate; these are supported with the boolean property `nokey`; likewise, there is one search (in SAMLProviderForm) that states that if there is no current certificate in the SAMLProvider and only one certificate can be found in the Authentik database, use that one; this is supported with the boolean property `singleton`. These changes replace 382 lines of object-oriented invocations with 36 lines of declarative configuration, and 98 lines for the class. Overall, the code for "find a crypto certificate" has been reduced by 46%. Suggestions for a better word than `singleton` are welcome! --- .../common/ak-crypto-certificate-search.ts | 129 ++++++++++++++++++ .../outposts/ServiceConnectionDockerForm.ts | 69 ++-------- .../admin/providers/ldap/LDAPProviderForm.ts | 36 +---- .../providers/oauth2/OAuth2ProviderForm.ts | 44 +----- .../providers/proxy/ProxyProviderForm.ts | 36 +---- .../admin/providers/saml/SAMLProviderForm.ts | 69 ++-------- web/src/admin/sources/ldap/LDAPSourceForm.ts | 68 ++------- web/src/admin/sources/saml/SAMLSourceForm.ts | 68 ++------- web/src/admin/tenants/TenantForm.ts | 42 +----- 9 files changed, 179 insertions(+), 382 deletions(-) create mode 100644 web/src/admin/common/ak-crypto-certificate-search.ts 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} - > - + Date: Fri, 4 Aug 2023 14:06:09 -0700 Subject: [PATCH 2/2] web: display tests for CryptoCertificateKeypair search This adds a Storybook for the CryptoCertificateKeypair search, including a mock fetch of the data. In the course of running the tests, we discovered that including the SearchSelect _class_ won't include the customElement declaration unless you include the whole file! Other bugs found: including the CSS from Storybook is different from that of LitElement native, so much so that the adapter needed to be included. FlowSearch had a similar bug. The problem only manifests when building via Webpack (which Storybook uses) and not Rollup, but we should support both in distribution. --- .../common/ak-crypto-certificate-search.ts | 5 +- .../admin/common/ak-flow-search/FlowSearch.ts | 1 + .../ak-crypto-certificate-search.stories.ts | 91 +++++++++++++++++++ web/src/admin/common/stories/samples.ts | 29 ++++++ web/src/elements/forms/SearchSelect.ts | 3 +- 5 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 web/src/admin/common/stories/ak-crypto-certificate-search.stories.ts create mode 100644 web/src/admin/common/stories/samples.ts diff --git a/web/src/admin/common/ak-crypto-certificate-search.ts b/web/src/admin/common/ak-crypto-certificate-search.ts index 3612b722b..15fe347c2 100644 --- a/web/src/admin/common/ak-crypto-certificate-search.ts +++ b/web/src/admin/common/ak-crypto-certificate-search.ts @@ -1,6 +1,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { AKElement } from "@goauthentik/elements/Base"; import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect"; +import "@goauthentik/elements/forms/SearchSelect"; import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"; import { html } from "lit"; @@ -29,7 +30,7 @@ const renderValue = (item: CertificateKeyPair | undefined): string | undefined = */ @customElement("ak-crypto-certificate-search") -export class CryptoCertificateSearch extends CustomListenerElement(AKElement) { +export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement) { @property({ type: String, reflect: true }) certificate?: string; @@ -126,4 +127,4 @@ export class CryptoCertificateSearch extends CustomListenerElement(AKElement) { } } -export default CryptoCertificateSearch; +export default AkCryptoCertificateSearch; diff --git a/web/src/admin/common/ak-flow-search/FlowSearch.ts b/web/src/admin/common/ak-flow-search/FlowSearch.ts index 484df6d6e..b19a8e4cf 100644 --- a/web/src/admin/common/ak-flow-search/FlowSearch.ts +++ b/web/src/admin/common/ak-flow-search/FlowSearch.ts @@ -2,6 +2,7 @@ import { RenderFlowOption } from "@goauthentik/admin/flows/utils"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { AKElement } from "@goauthentik/elements/Base"; import { SearchSelect } from "@goauthentik/elements/forms/SearchSelect"; +import "@goauthentik/elements/forms/SearchSelect"; import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"; import { html } from "lit"; diff --git a/web/src/admin/common/stories/ak-crypto-certificate-search.stories.ts b/web/src/admin/common/stories/ak-crypto-certificate-search.stories.ts new file mode 100644 index 000000000..ac31ec2b9 --- /dev/null +++ b/web/src/admin/common/stories/ak-crypto-certificate-search.stories.ts @@ -0,0 +1,91 @@ +import "@goauthentik/elements/forms/HorizontalFormElement"; +import "@goauthentik/elements/messages/MessageContainer"; +import { Meta } from "@storybook/web-components"; + +import { TemplateResult, html } from "lit"; + +import "../ak-crypto-certificate-search"; +import AkCryptoCertificateSearch from "../ak-crypto-certificate-search"; +import { dummyCryptoCertsSearch } from "./samples"; + +const metadata: Meta = { + title: "Components / Searches / CryptoCertificateKeyPair", + component: "ak-crypto-certificate-search", + parameters: { + docs: { + description: { + component: "A search function for cryptographic certificates in Authentik", + }, + }, + mockData: [ + { + url: "/api/v3/crypto/certificatekeypairs/?has_key=true&include_details=false&ordering=name", + method: "GET", + status: 200, + response: dummyCryptoCertsSearch, + }, + ], + }, + argTypes: { + // Typescript is unaware that arguments for components are treated as properties, and + // properties are typically renamed to lower case, even if the variable is not. + // @ts-expect-error + nokey: { + control: "boolean", + description: + "When true, certificates without valid keys will be included in the search", + }, + singleton: { + control: "boolean", + description: + "Supports the SAML Source search: when true, if there is no certificate in the current form and there is one and only one certificate in the Authentik database, use that certificate by default.", + }, + }, +}; + +export default metadata; + +const LIGHT = "pf-t-light"; +function injectTheme() { + setTimeout(() => { + if (!document.body.classList.contains(LIGHT)) { + document.body.classList.add(LIGHT); + } + }); +} + +const container = (testItem: TemplateResult) => { + injectTheme(); + return html`

+ + + ${testItem} +

+    
`; +}; + +export const CryptoCertificateSearch = () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const showMessage = (ev: CustomEvent) => { + const detail = ev.detail; + delete detail["target"]; + document.getElementById("message-pad")!.innerText = `Event: ${JSON.stringify( + detail, + null, + 2, + )}`; + }; + + return container( + html` + `, + ); +}; diff --git a/web/src/admin/common/stories/samples.ts b/web/src/admin/common/stories/samples.ts new file mode 100644 index 000000000..10e683d6e --- /dev/null +++ b/web/src/admin/common/stories/samples.ts @@ -0,0 +1,29 @@ +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, + }, + ], +}; diff --git a/web/src/elements/forms/SearchSelect.ts b/web/src/elements/forms/SearchSelect.ts index 02d1f1fc2..0aba008ee 100644 --- a/web/src/elements/forms/SearchSelect.ts +++ b/web/src/elements/forms/SearchSelect.ts @@ -1,5 +1,6 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { ascii_letters, digits, groupBy, randomString } from "@goauthentik/common/utils"; +import { adaptCSS } from "@goauthentik/common/utils"; import { AKElement } from "@goauthentik/elements/Base"; import { PreventFormSubmit } from "@goauthentik/elements/forms/Form"; import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; @@ -75,7 +76,7 @@ export class SearchSelect extends CustomEmitterElement(AKElement) { constructor() { super(); if (!document.adoptedStyleSheets.includes(PFDropdown)) { - document.adoptedStyleSheets = [...document.adoptedStyleSheets, PFDropdown]; + document.adoptedStyleSheets = adaptCSS([...document.adoptedStyleSheets, PFDropdown]); } this.dropdownContainer = document.createElement("div"); this.observer = new IntersectionObserver(() => {