Compare commits

...

11 Commits

Author SHA1 Message Date
efdecf949d fix a bunch of tests
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-29 19:47:25 +02:00
9ea5f56715 start updating some default policies
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-29 19:47:10 +02:00
8228b56b75 allow importing from API client
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-29 19:46:56 +02:00
518e10dbdb fix yaml?
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-29 19:46:55 +02:00
79ddad28a8 fiiine use the user pk
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-29 19:45:52 +02:00
beeec85c15 generate evaluator jwt secret
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-29 19:45:52 +02:00
0561b8d578 make it work
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-29 19:45:52 +02:00
201481bde3 generate api client
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-29 19:45:21 +02:00
7d04903d5b docs: remove imports
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-29 19:38:09 +02:00
db7d880116 wip
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-29 19:38:09 +02:00
259cc81723 wip
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-29 19:36:43 +02:00
22 changed files with 333 additions and 49 deletions

View File

@ -73,6 +73,11 @@ def auth_user_lookup(raw_header: bytes) -> User | None:
if user: if user:
CTX_AUTH_VIA.set("secret_key") CTX_AUTH_VIA.set("secret_key")
return user 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") raise AuthenticationFailed("Token invalid/expired")
@ -90,6 +95,13 @@ def token_secret_key(value: str) -> User | None:
return outpost.user 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): class TokenAuthentication(BaseAuthentication):
"""Token-based authentication using HTTP Bearer authentication""" """Token-based authentication using HTTP Bearer authentication"""

View File

@ -36,7 +36,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
_filename = model.name _filename = model.name
else: else:
_filename = str(model) _filename = str(model)
super().__init__(filename=_filename) super().__init__(None, filename=_filename)
req = PolicyRequest(user=User()) req = PolicyRequest(user=User())
req.obj = model req.obj = model
if user: if user:

View File

@ -95,6 +95,9 @@ outposts:
discover: true discover: true
disable_embedded_outpost: false disable_embedded_outpost: false
expressions:
global_runtime: python # or python_restricted
ldap: ldap:
task_timeout_hours: 2 task_timeout_hours: 2
page_size: 50 page_size: 50

View File

@ -3,27 +3,117 @@
import re import re
import socket import socket
from collections.abc import Iterable from collections.abc import Iterable
from datetime import timedelta
from functools import lru_cache
from ipaddress import ip_address, ip_network from ipaddress import ip_address, ip_network
from pathlib import Path
from tempfile import gettempdir
from textwrap import indent from textwrap import indent
from typing import Any 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 cachetools import TLRUCache, cached
from django.conf import settings
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
from jwt import PyJWTError, decode, encode
from rest_framework.serializers import ValidationError 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.hub import Hub
from sentry_sdk.tracing import Span from sentry_sdk.tracing import Span
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import User from authentik.core.models import User
from authentik.events.models import Event 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.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess from authentik.policies.process import PolicyProcess
from authentik.policies.types import PolicyRequest, PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult
from authentik.stages.authenticator import devices_for_user from authentik.stages.authenticator import devices_for_user
LOGGER = get_logger() 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: class BaseEvaluator:
@ -37,8 +127,14 @@ class BaseEvaluator:
# Filename used for exec # Filename used for exec
_filename: str _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._filename = filename if filename else "BaseEvaluator"
self._user = user
# update website/docs/expressions/_objects.md # update website/docs/expressions/_objects.md
# update website/docs/expressions/_functions.md # update website/docs/expressions/_functions.md
self._globals = { self._globals = {
@ -57,8 +153,44 @@ class BaseEvaluator:
"resolve_dns": BaseEvaluator.expr_resolve_dns, "resolve_dns": BaseEvaluator.expr_resolve_dns,
"reverse_dns": BaseEvaluator.expr_reverse_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 = {} 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)) @cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180))
@staticmethod @staticmethod
def expr_resolve_dns(host: str, ip_version: int | None = None) -> list[str]: 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: def compile(self, expression: str) -> Any:
"""Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect.""" """Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect."""
param_keys = self._context.keys() 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: def evaluate(self, expression_source: str) -> Any:
"""Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised. """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) self.handle_error(exc, expression_source)
raise exc raise exc
try: try:
if CONFIG.get("expressions.global_runtime") == "python_restricted":
self._globals["__builtins__"] = {
**safe_builtins,
**limited_builtins,
**utility_builtins,
"__import__": _safe_import,
}
_locals = self._context _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 # 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 # available here, and these policies can only be edited by admins, this is a risk
# we're willing to take. # we're willing to take.
@ -209,6 +360,7 @@ class BaseEvaluator:
exec(ast_obj, self._globals, _locals) # nosec # noqa exec(ast_obj, self._globals, _locals) # nosec # noqa
result = _locals["result"] result = _locals["result"]
except Exception as exc: except Exception as exc:
print(exception_to_string(exc))
# So, this is a bit questionable. Essentially, we are edit the stacktrace # So, this is a bit questionable. Essentially, we are edit the stacktrace
# so the user only sees information relevant to them # so the user only sees information relevant to them
# and none of our surrounding error handling # and none of our surrounding error handling

