wip: rename to authentik (#361)
* root: initial rename * web: rename custom element prefix * root: rename external functions with pb_ prefix * root: fix formatting * root: replace domain with goauthentik.io * proxy: update path * root: rename remaining prefixes * flows: rename file extension * root: pbadmin -> akadmin * docs: fix image filenames * lifecycle: ignore migration files * ci: copy default config from current source before loading last tagged * *: new sentry dsn * tests: fix missing python3.9-dev package * root: add additional migrations for service accounts created by outposts * core: mark system-created service accounts with attribute * policies/expression: fix pb_ replacement not working * web: fix last linting errors, add lit-analyse * policies/expressions: fix lint errors * web: fix sidebar display on screens where not all items fit * proxy: attempt to fix proxy pipeline * proxy: use go env GOPATH to get gopath * lib: fix user_default naming inconsistency * docs: add upgrade docs * docs: update screenshots to use authentik * admin: fix create button on empty-state of outpost * web: fix modal submit not refreshing SiteShell and Table * web: fix height of app-card and height of generic icon * web: fix rendering of subtext * admin: fix version check error not being caught * web: fix worker count not being shown * docs: update screenshots * root: new icon * web: fix lint error * admin: fix linting error * root: migrate coverage config to pyproject
This commit is contained in:
0
authentik/core/__init__.py
Normal file
0
authentik/core/__init__.py
Normal file
24
authentik/core/admin.py
Normal file
24
authentik/core/admin.py
Normal file
@ -0,0 +1,24 @@
|
||||
"""authentik core admin"""
|
||||
|
||||
from django.apps import AppConfig, apps
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.sites import AlreadyRegistered
|
||||
from guardian.admin import GuardedModelAdmin
|
||||
from structlog import get_logger
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def admin_autoregister(app: AppConfig):
|
||||
"""Automatically register all models from app"""
|
||||
for model in app.get_models():
|
||||
try:
|
||||
admin.site.register(model, GuardedModelAdmin)
|
||||
except AlreadyRegistered:
|
||||
pass
|
||||
|
||||
|
||||
for _app in apps.get_app_configs():
|
||||
if _app.label.startswith("authentik_"):
|
||||
LOGGER.debug("Registering application for dj-admin", application=_app.label)
|
||||
admin_autoregister(_app)
|
||||
0
authentik/core/api/__init__.py
Normal file
0
authentik/core/api/__init__.py
Normal file
81
authentik/core/api/applications.py
Normal file
81
authentik/core/api/applications.py
Normal file
@ -0,0 +1,81 @@
|
||||
"""Application API Views"""
|
||||
from django.db.models import QuerySet
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||
|
||||
from authentik.admin.api.overview_metrics import get_events_per_1h
|
||||
from authentik.audit.models import EventAction
|
||||
from authentik.core.models import Application
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
|
||||
|
||||
class ApplicationSerializer(ModelSerializer):
|
||||
"""Application Serializer"""
|
||||
|
||||
launch_url = SerializerMethodField()
|
||||
|
||||
def get_launch_url(self, instance: Application) -> str:
|
||||
"""Get generated launch URL"""
|
||||
return instance.get_launch_url() or ""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Application
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"slug",
|
||||
"provider",
|
||||
"launch_url",
|
||||
"meta_launch_url",
|
||||
"meta_icon",
|
||||
"meta_description",
|
||||
"meta_publisher",
|
||||
"policies",
|
||||
]
|
||||
|
||||
|
||||
class ApplicationViewSet(ModelViewSet):
|
||||
"""Application Viewset"""
|
||||
|
||||
queryset = Application.objects.all()
|
||||
serializer_class = ApplicationSerializer
|
||||
lookup_field = "slug"
|
||||
|
||||
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
||||
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
|
||||
for backend in list(self.filter_backends):
|
||||
if backend == ObjectPermissionsFilter:
|
||||
continue
|
||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
||||
return queryset
|
||||
|
||||
def list(self, request: Request) -> Response:
|
||||
"""Custom list method that checks Policy based access instead of guardian"""
|
||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||
self.paginate_queryset(queryset)
|
||||
allowed_applications = []
|
||||
for application in queryset.order_by("name"):
|
||||
engine = PolicyEngine(application, self.request.user, self.request)
|
||||
engine.build()
|
||||
if engine.passing:
|
||||
allowed_applications.append(application)
|
||||
serializer = self.get_serializer(allowed_applications, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
@action(detail=True)
|
||||
def metrics(self, request: Request, slug: str):
|
||||
"""Metrics for application logins"""
|
||||
# TODO: Check app read and audit read perms
|
||||
app = Application.objects.get(slug=slug)
|
||||
return Response(
|
||||
get_events_per_1h(
|
||||
action=EventAction.AUTHORIZE_APPLICATION,
|
||||
context__authorized_application__pk=app.pk.hex,
|
||||
)
|
||||
)
|
||||
21
authentik/core/api/groups.py
Normal file
21
authentik/core/api/groups.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""Groups API Viewset"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.models import Group
|
||||
|
||||
|
||||
class GroupSerializer(ModelSerializer):
|
||||
"""Group Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Group
|
||||
fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"]
|
||||
|
||||
|
||||
class GroupViewSet(ModelViewSet):
|
||||
"""Group Viewset"""
|
||||
|
||||
queryset = Group.objects.all()
|
||||
serializer_class = GroupSerializer
|
||||
30
authentik/core/api/propertymappings.py
Normal file
30
authentik/core/api/propertymappings.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""PropertyMapping API Views"""
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from authentik.core.models import PropertyMapping
|
||||
|
||||
|
||||
class PropertyMappingSerializer(ModelSerializer):
|
||||
"""PropertyMapping Serializer"""
|
||||
|
||||
__type__ = SerializerMethodField(method_name="get_type")
|
||||
|
||||
def get_type(self, obj):
|
||||
"""Get object type so that we know which API Endpoint to use to get the full object"""
|
||||
return obj._meta.object_name.lower().replace("propertymapping", "")
|
||||
|
||||
class Meta:
|
||||
|
||||
model = PropertyMapping
|
||||
fields = ["pk", "name", "expression", "__type__"]
|
||||
|
||||
|
||||
class PropertyMappingViewSet(ReadOnlyModelViewSet):
|
||||
"""PropertyMapping Viewset"""
|
||||
|
||||
queryset = PropertyMapping.objects.all()
|
||||
serializer_class = PropertyMappingSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return PropertyMapping.objects.select_subclasses()
|
||||
30
authentik/core/api/providers.py
Normal file
30
authentik/core/api/providers.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""Provider API Views"""
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from authentik.core.models import Provider
|
||||
|
||||
|
||||
class ProviderSerializer(ModelSerializer):
|
||||
"""Provider Serializer"""
|
||||
|
||||
__type__ = SerializerMethodField(method_name="get_type")
|
||||
|
||||
def get_type(self, obj):
|
||||
"""Get object type so that we know which API Endpoint to use to get the full object"""
|
||||
return obj._meta.object_name.lower().replace("provider", "")
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Provider
|
||||
fields = ["pk", "name", "authorization_flow", "property_mappings", "__type__"]
|
||||
|
||||
|
||||
class ProviderViewSet(ReadOnlyModelViewSet):
|
||||
"""Provider Viewset"""
|
||||
|
||||
queryset = Provider.objects.all()
|
||||
serializer_class = ProviderSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return Provider.objects.select_subclasses()
|
||||
31
authentik/core/api/sources.py
Normal file
31
authentik/core/api/sources.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""Source API Views"""
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from authentik.admin.forms.source import SOURCE_SERIALIZER_FIELDS
|
||||
from authentik.core.models import Source
|
||||
|
||||
|
||||
class SourceSerializer(ModelSerializer):
|
||||
"""Source Serializer"""
|
||||
|
||||
__type__ = SerializerMethodField(method_name="get_type")
|
||||
|
||||
def get_type(self, obj):
|
||||
"""Get object type so that we know which API Endpoint to use to get the full object"""
|
||||
return obj._meta.object_name.lower().replace("source", "")
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Source
|
||||
fields = SOURCE_SERIALIZER_FIELDS + ["__type__"]
|
||||
|
||||
|
||||
class SourceViewSet(ReadOnlyModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = Source.objects.all()
|
||||
serializer_class = SourceSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return Source.objects.select_subclasses()
|
||||
37
authentik/core/api/tokens.py
Normal file
37
authentik/core/api/tokens.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""Tokens API Viewset"""
|
||||
from django.http.response import Http404
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.audit.models import Event, EventAction
|
||||
from authentik.core.models import Token
|
||||
|
||||
|
||||
class TokenSerializer(ModelSerializer):
|
||||
"""Token Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Token
|
||||
fields = ["pk", "identifier", "intent", "user", "description"]
|
||||
|
||||
|
||||
class TokenViewSet(ModelViewSet):
|
||||
"""Token Viewset"""
|
||||
|
||||
lookup_field = "identifier"
|
||||
queryset = Token.filter_not_expired()
|
||||
serializer_class = TokenSerializer
|
||||
|
||||
@action(detail=True)
|
||||
def view_key(self, request: Request, identifier: str) -> Response:
|
||||
"""Return token key and log access"""
|
||||
tokens = Token.filter_not_expired(identifier=identifier)
|
||||
if not tokens.exists():
|
||||
raise Http404
|
||||
token = tokens.first()
|
||||
Event.new(EventAction.TOKEN_VIEW, token=token).from_http(request)
|
||||
return Response({"key": token.key})
|
||||
44
authentik/core/api/users.py
Normal file
44
authentik/core/api/users.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""User API Views"""
|
||||
from drf_yasg2.utils import swagger_auto_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import (
|
||||
BooleanField,
|
||||
ModelSerializer,
|
||||
SerializerMethodField,
|
||||
)
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.templatetags.authentik_utils import avatar
|
||||
|
||||
|
||||
class UserSerializer(ModelSerializer):
|
||||
"""User Serializer"""
|
||||
|
||||
is_superuser = BooleanField(read_only=True)
|
||||
avatar = SerializerMethodField()
|
||||
|
||||
def get_avatar(self, user: User) -> str:
|
||||
"""Add user's avatar as URL"""
|
||||
return avatar(user)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = User
|
||||
fields = ["pk", "username", "name", "is_superuser", "email", "avatar"]
|
||||
|
||||
|
||||
class UserViewSet(ModelViewSet):
|
||||
"""User Viewset"""
|
||||
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
|
||||
@swagger_auto_schema(responses={200: UserSerializer(many=False)})
|
||||
@action(detail=False)
|
||||
# pylint: disable=invalid-name
|
||||
def me(self, request: Request) -> Response:
|
||||
"""Get information about current user"""
|
||||
return Response(UserSerializer(request.user).data)
|
||||
11
authentik/core/apps.py
Normal file
11
authentik/core/apps.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""authentik core app config"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthentikCoreConfig(AppConfig):
|
||||
"""authentik core app config"""
|
||||
|
||||
name = "authentik.core"
|
||||
label = "authentik_core"
|
||||
verbose_name = "authentik Core"
|
||||
mountpoint = ""
|
||||
32
authentik/core/channels.py
Normal file
32
authentik/core/channels.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""Channels base classes"""
|
||||
from channels.generic.websocket import JsonWebsocketConsumer
|
||||
from structlog import get_logger
|
||||
|
||||
from authentik.api.auth import token_from_header
|
||||
from authentik.core.models import User
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class AuthJsonConsumer(JsonWebsocketConsumer):
|
||||
"""Authorize a client with a token"""
|
||||
|
||||
user: User
|
||||
|
||||
def connect(self):
|
||||
headers = dict(self.scope["headers"])
|
||||
if b"authorization" not in headers:
|
||||
LOGGER.warning("WS Request without authorization header")
|
||||
self.close()
|
||||
return False
|
||||
|
||||
raw_header = headers[b"authorization"]
|
||||
|
||||
token = token_from_header(raw_header)
|
||||
if not token:
|
||||
LOGGER.warning("Failed to authenticate")
|
||||
self.close()
|
||||
return False
|
||||
|
||||
self.user = token.user
|
||||
return True
|
||||
6
authentik/core/exceptions.py
Normal file
6
authentik/core/exceptions.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""authentik core exceptions"""
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
|
||||
|
||||
class PropertyMappingExpressionException(SentryIgnoredException):
|
||||
"""Error when a PropertyMapping Exception expression could not be parsed or evaluated."""
|
||||
21
authentik/core/expression.py
Normal file
21
authentik/core/expression.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""Property Mapping Evaluator"""
|
||||
from typing import Optional
|
||||
|
||||
from django.http import HttpRequest
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||
|
||||
|
||||
class PropertyMappingEvaluator(BaseEvaluator):
|
||||
"""Custom Evalautor that adds some different context variables."""
|
||||
|
||||
def set_context(
|
||||
self, user: Optional[User], request: Optional[HttpRequest], **kwargs
|
||||
):
|
||||
"""Update context with context from PropertyMapping's evaluate"""
|
||||
if user:
|
||||
self._context["user"] = user
|
||||
if request:
|
||||
self._context["request"] = request
|
||||
self._context.update(**kwargs)
|
||||
0
authentik/core/forms/__init__.py
Normal file
0
authentik/core/forms/__init__.py
Normal file
50
authentik/core/forms/applications.py
Normal file
50
authentik/core/forms/applications.py
Normal file
@ -0,0 +1,50 @@
|
||||
"""authentik Core Application forms"""
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.core.models import Application, Provider
|
||||
from authentik.lib.widgets import GroupedModelChoiceField
|
||||
|
||||
|
||||
class ApplicationForm(forms.ModelForm):
|
||||
"""Application Form"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["provider"].queryset = (
|
||||
Provider.objects.all().order_by("pk").select_subclasses()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Application
|
||||
fields = [
|
||||
"name",
|
||||
"slug",
|
||||
"provider",
|
||||
"meta_launch_url",
|
||||
"meta_icon",
|
||||
"meta_description",
|
||||
"meta_publisher",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"meta_launch_url": forms.TextInput(),
|
||||
"meta_publisher": forms.TextInput(),
|
||||
"meta_icon": forms.FileInput(),
|
||||
}
|
||||
help_texts = {
|
||||
"meta_launch_url": _(
|
||||
(
|
||||
"If left empty, authentik will try to extract the launch URL "
|
||||
"based on the selected provider."
|
||||
)
|
||||
),
|
||||
}
|
||||
field_classes = {"provider": GroupedModelChoiceField}
|
||||
labels = {
|
||||
"meta_launch_url": _("Launch URL"),
|
||||
"meta_icon": _("Icon"),
|
||||
"meta_description": _("Description"),
|
||||
"meta_publisher": _("Publisher"),
|
||||
}
|
||||
38
authentik/core/forms/groups.py
Normal file
38
authentik/core/forms/groups.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""authentik Core Group forms"""
|
||||
from django import forms
|
||||
|
||||
from authentik.admin.fields import CodeMirrorWidget, YAMLField
|
||||
from authentik.core.models import Group, User
|
||||
|
||||
|
||||
class GroupForm(forms.ModelForm):
|
||||
"""Group Form"""
|
||||
|
||||
members = forms.ModelMultipleChoiceField(
|
||||
User.objects.all(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance.pk:
|
||||
self.initial["members"] = self.instance.users.values_list("pk", flat=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
instance = super().save(*args, **kwargs)
|
||||
if instance.pk:
|
||||
instance.users.clear()
|
||||
instance.users.add(*self.cleaned_data["members"])
|
||||
return instance
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Group
|
||||
fields = ["name", "is_superuser", "parent", "members", "attributes"]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"attributes": CodeMirrorWidget,
|
||||
}
|
||||
field_classes = {
|
||||
"attributes": YAMLField,
|
||||
}
|
||||
22
authentik/core/forms/token.py
Normal file
22
authentik/core/forms/token.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""Core user token form"""
|
||||
from django import forms
|
||||
|
||||
from authentik.core.models import Token
|
||||
|
||||
|
||||
class UserTokenForm(forms.ModelForm):
|
||||
"""Token form, for tokens created by endusers"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Token
|
||||
fields = [
|
||||
"identifier",
|
||||
"expires",
|
||||
"expiring",
|
||||
"description",
|
||||
]
|
||||
widgets = {
|
||||
"identifier": forms.TextInput(),
|
||||
"description": forms.TextInput(),
|
||||
}
|
||||
15
authentik/core/forms/users.py
Normal file
15
authentik/core/forms/users.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""authentik core user forms"""
|
||||
|
||||
from django import forms
|
||||
|
||||
from authentik.core.models import User
|
||||
|
||||
|
||||
class UserDetailForm(forms.ModelForm):
|
||||
"""Update User Details"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = User
|
||||
fields = ["username", "name", "email"]
|
||||
widgets = {"name": forms.TextInput}
|
||||
56
authentik/core/middleware.py
Normal file
56
authentik/core/middleware.py
Normal file
@ -0,0 +1,56 @@
|
||||
"""authentik admin Middleware to impersonate users"""
|
||||
from logging import Logger
|
||||
from threading import local
|
||||
from typing import Callable
|
||||
from uuid import uuid4
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
SESSION_IMPERSONATE_USER = "authentik_impersonate_user"
|
||||
SESSION_IMPERSONATE_ORIGINAL_USER = "authentik_impersonate_original_user"
|
||||
LOCAL = local()
|
||||
|
||||
|
||||
class ImpersonateMiddleware:
|
||||
"""Middleware to impersonate users"""
|
||||
|
||||
get_response: Callable[[HttpRequest], HttpResponse]
|
||||
|
||||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||
# No permission checks are done here, they need to be checked before
|
||||
# SESSION_IMPERSONATE_USER is set.
|
||||
|
||||
if SESSION_IMPERSONATE_USER in request.session:
|
||||
request.user = request.session[SESSION_IMPERSONATE_USER]
|
||||
|
||||
return self.get_response(request)
|
||||
|
||||
|
||||
class RequestIDMiddleware:
|
||||
"""Add a unique ID to every request"""
|
||||
|
||||
get_response: Callable[[HttpRequest], HttpResponse]
|
||||
|
||||
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||
if not hasattr(request, "request_id"):
|
||||
request_id = uuid4().hex
|
||||
setattr(request, "request_id", request_id)
|
||||
LOCAL.authentik = {"request_id": request_id}
|
||||
response = self.get_response(request)
|
||||
response["X-authentik-id"] = request.request_id
|
||||
del LOCAL.authentik["request_id"]
|
||||
return response
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def structlog_add_request_id(logger: Logger, method_name: str, event_dict):
|
||||
"""If threadlocal has authentik defined, add request_id to log"""
|
||||
if hasattr(LOCAL, "authentik"):
|
||||
event_dict["request_id"] = LOCAL.authentik.get("request_id", "")
|
||||
return event_dict
|
||||
356
authentik/core/migrations/0001_initial.py
Normal file
356
authentik/core/migrations/0001_initial.py
Normal file
@ -0,0 +1,356 @@
|
||||
# Generated by Django 3.0.6 on 2020-05-19 22:07
|
||||
|
||||
import uuid
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import guardian.mixins
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import authentik.core.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_policies", "0001_initial"),
|
||||
("auth", "0011_update_proxy_permissions"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="User",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"username",
|
||||
models.CharField(
|
||||
error_messages={
|
||||
"unique": "A user with that username already exists."
|
||||
},
|
||||
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
|
||||
max_length=150,
|
||||
unique=True,
|
||||
validators=[
|
||||
django.contrib.auth.validators.UnicodeUsernameValidator()
|
||||
],
|
||||
verbose_name="username",
|
||||
),
|
||||
),
|
||||
(
|
||||
"first_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=30, verbose_name="first name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=150, verbose_name="last name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True, max_length=254, verbose_name="email address"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_staff",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates whether the user can log into this admin site.",
|
||||
verbose_name="staff status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
|
||||
verbose_name="active",
|
||||
),
|
||||
),
|
||||
(
|
||||
"date_joined",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="date joined"
|
||||
),
|
||||
),
|
||||
("uuid", models.UUIDField(default=uuid.uuid4, editable=False)),
|
||||
("name", models.TextField(help_text="User's display name.")),
|
||||
("password_change_date", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"attributes",
|
||||
models.JSONField(blank=True, default=dict),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"permissions": (("reset_user_password", "Reset Password"),),
|
||||
},
|
||||
bases=(guardian.mixins.GuardianUserMixin, models.Model),
|
||||
managers=[
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="PropertyMapping",
|
||||
fields=[
|
||||
(
|
||||
"pm_uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("name", models.TextField()),
|
||||
("expression", models.TextField()),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Property Mapping",
|
||||
"verbose_name_plural": "Property Mappings",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Source",
|
||||
fields=[
|
||||
(
|
||||
"policybindingmodel_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_policies.PolicyBindingModel",
|
||||
),
|
||||
),
|
||||
("name", models.TextField(help_text="Source's display Name.")),
|
||||
(
|
||||
"slug",
|
||||
models.SlugField(help_text="Internal source name, used in URLs."),
|
||||
),
|
||||
("enabled", models.BooleanField(default=True)),
|
||||
(
|
||||
"property_mappings",
|
||||
models.ManyToManyField(
|
||||
blank=True, default=None, to="authentik_core.PropertyMapping"
|
||||
),
|
||||
),
|
||||
],
|
||||
bases=("authentik_policies.policybindingmodel",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserSourceConnection",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
("last_updated", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"source",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_core.Source",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("user", "source")},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Token",
|
||||
fields=[
|
||||
(
|
||||
"token_uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
(
|
||||
"expires",
|
||||
models.DateTimeField(
|
||||
default=authentik.core.models.default_token_duration
|
||||
),
|
||||
),
|
||||
("expiring", models.BooleanField(default=True)),
|
||||
("description", models.TextField(blank=True, default="")),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Token",
|
||||
"verbose_name_plural": "Tokens",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Provider",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"property_mappings",
|
||||
models.ManyToManyField(
|
||||
blank=True, default=None, to="authentik_core.PropertyMapping"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Group",
|
||||
fields=[
|
||||
(
|
||||
"group_uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=80, verbose_name="name")),
|
||||
(
|
||||
"attributes",
|
||||
models.JSONField(blank=True, default=dict),
|
||||
),
|
||||
(
|
||||
"parent",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="children",
|
||||
to="authentik_core.Group",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"unique_together": {("name", "parent")},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Application",
|
||||
fields=[
|
||||
(
|
||||
"policybindingmodel_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_policies.PolicyBindingModel",
|
||||
),
|
||||
),
|
||||
("name", models.TextField(help_text="Application's display Name.")),
|
||||
(
|
||||
"slug",
|
||||
models.SlugField(
|
||||
help_text="Internal application name, used in URLs."
|
||||
),
|
||||
),
|
||||
("skip_authorization", models.BooleanField(default=False)),
|
||||
("meta_launch_url", models.URLField(blank=True, default="")),
|
||||
("meta_icon_url", models.TextField(blank=True, default="")),
|
||||
("meta_description", models.TextField(blank=True, default="")),
|
||||
("meta_publisher", models.TextField(blank=True, default="")),
|
||||
(
|
||||
"provider",
|
||||
models.OneToOneField(
|
||||
blank=True,
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="authentik_core.Provider",
|
||||
),
|
||||
),
|
||||
],
|
||||
bases=("authentik_policies.policybindingmodel",),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="groups",
|
||||
field=models.ManyToManyField(to="authentik_core.Group"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="sources",
|
||||
field=models.ManyToManyField(
|
||||
through="authentik_core.UserSourceConnection",
|
||||
to="authentik_core.Source",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="user_permissions",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
]
|
||||
55
authentik/core/migrations/0002_auto_20200523_1133.py
Normal file
55
authentik/core/migrations/0002_auto_20200523_1133.py
Normal file
@ -0,0 +1,55 @@
|
||||
# Generated by Django 3.0.6 on 2020-05-23 11:33
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0003_auto_20200523_1133"),
|
||||
("authentik_core", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="application",
|
||||
name="skip_authorization",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="source",
|
||||
name="authentication_flow",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Flow to use when authenticating existing users.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="source_authentication",
|
||||
to="authentik_flows.Flow",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="source",
|
||||
name="enrollment_flow",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Flow to use when enrolling new users.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="source_enrollment",
|
||||
to="authentik_flows.Flow",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="provider",
|
||||
name="authorization_flow",
|
||||
field=models.ForeignKey(
|
||||
help_text="Flow used when authorizing this provider.",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="provider_authorization",
|
||||
to="authentik_flows.Flow",
|
||||
),
|
||||
),
|
||||
]
|
||||
45
authentik/core/migrations/0003_default_user.py
Normal file
45
authentik/core/migrations/0003_default_user.py
Normal file
@ -0,0 +1,45 @@
|
||||
# Generated by Django 3.0.6 on 2020-05-23 16:40
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
# We have to use a direct import here, otherwise we get an object manager error
|
||||
from authentik.core.models import User
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
akadmin, _ = User.objects.using(db_alias).get_or_create(
|
||||
username="akadmin", email="root@localhost", name="authentik Default Admin"
|
||||
)
|
||||
akadmin.set_password("akadmin", signal=False) # noqa # nosec
|
||||
akadmin.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0002_auto_20200523_1133"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="is_superuser",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="is_staff",
|
||||
),
|
||||
migrations.RunPython(create_default_user),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="is_superuser",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user", name="is_staff", field=models.BooleanField(default=False)
|
||||
),
|
||||
]
|
||||
28
authentik/core/migrations/0004_auto_20200703_2213.py
Normal file
28
authentik/core/migrations/0004_auto_20200703_2213.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 3.0.7 on 2020-07-03 22:13
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0003_default_user"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="application",
|
||||
options={
|
||||
"verbose_name": "Application",
|
||||
"verbose_name_plural": "Applications",
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="user",
|
||||
options={
|
||||
"permissions": (("reset_user_password", "Reset Password"),),
|
||||
"verbose_name": "User",
|
||||
"verbose_name_plural": "Users",
|
||||
},
|
||||
),
|
||||
]
|
||||
24
authentik/core/migrations/0005_token_intent.py
Normal file
24
authentik/core/migrations/0005_token_intent.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.0.7 on 2020-07-05 21:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0004_auto_20200703_2213"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="token",
|
||||
name="intent",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("verification", "Intent Verification"),
|
||||
("api", "Intent Api"),
|
||||
],
|
||||
default="verification",
|
||||
),
|
||||
),
|
||||
]
|
||||
20
authentik/core/migrations/0006_auto_20200709_1608.py
Normal file
20
authentik/core/migrations/0006_auto_20200709_1608.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.0.8 on 2020-07-09 16:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0005_token_intent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="source",
|
||||
name="slug",
|
||||
field=models.SlugField(
|
||||
help_text="Internal source name, used in URLs.", unique=True
|
||||
),
|
||||
),
|
||||
]
|
||||
20
authentik/core/migrations/0007_auto_20200815_1841.py
Normal file
20
authentik/core/migrations/0007_auto_20200815_1841.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.1 on 2020-08-15 18:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0006_auto_20200709_1608"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="first_name",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=150, verbose_name="first name"
|
||||
),
|
||||
),
|
||||
]
|
||||
36
authentik/core/migrations/0008_auto_20200824_1532.py
Normal file
36
authentik/core/migrations/0008_auto_20200824_1532.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Generated by Django 3.1 on 2020-08-24 15:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
("authentik_core", "0007_auto_20200815_1841"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="groups",
|
||||
field=models.ManyToManyField(to="authentik_core.Group"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="groups",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="pb_groups",
|
||||
field=models.ManyToManyField(to="authentik_core.Group"),
|
||||
),
|
||||
]
|
||||
61
authentik/core/migrations/0009_group_is_superuser.py
Normal file
61
authentik/core/migrations/0009_group_is_superuser.py
Normal file
@ -0,0 +1,61 @@
|
||||
# Generated by Django 3.1.1 on 2020-09-15 19:53
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
import authentik.core.models
|
||||
|
||||
|
||||
def create_default_admin_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
Group = apps.get_model("authentik_core", "Group")
|
||||
User = apps.get_model("authentik_core", "User")
|
||||
|
||||
# Creates a default admin group
|
||||
group, _ = Group.objects.using(db_alias).get_or_create(
|
||||
is_superuser=True,
|
||||
defaults={
|
||||
"name": "authentik Admins",
|
||||
},
|
||||
)
|
||||
group.users.set(User.objects.filter(username="akadmin"))
|
||||
group.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0008_auto_20200824_1532"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="is_superuser",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="user",
|
||||
name="is_staff",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="pb_groups",
|
||||
field=models.ManyToManyField(
|
||||
related_name="users", to="authentik_core.Group"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="group",
|
||||
name="is_superuser",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Users added to this group will be superusers."
|
||||
),
|
||||
),
|
||||
migrations.RunPython(create_default_admin_group),
|
||||
migrations.AlterModelManagers(
|
||||
name="user",
|
||||
managers=[
|
||||
("objects", authentik.core.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
24
authentik/core/migrations/0010_auto_20200917_1021.py
Normal file
24
authentik/core/migrations/0010_auto_20200917_1021.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.1 on 2020-09-17 10:21
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0009_group_is_superuser"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="user",
|
||||
options={
|
||||
"permissions": (
|
||||
("reset_user_password", "Reset Password"),
|
||||
("impersonate", "Can impersonate other users"),
|
||||
),
|
||||
"verbose_name": "User",
|
||||
"verbose_name_plural": "Users",
|
||||
},
|
||||
),
|
||||
]
|
||||
19
authentik/core/migrations/0011_provider_name_temp.py
Normal file
19
authentik/core/migrations/0011_provider_name_temp.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.1.2 on 2020-10-03 17:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0010_auto_20200917_1021"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="provider",
|
||||
name="name_temp",
|
||||
field=models.TextField(default=""),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
20
authentik/core/migrations/0012_auto_20201003_1737.py
Normal file
20
authentik/core/migrations/0012_auto_20201003_1737.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.1.2 on 2020-10-03 17:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0011_provider_name_temp"),
|
||||
("authentik_providers_oauth2", "0006_remove_oauth2provider_name"),
|
||||
("authentik_providers_saml", "0006_remove_samlprovider_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="provider",
|
||||
old_name="name_temp",
|
||||
new_name="name",
|
||||
),
|
||||
]
|
||||
35
authentik/core/migrations/0013_auto_20201003_2132.py
Normal file
35
authentik/core/migrations/0013_auto_20201003_2132.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Generated by Django 3.1.2 on 2020-10-03 21:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0012_auto_20201003_1737"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="token",
|
||||
name="identifier",
|
||||
field=models.TextField(default=""),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="token",
|
||||
name="intent",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("verification", "Intent Verification"),
|
||||
("api", "Intent Api"),
|
||||
("recovery", "Intent Recovery"),
|
||||
],
|
||||
default="verification",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="token",
|
||||
unique_together={("identifier", "user")},
|
||||
),
|
||||
]
|
||||
50
authentik/core/migrations/0014_auto_20201018_1158.py
Normal file
50
authentik/core/migrations/0014_auto_20201018_1158.py
Normal file
@ -0,0 +1,50 @@
|
||||
# Generated by Django 3.1.2 on 2020-10-18 11:58
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
import authentik.core.models
|
||||
|
||||
|
||||
def set_default_token_key(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
Token = apps.get_model("authentik_core", "Token")
|
||||
|
||||
for token in Token.objects.using(db_alias).all():
|
||||
token.key = token.pk.hex
|
||||
token.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0013_auto_20201003_2132"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="token",
|
||||
name="key",
|
||||
field=models.TextField(default=authentik.core.models.default_token_key),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="token",
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="token",
|
||||
name="identifier",
|
||||
field=models.SlugField(max_length=255),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="token",
|
||||
index=models.Index(fields=["key"], name="authentik_co_key_e45007_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="token",
|
||||
index=models.Index(
|
||||
fields=["identifier"], name="authentik_co_identif_1a34a8_idx"
|
||||
),
|
||||
),
|
||||
migrations.RunPython(set_default_token_key),
|
||||
]
|
||||
24
authentik/core/migrations/0015_application_icon.py
Normal file
24
authentik/core/migrations/0015_application_icon.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-23 17:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0014_auto_20201018_1158"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="application",
|
||||
name="meta_icon_url",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="application",
|
||||
name="meta_icon",
|
||||
field=models.FileField(
|
||||
blank=True, default="", upload_to="application-icons/"
|
||||
),
|
||||
),
|
||||
]
|
||||
36
authentik/core/migrations/0016_auto_20201202_2234.py
Normal file
36
authentik/core/migrations/0016_auto_20201202_2234.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Generated by Django 3.1.3 on 2020-12-02 22:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0015_application_icon"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveIndex(
|
||||
model_name="token",
|
||||
name="authentik_co_key_e45007_idx",
|
||||
),
|
||||
migrations.RemoveIndex(
|
||||
model_name="token",
|
||||
name="authentik_co_identif_1a34a8_idx",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="user",
|
||||
old_name="pb_groups",
|
||||
new_name="ak_groups",
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="token",
|
||||
index=models.Index(
|
||||
fields=["identifier"], name="authentik_c_identif_d9d032_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="token",
|
||||
index=models.Index(fields=["key"], name="authentik_c_key_f71355_idx"),
|
||||
),
|
||||
]
|
||||
0
authentik/core/migrations/__init__.py
Normal file
0
authentik/core/migrations/__init__.py
Normal file
371
authentik/core/models.py
Normal file
371
authentik/core/models.py
Normal file
@ -0,0 +1,371 @@
|
||||
"""authentik core models"""
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, Optional, Type
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||
from django.db import models
|
||||
from django.db.models import Q, QuerySet
|
||||
from django.forms import ModelForm
|
||||
from django.http import HttpRequest
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from guardian.mixins import GuardianUserMixin
|
||||
from model_utils.managers import InheritanceManager
|
||||
from structlog import get_logger
|
||||
|
||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||
from authentik.core.signals import password_changed
|
||||
from authentik.core.types import UILoginButton
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.models import CreatedUpdatedModel
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
|
||||
LOGGER = get_logger()
|
||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
||||
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
|
||||
|
||||
|
||||
def default_token_duration():
|
||||
"""Default duration a Token is valid"""
|
||||
return now() + timedelta(minutes=30)
|
||||
|
||||
|
||||
def default_token_key():
|
||||
"""Default token key"""
|
||||
return uuid4().hex
|
||||
|
||||
|
||||
class Group(models.Model):
|
||||
"""Custom Group model which supports a basic hierarchy"""
|
||||
|
||||
group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
|
||||
name = models.CharField(_("name"), max_length=80)
|
||||
is_superuser = models.BooleanField(
|
||||
default=False, help_text=_("Users added to this group will be superusers.")
|
||||
)
|
||||
|
||||
parent = models.ForeignKey(
|
||||
"Group",
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="children",
|
||||
)
|
||||
attributes = models.JSONField(default=dict, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Group {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
unique_together = (
|
||||
(
|
||||
"name",
|
||||
"parent",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class UserManager(DjangoUserManager):
|
||||
"""Custom user manager that doesn't assign is_superuser and is_staff"""
|
||||
|
||||
def create_user(self, username, email=None, password=None, **extra_fields):
|
||||
"""Custom user manager that doesn't assign is_superuser and is_staff"""
|
||||
return self._create_user(username, email, password, **extra_fields)
|
||||
|
||||
|
||||
class User(GuardianUserMixin, AbstractUser):
|
||||
"""Custom User model to allow easier adding o f user-based settings"""
|
||||
|
||||
uuid = models.UUIDField(default=uuid4, editable=False)
|
||||
name = models.TextField(help_text=_("User's display name."))
|
||||
|
||||
sources = models.ManyToManyField("Source", through="UserSourceConnection")
|
||||
ak_groups = models.ManyToManyField("Group", related_name="users")
|
||||
password_change_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
attributes = models.JSONField(default=dict, blank=True)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
def group_attributes(self) -> Dict[str, Any]:
|
||||
"""Get a dictionary containing the attributes from all groups the user belongs to,
|
||||
including the users attributes"""
|
||||
final_attributes = {}
|
||||
for group in self.ak_groups.all().order_by("name"):
|
||||
final_attributes.update(group.attributes)
|
||||
final_attributes.update(self.attributes)
|
||||
return final_attributes
|
||||
|
||||
@cached_property
|
||||
def is_superuser(self) -> bool:
|
||||
"""Get supseruser status based on membership in a group with superuser status"""
|
||||
return self.ak_groups.filter(is_superuser=True).exists()
|
||||
|
||||
@property
|
||||
def is_staff(self) -> bool:
|
||||
"""superuser == staff user"""
|
||||
return self.is_superuser # type: ignore
|
||||
|
||||
def set_password(self, password, signal=True):
|
||||
if self.pk and signal:
|
||||
password_changed.send(sender=self, user=self, password=password)
|
||||
self.password_change_date = now()
|
||||
return super().set_password(password)
|
||||
|
||||
class Meta:
|
||||
|
||||
permissions = (
|
||||
("reset_user_password", "Reset Password"),
|
||||
("impersonate", "Can impersonate other users"),
|
||||
)
|
||||
verbose_name = _("User")
|
||||
verbose_name_plural = _("Users")
|
||||
|
||||
|
||||
class Provider(models.Model):
|
||||
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
|
||||
|
||||
name = models.TextField()
|
||||
|
||||
authorization_flow = models.ForeignKey(
|
||||
Flow,
|
||||
on_delete=models.CASCADE,
|
||||
help_text=_("Flow used when authorizing this provider."),
|
||||
related_name="provider_authorization",
|
||||
)
|
||||
|
||||
property_mappings = models.ManyToManyField(
|
||||
"PropertyMapping", default=None, blank=True
|
||||
)
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
@property
|
||||
def launch_url(self) -> Optional[str]:
|
||||
"""URL to this provider and initiate authorization for the user.
|
||||
Can return None for providers that are not URL-based"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
"""Return Form class used to edit this object"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Application(PolicyBindingModel):
|
||||
"""Every Application which uses authentik for authentication/identification/authorization
|
||||
needs an Application record. Other authentication types can subclass this Model to
|
||||
add custom fields and other properties"""
|
||||
|
||||
name = models.TextField(help_text=_("Application's display Name."))
|
||||
slug = models.SlugField(help_text=_("Internal application name, used in URLs."))
|
||||
provider = models.OneToOneField(
|
||||
"Provider", null=True, blank=True, default=None, on_delete=models.SET_DEFAULT
|
||||
)
|
||||
|
||||
meta_launch_url = models.URLField(default="", blank=True)
|
||||
# For template applications, this can be set to /static/authentik/applications/*
|
||||
meta_icon = models.FileField(upload_to="application-icons/", default="", blank=True)
|
||||
meta_description = models.TextField(default="", blank=True)
|
||||
meta_publisher = models.TextField(default="", blank=True)
|
||||
|
||||
def get_launch_url(self) -> Optional[str]:
|
||||
"""Get launch URL if set, otherwise attempt to get launch URL based on provider."""
|
||||
if self.meta_launch_url:
|
||||
return self.meta_launch_url
|
||||
if self.provider:
|
||||
return self.get_provider().launch_url
|
||||
return None
|
||||
|
||||
def get_provider(self) -> Optional[Provider]:
|
||||
"""Get casted provider instance"""
|
||||
if not self.provider:
|
||||
return None
|
||||
return Provider.objects.get_subclass(pk=self.provider.pk)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Application")
|
||||
verbose_name_plural = _("Applications")
|
||||
|
||||
|
||||
class Source(PolicyBindingModel):
|
||||
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
||||
|
||||
name = models.TextField(help_text=_("Source's display Name."))
|
||||
slug = models.SlugField(
|
||||
help_text=_("Internal source name, used in URLs."), unique=True
|
||||
)
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
property_mappings = models.ManyToManyField(
|
||||
"PropertyMapping", default=None, blank=True
|
||||
)
|
||||
|
||||
authentication_flow = models.ForeignKey(
|
||||
Flow,
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_NULL,
|
||||
help_text=_("Flow to use when authenticating existing users."),
|
||||
related_name="source_authentication",
|
||||
)
|
||||
enrollment_flow = models.ForeignKey(
|
||||
Flow,
|
||||
blank=True,
|
||||
null=True,
|
||||
default=None,
|
||||
on_delete=models.SET_NULL,
|
||||
help_text=_("Flow to use when enrolling new users."),
|
||||
related_name="source_enrollment",
|
||||
)
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
"""Return Form class used to edit this object"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def ui_login_button(self) -> Optional[UILoginButton]:
|
||||
"""If source uses a http-based flow, return UI Information about the login
|
||||
button. If source doesn't use http-based flow, return None."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def ui_additional_info(self) -> Optional[str]:
|
||||
"""Return additional Info, such as a callback URL. Show in the administration interface."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[str]:
|
||||
"""Entrypoint to integrate with User settings. Can either return None if no
|
||||
user settings are available, or a string with the URL to fetch."""
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class UserSourceConnection(CreatedUpdatedModel):
|
||||
"""Connection between User and Source."""
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
|
||||
unique_together = (("user", "source"),)
|
||||
|
||||
|
||||
class ExpiringModel(models.Model):
|
||||
"""Base Model which can expire, and is automatically cleaned up."""
|
||||
|
||||
expires = models.DateTimeField(default=default_token_duration)
|
||||
expiring = models.BooleanField(default=True)
|
||||
|
||||
@classmethod
|
||||
def filter_not_expired(cls, **kwargs) -> QuerySet:
|
||||
"""Filer for tokens which are not expired yet or are not expiring,
|
||||
and match filters in `kwargs`"""
|
||||
expired = Q(expires__lt=now(), expiring=True)
|
||||
return cls.objects.exclude(expired).filter(**kwargs)
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if token is expired yet."""
|
||||
return now() > self.expires
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
|
||||
|
||||
class TokenIntents(models.TextChoices):
|
||||
"""Intents a Token can be created for."""
|
||||
|
||||
# Single use token
|
||||
INTENT_VERIFICATION = "verification"
|
||||
|
||||
# Allow access to API
|
||||
INTENT_API = "api"
|
||||
|
||||
# Recovery use for the recovery app
|
||||
INTENT_RECOVERY = "recovery"
|
||||
|
||||
|
||||
class Token(ExpiringModel):
|
||||
"""Token used to authenticate the User for API Access or confirm another Stage like Email."""
|
||||
|
||||
token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
identifier = models.SlugField(max_length=255)
|
||||
key = models.TextField(default=default_token_key)
|
||||
intent = models.TextField(
|
||||
choices=TokenIntents.choices, default=TokenIntents.INTENT_VERIFICATION
|
||||
)
|
||||
user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="+")
|
||||
description = models.TextField(default="", blank=True)
|
||||
|
||||
def __str__(self):
|
||||
description = f"{self.identifier}"
|
||||
if self.expiring:
|
||||
description += f" (expires={self.expires})"
|
||||
return description
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Token")
|
||||
verbose_name_plural = _("Tokens")
|
||||
indexes = [
|
||||
models.Index(fields=["identifier"]),
|
||||
models.Index(fields=["key"]),
|
||||
]
|
||||
|
||||
|
||||
class PropertyMapping(models.Model):
|
||||
"""User-defined key -> x mapping which can be used by providers to expose extra data."""
|
||||
|
||||
pm_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||
name = models.TextField()
|
||||
expression = models.TextField()
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
"""Return Form class used to edit this object"""
|
||||
raise NotImplementedError
|
||||
|
||||
def evaluate(
|
||||
self, user: Optional[User], request: Optional[HttpRequest], **kwargs
|
||||
) -> Any:
|
||||
"""Evaluate `self.expression` using `**kwargs` as Context."""
|
||||
from authentik.core.expression import PropertyMappingEvaluator
|
||||
|
||||
evaluator = PropertyMappingEvaluator()
|
||||
evaluator.set_context(user, request, **kwargs)
|
||||
try:
|
||||
return evaluator.evaluate(self.expression)
|
||||
except (ValueError, SyntaxError) as exc:
|
||||
raise PropertyMappingExpressionException from exc
|
||||
|
||||
def __str__(self):
|
||||
return f"Property Mapping {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Property Mapping")
|
||||
verbose_name_plural = _("Property Mappings")
|
||||
5
authentik/core/signals.py
Normal file
5
authentik/core/signals.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""authentik core signals"""
|
||||
from django.core.signals import Signal
|
||||
|
||||
# Arguments: user: User, password: str
|
||||
password_changed = Signal()
|
||||
63
authentik/core/tasks.py
Normal file
63
authentik/core/tasks.py
Normal file
@ -0,0 +1,63 @@
|
||||
"""authentik core tasks"""
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
|
||||
from boto3.exceptions import Boto3Error
|
||||
from botocore.exceptions import BotoCoreError, ClientError
|
||||
from dbbackup.db.exceptions import CommandConnectorError
|
||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||
from django.core import management
|
||||
from django.utils.timezone import now
|
||||
from structlog import get_logger
|
||||
|
||||
from authentik.core.models import ExpiringModel
|
||||
from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||
from authentik.root.celery import CELERY_APP
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
def clean_expired_models(self: MonitoredTask):
|
||||
"""Remove expired objects"""
|
||||
messages = []
|
||||
for cls in ExpiringModel.__subclasses__():
|
||||
cls: ExpiringModel
|
||||
amount, _ = (
|
||||
cls.objects.all()
|
||||
.exclude(expiring=False)
|
||||
.exclude(expiring=True, expires__gt=now())
|
||||
.delete()
|
||||
)
|
||||
LOGGER.debug("Deleted expired models", model=cls, amount=amount)
|
||||
messages.append(f"Deleted {amount} expired {cls._meta.verbose_name_plural}")
|
||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages))
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
def backup_database(self: MonitoredTask): # pragma: no cover
|
||||
"""Database backup"""
|
||||
self.result_timeout_hours = 25
|
||||
try:
|
||||
start = datetime.now()
|
||||
out = StringIO()
|
||||
management.call_command("dbbackup", quiet=True, stdout=out)
|
||||
self.set_status(
|
||||
TaskResult(
|
||||
TaskResultStatus.SUCCESSFUL,
|
||||
[
|
||||
f"Successfully finished database backup {naturaltime(start)}",
|
||||
out.getvalue(),
|
||||
],
|
||||
)
|
||||
)
|
||||
LOGGER.info("Successfully backed up database.")
|
||||
except (
|
||||
IOError,
|
||||
BotoCoreError,
|
||||
ClientError,
|
||||
Boto3Error,
|
||||
PermissionError,
|
||||
CommandConnectorError,
|
||||
) as exc:
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
27
authentik/core/templates/403_csrf.html
Normal file
27
authentik/core/templates/403_csrf.html
Normal file
@ -0,0 +1,27 @@
|
||||
{% extends 'login/base.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
|
||||
{% block card_title %}
|
||||
{{ title }} <span>(403)</span>
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form>
|
||||
<h3>{{ main }}</h3>
|
||||
{% if no_referer %}
|
||||
<p>{{ no_referer1 }}</p>
|
||||
<p>{{ no_referer2 }}</p>
|
||||
<p>{{ no_referer3 }}</p>
|
||||
{% endif %}
|
||||
{% if no_cookie %}
|
||||
<p>{{ no_cookie1 }}</p>
|
||||
<p>{{ no_cookie2 }}</p>
|
||||
{% endif %}
|
||||
{% if 'back' in request.GET %}
|
||||
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
12
authentik/core/templates/base/page.html
Normal file
12
authentik/core/templates/base/page.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "base/skeleton.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block body %}
|
||||
<ak-messages></ak-messages>
|
||||
<div class="pf-c-page">
|
||||
<a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content">{% trans 'Skip to content' %}</a>
|
||||
{% block page_content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
41
authentik/core/templates/base/skeleton.html
Normal file
41
authentik/core/templates/base/skeleton.html
Normal file
@ -0,0 +1,41 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="preload" href="{% static 'dist/assets/fonts/DINEngschriftStd.woff2' %}" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="{% static 'dist/assets/fonts/DINEngschriftStd.woff' %}" as="font" type="font/woff" crossorigin>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>{% block title %}{% trans title|default:config.authentik.branding.title %}{% endblock %}</title>
|
||||
<link rel="icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
|
||||
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-addons.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/fontawesome.min.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||
<script src="{% url 'javascript-catalog' %}"></script>
|
||||
<script src="{% static 'dist/main.js' %}" type="module"></script>
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% if 'authentik_impersonate_user' in request.session %}
|
||||
<div class="pf-c-banner pf-m-warning pf-c-alert pf-m-sticky">
|
||||
<div class="pf-l-flex pf-m-justify-content-center pf-m-justify-content-space-between-on-lg pf-m-nowrap" style="height: 100%;">
|
||||
<div class="pf-u-display-none pf-u-display-block-on-lg">
|
||||
{% blocktrans with user=user %}You're currently impersonating {{ user }}.{% endblocktrans %}
|
||||
<a href="{% url 'authentik_core:impersonate-end' %}?back={{ request.get_full_path }}" id="acceptMessage">{% trans 'Stop impersonation' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
26
authentik/core/templates/error/generic.html
Normal file
26
authentik/core/templates/error/generic.html
Normal file
@ -0,0 +1,26 @@
|
||||
{% extends 'base/page.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
|
||||
{% block body %}
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-exclamation-circle pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans title %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% if message %}
|
||||
<h3>{% trans message %}</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if 'back' in request.GET %}
|
||||
<a href="{% back %}" class="pf-c-button pf-m-primary pf-m-block">{% trans 'Back' %}</a>
|
||||
{% endif %}
|
||||
<a href="/" class="pf-c-button pf-m-primary pf-m-block">{% trans 'Go to home' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
31
authentik/core/templates/generic/autosubmit_form.html
Normal file
31
authentik/core/templates/generic/autosubmit_form.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "login/base.html" %}
|
||||
|
||||
{% load authentik_utils %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{{ title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" action="{{ url }}" autosubmit>
|
||||
{% csrf_token %}
|
||||
{% for key, value in attrs.items %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}">
|
||||
{% endfor %}
|
||||
<div class="pf-c-form__group pf-u-display-flex pf-u-justify-content-center">
|
||||
<div class="pf-c-form__group-control">
|
||||
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<div class="pf-c-form__actions">
|
||||
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans 'Continue' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
34
authentik/core/templates/generic/autosubmit_form_full.html
Normal file
34
authentik/core/templates/generic/autosubmit_form_full.html
Normal file
@ -0,0 +1,34 @@
|
||||
{% extends "login/base_full.html" %}
|
||||
|
||||
{% load authentik_utils %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
{{ title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" action="{{ url }}" autosubmit>
|
||||
{% csrf_token %}
|
||||
{% for key, value in attrs.items %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}">
|
||||
{% endfor %}
|
||||
<div class="pf-c-form__group pf-u-display-flex pf-u-justify-content-center">
|
||||
<div class="pf-c-form__group-control">
|
||||
<span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<div class="pf-c-form__actions">
|
||||
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans 'Continue' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
document.querySelector("form").submit();
|
||||
</script>
|
||||
{% endblock %}
|
||||
43
authentik/core/templates/generic/delete.html
Normal file
43
authentik/core/templates/generic/delete.html
Normal file
@ -0,0 +1,43 @@
|
||||
{% extends container_template|default:"administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
{% block above_form %}
|
||||
<h1>
|
||||
{% blocktrans with object_type=object|verbose_name %}
|
||||
Delete {{ object_type }}
|
||||
{% endblocktrans %}
|
||||
</h1>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-l-stack">
|
||||
<div class="pf-l-stack__item">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
<form action="" method="post" class="pf-c-form">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
{% blocktrans with object_type=object|verbose_name name=object %}
|
||||
Are you sure you want to delete {{ object_type }} "{{ object }}"?
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<input type="hidden" name="confirmdelete" value="yes">
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<div class="pf-c-form__actions">
|
||||
<input class="pf-c-button pf-m-danger" type="submit" value="{% trans 'Delete' %}" />
|
||||
<a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Back" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
53
authentik/core/templates/library.html
Normal file
53
authentik/core/templates/library.html
Normal file
@ -0,0 +1,53 @@
|
||||
{% load i18n %}
|
||||
|
||||
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="pf-icon pf-icon-applications"></i>
|
||||
{% trans 'Applications' %}
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section">
|
||||
{% if applications %}
|
||||
<div class="pf-l-gallery pf-m-gutter">
|
||||
{% for app in applications %}
|
||||
<a href="{{ app.get_launch_url }}" class="pf-c-card pf-m-hoverable pf-m-compact ak-root-link">
|
||||
<div class="pf-c-card__header">
|
||||
{% if app.meta_icon %}
|
||||
<img class="app-icon pf-c-avatar" src="{{ app.meta_icon.url }}" alt="{% trans 'Application Icon' %}">
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-arrow"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pf-c-card__title">
|
||||
<p id="card-1-check-label">{{ app.name }}</p>
|
||||
<div class="pf-c-content">
|
||||
<small>{{ app.meta_publisher }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
{% trans app.meta_description|truncatewords:35 %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state pf-m-full-height">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">{% trans 'No Applications available.' %}</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans "Either no applications are defined, or you don't have access to any." %}
|
||||
</div>
|
||||
{% if perms.authentik_core.add_application %}
|
||||
<a href="{% url 'authentik_admin:application-create' %}" class="pf-c-button pf-m-primary" type="button">
|
||||
{% trans 'Create Application' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
</main>
|
||||
59
authentik/core/templates/login/base.html
Normal file
59
authentik/core/templates/login/base.html
Normal file
@ -0,0 +1,59 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<ak-messages url="{% url 'authentik_api:messages-list' %}"></ak-messages>
|
||||
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
{% block card_title %}
|
||||
{% trans title %}
|
||||
{% endblock %}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
{% include 'partials/form.html' %}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">Log in</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
</div>
|
||||
<footer class="pf-c-login__main-footer">
|
||||
{% if config.login.subtext %}
|
||||
<p>{{ config.login.subtext }}</p>
|
||||
{% endif %}
|
||||
<ul class="pf-c-login__main-footer-links">
|
||||
{% for source in sources %}
|
||||
<li class="pf-c-login__main-footer-links-item">
|
||||
<a href="{{ source.url }}" class="pf-c-login__main-footer-links-item-link">
|
||||
{% if source.icon_path %}
|
||||
<img src="{% static source.icon_path %}" alt="{{ source.name }}">
|
||||
{% elif source.icon_url %}
|
||||
<img src="icon_url" alt="{{ source.name }}">
|
||||
{% else %}
|
||||
<i class="pf-icon pf-icon-arrow" title="{{ source.name }}"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if enroll_url or recovery_url %}
|
||||
<div class="pf-c-login__main-footer-band">
|
||||
{% if enroll_url %}
|
||||
<p class="pf-c-login__main-footer-band-item">
|
||||
{% trans 'Need an account?' %}
|
||||
<a href="{{ enroll_url }}">{% trans 'Sign up.' %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if recovery_url %}
|
||||
<p class="pf-c-login__main-footer-band-item">
|
||||
<a href="{{ recovery_url }}">
|
||||
{% trans 'Forgot username or password?' %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</footer>
|
||||
75
authentik/core/templates/login/base_full.html
Normal file
75
authentik/core/templates/login/base_full.html
Normal file
@ -0,0 +1,75 @@
|
||||
{% extends 'base/skeleton.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
/* Fix logo/header/footer block moving around when card size changes */
|
||||
.pf-c-login__header {
|
||||
grid-row-start: 3;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="pf-c-background-image">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
|
||||
<filter id="image_overlay">
|
||||
<feColorMatrix type="matrix" values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0"></feColorMatrix>
|
||||
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
|
||||
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
|
||||
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
|
||||
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
|
||||
<feFuncA type="table" tableValues="0 1"></feFuncA>
|
||||
</feComponentTransfer>
|
||||
</filter>
|
||||
</svg>
|
||||
</div>
|
||||
<ak-messages url="{% url 'authentik_api:messages-list' %}"></ak-messages>
|
||||
<div class="pf-c-login">
|
||||
<div class="pf-c-login__container">
|
||||
<header class="pf-c-login__header">
|
||||
<div class="pf-c-brand ak-brand">
|
||||
<img src="{{ config.authentik.branding.logo }}" alt="authentik icon" />
|
||||
{% if config.authentik.branding.title_show %}
|
||||
<p>{{ config.authentik.branding.title }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<footer class="pf-c-login__footer">
|
||||
<p></p>
|
||||
<ul class="pf-c-list pf-m-inline">
|
||||
{% for link in config.authentik.footer_links %}
|
||||
<li>
|
||||
<a href="{{ link.href }}">{{ link.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if config.authentik.branding.title != "authentik" %}
|
||||
<li>
|
||||
<a href="https://github.com/beryju/authentik">
|
||||
{% trans 'Powered by authentik' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</footer>
|
||||
</header>
|
||||
{% block main_container %}
|
||||
<main class="pf-c-login__main">
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
{% block card_title %}
|
||||
{% endblock %}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
{% block card %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
19
authentik/core/templates/login/form.html
Normal file
19
authentik/core/templates/login/form.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends 'login/base.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
{% block above_form %}
|
||||
{% endblock %}
|
||||
|
||||
{% include 'partials/form.html' %}
|
||||
|
||||
{% block beneath_form %}
|
||||
{% endblock %}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans primary_action %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
18
authentik/core/templates/login/form_with_user.html
Normal file
18
authentik/core/templates/login/form_with_user.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends 'login/form.html' %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
|
||||
{% block above_form %}
|
||||
<div class="pf-c-form__group">
|
||||
<div class="form-control-static">
|
||||
<div class="left">
|
||||
<img class="pf-c-avatar" src="{% avatar user %}" alt="">
|
||||
{{ user.username }}
|
||||
</div>
|
||||
<div class="right">
|
||||
<a href="{% url 'authentik_flows:cancel' %}">{% trans 'Not you?' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
24
authentik/core/templates/login/loading.html
Normal file
24
authentik/core/templates/login/loading.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends 'login/base.html' %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
|
||||
{% block title %}
|
||||
{% trans title %}
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<meta http-equiv="refresh" content="0; url={{ target_url }}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<header class="login-pf-header">
|
||||
<h1>{% trans title %}</h1>
|
||||
</header>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="spinner spinner-lg"></div>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
73
authentik/core/templates/partials/form.html
Normal file
73
authentik/core/templates/partials/form.html
Normal file
@ -0,0 +1,73 @@
|
||||
{% load authentik_utils %}
|
||||
{% load i18n %}
|
||||
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="pf-c-form__group has-error">
|
||||
<p class="pf-c-form__helper-text pf-m-error">
|
||||
{{ form.non_field_errors }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for field in form %}
|
||||
{% if field.field.widget|fieldtype == 'HiddenInput' %}
|
||||
{{ field }}
|
||||
{% else %}
|
||||
<div class="pf-c-form__group {% if field.errors %} has-error {% endif %}">
|
||||
{% if field.field.widget|fieldtype == 'RadioSelect' %}
|
||||
<label class="pf-c-form__label" {% if field.field.required %}class="required" {% endif %}
|
||||
for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
{{ field.label }}
|
||||
</label>
|
||||
{% for c in field %}
|
||||
<div class="radio col-sm-10">
|
||||
<input type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
|
||||
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}" value="{{ c.data.value }}"
|
||||
{% if c.data.selected %} checked {% endif %}>
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% elif field.field.widget|fieldtype == 'Select' %}
|
||||
<label class="pf-c-form__label" {% if field.field.required %}class="required" {% endif %}
|
||||
for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
{{ field.label }}
|
||||
</label>
|
||||
<div class="select col-sm-10">
|
||||
{{ field }}
|
||||
</div>
|
||||
{% if field.help_text %}
|
||||
<span>
|
||||
{{ field.help_text }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% elif field.field.widget|fieldtype == 'CheckboxInput' %}
|
||||
<label class="checkbox-label">
|
||||
{{ field }} {{ field.label }}
|
||||
</label>
|
||||
{% if field.help_text %}
|
||||
<span>
|
||||
{{ field.help_text }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
<span class="pf-c-form__label-text">{{ field.label }}</span>
|
||||
{% if field.field.required %}
|
||||
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
{{ field|css_class:'pf-c-form-control' }}
|
||||
{% if field.help_text %}
|
||||
<span>
|
||||
{{ field.help_text }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<p class="pf-c-form__helper-text pf-m-error">
|
||||
{{ error }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
108
authentik/core/templates/partials/form_horizontal.html
Normal file
108
authentik/core/templates/partials/form_horizontal.html
Normal file
@ -0,0 +1,108 @@
|
||||
{% load authentik_utils %}
|
||||
{% load i18n %}
|
||||
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
<div class="pf-c-form__group {% if field.errors %} has-error {% endif %}">
|
||||
{% if field.field.widget|fieldtype == 'RadioSelect' %}
|
||||
<div class="pf-c-form__group-label">
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
<span class="pf-c-form__label-text">{{ field.label }}</span>
|
||||
{% if field.field.required %}
|
||||
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="pf-c-form__group-control">
|
||||
{% for c in field %}
|
||||
<div class="pf-c-radio">
|
||||
<input class="pf-c-radio__input"
|
||||
type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
|
||||
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}"
|
||||
value="{{ c.data.value }}"
|
||||
{% if c.data.selected %} checked {% endif %}/>
|
||||
<label class="pf-c-radio__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if field.help_text %}
|
||||
<p class="pf-c-form__helper-text">{{ field.help_text }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif field.field.widget|fieldtype == 'Select' %}
|
||||
<div class="pf-c-form__group-label">
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
<span class="pf-c-form__label-text">{{ field.label }}</span>
|
||||
{% if field.field.required %}
|
||||
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
{{ field|css_class:"pf-c-form-control" }}
|
||||
{% if field.help_text %}
|
||||
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% elif field.field.widget|fieldtype == 'CheckboxInput' %}
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
<div class="pf-c-check">
|
||||
{{ field|css_class:"pf-c-check__input" }}
|
||||
<label class="pf-c-check__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ field.label }}</label>
|
||||
</div>
|
||||
{% if field.help_text %}
|
||||
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% elif field.field.widget|fieldtype == "FileInput" %}
|
||||
<div class="pf-c-form__group-label">
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
<span class="pf-c-form__label-text">{{ field.label }}</span>
|
||||
{% if field.field.required %}
|
||||
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="c-form__horizontal-group">
|
||||
{{ field|css_class:"pf-c-form-control" }}
|
||||
{% if field.help_text %}
|
||||
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
{% if field.value %}
|
||||
<a target="_blank" href="{{ field.value.url }}" class="pf-c-form__helper-text">
|
||||
{% blocktrans with current=field.value %}
|
||||
Currently set to {{current}}.
|
||||
{% endblocktrans %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-form__group-label">
|
||||
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">
|
||||
<span class="pf-c-form__label-text">{{ field.label }}</span>
|
||||
{% if field.field.required %}
|
||||
<span class="pf-c-form__label-required" aria-hidden="true">*</span>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="pf-c-form__group-control">
|
||||
<div class="c-form__horizontal-group">
|
||||
{{ field|css_class:'pf-c-form-control' }}
|
||||
{% if field.help_text %}
|
||||
<p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for error in field.errors %}
|
||||
<p class="pf-c-form__helper-text pf-m-error">
|
||||
{{ error }}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
42
authentik/core/templates/partials/pagination.html
Normal file
42
authentik/core/templates/partials/pagination.html
Normal file
@ -0,0 +1,42 @@
|
||||
{% load i18n %}
|
||||
{% load authentik_utils %}
|
||||
|
||||
<div class="pf-c-toolbar__item pf-m-pagination ">
|
||||
<div class="pf-c-pagination pf-m-compact pf-m-hidden pf-m-visible-on-md">
|
||||
<div class="pf-c-pagination pf-m-compact pf-m-compact pf-m-hidden pf-m-visible-on-md">
|
||||
<div class="pf-c-options-menu">
|
||||
<div class="pf-c-options-menu__toggle pf-m-text pf-m-plain">
|
||||
<span class="pf-c-options-menu__toggle-text">
|
||||
{% blocktrans with start_index=page_obj.start_index end_index=page_obj.end_index total_items=paginator.count %}
|
||||
{{ start_index }} - {{ end_index }} of {{ total_items }}
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="pf-c-pagination__nav" aria-label="Pagination">
|
||||
<div class="pf-c-pagination__nav-control pf-m-prev">
|
||||
<a class="pf-c-button pf-m-plain"
|
||||
{% if page_obj.has_previous %}
|
||||
href="{{ request.path }}?{% query_transform page=page_obj.previous_page_number %}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}
|
||||
aria-label="{% trans 'Go to previous page' %}">
|
||||
<i class="fas fa-angle-left" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pf-c-pagination__nav-control pf-m-next">
|
||||
<a class="pf-c-button pf-m-plain"
|
||||
{% if page_obj.has_next %}
|
||||
href="{{ request.path }}?{% query_transform page=page_obj.next_page_number %}"
|
||||
{% else %}
|
||||
disabled
|
||||
{% endif %}
|
||||
aria-label="{% trans 'Go to next page' %}">
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
13
authentik/core/templates/partials/toolbar_search.html
Normal file
13
authentik/core/templates/partials/toolbar_search.html
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
<div class="pf-c-toolbar__group pf-m-filter-group">
|
||||
<div class="pf-c-toolbar__item pf-m-search-filter">
|
||||
<form class="pf-c-input-group" method="GET">
|
||||
{# include page data for pagination #}
|
||||
<input type="hidden" name="page" value="{{ page_obj.number }}">
|
||||
<input class="pf-c-form-control" name="search" type="search" placeholder="Search..." value="{{ request.GET.search }}">
|
||||
<button class="pf-c-button pf-m-control" type="submit">
|
||||
<i class="fas fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
5
authentik/core/templates/shell.html
Normal file
5
authentik/core/templates/shell.html
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends "base/skeleton.html" %}
|
||||
|
||||
{% block body %}
|
||||
<ak-interface-admin></ak-interface-admin>
|
||||
{% endblock %}
|
||||
78
authentik/core/templates/user/settings.html
Normal file
78
authentik/core/templates/user/settings.html
Normal file
@ -0,0 +1,78 @@
|
||||
{% load i18n %}
|
||||
{% load authentik_user_settings %}
|
||||
|
||||
<div class="pf-c-page">
|
||||
<main role="main" class="pf-c-page__main" tabindex="-1">
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="pf-icon pf-icon-user"></i>
|
||||
{% trans 'User Settings' %}
|
||||
</h1>
|
||||
<p>{% trans "Configure settings relevant to your user profile." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-u-display-flex pf-u-justify-content-center">
|
||||
<div class="pf-u-w-75">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__header pf-c-title pf-m-md">
|
||||
{% trans 'Update details' %}
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<form action="" method="post" class="pf-c-form pf-m-horizontal">
|
||||
{% include 'partials/form_horizontal.html' with form=form %}
|
||||
{% block beneath_form %}
|
||||
{% endblock %}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
<div class="pf-c-form__actions">
|
||||
<input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" />
|
||||
{% if unenrollment_enabled %}
|
||||
<a class="pf-c-button pf-m-danger"
|
||||
href="{% url 'authentik_flows:default-unenrollment' %}?back={{ request.get_full_path }}">{% trans "Delete account" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-u-display-flex pf-u-justify-content-center">
|
||||
<div class="pf-u-w-75">
|
||||
<ak-site-shell url="{% url 'authentik_core:user-tokens' %}">
|
||||
<div slot="body"></div>
|
||||
</ak-site-shell>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% user_stages as user_stages_loc %}
|
||||
{% for stage in user_stages_loc %}
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-u-display-flex pf-u-justify-content-center">
|
||||
<div class="pf-u-w-75">
|
||||
<ak-site-shell url="{{ stage }}">
|
||||
<div slot="body"></div>
|
||||
</ak-site-shell>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% user_sources as user_sources_loc %}
|
||||
{% for source in user_sources_loc %}
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-u-display-flex pf-u-justify-content-center">
|
||||
<div class="pf-u-w-75">
|
||||
<ak-site-shell url="{{ source }}">
|
||||
<div slot="body"></div>
|
||||
</ak-site-shell>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</main>
|
||||
</div>
|
||||
100
authentik/core/templates/user/token_list.html
Normal file
100
authentik/core/templates/user/token_list.html
Normal file
@ -0,0 +1,100 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__header pf-c-title pf-m-md">
|
||||
<p>{% trans "Tokens can be used to access authentik's API." %}</p>
|
||||
</div>
|
||||
{% 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-modal-button href="{% url 'authentik_core:user-tokens-create' %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
||||
{% trans 'Create' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-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 'Identifier' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Expires?' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Expiry Date' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Description' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for token in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<div>{{ token.identifier }}</div>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ token.expiring|yesno:"Yes,No" }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{% if not token.expiring %}
|
||||
-
|
||||
{% else %}
|
||||
{{ token.expires }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ token.description }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<ak-modal-button href="{% url 'authentik_core:user-tokens-update' identifier=token.identifier %}">
|
||||
<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_core:user-tokens-delete' identifier=token.identifier %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-danger">
|
||||
{% trans 'Delete' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
<ak-token-copy-button identifier="{{ token.identifier }}">
|
||||
{% trans 'Copy token' %}
|
||||
</ak-token-copy-button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-pagination pf-m-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Tokens.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no tokens exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<ak-modal-button href="{% url 'authentik_core:user-tokens-create' %}">
|
||||
<ak-spinner-button slot="trigger" class="pf-m-primary">
|
||||
{% trans 'Create' %}
|
||||
</ak-spinner-button>
|
||||
<div slot="modal"></div>
|
||||
</ak-modal-button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
0
authentik/core/templatetags/__init__.py
Normal file
0
authentik/core/templatetags/__init__.py
Normal file
44
authentik/core/templatetags/authentik_user_settings.py
Normal file
44
authentik/core/templatetags/authentik_user_settings.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""authentik user settings template tags"""
|
||||
from typing import Iterable
|
||||
|
||||
from django import template
|
||||
from django.template.context import RequestContext
|
||||
|
||||
from authentik.core.models import Source
|
||||
from authentik.flows.models import Stage
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
# pylint: disable=unused-argument
|
||||
def user_stages(context: RequestContext) -> list[str]:
|
||||
"""Return list of all stages which apply to user"""
|
||||
_all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses()
|
||||
matching_stages: list[str] = []
|
||||
for stage in _all_stages:
|
||||
user_settings = stage.ui_user_settings
|
||||
if not user_settings:
|
||||
continue
|
||||
matching_stages.append(user_settings)
|
||||
return matching_stages
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def user_sources(context: RequestContext) -> list[str]:
|
||||
"""Return a list of all sources which are enabled for the user"""
|
||||
user = context.get("request").user
|
||||
_all_sources: Iterable[Source] = Source.objects.filter(
|
||||
enabled=True
|
||||
).select_subclasses()
|
||||
matching_sources: list[str] = []
|
||||
for source in _all_sources:
|
||||
user_settings = source.ui_user_settings
|
||||
if not user_settings:
|
||||
continue
|
||||
policy_engine = PolicyEngine(source, user, context.get("request"))
|
||||
policy_engine.build()
|
||||
if policy_engine.passing:
|
||||
matching_sources.append(user_settings)
|
||||
return matching_sources
|
||||
0
authentik/core/tests/__init__.py
Normal file
0
authentik/core/tests/__init__.py
Normal file
56
authentik/core/tests/test_impersonation.py
Normal file
56
authentik/core/tests/test_impersonation.py
Normal file
@ -0,0 +1,56 @@
|
||||
"""impersonation tests"""
|
||||
from django.shortcuts import reverse
|
||||
from django.test.testcases import TestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
|
||||
|
||||
class TestImpersonation(TestCase):
|
||||
"""impersonation tests"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.other_user = User.objects.create(username="to-impersonate")
|
||||
self.akadmin = User.objects.get(username="akadmin")
|
||||
|
||||
def test_impersonate_simple(self):
|
||||
"""test simple impersonation and un-impersonation"""
|
||||
self.client.force_login(self.akadmin)
|
||||
|
||||
self.client.get(
|
||||
reverse(
|
||||
"authentik_core:impersonate-init",
|
||||
kwargs={"user_id": self.other_user.pk},
|
||||
)
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("authentik_api:user-me"))
|
||||
self.assertIn(self.other_user.username, response.content.decode())
|
||||
self.assertNotIn(self.akadmin.username, response.content.decode())
|
||||
|
||||
self.client.get(reverse("authentik_core:impersonate-end"))
|
||||
|
||||
response = self.client.get(reverse("authentik_api:user-me"))
|
||||
self.assertNotIn(self.other_user.username, response.content.decode())
|
||||
self.assertIn(self.akadmin.username, response.content.decode())
|
||||
|
||||
def test_impersonate_denied(self):
|
||||
"""test impersonation without permissions"""
|
||||
self.client.force_login(self.other_user)
|
||||
|
||||
self.client.get(
|
||||
reverse(
|
||||
"authentik_core:impersonate-init", kwargs={"user_id": self.akadmin.pk}
|
||||
)
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("authentik_api:user-me"))
|
||||
self.assertIn(self.other_user.username, response.content.decode())
|
||||
self.assertNotIn(self.akadmin.username, response.content.decode())
|
||||
|
||||
def test_un_impersonate_empty(self):
|
||||
"""test un-impersonation without impersonating first"""
|
||||
self.client.force_login(self.other_user)
|
||||
|
||||
response = self.client.get(reverse("authentik_core:impersonate-end"))
|
||||
self.assertRedirects(response, reverse("authentik_core:shell"))
|
||||
18
authentik/core/tests/test_tasks.py
Normal file
18
authentik/core/tests/test_tasks.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""authentik core task tests"""
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.core.models import Token
|
||||
from authentik.core.tasks import clean_expired_models
|
||||
|
||||
|
||||
class TestTasks(TestCase):
|
||||
"""Test Tasks"""
|
||||
|
||||
def test_token_cleanup(self):
|
||||
"""Test Token cleanup task"""
|
||||
Token.objects.create(expires=now(), user=get_anonymous_user())
|
||||
self.assertEqual(Token.objects.all().count(), 1)
|
||||
clean_expired_models.delay().get()
|
||||
self.assertEqual(Token.objects.all().count(), 0)
|
||||
42
authentik/core/tests/test_views_overview.py
Normal file
42
authentik/core/tests/test_views_overview.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""authentik user view tests"""
|
||||
import string
|
||||
from random import SystemRandom
|
||||
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
|
||||
|
||||
class TestOverviewViews(TestCase):
|
||||
"""Test Overview Views"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = User.objects.create_user(
|
||||
username="unittest user",
|
||||
email="unittest@example.com",
|
||||
password="".join(
|
||||
SystemRandom().choice(string.ascii_uppercase + string.digits)
|
||||
for _ in range(8)
|
||||
),
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_shell(self):
|
||||
"""Test shell"""
|
||||
self.assertEqual(
|
||||
self.client.get(reverse("authentik_core:shell")).status_code, 200
|
||||
)
|
||||
|
||||
def test_overview(self):
|
||||
"""Test overview"""
|
||||
self.assertEqual(
|
||||
self.client.get(reverse("authentik_core:overview")).status_code, 200
|
||||
)
|
||||
|
||||
def test_user_settings(self):
|
||||
"""Test user settings"""
|
||||
self.assertEqual(
|
||||
self.client.get(reverse("authentik_core:user-settings")).status_code, 200
|
||||
)
|
||||
30
authentik/core/tests/test_views_user.py
Normal file
30
authentik/core/tests/test_views_user.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""authentik user view tests"""
|
||||
import string
|
||||
from random import SystemRandom
|
||||
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
|
||||
|
||||
class TestUserViews(TestCase):
|
||||
"""Test User Views"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = User.objects.create_user(
|
||||
username="unittest user",
|
||||
email="unittest@example.com",
|
||||
password="".join(
|
||||
SystemRandom().choice(string.ascii_uppercase + string.digits)
|
||||
for _ in range(8)
|
||||
),
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_user_settings(self):
|
||||
"""Test UserSettingsView"""
|
||||
self.assertEqual(
|
||||
self.client.get(reverse("authentik_core:user-settings")).status_code, 200
|
||||
)
|
||||
20
authentik/core/types.py
Normal file
20
authentik/core/types.py
Normal file
@ -0,0 +1,20 @@
|
||||
"""authentik core dataclasses"""
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class UILoginButton:
|
||||
"""Dataclass for Source's ui_login_button"""
|
||||
|
||||
# Name, ran through i18n
|
||||
name: str
|
||||
|
||||
# URL Which Button points to
|
||||
url: str
|
||||
|
||||
# Icon name, ran through django's static
|
||||
icon_path: Optional[str] = None
|
||||
|
||||
# Icon URL, used as-is
|
||||
icon_url: Optional[str] = None
|
||||
39
authentik/core/urls.py
Normal file
39
authentik/core/urls.py
Normal file
@ -0,0 +1,39 @@
|
||||
"""authentik URL Configuration"""
|
||||
from django.urls import path
|
||||
|
||||
from authentik.core.views import impersonate, library, shell, user
|
||||
|
||||
urlpatterns = [
|
||||
path("", shell.ShellView.as_view(), name="shell"),
|
||||
# User views
|
||||
path("-/user/", user.UserSettingsView.as_view(), name="user-settings"),
|
||||
path("-/user/tokens/", user.TokenListView.as_view(), name="user-tokens"),
|
||||
path(
|
||||
"-/user/tokens/create/",
|
||||
user.TokenCreateView.as_view(),
|
||||
name="user-tokens-create",
|
||||
),
|
||||
path(
|
||||
"-/user/tokens/<slug:identifier>/update/",
|
||||
user.TokenUpdateView.as_view(),
|
||||
name="user-tokens-update",
|
||||
),
|
||||
path(
|
||||
"-/user/tokens/<slug:identifier>/delete/",
|
||||
user.TokenDeleteView.as_view(),
|
||||
name="user-tokens-delete",
|
||||
),
|
||||
# Libray
|
||||
path("library/", library.LibraryView.as_view(), name="overview"),
|
||||
# Impersonation
|
||||
path(
|
||||
"-/impersonation/<int:user_id>/",
|
||||
impersonate.ImpersonateInitView.as_view(),
|
||||
name="impersonate-init",
|
||||
),
|
||||
path(
|
||||
"-/impersonation/end/",
|
||||
impersonate.ImpersonateEndView.as_view(),
|
||||
name="impersonate-end",
|
||||
),
|
||||
]
|
||||
0
authentik/core/views/__init__.py
Normal file
0
authentik/core/views/__init__.py
Normal file
67
authentik/core/views/error.py
Normal file
67
authentik/core/views/error.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""authentik core error views"""
|
||||
|
||||
from django.http.response import (
|
||||
HttpResponseBadRequest,
|
||||
HttpResponseForbidden,
|
||||
HttpResponseNotFound,
|
||||
HttpResponseServerError,
|
||||
)
|
||||
from django.template.response import TemplateResponse
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class BadRequestTemplateResponse(TemplateResponse, HttpResponseBadRequest):
|
||||
"""Combine Template response with Http Code 400"""
|
||||
|
||||
|
||||
class ForbiddenTemplateResponse(TemplateResponse, HttpResponseForbidden):
|
||||
"""Combine Template response with Http Code 403"""
|
||||
|
||||
|
||||
class NotFoundTemplateResponse(TemplateResponse, HttpResponseNotFound):
|
||||
"""Combine Template response with Http Code 404"""
|
||||
|
||||
|
||||
class ServerErrorTemplateResponse(TemplateResponse, HttpResponseServerError):
|
||||
"""Combine Template response with Http Code 500"""
|
||||
|
||||
|
||||
class BadRequestView(TemplateView):
|
||||
"""Show Bad Request message"""
|
||||
|
||||
extra_context = {"title": "Bad Request"}
|
||||
|
||||
response_class = BadRequestTemplateResponse
|
||||
template_name = "error/generic.html"
|
||||
|
||||
|
||||
class ForbiddenView(TemplateView):
|
||||
"""Show Forbidden message"""
|
||||
|
||||
extra_context = {"title": "Forbidden"}
|
||||
|
||||
response_class = ForbiddenTemplateResponse
|
||||
template_name = "error/generic.html"
|
||||
|
||||
|
||||
class NotFoundView(TemplateView):
|
||||
"""Show Not Found message"""
|
||||
|
||||
extra_context = {"title": "Not Found"}
|
||||
|
||||
response_class = NotFoundTemplateResponse
|
||||
template_name = "error/generic.html"
|
||||
|
||||
|
||||
class ServerErrorView(TemplateView):
|
||||
"""Show Server Error message"""
|
||||
|
||||
extra_context = {"title": "Server Error"}
|
||||
|
||||
response_class = ServerErrorTemplateResponse
|
||||
template_name = "error/generic.html"
|
||||
|
||||
# pylint: disable=useless-super-delegation
|
||||
def dispatch(self, *args, **kwargs): # pragma: no cover
|
||||
"""Little wrapper so django accepts this function"""
|
||||
return super().dispatch(*args, **kwargs)
|
||||
58
authentik/core/views/impersonate.py
Normal file
58
authentik/core/views/impersonate.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""authentik impersonation views"""
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.views import View
|
||||
from structlog import get_logger
|
||||
|
||||
from authentik.audit.models import Event, EventAction
|
||||
from authentik.core.middleware import (
|
||||
SESSION_IMPERSONATE_ORIGINAL_USER,
|
||||
SESSION_IMPERSONATE_USER,
|
||||
)
|
||||
from authentik.core.models import User
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class ImpersonateInitView(View):
|
||||
"""Initiate Impersonation"""
|
||||
|
||||
def get(self, request: HttpRequest, user_id: int) -> HttpResponse:
|
||||
"""Impersonation handler, checks permissions"""
|
||||
if not request.user.has_perm("impersonate"):
|
||||
LOGGER.debug(
|
||||
"User attempted to impersonate without permissions", user=request.user
|
||||
)
|
||||
return HttpResponse("Unauthorized", status=401)
|
||||
|
||||
user_to_be = get_object_or_404(User, pk=user_id)
|
||||
|
||||
request.session[SESSION_IMPERSONATE_ORIGINAL_USER] = request.user
|
||||
request.session[SESSION_IMPERSONATE_USER] = user_to_be
|
||||
|
||||
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
||||
|
||||
return redirect("authentik_core:shell")
|
||||
|
||||
|
||||
class ImpersonateEndView(View):
|
||||
"""End User impersonation"""
|
||||
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
"""End Impersonation handler"""
|
||||
if (
|
||||
SESSION_IMPERSONATE_USER not in request.session
|
||||
or SESSION_IMPERSONATE_ORIGINAL_USER not in request.session
|
||||
):
|
||||
LOGGER.debug("Can't end impersonation", user=request.user)
|
||||
return redirect("authentik_core:shell")
|
||||
|
||||
original_user = request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||
|
||||
del request.session[SESSION_IMPERSONATE_USER]
|
||||
del request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||
|
||||
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
|
||||
|
||||
return redirect("authentik_core:shell")
|
||||
23
authentik/core/views/library.py
Normal file
23
authentik/core/views/library.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""authentik library view"""
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
|
||||
|
||||
class LibraryView(LoginRequiredMixin, TemplateView):
|
||||
"""Overview for logged in user, incase user opens authentik directly
|
||||
and is not being forwarded"""
|
||||
|
||||
template_name = "library.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["applications"] = []
|
||||
for application in Application.objects.all().order_by("name"):
|
||||
engine = PolicyEngine(application, self.request.user, self.request)
|
||||
engine.build()
|
||||
if engine.passing:
|
||||
kwargs["applications"].append(application)
|
||||
return super().get_context_data(**kwargs)
|
||||
9
authentik/core/views/shell.py
Normal file
9
authentik/core/views/shell.py
Normal file
@ -0,0 +1,9 @@
|
||||
"""core shell view"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
|
||||
class ShellView(LoginRequiredMixin, TemplateView):
|
||||
"""core shell view"""
|
||||
|
||||
template_name = "shell.html"
|
||||
137
authentik/core/views/user.py
Normal file
137
authentik/core/views/user.py
Normal file
@ -0,0 +1,137 @@
|
||||
"""authentik core user views"""
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.db.models.query import QuerySet
|
||||
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 guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
|
||||
from authentik.admin.views.utils import (
|
||||
DeleteMessageView,
|
||||
SearchListMixin,
|
||||
UserPaginateListMixin,
|
||||
)
|
||||
from authentik.core.forms.token import UserTokenForm
|
||||
from authentik.core.forms.users import UserDetailForm
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
|
||||
"""Update User settings"""
|
||||
|
||||
template_name = "user/settings.html"
|
||||
form_class = UserDetailForm
|
||||
|
||||
success_message = _("Successfully updated user.")
|
||||
success_url = reverse_lazy("authentik_core:user-settings")
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
unenrollment_flow = Flow.with_policy(
|
||||
self.request, designation=FlowDesignation.UNRENOLLMENT
|
||||
)
|
||||
kwargs["unenrollment_enabled"] = bool(unenrollment_flow)
|
||||
return kwargs
|
||||
|
||||
|
||||
class TokenListView(
|
||||
LoginRequiredMixin,
|
||||
PermissionListMixin,
|
||||
UserPaginateListMixin,
|
||||
SearchListMixin,
|
||||
ListView,
|
||||
):
|
||||
"""Show list of all tokens"""
|
||||
|
||||
model = Token
|
||||
ordering = "expires"
|
||||
permission_required = "authentik_core.view_token"
|
||||
|
||||
template_name = "user/token_list.html"
|
||||
search_fields = [
|
||||
"identifier",
|
||||
"intent",
|
||||
"description",
|
||||
]
|
||||
|
||||
def get_queryset(self) -> QuerySet:
|
||||
return super().get_queryset().filter(intent=TokenIntents.INTENT_API)
|
||||
|
||||
|
||||
class TokenCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
):
|
||||
"""Create new Token"""
|
||||
|
||||
model = Token
|
||||
form_class = UserTokenForm
|
||||
permission_required = "authentik_core.add_token"
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("authentik_core:user-tokens")
|
||||
success_message = _("Successfully created Token")
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["container_template"] = "user/base.html"
|
||||
return kwargs
|
||||
|
||||
def form_valid(self, form: UserTokenForm) -> HttpResponse:
|
||||
form.instance.user = self.request.user
|
||||
form.instance.intent = TokenIntents.INTENT_API
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class TokenUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
):
|
||||
"""Update token"""
|
||||
|
||||
model = Token
|
||||
form_class = UserTokenForm
|
||||
permission_required = "authentik_core.update_token"
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("authentik_core:user-tokens")
|
||||
success_message = _("Successfully updated Token")
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["container_template"] = "user/base.html"
|
||||
return kwargs
|
||||
|
||||
def get_object(self) -> Token:
|
||||
identifier = self.kwargs.get("identifier")
|
||||
return get_objects_for_user(
|
||||
self.request.user, "authentik_core.update_token", self.model
|
||||
).filter(intent=TokenIntents.INTENT_API, identifier=identifier)
|
||||
|
||||
|
||||
class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete token"""
|
||||
|
||||
model = Token
|
||||
permission_required = "authentik_core.delete_token"
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("authentik_core:user-tokens")
|
||||
success_message = _("Successfully deleted Token")
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["container_template"] = "user/base.html"
|
||||
return kwargs
|
||||
Reference in New Issue
Block a user