Compare commits
	
		
			11 Commits
		
	
	
		
			dependabot
			...
			restricted
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| efdecf949d | |||
| 9ea5f56715 | |||
| 8228b56b75 | |||
| 518e10dbdb | |||
| 79ddad28a8 | |||
| beeec85c15 | |||
| 0561b8d578 | |||
| 201481bde3 | |||
| 7d04903d5b | |||
| db7d880116 | |||
| 259cc81723 | 
| @ -73,6 +73,11 @@ def auth_user_lookup(raw_header: bytes) -> User | None: | ||||
|     if user: | ||||
|         CTX_AUTH_VIA.set("secret_key") | ||||
|         return user | ||||
|     # then try to auth via expression JWT | ||||
|     user = token_expression_jwt(auth_credentials) | ||||
|     if user: | ||||
|         CTX_AUTH_VIA.set("expression_jwt") | ||||
|         return user | ||||
|     raise AuthenticationFailed("Token invalid/expired") | ||||
|  | ||||
|  | ||||
| @ -90,6 +95,13 @@ def token_secret_key(value: str) -> User | None: | ||||
|     return outpost.user | ||||
|  | ||||
|  | ||||
| def token_expression_jwt(value: str) -> User | None: | ||||
|     """Authenticate API call made by Expressions""" | ||||
|     from authentik.lib.expression.evaluator import authenticate_token | ||||
|  | ||||
|     return authenticate_token(value) | ||||
|  | ||||
|  | ||||
| class TokenAuthentication(BaseAuthentication): | ||||
|     """Token-based authentication using HTTP Bearer authentication""" | ||||
|  | ||||
|  | ||||
| @ -36,7 +36,7 @@ class PropertyMappingEvaluator(BaseEvaluator): | ||||
|             _filename = model.name | ||||
|         else: | ||||
|             _filename = str(model) | ||||
|         super().__init__(filename=_filename) | ||||
|         super().__init__(None, filename=_filename) | ||||
|         req = PolicyRequest(user=User()) | ||||
|         req.obj = model | ||||
|         if user: | ||||
|  | ||||
| @ -95,6 +95,9 @@ outposts: | ||||
|   discover: true | ||||
|   disable_embedded_outpost: false | ||||
|  | ||||
| expressions: | ||||
|   global_runtime: python # or python_restricted | ||||
|  | ||||
| ldap: | ||||
|   task_timeout_hours: 2 | ||||
|   page_size: 50 | ||||
|  | ||||
| @ -3,27 +3,117 @@ | ||||
| import re | ||||
| import socket | ||||
| from collections.abc import Iterable | ||||
| from datetime import timedelta | ||||
| from functools import lru_cache | ||||
| from ipaddress import ip_address, ip_network | ||||
| from pathlib import Path | ||||
| from tempfile import gettempdir | ||||
| from textwrap import indent | ||||
| from typing import Any | ||||
|  | ||||
| from authentik_client.api.admin_api import AdminApi | ||||
| from authentik_client.api.authenticators_api import AuthenticatorsApi | ||||
| from authentik_client.api.core_api import CoreApi | ||||
| from authentik_client.api.crypto_api import CryptoApi | ||||
| from authentik_client.api.enterprise_api import EnterpriseApi | ||||
| from authentik_client.api.events_api import EventsApi | ||||
| from authentik_client.api.flows_api import FlowsApi | ||||
| from authentik_client.api.managed_api import ManagedApi | ||||
| from authentik_client.api.oauth2_api import Oauth2Api | ||||
| from authentik_client.api.outposts_api import OutpostsApi | ||||
| from authentik_client.api.policies_api import PoliciesApi | ||||
| from authentik_client.api.propertymappings_api import PropertymappingsApi | ||||
| from authentik_client.api.providers_api import ProvidersApi | ||||
| from authentik_client.api.rac_api import RacApi | ||||
| from authentik_client.api.rbac_api import RbacApi | ||||
| from authentik_client.api.root_api import RootApi | ||||
| from authentik_client.api.schema_api import SchemaApi | ||||
| from authentik_client.api.sources_api import SourcesApi | ||||
| from authentik_client.api.stages_api import StagesApi | ||||
| from authentik_client.api.tenants_api import TenantsApi | ||||
| from authentik_client.api_client import ApiClient | ||||
| from authentik_client.configuration import Configuration | ||||
| from cachetools import TLRUCache, cached | ||||
| from django.conf import settings | ||||
| from django.core.exceptions import FieldError | ||||
| from django.utils.timezone import now | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
| from jwt import PyJWTError, decode, encode | ||||
| from rest_framework.serializers import ValidationError | ||||
| from RestrictedPython import compile_restricted, limited_builtins, safe_builtins, utility_builtins | ||||
| from sentry_sdk.hub import Hub | ||||
| from sentry_sdk.tracing import Span | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.events.models import Event | ||||
| from authentik.lib.utils.http import get_http_session | ||||
| from authentik.lib.config import CONFIG | ||||
| from authentik.lib.generators import generate_key | ||||
| from authentik.lib.utils.errors import exception_to_string | ||||
| from authentik.lib.utils.http import authentik_user_agent, get_http_session | ||||
| from authentik.lib.utils.reflection import get_apps | ||||
| from authentik.policies.models import Policy, PolicyBinding | ||||
| from authentik.policies.process import PolicyProcess | ||||
| from authentik.policies.types import PolicyRequest, PolicyResult | ||||
| from authentik.stages.authenticator import devices_for_user | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| _tmp = Path(gettempdir()) | ||||
| token_path = _tmp / "authentik-evaluator-token" | ||||
|  | ||||
| API_CLIENTS = { | ||||
|     "AdminApi": AdminApi, | ||||
|     "AuthenticatorsApi": AuthenticatorsApi, | ||||
|     "CoreApi": CoreApi, | ||||
|     "CryptoApi": CryptoApi, | ||||
|     "EnterpriseApi": EnterpriseApi, | ||||
|     "EventsApi": EventsApi, | ||||
|     "FlowsApi": FlowsApi, | ||||
|     "ManagedApi": ManagedApi, | ||||
|     "Oauth2Api": Oauth2Api, | ||||
|     "OutpostsApi": OutpostsApi, | ||||
|     "PoliciesApi": PoliciesApi, | ||||
|     "PropertymappingsApi": PropertymappingsApi, | ||||
|     "ProvidersApi": ProvidersApi, | ||||
|     "RacApi": RacApi, | ||||
|     "RbacApi": RbacApi, | ||||
|     "RootApi": RootApi, | ||||
|     "SchemaApi": SchemaApi, | ||||
|     "SourcesApi": SourcesApi, | ||||
|     "StagesApi": StagesApi, | ||||
|     "TenantsApi": TenantsApi, | ||||
| } | ||||
|  | ||||
| JWT_AUD = "goauthentik.io/api/expression" | ||||
|  | ||||
| _SAFE_MODULES = frozenset(("authentik_client",)) | ||||
|  | ||||
|  | ||||
| def _safe_import(name, *args, **kwargs): | ||||
|     if name not in _SAFE_MODULES: | ||||
|         raise Exception(f"Don't you even think about {name!r}") | ||||
|     return __import__(name, *args, **kwargs) | ||||
|  | ||||
|  | ||||
| @lru_cache | ||||
| def get_api_token_secret(): | ||||
|     if token_path.exists(): | ||||
|         with open(token_path) as _token_file: | ||||
|             return _token_file.read() | ||||
|     key = generate_key() | ||||
|     with open(_tmp / "authentik-evaluator-token", "w") as _token_file: | ||||
|         _token_file.write(key) | ||||
|     return key | ||||
|  | ||||
|  | ||||
| def authenticate_token(raw_value: str): | ||||
|     """Authenticate API call from evaluator token""" | ||||
|     try: | ||||
|         jwt = decode(raw_value, get_api_token_secret(), ["HS256"], audience=JWT_AUD) | ||||
|         return User.objects.filter(pk=jwt["sub"]).first() | ||||
|     except PyJWTError as exc: | ||||
|         LOGGER.debug("failed to auth", exc=exc) | ||||
|         return None | ||||
|  | ||||
|  | ||||
| class BaseEvaluator: | ||||
| @ -37,8 +127,14 @@ class BaseEvaluator: | ||||
|     # Filename used for exec | ||||
|     _filename: str | ||||
|  | ||||
|     def __init__(self, filename: str | None = None): | ||||
|     _user: User | ||||
|  | ||||
|     # Timeout in seconds, used for the expiration of the API key | ||||
|     timeout = 30 | ||||
|  | ||||
|     def __init__(self, user: User, filename: str | None = None): | ||||
|         self._filename = filename if filename else "BaseEvaluator" | ||||
|         self._user = user | ||||
|         # update website/docs/expressions/_objects.md | ||||
|         # update website/docs/expressions/_functions.md | ||||
|         self._globals = { | ||||
| @ -57,8 +153,44 @@ class BaseEvaluator: | ||||
|             "resolve_dns": BaseEvaluator.expr_resolve_dns, | ||||
|             "reverse_dns": BaseEvaluator.expr_reverse_dns, | ||||
|         } | ||||
|         for app in get_apps(): | ||||
|             # Load models from each app | ||||
|             for model in app.get_models(): | ||||
|                 self._globals[model.__name__] = model | ||||
|         self._globals.update(API_CLIENTS) | ||||
|         self._context = {} | ||||
|  | ||||
|     def get_token(self) -> str: | ||||
|         """Generate API token to be used by the API Client""" | ||||
|         _now = now() | ||||
|         if not self._user: | ||||
|             self._user = get_anonymous_user() | ||||
|         return encode( | ||||
|             { | ||||
|                 "aud": JWT_AUD, | ||||
|                 "iss": f"goauthentik.io/expression/{self._filename}", | ||||
|                 "sub": str(self._user.pk), | ||||
|                 "iat": int(_now.timestamp()), | ||||
|                 "exp": int((_now + timedelta(seconds=self.timeout)).timestamp()), | ||||
|             }, | ||||
|             get_api_token_secret(), | ||||
|         ) | ||||
|  | ||||
|     def get_api_client(self): | ||||
|         token = self.get_token() | ||||
|         config = Configuration( | ||||
|             f"unix://{str(_tmp.joinpath('authentik-core.sock'))}/api/v3", | ||||
|             api_key={ | ||||
|                 "authentik": token, | ||||
|             }, | ||||
|             api_key_prefix={"authentik": "Bearer"}, | ||||
|         ) | ||||
|         if settings.DEBUG: | ||||
|             config.host = "http://localhost:8000/api/v3" | ||||
|         client = ApiClient(config) | ||||
|         client.user_agent = authentik_user_agent() | ||||
|         return client | ||||
|  | ||||
|     @cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180)) | ||||
|     @staticmethod | ||||
|     def expr_resolve_dns(host: str, ip_version: int | None = None) -> list[str]: | ||||
| @ -185,7 +317,16 @@ class BaseEvaluator: | ||||
|     def compile(self, expression: str) -> Any: | ||||
|         """Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect.""" | ||||
|         param_keys = self._context.keys() | ||||
|         return compile(self.wrap_expression(expression, param_keys), self._filename, "exec") | ||||
|         compiler = ( | ||||
|             compile_restricted | ||||
|             if CONFIG.get("expressions.global_runtime") == "python_restricted" | ||||
|             else compile | ||||
|         ) | ||||
|         return compiler( | ||||
|             self.wrap_expression(expression, param_keys), | ||||
|             self._filename, | ||||
|             "exec", | ||||
|         ) | ||||
|  | ||||
|     def evaluate(self, expression_source: str) -> Any: | ||||
|         """Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised. | ||||
| @ -201,7 +342,17 @@ class BaseEvaluator: | ||||
|                 self.handle_error(exc, expression_source) | ||||
|                 raise exc | ||||
|             try: | ||||
|                 if CONFIG.get("expressions.global_runtime") == "python_restricted": | ||||
|                     self._globals["__builtins__"] = { | ||||
|                         **safe_builtins, | ||||
|                         **limited_builtins, | ||||
|                         **utility_builtins, | ||||
|                         "__import__": _safe_import, | ||||
|                     } | ||||
|                 _locals = self._context | ||||
|                 # We need to create the API Client later so that the token is valid | ||||
|                 # from when the execution starts | ||||
|                 self._globals["api"] = self.get_api_client() | ||||
|                 # Yes this is an exec, yes it is potentially bad. Since we limit what variables are | ||||
|                 # available here, and these policies can only be edited by admins, this is a risk | ||||
|                 # we're willing to take. | ||||
| @ -209,6 +360,7 @@ class BaseEvaluator: | ||||
|                 exec(ast_obj, self._globals, _locals)  # nosec # noqa | ||||
|                 result = _locals["result"] | ||||
|             except Exception as exc: | ||||
|                 print(exception_to_string(exc)) | ||||
|                 # So, this is a bit questionable. Essentially, we are edit the stacktrace | ||||
|                 # so the user only sees information relevant to them | ||||
|                 # and none of our surrounding error handling | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| """Test Evaluator base functions""" | ||||
|  | ||||
| from django.test import TestCase | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
|  | ||||
| from authentik.core.tests.utils import create_test_admin_user | ||||
| from authentik.events.models import Event | ||||
| @ -33,7 +34,7 @@ class TestEvaluator(TestCase): | ||||
|  | ||||
|     def test_expr_event_create(self): | ||||
|         """Test expr_event_create""" | ||||
|         evaluator = BaseEvaluator(generate_id()) | ||||
|         evaluator = BaseEvaluator(get_anonymous_user(), generate_id()) | ||||
|         evaluator._context = { | ||||
|             "foo": "bar", | ||||
|         } | ||||
|  | ||||
| @ -14,12 +14,13 @@ class ExpressionPolicySerializer(PolicySerializer): | ||||
|     def validate_expression(self, expr: str) -> str: | ||||
|         """validate the syntax of the expression""" | ||||
|         name = "temp-policy" if not self.instance else self.instance.name | ||||
|         PolicyEvaluator(name).validate(expr) | ||||
|         request = self.context.get("request") | ||||
|         PolicyEvaluator(request.user if request else None, name).validate(expr) | ||||
|         return expr | ||||
|  | ||||
|     class Meta: | ||||
|         model = ExpressionPolicy | ||||
|         fields = PolicySerializer.Meta.fields + ["expression"] | ||||
|         fields = PolicySerializer.Meta.fields + ["expression", "execution_user"] | ||||
|  | ||||
|  | ||||
| class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet): | ||||
|  | ||||
| @ -1,11 +1,12 @@ | ||||
| """Authentik policy_expression app config""" | ||||
|  | ||||
| from django.apps import AppConfig | ||||
| from authentik.blueprints.apps import ManagedAppConfig | ||||
|  | ||||
|  | ||||
| class AuthentikPolicyExpressionConfig(AppConfig): | ||||
| class AuthentikPolicyExpressionConfig(ManagedAppConfig): | ||||
|     """Authentik policy_expression app config""" | ||||
|  | ||||
|     name = "authentik.policies.expression" | ||||
|     label = "authentik_policies_expression" | ||||
|     verbose_name = "authentik Policies.Expression" | ||||
|     default = True | ||||
|  | ||||
| @ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Optional | ||||
| from django.http import HttpRequest | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.flows.planner import PLAN_CONTEXT_SSO | ||||
| from authentik.lib.expression.evaluator import BaseEvaluator | ||||
| from authentik.policies.exceptions import PolicyException | ||||
| @ -24,8 +25,8 @@ class PolicyEvaluator(BaseEvaluator): | ||||
|  | ||||
|     policy: Optional["ExpressionPolicy"] = None | ||||
|  | ||||
|     def __init__(self, policy_name: str | None = None): | ||||
|         super().__init__(policy_name or "PolicyEvaluator") | ||||
|     def __init__(self, user: User, policy_name: str | None = None): | ||||
|         super().__init__(user, policy_name or "PolicyEvaluator") | ||||
|         self._messages = [] | ||||
|         # update website/docs/expressions/_objects.md | ||||
|         # update website/docs/expressions/_functions.md | ||||
| @ -44,6 +45,8 @@ class PolicyEvaluator(BaseEvaluator): | ||||
|         if request.http_request: | ||||
|             self.set_http_request(request.http_request) | ||||
|         self._context["request"] = request | ||||
|         if not self._user: | ||||
|             self._user = request.user | ||||
|         self._context["context"] = request.context | ||||
|  | ||||
|     def set_http_request(self, request: HttpRequest): | ||||
|  | ||||
| @ -0,0 +1,26 @@ | ||||
| # Generated by Django 5.0.3 on 2024-03-20 12:14 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.conf import settings | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_policies_expression", "0004_expressionpolicy_authentik_p_policy__fb6feb_idx"), | ||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="expressionpolicy", | ||||
|             name="execution_user", | ||||
|             field=models.ForeignKey( | ||||
|                 default=None, | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.SET_DEFAULT, | ||||
|                 to=settings.AUTH_USER_MODEL, | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -12,6 +12,9 @@ from authentik.policies.types import PolicyRequest, PolicyResult | ||||
| class ExpressionPolicy(Policy): | ||||
|     """Execute arbitrary Python code to implement custom checks and validation.""" | ||||
|  | ||||
|     execution_user = models.ForeignKey( | ||||
|         "authentik_core.User", default=None, null=True, on_delete=models.SET_DEFAULT | ||||
|     ) | ||||
|     expression = models.TextField() | ||||
|  | ||||
|     @property | ||||
| @ -26,17 +29,11 @@ class ExpressionPolicy(Policy): | ||||
|  | ||||
|     def passes(self, request: PolicyRequest) -> PolicyResult: | ||||
|         """Evaluate and render expression. Returns PolicyResult(false) on error.""" | ||||
|         evaluator = PolicyEvaluator(self.name) | ||||
|         evaluator = PolicyEvaluator(self.execution_user, self.name) | ||||
|         evaluator.policy = self | ||||
|         evaluator.set_policy_request(request) | ||||
|         return evaluator.evaluate(self.expression) | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         evaluator = PolicyEvaluator(self.name) | ||||
|         evaluator.policy = self | ||||
|         evaluator.validate(self.expression) | ||||
|         return super().save(*args, **kwargs) | ||||
|  | ||||
|     class Meta(Policy.PolicyMeta): | ||||
|         verbose_name = _("Expression Policy") | ||||
|         verbose_name_plural = _("Expression Policies") | ||||
|  | ||||
							
								
								
									
										13
									
								
								authentik/policies/expression/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								authentik/policies/expression/signals.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| from django.db.models.signals import pre_save | ||||
| from django.dispatch import receiver | ||||
|  | ||||
| from authentik.policies.expression.evaluator import PolicyEvaluator | ||||
| from authentik.policies.expression.models import ExpressionPolicy | ||||
|  | ||||
|  | ||||
| @receiver(pre_save, sender=ExpressionPolicy) | ||||
| def pre_save_expression_policy(sender: type[ExpressionPolicy], instance: ExpressionPolicy, **_): | ||||
|     """Ensure policy is valid before saving""" | ||||
|     evaluator = PolicyEvaluator(instance.execution_user, instance.name) | ||||
|     evaluator.policy = instance | ||||
|     evaluator.validate(instance.expression) | ||||
| @ -41,14 +41,14 @@ class TestEvaluator(TestCase): | ||||
|     def test_valid(self): | ||||
|         """test simple value expression""" | ||||
|         template = "return True" | ||||
|         evaluator = PolicyEvaluator("test") | ||||
|         evaluator = PolicyEvaluator(self.request.user, "test") | ||||
|         evaluator.set_policy_request(self.request) | ||||
|         self.assertEqual(evaluator.evaluate(template).passing, True) | ||||
|  | ||||
|     def test_messages(self): | ||||
|         """test expression with message return""" | ||||
|         template = 'ak_message("some message");return False' | ||||
|         evaluator = PolicyEvaluator("test") | ||||
|         evaluator = PolicyEvaluator(self.request.user, "test") | ||||
|         evaluator.set_policy_request(self.request) | ||||
|         result = evaluator.evaluate(template) | ||||
|         self.assertEqual(result.passing, False) | ||||
| @ -57,7 +57,7 @@ class TestEvaluator(TestCase): | ||||
|     def test_invalid_syntax(self): | ||||
|         """test invalid syntax""" | ||||
|         template = ";" | ||||
|         evaluator = PolicyEvaluator("test") | ||||
|         evaluator = PolicyEvaluator(self.request.user, "test") | ||||
|         evaluator.set_policy_request(self.request) | ||||
|         with self.assertRaises(PolicyException): | ||||
|             evaluator.evaluate(template) | ||||
| @ -65,14 +65,14 @@ class TestEvaluator(TestCase): | ||||
|     def test_validate(self): | ||||
|         """test validate""" | ||||
|         template = "True" | ||||
|         evaluator = PolicyEvaluator("test") | ||||
|         evaluator = PolicyEvaluator(self.request.user, "test") | ||||
|         result = evaluator.validate(template) | ||||
|         self.assertEqual(result, True) | ||||
|  | ||||
|     def test_validate_invalid(self): | ||||
|         """test validate""" | ||||
|         template = ";" | ||||
|         evaluator = PolicyEvaluator("test") | ||||
|         evaluator = PolicyEvaluator(self.request.user, "test") | ||||
|         with self.assertRaises(ValidationError): | ||||
|             evaluator.validate(template) | ||||
|  | ||||
| @ -83,7 +83,7 @@ class TestEvaluator(TestCase): | ||||
|             execution_logging=True, | ||||
|             expression="ak_message(request.http_request.path)\nreturn True", | ||||
|         ) | ||||
|         evaluator = PolicyEvaluator("test") | ||||
|         evaluator = PolicyEvaluator(self.request.user, "test") | ||||
|         evaluator.set_policy_request(self.request) | ||||
|         proc = PolicyProcess(PolicyBinding(policy=expr), request=self.request, connection=None) | ||||
|         res = proc.profiling_wrapper() | ||||
|  | ||||
| @ -85,30 +85,19 @@ entries: | ||||
|     model: authentik_stages_prompt.prompt | ||||
|   - attrs: | ||||
|       expression: | | ||||
|         from authentik.core.models import ( | ||||
|             USER_ATTRIBUTE_CHANGE_EMAIL, | ||||
|             USER_ATTRIBUTE_CHANGE_NAME, | ||||
|             USER_ATTRIBUTE_CHANGE_USERNAME | ||||
|         ) | ||||
|         prompt_data = request.context.get("prompt_data") | ||||
|  | ||||
|         if not request.user.group_attributes(request.http_request).get( | ||||
|             USER_ATTRIBUTE_CHANGE_EMAIL, request.http_request.tenant.default_user_change_email | ||||
|         ): | ||||
|         user_group_attributes = request.user.group_attributes(request.http_request) | ||||
|         if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_EMAIL, request.http_request.tenant.default_user_change_email): | ||||
|             if prompt_data.get("email") != request.user.email: | ||||
|                 ak_message("Not allowed to change email address.") | ||||
|                 return False | ||||
|  | ||||
|         if not request.user.group_attributes(request.http_request).get( | ||||
|             USER_ATTRIBUTE_CHANGE_NAME, request.http_request.tenant.default_user_change_name | ||||
|         ): | ||||
|         if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_NAME, request.http_request.tenant.default_user_change_name): | ||||
|             if prompt_data.get("name") != request.user.name: | ||||
|                 ak_message("Not allowed to change name.") | ||||
|                 return False | ||||
|  | ||||
|         if not request.user.group_attributes(request.http_request).get( | ||||
|             USER_ATTRIBUTE_CHANGE_USERNAME, request.http_request.tenant.default_user_change_username | ||||
|         ): | ||||
|         if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_USERNAME, request.http_request.tenant.default_user_change_username): | ||||
|             if prompt_data.get("username") != request.user.username: | ||||
|                 ak_message("Not allowed to change username.") | ||||
|                 return False | ||||
|  | ||||
| @ -89,10 +89,9 @@ entries: | ||||
|     expression: | | ||||
|       # This policy ensures that the setup flow can only be | ||||
|       # used one time | ||||
|       from authentik.flows.models import Flow, FlowAuthenticationRequirement | ||||
|       Flow.objects.filter(slug="initial-setup").update( | ||||
|           authentication=FlowAuthenticationRequirement.REQUIRE_SUPERUSER, | ||||
|       ) | ||||
|       FlowsApi(api).flows_instances_partial_update("initial-setup", { | ||||
|           "authentication": "REQUIRE_SUPERUSER" | ||||
|       }) | ||||
|       return True | ||||
|   id: policy-default-oobe-flow-set-authentication | ||||
|   identifiers: | ||||
|  | ||||
| @ -3477,6 +3477,10 @@ | ||||
|                     "type": "string", | ||||
|                     "minLength": 1, | ||||
|                     "title": "Expression" | ||||
|                 }, | ||||
|                 "execution_user": { | ||||
|                     "type": "integer", | ||||
|                     "title": "Execution user" | ||||
|                 } | ||||
|             }, | ||||
|             "required": [] | ||||
|  | ||||
							
								
								
									
										34
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										34
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @ -269,6 +269,23 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] | ||||
| tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] | ||||
| tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] | ||||
|  | ||||
| [[package]] | ||||
| name = "authentik-client" | ||||
| version = "2024.4.1.post1714149882" | ||||
| description = "authentik" | ||||
| optional = false | ||||
| python-versions = "<4.0,>=3.7" | ||||
| files = [ | ||||
|     {file = "authentik_client-2024.4.1.post1714149882-py3-none-any.whl", hash = "sha256:867b66b6a0fd59fac5a96d2098889fca1fa17409d042c00923d06ba21db27622"}, | ||||
|     {file = "authentik_client-2024.4.1.post1714149882.tar.gz", hash = "sha256:2917c6f531dcbe44392d6f758fe10065399972de0c324d5431ea183e4b11ed7b"}, | ||||
| ] | ||||
|  | ||||
| [package.dependencies] | ||||
| pydantic = ">=2" | ||||
| python-dateutil = ">=2.8.2" | ||||
| typing-extensions = ">=4.7.1" | ||||
| urllib3 = ">=1.25.3" | ||||
|  | ||||
| [[package]] | ||||
| name = "autobahn" | ||||
| version = "23.6.2" | ||||
| @ -3385,6 +3402,21 @@ requests = ">=2.0.0" | ||||
| [package.extras] | ||||
| rsa = ["oauthlib[signedtoken] (>=3.0.0)"] | ||||
|  | ||||
| [[package]] | ||||
| name = "restrictedpython" | ||||
| version = "7.1" | ||||
| description = "RestrictedPython is a defined subset of the Python language which allows to provide a program input into a trusted environment." | ||||
| optional = false | ||||
| python-versions = ">=3.7, <3.13" | ||||
| files = [ | ||||
|     {file = "RestrictedPython-7.1-py3-none-any.whl", hash = "sha256:56d0c73e5de1757702053383601b0fcd3fb2e428039ee1df860409ad67b17d2b"}, | ||||
|     {file = "RestrictedPython-7.1.tar.gz", hash = "sha256:875aeb51c139d78e34cef8605dc65309b449168060dd08551a1fe9edb47cb9a5"}, | ||||
| ] | ||||
|  | ||||
| [package.extras] | ||||
| docs = ["Sphinx", "sphinx-rtd-theme"] | ||||
| test = ["pytest", "pytest-mock"] | ||||
|  | ||||
| [[package]] | ||||
| name = "rich" | ||||
| version = "13.7.0" | ||||
| @ -4683,4 +4715,4 @@ files = [ | ||||
| [metadata] | ||||
| lock-version = "2.0" | ||||
| python-versions = "~3.12" | ||||
| content-hash = "a5774b4e09217805c887700b8a0f457a39c7af40ca59823f00c1f6e8678469e1" | ||||
| content-hash = "647a426ecb06caf2b8f3165c4e85ed0439568e692b6ade8c7933683985c90b18" | ||||
|  | ||||
| @ -83,6 +83,7 @@ filterwarnings = [ | ||||
| ] | ||||
|  | ||||
| [tool.poetry.dependencies] | ||||
| authentik_client = "2024.4.1.post1714149882" | ||||
| argon2-cffi = "*" | ||||
| celery = "*" | ||||
| channels = { version = "*", extras = ["daphne"] } | ||||
| @ -132,6 +133,7 @@ pyjwt = "*" | ||||
| python = "~3.12" | ||||
| pyyaml = "*" | ||||
| requests-oauthlib = "*" | ||||
| restrictedpython = "*" | ||||
| scim2-filter-parser = "*" | ||||
| sentry-sdk = "*" | ||||
| service_identity = "*" | ||||
|  | ||||
							
								
								
									
										13
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								schema.yml
									
									
									
									
									
								
							| @ -11852,6 +11852,10 @@ paths: | ||||
|         name: execution_logging | ||||
|         schema: | ||||
|           type: boolean | ||||
|       - in: query | ||||
|         name: execution_user | ||||
|         schema: | ||||
|           type: integer | ||||
|       - in: query | ||||
|         name: expression | ||||
|         schema: | ||||
| @ -33565,6 +33569,9 @@ components: | ||||
|           readOnly: true | ||||
|         expression: | ||||
|           type: string | ||||
|         execution_user: | ||||
|           type: integer | ||||
|           nullable: true | ||||
|       required: | ||||
|       - bound_to | ||||
|       - component | ||||
| @ -33588,6 +33595,9 @@ components: | ||||
|         expression: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|         execution_user: | ||||
|           type: integer | ||||
|           nullable: true | ||||
|       required: | ||||
|       - expression | ||||
|       - name | ||||
| @ -38841,6 +38851,9 @@ components: | ||||
|         expression: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|         execution_user: | ||||
|           type: integer | ||||
|           nullable: true | ||||
|     PatchedFlowRequest: | ||||
|       type: object | ||||
|       description: Flow Serializer | ||||
|  | ||||
| @ -12,7 +12,13 @@ import { TemplateResult, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { ExpressionPolicy, PoliciesApi } from "@goauthentik/api"; | ||||
| import { | ||||
|     CoreApi, | ||||
|     CoreUsersListRequest, | ||||
|     ExpressionPolicy, | ||||
|     PoliciesApi, | ||||
|     User, | ||||
| } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-policy-expression-form") | ||||
| export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> { | ||||
| @ -92,6 +98,39 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> { | ||||
|                             </a> | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal label=${msg("Execution user")} name="executionUser"> | ||||
|                         <ak-search-select | ||||
|                             .fetchObjects=${async (query?: string): Promise<User[]> => { | ||||
|                                 const args: CoreUsersListRequest = { | ||||
|                                     ordering: "username", | ||||
|                                 }; | ||||
|                                 if (query !== undefined) { | ||||
|                                     args.search = query; | ||||
|                                 } | ||||
|                                 const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList(args); | ||||
|                                 return users.results; | ||||
|                             }} | ||||
|                             .renderElement=${(user: User): string => { | ||||
|                                 return user.username; | ||||
|                             }} | ||||
|                             .renderDescription=${(user: User): TemplateResult => { | ||||
|                                 return html`${user.name}`; | ||||
|                             }} | ||||
|                             .value=${(user: User | undefined): number | undefined => { | ||||
|                                 return user?.pk; | ||||
|                             }} | ||||
|                             .selected=${(user: User): boolean => { | ||||
|                                 return this.instance?.executionUser === user.pk; | ||||
|                             }} | ||||
|                             blankable | ||||
|                         > | ||||
|                         </ak-search-select> | ||||
|                         <p class="pf-c-form__helper-text"> | ||||
|                             ${msg( | ||||
|                                 "Configure which user the bundled API client authenticates as. When left empty, the API client will inherit the permissions of the user triggering the policy execution.", | ||||
|                             )} | ||||
|                         </p> | ||||
|                     </ak-form-element-horizontal> | ||||
|                 </div> | ||||
|             </ak-form-group>`; | ||||
|     } | ||||
|  | ||||
| @ -44,7 +44,7 @@ export function docLink(path: string): string { | ||||
|     const ak = globalAK(); | ||||
|     // Default case or beta build which should always point to latest | ||||
|     if (!ak || ak.build !== "") { | ||||
|         return `https://goauthentik.io${path}`; | ||||
|         return `https://docs.goauthentik.io${path}`; | ||||
|     } | ||||
|     return `https://${ak.versionSubdomain}.goauthentik.io${path}`; | ||||
| } | ||||
|  | ||||
| @ -13,14 +13,12 @@ Depending on what kind of device you want to require the user to have: | ||||
| #### WebAuthn | ||||
|  | ||||
| ```python | ||||
| from authentik.stages.authenticator_webauthn.models import WebAuthnDevice | ||||
| return WebAuthnDevice.objects.filter(user=request.context['pending_user'], confirmed=True).exists() | ||||
| ``` | ||||
|  | ||||
| #### Duo | ||||
|  | ||||
| ```python | ||||
| from authentik.stages.authenticator_duo.models import DuoDevice | ||||
| return DuoDevice.objects.filter(user=request.context['pending_user'], confirmed=True).exists() | ||||
| ``` | ||||
|  | ||||
|  | ||||
| @ -11,7 +11,6 @@ Newly created users can be created as inactive and can be assigned to a selected | ||||
| Starting with authentik 2022.5, users can be added to dynamic groups. To do so, simply set `groups` in the flow plan context before this stage is run, for example | ||||
|  | ||||
| ```python | ||||
| from authentik.core.models import Group | ||||
| group, _ = Group.objects.get_or_create(name="some-group") | ||||
| # ["groups"] *must* be set to an array of Group objects, names alone are not enough. | ||||
| request.context["flow_plan"].context["groups"] = [group] | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	