Merge branch 'master' into version-2021.2
This commit is contained in:
commit
d93927755a
|
@ -16,6 +16,14 @@ updates:
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
assignees:
|
assignees:
|
||||||
- BeryJu
|
- BeryJu
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: "/website"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
time: "04:00"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
assignees:
|
||||||
|
- BeryJu
|
||||||
- package-ecosystem: pip
|
- package-ecosystem: pip
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
|
|
|
@ -53,10 +53,10 @@
|
||||||
},
|
},
|
||||||
"autobahn": {
|
"autobahn": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:410a93e0e29882c8b5d5ab05d220b07609b886ef5f23c0b8d39153254ffd6895",
|
"sha256:93df8fc9d1821c9dabff9fed52181a9ad6eea5e9989d53102c391607d7c1666e",
|
||||||
"sha256:52ee4236ff9a1fcbbd9500439dcf3284284b37f8a6b31ecc8a36e00cf9f95049"
|
"sha256:cceed2121b7a93024daa93c91fae33007f8346f0e522796421f36a6183abea99"
|
||||||
],
|
],
|
||||||
"version": "==20.12.3"
|
"version": "==21.1.1"
|
||||||
},
|
},
|
||||||
"automat": {
|
"automat": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -74,17 +74,18 @@
|
||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1a282c1cd7d5028cbb3a75d747df32162295253f55d263ac85840e264830963b"
|
"sha256:92041aa7589c886020cabd80eb58b89ace2f0094571792fccae24b9a8b3b97d7",
|
||||||
|
"sha256:9f132c34e20110dea019293c89cede49b0a56be615b3e1debf98390ed9f1f7b9"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.17.2"
|
"version": "==1.17.3"
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7442fdbbdc841bfac7f94f92ecb807de070e32ed205743eb72d4ea27c5e8e778",
|
"sha256:1dae84c68b109f596f58cc2e9fa87704ccd40dcbc12144a89205f85efa7f9135",
|
||||||
"sha256:bf587b044983a91a0124cc133ff167b8528c19fbbc8f0b956d9a1ac256cad7d7"
|
"sha256:a0fdded1c9636899ab273f50bf123f79b91439a8c282b5face8b5f4a48b493cb"
|
||||||
],
|
],
|
||||||
"version": "==1.20.2"
|
"version": "==1.20.3"
|
||||||
},
|
},
|
||||||
"cachetools": {
|
"cachetools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends base_template|default:"generic/form.html" %}
|
||||||
|
|
||||||
|
{% load authentik_utils %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block above_form %}
|
||||||
|
<h1>
|
||||||
|
{% trans 'Generate Certificate-Key Pair' %}
|
||||||
|
</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block action %}
|
||||||
|
{% trans 'Generate Certificate-Key Pair' %}
|
||||||
|
{% endblock %}
|
|
@ -26,6 +26,12 @@
|
||||||
</ak-spinner-button>
|
</ak-spinner-button>
|
||||||
<div slot="modal"></div>
|
<div slot="modal"></div>
|
||||||
</ak-modal-button>
|
</ak-modal-button>
|
||||||
|
<ak-modal-button href="{% url 'authentik_admin:certificatekeypair-generate' %}">
|
||||||
|
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
||||||
|
{% trans 'Generate' %}
|
||||||
|
</ak-spinner-button>
|
||||||
|
<div slot="modal"></div>
|
||||||
|
</ak-modal-button>
|
||||||
<button role="ak-refresh" class="pf-c-button pf-m-primary">
|
<button role="ak-refresh" class="pf-c-button pf-m-primary">
|
||||||
{% trans 'Refresh' %}
|
{% trans 'Refresh' %}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<span class="pf-c-form__label-text">{% trans 'Passing' %}</span>
|
<span class="pf-c-form__label-text">{% trans 'Passing' %}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-form__group-control">
|
<div class="pf-c-form__group-label">
|
||||||
<div class="c-form__horizontal-group">
|
<div class="c-form__horizontal-group">
|
||||||
<span class="pf-c-form__label-text">{{ result.passing|yesno:"Yes,No" }}</span>
|
<span class="pf-c-form__label-text">{{ result.passing|yesno:"Yes,No" }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
<span class="pf-c-form__label-text">{% trans 'Messages' %}</span>
|
<span class="pf-c-form__label-text">{% trans 'Messages' %}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-form__group-control">
|
<div class="pf-c-form__group-label">
|
||||||
<div class="c-form__horizontal-group">
|
<div class="c-form__horizontal-group">
|
||||||
<ul>
|
<ul>
|
||||||
{% for m in result.messages %}
|
{% for m in result.messages %}
|
||||||
|
|
|
@ -1,181 +0,0 @@
|
||||||
{% extends "administration/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
{% load authentik_utils %}
|
|
||||||
{% load admin_reflection %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<section class="pf-c-page__main-section pf-m-light">
|
|
||||||
<div class="pf-c-content">
|
|
||||||
<h1>
|
|
||||||
<i class="pf-icon pf-icon-integration"></i>
|
|
||||||
{% trans 'Providers' %}
|
|
||||||
</h1>
|
|
||||||
<p>{% trans "Provide support for protocols like SAML and OAuth to assigned applications." %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
|
||||||
<div class="pf-c-card">
|
|
||||||
{% if object_list %}
|
|
||||||
<div class="pf-c-toolbar">
|
|
||||||
<div class="pf-c-toolbar__content">
|
|
||||||
{% include 'partials/toolbar_search.html' %}
|
|
||||||
<div class="pf-c-toolbar__bulk-select">
|
|
||||||
<ak-dropdown class="pf-c-dropdown">
|
|
||||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
|
||||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
|
||||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<ul class="pf-c-dropdown__menu" hidden>
|
|
||||||
{% for type, name in types.items %}
|
|
||||||
<li>
|
|
||||||
<ak-modal-button href="{% url 'authentik_admin:provider-create' %}?type={{ type }}">
|
|
||||||
<button slot="trigger" class="pf-c-dropdown__menu-item">
|
|
||||||
{{ name|verbose_name }}<br>
|
|
||||||
<small>
|
|
||||||
{{ name|doc }}
|
|
||||||
</small>
|
|
||||||
</button>
|
|
||||||
<div slot="modal"></div>
|
|
||||||
</ak-modal-button>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
<li>
|
|
||||||
<ak-modal-button href="{% url 'authentik_admin:provider-saml-from-metadata' %}">
|
|
||||||
<button slot="trigger" class="pf-c-dropdown__menu-item">
|
|
||||||
{% trans 'SAML Provider from Metadata' %}<br>
|
|
||||||
<small>
|
|
||||||
{% trans "Create a SAML Provider by importing its Metadata." %}
|
|
||||||
</small>
|
|
||||||
</button>
|
|
||||||
<div slot="modal"></div>
|
|
||||||
</ak-modal-button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</ak-dropdown>
|
|
||||||
<button role="ak-refresh" class="pf-c-button pf-m-primary">
|
|
||||||
{% trans 'Refresh' %}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% include 'partials/pagination.html' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
|
||||||
<thead>
|
|
||||||
<tr role="row">
|
|
||||||
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
|
|
||||||
<th role="columnheader" scope="col">{% trans 'Type' %}</th>
|
|
||||||
<th role="cell"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody role="rowgroup">
|
|
||||||
{% for provider in object_list %}
|
|
||||||
<tr role="row">
|
|
||||||
<th role="columnheader">
|
|
||||||
<div>
|
|
||||||
<div>{{ provider.name }}</div>
|
|
||||||
{% if not provider.application %}
|
|
||||||
<i class="pf-icon pf-icon-warning-triangle"></i>
|
|
||||||
<small>{% trans 'Warning: Provider not assigned to any application.' %}</small>
|
|
||||||
{% else %}
|
|
||||||
<i class="pf-icon pf-icon-ok"></i>
|
|
||||||
<small>
|
|
||||||
{% blocktrans with app=provider.application %}
|
|
||||||
Assigned to application {{ app }}.
|
|
||||||
{% endblocktrans %}
|
|
||||||
</small>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<td role="cell">
|
|
||||||
<span>
|
|
||||||
{{ provider|verbose_name }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<ak-modal-button href="{% url 'authentik_admin:provider-update' pk=provider.pk %}">
|
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-secondary">
|
|
||||||
{% trans 'Edit' %}
|
|
||||||
</ak-spinner-button>
|
|
||||||
<div slot="modal"></div>
|
|
||||||
</ak-modal-button>
|
|
||||||
<ak-modal-button href="{% url 'authentik_admin:provider-delete' pk=provider.pk %}">
|
|
||||||
<ak-spinner-button slot="trigger" class="pf-m-danger">
|
|
||||||
{% trans 'Delete' %}
|
|
||||||
</ak-spinner-button>
|
|
||||||
<div slot="modal"></div>
|
|
||||||
</ak-modal-button>
|
|
||||||
{% get_links provider as links %}
|
|
||||||
{% for name, href in links.items %}
|
|
||||||
<a class="pf-c-button pf-m-tertiary ak-root-link" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
|
|
||||||
{% endfor %}
|
|
||||||
{% get_htmls provider as htmls %}
|
|
||||||
{% for html in htmls %}
|
|
||||||
{{ html|safe }}
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="pf-c-pagination pf-m-bottom">
|
|
||||||
{% include 'partials/pagination.html' %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="pf-c-toolbar">
|
|
||||||
<div class="pf-c-toolbar__content">
|
|
||||||
{% include 'partials/toolbar_search.html' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-empty-state">
|
|
||||||
<div class="pf-c-empty-state__content">
|
|
||||||
<i class="pf-icon-integration pf-c-empty-state__icon" aria-hidden="true"></i>
|
|
||||||
<h1 class="pf-c-title pf-m-lg">
|
|
||||||
{% trans 'No Providers.' %}
|
|
||||||
</h1>
|
|
||||||
<div class="pf-c-empty-state__body">
|
|
||||||
{% if request.GET.search != "" %}
|
|
||||||
{% trans "Your search query doesn't match any providers." %}
|
|
||||||
{% else %}
|
|
||||||
{% trans 'Currently no providers exist. Click the button below to create one.' %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<ak-dropdown class="pf-c-dropdown">
|
|
||||||
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
|
|
||||||
<span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
|
|
||||||
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
<ul class="pf-c-dropdown__menu" hidden>
|
|
||||||
{% for type, name in types.items %}
|
|
||||||
<li>
|
|
||||||
<ak-modal-button href="{% url 'authentik_admin:provider-create' %}?type={{ type }}">
|
|
||||||
<button slot="trigger" class="pf-c-dropdown__menu-item">
|
|
||||||
{{ name|verbose_name }}<br>
|
|
||||||
<small>
|
|
||||||
{{ name|doc }}
|
|
||||||
</small>
|
|
||||||
</button>
|
|
||||||
<div slot="modal"></div>
|
|
||||||
</ak-modal-button>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
<li>
|
|
||||||
<ak-modal-button href="{% url 'authentik_admin:provider-saml-from-metadata' %}">
|
|
||||||
<button slot="trigger" class="pf-c-dropdown__menu-item">
|
|
||||||
{% trans 'SAML Provider from Metadata' %}<br>
|
|
||||||
<small>
|
|
||||||
{% trans "Create a SAML Provider by importing its Metadata." %}
|
|
||||||
</small>
|
|
||||||
</button>
|
|
||||||
<div slot="modal"></div>
|
|
||||||
</ak-modal-button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</ak-dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
|
@ -24,7 +24,7 @@ from authentik.admin.views import (
|
||||||
tokens,
|
tokens,
|
||||||
users,
|
users,
|
||||||
)
|
)
|
||||||
from authentik.providers.saml.views import MetadataImportView
|
from authentik.providers.saml.views.metadata import MetadataImportView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
|
@ -113,7 +113,6 @@ urlpatterns = [
|
||||||
name="policy-binding-delete",
|
name="policy-binding-delete",
|
||||||
),
|
),
|
||||||
# Providers
|
# Providers
|
||||||
path("providers/", providers.ProviderListView.as_view(), name="providers"),
|
|
||||||
path(
|
path(
|
||||||
"providers/create/",
|
"providers/create/",
|
||||||
providers.ProviderCreateView.as_view(),
|
providers.ProviderCreateView.as_view(),
|
||||||
|
@ -296,6 +295,11 @@ urlpatterns = [
|
||||||
certificate_key_pair.CertificateKeyPairCreateView.as_view(),
|
certificate_key_pair.CertificateKeyPairCreateView.as_view(),
|
||||||
name="certificatekeypair-create",
|
name="certificatekeypair-create",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"crypto/certificates/generate/",
|
||||||
|
certificate_key_pair.CertificateKeyPairGenerateView.as_view(),
|
||||||
|
name="certificatekeypair-generate",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"crypto/certificates/<uuid:pk>/update/",
|
"crypto/certificates/<uuid:pk>/update/",
|
||||||
certificate_key_pair.CertificateKeyPairUpdateView.as_view(),
|
certificate_key_pair.CertificateKeyPairUpdateView.as_view(),
|
||||||
|
@ -329,22 +333,22 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
# Outpost Service Connections
|
# Outpost Service Connections
|
||||||
path(
|
path(
|
||||||
"outposts/service_connections/",
|
"outpost_service_connections/",
|
||||||
outposts_service_connections.OutpostServiceConnectionListView.as_view(),
|
outposts_service_connections.OutpostServiceConnectionListView.as_view(),
|
||||||
name="outpost-service-connections",
|
name="outpost-service-connections",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"outposts/service_connections/create/",
|
"outpost_service_connections/create/",
|
||||||
outposts_service_connections.OutpostServiceConnectionCreateView.as_view(),
|
outposts_service_connections.OutpostServiceConnectionCreateView.as_view(),
|
||||||
name="outpost-service-connection-create",
|
name="outpost-service-connection-create",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"outposts/service_connections/<uuid:pk>/update/",
|
"outpost_service_connections/<uuid:pk>/update/",
|
||||||
outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(),
|
outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(),
|
||||||
name="outpost-service-connection-update",
|
name="outpost-service-connection-update",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"outposts/service_connections/<uuid:pk>/delete/",
|
"outpost_service_connections/<uuid:pk>/delete/",
|
||||||
outposts_service_connections.OutpostServiceConnectionDeleteView.as_view(),
|
outposts_service_connections.OutpostServiceConnectionDeleteView.as_view(),
|
||||||
name="outpost-service-connection-delete",
|
name="outpost-service-connection-delete",
|
||||||
),
|
),
|
||||||
|
|
|
@ -4,9 +4,11 @@ from django.contrib.auth.mixins import (
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||||
)
|
)
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
|
from django.http.response import HttpResponse
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
|
from django.views.generic.edit import FormView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from authentik.admin.views.utils import (
|
from authentik.admin.views.utils import (
|
||||||
|
@ -15,7 +17,11 @@ from authentik.admin.views.utils import (
|
||||||
SearchListMixin,
|
SearchListMixin,
|
||||||
UserPaginateListMixin,
|
UserPaginateListMixin,
|
||||||
)
|
)
|
||||||
from authentik.crypto.forms import CertificateKeyPairForm
|
from authentik.crypto.builder import CertificateBuilder
|
||||||
|
from authentik.crypto.forms import (
|
||||||
|
CertificateKeyPairForm,
|
||||||
|
CertificateKeyPairGenerateForm,
|
||||||
|
)
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.lib.views import CreateAssignPermView
|
from authentik.lib.views import CreateAssignPermView
|
||||||
|
|
||||||
|
@ -52,7 +58,35 @@ class CertificateKeyPairCreateView(
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
template_name = "generic/create.html"
|
||||||
success_url = reverse_lazy("authentik_admin:certificate_key_pair")
|
success_url = reverse_lazy("authentik_admin:certificate_key_pair")
|
||||||
success_message = _("Successfully created CertificateKeyPair")
|
success_message = _("Successfully created Certificate-Key Pair")
|
||||||
|
|
||||||
|
|
||||||
|
class CertificateKeyPairGenerateView(
|
||||||
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
LoginRequiredMixin,
|
||||||
|
DjangoPermissionRequiredMixin,
|
||||||
|
FormView,
|
||||||
|
):
|
||||||
|
"""Generate new CertificateKeyPair"""
|
||||||
|
|
||||||
|
model = CertificateKeyPair
|
||||||
|
form_class = CertificateKeyPairGenerateForm
|
||||||
|
permission_required = "authentik_crypto.add_certificatekeypair"
|
||||||
|
|
||||||
|
template_name = "administration/certificatekeypair/generate.html"
|
||||||
|
success_url = reverse_lazy("authentik_admin:certificate_key_pair")
|
||||||
|
success_message = _("Successfully generated Certificate-Key Pair")
|
||||||
|
|
||||||
|
def form_valid(self, form: CertificateKeyPairGenerateForm) -> HttpResponse:
|
||||||
|
builder = CertificateBuilder()
|
||||||
|
builder.common_name = form.data["common_name"]
|
||||||
|
builder.build(
|
||||||
|
subject_alt_names=form.data.get("subject_alt_name", "").split(","),
|
||||||
|
validity_days=int(form.data["validity_days"]),
|
||||||
|
)
|
||||||
|
builder.save()
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairUpdateView(
|
class CertificateKeyPairUpdateView(
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
|
from django.urls.base import reverse_lazy
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
@ -20,7 +21,7 @@ class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
|
||||||
form_class = PolicyCacheClearForm
|
form_class = PolicyCacheClearForm
|
||||||
|
|
||||||
template_name = "generic/form_non_model.html"
|
template_name = "generic/form_non_model.html"
|
||||||
success_url = "/"
|
success_url = reverse_lazy("authentik_core:shell")
|
||||||
success_message = _("Successfully cleared Policy cache")
|
success_message = _("Successfully cleared Policy cache")
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
@ -39,7 +40,7 @@ class FlowCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
|
||||||
form_class = FlowCacheClearForm
|
form_class = FlowCacheClearForm
|
||||||
|
|
||||||
template_name = "generic/form_non_model.html"
|
template_name = "generic/form_non_model.html"
|
||||||
success_url = "/"
|
success_url = reverse_lazy("authentik_core:shell")
|
||||||
success_message = _("Successfully cleared Flow cache")
|
success_message = _("Successfully cleared Flow cache")
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
|
|
@ -115,6 +115,7 @@ class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, Fo
|
||||||
user = form.cleaned_data.get("user")
|
user = form.cleaned_data.get("user")
|
||||||
|
|
||||||
p_request = PolicyRequest(user)
|
p_request = PolicyRequest(user)
|
||||||
|
p_request.debug = True
|
||||||
p_request.http_request = self.request
|
p_request.http_request = self.request
|
||||||
p_request.context = form.cleaned_data.get("context", {})
|
p_request.context = form.cleaned_data.get("context", {})
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ class PropertyMappingCreateView(
|
||||||
permission_required = "authentik_core.add_propertymapping"
|
permission_required = "authentik_core.add_propertymapping"
|
||||||
|
|
||||||
template_name = "generic/create.html"
|
template_name = "generic/create.html"
|
||||||
success_url = reverse_lazy("authentik_admin:property-mappings")
|
success_url = reverse_lazy("authentik_core:shell")
|
||||||
success_message = _("Successfully created Property Mapping")
|
success_message = _("Successfully created Property Mapping")
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class PropertyMappingUpdateView(
|
||||||
permission_required = "authentik_core.change_propertymapping"
|
permission_required = "authentik_core.change_propertymapping"
|
||||||
|
|
||||||
template_name = "generic/update.html"
|
template_name = "generic/update.html"
|
||||||
success_url = reverse_lazy("authentik_admin:property-mappings")
|
success_url = reverse_lazy("authentik_core:shell")
|
||||||
success_message = _("Successfully updated Property Mapping")
|
success_message = _("Successfully updated Property Mapping")
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ class PropertyMappingDeleteView(
|
||||||
permission_required = "authentik_core.delete_propertymapping"
|
permission_required = "authentik_core.delete_propertymapping"
|
||||||
|
|
||||||
template_name = "generic/delete.html"
|
template_name = "generic/delete.html"
|
||||||
success_url = reverse_lazy("authentik_admin:property-mappings")
|
success_url = reverse_lazy("authentik_core:shell")
|
||||||
success_message = _("Successfully deleted Property Mapping")
|
success_message = _("Successfully deleted Property Mapping")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,36 +6,17 @@ from django.contrib.auth.mixins import (
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionRequiredMixin
|
||||||
|
|
||||||
from authentik.admin.views.utils import (
|
from authentik.admin.views.utils import (
|
||||||
BackSuccessUrlMixin,
|
BackSuccessUrlMixin,
|
||||||
DeleteMessageView,
|
DeleteMessageView,
|
||||||
InheritanceCreateView,
|
InheritanceCreateView,
|
||||||
InheritanceListView,
|
|
||||||
InheritanceUpdateView,
|
InheritanceUpdateView,
|
||||||
SearchListMixin,
|
|
||||||
UserPaginateListMixin,
|
|
||||||
)
|
)
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
|
|
||||||
|
|
||||||
class ProviderListView(
|
|
||||||
LoginRequiredMixin,
|
|
||||||
PermissionListMixin,
|
|
||||||
UserPaginateListMixin,
|
|
||||||
SearchListMixin,
|
|
||||||
InheritanceListView,
|
|
||||||
):
|
|
||||||
"""Show list of all providers"""
|
|
||||||
|
|
||||||
model = Provider
|
|
||||||
permission_required = "authentik_core.add_provider"
|
|
||||||
template_name = "administration/provider/list.html"
|
|
||||||
ordering = "pk"
|
|
||||||
search_fields = ["pk", "name"]
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderCreateView(
|
class ProviderCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
BackSuccessUrlMixin,
|
BackSuccessUrlMixin,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""Create self-signed certificates"""
|
"""Create self-signed certificates"""
|
||||||
import datetime
|
import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
@ -8,6 +9,9 @@ from cryptography.hazmat.primitives import hashes, serialization
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
from cryptography.x509.oid import NameOID
|
from cryptography.x509.oid import NameOID
|
||||||
|
|
||||||
|
from authentik import __version__
|
||||||
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
|
||||||
|
|
||||||
class CertificateBuilder:
|
class CertificateBuilder:
|
||||||
"""Build self-signed certificates"""
|
"""Build self-signed certificates"""
|
||||||
|
@ -17,19 +21,39 @@ class CertificateBuilder:
|
||||||
__builder = None
|
__builder = None
|
||||||
__certificate = None
|
__certificate = None
|
||||||
|
|
||||||
|
common_name: str
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.__public_key = None
|
self.__public_key = None
|
||||||
self.__private_key = None
|
self.__private_key = None
|
||||||
self.__builder = None
|
self.__builder = None
|
||||||
self.__certificate = None
|
self.__certificate = None
|
||||||
|
self.common_name = "authentik Self-signed Certificate"
|
||||||
|
|
||||||
def build(self):
|
def save(self) -> Optional[CertificateKeyPair]:
|
||||||
|
"""Save generated certificate as model"""
|
||||||
|
if not self.__certificate:
|
||||||
|
return None
|
||||||
|
return CertificateKeyPair.objects.create(
|
||||||
|
name=self.common_name,
|
||||||
|
certificate_data=self.certificate,
|
||||||
|
key_data=self.private_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
def build(
|
||||||
|
self,
|
||||||
|
validity_days: int = 365,
|
||||||
|
subject_alt_names: Optional[list[str]] = None,
|
||||||
|
):
|
||||||
"""Build self-signed certificate"""
|
"""Build self-signed certificate"""
|
||||||
one_day = datetime.timedelta(1, 0, 0)
|
one_day = datetime.timedelta(1, 0, 0)
|
||||||
self.__private_key = rsa.generate_private_key(
|
self.__private_key = rsa.generate_private_key(
|
||||||
public_exponent=65537, key_size=2048, backend=default_backend()
|
public_exponent=65537, key_size=2048, backend=default_backend()
|
||||||
)
|
)
|
||||||
self.__public_key = self.__private_key.public_key()
|
self.__public_key = self.__private_key.public_key()
|
||||||
|
alt_names: list[x509.GeneralName] = [
|
||||||
|
x509.DNSName(x) for x in subject_alt_names or []
|
||||||
|
]
|
||||||
self.__builder = (
|
self.__builder = (
|
||||||
x509.CertificateBuilder()
|
x509.CertificateBuilder()
|
||||||
.subject_name(
|
.subject_name(
|
||||||
|
@ -37,7 +61,7 @@ class CertificateBuilder:
|
||||||
[
|
[
|
||||||
x509.NameAttribute(
|
x509.NameAttribute(
|
||||||
NameOID.COMMON_NAME,
|
NameOID.COMMON_NAME,
|
||||||
"authentik Self-signed Certificate",
|
self.common_name,
|
||||||
),
|
),
|
||||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "authentik"),
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "authentik"),
|
||||||
x509.NameAttribute(
|
x509.NameAttribute(
|
||||||
|
@ -51,13 +75,16 @@ class CertificateBuilder:
|
||||||
[
|
[
|
||||||
x509.NameAttribute(
|
x509.NameAttribute(
|
||||||
NameOID.COMMON_NAME,
|
NameOID.COMMON_NAME,
|
||||||
"authentik Self-signed Certificate",
|
f"authentik {__version__}",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.add_extension(x509.SubjectAlternativeName(alt_names), critical=True)
|
||||||
.not_valid_before(datetime.datetime.today() - one_day)
|
.not_valid_before(datetime.datetime.today() - one_day)
|
||||||
.not_valid_after(datetime.datetime.today() + datetime.timedelta(days=365))
|
.not_valid_after(
|
||||||
|
datetime.datetime.today() + datetime.timedelta(days=validity_days)
|
||||||
|
)
|
||||||
.serial_number(int(uuid.uuid4()))
|
.serial_number(int(uuid.uuid4()))
|
||||||
.public_key(self.__public_key)
|
.public_key(self.__public_key)
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,6 +8,14 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
|
||||||
|
|
||||||
|
class CertificateKeyPairGenerateForm(forms.Form):
|
||||||
|
"""CertificateKeyPair generation form"""
|
||||||
|
|
||||||
|
common_name = forms.CharField()
|
||||||
|
subject_alt_name = forms.CharField(required=False, label=_("Subject-alt name"))
|
||||||
|
validity_days = forms.IntegerField(initial=365)
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairForm(forms.ModelForm):
|
class CertificateKeyPairForm(forms.ModelForm):
|
||||||
"""CertificateKeyPair Form"""
|
"""CertificateKeyPair Form"""
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,7 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
|
||||||
# Create the notification objects
|
# Create the notification objects
|
||||||
for transport in trigger.transports.all():
|
for transport in trigger.transports.all():
|
||||||
for user in trigger.group.users.all():
|
for user in trigger.group.users.all():
|
||||||
LOGGER.debug("created notif")
|
LOGGER.debug("created notification")
|
||||||
notification = Notification.objects.create(
|
notification = Notification.objects.create(
|
||||||
severity=trigger.severity, body=event.summary, event=event, user=user
|
severity=trigger.severity, body=event.summary, event=event, user=user
|
||||||
)
|
)
|
||||||
|
|
|
@ -80,7 +80,7 @@ class PolicyProcess(PROCESS_CLASS):
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
policy_result = self.binding.policy.passes(self.request)
|
policy_result = self.binding.policy.passes(self.request)
|
||||||
if self.binding.policy.execution_logging:
|
if self.binding.policy.execution_logging and not self.request.debug:
|
||||||
self.create_event(
|
self.create_event(
|
||||||
EventAction.POLICY_EXECUTION,
|
EventAction.POLICY_EXECUTION,
|
||||||
message="Policy Execution",
|
message="Policy Execution",
|
||||||
|
@ -94,7 +94,8 @@ class PolicyProcess(PROCESS_CLASS):
|
||||||
+ "".join(format_tb(src_exc.__traceback__))
|
+ "".join(format_tb(src_exc.__traceback__))
|
||||||
+ str(src_exc)
|
+ str(src_exc)
|
||||||
)
|
)
|
||||||
# Create policy exception event
|
# Create policy exception event, only when we're not debugging
|
||||||
|
if not self.request.debug:
|
||||||
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)
|
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)
|
||||||
LOGGER.debug("P_ENG(proc): error", exc=src_exc)
|
LOGGER.debug("P_ENG(proc): error", exc=src_exc)
|
||||||
policy_result = PolicyResult(False, str(src_exc))
|
policy_result = PolicyResult(False, str(src_exc))
|
||||||
|
|
|
@ -20,6 +20,7 @@ class PolicyRequest:
|
||||||
http_request: Optional[HttpRequest]
|
http_request: Optional[HttpRequest]
|
||||||
obj: Optional[Model]
|
obj: Optional[Model]
|
||||||
context: dict[str, Any]
|
context: dict[str, Any]
|
||||||
|
debug: bool = False
|
||||||
|
|
||||||
def __init__(self, user: User):
|
def __init__(self, user: User):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
|
@ -11,7 +11,7 @@ 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
|
from authentik.providers.saml.views.metadata import DescriptorDownloadView
|
||||||
|
|
||||||
|
|
||||||
class SAMLProviderSerializer(ProviderSerializer):
|
class SAMLProviderSerializer(ProviderSerializer):
|
||||||
|
|
|
@ -19,6 +19,7 @@ class SAMLProviderManager(ObjectManager):
|
||||||
name="authentik default SAML Mapping: UPN",
|
name="authentik default SAML Mapping: UPN",
|
||||||
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn",
|
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn",
|
||||||
expression="return user.attributes.get('upn', user.email)",
|
expression="return user.attributes.get('upn', user.email)",
|
||||||
|
friendly_name="",
|
||||||
),
|
),
|
||||||
EnsureExists(
|
EnsureExists(
|
||||||
SAMLPropertyMapping,
|
SAMLPropertyMapping,
|
||||||
|
@ -26,6 +27,7 @@ class SAMLProviderManager(ObjectManager):
|
||||||
name="authentik default SAML Mapping: Name",
|
name="authentik default SAML Mapping: Name",
|
||||||
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
|
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
|
||||||
expression="return user.name",
|
expression="return user.name",
|
||||||
|
friendly_name="",
|
||||||
),
|
),
|
||||||
EnsureExists(
|
EnsureExists(
|
||||||
SAMLPropertyMapping,
|
SAMLPropertyMapping,
|
||||||
|
@ -33,6 +35,7 @@ class SAMLProviderManager(ObjectManager):
|
||||||
name="authentik default SAML Mapping: Email",
|
name="authentik default SAML Mapping: Email",
|
||||||
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||||
expression="return user.email",
|
expression="return user.email",
|
||||||
|
friendly_name="",
|
||||||
),
|
),
|
||||||
EnsureExists(
|
EnsureExists(
|
||||||
SAMLPropertyMapping,
|
SAMLPropertyMapping,
|
||||||
|
@ -40,6 +43,7 @@ class SAMLProviderManager(ObjectManager):
|
||||||
name="authentik default SAML Mapping: Username",
|
name="authentik default SAML Mapping: Username",
|
||||||
saml_name="http://schemas.goauthentik.io/2021/02/saml/username",
|
saml_name="http://schemas.goauthentik.io/2021/02/saml/username",
|
||||||
expression="return user.username",
|
expression="return user.username",
|
||||||
|
friendly_name="",
|
||||||
),
|
),
|
||||||
EnsureExists(
|
EnsureExists(
|
||||||
SAMLPropertyMapping,
|
SAMLPropertyMapping,
|
||||||
|
@ -47,6 +51,7 @@ class SAMLProviderManager(ObjectManager):
|
||||||
name="authentik default SAML Mapping: User ID",
|
name="authentik default SAML Mapping: User ID",
|
||||||
saml_name="http://schemas.goauthentik.io/2021/02/saml/uid",
|
saml_name="http://schemas.goauthentik.io/2021/02/saml/uid",
|
||||||
expression="return user.pk",
|
expression="return user.pk",
|
||||||
|
friendly_name="",
|
||||||
),
|
),
|
||||||
EnsureExists(
|
EnsureExists(
|
||||||
SAMLPropertyMapping,
|
SAMLPropertyMapping,
|
||||||
|
@ -54,6 +59,7 @@ class SAMLProviderManager(ObjectManager):
|
||||||
name="authentik default SAML Mapping: Groups",
|
name="authentik default SAML Mapping: Groups",
|
||||||
saml_name="http://schemas.xmlsoap.org/claims/Group",
|
saml_name="http://schemas.xmlsoap.org/claims/Group",
|
||||||
expression=GROUP_EXPRESSION,
|
expression=GROUP_EXPRESSION,
|
||||||
|
friendly_name="",
|
||||||
),
|
),
|
||||||
EnsureExists(
|
EnsureExists(
|
||||||
SAMLPropertyMapping,
|
SAMLPropertyMapping,
|
||||||
|
@ -63,5 +69,6 @@ class SAMLProviderManager(ObjectManager):
|
||||||
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
||||||
),
|
),
|
||||||
expression="return user.username",
|
expression="return user.username",
|
||||||
|
friendly_name="",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,29 +1,29 @@
|
||||||
"""authentik SAML IDP URLs"""
|
"""authentik SAML IDP URLs"""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from authentik.providers.saml import views
|
from authentik.providers.saml.views import metadata, sso
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# SSO Bindings
|
# SSO Bindings
|
||||||
path(
|
path(
|
||||||
"<slug:application_slug>/sso/binding/redirect/",
|
"<slug:application_slug>/sso/binding/redirect/",
|
||||||
views.SAMLSSOBindingRedirectView.as_view(),
|
sso.SAMLSSOBindingRedirectView.as_view(),
|
||||||
name="sso-redirect",
|
name="sso-redirect",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"<slug:application_slug>/sso/binding/post/",
|
"<slug:application_slug>/sso/binding/post/",
|
||||||
views.SAMLSSOBindingPOSTView.as_view(),
|
sso.SAMLSSOBindingPOSTView.as_view(),
|
||||||
name="sso-post",
|
name="sso-post",
|
||||||
),
|
),
|
||||||
# SSO IdP Initiated
|
# SSO IdP Initiated
|
||||||
path(
|
path(
|
||||||
"<slug:application_slug>/sso/binding/init/",
|
"<slug:application_slug>/sso/binding/init/",
|
||||||
views.SAMLSSOBindingInitView.as_view(),
|
sso.SAMLSSOBindingInitView.as_view(),
|
||||||
name="sso-init",
|
name="sso-init",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"<slug:application_slug>/metadata/",
|
"<slug:application_slug>/metadata/",
|
||||||
views.DescriptorDownloadView.as_view(),
|
metadata.DescriptorDownloadView.as_view(),
|
||||||
name="metadata",
|
name="metadata",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,284 +0,0 @@
|
||||||
"""authentik SAML IDP Views"""
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.core.validators import URLValidator
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
|
||||||
from django.urls.base import reverse_lazy
|
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
from django.utils.http import urlencode
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.views import View
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.views.generic.edit import FormView
|
|
||||||
from structlog.stdlib import get_logger
|
|
||||||
|
|
||||||
from authentik.core.models import Application, Provider
|
|
||||||
from authentik.events.models import Event, EventAction
|
|
||||||
from authentik.flows.models import in_memory_stage
|
|
||||||
from authentik.flows.planner import (
|
|
||||||
PLAN_CONTEXT_APPLICATION,
|
|
||||||
PLAN_CONTEXT_SSO,
|
|
||||||
FlowPlanner,
|
|
||||||
)
|
|
||||||
from authentik.flows.stage import StageView
|
|
||||||
from authentik.flows.views import SESSION_KEY_PLAN
|
|
||||||
from authentik.lib.utils.urls import redirect_with_qs
|
|
||||||
from authentik.lib.views import bad_request_message
|
|
||||||
from authentik.policies.views import PolicyAccessView
|
|
||||||
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
|
||||||
from authentik.providers.saml.forms import SAMLProviderImportForm
|
|
||||||
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
|
|
||||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
|
||||||
from authentik.providers.saml.processors.metadata import MetadataProcessor
|
|
||||||
from authentik.providers.saml.processors.metadata_parser import (
|
|
||||||
ServiceProviderMetadataParser,
|
|
||||||
)
|
|
||||||
from authentik.providers.saml.processors.request_parser import (
|
|
||||||
AuthNRequest,
|
|
||||||
AuthNRequestParser,
|
|
||||||
)
|
|
||||||
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode, nice64
|
|
||||||
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_TEMPLATE
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
URL_VALIDATOR = URLValidator(schemes=("http", "https"))
|
|
||||||
REQUEST_KEY_SAML_REQUEST = "SAMLRequest"
|
|
||||||
REQUEST_KEY_SAML_SIGNATURE = "Signature"
|
|
||||||
REQUEST_KEY_SAML_SIG_ALG = "SigAlg"
|
|
||||||
REQUEST_KEY_SAML_RESPONSE = "SAMLResponse"
|
|
||||||
REQUEST_KEY_RELAY_STATE = "RelayState"
|
|
||||||
|
|
||||||
SESSION_KEY_AUTH_N_REQUEST = "authn_request"
|
|
||||||
|
|
||||||
|
|
||||||
class SAMLSSOView(PolicyAccessView):
|
|
||||||
""" "SAML SSO Base View, which plans a flow and injects our final stage.
|
|
||||||
Calls get/post handler."""
|
|
||||||
|
|
||||||
def resolve_provider_application(self):
|
|
||||||
self.application = get_object_or_404(
|
|
||||||
Application, slug=self.kwargs["application_slug"]
|
|
||||||
)
|
|
||||||
self.provider: SAMLProvider = get_object_or_404(
|
|
||||||
SAMLProvider, pk=self.application.provider_id
|
|
||||||
)
|
|
||||||
|
|
||||||
def check_saml_request(self) -> Optional[HttpRequest]:
|
|
||||||
"""Handler to verify the SAML Request. Must be implemented by a subclass"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
|
||||||
"""Verify the SAML Request, and if valid initiate the FlowPlanner for the application"""
|
|
||||||
# Call the method handler, which checks the SAML
|
|
||||||
# Request and returns a HTTP Response on error
|
|
||||||
method_response = self.check_saml_request()
|
|
||||||
if method_response:
|
|
||||||
return method_response
|
|
||||||
# Regardless, we start the planner and return to it
|
|
||||||
planner = FlowPlanner(self.provider.authorization_flow)
|
|
||||||
planner.allow_empty_flows = True
|
|
||||||
plan = planner.plan(
|
|
||||||
request,
|
|
||||||
{
|
|
||||||
PLAN_CONTEXT_SSO: True,
|
|
||||||
PLAN_CONTEXT_APPLICATION: self.application,
|
|
||||||
PLAN_CONTEXT_CONSENT_TEMPLATE: "providers/saml/consent.html",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
plan.append(in_memory_stage(SAMLFlowFinalView))
|
|
||||||
request.session[SESSION_KEY_PLAN] = plan
|
|
||||||
return redirect_with_qs(
|
|
||||||
"authentik_flows:flow-executor-shell",
|
|
||||||
request.GET,
|
|
||||||
flow_slug=self.provider.authorization_flow.slug,
|
|
||||||
)
|
|
||||||
|
|
||||||
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
|
||||||
"""GET and POST use the same handler, but we can't
|
|
||||||
override .dispatch easily because PolicyAccessView's dispatch"""
|
|
||||||
return self.get(request, application_slug)
|
|
||||||
|
|
||||||
|
|
||||||
class SAMLSSOBindingRedirectView(SAMLSSOView):
|
|
||||||
"""SAML Handler for SSO/Redirect bindings, which are sent via GET"""
|
|
||||||
|
|
||||||
def check_saml_request(self) -> Optional[HttpRequest]:
|
|
||||||
"""Handle REDIRECT bindings"""
|
|
||||||
if REQUEST_KEY_SAML_REQUEST not in self.request.GET:
|
|
||||||
LOGGER.info("handle_saml_request: SAML payload missing")
|
|
||||||
return bad_request_message(
|
|
||||||
self.request, "The SAML request payload is missing."
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
auth_n_request = AuthNRequestParser(self.provider).parse_detached(
|
|
||||||
self.request.GET[REQUEST_KEY_SAML_REQUEST],
|
|
||||||
self.request.GET.get(REQUEST_KEY_RELAY_STATE),
|
|
||||||
self.request.GET.get(REQUEST_KEY_SAML_SIGNATURE),
|
|
||||||
self.request.GET.get(REQUEST_KEY_SAML_SIG_ALG),
|
|
||||||
)
|
|
||||||
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
|
|
||||||
except CannotHandleAssertion as exc:
|
|
||||||
Event.new(
|
|
||||||
EventAction.CONFIGURATION_ERROR,
|
|
||||||
provider=self.provider,
|
|
||||||
message=str(exc),
|
|
||||||
).save()
|
|
||||||
LOGGER.info(str(exc))
|
|
||||||
return bad_request_message(self.request, str(exc))
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(csrf_exempt, name="dispatch")
|
|
||||||
class SAMLSSOBindingPOSTView(SAMLSSOView):
|
|
||||||
"""SAML Handler for SSO/POST bindings"""
|
|
||||||
|
|
||||||
def check_saml_request(self) -> Optional[HttpRequest]:
|
|
||||||
"""Handle POST bindings"""
|
|
||||||
if REQUEST_KEY_SAML_REQUEST not in self.request.POST:
|
|
||||||
LOGGER.info("check_saml_request: SAML payload missing")
|
|
||||||
return bad_request_message(
|
|
||||||
self.request, "The SAML request payload is missing."
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
auth_n_request = AuthNRequestParser(self.provider).parse(
|
|
||||||
self.request.POST[REQUEST_KEY_SAML_REQUEST],
|
|
||||||
self.request.POST.get(REQUEST_KEY_RELAY_STATE),
|
|
||||||
)
|
|
||||||
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
|
|
||||||
except CannotHandleAssertion as exc:
|
|
||||||
LOGGER.info(str(exc))
|
|
||||||
return bad_request_message(self.request, str(exc))
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class SAMLSSOBindingInitView(SAMLSSOView):
|
|
||||||
"""SAML Handler for for IdP Initiated login flows"""
|
|
||||||
|
|
||||||
def check_saml_request(self) -> Optional[HttpRequest]:
|
|
||||||
"""Create SAML Response from scratch"""
|
|
||||||
LOGGER.debug(
|
|
||||||
"handle_saml_no_request: No SAML Request, using IdP-initiated flow."
|
|
||||||
)
|
|
||||||
auth_n_request = AuthNRequestParser(self.provider).idp_initiated()
|
|
||||||
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
|
|
||||||
|
|
||||||
|
|
||||||
# This View doesn't have a URL on purpose, as its called by the FlowExecutor
|
|
||||||
class SAMLFlowFinalView(StageView):
|
|
||||||
"""View used by FlowExecutor after all stages have passed. Logs the authorization,
|
|
||||||
and redirects to the SP (if REDIRECT is configured) or shows and auto-submit for
|
|
||||||
(if POST is configured)."""
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|
||||||
application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
|
|
||||||
provider: SAMLProvider = get_object_or_404(
|
|
||||||
SAMLProvider, pk=application.provider_id
|
|
||||||
)
|
|
||||||
# Log Application Authorization
|
|
||||||
Event.new(
|
|
||||||
EventAction.AUTHORIZE_APPLICATION,
|
|
||||||
authorized_application=application,
|
|
||||||
flow=self.executor.plan.flow_pk,
|
|
||||||
).from_http(self.request)
|
|
||||||
|
|
||||||
if SESSION_KEY_AUTH_N_REQUEST not in self.request.session:
|
|
||||||
return self.executor.stage_invalid()
|
|
||||||
|
|
||||||
auth_n_request: AuthNRequest = self.request.session.pop(
|
|
||||||
SESSION_KEY_AUTH_N_REQUEST
|
|
||||||
)
|
|
||||||
response = AssertionProcessor(
|
|
||||||
provider, request, auth_n_request
|
|
||||||
).build_response()
|
|
||||||
|
|
||||||
if provider.sp_binding == SAMLBindings.POST:
|
|
||||||
form_attrs = {
|
|
||||||
"ACSUrl": provider.acs_url,
|
|
||||||
REQUEST_KEY_SAML_RESPONSE: nice64(response),
|
|
||||||
}
|
|
||||||
if auth_n_request.relay_state:
|
|
||||||
form_attrs[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state
|
|
||||||
return render(
|
|
||||||
self.request,
|
|
||||||
"generic/autosubmit_form.html",
|
|
||||||
{
|
|
||||||
"url": provider.acs_url,
|
|
||||||
"title": _("Redirecting to %(app)s..." % {"app": application.name}),
|
|
||||||
"attrs": form_attrs,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if provider.sp_binding == SAMLBindings.REDIRECT:
|
|
||||||
url_args = {
|
|
||||||
REQUEST_KEY_SAML_RESPONSE: deflate_and_base64_encode(response),
|
|
||||||
}
|
|
||||||
if auth_n_request.relay_state:
|
|
||||||
url_args[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state
|
|
||||||
querystring = urlencode(url_args)
|
|
||||||
return redirect(f"{provider.acs_url}?{querystring}")
|
|
||||||
return bad_request_message(request, "Invalid sp_binding specified")
|
|
||||||
|
|
||||||
|
|
||||||
class DescriptorDownloadView(View):
|
|
||||||
"""Replies with the XML Metadata IDSSODescriptor."""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_metadata(request: HttpRequest, provider: SAMLProvider) -> str:
|
|
||||||
"""Return rendered XML Metadata"""
|
|
||||||
return MetadataProcessor(provider, request).build_entity_descriptor()
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
|
||||||
"""Replies with the XML Metadata IDSSODescriptor."""
|
|
||||||
application = get_object_or_404(Application, slug=application_slug)
|
|
||||||
provider: SAMLProvider = get_object_or_404(
|
|
||||||
SAMLProvider, pk=application.provider_id
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
metadata = DescriptorDownloadView.get_metadata(request, provider)
|
|
||||||
except Provider.application.RelatedObjectDoesNotExist: # pylint: disable=no-member
|
|
||||||
return bad_request_message(
|
|
||||||
request, "Provider is not assigned to an application."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
response = HttpResponse(metadata, content_type="application/xml")
|
|
||||||
response[
|
|
||||||
"Content-Disposition"
|
|
||||||
] = f'attachment; filename="{provider.name}_authentik_meta.xml"'
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataImportView(LoginRequiredMixin, FormView):
|
|
||||||
"""Import Metadata from XML, and create provider"""
|
|
||||||
|
|
||||||
form_class = SAMLProviderImportForm
|
|
||||||
template_name = "providers/saml/import.html"
|
|
||||||
success_url = reverse_lazy("authentik_admin:providers")
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
if not request.user.is_superuser:
|
|
||||||
return self.handle_no_permission()
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def form_valid(self, form: SAMLProviderImportForm) -> HttpResponse:
|
|
||||||
try:
|
|
||||||
metadata = ServiceProviderMetadataParser().parse(
|
|
||||||
form.cleaned_data["metadata"].read().decode()
|
|
||||||
)
|
|
||||||
metadata.to_provider(
|
|
||||||
form.cleaned_data["provider_name"],
|
|
||||||
form.cleaned_data["authorization_flow"],
|
|
||||||
)
|
|
||||||
messages.success(self.request, _("Successfully created Provider"))
|
|
||||||
except ValueError as exc:
|
|
||||||
LOGGER.warning(str(exc))
|
|
||||||
messages.error(
|
|
||||||
self.request,
|
|
||||||
_("Failed to import Metadata: %(message)s" % {"message": str(exc)}),
|
|
||||||
)
|
|
||||||
return super().form_invalid(form)
|
|
||||||
return super().form_valid(form)
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
"""authentik SAML IDP Views"""
|
||||||
|
from django.core.validators import URLValidator
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
from django.utils.http import urlencode
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.core.models import Application
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION
|
||||||
|
from authentik.flows.stage import StageView
|
||||||
|
from authentik.lib.views import bad_request_message
|
||||||
|
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
|
||||||
|
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||||
|
from authentik.providers.saml.processors.request_parser import AuthNRequest
|
||||||
|
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode, nice64
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
URL_VALIDATOR = URLValidator(schemes=("http", "https"))
|
||||||
|
REQUEST_KEY_SAML_REQUEST = "SAMLRequest"
|
||||||
|
REQUEST_KEY_SAML_SIGNATURE = "Signature"
|
||||||
|
REQUEST_KEY_SAML_SIG_ALG = "SigAlg"
|
||||||
|
REQUEST_KEY_SAML_RESPONSE = "SAMLResponse"
|
||||||
|
REQUEST_KEY_RELAY_STATE = "RelayState"
|
||||||
|
|
||||||
|
SESSION_KEY_AUTH_N_REQUEST = "authn_request"
|
||||||
|
|
||||||
|
|
||||||
|
# This View doesn't have a URL on purpose, as its called by the FlowExecutor
|
||||||
|
class SAMLFlowFinalView(StageView):
|
||||||
|
"""View used by FlowExecutor after all stages have passed. Logs the authorization,
|
||||||
|
and redirects to the SP (if REDIRECT is configured) or shows and auto-submit for
|
||||||
|
(if POST is configured)."""
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
|
||||||
|
provider: SAMLProvider = get_object_or_404(
|
||||||
|
SAMLProvider, pk=application.provider_id
|
||||||
|
)
|
||||||
|
# Log Application Authorization
|
||||||
|
Event.new(
|
||||||
|
EventAction.AUTHORIZE_APPLICATION,
|
||||||
|
authorized_application=application,
|
||||||
|
flow=self.executor.plan.flow_pk,
|
||||||
|
).from_http(self.request)
|
||||||
|
|
||||||
|
if SESSION_KEY_AUTH_N_REQUEST not in self.request.session:
|
||||||
|
return self.executor.stage_invalid()
|
||||||
|
|
||||||
|
auth_n_request: AuthNRequest = self.request.session.pop(
|
||||||
|
SESSION_KEY_AUTH_N_REQUEST
|
||||||
|
)
|
||||||
|
response = AssertionProcessor(
|
||||||
|
provider, request, auth_n_request
|
||||||
|
).build_response()
|
||||||
|
|
||||||
|
if provider.sp_binding == SAMLBindings.POST:
|
||||||
|
form_attrs = {
|
||||||
|
"ACSUrl": provider.acs_url,
|
||||||
|
REQUEST_KEY_SAML_RESPONSE: nice64(response),
|
||||||
|
}
|
||||||
|
if auth_n_request.relay_state:
|
||||||
|
form_attrs[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state
|
||||||
|
return render(
|
||||||
|
self.request,
|
||||||
|
"generic/autosubmit_form.html",
|
||||||
|
{
|
||||||
|
"url": provider.acs_url,
|
||||||
|
"title": _("Redirecting to %(app)s..." % {"app": application.name}),
|
||||||
|
"attrs": form_attrs,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if provider.sp_binding == SAMLBindings.REDIRECT:
|
||||||
|
url_args = {
|
||||||
|
REQUEST_KEY_SAML_RESPONSE: deflate_and_base64_encode(response),
|
||||||
|
}
|
||||||
|
if auth_n_request.relay_state:
|
||||||
|
url_args[REQUEST_KEY_RELAY_STATE] = auth_n_request.relay_state
|
||||||
|
querystring = urlencode(url_args)
|
||||||
|
return redirect(f"{provider.acs_url}?{querystring}")
|
||||||
|
return bad_request_message(request, "Invalid sp_binding specified")
|
|
@ -0,0 +1,82 @@
|
||||||
|
"""authentik SAML IDP Views"""
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.urls.base import reverse_lazy
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views import View
|
||||||
|
from django.views.generic.edit import FormView
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.core.models import Application, Provider
|
||||||
|
from authentik.lib.views import bad_request_message
|
||||||
|
from authentik.providers.saml.forms import SAMLProviderImportForm
|
||||||
|
from authentik.providers.saml.models import SAMLProvider
|
||||||
|
from authentik.providers.saml.processors.metadata import MetadataProcessor
|
||||||
|
from authentik.providers.saml.processors.metadata_parser import (
|
||||||
|
ServiceProviderMetadataParser,
|
||||||
|
)
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class DescriptorDownloadView(View):
|
||||||
|
"""Replies with the XML Metadata IDSSODescriptor."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_metadata(request: HttpRequest, provider: SAMLProvider) -> str:
|
||||||
|
"""Return rendered XML Metadata"""
|
||||||
|
return MetadataProcessor(provider, request).build_entity_descriptor()
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||||
|
"""Replies with the XML Metadata IDSSODescriptor."""
|
||||||
|
application = get_object_or_404(Application, slug=application_slug)
|
||||||
|
provider: SAMLProvider = get_object_or_404(
|
||||||
|
SAMLProvider, pk=application.provider_id
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
metadata = DescriptorDownloadView.get_metadata(request, provider)
|
||||||
|
except Provider.application.RelatedObjectDoesNotExist: # pylint: disable=no-member
|
||||||
|
return bad_request_message(
|
||||||
|
request, "Provider is not assigned to an application."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response = HttpResponse(metadata, content_type="application/xml")
|
||||||
|
response[
|
||||||
|
"Content-Disposition"
|
||||||
|
] = f'attachment; filename="{provider.name}_authentik_meta.xml"'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class MetadataImportView(LoginRequiredMixin, FormView):
|
||||||
|
"""Import Metadata from XML, and create provider"""
|
||||||
|
|
||||||
|
form_class = SAMLProviderImportForm
|
||||||
|
template_name = "providers/saml/import.html"
|
||||||
|
success_url = reverse_lazy("authentik_admin:providers")
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
return self.handle_no_permission()
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def form_valid(self, form: SAMLProviderImportForm) -> HttpResponse:
|
||||||
|
try:
|
||||||
|
metadata = ServiceProviderMetadataParser().parse(
|
||||||
|
form.cleaned_data["metadata"].read().decode()
|
||||||
|
)
|
||||||
|
metadata.to_provider(
|
||||||
|
form.cleaned_data["provider_name"],
|
||||||
|
form.cleaned_data["authorization_flow"],
|
||||||
|
)
|
||||||
|
messages.success(self.request, _("Successfully created Provider"))
|
||||||
|
except ValueError as exc:
|
||||||
|
LOGGER.warning(str(exc))
|
||||||
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
_("Failed to import Metadata: %(message)s" % {"message": str(exc)}),
|
||||||
|
)
|
||||||
|
return super().form_invalid(form)
|
||||||
|
return super().form_valid(form)
|
|
@ -0,0 +1,150 @@
|
||||||
|
"""authentik SAML IDP Views"""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.core.models import Application
|
||||||
|
from authentik.events.models import Event, EventAction
|
||||||
|
from authentik.flows.models import in_memory_stage
|
||||||
|
from authentik.flows.planner import (
|
||||||
|
PLAN_CONTEXT_APPLICATION,
|
||||||
|
PLAN_CONTEXT_SSO,
|
||||||
|
FlowPlanner,
|
||||||
|
)
|
||||||
|
from authentik.flows.views import SESSION_KEY_PLAN
|
||||||
|
from authentik.lib.utils.urls import redirect_with_qs
|
||||||
|
from authentik.lib.views import bad_request_message
|
||||||
|
from authentik.policies.views import PolicyAccessView
|
||||||
|
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
||||||
|
from authentik.providers.saml.models import SAMLProvider
|
||||||
|
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
|
||||||
|
from authentik.providers.saml.views.flows import (
|
||||||
|
REQUEST_KEY_RELAY_STATE,
|
||||||
|
REQUEST_KEY_SAML_REQUEST,
|
||||||
|
REQUEST_KEY_SAML_SIG_ALG,
|
||||||
|
REQUEST_KEY_SAML_SIGNATURE,
|
||||||
|
SESSION_KEY_AUTH_N_REQUEST,
|
||||||
|
SAMLFlowFinalView,
|
||||||
|
)
|
||||||
|
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_TEMPLATE
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class SAMLSSOView(PolicyAccessView):
|
||||||
|
""" "SAML SSO Base View, which plans a flow and injects our final stage.
|
||||||
|
Calls get/post handler."""
|
||||||
|
|
||||||
|
def resolve_provider_application(self):
|
||||||
|
self.application = get_object_or_404(
|
||||||
|
Application, slug=self.kwargs["application_slug"]
|
||||||
|
)
|
||||||
|
self.provider: SAMLProvider = get_object_or_404(
|
||||||
|
SAMLProvider, pk=self.application.provider_id
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_saml_request(self) -> Optional[HttpRequest]:
|
||||||
|
"""Handler to verify the SAML Request. Must be implemented by a subclass"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def get(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||||
|
"""Verify the SAML Request, and if valid initiate the FlowPlanner for the application"""
|
||||||
|
# Call the method handler, which checks the SAML
|
||||||
|
# Request and returns a HTTP Response on error
|
||||||
|
method_response = self.check_saml_request()
|
||||||
|
if method_response:
|
||||||
|
return method_response
|
||||||
|
# Regardless, we start the planner and return to it
|
||||||
|
planner = FlowPlanner(self.provider.authorization_flow)
|
||||||
|
planner.allow_empty_flows = True
|
||||||
|
plan = planner.plan(
|
||||||
|
request,
|
||||||
|
{
|
||||||
|
PLAN_CONTEXT_SSO: True,
|
||||||
|
PLAN_CONTEXT_APPLICATION: self.application,
|
||||||
|
PLAN_CONTEXT_CONSENT_TEMPLATE: "providers/saml/consent.html",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
plan.append(in_memory_stage(SAMLFlowFinalView))
|
||||||
|
request.session[SESSION_KEY_PLAN] = plan
|
||||||
|
return redirect_with_qs(
|
||||||
|
"authentik_flows:flow-executor-shell",
|
||||||
|
request.GET,
|
||||||
|
flow_slug=self.provider.authorization_flow.slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
|
||||||
|
"""GET and POST use the same handler, but we can't
|
||||||
|
override .dispatch easily because PolicyAccessView's dispatch"""
|
||||||
|
return self.get(request, application_slug)
|
||||||
|
|
||||||
|
|
||||||
|
class SAMLSSOBindingRedirectView(SAMLSSOView):
|
||||||
|
"""SAML Handler for SSO/Redirect bindings, which are sent via GET"""
|
||||||
|
|
||||||
|
def check_saml_request(self) -> Optional[HttpRequest]:
|
||||||
|
"""Handle REDIRECT bindings"""
|
||||||
|
if REQUEST_KEY_SAML_REQUEST not in self.request.GET:
|
||||||
|
LOGGER.info("handle_saml_request: SAML payload missing")
|
||||||
|
return bad_request_message(
|
||||||
|
self.request, "The SAML request payload is missing."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
auth_n_request = AuthNRequestParser(self.provider).parse_detached(
|
||||||
|
self.request.GET[REQUEST_KEY_SAML_REQUEST],
|
||||||
|
self.request.GET.get(REQUEST_KEY_RELAY_STATE),
|
||||||
|
self.request.GET.get(REQUEST_KEY_SAML_SIGNATURE),
|
||||||
|
self.request.GET.get(REQUEST_KEY_SAML_SIG_ALG),
|
||||||
|
)
|
||||||
|
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
|
||||||
|
except CannotHandleAssertion as exc:
|
||||||
|
Event.new(
|
||||||
|
EventAction.CONFIGURATION_ERROR,
|
||||||
|
provider=self.provider,
|
||||||
|
message=str(exc),
|
||||||
|
).save()
|
||||||
|
LOGGER.info(str(exc))
|
||||||
|
return bad_request_message(self.request, str(exc))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name="dispatch")
|
||||||
|
class SAMLSSOBindingPOSTView(SAMLSSOView):
|
||||||
|
"""SAML Handler for SSO/POST bindings"""
|
||||||
|
|
||||||
|
def check_saml_request(self) -> Optional[HttpRequest]:
|
||||||
|
"""Handle POST bindings"""
|
||||||
|
if REQUEST_KEY_SAML_REQUEST not in self.request.POST:
|
||||||
|
LOGGER.info("check_saml_request: SAML payload missing")
|
||||||
|
return bad_request_message(
|
||||||
|
self.request, "The SAML request payload is missing."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
auth_n_request = AuthNRequestParser(self.provider).parse(
|
||||||
|
self.request.POST[REQUEST_KEY_SAML_REQUEST],
|
||||||
|
self.request.POST.get(REQUEST_KEY_RELAY_STATE),
|
||||||
|
)
|
||||||
|
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
|
||||||
|
except CannotHandleAssertion as exc:
|
||||||
|
LOGGER.info(str(exc))
|
||||||
|
return bad_request_message(self.request, str(exc))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class SAMLSSOBindingInitView(SAMLSSOView):
|
||||||
|
"""SAML Handler for for IdP Initiated login flows"""
|
||||||
|
|
||||||
|
def check_saml_request(self) -> Optional[HttpRequest]:
|
||||||
|
"""Create SAML Response from scratch"""
|
||||||
|
LOGGER.debug(
|
||||||
|
"handle_saml_no_request: No SAML Request, using IdP-initiated flow."
|
||||||
|
)
|
||||||
|
auth_n_request = AuthNRequestParser(self.provider).idp_initiated()
|
||||||
|
self.request.session[SESSION_KEY_AUTH_N_REQUEST] = auth_n_request
|
|
@ -94,12 +94,6 @@ class ASGILogger:
|
||||||
self.log(runtime)
|
self.log(runtime)
|
||||||
await send(message)
|
await send(message)
|
||||||
|
|
||||||
if self.headers.get(b"host", b"") == b"authentik-healthcheck-host":
|
|
||||||
# Don't log healthcheck/readiness requests
|
|
||||||
await send({"type": "http.response.start", "status": 204, "headers": []})
|
|
||||||
await send({"type": "http.response.body", "body": ""})
|
|
||||||
return
|
|
||||||
|
|
||||||
self.start = time()
|
self.start = time()
|
||||||
if scope["type"] == "lifespan":
|
if scope["type"] == "lifespan":
|
||||||
# https://code.djangoproject.com/ticket/31508
|
# https://code.djangoproject.com/ticket/31508
|
||||||
|
@ -129,7 +123,7 @@ class ASGILogger:
|
||||||
method=self.scope.get("method", ""),
|
method=self.scope.get("method", ""),
|
||||||
scheme=self.scope.get("scheme", ""),
|
scheme=self.scope.get("scheme", ""),
|
||||||
status=self.status_code,
|
status=self.status_code,
|
||||||
size=self.content_length / 1000 if self.content_length > 0 else "-",
|
size=self.content_length / 1000 if self.content_length > 0 else 0,
|
||||||
runtime=runtime,
|
runtime=runtime,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import connections
|
||||||
|
from django.db.utils import OperationalError
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django_prometheus.exports import ExportToDjangoView
|
from django_prometheus.exports import ExportToDjangoView
|
||||||
|
@ -23,3 +25,22 @@ class MetricsView(View):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
return ExportToDjangoView(request)
|
return ExportToDjangoView(request)
|
||||||
|
|
||||||
|
|
||||||
|
class LiveView(View):
|
||||||
|
"""View for liveness probe, always returns Http 201"""
|
||||||
|
|
||||||
|
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
return HttpResponse(status=201)
|
||||||
|
|
||||||
|
|
||||||
|
class ReadyView(View):
|
||||||
|
"""View for liveness probe, always returns Http 201"""
|
||||||
|
|
||||||
|
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
db_conn = connections["default"]
|
||||||
|
try:
|
||||||
|
_ = db_conn.cursor()
|
||||||
|
except OperationalError:
|
||||||
|
return HttpResponse(status=503)
|
||||||
|
return HttpResponse(status=201)
|
||||||
|
|
|
@ -9,7 +9,7 @@ from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.views import error
|
from authentik.core.views import error
|
||||||
from authentik.lib.utils.reflection import get_apps
|
from authentik.lib.utils.reflection import get_apps
|
||||||
from authentik.root.monitoring import MetricsView
|
from authentik.root.monitoring import LiveView, MetricsView, ReadyView
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
admin.autodiscover()
|
admin.autodiscover()
|
||||||
|
@ -57,6 +57,8 @@ for _authentik_app in get_apps():
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
path("administration/django/", admin.site.urls),
|
path("administration/django/", admin.site.urls),
|
||||||
path("metrics/", MetricsView.as_view(), name="metrics"),
|
path("metrics/", MetricsView.as_view(), name="metrics"),
|
||||||
|
path("-/health/live/", LiveView.as_view(), name="health-live"),
|
||||||
|
path("-/health/ready/", ReadyView.as_view(), name="health-ready"),
|
||||||
path("-/jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"),
|
path("-/jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ services:
|
||||||
traefik.http.routers.app-router.rule: PathPrefix(`/`)
|
traefik.http.routers.app-router.rule: PathPrefix(`/`)
|
||||||
traefik.http.routers.app-router.service: app-service
|
traefik.http.routers.app-router.service: app-service
|
||||||
traefik.http.routers.app-router.tls: 'true'
|
traefik.http.routers.app-router.tls: 'true'
|
||||||
traefik.http.services.app-service.loadbalancer.healthcheck.hostname: authentik-healthcheck-host
|
traefik.http.services.app-service.loadbalancer.healthcheck.path: /-/health/live/
|
||||||
traefik.http.services.app-service.loadbalancer.server.port: '8000'
|
traefik.http.services.app-service.loadbalancer.server.port: '8000'
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|
|
@ -97,18 +97,12 @@ spec:
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /-/health/live/
|
||||||
port: http
|
port: http
|
||||||
httpHeaders:
|
|
||||||
- name: Host
|
|
||||||
value: authentik-healthcheck-host
|
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /-/health/ready/
|
||||||
port: http
|
port: http
|
||||||
httpHeaders:
|
|
||||||
- name: Host
|
|
||||||
value: authentik-healthcheck-host
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: 100m
|
cpu: 100m
|
||||||
|
|
|
@ -77,7 +77,15 @@ func (ac *APIController) startWSHandler() {
|
||||||
logger.WithField("wait", notConnectedWait).Info("Not connected, trying again...")
|
logger.WithField("wait", notConnectedWait).Info("Not connected, trying again...")
|
||||||
time.Sleep(notConnectedWait)
|
time.Sleep(notConnectedWait)
|
||||||
notConnectedBackoff += notConnectedBackoff
|
notConnectedBackoff += notConnectedBackoff
|
||||||
|
// Limit backoff to max 60 seconds
|
||||||
|
if notConnectedBackoff >= 60 {
|
||||||
|
notConnectedBackoff = 60
|
||||||
|
}
|
||||||
|
ac.wsConn.CloseAndReconnect()
|
||||||
continue
|
continue
|
||||||
|
} else {
|
||||||
|
// When we're connected, reset backoff to 1
|
||||||
|
notConnectedBackoff = 1
|
||||||
}
|
}
|
||||||
var wsMsg websocketMessage
|
var wsMsg websocketMessage
|
||||||
err := ac.wsConn.ReadJSON(&wsMsg)
|
err := ac.wsConn.ReadJSON(&wsMsg)
|
||||||
|
@ -109,7 +117,7 @@ func (ac *APIController) startWSHealth() {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := ac.wsConn.WriteJSON(aliveMsg)
|
err := ac.wsConn.WriteJSON(aliveMsg)
|
||||||
ac.logger.WithField("loop", "ws-health").Debug("hello'd")
|
ac.logger.WithField("loop", "ws-health").Trace("hello'd")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ac.logger.WithField("loop", "ws-health").Println("write:", err)
|
ac.logger.WithField("loop", "ws-health").Println("write:", err)
|
||||||
ac.wsConn.CloseAndReconnect()
|
ac.wsConn.CloseAndReconnect()
|
||||||
|
|
|
@ -1537,9 +1537,9 @@
|
||||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||||
},
|
},
|
||||||
"fsevents": {
|
"fsevents": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
"integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==",
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"functional-red-black-tree": {
|
"functional-red-black-tree": {
|
||||||
|
@ -2638,9 +2638,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rollup": {
|
"rollup": {
|
||||||
"version": "2.38.4",
|
"version": "2.38.5",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.38.4.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.38.5.tgz",
|
||||||
"integrity": "sha512-B0LcJhjiwKkTl79aGVF/u5KdzsH8IylVfV56Ut6c9ouWLJcUK17T83aZBetNYSnZtXf2OHD4+2PbmRW+Fp5ulg==",
|
"integrity": "sha512-VoWt8DysFGDVRGWuHTqZzT02J0ASgjVq/hPs9QcBOGMd7B+jfTr/iqMVEyOi901rE3xq+Deq66GzIT1yt7sGwQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"fsevents": "~2.3.1"
|
"fsevents": "~2.3.1"
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
"flowchart.js": "^1.15.0",
|
"flowchart.js": "^1.15.0",
|
||||||
"lit-element": "^2.4.0",
|
"lit-element": "^2.4.0",
|
||||||
"lit-html": "^1.3.0",
|
"lit-html": "^1.3.0",
|
||||||
"rollup": "^2.38.4",
|
"rollup": "^2.38.5",
|
||||||
"rollup-plugin-copy": "^3.3.0",
|
"rollup-plugin-copy": "^3.3.0",
|
||||||
"rollup-plugin-cssimport": "^1.0.2",
|
"rollup-plugin-cssimport": "^1.0.2",
|
||||||
"rollup-plugin-external-globals": "^0.6.1",
|
"rollup-plugin-external-globals": "^0.6.1",
|
||||||
|
|
|
@ -28,7 +28,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
|
||||||
),
|
),
|
||||||
new SidebarItem("Providers", "/providers"),
|
new SidebarItem("Providers", "/providers"),
|
||||||
new SidebarItem("Outposts", "/administration/outposts/"),
|
new SidebarItem("Outposts", "/administration/outposts/"),
|
||||||
new SidebarItem("Outpost Service Connections", "/administration/outposts/service_connections/"),
|
new SidebarItem("Outpost Service Connections", "/administration/outpost_service_connections/"),
|
||||||
).when((): Promise<boolean> => {
|
).when((): Promise<boolean> => {
|
||||||
return User.me().then(u => u.is_superuser);
|
return User.me().then(u => u.is_superuser);
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -36,7 +36,7 @@ export class AdminOverviewPage extends LitElement {
|
||||||
<ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Apps with most usage" style="grid-column-end: span 2;grid-row-end: span 3;">
|
<ak-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Apps with most usage" style="grid-column-end: span 2;grid-row-end: span 3;">
|
||||||
<ak-top-applications-table></ak-top-applications-table>
|
<ak-top-applications-table></ak-top-applications-table>
|
||||||
</ak-aggregate-card>
|
</ak-aggregate-card>
|
||||||
<ak-admin-status-card-provider class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-plugged" header="Providers" headerLink="#/administration/providers/">
|
<ak-admin-status-card-provider class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-plugged" header="Providers" headerLink="#/providers/">
|
||||||
</ak-admin-status-card-provider>
|
</ak-admin-status-card-provider>
|
||||||
<ak-admin-status-card-policy-unbound class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-infrastructure" header="Policies" headerLink="#/administration/policies/">
|
<ak-admin-status-card-policy-unbound class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-infrastructure" header="Policies" headerLink="#/administration/policies/">
|
||||||
</ak-admin-status-card-policy-unbound>
|
</ak-admin-status-card-policy-unbound>
|
||||||
|
|
|
@ -68,7 +68,6 @@ In the `SAML Enabled Identity Providers` paste the following configuration:
|
||||||
"attr_user_permanent_id": "http://schemas.goauthentik.io/2021/02/saml/uid",
|
"attr_user_permanent_id": "http://schemas.goauthentik.io/2021/02/saml/uid",
|
||||||
"x509cert": "MIIDEjCCAfqgAwIBAgIRAJZ9pOZ1g0xjiHtQAAejsMEwDQYJKoZIhvcNAQELBQAwMDEuMCwGA1UEAwwlcGFzc2Jvb2sgU2VsZi1zaWduZWQgU0FNTCBDZXJ0aWZpY2F0ZTAeFw0xOTEyMjYyMDEwNDFaFw0yMDEyMjYyMDEwNDFaMFkxLjAsBgNVBAMMJXBhc3Nib29rIFNlbGYtc2lnbmVkIFNBTUwgQ2VydGlmaWNhdGUxETAPBgNVBAoMCHBhc3Nib29rMRQwEgYDVQQLDAtTZWxmLXNpZ25lZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO/ktBYZkY9xAijF4acvzX6Q1K8KoIZeyde8fVgcWBz4L5FgDQ4/dni4k2YAcPdwteGL4nKVzetUzjbRCBUNuO6lqU4J4WNNX4Xg4Ir7XLRoAQeo+omTPBdpJ1p02HjtN5jT01umN3bK2yto1e37CJhK6WJiaXqRewPxh4lI4aqdj3BhFkJ3I3r2qxaWOAXQ6X7fg3w/ny7QP53//ouZo7hSLY3GIcRKgvdjjVM3OW5C3WLpOq5Dez5GWVJ17aeFCfGQ8bwFKde6qfYqyGcU9xHB36TtVHB9hSFP/tUFhkiSOxtsrYwCgCyXm4UTSpP+wiNyjKfFw7qGLBvA2hGTNw8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh9PeAqPRQk1/SSygIFADZBi08O/DPCshFwEHvJATIcTzcDD8UGAjXh+H5OlkDyX7KyrcaNvYaafCUo63A+WprdtdY5Ty6SBEwTYyiQyQfwM9BfK+imCoif1Ai7xAelD7p9lNazWq7JU+H/Ep7U7Q7LvpxAbK0JArt+IWTb2NcMb3OWE1r0gFbs44O1l6W9UbJTbyLMzbGbe5i+NHlgnwPwuhtRMh0NUYabGHKcHbhwyFhfGAQv2dAp5KF1E5gu6ZzCiFePzc0FrqXQyb2zpFYcJHXquiqaOeG7cZxRHYcjrl10Vxzki64XVA9BpdELgKSnupDGUEJsRUt3WVOmvZuA==",
|
"x509cert": "MIIDEjCCAfqgAwIBAgIRAJZ9pOZ1g0xjiHtQAAejsMEwDQYJKoZIhvcNAQELBQAwMDEuMCwGA1UEAwwlcGFzc2Jvb2sgU2VsZi1zaWduZWQgU0FNTCBDZXJ0aWZpY2F0ZTAeFw0xOTEyMjYyMDEwNDFaFw0yMDEyMjYyMDEwNDFaMFkxLjAsBgNVBAMMJXBhc3Nib29rIFNlbGYtc2lnbmVkIFNBTUwgQ2VydGlmaWNhdGUxETAPBgNVBAoMCHBhc3Nib29rMRQwEgYDVQQLDAtTZWxmLXNpZ25lZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO/ktBYZkY9xAijF4acvzX6Q1K8KoIZeyde8fVgcWBz4L5FgDQ4/dni4k2YAcPdwteGL4nKVzetUzjbRCBUNuO6lqU4J4WNNX4Xg4Ir7XLRoAQeo+omTPBdpJ1p02HjtN5jT01umN3bK2yto1e37CJhK6WJiaXqRewPxh4lI4aqdj3BhFkJ3I3r2qxaWOAXQ6X7fg3w/ny7QP53//ouZo7hSLY3GIcRKgvdjjVM3OW5C3WLpOq5Dez5GWVJ17aeFCfGQ8bwFKde6qfYqyGcU9xHB36TtVHB9hSFP/tUFhkiSOxtsrYwCgCyXm4UTSpP+wiNyjKfFw7qGLBvA2hGTNw8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh9PeAqPRQk1/SSygIFADZBi08O/DPCshFwEHvJATIcTzcDD8UGAjXh+H5OlkDyX7KyrcaNvYaafCUo63A+WprdtdY5Ty6SBEwTYyiQyQfwM9BfK+imCoif1Ai7xAelD7p9lNazWq7JU+H/Ep7U7Q7LvpxAbK0JArt+IWTb2NcMb3OWE1r0gFbs44O1l6W9UbJTbyLMzbGbe5i+NHlgnwPwuhtRMh0NUYabGHKcHbhwyFhfGAQv2dAp5KF1E5gu6ZzCiFePzc0FrqXQyb2zpFYcJHXquiqaOeG7cZxRHYcjrl10Vxzki64XVA9BpdELgKSnupDGUEJsRUt3WVOmvZuA==",
|
||||||
"url": "https://authentik.company/application/saml/awx/login/",
|
"url": "https://authentik.company/application/saml/awx/login/",
|
||||||
"attr_last_name": "User.LastName",
|
|
||||||
"entity_id": "https://awx.company/sso/metadata/saml/",
|
"entity_id": "https://awx.company/sso/metadata/saml/",
|
||||||
"attr_email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
"attr_email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||||
"attr_first_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
|
"attr_first_name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
|
||||||
|
|
|
@ -18,14 +18,36 @@ The following placeholders will be used:
|
||||||
- `rancher.company` is the FQDN of the Rancher install.
|
- `rancher.company` is the FQDN of the Rancher install.
|
||||||
- `authentik.company` is the FQDN of the authentik install.
|
- `authentik.company` is the FQDN of the authentik install.
|
||||||
|
|
||||||
Create an application in authentik and note the slug, as this will be used later. Create a SAML provider with the following parameters:
|
Under *Property Mappings*, create a *SAML Property Mapping*. Give it a name like "SAML Rancher User ID". Set the SAML name to `rancherUidUsername` and the expression to the following
|
||||||
|
|
||||||
|
```python
|
||||||
|
return f"{user.pk}-{user.username}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Create an application in authentik. Create a SAML provider with the following parameters:
|
||||||
|
|
||||||
- ACS URL: `https://rancher.company/v1-saml/adfs/saml/acs`
|
- ACS URL: `https://rancher.company/v1-saml/adfs/saml/acs`
|
||||||
- Audience: `https://rancher.company/v1-saml/adfs/saml/metadata`
|
- Audience: `https://rancher.company/v1-saml/adfs/saml/metadata`
|
||||||
- Issuer: `authentik`
|
- Issuer: `authentik`
|
||||||
|
- Property mappings: Select all default mappings and the mapping you've created above.
|
||||||
|
|
||||||
You can of course use a custom signing certificate, and adjust durations.
|
You can of course use a custom signing certificate, and adjust durations.
|
||||||
|
|
||||||
## Rancher
|
## Rancher
|
||||||
|
|
||||||
|
In Rancher, navigate to *Global* -> *Security* -> *Authentication*, and select ADFS.
|
||||||
|
|
||||||
|
Fill in the fields
|
||||||
|
|
||||||
|
- Display Name Field: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name`
|
||||||
|
- User Name Field: `http://schemas.goauthentik.io/2021/02/saml/username`
|
||||||
|
- UID Field: `rancherUidUsername`
|
||||||
|
- Groups Field: `http://schemas.xmlsoap.org/claims/Group`
|
||||||
|
|
||||||
|
For the private key and certificate, you can either generate a new pair (in authentik, navigate to *Identity & Cryptography* -> *Certificates* and select Generate), or use an existing pair.
|
||||||
|
|
||||||
|
Copy the metadata from authentik, and paste it in the metadata field.
|
||||||
|
|
||||||
|
Click on save to test the authentication.
|
||||||
|
|
||||||
![](./rancher.png)
|
![](./rancher.png)
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 549 KiB After Width: | Height: | Size: 320 KiB |
|
@ -41,8 +41,8 @@ In authentik, get the Metadata URL by right-clicking `Download Metadata` and sel
|
||||||
|
|
||||||
On the next screen, input these Values
|
On the next screen, input these Values
|
||||||
|
|
||||||
IdP User ID: `http://schemas.goauthentik.io/2021/02/saml/uid`
|
- IdP User ID: `http://schemas.goauthentik.io/2021/02/saml/uid`
|
||||||
User Email: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`
|
- User Email: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`
|
||||||
First Name: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name`
|
- First Name: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name`
|
||||||
|
|
||||||
After confirming, Sentry will authenticate with authentik, and you should be redirected back to a page confirming your settings.
|
After confirming, Sentry will authenticate with authentik, and you should be redirected back to a page confirming your settings.
|
||||||
|
|
|
@ -44,10 +44,11 @@ Due to the switch to managed objects, some default property mappings are changin
|
||||||
The change affects the "SAML Name" property, which has been changed from an oid to a Schema URI to aid readability.
|
The change affects the "SAML Name" property, which has been changed from an oid to a Schema URI to aid readability.
|
||||||
|
|
||||||
The integrations affected are:
|
The integrations affected are:
|
||||||
- [NextCloud](../integrations/services/nextcloud/index)
|
- [Ansible Tower/AWX](/docs/integrations/services/awx-tower/index)
|
||||||
- [Sentry](../integrations/services/sentry/index)
|
- [GitLab](/docs/integrations/services/gitlab/index)
|
||||||
- [GitLab](../integrations/services/gitlab/index)
|
- [NextCloud](/docs/integrations/services/nextcloud/index)
|
||||||
- [Ansible Tower/AWX](../integrations/services/awx-tower/index)
|
- [Rancher](/docs/integrations/services/rancher/index)
|
||||||
|
- [Sentry](/docs/integrations/services/sentry/index)
|
||||||
|
|
||||||
### docker-compose
|
### docker-compose
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,19 +1,19 @@
|
||||||
{
|
{
|
||||||
"name": "docs",
|
"name": "authentik-docs",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docusaurus": "docusaurus",
|
"docusaurus": "docusaurus",
|
||||||
"start": "docusaurus start",
|
"watch": "docusaurus start",
|
||||||
"build": "docusaurus build",
|
"build": "docusaurus build",
|
||||||
"swizzle": "docusaurus swizzle",
|
"swizzle": "docusaurus swizzle",
|
||||||
"deploy": "docusaurus deploy",
|
"deploy": "docusaurus deploy",
|
||||||
"serve": "docusaurus serve"
|
"serve": "docusaurus serve"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "2.0.0-alpha.66",
|
"@docusaurus/core": "2.0.0-alpha.70",
|
||||||
"@docusaurus/preset-classic": "2.0.0-alpha.66",
|
"@docusaurus/preset-classic": "2.0.0-alpha.70",
|
||||||
"@mdx-js/react": "^1.5.8",
|
"@mdx-js/react": "^1.6.22",
|
||||||
"clsx": "^1.1.1",
|
"clsx": "^1.1.1",
|
||||||
"react": "^16.8.4",
|
"react": "^16.8.4",
|
||||||
"react-dom": "^16.8.4"
|
"react-dom": "^16.8.4"
|
||||||
|
@ -31,6 +31,6 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"prettier": "2.1.2"
|
"prettier": "2.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,6 +141,7 @@ module.exports = {
|
||||||
"releases/0.13",
|
"releases/0.13",
|
||||||
"releases/0.14",
|
"releases/0.14",
|
||||||
"releases/2021.1",
|
"releases/2021.1",
|
||||||
|
"releases/2021.2",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Reference in New Issue