Compare commits
11 Commits
main
...
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