core: add new token intent and auth backend (#1284)
* core: add new token intent and auth backend Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * root: update schema Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: allow users to create app password tokens Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: display token's intents Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * stages/password: auto-enable app password backend Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: fix missing app passwords backend Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * core: use custom inbuilt backend, set backend login information in flow plan for events Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * website/docs: add docs for `auth_method` and `auth_method_args` fields Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * website: fix example flows using incorrect backend Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * root: add alias for akflow files Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * core: fix token intent not defaulting correctly Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * website: update akflows orders Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: improve delete modal for stage bindings and policy bindings Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * events: fix linting Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * website: make default login-2fa flow ignore 2fa with app passwords Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web/admin: select all password stage backends by default Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * root: fix mis-matched postgres version for CI Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * web: fix lint error Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * core: fix authentication error when no request is given Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * ci: set debug log level Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * stages/user_write: fix wrong fallback authentication backend Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * core: add token tests for invalid intent and token auth Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
commit
6b7a8b6ac7
22
.vscode/settings.json
vendored
Normal file
22
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"asgi",
|
||||
"authentik",
|
||||
"authn",
|
||||
"goauthentik",
|
||||
"jwks",
|
||||
"oidc",
|
||||
"openid",
|
||||
"plex",
|
||||
"saml",
|
||||
"totp",
|
||||
"webauthn"
|
||||
],
|
||||
"python.linting.pylintEnabled": true,
|
||||
"todo-tree.tree.showCountsInTree": true,
|
||||
"todo-tree.tree.showBadges": true,
|
||||
"python.formatting.provider": "black",
|
||||
"files.associations": {
|
||||
"*.akflow": "json"
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
"""Tokens API Viewset"""
|
||||
from typing import Any
|
||||
|
||||
from django.http.response import Http404
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
@ -22,6 +25,13 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
|
|||
|
||||
user = UserSerializer(required=False)
|
||||
|
||||
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
||||
"""Ensure only API or App password tokens are created."""
|
||||
attrs.setdefault("intent", TokenIntents.INTENT_API)
|
||||
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
|
||||
raise ValidationError(f"Invalid intent {attrs.get('intent')}")
|
||||
return attrs
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Token
|
||||
|
@ -69,7 +79,6 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
|||
def perform_create(self, serializer: TokenSerializer):
|
||||
serializer.save(
|
||||
user=self.request.user,
|
||||
intent=TokenIntents.INTENT_API,
|
||||
expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True),
|
||||
)
|
||||
|
||||
|
|
58
authentik/core/auth.py
Normal file
58
authentik/core/auth.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
"""Authenticate with tokens"""
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.http.request import HttpRequest
|
||||
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
|
||||
|
||||
class InbuiltBackend(ModelBackend):
|
||||
"""Inbuilt backend"""
|
||||
|
||||
def authenticate(
|
||||
self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any
|
||||
) -> Optional[User]:
|
||||
user = super().authenticate(request, username=username, password=password, **kwargs)
|
||||
if not user:
|
||||
return None
|
||||
self.set_method("password", request)
|
||||
return user
|
||||
|
||||
def set_method(self, method: str, request: Optional[HttpRequest], **kwargs):
|
||||
"""Set method data on current flow, if possbiel"""
|
||||
if not request:
|
||||
return
|
||||
# Since we can't directly pass other variables to signals, and we want to log the method
|
||||
# and the token used, we assume we're running in a flow and set a variable in the context
|
||||
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
|
||||
flow_plan.context[PLAN_CONTEXT_METHOD] = method
|
||||
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS] = kwargs
|
||||
request.session[SESSION_KEY_PLAN] = flow_plan
|
||||
|
||||
|
||||
class TokenBackend(InbuiltBackend):
|
||||
"""Authenticate with token"""
|
||||
|
||||
def authenticate(
|
||||
self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any
|
||||
) -> Optional[User]:
|
||||
try:
|
||||
user = User._default_manager.get_by_natural_key(username)
|
||||
except User.DoesNotExist:
|
||||
# Run the default password hasher once to reduce the timing
|
||||
# difference between an existing and a nonexistent user (#20760).
|
||||
User().set_password(password)
|
||||
return None
|
||||
tokens = Token.filter_not_expired(
|
||||
user=user, key=password, intent=TokenIntents.INTENT_APP_PASSWORD
|
||||
)
|
||||
if not tokens.exists():
|
||||
return None
|
||||
token = tokens.first()
|
||||
self.set_method("password", request, token=token)
|
||||
return token.user
|
26
authentik/core/migrations/0028_alter_token_intent.py
Normal file
26
authentik/core/migrations/0028_alter_token_intent.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Generated by Django 3.2.6 on 2021-08-23 14:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0027_bootstrap_token"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="token",
|
||||
name="intent",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("verification", "Intent Verification"),
|
||||
("api", "Intent Api"),
|
||||
("recovery", "Intent Recovery"),
|
||||
("app_password", "Intent App Password"),
|
||||
],
|
||||
default="verification",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -411,6 +411,9 @@ class TokenIntents(models.TextChoices):
|
|||
# Recovery use for the recovery app
|
||||
INTENT_RECOVERY = "recovery"
|
||||
|
||||
# App-specific passwords
|
||||
INTENT_APP_PASSWORD = "app_password" # nosec
|
||||
|
||||
|
||||
class Token(ManagedModel, ExpiringModel):
|
||||
"""Token used to authenticate the User for API Access or confirm another Stage like Email."""
|
||||
|
|
|
@ -25,7 +25,7 @@ from authentik.flows.planner import (
|
|||
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.policies.utils import delete_none_keys
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
|
@ -189,7 +189,7 @@ class SourceFlowManager:
|
|||
kwargs.update(
|
||||
{
|
||||
# Since we authenticate the user by their token, they have no backend set
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
|
||||
PLAN_CONTEXT_SSO: True,
|
||||
PLAN_CONTEXT_SOURCE: self.source,
|
||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
"""Test Source flow_manager"""
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http.request import HttpRequest
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from guardian.utils import get_anonymous_user
|
||||
|
||||
from authentik.core.models import SourceUserMatchingModes, User
|
||||
from authentik.core.sources.flow_manager import Action
|
||||
from authentik.flows.tests.test_planner import dummy_get_response
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.views.callback import OAuthSourceFlowManager
|
||||
|
||||
|
@ -24,22 +21,10 @@ class TestSourceFlowManager(TestCase):
|
|||
self.factory = RequestFactory()
|
||||
self.identifier = generate_id()
|
||||
|
||||
def get_request(self, user: User) -> HttpRequest:
|
||||
"""Helper to create a get request with session and message middleware"""
|
||||
request = self.factory.get("/")
|
||||
request.user = user
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
middleware = MessageMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
return request
|
||||
|
||||
def test_unauthenticated_enroll(self):
|
||||
"""Test un-authenticated user enrolling"""
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.ENROLL)
|
||||
|
@ -52,7 +37,7 @@ class TestSourceFlowManager(TestCase):
|
|||
)
|
||||
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.AUTH)
|
||||
|
@ -65,7 +50,7 @@ class TestSourceFlowManager(TestCase):
|
|||
)
|
||||
user = User.objects.create(username="foo", email="foo@bar.baz")
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(user), self.identifier, {}
|
||||
self.source, get_request("/", user=user), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.LINK)
|
||||
|
@ -78,7 +63,7 @@ class TestSourceFlowManager(TestCase):
|
|||
|
||||
# Without email, deny
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.DENY)
|
||||
|
@ -86,7 +71,7 @@ class TestSourceFlowManager(TestCase):
|
|||
# With email
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{"email": "foo@bar.baz"},
|
||||
)
|
||||
|
@ -101,7 +86,7 @@ class TestSourceFlowManager(TestCase):
|
|||
|
||||
# Without username, deny
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.DENY)
|
||||
|
@ -109,7 +94,7 @@ class TestSourceFlowManager(TestCase):
|
|||
# With username
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{"username": "foo"},
|
||||
)
|
||||
|
@ -125,7 +110,7 @@ class TestSourceFlowManager(TestCase):
|
|||
# With non-existent username, enroll
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{
|
||||
"username": "bar",
|
||||
|
@ -137,7 +122,7 @@ class TestSourceFlowManager(TestCase):
|
|||
# With username
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{"username": "foo"},
|
||||
)
|
||||
|
@ -151,7 +136,7 @@ class TestSourceFlowManager(TestCase):
|
|||
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{"username": "foo"},
|
||||
)
|
||||
|
|
|
@ -27,6 +27,14 @@ class TestTokenAPI(APITestCase):
|
|||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||
self.assertEqual(token.expiring, True)
|
||||
|
||||
def test_token_create_invalid(self):
|
||||
"""Test token creation endpoint (invalid data)"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:token-list"),
|
||||
{"identifier": "test-token", "intent": TokenIntents.INTENT_RECOVERY},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_token_create_non_expiring(self):
|
||||
"""Test token creation endpoint"""
|
||||
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = False
|
||||
|
|
40
authentik/core/tests/test_token_auth.py
Normal file
40
authentik/core/tests/test_token_auth.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
"""Test token auth"""
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.auth import TokenBackend
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.lib.tests.utils import get_request
|
||||
|
||||
|
||||
class TestTokenAuth(TestCase):
|
||||
"""Test token auth"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.create(username="test-user")
|
||||
self.token = Token.objects.create(
|
||||
expiring=False, user=self.user, intent=TokenIntents.INTENT_APP_PASSWORD
|
||||
)
|
||||
# To test with session we need to create a request and pass it through all middlewares
|
||||
self.request = get_request("/")
|
||||
self.request.session[SESSION_KEY_PLAN] = FlowPlan("test")
|
||||
|
||||
def test_token_auth(self):
|
||||
"""Test auth with token"""
|
||||
self.assertEqual(
|
||||
TokenBackend().authenticate(self.request, "test-user", self.token.key), self.user
|
||||
)
|
||||
|
||||
def test_token_auth_none(self):
|
||||
"""Test auth with token (non-existent user)"""
|
||||
self.assertIsNone(
|
||||
TokenBackend().authenticate(self.request, "test-user-foo", self.token.key), self.user
|
||||
)
|
||||
|
||||
def test_token_auth_invalid(self):
|
||||
"""Test auth with token (invalid token)"""
|
||||
self.assertIsNone(
|
||||
TokenBackend().authenticate(self.request, "test-user", self.token.key + "foo"),
|
||||
self.user,
|
||||
)
|
|
@ -15,6 +15,7 @@ from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
|
|||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.stages.invitation.models import Invitation
|
||||
from authentik.stages.invitation.signals import invitation_used
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
from authentik.stages.user_write.signals import user_write
|
||||
|
||||
|
||||
|
@ -46,7 +47,13 @@ def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
|
|||
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
|
||||
if PLAN_CONTEXT_SOURCE in flow_plan.context:
|
||||
# Login request came from an external source, save it in the context
|
||||
thread.kwargs["using_source"] = flow_plan.context[PLAN_CONTEXT_SOURCE]
|
||||
thread.kwargs[PLAN_CONTEXT_SOURCE] = flow_plan.context[PLAN_CONTEXT_SOURCE]
|
||||
if PLAN_CONTEXT_METHOD in flow_plan.context:
|
||||
thread.kwargs[PLAN_CONTEXT_METHOD] = flow_plan.context[PLAN_CONTEXT_METHOD]
|
||||
# Save the login method used
|
||||
thread.kwargs[PLAN_CONTEXT_METHOD_ARGS] = flow_plan.context.get(
|
||||
PLAN_CONTEXT_METHOD_ARGS, {}
|
||||
)
|
||||
thread.user = user
|
||||
thread.run()
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|||
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.stages.identification.models import UserFields
|
||||
from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP
|
||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
||||
|
||||
|
||||
def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
|
@ -26,7 +26,7 @@ def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSc
|
|||
|
||||
password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
|
||||
name="default-authentication-password",
|
||||
defaults={"backends": [BACKEND_DJANGO, BACKEND_LDAP]},
|
||||
defaults={"backends": [BACKEND_INBUILT, BACKEND_LDAP, BACKEND_APP_PASSWORD]},
|
||||
)
|
||||
|
||||
login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(
|
||||
|
|
|
@ -3,7 +3,6 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
|||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpRequest
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
@ -13,6 +12,7 @@ from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableExce
|
|||
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
||||
from authentik.lib.tests.utils import dummy_get_response
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.policies.types import PolicyResult
|
||||
|
@ -24,11 +24,6 @@ CACHE_MOCK = Mock(wraps=cache)
|
|||
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
|
||||
|
||||
|
||||
def dummy_get_response(request: HttpRequest): # pragma: no cover
|
||||
"""Dummy get_response for SessionMiddleware"""
|
||||
return None
|
||||
|
||||
|
||||
class TestFlowPlanner(TestCase):
|
||||
"""Test planner logic"""
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ def get_attrs(obj: SerializerModel) -> dict[str, Any]:
|
|||
"component",
|
||||
"flow_set",
|
||||
"promptstage_set",
|
||||
"policybindingmodel_ptr_id",
|
||||
"export_url",
|
||||
)
|
||||
for to_remove_name in to_remove:
|
||||
if to_remove_name in data:
|
||||
|
|
27
authentik/lib/tests/utils.py
Normal file
27
authentik/lib/tests/utils.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
"""Test utils"""
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http import HttpRequest
|
||||
from django.test.client import RequestFactory
|
||||
from guardian.utils import get_anonymous_user
|
||||
|
||||
|
||||
def dummy_get_response(request: HttpRequest): # pragma: no cover
|
||||
"""Dummy get_response for SessionMiddleware"""
|
||||
return None
|
||||
|
||||
|
||||
def get_request(*args, user=None, **kwargs):
|
||||
"""Get a request with usable session"""
|
||||
request = RequestFactory().get(*args, **kwargs)
|
||||
if user:
|
||||
request.user = user
|
||||
else:
|
||||
request.user = get_anonymous_user()
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
middleware = MessageMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
return request
|
|
@ -1,16 +1,14 @@
|
|||
"""Test AuthN Request generator and parser"""
|
||||
from base64 import b64encode
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http.request import QueryDict
|
||||
from django.test import RequestFactory, TestCase
|
||||
from guardian.utils import get_anonymous_user
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.flows.tests.test_planner import dummy_get_response
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.managed.manager import ObjectManager
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||
|
@ -99,11 +97,7 @@ class TestAuthNRequest(TestCase):
|
|||
|
||||
def test_signed_valid(self):
|
||||
"""Test generated AuthNRequest with valid signature"""
|
||||
http_request = self.factory.get("/")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
|
@ -117,12 +111,7 @@ class TestAuthNRequest(TestCase):
|
|||
|
||||
def test_request_full_signed(self):
|
||||
"""Test full SAML Request/Response flow, fully signed"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = get_anonymous_user()
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
|
@ -145,12 +134,7 @@ class TestAuthNRequest(TestCase):
|
|||
|
||||
def test_request_id_invalid(self):
|
||||
"""Test generated AuthNRequest with invalid request ID"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = get_anonymous_user()
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
|
@ -179,11 +163,7 @@ class TestAuthNRequest(TestCase):
|
|||
|
||||
def test_signed_valid_detached(self):
|
||||
"""Test generated AuthNRequest with valid signature (detached)"""
|
||||
http_request = self.factory.get("/")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
|
@ -243,12 +223,7 @@ class TestAuthNRequest(TestCase):
|
|||
|
||||
def test_request_attributes(self):
|
||||
"""Test full SAML Request/Response flow, fully signed"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = User.objects.get(username="akadmin")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/", user=User.objects.get(username="akadmin"))
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
|
@ -264,12 +239,7 @@ class TestAuthNRequest(TestCase):
|
|||
|
||||
def test_request_attributes_invalid(self):
|
||||
"""Test full SAML Request/Response flow, fully signed"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = User.objects.get(username="akadmin")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/", user=User.objects.get(username="akadmin"))
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
"""Test Requests and Responses against schema"""
|
||||
from base64 import b64encode
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test import RequestFactory, TestCase
|
||||
from guardian.utils import get_anonymous_user
|
||||
from lxml import etree # nosec
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.managed.manager import ObjectManager
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
|
||||
from authentik.providers.saml.tests.test_auth_n_request import dummy_get_response
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.processors.request import RequestProcessor
|
||||
|
||||
|
@ -43,11 +41,7 @@ class TestSchema(TestCase):
|
|||
|
||||
def test_request_schema(self):
|
||||
"""Test generated AuthNRequest against Schema"""
|
||||
http_request = self.factory.get("/")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
|
@ -60,12 +54,7 @@ class TestSchema(TestCase):
|
|||
|
||||
def test_response_schema(self):
|
||||
"""Test generated AuthNRequest against Schema"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = get_anonymous_user()
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
|
|
|
@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
|
|||
from django.views import View
|
||||
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
|
||||
|
||||
class UseTokenView(View):
|
||||
|
@ -19,7 +19,7 @@ class UseTokenView(View):
|
|||
if not tokens.exists():
|
||||
raise Http404
|
||||
token = tokens.first()
|
||||
login(request, token.user, backend=BACKEND_DJANGO)
|
||||
login(request, token.user, backend=BACKEND_INBUILT)
|
||||
token.delete()
|
||||
messages.warning(request, _("Used recovery-link to authenticate."))
|
||||
return redirect("authentik_core:if-admin")
|
||||
|
|
|
@ -73,7 +73,8 @@ LANGUAGE_COOKIE_NAME = f"authentik_language{_cookie_suffix}"
|
|||
SESSION_COOKIE_NAME = f"authentik_session{_cookie_suffix}"
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
"authentik.core.auth.InbuiltBackend",
|
||||
"authentik.core.auth.TokenBackend",
|
||||
"guardian.backends.ObjectPermissionBackend",
|
||||
]
|
||||
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
from typing import Optional
|
||||
|
||||
import ldap3
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.http import HttpRequest
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.auth import InbuiltBackend
|
||||
from authentik.core.models import User
|
||||
from authentik.sources.ldap.models import LDAPSource
|
||||
|
||||
|
@ -13,7 +13,7 @@ LOGGER = get_logger()
|
|||
LDAP_DISTINGUISHED_NAME = "distinguishedName"
|
||||
|
||||
|
||||
class LDAPBackend(ModelBackend):
|
||||
class LDAPBackend(InbuiltBackend):
|
||||
"""Authenticate users against LDAP Server"""
|
||||
|
||||
def authenticate(self, request: HttpRequest, **kwargs):
|
||||
|
@ -24,6 +24,7 @@ class LDAPBackend(ModelBackend):
|
|||
LOGGER.debug("LDAP Auth attempt", source=source)
|
||||
user = self.auth_user(source, **kwargs)
|
||||
if user:
|
||||
self.set_method("ldap", request, source=source)
|
||||
return user
|
||||
return None
|
||||
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
"""LDAP Settings"""
|
||||
from celery.schedules import crontab
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"authentik.sources.ldap.auth.LDAPBackend",
|
||||
]
|
||||
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"sources_ldap_sync": {
|
||||
"task": "authentik.sources.ldap.tasks.ldap_sync_all",
|
||||
|
|
|
@ -39,7 +39,7 @@ from authentik.sources.saml.processors.constants import (
|
|||
from authentik.sources.saml.processors.request import SESSION_REQUEST_ID
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
from authentik.stages.user_login.stage import BACKEND_DJANGO
|
||||
from authentik.stages.user_login.stage import BACKEND_INBUILT
|
||||
|
||||
LOGGER = get_logger()
|
||||
if TYPE_CHECKING:
|
||||
|
@ -136,7 +136,7 @@ class ResponseProcessor:
|
|||
self._source.authentication_flow,
|
||||
**{
|
||||
PLAN_CONTEXT_PENDING_USER: user,
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -199,7 +199,7 @@ class ResponseProcessor:
|
|||
self._source.authentication_flow,
|
||||
**{
|
||||
PLAN_CONTEXT_PENDING_USER: matching_users.first(),
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
|
||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""Test validator stage"""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls.base import reverse
|
||||
|
@ -12,8 +11,8 @@ from rest_framework.exceptions import ValidationError
|
|||
from authentik.core.models import User
|
||||
from authentik.flows.challenge import ChallengeTypes
|
||||
from authentik.flows.models import Flow, FlowStageBinding, NotConfiguredAction
|
||||
from authentik.flows.tests.test_planner import dummy_get_response
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
|
||||
from authentik.stages.authenticator_validate.challenge import (
|
||||
|
@ -97,11 +96,8 @@ class AuthenticatorValidateStageTests(TestCase):
|
|||
|
||||
def test_device_challenge_webauthn(self):
|
||||
"""Test webauthn"""
|
||||
request = self.request_factory.get("/")
|
||||
request = get_request("/")
|
||||
request.user = self.user
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
|
||||
webauthn_device = WebAuthnDevice.objects.create(
|
||||
user=self.user,
|
||||
|
|
|
@ -9,7 +9,7 @@ from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
|||
from authentik.lib.generators import generate_key
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.models import PasswordStage
|
||||
|
||||
|
||||
|
@ -68,7 +68,7 @@ class TestIdentificationStage(TestCase):
|
|||
|
||||
def test_valid_with_password(self):
|
||||
"""Test with valid email and password in single step"""
|
||||
pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_DJANGO])
|
||||
pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_INBUILT])
|
||||
self.stage.password_stage = pw_stage
|
||||
self.stage.save()
|
||||
form_data = {"uid_field": self.user.email, "password": self.password}
|
||||
|
@ -86,7 +86,7 @@ class TestIdentificationStage(TestCase):
|
|||
|
||||
def test_invalid_with_password(self):
|
||||
"""Test with valid email and invalid password in single step"""
|
||||
pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_DJANGO])
|
||||
pw_stage = PasswordStage.objects.create(name="password", backends=[BACKEND_INBUILT])
|
||||
self.stage.password_stage = pw_stage
|
||||
self.stage.save()
|
||||
form_data = {
|
||||
|
|
|
@ -17,7 +17,7 @@ from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
|||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.stages.invitation.models import Invitation, InvitationStage
|
||||
from authentik.stages.invitation.stage import INVITATION_TOKEN_KEY, PLAN_CONTEXT_PROMPT
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
|
||||
|
||||
|
@ -45,7 +45,7 @@ class TestUserLoginStage(TestCase):
|
|||
"""Test without any invitation, continue_flow_without_invitation not set."""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
@ -74,7 +74,7 @@ class TestUserLoginStage(TestCase):
|
|||
self.stage.save()
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
"""Backend paths"""
|
||||
BACKEND_DJANGO = "django.contrib.auth.backends.ModelBackend"
|
||||
BACKEND_INBUILT = "authentik.core.auth.InbuiltBackend"
|
||||
BACKEND_LDAP = "authentik.sources.ldap.auth.LDAPBackend"
|
||||
BACKEND_APP_PASSWORD = "authentik.core.auth.TokenBackend" # nosec
|
||||
|
|
60
authentik/stages/password/migrations/0007_app_password.py
Normal file
60
authentik/stages/password/migrations/0007_app_password.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
# Generated by Django 3.2.6 on 2021-08-23 14:34
|
||||
import django.contrib.postgres.fields
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT
|
||||
|
||||
|
||||
def update_default_backends(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
PasswordStage = apps.get_model("authentik_stages_password", "passwordstage")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
stages = PasswordStage.objects.using(db_alias).filter(name="default-authentication-password")
|
||||
if not stages.exists():
|
||||
return
|
||||
stage = stages.first()
|
||||
stage.backends.append(BACKEND_APP_PASSWORD)
|
||||
stage.save()
|
||||
|
||||
|
||||
def replace_inbuilt(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
PasswordStage = apps.get_model("authentik_stages_password", "passwordstage")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
for stage in PasswordStage.objects.using(db_alias).all():
|
||||
if "django.contrib.auth.backends.ModelBackend" not in stage.backends:
|
||||
continue
|
||||
stage.backends.remove("django.contrib.auth.backends.ModelBackend")
|
||||
stage.backends.append(BACKEND_INBUILT)
|
||||
stage.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_password", "0006_passwordchange_rename"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="passwordstage",
|
||||
name="backends",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(
|
||||
choices=[
|
||||
("authentik.core.auth.InbuiltBackend", "User database + standard password"),
|
||||
("authentik.core.auth.TokenBackend", "User database + app passwords"),
|
||||
(
|
||||
"authentik.sources.ldap.auth.LDAPBackend",
|
||||
"User database + LDAP password",
|
||||
),
|
||||
]
|
||||
),
|
||||
help_text="Selection of backends to test the password against.",
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(update_default_backends),
|
||||
]
|
|
@ -9,19 +9,23 @@ from rest_framework.serializers import BaseSerializer
|
|||
|
||||
from authentik.core.types import UserSettingSerializer
|
||||
from authentik.flows.models import ConfigurableStage, Stage
|
||||
from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP
|
||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
||||
|
||||
|
||||
def get_authentication_backends():
|
||||
"""Return all available authentication backends as tuple set"""
|
||||
return [
|
||||
(
|
||||
BACKEND_DJANGO,
|
||||
_("authentik-internal Userdatabase"),
|
||||
BACKEND_INBUILT,
|
||||
_("User database + standard password"),
|
||||
),
|
||||
(
|
||||
BACKEND_APP_PASSWORD,
|
||||
_("User database + app passwords"),
|
||||
),
|
||||
(
|
||||
BACKEND_LDAP,
|
||||
_("authentik LDAP"),
|
||||
_("User database + LDAP password"),
|
||||
),
|
||||
]
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@ from authentik.stages.password.models import PasswordStage
|
|||
|
||||
LOGGER = get_logger()
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND = "user_backend"
|
||||
PLAN_CONTEXT_METHOD = "auth_method"
|
||||
PLAN_CONTEXT_METHOD_ARGS = "auth_method_args"
|
||||
SESSION_INVALID_TRIES = "user_invalid_tries"
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
|||
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.models import PasswordStage
|
||||
|
||||
MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test"))
|
||||
|
@ -36,7 +36,7 @@ class TestPasswordStage(TestCase):
|
|||
slug="test-password",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
self.stage = PasswordStage.objects.create(name="password", backends=[BACKEND_DJANGO])
|
||||
self.stage = PasswordStage.objects.create(name="password", backends=[BACKEND_INBUILT])
|
||||
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
|
||||
@patch(
|
||||
|
@ -158,7 +158,7 @@ class TestPasswordStage(TestCase):
|
|||
TO_STAGE_RESPONSE_MOCK,
|
||||
)
|
||||
@patch(
|
||||
"django.contrib.auth.backends.ModelBackend.authenticate",
|
||||
"authentik.core.auth.InbuiltBackend.authenticate",
|
||||
MOCK_BACKEND_AUTHENTICATE,
|
||||
)
|
||||
def test_permission_denied(self):
|
||||
|
|
|
@ -8,7 +8,7 @@ from structlog.stdlib import get_logger
|
|||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
@ -26,7 +26,7 @@ class UserLoginStageView(StageView):
|
|||
LOGGER.debug(message)
|
||||
return self.executor.stage_invalid()
|
||||
backend = self.executor.plan.context.get(
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND, BACKEND_DJANGO
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND, BACKEND_INBUILT
|
||||
)
|
||||
login(
|
||||
self.request,
|
||||
|
|
|
@ -9,7 +9,7 @@ from authentik.flows.markers import StageMarker
|
|||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.user_logout.models import UserLogoutStage
|
||||
|
||||
|
@ -34,7 +34,7 @@ class TestUserLogoutStage(TestCase):
|
|||
"""Test with a valid pending user and backend"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_DJANGO
|
||||
plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""Write stage logic"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.db import transaction
|
||||
from django.db.utils import IntegrityError
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
@ -13,7 +12,7 @@ from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnec
|
|||
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.lib.utils.reflection import class_to_path
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
from authentik.stages.user_write.signals import user_write
|
||||
|
@ -42,9 +41,7 @@ class UserWriteStageView(StageView):
|
|||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
|
||||
is_active=not self.executor.current_stage.create_users_as_inactive
|
||||
)
|
||||
self.executor.plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = class_to_path(
|
||||
ModelBackend
|
||||
)
|
||||
self.executor.plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT
|
||||
LOGGER.debug(
|
||||
"Created new user",
|
||||
flow_slug=self.executor.flow.slug,
|
||||
|
|
|
@ -2515,6 +2515,7 @@ paths:
|
|||
type: string
|
||||
enum:
|
||||
- api
|
||||
- app_password
|
||||
- recovery
|
||||
- verification
|
||||
- name: ordering
|
||||
|
@ -20381,7 +20382,8 @@ components:
|
|||
- url
|
||||
BackendsEnum:
|
||||
enum:
|
||||
- django.contrib.auth.backends.ModelBackend
|
||||
- authentik.core.auth.InbuiltBackend
|
||||
- authentik.core.auth.TokenBackend
|
||||
- authentik.sources.ldap.auth.LDAPBackend
|
||||
type: string
|
||||
BindingTypeEnum:
|
||||
|
@ -22211,6 +22213,7 @@ components:
|
|||
- verification
|
||||
- api
|
||||
- recovery
|
||||
- app_password
|
||||
type: string
|
||||
InvalidResponseActionEnum:
|
||||
enum:
|
||||
|
|
|
@ -3,7 +3,7 @@ version: '3.7'
|
|||
services:
|
||||
postgresql:
|
||||
container_name: postgres
|
||||
image: library/postgres:11
|
||||
image: library/postgres:12
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
|
|
|
@ -3,7 +3,7 @@ version: '3.7'
|
|||
services:
|
||||
postgresql:
|
||||
container_name: postgres
|
||||
image: library/postgres:11
|
||||
image: library/postgres:12
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
|
|
|
@ -4,5 +4,6 @@ from yaml import safe_dump
|
|||
|
||||
with open("local.env.yml", "w") as _config:
|
||||
safe_dump({
|
||||
"secret_key": generate_id()
|
||||
"log_level": "debug",
|
||||
"secret_key": generate_id(),
|
||||
}, _config, default_flow_style=False)
|
||||
|
|
|
@ -90,6 +90,7 @@ export default [
|
|||
// Main Application
|
||||
{
|
||||
input: "./src/interfaces/AdminInterface.ts",
|
||||
context: "window",
|
||||
output: [
|
||||
{
|
||||
format: "es",
|
||||
|
@ -122,6 +123,7 @@ export default [
|
|||
// Flow executor
|
||||
{
|
||||
input: "./src/interfaces/FlowInterface.ts",
|
||||
context: "window",
|
||||
output: [
|
||||
{
|
||||
format: "es",
|
||||
|
|
|
@ -63,6 +63,10 @@ msgstr "ANY, any policy must match to grant access."
|
|||
msgid "ANY, any policy must match to include this stage access."
|
||||
msgstr "ANY, any policy must match to include this stage access."
|
||||
|
||||
#: src/pages/tokens/TokenListPage.ts
|
||||
msgid "API Access"
|
||||
msgstr "API Access"
|
||||
|
||||
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
|
||||
msgid "API Hostname"
|
||||
msgstr "API Hostname"
|
||||
|
@ -231,6 +235,10 @@ msgstr "Always require consent"
|
|||
msgid "App"
|
||||
msgstr "App"
|
||||
|
||||
#: src/pages/tokens/TokenListPage.ts
|
||||
msgid "App password"
|
||||
msgstr "App password"
|
||||
|
||||
#: src/elements/user/UserConsentList.ts
|
||||
#: src/pages/admin-overview/TopApplicationsTable.ts
|
||||
#: src/pages/providers/ProviderListPage.ts
|
||||
|
@ -952,6 +960,11 @@ msgstr "Copy recovery link"
|
|||
msgid "Create"
|
||||
msgstr "Create"
|
||||
|
||||
#: src/pages/user-settings/tokens/UserTokenList.ts
|
||||
#: src/pages/user-settings/tokens/UserTokenList.ts
|
||||
msgid "Create App password"
|
||||
msgstr "Create App password"
|
||||
|
||||
#: src/pages/applications/ApplicationListPage.ts
|
||||
#: src/pages/providers/RelatedApplicationButton.ts
|
||||
msgid "Create Application"
|
||||
|
@ -1018,6 +1031,7 @@ msgstr "Create Stage binding"
|
|||
msgid "Create Tenant"
|
||||
msgstr "Create Tenant"
|
||||
|
||||
#: src/pages/user-settings/tokens/UserTokenList.ts
|
||||
#: src/pages/user-settings/tokens/UserTokenList.ts
|
||||
msgid "Create Token"
|
||||
msgstr "Create Token"
|
||||
|
@ -2047,6 +2061,11 @@ msgstr "Integration key"
|
|||
msgid "Integrations"
|
||||
msgstr "Integrations"
|
||||
|
||||
#: src/pages/tokens/TokenListPage.ts
|
||||
#: src/pages/user-settings/tokens/UserTokenList.ts
|
||||
msgid "Intent"
|
||||
msgstr "Intent"
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
|
||||
msgid "Internal Host"
|
||||
msgstr "Internal Host"
|
||||
|
@ -2807,6 +2826,7 @@ msgstr "Optionally set this to your parent domain, if you want authentication an
|
|||
#: src/pages/flows/BoundStagesList.ts
|
||||
#: src/pages/flows/StageBindingForm.ts
|
||||
#: src/pages/policies/BoundPoliciesList.ts
|
||||
#: src/pages/policies/BoundPoliciesList.ts
|
||||
#: src/pages/policies/PolicyBindingForm.ts
|
||||
#: src/pages/stages/prompt/PromptForm.ts
|
||||
#: src/pages/stages/prompt/PromptListPage.ts
|
||||
|
@ -2954,6 +2974,7 @@ msgstr "Policy / Group / User Bindings"
|
|||
msgid "Policy / Policies"
|
||||
msgstr "Policy / Policies"
|
||||
|
||||
#: src/pages/policies/BoundPoliciesList.ts
|
||||
#: src/pages/policies/BoundPoliciesList.ts
|
||||
msgid "Policy / User / Group"
|
||||
msgstr "Policy / User / Group"
|
||||
|
@ -3203,6 +3224,7 @@ msgid "Receive a push notification on your phone to prove your identity."
|
|||
msgstr "Receive a push notification on your phone to prove your identity."
|
||||
|
||||
#: src/pages/flows/FlowForm.ts
|
||||
#: src/pages/tokens/TokenListPage.ts
|
||||
#: src/pages/users/UserListPage.ts
|
||||
msgid "Recovery"
|
||||
msgstr "Recovery"
|
||||
|
@ -3696,6 +3718,7 @@ msgstr "Sources"
|
|||
msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins"
|
||||
msgstr "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins"
|
||||
|
||||
#: src/pages/flows/BoundStagesList.ts
|
||||
#: src/pages/flows/StageBindingForm.ts
|
||||
msgid "Stage"
|
||||
msgstr "Stage"
|
||||
|
@ -3716,6 +3739,10 @@ msgstr "Stage Configuration"
|
|||
msgid "Stage binding(s)"
|
||||
msgstr "Stage binding(s)"
|
||||
|
||||
#: src/pages/flows/BoundStagesList.ts
|
||||
msgid "Stage type"
|
||||
msgstr "Stage type"
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
msgstr "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
|
@ -4388,10 +4415,13 @@ msgstr "Token(s)"
|
|||
#: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts
|
||||
#: src/interfaces/AdminInterface.ts
|
||||
#: src/pages/tokens/TokenListPage.ts
|
||||
#: src/pages/user-settings/UserSettingsPage.ts
|
||||
msgid "Tokens"
|
||||
msgstr "Tokens"
|
||||
|
||||
#: src/pages/user-settings/UserSettingsPage.ts
|
||||
msgid "Tokens and App passwords"
|
||||
msgstr "Tokens and App passwords"
|
||||
|
||||
#: src/pages/tokens/TokenListPage.ts
|
||||
msgid "Tokens are used throughout authentik for Email validation stages, Recovery keys and API access."
|
||||
msgstr "Tokens are used throughout authentik for Email validation stages, Recovery keys and API access."
|
||||
|
@ -4740,6 +4770,18 @@ msgstr "User Reputation"
|
|||
msgid "User Settings"
|
||||
msgstr "User Settings"
|
||||
|
||||
#: src/pages/stages/password/PasswordStageForm.ts
|
||||
msgid "User database + LDAP password"
|
||||
msgstr "User database + LDAP password"
|
||||
|
||||
#: src/pages/stages/password/PasswordStageForm.ts
|
||||
msgid "User database + app passwords"
|
||||
msgstr "User database + app passwords"
|
||||
|
||||
#: src/pages/stages/password/PasswordStageForm.ts
|
||||
msgid "User database + standard password"
|
||||
msgstr "User database + standard password"
|
||||
|
||||
#: src/pages/user-settings/UserSettingsPage.ts
|
||||
msgid "User details"
|
||||
msgstr "User details"
|
||||
|
@ -4859,6 +4901,10 @@ msgstr "Validation Policies"
|
|||
msgid "Validity days"
|
||||
msgstr "Validity days"
|
||||
|
||||
#: src/pages/tokens/TokenListPage.ts
|
||||
msgid "Verification"
|
||||
msgstr "Verification"
|
||||
|
||||
#: src/pages/providers/saml/SAMLProviderForm.ts
|
||||
msgid "Verification Certificate"
|
||||
msgstr "Verification Certificate"
|
||||
|
@ -5022,13 +5068,13 @@ msgstr "You can only select providers that match the type of the outpost."
|
|||
msgid "You're currently impersonating {0}. Click to stop."
|
||||
msgstr "You're currently impersonating {0}. Click to stop."
|
||||
|
||||
#: src/pages/stages/password/PasswordStageForm.ts
|
||||
msgid "authentik Builtin Database"
|
||||
msgstr "authentik Builtin Database"
|
||||
#:
|
||||
#~ msgid "authentik Builtin Database"
|
||||
#~ msgstr "authentik Builtin Database"
|
||||
|
||||
#: src/pages/stages/password/PasswordStageForm.ts
|
||||
msgid "authentik LDAP Backend"
|
||||
msgstr "authentik LDAP Backend"
|
||||
#:
|
||||
#~ msgid "authentik LDAP Backend"
|
||||
#~ msgstr "authentik LDAP Backend"
|
||||
|
||||
#: src/elements/forms/DeleteForm.ts
|
||||
msgid "connecting object will be deleted"
|
||||
|
|
|
@ -63,6 +63,10 @@ msgstr ""
|
|||
msgid "ANY, any policy must match to include this stage access."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/tokens/TokenListPage.ts
|
||||
msgid "API Access"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
|
||||
msgid "API Hostname"
|
||||
msgstr ""
|
||||
|
@ -231,6 +235,10 @@ msgstr ""
|
|||
msgid "App"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/tokens/TokenListPage.ts
|
||||
msgid "App password"
|
||||
msgstr ""
|
||||
|
||||
#: src/elements/user/UserConsentList.ts
|
||||
#: src/pages/admin-overview/TopApplicationsTable.ts
|
||||
#: src/pages/providers/ProviderListPage.ts
|
||||
|
@ -946,6 +954,11 @@ msgstr ""
|
|||
msgid "Create"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/user-settings/tokens/UserTokenList.ts
|
||||
#: src/pages/user-settings/tokens/UserTokenList.ts
|
||||
msgid "Create App password"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/applications/ApplicationListPage.ts
|
||||
#: src/pages/providers/RelatedApplicationButton.ts
|
||||
msgid "Create Application"
|
||||
|
@ -1012,6 +1025,7 @@ msgstr ""
|
|||
msgid "Create Tenant"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/user-settings/tokens/UserTokenList.ts
|
||||
#: src/pages/user-settings/tokens/UserTokenList.ts
|
||||
msgid "Create Token"
|
||||
msgstr ""
|
||||
|
@ -2039,6 +2053,11 @@ msgstr ""
|
|||
msgid "Integrations"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/tokens/TokenListPage.ts
|
||||
#: src/pages/user-settings/tokens/UserTokenList.ts
|
||||
msgid "Intent"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
|
||||
msgid "Internal Host"
|
||||
msgstr ""
|
||||
|
@ -2799,6 +2818,7 @@ msgstr ""
|
|||
#: src/pages/flows/BoundStagesList.ts
|
||||
#: src/pages/flows/StageBindingForm.ts
|
||||
#: src/pages/policies/BoundPoliciesList.ts
|
||||
#: src/pages/policies/BoundPoliciesList.ts
|
||||
#: src/pages/policies/PolicyBindingForm.ts
|
||||
#: src/pages/stages/prompt/PromptForm.ts
|
||||
#: src/pages/stages/prompt/PromptListPage.ts
|
||||
|
@ -2946,6 +2966,7 @@ msgstr ""
|
|||
msgid "Policy / Policies"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/policies/BoundPoliciesList.ts
|
||||
#: src/pages/policies/BoundPoliciesList.ts
|
||||
msgid "Policy / User / Group"
|
||||
msgstr ""
|
||||
|
@ -3195,6 +3216,7 @@ msgid "Receive a push notification on your phone to prove your identity."
|
|||
msgstr ""
|
||||
|
||||
#: src/pages/flows/FlowForm.ts
|
||||
#: src/pages/tokens/TokenListPage.ts
|
||||
#: src/pages/users/UserListPage.ts
|
||||
msgid "Recovery"
|
||||
msgstr ""
|
||||
|
@ -3688,6 +3710,7 @@ msgstr ""
|
|||
msgid "Sources of identities, which can either be synced into authentik's database, like LDAP, or can be used by users to authenticate and enroll themselves, like OAuth and social logins"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/BoundStagesList.ts
|
||||
#: src/pages/flows/StageBindingForm.ts
|
||||
msgid "Stage"
|
||||
msgstr ""
|
||||
|
@ -3708,6 +3731,10 @@ msgstr ""
|
|||
msgid "Stage binding(s)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/BoundStagesList.ts
|
||||
msgid "Stage type"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
|
||||
msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again."
|
||||
msgstr ""
|
||||
|
@ -4373,10 +4400,13 @@ msgstr ""
|
|||
#: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts
|
||||
#: src/interfaces/AdminInterface.ts
|
||||
#: src/pages/tokens/TokenListPage.ts
|
||||
#: src/pages/user-settings/UserSettingsPage.ts
|
||||
msgid "Tokens"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/user-settings/UserSettingsPage.ts
|
||||
msgid "Tokens and App passwords"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/tokens/TokenListPage.ts
|
||||
msgid "Tokens are used throughout authentik for Email validation stages, Recovery keys and API access."
|
||||
msgstr ""
|
||||
|
@ -4725,6 +4755,18 @@ msgstr ""
|
|||
msgid "User Settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/password/PasswordStageForm.ts
|
||||
msgid "User database + LDAP password"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/password/PasswordStageForm.ts
|
||||
msgid "User database + app passwords"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/password/PasswordStageForm.ts
|
||||
msgid "User database + standard password"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/user-settings/UserSettingsPage.ts
|
||||
msgid "User details"
|
||||
msgstr ""
|
||||
|
@ -4844,6 +4886,10 @@ msgstr ""
|
|||
msgid "Validity days"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/tokens/TokenListPage.ts
|
||||
msgid "Verification"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/providers/saml/SAMLProviderForm.ts
|
||||
msgid "Verification Certificate"
|
||||
msgstr ""
|
||||
|
@ -5005,13 +5051,13 @@ msgstr ""
|
|||
msgid "You're currently impersonating {0}. Click to stop."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/password/PasswordStageForm.ts
|
||||
msgid "authentik Builtin Database"
|
||||
msgstr ""
|
||||
#:
|
||||
#~ msgid "authentik Builtin Database"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/pages/stages/password/PasswordStageForm.ts
|
||||
msgid "authentik LDAP Backend"
|
||||
msgstr ""
|
||||
#:
|
||||
#~ msgid "authentik LDAP Backend"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/elements/forms/DeleteForm.ts
|
||||
msgid "connecting object will be deleted"
|
||||
|
|
|
@ -48,6 +48,12 @@ export class BoundStagesList extends Table<FlowStageBinding> {
|
|||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${t`Stage binding(s)`}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: FlowStageBinding) => {
|
||||
return [
|
||||
{ key: t`Stage`, value: item.stageObj?.name },
|
||||
{ key: t`Stage type`, value: item.stageObj?.verboseName },
|
||||
];
|
||||
}}
|
||||
.usedBy=${(item: FlowStageBinding) => {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsBindingsUsedByList({
|
||||
fsbUuid: item.pk,
|
||||
|
|
|
@ -100,6 +100,12 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
|
|||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${t`Policy binding(s)`}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: PolicyBinding) => {
|
||||
return [
|
||||
{ key: t`Order`, value: item.order.toString() },
|
||||
{ key: t`Policy / User / Group`, value: this.getPolicyUserGroupRow(item) },
|
||||
];
|
||||
}}
|
||||
.usedBy=${(item: PolicyBinding) => {
|
||||
return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsUsedByList({
|
||||
policyBindingUuid: item.pk,
|
||||
|
|
|
@ -156,11 +156,20 @@ export class IdentificationStageForm extends ModelForm<IdentificationStage, stri
|
|||
.sourcesAllList({})
|
||||
.then((sources) => {
|
||||
return sources.results.map((source) => {
|
||||
const selected = Array.from(
|
||||
let selected = Array.from(
|
||||
this.instance?.sources || [],
|
||||
).some((su) => {
|
||||
return su == source.pk;
|
||||
});
|
||||
// Creating a new instance, auto-select built-in source
|
||||
// Only when no other sources exist
|
||||
if (
|
||||
!this.instance &&
|
||||
source.component === "" &&
|
||||
sources.results.length < 2
|
||||
) {
|
||||
selected = true;
|
||||
}
|
||||
return html`<option
|
||||
value=${ifDefined(source.pk)}
|
||||
?selected=${selected}
|
||||
|
|
|
@ -46,8 +46,11 @@ export class PasswordStageForm extends ModelForm<PasswordStage, string> {
|
|||
};
|
||||
|
||||
isBackendSelected(field: BackendsEnum): boolean {
|
||||
if (!this.instance) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
(this.instance?.backends || []).filter((isField) => {
|
||||
this.instance.backends.filter((isField) => {
|
||||
return field === isField;
|
||||
}).length > 0
|
||||
);
|
||||
|
@ -76,20 +79,28 @@ export class PasswordStageForm extends ModelForm<PasswordStage, string> {
|
|||
>
|
||||
<select name="users" class="pf-c-form-control" multiple>
|
||||
<option
|
||||
value=${BackendsEnum.DjangoContribAuthBackendsModelBackend}
|
||||
value=${BackendsEnum.CoreAuthInbuiltBackend}
|
||||
?selected=${this.isBackendSelected(
|
||||
BackendsEnum.DjangoContribAuthBackendsModelBackend,
|
||||
BackendsEnum.CoreAuthInbuiltBackend,
|
||||
)}
|
||||
>
|
||||
${t`authentik Builtin Database`}
|
||||
${t`User database + standard password`}
|
||||
</option>
|
||||
<option
|
||||
value=${BackendsEnum.AuthentikSourcesLdapAuthLdapBackend}
|
||||
value=${BackendsEnum.CoreAuthTokenBackend}
|
||||
?selected=${this.isBackendSelected(
|
||||
BackendsEnum.AuthentikSourcesLdapAuthLdapBackend,
|
||||
BackendsEnum.CoreAuthTokenBackend,
|
||||
)}
|
||||
>
|
||||
${t`authentik LDAP Backend`}
|
||||
${t`User database + app passwords`}
|
||||
</option>
|
||||
<option
|
||||
value=${BackendsEnum.SourcesLdapAuthLdapBackend}
|
||||
?selected=${this.isBackendSelected(
|
||||
BackendsEnum.SourcesLdapAuthLdapBackend,
|
||||
)}
|
||||
>
|
||||
${t`User database + LDAP password`}
|
||||
</option>
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
|
|
|
@ -8,9 +8,22 @@ import "../../elements/buttons/TokenCopyButton";
|
|||
import "../../elements/forms/DeleteBulkForm";
|
||||
import { TableColumn } from "../../elements/table/Table";
|
||||
import { PAGE_SIZE } from "../../constants";
|
||||
import { CoreApi, Token } from "@goauthentik/api";
|
||||
import { CoreApi, IntentEnum, Token } from "@goauthentik/api";
|
||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||
|
||||
export function IntentToLabel(intent: IntentEnum): string {
|
||||
switch (intent) {
|
||||
case IntentEnum.Api:
|
||||
return t`API Access`;
|
||||
case IntentEnum.AppPassword:
|
||||
return t`App password`;
|
||||
case IntentEnum.Recovery:
|
||||
return t`Recovery`;
|
||||
case IntentEnum.Verification:
|
||||
return t`Verification`;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ak-token-list")
|
||||
export class TokenListPage extends TablePage<Token> {
|
||||
searchEnabled(): boolean {
|
||||
|
@ -46,6 +59,7 @@ export class TokenListPage extends TablePage<Token> {
|
|||
new TableColumn(t`User`, "user"),
|
||||
new TableColumn(t`Expires?`, "expiring"),
|
||||
new TableColumn(t`Expiry date`, "expires"),
|
||||
new TableColumn(t`Intent`, "intent"),
|
||||
new TableColumn(t`Actions`),
|
||||
];
|
||||
}
|
||||
|
@ -78,6 +92,7 @@ export class TokenListPage extends TablePage<Token> {
|
|||
html`${item.user?.username}`,
|
||||
html`${item.expiring ? t`Yes` : t`No`}`,
|
||||
html`${item.expiring ? item.expires?.toLocaleString() : "-"}`,
|
||||
html`${IntentToLabel(item.intent || IntentEnum.Api)}`,
|
||||
html`
|
||||
<ak-token-copy-button identifier="${item.identifier}">
|
||||
${t`Copy Key`}
|
||||
|
|
|
@ -148,7 +148,7 @@ export class UserSettingsPage extends LitElement {
|
|||
</section>
|
||||
<section
|
||||
slot="page-tokens"
|
||||
data-tab-title="${t`Tokens`}"
|
||||
data-tab-title="${t`Tokens and App passwords`}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<ak-user-token-list></ak-user-token-list>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { CoreApi, Token } from "@goauthentik/api";
|
||||
import { CoreApi, IntentEnum, Token } from "@goauthentik/api";
|
||||
import { t } from "@lingui/macro";
|
||||
import { customElement } from "lit-element";
|
||||
import { customElement, property } from "lit-element";
|
||||
import { html, TemplateResult } from "lit-html";
|
||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
@ -9,6 +9,9 @@ import { ModelForm } from "../../../elements/forms/ModelForm";
|
|||
|
||||
@customElement("ak-user-token-form")
|
||||
export class UserTokenForm extends ModelForm<Token, string> {
|
||||
@property()
|
||||
intent: IntentEnum = IntentEnum.Api;
|
||||
|
||||
loadInstance(pk: string): Promise<Token> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreTokensRetrieve({
|
||||
identifier: pk,
|
||||
|
@ -30,6 +33,7 @@ export class UserTokenForm extends ModelForm<Token, string> {
|
|||
tokenRequest: data,
|
||||
});
|
||||
} else {
|
||||
data.intent = this.intent;
|
||||
return new CoreApi(DEFAULT_CONFIG).coreTokensCreate({
|
||||
tokenRequest: data,
|
||||
});
|
||||
|
|
|
@ -10,9 +10,10 @@ import "../../../elements/buttons/Dropdown";
|
|||
import "../../../elements/buttons/TokenCopyButton";
|
||||
import { Table, TableColumn } from "../../../elements/table/Table";
|
||||
import { PAGE_SIZE } from "../../../constants";
|
||||
import { CoreApi, Token } from "@goauthentik/api";
|
||||
import { CoreApi, IntentEnum, Token } from "@goauthentik/api";
|
||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||
import "./UserTokenForm";
|
||||
import { IntentToLabel } from "../../tokens/TokenListPage";
|
||||
|
||||
@customElement("ak-user-token-list")
|
||||
export class UserTokenList extends Table<Token> {
|
||||
|
@ -48,8 +49,19 @@ export class UserTokenList extends Table<Token> {
|
|||
<ak-forms-modal>
|
||||
<span slot="submit"> ${t`Create`} </span>
|
||||
<span slot="header"> ${t`Create Token`} </span>
|
||||
<ak-user-token-form slot="form"> </ak-user-token-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${t`Create`}</button>
|
||||
<ak-user-token-form intent=${IntentEnum.Api} slot="form"> </ak-user-token-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${t`Create Token`}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${t`Create`} </span>
|
||||
<span slot="header"> ${t`Create App password`} </span>
|
||||
<ak-user-token-form intent=${IntentEnum.AppPassword} slot="form">
|
||||
</ak-user-token-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${t`Create App password`}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
${super.renderToolbar()}
|
||||
`;
|
||||
|
@ -89,6 +101,16 @@ export class UserTokenList extends Table<Token> {
|
|||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${t`Intent`}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${IntentToLabel(item.intent || IntentEnum.Api)}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
@ -59,3 +59,31 @@ This includes the following:
|
|||
- `prompt_data`: Data which has been saved from a prompt stage or an external source.
|
||||
- `application`: The application the user is in the process of authorizing.
|
||||
- `pending_user`: The currently pending user, see [User](/docs/expressions/reference/user-object)
|
||||
- `auth_method`: Authentication method set (this value is set by password stages)
|
||||
|
||||
Depending on method, `auth_method_args` is also set.
|
||||
|
||||
Can be any of:
|
||||
|
||||
- `password`: Standard password login
|
||||
- `app_password`: App passowrd (token)
|
||||
|
||||
Sets `auth_method_args` to
|
||||
```json
|
||||
{
|
||||
"token": {
|
||||
"pk": "f6d639aac81940f38dcfdc6e0fe2a786",
|
||||
"app": "authentik_core",
|
||||
"name": "test (expires=2021-08-23 15:45:54.725880+00:00)",
|
||||
"model_name": "token"
|
||||
}
|
||||
}
|
||||
```
|
||||
- `ldap`: LDAP bind authentication
|
||||
|
||||
Sets `auth_method_args` to
|
||||
```json
|
||||
{
|
||||
"source": {} // Information about the source used
|
||||
}
|
||||
```
|
||||
|
|
|
@ -133,7 +133,7 @@
|
|||
"pk": "34e1e7d5-8eed-4549-bc7a-305069ff7df0",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "20375f30-7fa7-4562-8f6e-0f61889f2963",
|
||||
"order": 0
|
||||
"order": 10
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -145,7 +145,7 @@
|
|||
"pk": "e40467a6-3052-488c-a1b5-1ad7a80fe7b3",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "6c342b94-790d-425a-ae31-6196b6570722",
|
||||
"order": 1
|
||||
"order": 11
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -157,7 +157,7 @@
|
|||
"pk": "76bc594e-2715-49ab-bd40-994abd9a7b70",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "a4090add-f483-4ac6-8917-10b493ef843e",
|
||||
"order": 2
|
||||
"order": 20
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -169,7 +169,7 @@
|
|||
"pk": "2f324f6d-7646-4108-a6e2-e7f90985477f",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "77090897-eb3f-40db-81e6-b4074b1998c4",
|
||||
"order": 3
|
||||
"order": 100
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
|
|
@ -154,7 +154,7 @@
|
|||
"pk": "34e1e7d5-8eed-4549-bc7a-305069ff7df0",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "20375f30-7fa7-4562-8f6e-0f61889f2963",
|
||||
"order": 0
|
||||
"order": 10
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -166,7 +166,7 @@
|
|||
"pk": "e40467a6-3052-488c-a1b5-1ad7a80fe7b3",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "6c342b94-790d-425a-ae31-6196b6570722",
|
||||
"order": 1
|
||||
"order": 11
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -178,7 +178,7 @@
|
|||
"pk": "76bc594e-2715-49ab-bd40-994abd9a7b70",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "a4090add-f483-4ac6-8917-10b493ef843e",
|
||||
"order": 2
|
||||
"order": 20
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -190,7 +190,7 @@
|
|||
"pk": "1db34a14-8985-4184-b5c9-254cd585d94f",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "096e6282-6b30-4695-bd03-3b143eab5580",
|
||||
"order": 3
|
||||
"order": 30
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -202,7 +202,7 @@
|
|||
"pk": "2f324f6d-7646-4108-a6e2-e7f90985477f",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "77090897-eb3f-40db-81e6-b4074b1998c4",
|
||||
"order": 4
|
||||
"order": 40
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
|
|
@ -13,6 +13,18 @@
|
|||
"designation": "authentication"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "7db93f1e-788b-4af6-8dc6-5cdeb59d8be7"
|
||||
},
|
||||
"model": "authentik_policies_expression.expressionpolicy",
|
||||
"attrs": {
|
||||
"name": "test-not-app-password",
|
||||
"execution_logging": false,
|
||||
"bound_to": 1,
|
||||
"expression": "return auth_method != \"app_password\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "69d41125-3987-499b-8d74-ef27b54b88c8",
|
||||
|
@ -39,7 +51,7 @@
|
|||
{
|
||||
"identifiers": {
|
||||
"pk": "37f709c3-8817-45e8-9a93-80a925d293c2",
|
||||
"name": "default-authentication-flow-totp"
|
||||
"name": "default-authentication-flow-mfa"
|
||||
},
|
||||
"model": "authentik_stages_authenticator_validate.AuthenticatorValidateStage",
|
||||
"attrs": {}
|
||||
|
@ -52,7 +64,8 @@
|
|||
"model": "authentik_stages_password.passwordstage",
|
||||
"attrs": {
|
||||
"backends": [
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
"authentik.core.auth.InbuiltBackend",
|
||||
"authentik.core.auth.TokenBackend",
|
||||
"authentik.sources.ldap.auth.LDAPBackend"
|
||||
]
|
||||
}
|
||||
|
@ -62,7 +75,7 @@
|
|||
"pk": "a3056482-b692-4e3a-93f1-7351c6a351c7",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "5f594f27-0def-488d-9855-fe604eb13de5",
|
||||
"order": 0
|
||||
"order": 10
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -74,7 +87,7 @@
|
|||
"pk": "4e8538cf-3e18-4a68-82ae-6df6725fa2e6",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "d8affa62-500c-4c5c-a01f-5835e1ffdf40",
|
||||
"order": 1
|
||||
"order": 20
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -86,7 +99,22 @@
|
|||
"pk": "688aec6f-5622-42c6-83a5-d22072d7e798",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "37f709c3-8817-45e8-9a93-80a925d293c2",
|
||||
"order": 2
|
||||
"order": 30
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"evaluate_on_plan": false,
|
||||
"re_evaluate_policies": true,
|
||||
"policy_engine_mode": "any",
|
||||
"invalid_response_action": "retry"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "f3fede3a-a9b5-4232-9ec7-be7ff4194b27",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "69d41125-3987-499b-8d74-ef27b54b88c8",
|
||||
"order": 100
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -95,14 +123,16 @@
|
|||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "f3fede3a-a9b5-4232-9ec7-be7ff4194b27",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "69d41125-3987-499b-8d74-ef27b54b88c8",
|
||||
"order": 3
|
||||
"pk": "6e40ae4d-a4ed-4bd7-a784-27b1fe5859d2",
|
||||
"policy": "7db93f1e-788b-4af6-8dc6-5cdeb59d8be7",
|
||||
"target": "688aec6f-5622-42c6-83a5-d22072d7e798",
|
||||
"order": 0
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"model": "authentik_policies.policybinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
"negate": false,
|
||||
"enabled": true,
|
||||
"timeout": 30
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -55,7 +55,8 @@
|
|||
"model": "authentik_stages_password.passwordstage",
|
||||
"attrs": {
|
||||
"backends": [
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
"authentik.core.auth.InbuiltBackend",
|
||||
"authentik.core.auth.TokenBackend",
|
||||
"authentik.sources.ldap.auth.LDAPBackend"
|
||||
]
|
||||
}
|
||||
|
@ -65,7 +66,7 @@
|
|||
"pk": "a3056482-b692-4e3a-93f1-7351c6a351c7",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "5f594f27-0def-488d-9855-fe604eb13de5",
|
||||
"order": 0
|
||||
"order": 10
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -77,7 +78,7 @@
|
|||
"pk": "4e8538cf-3e18-4a68-82ae-6df6725fa2e6",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "d8affa62-500c-4c5c-a01f-5835e1ffdf40",
|
||||
"order": 1
|
||||
"order": 20
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -89,7 +90,7 @@
|
|||
"pk": "3bcd6af0-48a6-4e18-87f3-d251a1a58226",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "a368cafc-1494-45e9-b75b-b5e7ac2bd3e4",
|
||||
"order": 2
|
||||
"order": 30
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -102,7 +103,7 @@
|
|||
"pk": "f3fede3a-a9b5-4232-9ec7-be7ff4194b27",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "69d41125-3987-499b-8d74-ef27b54b88c8",
|
||||
"order": 3
|
||||
"order": 100
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
|
|
@ -123,7 +123,7 @@
|
|||
"pk": "7af7558e-2196-4b9f-a08e-d38420b7cfbb",
|
||||
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
|
||||
"stage": "e54045a7-6ecb-4ad9-ad37-28e72d8e565e",
|
||||
"order": 0
|
||||
"order": 10
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -135,7 +135,7 @@
|
|||
"pk": "29446fd6-dd93-4e92-9830-2d81debad5ae",
|
||||
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
|
||||
"stage": "66f948dc-3f74-42b2-b26b-b8b9df109efb",
|
||||
"order": 1
|
||||
"order": 20
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -147,7 +147,7 @@
|
|||
"pk": "1219d06e-2c06-4c5b-a162-78e3959c6cf0",
|
||||
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
|
||||
"stage": "975d5502-1e22-4d10-b560-fbc5bd70ff4d",
|
||||
"order": 2
|
||||
"order": 30
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -159,7 +159,7 @@
|
|||
"pk": "66de86ba-0707-46a0-8475-ff2e260d6935",
|
||||
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
|
||||
"stage": "3909fd60-b013-4668-8806-12e9507dab97",
|
||||
"order": 3
|
||||
"order": 40
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
@ -171,7 +171,7 @@
|
|||
"pk": "9cec2334-d4a2-4895-a2b2-bc5ae4e9639a",
|
||||
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
|
||||
"stage": "fcdd4206-0d35-4ad2-a59f-5a72422936bb",
|
||||
"order": 4
|
||||
"order": 100
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"pk": "eb9aff2b-b95d-40b3-ad08-233aa77bbcf3",
|
||||
"target": "59a576ce-2f23-4a63-b63a-d18dc7e550f5",
|
||||
"stage": "c62ac2a4-2735-4a0f-abd0-8523d68c1209",
|
||||
"order": 0
|
||||
"order": 10
|
||||
},
|
||||
"model": "authentik_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
|
|
Reference in a new issue