View File

@ -1,6 +1,7 @@
"""Test Evaluator base functions""" """Test Evaluator base functions"""
from django.test import TestCase from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.core.tests.utils import create_test_admin_user from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event from authentik.events.models import Event
@ -33,7 +34,7 @@ class TestEvaluator(TestCase):
def test_expr_event_create(self): def test_expr_event_create(self):
"""Test expr_event_create""" """Test expr_event_create"""
evaluator = BaseEvaluator(generate_id()) evaluator = BaseEvaluator(get_anonymous_user(), generate_id())
evaluator._context = { evaluator._context = {
"foo": "bar", "foo": "bar",
} }

View File

@ -14,12 +14,13 @@ class ExpressionPolicySerializer(PolicySerializer):
def validate_expression(self, expr: str) -> str: def validate_expression(self, expr: str) -> str:
"""validate the syntax of the expression""" """validate the syntax of the expression"""
name = "temp-policy" if not self.instance else self.instance.name 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 return expr
class Meta: class Meta:
model = ExpressionPolicy model = ExpressionPolicy
fields = PolicySerializer.Meta.fields + ["expression"] fields = PolicySerializer.Meta.fields + ["expression", "execution_user"]
class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet): class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet):

View File

@ -1,11 +1,12 @@
"""Authentik policy_expression app config""" """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""" """Authentik policy_expression app config"""
name = "authentik.policies.expression" name = "authentik.policies.expression"
label = "authentik_policies_expression" label = "authentik_policies_expression"
verbose_name = "authentik Policies.Expression" verbose_name = "authentik Policies.Expression"
default = True

View File

@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Optional
from django.http import HttpRequest from django.http import HttpRequest
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.flows.planner import PLAN_CONTEXT_SSO from authentik.flows.planner import PLAN_CONTEXT_SSO
from authentik.lib.expression.evaluator import BaseEvaluator from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.policies.exceptions import PolicyException from authentik.policies.exceptions import PolicyException
@ -24,8 +25,8 @@ class PolicyEvaluator(BaseEvaluator):
policy: Optional["ExpressionPolicy"] = None policy: Optional["ExpressionPolicy"] = None
def __init__(self, policy_name: str | None = None): def __init__(self, user: User, policy_name: str | None = None):
super().__init__(policy_name or "PolicyEvaluator") super().__init__(user, policy_name or "PolicyEvaluator")
self._messages = [] self._messages = []
# update website/docs/expressions/_objects.md # update website/docs/expressions/_objects.md
# update website/docs/expressions/_functions.md # update website/docs/expressions/_functions.md
@ -44,6 +45,8 @@ class PolicyEvaluator(BaseEvaluator):
if request.http_request: if request.http_request:
self.set_http_request(request.http_request) self.set_http_request(request.http_request)
self._context["request"] = request self._context["request"] = request
if not self._user:
self._user = request.user
self._context["context"] = request.context self._context["context"] = request.context
def set_http_request(self, request: HttpRequest): def set_http_request(self, request: HttpRequest):

View File

@ -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,
),
),
]

View File

