Merge branch 'master' into version-2021.2

This commit is contained in:
Jens Langhammer 2021-02-07 19:04:37 +01:00
commit d93927755a
43 changed files with 3333 additions and 1126 deletions

View File

@ -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:

17
Pipfile.lock generated
View File

@ -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": [

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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",
),

View File

@ -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(

View File

@ -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:

View File

@ -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", {})

View File

@ -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")

View File

@ -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,

View File

@ -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)
)

View File

@ -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"""

View File

@ -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
)

View File

@ -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

View File

@ -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__()

View File

@ -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):

View File

@ -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="",
),
]

View File

@ -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",
),
]

View File

@ -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)

View File

@ -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")

View File

@ -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)

View File

@ -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

View File

@ -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,
)

View File

@ -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)

View File

@ -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"),
]

View File

@ -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

View File

@ -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

View File

@ -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()

12
web/package-lock.json generated
View File

@ -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"
}

View File

@ -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",

View File

@ -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);
}),

View File

@ -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>

View File

@ -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"

View File

@ -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

View File

@ -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.

View File

@ -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

3336
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -141,6 +141,7 @@ module.exports = {
"releases/0.13",
"releases/0.14",
"releases/2021.1",
"releases/2021.2",
],
},
{