diff --git a/authentik/providers/saml/api.py b/authentik/providers/saml/api.py index a9beb0641..ee2badc73 100644 --- a/authentik/providers/saml/api.py +++ b/authentik/providers/saml/api.py @@ -1,17 +1,17 @@ """SAMLProvider API Views""" from drf_yasg2.utils import swagger_auto_schema -from rest_framework.fields import ReadOnlyField -from authentik.providers.saml.views import DescriptorDownloadView -from rest_framework.generics import get_object_or_404 -from rest_framework.serializers import ModelSerializer, Serializer -from rest_framework.viewsets import ModelViewSet from rest_framework.decorators import action +from rest_framework.fields import ReadOnlyField +from rest_framework.generics import get_object_or_404 from rest_framework.request import Request from rest_framework.response import Response -from guardian.shortcuts import get_objects_for_user +from rest_framework.serializers import ModelSerializer, Serializer +from rest_framework.viewsets import ModelViewSet + from authentik.core.api.providers import ProviderSerializer from authentik.core.api.utils import MetaNameSerializer from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider +from authentik.providers.saml.views import DescriptorDownloadView class SAMLProviderSerializer(ProviderSerializer): @@ -41,6 +41,12 @@ class SAMLMetadataSerializer(Serializer): metadata = ReadOnlyField() + def create(self, request: Request) -> Response: + raise NotImplementedError + + def update(self, request: Request) -> Response: + raise NotImplementedError + class SAMLProviderViewSet(ModelViewSet): """SAMLProvider Viewset""" @@ -55,9 +61,7 @@ class SAMLProviderViewSet(ModelViewSet): """Return metadata as XML string""" provider = get_object_or_404(SAMLProvider, pk=pk) metadata = DescriptorDownloadView.get_metadata(request, provider) - return Response({ - "metadata": metadata - }) + return Response({"metadata": metadata}) class SAMLPropertyMappingSerializer(ModelSerializer, MetaNameSerializer): diff --git a/authentik/providers/saml/models.py b/authentik/providers/saml/models.py index cfee5e66b..77e900444 100644 --- a/authentik/providers/saml/models.py +++ b/authentik/providers/saml/models.py @@ -4,15 +4,12 @@ from urllib.parse import urlparse from django.db import models from django.forms import ModelForm -from django.http import HttpRequest -from django.shortcuts import reverse from django.utils.translation import gettext_lazy as _ from rest_framework.serializers import Serializer from structlog.stdlib import get_logger from authentik.core.models import PropertyMapping, Provider from authentik.crypto.models import CertificateKeyPair -from authentik.lib.utils.template import render_to_string from authentik.lib.utils.time import timedelta_string_validator from authentik.sources.saml.processors.constants import ( DSA_SHA1, @@ -182,31 +179,6 @@ class SAMLProvider(Provider): def __str__(self): return f"SAML Provider {self.name}" - def link_download_metadata(self): - """Get link to download XML metadata for admin interface""" - try: - # pylint: disable=no-member - return reverse( - "authentik_providers_saml:metadata", - kwargs={"application_slug": self.application.slug}, - ) - except Provider.application.RelatedObjectDoesNotExist: - return None - - def html_metadata_view(self, request: HttpRequest) -> Optional[str]: - """return template and context modal to view Metadata without downloading it""" - from authentik.providers.saml.views import DescriptorDownloadView - - try: - # pylint: disable=no-member - metadata = DescriptorDownloadView.get_metadata(request, self) - return render_to_string( - "providers/saml/admin_metadata_modal.html", - {"provider": self, "metadata": metadata}, - ) - except Provider.application.RelatedObjectDoesNotExist: - return None - class Meta: verbose_name = _("SAML Provider") diff --git a/web/src/api/Providers.ts b/web/src/api/Providers.ts index 5db5ed742..3811db82d 100644 --- a/web/src/api/Providers.ts +++ b/web/src/api/Providers.ts @@ -4,6 +4,7 @@ export class Provider { pk: number; name: string; authorization_flow: string; + object_type: string; assigned_application_slug?: string; assigned_application_name?: string; diff --git a/web/src/api/providers/SAML.ts b/web/src/api/providers/SAML.ts new file mode 100644 index 000000000..814653b81 --- /dev/null +++ b/web/src/api/providers/SAML.ts @@ -0,0 +1,33 @@ +import { DefaultClient } from "../Client"; +import { Provider } from "../Providers"; + +export class SAMLProvider extends Provider { + acs_url: string; + audience: string; + issuer: string; + assertion_valid_not_before: string; + assertion_valid_not_on_or_after: string; + session_valid_not_on_or_after: string; + name_id_mapping?: string; + digest_algorithm: string; + signature_algorithm: string; + signing_kp?: string; + verification_kp?: string; + + constructor() { + super(); + throw Error(); + } + + static get(id: number): Promise { + return DefaultClient.fetch(["providers", "saml", id.toString()]); + } + + static getMetadata(id: number): Promise<{ metadata: string }> { + return DefaultClient.fetch(["providers", "saml", id.toString(), "metadata"]); + } + + static appUrl(rest: string): string { + return `/application/saml/${rest}`; + } +} diff --git a/web/src/elements/Page.ts b/web/src/elements/Page.ts new file mode 100644 index 000000000..cb22f36e9 --- /dev/null +++ b/web/src/elements/Page.ts @@ -0,0 +1,25 @@ +import { gettext } from "django"; +import { LitElement } from "lit-element"; +import { html, TemplateResult } from "lit-html"; + +export abstract class Page extends LitElement { + abstract pageTitle(): string; + abstract pageDescription(): string | undefined; + abstract pageIcon(): string; + + abstract renderContent(): TemplateResult; + + render(): TemplateResult { + const description = this.pageDescription(); + return html`
+
+

+ + ${gettext(this.pageTitle())} +

+ ${description ? html`

${gettext(description)}

` : html``} +
+
+ ${this.renderContent()}`; + } +} diff --git a/web/src/pages/applications/ApplicationViewPage.ts b/web/src/pages/applications/ApplicationViewPage.ts index f461e5b38..76f6dfae8 100644 --- a/web/src/pages/applications/ApplicationViewPage.ts +++ b/web/src/pages/applications/ApplicationViewPage.ts @@ -49,7 +49,7 @@ export class ApplicationViewPage extends LitElement { -
+
-
+
diff --git a/web/src/pages/providers/ProviderViewPage.ts b/web/src/pages/providers/ProviderViewPage.ts index b2293d38b..7321929b9 100644 --- a/web/src/pages/providers/ProviderViewPage.ts +++ b/web/src/pages/providers/ProviderViewPage.ts @@ -1,8 +1,12 @@ -import { customElement, LitElement, property } from "lit-element"; +import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { Provider } from "../../api/Providers"; +import { COMMON_STYLES } from "../../common/styles"; import "../../elements/buttons/ModalButton"; import "../../elements/buttons/SpinnerButton"; +import { SpinnerSize } from "../../elements/Spinner"; + +import "./SAMLProviderViewPage"; @customElement("ak-provider-view") export class ProviderViewPage extends LitElement { @@ -19,4 +23,27 @@ export class ProviderViewPage extends LitElement { @property({ attribute: false }) provider?: Provider; + static get styles(): CSSResult[] { + return COMMON_STYLES; + } + + render(): TemplateResult { + if (!this.provider) { + return html`
+
+
+
+ +
+
+
+
`; + } + switch (this.provider?.object_type) { + case "saml": + return html``; + default: + return html`

Invalid provider type ${this.provider?.object_type}

`; + } + } } diff --git a/web/src/pages/providers/SAMLProviderViewPage.ts b/web/src/pages/providers/SAMLProviderViewPage.ts new file mode 100644 index 000000000..5af846cbe --- /dev/null +++ b/web/src/pages/providers/SAMLProviderViewPage.ts @@ -0,0 +1,142 @@ +import { gettext } from "django"; +import { CSSResult, customElement, html, property, TemplateResult } from "lit-element"; +import { until } from "lit-html/directives/until"; +import { Provider } from "../../api/Providers"; +import { SAMLProvider } from "../../api/providers/SAML"; +import { COMMON_STYLES } from "../../common/styles"; + +import "../../elements/buttons/ModalButton"; +import "../../elements/buttons/SpinnerButton"; +import "../../elements/CodeMirror"; +import "../../elements/Tabs"; +import { Page } from "../../elements/Page"; + +@customElement("ak-provider-saml-view") +export class SAMLProviderViewPage extends Page { + pageTitle(): string { + return gettext(`SAML Provider ${this.provider?.name}`); + } + pageDescription(): string | undefined { + return; + } + pageIcon(): string { + return "pf-icon pf-icon-integration"; + } + + @property() + set args(value: { [key: string]: number }) { + this.providerID = value.id; + } + + @property({type: Number}) + set providerID(value: number) { + SAMLProvider.get(value).then((app) => (this.provider = app)); + } + + @property({ attribute: false }) + provider?: SAMLProvider; + + static get styles(): CSSResult[] { + return COMMON_STYLES; + } + + constructor() { + super(); + this.addEventListener("ak-refresh", () => { + if (!this.provider?.pk) return; + this.providerID = this.provider?.pk; + }); + } + + renderContent(): TemplateResult { + if (!this.provider) { + return html``; + } + return html` +
+
+
+
+
+
+
+
+ ${gettext("Name")} +
+
+
${this.provider.name}
+
+
+
+
+ ${gettext("Assigned to application")} +
+
+ +
+
+
+
+ ${gettext("ACS URL")} +
+
+
${this.provider.acs_url}
+
+
+
+
+ ${gettext("Audience")} +
+
+
${this.provider.audience}
+
+
+
+
+ ${gettext("Issuer")} +
+
+
${this.provider.issuer}
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ ${until( + SAMLProvider.getMetadata(this.provider.pk).then(m => { + return html``; + }) + )} +
+ +
+
+
+
+
`; + } +}