@ -12,6 +12,9 @@ from authentik.policies.types import PolicyRequest, PolicyResult
class ExpressionPolicy(Policy): class ExpressionPolicy(Policy):
"""Execute arbitrary Python code to implement custom checks and validation.""" """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() expression = models.TextField()
@property @property
@ -26,17 +29,11 @@ class ExpressionPolicy(Policy):
def passes(self, request: PolicyRequest) -> PolicyResult: def passes(self, request: PolicyRequest) -> PolicyResult:
"""Evaluate and render expression. Returns PolicyResult(false) on error.""" """Evaluate and render expression. Returns PolicyResult(false) on error."""
evaluator = PolicyEvaluator(self.name) evaluator = PolicyEvaluator(self.execution_user, self.name)
evaluator.policy = self evaluator.policy = self
evaluator.set_policy_request(request) evaluator.set_policy_request(request)
return evaluator.evaluate(self.expression) 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): class Meta(Policy.PolicyMeta):
verbose_name = _("Expression Policy") verbose_name = _("Expression Policy")
verbose_name_plural = _("Expression Policies") verbose_name_plural = _("Expression Policies")

View 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)

View File

@ -41,14 +41,14 @@ class TestEvaluator(TestCase):
def test_valid(self): def test_valid(self):
"""test simple value expression""" """test simple value expression"""
template = "return True" template = "return True"
evaluator = PolicyEvaluator("test") evaluator = PolicyEvaluator(self.request.user, "test")
evaluator.set_policy_request(self.request) evaluator.set_policy_request(self.request)
self.assertEqual(evaluator.evaluate(template).passing, True) self.assertEqual(evaluator.evaluate(template).passing, True)
def test_messages(self): def test_messages(self):
"""test expression with message return""" """test expression with message return"""
template = 'ak_message("some message");return False' template = 'ak_message("some message");return False'
evaluator = PolicyEvaluator("test") evaluator = PolicyEvaluator(self.request.user, "test")
evaluator.set_policy_request(self.request) evaluator.set_policy_request(self.request)
result = evaluator.evaluate(template) result = evaluator.evaluate(template)
self.assertEqual(result.passing, False) self.assertEqual(result.passing, False)
@ -57,7 +57,7 @@ class TestEvaluator(TestCase):
def test_invalid_syntax(self): def test_invalid_syntax(self):
"""test invalid syntax""" """test invalid syntax"""
template = ";" template = ";"
evaluator = PolicyEvaluator("test") evaluator = PolicyEvaluator(self.request.user, "test")
evaluator.set_policy_request(self.request) evaluator.set_policy_request(self.request)
with self.assertRaises(PolicyException): with self.assertRaises(PolicyException):
evaluator.evaluate(template) evaluator.evaluate(template)
@ -65,14 +65,14 @@ class TestEvaluator(TestCase):
def test_validate(self): def test_validate(self):
"""test validate""" """test validate"""
template = "True" template = "True"
evaluator = PolicyEvaluator("test") evaluator = PolicyEvaluator(self.request.user, "test")
result = evaluator.validate(template) result = evaluator.validate(template)
self.assertEqual(result, True) self.assertEqual(result, True)
def test_validate_invalid(self): def test_validate_invalid(self):
"""test validate""" """test validate"""
template = ";" template = ";"
evaluator = PolicyEvaluator("test") evaluator = PolicyEvaluator(self.request.user, "test")
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
evaluator.validate(template) evaluator.validate(template)
@ -83,7 +83,7 @@ class TestEvaluator(TestCase):
execution_logging=True, execution_logging=True,
expression="ak_message(request.http_request.path)\nreturn 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) evaluator.set_policy_request(self.request)
proc = PolicyProcess(PolicyBinding(policy=expr), request=self.request, connection=None) proc = PolicyProcess(PolicyBinding(policy=expr), request=self.request, connection=None)
res = proc.profiling_wrapper() res = proc.profiling_wrapper()

View File

@ -85,30 +85,19 @@ entries:
model: authentik_stages_prompt.prompt model: authentik_stages_prompt.prompt
- attrs: - attrs:
expression: | 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") prompt_data = request.context.get("prompt_data")
user_group_attributes = request.user.group_attributes(request.http_request)
if not request.user.group_attributes(request.http_request).get( if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_EMAIL, request.http_request.tenant.default_user_change_email):
USER_ATTRIBUTE_CHANGE_EMAIL, request.http_request.tenant.default_user_change_email
):
if prompt_data.get("email") != request.user.email: if prompt_data.get("email") != request.user.email:
ak_message("Not allowed to change email address.") ak_message("Not allowed to change email address.")
return False return False
if not request.user.group_attributes(request.http_request).get( if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_NAME, request.http_request.tenant.default_user_change_name):
USER_ATTRIBUTE_CHANGE_NAME, request.http_request.tenant.default_user_change_name
):
if prompt_data.get("name") != request.user.name: if prompt_data.get("name") != request.user.name:
ak_message("Not allowed to change name.") ak_message("Not allowed to change name.")
return False return False
if not request.user.group_attributes(request.http_request).get( if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_USERNAME, request.http_request.tenant.default_user_change_username):
USER_ATTRIBUTE_CHANGE_USERNAME, request.http_request.tenant.default_user_change_username
):
if prompt_data.get("username") != request.user.username: if prompt_data.get("username") != request.user.username:
ak_message("Not allowed to change username.") ak_message("Not allowed to change username.")
return False return False

View File

@ -89,10 +89,9 @@ entries:
expression: | expression: |
# This policy ensures that the setup flow can only be # This policy ensures that the setup flow can only be
# used one time # used one time
from authentik.flows.models import Flow, FlowAuthenticationRequirement FlowsApi(api).flows_instances_partial_update("initial-setup", {
Flow.objects.filter(slug="initial-setup").update( "authentication": "REQUIRE_SUPERUSER"
authentication=FlowAuthenticationRequirement.REQUIRE_SUPERUSER, })
)
return True return True
id: policy-default-oobe-flow-set-authentication id: policy-default-oobe-flow-set-authentication
identifiers: identifiers:

View File

@ -3477,6 +3477,10 @@
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
"title": "Expression" "title": "Expression"
},
"execution_user": {
"type": "integer",
"title": "Execution user"
} }
}, },
"required": [] "required": []

34
poetry.lock generated
View File

@ -269,6 +269,23 @@ tests = ["attrs[tests-no-zope]", "zope-interface"]
tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] 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]] [[package]]
name = "autobahn" name = "autobahn"
version = "23.6.2" version = "23.6.2"
@ -3385,6 +3402,21 @@ requests = ">=2.0.0"
[package.extras] [package.extras]
rsa = ["oauthlib[signedtoken] (>=3.0.0)"] 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]] [[package]]
name = "rich" name = "rich"
version = "13.7.0" version = "13.7.0"
@ -4683,4 +4715,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "~3.12" python-versions = "~3.12"
content-hash = "a5774b4e09217805c887700b8a0f457a39c7af40ca59823f00c1f6e8678469e1" content-hash = "647a426ecb06caf2b8f3165c4e85ed0439568e692b6ade8c7933683985c90b18"

View File

@ -83,6 +83,7 @@ filterwarnings = [
] ]
[tool.poetry.dependencies] [tool.poetry.dependencies]
authentik_client = "2024.4.1.post1714149882"
argon2-cffi = "*" argon2-cffi = "*"
celery = "*" celery = "*"
channels = { version = "*", extras = ["daphne"] } channels = { version = "*", extras = ["daphne"] }
@ -132,6 +133,7 @@ pyjwt = "*"
python = "~3.12" python = "~3.12"
pyyaml = "*" pyyaml = "*"
requests-oauthlib = "*" requests-oauthlib = "*"
restrictedpython = "*"
scim2-filter-parser = "*" scim2-filter-parser = "*"
sentry-sdk = "*" sentry-sdk = "*"
service_identity = "*" service_identity = "*"

View File

@ -11852,6 +11852,10 @@ paths:
name: execution_logging name: execution_logging
schema: schema:
type: boolean type: boolean
- in: query
name: execution_user
schema:
type: integer
- in: query - in: query
name: expression name: expression
schema: schema:
@ -33565,6 +33569,9 @@ components:
readOnly: true readOnly: true
expression: expression:
type: string type: string
execution_user:
type: integer
nullable: true
required: required:
- bound_to - bound_to
- component - component
@ -33588,6 +33595,9 @@ components:
expression: expression:
type: string type: string
minLength: 1 minLength: 1
execution_user:
type: integer
nullable: true
required: required:
- expression - expression
- name - name
@ -38841,6 +38851,9 @@ components:
expression: expression:
type: string type: string
minLength: 1 minLength: 1
execution_user:
type: integer
nullable: true
PatchedFlowRequest: PatchedFlowRequest:
type: object type: object
description: Flow Serializer description: Flow Serializer

View File

@ -12,7 +12,13 @@ import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.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") @customElement("ak-policy-expression-form")
export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> { export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
@ -92,6 +98,39 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
</a> </a>
</p> </p>
</ak-form-element-horizontal> </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> </div>
</ak-form-group>`; </ak-form-group>`;
} }

View File

@ -44,7 +44,7 @@ export function docLink(path: string): string {
const ak = globalAK(); const ak = globalAK();
// Default case or beta build which should always point to latest // Default case or beta build which should always point to latest
if (!ak || ak.build !== "") { if (!ak || ak.build !== "") {
return `https://goauthentik.io${path}`; return `https://docs.goauthentik.io${path}`;
} }
return `https://${ak.versionSubdomain}.goauthentik.io${path}`; return `https://${ak.versionSubdomain}.goauthentik.io${path}`;
} }

View File

@ -13,14 +13,12 @@ Depending on what kind of device you want to require the user to have:
#### WebAuthn #### WebAuthn
```python ```python
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
return WebAuthnDevice.objects.filter(user=request.context['pending_user'], confirmed=True).exists() return WebAuthnDevice.objects.filter(user=request.context['pending_user'], confirmed=True).exists()
``` ```
#### Duo #### Duo
```python ```python
from authentik.stages.authenticator_duo.models import DuoDevice
return DuoDevice.objects.filter(user=request.context['pending_user'], confirmed=True).exists() return DuoDevice.objects.filter(user=request.context['pending_user'], confirmed=True).exists()
``` ```

View File

@ -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 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 ```python
from authentik.core.models import Group
group, _ = Group.objects.get_or_create(name="some-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. # ["groups"] *must* be set to an array of Group objects, names alone are not enough.
request.context["flow_plan"].context["groups"] = [group] request.context["flow_plan"].context["groups"] = [group]