core: use custom inbuilt backend, set backend login information in flow plan for events
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
2655768f5a
commit
69a0153619
56
authentik/core/auth.py
Normal file
56
authentik/core/auth.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
"""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
|
||||
# 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] = "password"
|
||||
request.session[SESSION_KEY_PLAN] = flow_plan
|
||||
return user
|
||||
|
||||
|
||||
class TokenBackend(ModelBackend):
|
||||
"""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()
|
||||
# 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] = "app_password"
|
||||
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS] = {"token": token}
|
||||
request.session[SESSION_KEY_PLAN] = flow_plan
|
||||
return token.user
|
|
@ -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,29 +0,0 @@
|
|||
"""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
|
||||
|
||||
|
||||
class TokenBackend(ModelBackend):
|
||||
"""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
|
||||
return tokens.first().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
|
||||
|
||||
|
||||
|
@ -47,6 +48,10 @@ def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
|
|||
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]
|
||||
if PLAN_CONTEXT_METHOD in flow_plan.context:
|
||||
thread.kwargs["method"] = flow_plan.context[PLAN_CONTEXT_METHOD]
|
||||
# Save the login method used
|
||||
thread.kwargs["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_APP_PASSWORD, 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, BACKEND_APP_PASSWORD]},
|
||||
defaults={"backends": [BACKEND_INBUILT, BACKEND_LDAP, BACKEND_APP_PASSWORD]},
|
||||
)
|
||||
|
||||
login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
@ -7,7 +7,10 @@ from django.http import HttpRequest
|
|||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.sources.ldap.models import LDAPSource
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
|
||||
LOGGER = get_logger()
|
||||
LDAP_DISTINGUISHED_NAME = "distinguishedName"
|
||||
|
@ -24,6 +27,13 @@ class LDAPBackend(ModelBackend):
|
|||
LOGGER.debug("LDAP Auth attempt", source=source)
|
||||
user = self.auth_user(source, **kwargs)
|
||||
if user:
|
||||
# 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] = "ldap"
|
||||
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS] = {"source": source}
|
||||
request.session[SESSION_KEY_PLAN] = flow_plan
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -9,7 +9,7 @@ from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
|||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
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,4 +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.token_auth.TokenBackend" # nosec
|
||||
BACKEND_APP_PASSWORD = "authentik.core.auth.TokenBackend" # nosec
|
||||
|
|
|
@ -4,7 +4,7 @@ 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
|
||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT
|
||||
|
||||
|
||||
def update_default_backends(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
|
@ -19,6 +19,18 @@ def update_default_backends(apps: Apps, schema_editor: BaseDatabaseSchemaEditor)
|
|||
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 = [
|
||||
|
@ -33,11 +45,8 @@ class Migration(migrations.Migration):
|
|||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(
|
||||
choices=[
|
||||
(
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
"User database + standard password",
|
||||
),
|
||||
("authentik.core.token_auth.TokenBackend", "User database + app passwords"),
|
||||
("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",
|
||||
|
|
|
@ -9,14 +9,14 @@ 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_APP_PASSWORD, 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,
|
||||
BACKEND_INBUILT,
|
||||
_("User database + standard password"),
|
||||
),
|
||||
(
|
||||
|
|
|
@ -27,6 +27,8 @@ from authentik.stages.password.models import PasswordStage
|
|||
|
||||
LOGGER = get_logger()
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND = "user_backend"
|
||||
PLAN_CONTEXT_METHOD = "method"
|
||||
PLAN_CONTEXT_METHOD_ARGS = "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.providers.oauth2.generators import generate_client_secret
|
||||
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(
|
||||
|
|
|
@ -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()
|
||||
|
|
Reference in a new issue