web: add view page for SAML Provider
This commit is contained in:
parent
91d6a3c8c7
commit
6aa6615608
|
@ -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):
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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">
|
||||||
|
|
|
@ -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>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>`;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue