*: remove unused templates and code, move avatar to User model
This commit is contained in:
parent
cf7e7c44ff
commit
890e0e9054
|
@ -2,28 +2,20 @@
|
||||||
from drf_yasg2.utils import swagger_auto_schema
|
from drf_yasg2.utils import swagger_auto_schema
|
||||||
from guardian.utils import get_anonymous_user
|
from guardian.utils import get_anonymous_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.fields import CharField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import (
|
from rest_framework.serializers import BooleanField, ModelSerializer
|
||||||
BooleanField,
|
|
||||||
ModelSerializer,
|
|
||||||
SerializerMethodField,
|
|
||||||
)
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.lib.templatetags.authentik_utils import avatar
|
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(ModelSerializer):
|
class UserSerializer(ModelSerializer):
|
||||||
"""User Serializer"""
|
"""User Serializer"""
|
||||||
|
|
||||||
is_superuser = BooleanField(read_only=True)
|
is_superuser = BooleanField(read_only=True)
|
||||||
avatar = SerializerMethodField()
|
avatar = CharField(read_only=True)
|
||||||
|
|
||||||
def get_avatar(self, user: User) -> str:
|
|
||||||
"""Add user's avatar as URL"""
|
|
||||||
return avatar(user)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
"""authentik core models"""
|
"""authentik core models"""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from hashlib import sha256
|
from hashlib import md5, sha256
|
||||||
from typing import Any, Optional, Type
|
from typing import Any, Optional, Type
|
||||||
|
from urllib.parse import urlencode
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -11,7 +12,9 @@ from django.db import models
|
||||||
from django.db.models import Q, QuerySet
|
from django.db.models import Q, QuerySet
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
from django.templatetags.static import static
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.html import escape
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from guardian.mixins import GuardianUserMixin
|
from guardian.mixins import GuardianUserMixin
|
||||||
|
@ -23,6 +26,7 @@ from authentik.core.exceptions import PropertyMappingExpressionException
|
||||||
from authentik.core.signals import password_changed
|
from authentik.core.signals import password_changed
|
||||||
from authentik.core.types import UILoginButton
|
from authentik.core.types import UILoginButton
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
|
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
|
||||||
from authentik.managed.models import ManagedModel
|
from authentik.managed.models import ManagedModel
|
||||||
from authentik.policies.models import PolicyBindingModel
|
from authentik.policies.models import PolicyBindingModel
|
||||||
|
@ -31,6 +35,9 @@ LOGGER = get_logger()
|
||||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
||||||
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
|
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
|
||||||
|
|
||||||
|
GRAVATAR_URL = "https://secure.gravatar.com"
|
||||||
|
DEFAULT_AVATAR = static("authentik/user_default.png")
|
||||||
|
|
||||||
|
|
||||||
def default_token_duration():
|
def default_token_duration():
|
||||||
"""Default duration a Token is valid"""
|
"""Default duration a Token is valid"""
|
||||||
|
@ -126,6 +133,25 @@ class User(GuardianUserMixin, AbstractUser):
|
||||||
"""Generate a globall unique UID, based on the user ID and the hashed secret key"""
|
"""Generate a globall unique UID, based on the user ID and the hashed secret key"""
|
||||||
return sha256(f"{self.id}-{settings.SECRET_KEY}".encode("ascii")).hexdigest()
|
return sha256(f"{self.id}-{settings.SECRET_KEY}".encode("ascii")).hexdigest()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def avatar(self) -> str:
|
||||||
|
"""Get avatar, depending on authentik.avatar setting"""
|
||||||
|
mode = CONFIG.raw.get("authentik").get("avatars")
|
||||||
|
if mode == "none":
|
||||||
|
return DEFAULT_AVATAR
|
||||||
|
if mode == "gravatar":
|
||||||
|
parameters = [
|
||||||
|
("s", "158"),
|
||||||
|
("r", "g"),
|
||||||
|
]
|
||||||
|
# gravatar uses md5 for their URLs, so md5 can't be avoided
|
||||||
|
mail_hash = md5(self.email.encode("utf-8")).hexdigest() # nosec
|
||||||
|
gravatar_url = (
|
||||||
|
f"{GRAVATAR_URL}/avatar/{mail_hash}?{urlencode(parameters, doseq=True)}"
|
||||||
|
)
|
||||||
|
return escape(gravatar_url)
|
||||||
|
raise ValueError(f"Invalid avatar mode {mode}")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
permissions = (
|
permissions = (
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
{% 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 %}
|
|
|
@ -1,18 +0,0 @@
|
||||||
{% 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 %}
|
|
|
@ -1,24 +0,0 @@
|
||||||
{% 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 %}
|
|
|
@ -1,14 +1,12 @@
|
||||||
"""authentik stage Base view"""
|
"""authentik stage Base view"""
|
||||||
from typing import Any, Optional
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.http.request import QueryDict
|
from django.http.request import QueryDict
|
||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.views.generic.base import View
|
||||||
from django.views.generic import TemplateView
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import DEFAULT_AVATAR, User
|
||||||
from authentik.flows.challenge import (
|
from authentik.flows.challenge import (
|
||||||
Challenge,
|
Challenge,
|
||||||
ChallengeResponse,
|
ChallengeResponse,
|
||||||
|
@ -17,53 +15,29 @@ from authentik.flows.challenge import (
|
||||||
)
|
)
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.views import FlowExecutorView
|
from authentik.flows.views import FlowExecutorView
|
||||||
from authentik.lib.templatetags.authentik_utils import avatar
|
|
||||||
|
|
||||||
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
|
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class StageView(TemplateView):
|
class StageView(View):
|
||||||
"""Abstract Stage, inherits TemplateView but can be combined with FormView"""
|
"""Abstract Stage, inherits TemplateView but can be combined with FormView"""
|
||||||
|
|
||||||
template_name = "login/form_with_user.html"
|
|
||||||
|
|
||||||
executor: FlowExecutorView
|
executor: FlowExecutorView
|
||||||
|
|
||||||
request: HttpRequest = None
|
request: HttpRequest = None
|
||||||
|
|
||||||
def __init__(self, executor: FlowExecutorView):
|
def __init__(self, executor: FlowExecutorView, **kwargs):
|
||||||
self.executor = executor
|
self.executor = executor
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
def get_pending_user(self) -> User:
|
||||||
kwargs["title"] = self.executor.flow.title
|
|
||||||
# Either show the matched User object or show what the user entered,
|
|
||||||
# based on what the earlier stage (mostly IdentificationStage) set.
|
|
||||||
# _USER_IDENTIFIER overrides the first User, as PENDING_USER is used for
|
|
||||||
# other things besides the form display
|
|
||||||
if PLAN_CONTEXT_PENDING_USER in self.executor.plan.context:
|
|
||||||
kwargs["user"] = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
|
||||||
if PLAN_CONTEXT_PENDING_USER_IDENTIFIER in self.executor.plan.context:
|
|
||||||
kwargs["user"] = User(
|
|
||||||
username=self.executor.plan.context.get(
|
|
||||||
PLAN_CONTEXT_PENDING_USER_IDENTIFIER
|
|
||||||
),
|
|
||||||
email="",
|
|
||||||
)
|
|
||||||
kwargs["primary_action"] = _("Continue")
|
|
||||||
return super().get_context_data(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ChallengeStageView(StageView):
|
|
||||||
"""Stage view which response with a challenge"""
|
|
||||||
|
|
||||||
response_class = ChallengeResponse
|
|
||||||
|
|
||||||
def get_pending_user(self) -> Optional[User]:
|
|
||||||
"""Either show the matched User object or show what the user entered,
|
"""Either show the matched User object or show what the user entered,
|
||||||
based on what the earlier stage (mostly IdentificationStage) set.
|
based on what the earlier stage (mostly IdentificationStage) set.
|
||||||
_USER_IDENTIFIER overrides the first User, as PENDING_USER is used for
|
_USER_IDENTIFIER overrides the first User, as PENDING_USER is used for
|
||||||
other things besides the form display"""
|
other things besides the form display.
|
||||||
|
|
||||||
|
If no user is pending, returns request.user"""
|
||||||
if PLAN_CONTEXT_PENDING_USER_IDENTIFIER in self.executor.plan.context:
|
if PLAN_CONTEXT_PENDING_USER_IDENTIFIER in self.executor.plan.context:
|
||||||
return User(
|
return User(
|
||||||
username=self.executor.plan.context.get(
|
username=self.executor.plan.context.get(
|
||||||
|
@ -73,13 +47,20 @@ class ChallengeStageView(StageView):
|
||||||
)
|
)
|
||||||
if PLAN_CONTEXT_PENDING_USER in self.executor.plan.context:
|
if PLAN_CONTEXT_PENDING_USER in self.executor.plan.context:
|
||||||
return self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
return self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||||
return None
|
return self.request.user
|
||||||
|
|
||||||
|
|
||||||
|
class ChallengeStageView(StageView):
|
||||||
|
"""Stage view which response with a challenge"""
|
||||||
|
|
||||||
|
response_class = ChallengeResponse
|
||||||
|
|
||||||
def get_response_instance(self, data: QueryDict) -> ChallengeResponse:
|
def get_response_instance(self, data: QueryDict) -> ChallengeResponse:
|
||||||
"""Return the response class type"""
|
"""Return the response class type"""
|
||||||
return self.response_class(None, data=data, stage=self)
|
return self.response_class(None, data=data, stage=self)
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
"""Return a challenge for the frontend to solve"""
|
||||||
challenge = self._get_challenge(*args, **kwargs)
|
challenge = self._get_challenge(*args, **kwargs)
|
||||||
if not challenge.is_valid():
|
if not challenge.is_valid():
|
||||||
LOGGER.warning(challenge.errors)
|
LOGGER.warning(challenge.errors)
|
||||||
|
@ -103,7 +84,9 @@ class ChallengeStageView(StageView):
|
||||||
# If there's no user set, an error is raised later.
|
# If there's no user set, an error is raised later.
|
||||||
if user := self.get_pending_user():
|
if user := self.get_pending_user():
|
||||||
challenge.initial_data["pending_user"] = user.username
|
challenge.initial_data["pending_user"] = user.username
|
||||||
challenge.initial_data["pending_user_avatar"] = avatar(user)
|
challenge.initial_data["pending_user_avatar"] = DEFAULT_AVATAR
|
||||||
|
if not isinstance(user, AnonymousUser):
|
||||||
|
challenge.initial_data["pending_user_avatar"] = user.avatar
|
||||||
return challenge
|
return challenge
|
||||||
|
|
||||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||||
|
|
|
@ -8,6 +8,7 @@ from django.urls import reverse
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
from authentik.flows.challenge import ChallengeTypes
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
||||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
|
@ -404,7 +405,14 @@ class TestFlowExecutor(TestCase):
|
||||||
# First request, run the planner
|
# First request, run the planner
|
||||||
response = self.client.get(exec_url)
|
response = self.client.get(exec_url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertIn("dummy1", force_str(response.content))
|
self.assertJSONEqual(
|
||||||
|
force_str(response.content),
|
||||||
|
{
|
||||||
|
"type": ChallengeTypes.native.value,
|
||||||
|
"component": "",
|
||||||
|
"title": binding.stage.name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||||
|
|
||||||
|
@ -427,7 +435,14 @@ class TestFlowExecutor(TestCase):
|
||||||
# but it won't save it, hence we cant' check the plan
|
# but it won't save it, hence we cant' check the plan
|
||||||
response = self.client.get(exec_url)
|
response = self.client.get(exec_url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertIn("dummy4", force_str(response.content))
|
self.assertJSONEqual(
|
||||||
|
force_str(response.content),
|
||||||
|
{
|
||||||
|
"type": ChallengeTypes.native.value,
|
||||||
|
"component": "",
|
||||||
|
"title": binding4.stage.name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# fourth request, this confirms the last stage (dummy4)
|
# fourth request, this confirms the last stage (dummy4)
|
||||||
# We do this request without the patch, so the policy results in false
|
# We do this request without the patch, so the policy results in false
|
||||||
|
@ -466,4 +481,4 @@ class TestFlowExecutor(TestCase):
|
||||||
executor.flow = flow
|
executor.flow = flow
|
||||||
|
|
||||||
stage_view = StageView(executor)
|
stage_view = StageView(executor)
|
||||||
self.assertEqual(ident, stage_view.get_context_data()["user"].username)
|
self.assertEqual(ident, stage_view.get_pending_user().username)
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
{% load authentik_utils %}
|
|
||||||
|
|
||||||
{% spaceless %}
|
|
||||||
<div class="dynamic-array-widget">
|
|
||||||
{% for widget in widget.subwidgets %}
|
|
||||||
<div class="array-item input-group">
|
|
||||||
{% include widget.template_name %}
|
|
||||||
<div class="input-group-btn">
|
|
||||||
<button class="array-remove btn btn-danger" type="button">
|
|
||||||
<span class="pficon-delete"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
<div><button type="button" class="add-array-item btn btn-default">Add another</button></div>
|
|
||||||
</div>
|
|
||||||
{% endspaceless %}
|
|
|
@ -1,25 +1,15 @@
|
||||||
"""authentik lib Templatetags"""
|
"""authentik lib Templatetags"""
|
||||||
from hashlib import md5
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.http.request import HttpRequest
|
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
from django.templatetags.static import static
|
|
||||||
from django.utils.html import escape, mark_safe
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
from authentik.lib.utils.urls import is_url_absolute
|
from authentik.lib.utils.urls import is_url_absolute
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
GRAVATAR_URL = "https://secure.gravatar.com"
|
|
||||||
DEFAULT_AVATAR = static("authentik/user_default.png")
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def back(context: Context) -> str:
|
def back(context: Context) -> str:
|
||||||
|
@ -46,38 +36,12 @@ def fieldtype(field):
|
||||||
return field.__class__.__name__
|
return field.__class__.__name__
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
|
||||||
def config(path, default=""):
|
|
||||||
"""Get a setting from the database. Returns default is setting doesn't exist."""
|
|
||||||
return CONFIG.y(path, default)
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name="css_class")
|
@register.filter(name="css_class")
|
||||||
def css_class(field, css):
|
def css_class(field, css):
|
||||||
"""Add css class to form field"""
|
"""Add css class to form field"""
|
||||||
return field.as_widget(attrs={"class": css})
|
return field.as_widget(attrs={"class": css})
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
|
||||||
def avatar(user: User) -> str:
|
|
||||||
"""Get avatar, depending on authentik.avatar setting"""
|
|
||||||
mode = CONFIG.raw.get("authentik").get("avatars")
|
|
||||||
if mode == "none":
|
|
||||||
return DEFAULT_AVATAR
|
|
||||||
if mode == "gravatar":
|
|
||||||
parameters = [
|
|
||||||
("s", "158"),
|
|
||||||
("r", "g"),
|
|
||||||
]
|
|
||||||
# gravatar uses md5 for their URLs, so md5 can't be avoided
|
|
||||||
mail_hash = md5(user.email.encode("utf-8")).hexdigest() # nosec
|
|
||||||
gravatar_url = (
|
|
||||||
f"{GRAVATAR_URL}/avatar/{mail_hash}?{urlencode(parameters, doseq=True)}"
|
|
||||||
)
|
|
||||||
return escape(gravatar_url)
|
|
||||||
raise ValueError(f"Invalid avatar mode {mode}")
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def verbose_name(obj) -> str:
|
def verbose_name(obj) -> str:
|
||||||
"""Return Object's Verbose Name"""
|
"""Return Object's Verbose Name"""
|
||||||
|
@ -94,21 +58,3 @@ def form_verbose_name(obj) -> str:
|
||||||
if not obj:
|
if not obj:
|
||||||
return ""
|
return ""
|
||||||
return verbose_name(obj._meta.model)
|
return verbose_name(obj._meta.model)
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
|
||||||
def doc(obj) -> str:
|
|
||||||
"""Return docstring of object"""
|
|
||||||
return mark_safe(obj.__doc__.replace("\n", "<br>"))
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
|
||||||
def query_transform(context: Context, **kwargs) -> str:
|
|
||||||
"""Append objects to the current querystring"""
|
|
||||||
if "request" not in context:
|
|
||||||
return ""
|
|
||||||
request: HttpRequest = context["request"]
|
|
||||||
updated = request.GET.copy()
|
|
||||||
for key, value in kwargs.items():
|
|
||||||
updated[key] = value
|
|
||||||
return updated.urlencode()
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
"""authentik UI utils"""
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
def human_list(_list: list[Any]) -> str:
|
|
||||||
"""Convert a list of items into 'a, b or c'"""
|
|
||||||
last_item = _list.pop()
|
|
||||||
if len(_list) < 1:
|
|
||||||
return last_item
|
|
||||||
result = ", ".join(_list)
|
|
||||||
return "%s or %s" % (result, last_item)
|
|
|
@ -233,7 +233,9 @@ class OAuthFulfillmentStage(StageView):
|
||||||
params: OAuthAuthorizationParams
|
params: OAuthAuthorizationParams
|
||||||
provider: OAuth2Provider
|
provider: OAuth2Provider
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
"""final Stage of an OAuth2 Flow"""
|
||||||
self.params: OAuthAuthorizationParams = self.executor.plan.context.pop(
|
self.params: OAuthAuthorizationParams = self.executor.plan.context.pop(
|
||||||
PLAN_CONTEXT_PARAMS
|
PLAN_CONTEXT_PARAMS
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,7 +14,9 @@ class PostUserEnrollmentStage(StageView):
|
||||||
"""Dynamically injected stage which saves the OAuth Connection after
|
"""Dynamically injected stage which saves the OAuth Connection after
|
||||||
the user has been enrolled."""
|
the user has been enrolled."""
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
"""Stage used after the user has been enrolled"""
|
||||||
access: UserOAuthSourceConnection = self.executor.plan.context[
|
access: UserOAuthSourceConnection = self.executor.plan.context[
|
||||||
PLAN_CONTEXT_SOURCES_OAUTH_ACCESS
|
PLAN_CONTEXT_SOURCES_OAUTH_ACCESS
|
||||||
]
|
]
|
||||||
|
|
|
@ -16,7 +16,6 @@ from webauthn.webauthn import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.lib.templatetags.authentik_utils import avatar
|
|
||||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||||
from authentik.stages.authenticator_webauthn.utils import generate_challenge
|
from authentik.stages.authenticator_webauthn.utils import generate_challenge
|
||||||
|
|
||||||
|
@ -57,7 +56,7 @@ def get_webauthn_challenge(request: HttpRequest, device: WebAuthnDevice) -> dict
|
||||||
device.user.uid,
|
device.user.uid,
|
||||||
device.user.username,
|
device.user.username,
|
||||||
device.user.name,
|
device.user.name,
|
||||||
avatar(device.user),
|
device.user.avatar,
|
||||||
device.credential_id,
|
device.credential_id,
|
||||||
device.public_key,
|
device.public_key,
|
||||||
device.sign_count,
|
device.sign_count,
|
||||||
|
@ -92,7 +91,7 @@ def validate_challenge_webauthn(data: dict, request: HttpRequest, user: User) ->
|
||||||
user.uid,
|
user.uid,
|
||||||
user.username,
|
user.username,
|
||||||
user.name,
|
user.name,
|
||||||
avatar(user),
|
user.avatar,
|
||||||
device.credential_id,
|
device.credential_id,
|
||||||
device.public_key,
|
device.public_key,
|
||||||
device.sign_count,
|
device.sign_count,
|
||||||
|
|
|
@ -16,7 +16,6 @@ from authentik.core.models import User
|
||||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.lib.templatetags.authentik_utils import avatar
|
|
||||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||||
from authentik.stages.authenticator_webauthn.utils import (
|
from authentik.stages.authenticator_webauthn.utils import (
|
||||||
generate_challenge,
|
generate_challenge,
|
||||||
|
@ -118,7 +117,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
||||||
user.uid,
|
user.uid,
|
||||||
user.username,
|
user.username,
|
||||||
user.name,
|
user.name,
|
||||||
avatar(user or User()),
|
user.avatar,
|
||||||
)
|
)
|
||||||
|
|
||||||
return AuthenticatorWebAuthnChallenge(
|
return AuthenticatorWebAuthnChallenge(
|
||||||
|
|
|
@ -12,7 +12,6 @@ from authentik.flows.challenge import (
|
||||||
)
|
)
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.lib.templatetags.authentik_utils import avatar
|
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent
|
from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent
|
||||||
|
|
||||||
|
@ -56,7 +55,7 @@ class ConsentStageView(ChallengeStageView):
|
||||||
# If there's no user set, an error is raised later.
|
# If there's no user set, an error is raised later.
|
||||||
if user := self.get_pending_user():
|
if user := self.get_pending_user():
|
||||||
challenge.initial_data["pending_user"] = user.username
|
challenge.initial_data["pending_user"] = user.username
|
||||||
challenge.initial_data["pending_user_avatar"] = avatar(user)
|
challenge.initial_data["pending_user_avatar"] = user.avatar
|
||||||
return challenge
|
return challenge
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
|
|
@ -1,19 +1,31 @@
|
||||||
"""authentik multi-stage authentication engine"""
|
"""authentik multi-stage authentication engine"""
|
||||||
from typing import Any
|
from django.http.response import HttpResponse
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
||||||
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.flows.stage import StageView
|
|
||||||
|
|
||||||
|
|
||||||
class DummyStageView(StageView):
|
class DummyChallenge(Challenge):
|
||||||
|
"""Dummy challenge"""
|
||||||
|
|
||||||
|
|
||||||
|
class DummyChallengeResponse(ChallengeResponse):
|
||||||
|
"""Dummy challenge response"""
|
||||||
|
|
||||||
|
|
||||||
|
class DummyStageView(ChallengeStageView):
|
||||||
"""Dummy stage for testing with multiple stages"""
|
"""Dummy stage for testing with multiple stages"""
|
||||||
|
|
||||||
def post(self, request: HttpRequest):
|
response_class = DummyChallengeResponse
|
||||||
"""Just redirect to next stage"""
|
|
||||||
|
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
|
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||||
kwargs = super().get_context_data(**kwargs)
|
return DummyChallenge(
|
||||||
kwargs["title"] = self.executor.current_stage.name
|
data={
|
||||||
return kwargs
|
"type": ChallengeTypes.native,
|
||||||
|
"component": "",
|
||||||
|
"title": self.executor.current_stage.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -15,6 +15,7 @@ class InvitationStageView(StageView):
|
||||||
"""Finalise Authentication flow by logging the user in"""
|
"""Finalise Authentication flow by logging the user in"""
|
||||||
|
|
||||||
def get(self, request: HttpRequest) -> HttpResponse:
|
def get(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
"""Apply data to the current flow based on a URL"""
|
||||||
stage: InvitationStage = self.executor.current_stage
|
stage: InvitationStage = self.executor.current_stage
|
||||||
if INVITATION_TOKEN_KEY not in request.GET:
|
if INVITATION_TOKEN_KEY not in request.GET:
|
||||||
# No Invitation was given, raise error or continue
|
# No Invitation was given, raise error or continue
|
||||||
|
|
|
@ -88,7 +88,9 @@ class PasswordStageView(ChallengeStageView):
|
||||||
"authentik_flows:flow-executor-shell",
|
"authentik_flows:flow-executor-shell",
|
||||||
kwargs={"flow_slug": recovery_flow.first().slug},
|
kwargs={"flow_slug": recovery_flow.first().slug},
|
||||||
)
|
)
|
||||||
challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(recover_url)
|
challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(
|
||||||
|
recover_url
|
||||||
|
)
|
||||||
return challenge
|
return challenge
|
||||||
|
|
||||||
def challenge_invalid(self, response: PasswordChallengeResponse) -> HttpResponse:
|
def challenge_invalid(self, response: PasswordChallengeResponse) -> HttpResponse:
|
||||||
|
|
|
@ -17,6 +17,7 @@ class UserLoginStageView(StageView):
|
||||||
"""Finalise Authentication flow by logging the user in"""
|
"""Finalise Authentication flow by logging the user in"""
|
||||||
|
|
||||||
def get(self, request: HttpRequest) -> HttpResponse:
|
def get(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
"""Attach the currently pending user to the current session"""
|
||||||
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
||||||
message = _("No Pending user to login.")
|
message = _("No Pending user to login.")
|
||||||
messages.error(request, message)
|
messages.error(request, message)
|
||||||
|
|
|
@ -12,6 +12,7 @@ class UserLogoutStageView(StageView):
|
||||||
"""Finalise Authentication flow by logging the user in"""
|
"""Finalise Authentication flow by logging the user in"""
|
||||||
|
|
||||||
def get(self, request: HttpRequest) -> HttpResponse:
|
def get(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
"""Remove the user from the current session"""
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Logged out",
|
"Logged out",
|
||||||
user=request.user,
|
user=request.user,
|
||||||
|
|
|
@ -22,6 +22,8 @@ class UserWriteStageView(StageView):
|
||||||
"""Finalise Enrollment flow by creating a user object."""
|
"""Finalise Enrollment flow by creating a user object."""
|
||||||
|
|
||||||
def get(self, request: HttpRequest) -> HttpResponse:
|
def get(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
"""Save data in the current flow to the currently pending user. If no user is pending,
|
||||||
|
a new user is created."""
|
||||||
if PLAN_CONTEXT_PROMPT not in self.executor.plan.context:
|
if PLAN_CONTEXT_PROMPT not in self.executor.plan.context:
|
||||||
message = _("No Pending data.")
|
message = _("No Pending data.")
|
||||||
messages.error(request, message)
|
messages.error(request, message)
|
||||||
|
|
Reference in a new issue