Merge branch 'master' into version-2021.2
This commit is contained in:
commit
d93927755a
|
@ -16,6 +16,14 @@ updates:
|
|||
open-pull-requests-limit: 10
|
||||
assignees:
|
||||
- BeryJu
|
||||
- package-ecosystem: npm
|
||||
directory: "/website"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
assignees:
|
||||
- BeryJu
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
|
|
@ -53,10 +53,10 @@
|
|||
},
|
||||
"autobahn": {
|
||||
"hashes": [
|
||||
"sha256:410a93e0e29882c8b5d5ab05d220b07609b886ef5f23c0b8d39153254ffd6895",
|
||||
"sha256:52ee4236ff9a1fcbbd9500439dcf3284284b37f8a6b31ecc8a36e00cf9f95049"
|
||||
"sha256:93df8fc9d1821c9dabff9fed52181a9ad6eea5e9989d53102c391607d7c1666e",
|
||||
"sha256:cceed2121b7a93024daa93c91fae33007f8346f0e522796421f36a6183abea99"
|
||||
],
|
||||
"version": "==20.12.3"
|
||||
"version": "==21.1.1"
|
||||
},
|
||||
"automat": {
|
||||
"hashes": [
|
||||
|
@ -74,17 +74,18 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:1a282c1cd7d5028cbb3a75d747df32162295253f55d263ac85840e264830963b"
|
||||
"sha256:92041aa7589c886020cabd80eb58b89ace2f0094571792fccae24b9a8b3b97d7",
|
||||
"sha256:9f132c34e20110dea019293c89cede49b0a56be615b3e1debf98390ed9f1f7b9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.17.2"
|
||||
"version": "==1.17.3"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:7442fdbbdc841bfac7f94f92ecb807de070e32ed205743eb72d4ea27c5e8e778",
|
||||
"sha256:bf587b044983a91a0124cc133ff167b8528c19fbbc8f0b956d9a1ac256cad7d7"
|
||||
"sha256:1dae84c68b109f596f58cc2e9fa87704ccd40dcbc12144a89205f85efa7f9135",
|
||||
"sha256:a0fdded1c9636899ab273f50bf123f79b91439a8c282b5face8b5f4a48b493cb"
|
||||
],
|
||||
"version": "==1.20.2"
|
||||
"version": "==1.20.3"
|
||||
},
|
||||
"cachetools": {
|
||||
"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>
|
||||
<div slot="modal"></div>
|
||||
</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">
|
||||
{% trans 'Refresh' %}
|
||||
</button>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<span class="pf-c-form__label-text">{% trans 'Passing' %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="pf-c-form__group-label">
|
||||
<div class="c-form__horizontal-group">
|
||||
<span class="pf-c-form__label-text">{{ result.passing|yesno:"Yes,No" }}</span>
|
||||
</div>
|
||||
|
@ -26,7 +26,7 @@
|
|||
<span class="pf-c-form__label-text">{% trans 'Messages' %}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="pf-c-form__group-label">
|
||||
<div class="c-form__horizontal-group">
|
||||
<ul>
|
||||
{% 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,
|
||||
users,
|
||||
)
|
||||
from authentik.providers.saml.views import MetadataImportView
|
||||
from authentik.providers.saml.views.metadata import MetadataImportView
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
|
@ -113,7 +113,6 @@ urlpatterns = [
|
|||
name="policy-binding-delete",
|
||||
),
|
||||
# Providers
|
||||
path("providers/", providers.ProviderListView.as_view(), name="providers"),
|
||||
path(
|
||||
"providers/create/",
|
||||
providers.ProviderCreateView.as_view(),
|
||||
|
@ -296,6 +295,11 @@ urlpatterns = [
|
|||
certificate_key_pair.CertificateKeyPairCreateView.as_view(),
|
||||
name="certificatekeypair-create",
|
||||
),
|
||||
path(
|
||||
"crypto/certificates/generate/",
|
||||
certificate_key_pair.CertificateKeyPairGenerateView.as_view(),
|
||||
name="certificatekeypair-generate",
|
||||
),
|
||||
path(
|
||||
"crypto/certificates/<uuid:pk>/update/",
|
||||
certificate_key_pair.CertificateKeyPairUpdateView.as_view(),
|
||||
|
@ -329,22 +333,22 @@ urlpatterns = [
|
|||
),
|
||||
# Outpost Service Connections
|
||||
path(
|
||||
"outposts/service_connections/",
|
||||
"outpost_service_connections/",
|
||||
outposts_service_connections.OutpostServiceConnectionListView.as_view(),
|
||||
name="outpost-service-connections",
|
||||
),
|
||||
path(
|
||||
"outposts/service_connections/create/",
|
||||
"outpost_service_connections/create/",
|
||||
outposts_service_connections.OutpostServiceConnectionCreateView.as_view(),
|
||||
name="outpost-service-connection-create",
|
||||
),
|
||||
path(
|
||||
"outposts/service_connections/<uuid:pk>/update/",
|
||||
"outpost_service_connections/<uuid:pk>/update/",
|
||||
outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(),
|
||||
name="outpost-service-connection-update",
|
||||
),
|
||||
path(
|
||||
"outposts/service_connections/<uuid:pk>/delete/",
|
||||
"outpost_service_connections/<uuid:pk>/delete/",
|
||||
outposts_service_connections.OutpostServiceConnectionDeleteView.as_view(),
|
||||
name="outpost-service-connection-delete",
|
||||
),
|
||||
|
|
|
@ -4,9 +4,11 @@ from django.contrib.auth.mixins import (
|
|||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http.response import HttpResponse
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from django.views.generic.edit import FormView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from authentik.admin.views.utils import (
|
||||
|
@ -15,7 +17,11 @@ from authentik.admin.views.utils import (
|
|||
SearchListMixin,
|
||||
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.lib.views import CreateAssignPermView
|
||||
|
||||
|
@ -52,7 +58,35 @@ class CertificateKeyPairCreateView(
|
|||
|
||||
template_name = "generic/create.html"
|
||||
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(
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.contrib.messages.views import SuccessMessageMixin
|
|||
from django.core.cache import cache
|
||||
from django.http.request import HttpRequest
|
||||
from django.http.response import HttpResponse
|
||||
from django.urls.base import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import FormView
|
||||
from structlog.stdlib import get_logger
|
||||
|
@ -20,7 +21,7 @@ class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
|
|||
form_class = PolicyCacheClearForm
|
||||
|
||||
template_name = "generic/form_non_model.html"
|
||||
success_url = "/"
|
||||
success_url = reverse_lazy("authentik_core:shell")
|
||||
success_message = _("Successfully cleared Policy cache")
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
|
@ -39,7 +40,7 @@ class FlowCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
|
|||
form_class = FlowCacheClearForm
|
||||
|
||||
template_name = "generic/form_non_model.html"
|
||||
success_url = "/"
|
||||
success_url = reverse_lazy("authentik_core:shell")
|
||||
success_message = _("Successfully cleared Flow cache")
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
|
|
|
@ -115,6 +115,7 @@ class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, Fo
|
|||
user = form.cleaned_data.get("user")
|
||||
|
||||
p_request = PolicyRequest(user)
|
||||
p_request.debug = True
|
||||
p_request.http_request = self.request
|
||||
p_request.context = form.cleaned_data.get("context", {})
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ class PropertyMappingCreateView(
|
|||
permission_required = "authentik_core.add_propertymapping"
|
||||
|
||||
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")
|
||||
|
||||
|
||||
|
@ -54,7 +54,7 @@ class PropertyMappingUpdateView(
|
|||
permission_required = "authentik_core.change_propertymapping"
|
||||
|
||||
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")
|
||||
|
||||
|
||||
|
@ -67,7 +67,7 @@ class PropertyMappingDeleteView(
|
|||
permission_required = "authentik_core.delete_propertymapping"
|
||||
|
||||
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")
|
||||
|
||||
|
||||
|
|
|
@ -6,36 +6,17 @@ from django.contrib.auth.mixins import (
|
|||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
from guardian.mixins import PermissionRequiredMixin
|
||||
|
||||
from authentik.admin.views.utils import (
|
||||
BackSuccessUrlMixin,
|
||||
DeleteMessageView,
|
||||
InheritanceCreateView,
|
||||
InheritanceListView,
|
||||
InheritanceUpdateView,
|
||||
SearchListMixin,
|
||||
UserPaginateListMixin,
|
||||
)
|
||||
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(
|
||||
SuccessMessageMixin,
|
||||
BackSuccessUrlMixin,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""Create self-signed certificates"""
|
||||
import datetime
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from cryptography import x509
|
||||
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.x509.oid import NameOID
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
|
||||
|
||||
class CertificateBuilder:
|
||||
"""Build self-signed certificates"""
|
||||
|
@ -17,19 +21,39 @@ class CertificateBuilder:
|
|||
__builder = None
|
||||
__certificate = None
|
||||
|
||||
common_name: str
|
||||
|
||||
def __init__(self):
|
||||
self.__public_key = None
|
||||
self.__private_key = None
|
||||
self.__builder = 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"""
|
||||
one_day = datetime.timedelta(1, 0, 0)
|
||||
self.__private_key = rsa.generate_private_key(
|
||||
public_exponent=65537, key_size=2048, backend=default_backend()
|
||||
)
|
||||
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 = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(
|
||||
|
@ -37,7 +61,7 @@ class CertificateBuilder:
|
|||
[
|
||||
x509.NameAttribute(
|
||||
NameOID.COMMON_NAME,
|
||||
"authentik Self-signed Certificate",
|
||||
self.common_name,
|
||||
),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "authentik"),
|
||||
x509.NameAttribute(
|
||||
|
@ -51,13 +75,16 @@ class CertificateBuilder:
|
|||
[
|
||||
x509.NameAttribute(
|
||||
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_after(datetime.datetime.today() + datetime.timedelta(days=365))
|
||||
.not_valid_after(
|
||||
datetime.datetime.today() + datetime.timedelta(days=validity_days)
|
||||
)
|
||||
.serial_number(int(uuid.uuid4()))
|
||||
.public_key(self.__public_key)
|
||||
)
|
||||
|
|
|
@ -8,6 +8,14 @@ from django.utils.translation import gettext_lazy as _
|
|||
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):
|
||||
"""CertificateKeyPair Form"""
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
|
|||
# Create the notification objects
|
||||
for transport in trigger.transports.all():
|
||||
for user in trigger.group.users.all():
|
||||
LOGGER.debug("created notif")
|
||||
LOGGER.debug("created notification")
|
||||
notification = Notification.objects.create(
|
||||
severity=trigger.severity, body=event.summary, event=event, user=user
|
||||
)
|
||||
|
|
|
@ -80,7 +80,7 @@ class PolicyProcess(PROCESS_CLASS):
|
|||
)
|
||||
try:
|
||||
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(
|
||||
EventAction.POLICY_EXECUTION,
|
||||
message="Policy Execution",
|
||||
|
@ -94,8 +94,9 @@ class PolicyProcess(PROCESS_CLASS):
|
|||
+ "".join(format_tb(src_exc.__traceback__))
|
||||
+ str(src_exc)
|
||||
)
|
||||
# Create policy exception event
|
||||
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)
|
||||
# Create policy exception event, only when we're not debugging
|
||||
if not self.request.debug:
|
||||
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)
|
||||
LOGGER.debug("P_ENG(proc): error", exc=src_exc)
|
||||
policy_result = PolicyResult(False, str(src_exc))
|
||||
policy_result.source_policy = self.binding.policy
|
||||
|
|
|
@ -20,6 +20,7 @@ class PolicyRequest:
|
|||
http_request: Optional[HttpRequest]
|
||||
obj: Optional[Model]
|
||||
context: dict[str, Any]
|
||||
debug: bool = False
|
||||
|
||||
def __init__(self, user: User):
|
||||
super().__init__()
|
||||
|
|
|
@ -11,7 +11,7 @@ from rest_framework.viewsets import ModelViewSet
|
|||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.views import DescriptorDownloadView
|
||||
from authentik.providers.saml.views.metadata import DescriptorDownloadView
|
||||
|
||||
|
||||
class SAMLProviderSerializer(ProviderSerializer):
|
||||
|
|
|
@ -19,6 +19,7 @@ class SAMLProviderManager(ObjectManager):
|
|||
name="authentik default SAML Mapping: UPN",
|
||||
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn",
|
||||
expression="return user.attributes.get('upn', user.email)",
|
||||
friendly_name="",
|
||||
),
|
||||
EnsureExists(
|
||||
SAMLPropertyMapping,
|
||||
|
@ -26,6 +27,7 @@ class SAMLProviderManager(ObjectManager):
|
|||
name="authentik default SAML Mapping: Name",
|
||||
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
|
||||
expression="return user.name",
|
||||
friendly_name="",
|
||||
),
|
||||
EnsureExists(
|
||||
SAMLPropertyMapping,
|
||||
|
@ -33,6 +35,7 @@ class SAMLProviderManager(ObjectManager):
|
|||
name="authentik default SAML Mapping: Email",
|
||||
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||
expression="return user.email",
|
||||
friendly_name="",
|
||||
),
|
||||
EnsureExists(
|
||||
SAMLPropertyMapping,
|
||||
|
@ -40,6 +43,7 @@ class SAMLProviderManager(ObjectManager):
|
|||
name="authentik default SAML Mapping: Username",
|
||||
saml_name="http://schemas.goauthentik.io/2021/02/saml/username",
|
||||
expression="return user.username",
|
||||
friendly_name="",
|
||||
),
|
||||
EnsureExists(
|
||||
SAMLPropertyMapping,
|
||||
|
@ -47,6 +51,7 @@ class SAMLProviderManager(ObjectManager):
|
|||
name="authentik default SAML Mapping: User ID",
|
||||
saml_name="http://schemas.goauthentik.io/2021/02/saml/uid",
|
||||
expression="return user.pk",
|
||||
friendly_name="",
|
||||
),
|
||||
EnsureExists(
|
||||
SAMLPropertyMapping,
|
||||
|
@ -54,6 +59,7 @@ class SAMLProviderManager(ObjectManager):
|
|||
name="authentik default SAML Mapping: Groups",
|
||||
saml_name="http://schemas.xmlsoap.org/claims/Group",
|
||||
expression=GROUP_EXPRESSION,
|
||||
friendly_name="",
|
||||
),
|
||||
EnsureExists(
|
||||
SAMLPropertyMapping,
|
||||
|
@ -63,5 +69,6 @@ class SAMLProviderManager(ObjectManager):
|
|||
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
||||
),
|
||||
expression="return user.username",
|
||||
friendly_name="",
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
"""authentik SAML IDP URLs"""
|
||||
from django.urls import path
|
||||
|
||||
from authentik.providers.saml import views
|
||||
from authentik.providers.saml.views import metadata, sso
|
||||
|
||||
urlpatterns = [
|
||||
# SSO Bindings
|
||||
path(
|
||||
"<slug:application_slug>/sso/binding/redirect/",
|
||||
views.SAMLSSOBindingRedirectView.as_view(),
|
||||
sso.SAMLSSOBindingRedirectView.as_view(),
|
||||
name="sso-redirect",
|
||||
),
|
||||
path(
|
||||
"<slug:application_slug>/sso/binding/post/",
|
||||
views.SAMLSSOBindingPOSTView.as_view(),
|
||||
sso.SAMLSSOBindingPOSTView.as_view(),
|
||||
name="sso-post",
|
||||
),
|
||||
# SSO IdP Initiated
|
||||
path(
|
||||
"<slug:application_slug>/sso/binding/init/",
|
||||
views.SAMLSSOBindingInitView.as_view(),
|
||||
sso.SAMLSSOBindingInitView.as_view(),
|
||||
name="sso-init",
|
||||
),
|
||||
path(
|
||||
"<slug:application_slug>/metadata/",
|
||||
views.DescriptorDownloadView.as_view(),
|
||||
metadata.DescriptorDownloadView.as_view(),
|
||||
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)
|
||||
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()
|
||||
if scope["type"] == "lifespan":
|
||||
# https://code.djangoproject.com/ticket/31508
|
||||
|
@ -129,7 +123,7 @@ class ASGILogger:
|
|||
method=self.scope.get("method", ""),
|
||||
scheme=self.scope.get("scheme", ""),
|
||||
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,
|
||||
)
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
from base64 import b64encode
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import connections
|
||||
from django.db.utils import OperationalError
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.views import View
|
||||
from django_prometheus.exports import ExportToDjangoView
|
||||
|
@ -23,3 +25,22 @@ class MetricsView(View):
|
|||
return response
|
||||
|
||||
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.lib.utils.reflection import get_apps
|
||||
from authentik.root.monitoring import MetricsView
|
||||
from authentik.root.monitoring import LiveView, MetricsView, ReadyView
|
||||
|
||||
LOGGER = get_logger()
|
||||
admin.autodiscover()
|
||||
|
@ -57,6 +57,8 @@ for _authentik_app in get_apps():
|
|||
urlpatterns += [
|
||||
path("administration/django/", admin.site.urls),
|
||||
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"),
|
||||
]
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ services:
|
|||
traefik.http.routers.app-router.rule: PathPrefix(`/`)
|
||||
traefik.http.routers.app-router.service: app-service
|
||||
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'
|
||||
env_file:
|
||||
- .env
|
||||
|
|
|
@ -97,18 +97,12 @@ spec:
|
|||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
path: /-/health/live/
|
||||
port: http
|
||||
httpHeaders:
|
||||
- name: Host
|
||||
value: authentik-healthcheck-host
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
path: /-/health/ready/
|
||||
port: http
|
||||
httpHeaders:
|
||||
- name: Host
|
||||
value: authentik-healthcheck-host
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
|
|
|
@ -77,7 +77,15 @@ func (ac *APIController) startWSHandler() {
|
|||
logger.WithField("wait", notConnectedWait).Info("Not connected, trying again...")
|
||||
time.Sleep(notConnectedWait)
|
||||
notConnectedBackoff += notConnectedBackoff
|
||||
// Limit backoff to max 60 seconds
|
||||
if notConnectedBackoff >= 60 {
|
||||
notConnectedBackoff = 60
|
||||
}
|
||||
ac.wsConn.CloseAndReconnect()
|
||||
continue
|
||||
} else {
|
||||
// When we're connected, reset backoff to 1
|
||||
notConnectedBackoff = 1
|
||||
}
|
||||
var wsMsg websocketMessage
|
||||
err := ac.wsConn.ReadJSON(&wsMsg)
|
||||
|
@ -109,7 +117,7 @@ func (ac *APIController) startWSHealth() {
|
|||
},
|
||||
}
|
||||
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 {
|
||||
ac.logger.WithField("loop", "ws-health").Println("write:", err)
|
||||
ac.wsConn.CloseAndReconnect()
|
||||
|
|
|
@ -1537,9 +1537,9 @@
|
|||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz",
|
||||
"integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"optional": true
|
||||
},
|
||||
"functional-red-black-tree": {
|
||||
|
@ -2638,9 +2638,9 @@
|
|||
}
|
||||
},
|
||||
"rollup": {
|
||||
"version": "2.38.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.38.4.tgz",
|
||||
"integrity": "sha512-B0LcJhjiwKkTl79aGVF/u5KdzsH8IylVfV56Ut6c9ouWLJcUK17T83aZBetNYSnZtXf2OHD4+2PbmRW+Fp5ulg==",
|
||||
"version": "2.38.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.38.5.tgz",
|
||||
"integrity": "sha512-VoWt8DysFGDVRGWuHTqZzT02J0ASgjVq/hPs9QcBOGMd7B+jfTr/iqMVEyOi901rE3xq+Deq66GzIT1yt7sGwQ==",
|
||||
"requires": {
|
||||
"fsevents": "~2.3.1"
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
"flowchart.js": "^1.15.0",
|
||||
"lit-element": "^2.4.0",
|
||||
"lit-html": "^1.3.0",
|
||||
"rollup": "^2.38.4",
|
||||
"rollup": "^2.38.5",
|
||||
"rollup-plugin-copy": "^3.3.0",
|
||||
"rollup-plugin-cssimport": "^1.0.2",
|
||||
"rollup-plugin-external-globals": "^0.6.1",
|
||||
|
|
|
@ -28,7 +28,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
|
|||
),
|
||||
new SidebarItem("Providers", "/providers"),
|
||||
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> => {
|
||||
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-top-applications-table></ak-top-applications-table>
|
||||
</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-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>
|
||||
|
|
|
@ -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",
|
||||
"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/",
|
||||
"attr_last_name": "User.LastName",
|
||||
"entity_id": "https://awx.company/sso/metadata/saml/",
|
||||
"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"
|
||||
|
|
|
@ -15,17 +15,39 @@ Rancher is a platform built to address the needs of the DevOps teams deploying a
|
|||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `rancher.company` is the FQDN of the Rancher install.
|
||||
- `authentik.company` is the FQDN of the authentik install.
|
||||
- `rancher.company` is the FQDN of the Rancher 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
|
||||
|
||||
- ACS URL: `https://rancher.company/v1-saml/adfs/saml/acs`
|
||||
- Audience: `https://rancher.company/v1-saml/adfs/saml/metadata`
|
||||
- Issuer: `authentik`
|
||||
```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`
|
||||
- Audience: `https://rancher.company/v1-saml/adfs/saml/metadata`
|
||||
- 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.
|
||||
|
||||
## 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)
|
||||
|
|
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
|
||||
|
||||
IdP User ID: `http://schemas.goauthentik.io/2021/02/saml/uid`
|
||||
User Email: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`
|
||||
First Name: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name`
|
||||
- IdP User ID: `http://schemas.goauthentik.io/2021/02/saml/uid`
|
||||
- User Email: `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`
|
||||
- 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.
|
||||
|
|
|
@ -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 integrations affected are:
|
||||
- [NextCloud](../integrations/services/nextcloud/index)
|
||||
- [Sentry](../integrations/services/sentry/index)
|
||||
- [GitLab](../integrations/services/gitlab/index)
|
||||
- [Ansible Tower/AWX](../integrations/services/awx-tower/index)
|
||||
- [Ansible Tower/AWX](/docs/integrations/services/awx-tower/index)
|
||||
- [GitLab](/docs/integrations/services/gitlab/index)
|
||||
- [NextCloud](/docs/integrations/services/nextcloud/index)
|
||||
- [Rancher](/docs/integrations/services/rancher/index)
|
||||
- [Sentry](/docs/integrations/services/sentry/index)
|
||||
|
||||
### 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",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"watch": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"serve": "docusaurus serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "2.0.0-alpha.66",
|
||||
"@docusaurus/preset-classic": "2.0.0-alpha.66",
|
||||
"@mdx-js/react": "^1.5.8",
|
||||
"@docusaurus/core": "2.0.0-alpha.70",
|
||||
"@docusaurus/preset-classic": "2.0.0-alpha.70",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.1.1",
|
||||
"react": "^16.8.4",
|
||||
"react-dom": "^16.8.4"
|
||||
|
@ -31,6 +31,6 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "2.1.2"
|
||||
"prettier": "2.2.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -141,6 +141,7 @@ module.exports = {
|
|||
"releases/0.13",
|
||||
"releases/0.14",
|
||||
"releases/2021.1",
|
||||
"releases/2021.2",
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
Reference in New Issue