diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..90277c2e6 --- /dev/null +++ b/.vscode/settings.json @@ -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" + } +} diff --git a/authentik/core/api/tokens.py b/authentik/core/api/tokens.py index e8268ebe8..c4f8e0b7c 100644 --- a/authentik/core/api/tokens.py +++ b/authentik/core/api/tokens.py @@ -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), ) diff --git a/authentik/core/auth.py b/authentik/core/auth.py new file mode 100644 index 000000000..851177bec --- /dev/null +++ b/authentik/core/auth.py @@ -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 diff --git a/authentik/core/migrations/0028_alter_token_intent.py b/authentik/core/migrations/0028_alter_token_intent.py new file mode 100644 index 000000000..77fe3e0a1 --- /dev/null +++ b/authentik/core/migrations/0028_alter_token_intent.py @@ -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", + ), + ), + ] diff --git a/authentik/core/models.py b/authentik/core/models.py index 659fbb3ba..b1c0b3ec0 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -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.""" diff --git a/authentik/core/sources/flow_manager.py b/authentik/core/sources/flow_manager.py index 7666da07f..0cac12440 100644 --- a/authentik/core/sources/flow_manager.py +++ b/authentik/core/sources/flow_manager.py @@ -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, diff --git a/authentik/core/tests/test_source_flow_manager.py b/authentik/core/tests/test_source_flow_manager.py index dd47baf85..94ab52ba0 100644 --- a/authentik/core/tests/test_source_flow_manager.py +++ b/authentik/core/tests/test_source_flow_manager.py @@ -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"}, ) diff --git a/authentik/core/tests/test_token_api.py b/authentik/core/tests/test_token_api.py index 0e19a9a98..4a34f6bca 100644 --- a/authentik/core/tests/test_token_api.py +++ b/authentik/core/tests/test_token_api.py @@ -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 diff --git a/authentik/core/tests/test_token_auth.py b/authentik/core/tests/test_token_auth.py new file mode 100644 index 000000000..f7b285bbc --- /dev/null +++ b/authentik/core/tests/test_token_auth.py @@ -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, + ) diff --git a/authentik/events/signals.py b/authentik/events/signals.py index 7a4b5a032..b659f2b42 100644 --- a/authentik/events/signals.py +++ b/authentik/events/signals.py @@ -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() diff --git a/authentik/flows/migrations/0008_default_flows.py b/authentik/flows/migrations/0008_default_flows.py index 8de070f80..d507f209c 100644 --- a/authentik/flows/migrations/0008_default_flows.py +++ b/authentik/flows/migrations/0008_default_flows.py @@ -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( diff --git a/authentik/flows/tests/test_planner.py b/authentik/flows/tests/test_planner.py index 63c4c5193..cb7bf44f7 100644 --- a/authentik/flows/tests/test_planner.py +++ b/authentik/flows/tests/test_planner.py @@ -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""" diff --git a/authentik/flows/transfer/common.py b/authentik/flows/transfer/common.py index fb42282ad..031a43e2d 100644 --- a/authentik/flows/transfer/common.py +++ b/authentik/flows/transfer/common.py @@ -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: diff --git a/authentik/lib/tests/utils.py b/authentik/lib/tests/utils.py new file mode 100644 index 000000000..35ec8391d --- /dev/null +++ b/authentik/lib/tests/utils.py @@ -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 diff --git a/authentik/providers/saml/tests/test_auth_n_request.py b/authentik/providers/saml/tests/test_auth_n_request.py index ae8986956..a78373fd8 100644 --- a/authentik/providers/saml/tests/test_auth_n_request.py +++ b/authentik/providers/saml/tests/test_auth_n_request.py @@ -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") diff --git a/authentik/providers/saml/tests/test_schema.py b/authentik/providers/saml/tests/test_schema.py index b5f2d62e3..800df49d9 100644 --- a/authentik/providers/saml/tests/test_schema.py +++ b/authentik/providers/saml/tests/test_schema.py @@ -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") diff --git a/authentik/recovery/views.py b/authentik/recovery/views.py index 27b0023af..3253ab332 100644 --- a/authentik/recovery/views.py +++ b/authentik/recovery/views.py @@ -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") diff --git a/authentik/root/settings.py b/authentik/root/settings.py index ab3f59cc5..08dc5697b 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -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", ] diff --git a/authentik/sources/ldap/auth.py b/authentik/sources/ldap/auth.py index 9cf55d36d..0d052a42a 100644 --- a/authentik/sources/ldap/auth.py +++ b/authentik/sources/ldap/auth.py @@ -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 diff --git a/authentik/sources/ldap/settings.py b/authentik/sources/ldap/settings.py index 850e9a04d..d3b90e901 100644 --- a/authentik/sources/ldap/settings.py +++ b/authentik/sources/ldap/settings.py @@ -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", diff --git a/authentik/sources/saml/processors/response.py b/authentik/sources/saml/processors/response.py index 0e41b44c8..c2537770c 100644 --- a/authentik/sources/saml/processors/response.py +++ b/authentik/sources/saml/processors/response.py @@ -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, }, ) diff --git a/authentik/stages/authenticator_validate/tests.py b/authentik/stages/authenticator_validate/tests.py index 35d6b862a..6eacb525a 100644 --- a/authentik/stages/authenticator_validate/tests.py +++ b/authentik/stages/authenticator_validate/tests.py @@ -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, diff --git a/authentik/stages/identification/tests.py b/authentik/stages/identification/tests.py index c4ed8c23f..5cd8d63cf 100644 --- a/authentik/stages/identification/tests.py +++ b/authentik/stages/identification/tests.py @@ -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 = { diff --git a/authentik/stages/invitation/tests.py b/authentik/stages/invitation/tests.py index 45d0c97f5..6f84dca7b 100644 --- a/authentik/stages/invitation/tests.py +++ b/authentik/stages/invitation/tests.py @@ -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() diff --git a/authentik/stages/password/__init__.py b/authentik/stages/password/__init__.py index a9c71fe82..d5c73cc96 100644 --- a/authentik/stages/password/__init__.py +++ b/authentik/stages/password/__init__.py @@ -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 diff --git a/authentik/stages/password/migrations/0007_app_password.py b/authentik/stages/password/migrations/0007_app_password.py new file mode 100644 index 000000000..41d99be77 --- /dev/null +++ b/authentik/stages/password/migrations/0007_app_password.py @@ -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), + ] diff --git a/authentik/stages/password/models.py b/authentik/stages/password/models.py index e0df6b77b..5369052a2 100644 --- a/authentik/stages/password/models.py +++ b/authentik/stages/password/models.py @@ -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"), ), ] diff --git a/authentik/stages/password/stage.py b/authentik/stages/password/stage.py index f5cf96c22..646f68bdf 100644 --- a/authentik/stages/password/stage.py +++ b/authentik/stages/password/stage.py @@ -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" diff --git a/authentik/stages/password/tests.py b/authentik/stages/password/tests.py index e7a0ef4e5..ebd1e137d 100644 --- a/authentik/stages/password/tests.py +++ b/authentik/stages/password/tests.py @@ -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): diff --git a/authentik/stages/user_login/stage.py b/authentik/stages/user_login/stage.py index 773ccc3ab..9de80233d 100644 --- a/authentik/stages/user_login/stage.py +++ b/authentik/stages/user_login/stage.py @@ -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, diff --git a/authentik/stages/user_logout/tests.py b/authentik/stages/user_logout/tests.py index 2958e3f97..cf2ec16bd 100644 --- a/authentik/stages/user_logout/tests.py +++ b/authentik/stages/user_logout/tests.py @@ -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() diff --git a/authentik/stages/user_write/stage.py b/authentik/stages/user_write/stage.py index ea86fe9a3..39945ba95 100644 --- a/authentik/stages/user_write/stage.py +++ b/authentik/stages/user_write/stage.py @@ -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, diff --git a/schema.yml b/schema.yml index 271547231..421488100 100644 --- a/schema.yml +++ b/schema.yml @@ -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: diff --git a/scripts/ci.docker-compose.yml b/scripts/ci.docker-compose.yml index 9639c2ea1..1be22faa6 100644 --- a/scripts/ci.docker-compose.yml +++ b/scripts/ci.docker-compose.yml @@ -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: diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml index 966c6c305..46855e3bb 100644 --- a/scripts/docker-compose.yml +++ b/scripts/docker-compose.yml @@ -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: diff --git a/scripts/generate_ci_config.py b/scripts/generate_ci_config.py index cb75b778d..6a9613ada 100644 --- a/scripts/generate_ci_config.py +++ b/scripts/generate_ci_config.py @@ -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) diff --git a/web/rollup.config.js b/web/rollup.config.js index 89f19ddf0..a06e836c9 100644 --- a/web/rollup.config.js +++ b/web/rollup.config.js @@ -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", diff --git a/web/src/locales/en.po b/web/src/locales/en.po index f2c776929..95ab42d42 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -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" diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index 5de2beb61..21142156d 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -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" diff --git a/web/src/pages/flows/BoundStagesList.ts b/web/src/pages/flows/BoundStagesList.ts index ab83c39c7..4c30865de 100644 --- a/web/src/pages/flows/BoundStagesList.ts +++ b/web/src/pages/flows/BoundStagesList.ts @@ -48,6 +48,12 @@ export class BoundStagesList extends Table { return html` { + 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, diff --git a/web/src/pages/policies/BoundPoliciesList.ts b/web/src/pages/policies/BoundPoliciesList.ts index a33b4f65f..429b51914 100644 --- a/web/src/pages/policies/BoundPoliciesList.ts +++ b/web/src/pages/policies/BoundPoliciesList.ts @@ -100,6 +100,12 @@ export class BoundPoliciesList extends Table { return html` { + 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, diff --git a/web/src/pages/stages/identification/IdentificationStageForm.ts b/web/src/pages/stages/identification/IdentificationStageForm.ts index dd9b67097..b29ad8688 100644 --- a/web/src/pages/stages/identification/IdentificationStageForm.ts +++ b/web/src/pages/stages/identification/IdentificationStageForm.ts @@ -156,11 +156,20 @@ export class IdentificationStageForm extends ModelForm { 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`