web: add view page for SAML Provider

This commit is contained in:
Jens Langhammer 2021-02-06 17:55:41 +01:00
parent 91d6a3c8c7
commit 6aa6615608
8 changed files with 244 additions and 40 deletions

View File

@ -1,17 +1,17 @@
"""SAMLProvider API Views""" """SAMLProvider API Views"""
from drf_yasg2.utils import swagger_auto_schema 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.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.request import Request
from rest_framework.response import Response 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.providers import ProviderSerializer
from authentik.core.api.utils import MetaNameSerializer from authentik.core.api.utils import MetaNameSerializer
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.views import DescriptorDownloadView
class SAMLProviderSerializer(ProviderSerializer): class SAMLProviderSerializer(ProviderSerializer):
@ -41,6 +41,12 @@ class SAMLMetadataSerializer(Serializer):
metadata = ReadOnlyField() metadata = ReadOnlyField()
def create(self, request: Request) -> Response:
raise NotImplementedError
def update(self, request: Request) -> Response:
raise NotImplementedError
class SAMLProviderViewSet(ModelViewSet): class SAMLProviderViewSet(ModelViewSet):
"""SAMLProvider Viewset""" """SAMLProvider Viewset"""
@ -55,9 +61,7 @@ class SAMLProviderViewSet(ModelViewSet):
"""Return metadata as XML string""" """Return metadata as XML string"""
provider = get_object_or_404(SAMLProvider, pk=pk) provider = get_object_or_404(SAMLProvider, pk=pk)
metadata = DescriptorDownloadView.get_metadata(request, provider) metadata = DescriptorDownloadView.get_metadata(request, provider)
return Response({ return Response({"metadata": metadata})
"metadata": metadata
})
class SAMLPropertyMappingSerializer(ModelSerializer, MetaNameSerializer): class SAMLPropertyMappingSerializer(ModelSerializer, MetaNameSerializer):

View File

@ -4,15 +4,12 @@ from urllib.parse import urlparse
from django.db import models from django.db import models
from django.forms import ModelForm from django.forms import ModelForm
from django.http import HttpRequest
from django.shortcuts import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import PropertyMapping, Provider from authentik.core.models import PropertyMapping, Provider
from authentik.crypto.models import CertificateKeyPair 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.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.processors.constants import ( from authentik.sources.saml.processors.constants import (
DSA_SHA1, DSA_SHA1,
@ -182,31 +179,6 @@ class SAMLProvider(Provider):
def __str__(self): def __str__(self):
return f"SAML Provider {self.name}" 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: class Meta:
verbose_name = _("SAML Provider") verbose_name = _("SAML Provider")

View File

@ -4,6 +4,7 @@ export class Provider {
pk: number; pk: number;
name: string; name: string;
authorization_flow: string; authorization_flow: string;
object_type: string;
assigned_application_slug?: string; assigned_application_slug?: string;
assigned_application_name?: string; assigned_application_name?: string;

View File

@ -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<SAMLProvider> {
return DefaultClient.fetch<SAMLProvider>(["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}`;
}
}

25
web/src/elements/Page.ts Normal file
View File

@ -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`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="${this.pageIcon()}"></i>
${gettext(this.pageTitle())}
</h1>
${description ? html`<p>${gettext(description)}</p>` : html``}
</div>
</section>
${this.renderContent()}`;
}
}

View File

@ -49,7 +49,7 @@ export class ApplicationViewPage extends LitElement {
</div> </div>
</section> </section>
<ak-tabs> <ak-tabs>
<section slot="page-1" data-tab-title="Users" class="pf-c-page__main-section pf-m-no-padding-mobile"> <section slot="page-1" data-tab-title="${gettext("Users")}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-l-gallery pf-m-gutter"> <div class="pf-l-gallery pf-m-gutter">
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 3;grid-row-end: span 2;"> <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 3;grid-row-end: span 2;">
<div class="pf-c-card__header"> <div class="pf-c-card__header">
@ -66,7 +66,7 @@ export class ApplicationViewPage extends LitElement {
</div> </div>
</div> </div>
</section> </section>
<div slot="page-2" data-tab-title="Policy Bindings" class="pf-c-page__main-section pf-m-no-padding-mobile"> <div slot="page-2" data-tab-title="${gettext("Policy Bindings")}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card"> <div class="pf-c-card">
<div class="pf-c-card__header"> <div class="pf-c-card__header">
<div class="pf-c-card__header-main"> <div class="pf-c-card__header-main">

View File

@ -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 { Provider } from "../../api/Providers";
import { COMMON_STYLES } from "../../common/styles";
import "../../elements/buttons/ModalButton"; import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton"; import "../../elements/buttons/SpinnerButton";
import { SpinnerSize } from "../../elements/Spinner";
import "./SAMLProviderViewPage";
@customElement("ak-provider-view") @customElement("ak-provider-view")
export class ProviderViewPage extends LitElement { export class ProviderViewPage extends LitElement {
@ -19,4 +23,27 @@ export class ProviderViewPage extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
provider?: Provider; provider?: Provider;
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
render(): TemplateResult {
if (!this.provider) {
return html`<div class="pf-c-empty-state pf-m-full-height">
<div class="pf-c-empty-state__content">
<div class="pf-l-bullseye">
<div class="pf-l-bullseye__item">
<ak-spinner size="${SpinnerSize.XLarge}"></ak-spinner>
</div>
</div>
</div>
</div>`;
}
switch (this.provider?.object_type) {
case "saml":
return html`<ak-provider-saml-view providerID=${this.provider.pk}></ak-provider-saml-view>`;
default:
return html`<p>Invalid provider type ${this.provider?.object_type}</p>`;
}
}
} }

View File

@ -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`<ak-tabs>
<section slot="page-1" data-tab-title="${gettext("Overview")}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
<div class="pf-c-card pf-c-card-aggregate">
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-3-col-on-lg">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${gettext("Name")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${this.provider.name}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${gettext("Assigned to application")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<a href="#/applications/${this.provider.assigned_application_slug}">
${this.provider.assigned_application_name}
</a>
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${gettext("ACS URL")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${this.provider.acs_url}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${gettext("Audience")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${this.provider.audience}</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${gettext("Issuer")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">${this.provider.issuer}</div>
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__footer">
<ak-modal-button href="${Provider.adminUrl(`${this.provider.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</div>
</div>
</div>
</div>
</section>
<section slot="page-2" data-tab-title="${gettext("Metadata")}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-u-display-flex pf-u-justify-content-center">
<div class="pf-u-w-75">
<div class="pf-c-card pf-c-card-aggregate">
<div class="pf-c-card__body">
${until(
SAMLProvider.getMetadata(this.provider.pk).then(m => {
return html`<ak-codemirror mode="xml"><textarea class="pf-c-form-control" readonly>${m.metadata}</textarea></ak-codemirror>`;
})
)}
</div>
<div class="pf-c-card__footer">
<a class="pf-c-button pf-m-primary" target="_blank" href="${SAMLProvider.appUrl(`${this.provider.name}/metadata/`)}">
${gettext("Download")}
</a>
</div>
</div>
</div>
</div>
</section>
</ak-tabs>`;
}
}