mirror of
https://github.com/goauthentik/authentik
synced 2026-05-05 22:52:42 +02:00
Compare commits
2 Commits
upgr_pyjwt
...
feature/ag
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36ca27a979 | ||
|
|
a6b95c15db |
@@ -1,5 +1,6 @@
|
||||
"""Tokens API Viewset"""
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from django.utils.timezone import now
|
||||
@@ -18,12 +19,14 @@ from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK,
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
|
||||
Token,
|
||||
TokenIntents,
|
||||
User,
|
||||
default_token_duration,
|
||||
default_token_key,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import model_to_dict
|
||||
@@ -171,6 +174,40 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
||||
Event.new(EventAction.SECRET_VIEW, secret=token).from_http(request) # noqa # nosec
|
||||
return Response(TokenViewSerializer({"key": token.key}).data)
|
||||
|
||||
@extend_schema(
|
||||
request=None,
|
||||
responses={
|
||||
200: TokenViewSerializer(many=False),
|
||||
403: OpenApiResponse(description="Not the token owner, agent owner, or superuser"),
|
||||
404: OpenApiResponse(description="Token not found"),
|
||||
},
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
|
||||
def rotate(self, request: Request, identifier: str) -> Response:
|
||||
"""Rotate the token key and reset the expiry to 24 hours. Only callable by the token
|
||||
owner, the owning agent's human owner, or a superuser."""
|
||||
token = (
|
||||
Token.objects.including_expired()
|
||||
.select_related("user")
|
||||
.filter(identifier=identifier)
|
||||
.first()
|
||||
)
|
||||
if not token:
|
||||
return Response(status=404)
|
||||
|
||||
if not request.user.is_superuser:
|
||||
is_token_owner = token.user_id == request.user.pk
|
||||
owner_pk = token.user.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
is_agent_owner = owner_pk and str(request.user.pk) == owner_pk
|
||||
if not is_token_owner and not is_agent_owner:
|
||||
return Response(status=403)
|
||||
|
||||
token.key = default_token_key()
|
||||
token.expires = now() + timedelta(hours=24)
|
||||
token.save()
|
||||
Event.new(EventAction.SECRET_ROTATE, secret=token).from_http(request) # noqa # nosec
|
||||
return Response(TokenViewSerializer({"key": token.key}).data)
|
||||
|
||||
@permission_required("authentik_core.set_token_key")
|
||||
@extend_schema(
|
||||
request=TokenSetKeySerializer(),
|
||||
|
||||
@@ -75,9 +75,12 @@ from authentik.core.middleware import (
|
||||
SESSION_KEY_IMPERSONATE_USER,
|
||||
)
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_AGENT_ALLOWED_APPS,
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK,
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||
USER_PATH_SERVICE_ACCOUNT,
|
||||
USERNAME_MAX_LENGTH,
|
||||
Application,
|
||||
Group,
|
||||
Session,
|
||||
Token,
|
||||
@@ -88,6 +91,7 @@ from authentik.core.models import (
|
||||
)
|
||||
from authentik.endpoints.connectors.agent.auth import AgentAuth
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import model_to_dict, sanitize_dict
|
||||
from authentik.flows.exceptions import FlowNonApplicableException
|
||||
from authentik.flows.models import FlowToken
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||
@@ -249,8 +253,28 @@ class UserSerializer(ModelSerializer):
|
||||
raise ValidationError(_("Can't change internal service account to other user type."))
|
||||
if not self.instance and user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT.value:
|
||||
raise ValidationError(_("Setting a user to internal service account is not allowed."))
|
||||
if (
|
||||
self.instance
|
||||
and self.instance.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
and user_type != UserTypes.INTERNAL.value
|
||||
):
|
||||
raise ValidationError(_("Can't change agent user to non-internal type."))
|
||||
return user_type
|
||||
|
||||
def validate_attributes(self, attrs: dict) -> dict:
|
||||
"""Prevent removal of agent owner attribute (agents must keep their owner)"""
|
||||
if not self.instance:
|
||||
return attrs
|
||||
existing_owner = self.instance.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
if not existing_owner:
|
||||
return attrs
|
||||
new_owner = attrs.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
if not new_owner:
|
||||
raise ValidationError(_("Can't remove agent marker from agent user."))
|
||||
if new_owner != existing_owner:
|
||||
raise ValidationError(_("Can't change owner of agent user."))
|
||||
return attrs
|
||||
|
||||
def validate(self, attrs: dict) -> dict:
|
||||
if self.instance and self.instance.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
|
||||
raise ValidationError(_("Can't modify internal service account users"))
|
||||
@@ -405,6 +429,26 @@ class UserServiceAccountSerializer(PassiveSerializer):
|
||||
)
|
||||
|
||||
|
||||
class UserAgentSerializer(PassiveSerializer):
|
||||
"""Payload to create an agent user"""
|
||||
|
||||
name = CharField(max_length=150)
|
||||
owner = PrimaryKeyRelatedField(queryset=User.objects.all(), required=False, default=None)
|
||||
|
||||
|
||||
class UserAgentAllowedAppsSerializer(PassiveSerializer):
|
||||
"""Payload to replace an agent's allowed applications"""
|
||||
|
||||
allowed_apps = ListField(child=UUIDField())
|
||||
|
||||
|
||||
class UserAgentAllowedAppSerializer(PassiveSerializer):
|
||||
"""Payload to add or remove a single allowed application"""
|
||||
|
||||
app = UUIDField()
|
||||
action = ChoiceField(choices=[("add", "Add"), ("remove", "Remove")])
|
||||
|
||||
|
||||
class UserRecoveryLinkSerializer(PassiveSerializer):
|
||||
"""Payload to create a recovery link"""
|
||||
|
||||
@@ -691,6 +735,265 @@ class UserViewSet(
|
||||
status=500,
|
||||
)
|
||||
|
||||
@permission_required(
|
||||
None,
|
||||
[
|
||||
"authentik_core.add_user",
|
||||
"authentik_core.add_token",
|
||||
"authentik_core.add_agent_user",
|
||||
],
|
||||
)
|
||||
@extend_schema(
|
||||
request=UserAgentSerializer,
|
||||
responses={
|
||||
200: inline_serializer(
|
||||
"UserAgentResponse",
|
||||
{
|
||||
"username": CharField(required=True),
|
||||
"token": CharField(required=True),
|
||||
"user_uid": CharField(required=True),
|
||||
"user_pk": IntegerField(required=True),
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["POST"],
|
||||
pagination_class=None,
|
||||
filter_backends=[],
|
||||
)
|
||||
@validate(UserAgentSerializer)
|
||||
def agent(self, request: Request, body: UserAgentSerializer) -> Response:
|
||||
"""Create a new agent user. Enterprise only. Caller must be an internal user.
|
||||
Agent users are internal users with an owner attribute that grants scoped
|
||||
application access on behalf of the owner."""
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
|
||||
if not LicenseKey.cached_summary().status.is_valid:
|
||||
raise ValidationError(_("Enterprise is required to use this endpoint."))
|
||||
|
||||
if request.user.type != UserTypes.INTERNAL:
|
||||
raise ValidationError(_("Only internal users can create agent users."))
|
||||
|
||||
requested_owner = body.validated_data.get("owner")
|
||||
if requested_owner and not request.user.is_superuser:
|
||||
if requested_owner.pk != request.user.pk:
|
||||
raise ValidationError(
|
||||
_("Non-superusers can only create agents owned by themselves.")
|
||||
)
|
||||
owner = requested_owner or request.user
|
||||
|
||||
username = body.validated_data["name"]
|
||||
with atomic():
|
||||
try:
|
||||
user: User = User.objects.create(
|
||||
username=username,
|
||||
name=username,
|
||||
type=UserTypes.INTERNAL,
|
||||
attributes={
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk),
|
||||
USER_ATTRIBUTE_AGENT_ALLOWED_APPS: [],
|
||||
},
|
||||
)
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
|
||||
token = Token.objects.create(
|
||||
identifier=slugify(f"agent-{username}-token"),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
user=user,
|
||||
expires=now() + timedelta(hours=24),
|
||||
expiring=True,
|
||||
)
|
||||
user.assign_perms_to_managed_role("authentik_core.view_token_key", token)
|
||||
|
||||
owner.assign_perms_to_managed_role("authentik_core.view_user", user)
|
||||
owner.assign_perms_to_managed_role("authentik_core.change_user", user)
|
||||
owner.assign_perms_to_managed_role("authentik_core.delete_user", user)
|
||||
owner.assign_perms_to_managed_role("authentik_core.view_user_applications", user)
|
||||
|
||||
Event.new(
|
||||
EventAction.MODEL_CREATED,
|
||||
model=sanitize_dict(model_to_dict(user)),
|
||||
agent_owner=sanitize_dict(model_to_dict(owner)),
|
||||
).from_http(request)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"username": user.username,
|
||||
"user_uid": user.uid,
|
||||
"user_pk": user.pk,
|
||||
"token": token.key,
|
||||
}
|
||||
)
|
||||
except IntegrityError as exc:
|
||||
error_msg = str(exc).lower()
|
||||
if "unique" in error_msg:
|
||||
return Response(
|
||||
data={"non_field_errors": [_("A user with this username already exists")]},
|
||||
status=400,
|
||||
)
|
||||
else:
|
||||
LOGGER.warning("Agent user creation failed", exc=exc)
|
||||
return Response(
|
||||
data={"non_field_errors": [_("Unable to create user")]},
|
||||
status=400,
|
||||
)
|
||||
except (ValueError, TypeError) as exc:
|
||||
LOGGER.error("Unexpected error during agent user creation", exc=exc)
|
||||
return Response(
|
||||
data={"non_field_errors": [_("Unknown error occurred")]},
|
||||
status=500,
|
||||
)
|
||||
|
||||
@extend_schema(
|
||||
request=UserAgentAllowedAppsSerializer,
|
||||
responses={
|
||||
200: UserAgentAllowedAppsSerializer,
|
||||
400: OpenApiResponse(description="Invalid app UUIDs or owner lacks access"),
|
||||
403: OpenApiResponse(description="Not the agent's owner or superuser"),
|
||||
},
|
||||
)
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["PUT"],
|
||||
url_path="agent_allowed_apps",
|
||||
url_name="agent-allowed-apps",
|
||||
pagination_class=None,
|
||||
filter_backends=[],
|
||||
)
|
||||
@validate(UserAgentAllowedAppsSerializer)
|
||||
def agent_allowed_apps(
|
||||
self, request: Request, pk: int, body: UserAgentAllowedAppsSerializer
|
||||
) -> Response:
|
||||
"""Replace the allowed application list for an agent user.
|
||||
Caller must be the agent's owner or a superuser."""
|
||||
from authentik.core.apps import AppAccessWithoutBindings
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
|
||||
agent, owner = self._get_agent_and_owner(request)
|
||||
|
||||
app_uuids = body.validated_data["allowed_apps"]
|
||||
errors = []
|
||||
for app_uuid in app_uuids:
|
||||
try:
|
||||
app = Application.objects.get(pk=app_uuid)
|
||||
except Application.DoesNotExist:
|
||||
errors.append(str(app_uuid))
|
||||
continue
|
||||
engine = PolicyEngine(app, owner, request)
|
||||
engine.empty_result = AppAccessWithoutBindings.get()
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
if not engine.passing:
|
||||
errors.append(str(app_uuid))
|
||||
|
||||
if errors:
|
||||
return Response(
|
||||
data={
|
||||
"allowed_apps": [
|
||||
_(
|
||||
"Owner does not have access to application %(uuid)s "
|
||||
"or application does not exist."
|
||||
)
|
||||
% {"uuid": uuid}
|
||||
for uuid in errors
|
||||
]
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS] = [str(u) for u in app_uuids]
|
||||
agent.save(update_fields=["attributes"])
|
||||
return Response({"allowed_apps": [str(u) for u in app_uuids]})
|
||||
|
||||
@extend_schema(
|
||||
request=UserAgentAllowedAppSerializer,
|
||||
responses={
|
||||
200: UserAgentAllowedAppsSerializer,
|
||||
204: OpenApiResponse(description="Application removed"),
|
||||
400: OpenApiResponse(description="Invalid app UUID or owner lacks access"),
|
||||
403: OpenApiResponse(description="Not the agent's owner or superuser"),
|
||||
},
|
||||
)
|
||||
@action(
|
||||
detail=True,
|
||||
methods=["PATCH"],
|
||||
url_path="agent_allowed_app",
|
||||
url_name="agent-allowed-app",
|
||||
pagination_class=None,
|
||||
filter_backends=[],
|
||||
)
|
||||
@validate(UserAgentAllowedAppSerializer)
|
||||
def agent_allowed_app(
|
||||
self, request: Request, pk: int, body: UserAgentAllowedAppSerializer
|
||||
) -> Response:
|
||||
"""Add or remove a single application from an agent's allowed list.
|
||||
Caller must be the agent's owner or a superuser."""
|
||||
from authentik.core.apps import AppAccessWithoutBindings
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
|
||||
agent, owner = self._get_agent_and_owner(request)
|
||||
|
||||
app_uuid = str(body.validated_data["app"])
|
||||
action = body.validated_data["action"]
|
||||
current = agent.attributes.get(USER_ATTRIBUTE_AGENT_ALLOWED_APPS, [])
|
||||
|
||||
if action == "add":
|
||||
try:
|
||||
app = Application.objects.get(pk=app_uuid)
|
||||
except Application.DoesNotExist:
|
||||
return Response(
|
||||
data={"app": [_("Application does not exist.")]},
|
||||
status=400,
|
||||
)
|
||||
engine = PolicyEngine(app, owner, request)
|
||||
engine.empty_result = AppAccessWithoutBindings.get()
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
if not engine.passing:
|
||||
return Response(
|
||||
data={"app": [_("Owner does not have access to this application.")]},
|
||||
status=400,
|
||||
)
|
||||
if app_uuid not in current:
|
||||
current.append(app_uuid)
|
||||
agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS] = current
|
||||
agent.save(update_fields=["attributes"])
|
||||
return Response({"allowed_apps": current})
|
||||
|
||||
if action == "remove":
|
||||
if app_uuid in current:
|
||||
current.remove(app_uuid)
|
||||
agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS] = current
|
||||
agent.save(update_fields=["attributes"])
|
||||
return Response(status=204)
|
||||
|
||||
return Response(
|
||||
data={"action": [_("Invalid action.")]},
|
||||
status=400,
|
||||
)
|
||||
|
||||
def _get_agent_and_owner(self, request: Request) -> tuple[User, User]:
|
||||
"""Validate that the target is an agent user and the caller is authorized."""
|
||||
agent: User = self.get_object()
|
||||
|
||||
owner_pk = agent.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
if not owner_pk:
|
||||
raise ValidationError(_("User is not an agent user."))
|
||||
|
||||
is_owner = str(request.user.pk) == owner_pk
|
||||
if not request.user.is_superuser and not is_owner:
|
||||
raise ValidationError(_("Not the agent's owner or superuser."))
|
||||
|
||||
try:
|
||||
owner = User.objects.get(pk=owner_pk)
|
||||
except User.DoesNotExist as exc:
|
||||
raise ValidationError(_("Agent owner not found.")) from exc
|
||||
|
||||
return agent, owner
|
||||
|
||||
@extend_schema(responses={200: SessionUserSerializer(many=False)})
|
||||
@action(
|
||||
url_path="me",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Change user type"""
|
||||
|
||||
from authentik.core.models import User, UserTypes
|
||||
from authentik.core.models import USER_ATTRIBUTE_AGENT_OWNER_PK, User, UserTypes
|
||||
from authentik.tenants.management import TenantCommand
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ class Command(TenantCommand):
|
||||
User.objects.exclude_anonymous()
|
||||
.exclude(type=UserTypes.SERVICE_ACCOUNT)
|
||||
.exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
|
||||
.exclude(attributes__has_key=USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
)
|
||||
if options["usernames"] and options["all"]:
|
||||
self.stderr.write("--all and usernames specified, only one can be specified")
|
||||
|
||||
27
authentik/core/migrations/0058_alter_user_options.py
Normal file
27
authentik/core/migrations/0058_alter_user_options.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 5.2.13 on 2026-04-16 12:00
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0057_remove_user_groups_remove_user_user_permissions_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="user",
|
||||
options={
|
||||
"permissions": [
|
||||
("reset_user_password", "Reset Password"),
|
||||
("impersonate", "Can impersonate other users"),
|
||||
("preview_user", "Can preview user data sent to providers"),
|
||||
("view_user_applications", "View applications the user has access to"),
|
||||
("add_agent_user", "Can create agent users"),
|
||||
],
|
||||
"verbose_name": "User",
|
||||
"verbose_name_plural": "Users",
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -67,6 +67,9 @@ USER_ATTRIBUTE_CHANGE_USERNAME = f"{_USER_ATTR_PREFIX}/can-change-username"
|
||||
USER_ATTRIBUTE_CHANGE_NAME = f"{_USER_ATTR_PREFIX}/can-change-name"
|
||||
USER_ATTRIBUTE_CHANGE_EMAIL = f"{_USER_ATTR_PREFIX}/can-change-email"
|
||||
USER_PATH_SERVICE_ACCOUNT = f"{USER_PATH_SYSTEM_PREFIX}/service-accounts"
|
||||
_USER_ATTR_AGENT_PREFIX = f"{USER_PATH_SYSTEM_PREFIX}/agent"
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK = f"{_USER_ATTR_AGENT_PREFIX}/owner-pk"
|
||||
USER_ATTRIBUTE_AGENT_ALLOWED_APPS = f"{_USER_ATTR_AGENT_PREFIX}/allowed-apps"
|
||||
|
||||
options.DEFAULT_NAMES = options.DEFAULT_NAMES + (
|
||||
# used_by API that allows models to specify if they shadow an object
|
||||
@@ -385,6 +388,7 @@ class User(SerializerModel, AttributesMixin, AbstractUser):
|
||||
("impersonate", _("Can impersonate other users")),
|
||||
("preview_user", _("Can preview user data sent to providers")),
|
||||
("view_user_applications", _("View applications the user has access to")),
|
||||
("add_agent_user", _("Can create agent users")),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(fields=["last_login"]),
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.http.request import HttpRequest
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK,
|
||||
Application,
|
||||
AuthenticatedSession,
|
||||
BackchannelProvider,
|
||||
@@ -69,6 +70,34 @@ def authenticated_session_delete(sender: type[Model], instance: AuthenticatedSes
|
||||
Session.objects.filter(session_key=instance.pk).delete()
|
||||
|
||||
|
||||
def _agent_qs_for_owner(owner_pk: int):
|
||||
"""Return a queryset of agent users belonging to the given owner pk"""
|
||||
return User.objects.filter(
|
||||
attributes__contains={USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner_pk)},
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=User)
|
||||
def user_delete_cascade_agents(sender: type[Model], instance: User, **_):
|
||||
"""Delete agent users when their owner is deleted"""
|
||||
_agent_qs_for_owner(instance.pk).delete()
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def user_save_propagate_agent_active(
|
||||
sender: type[Model], instance: User, update_fields: frozenset[str] | None = None, **_
|
||||
):
|
||||
"""Propagate is_active changes to owned agent users"""
|
||||
if update_fields is not None and "is_active" not in update_fields:
|
||||
return
|
||||
agents = _agent_qs_for_owner(instance.pk)
|
||||
if not instance.is_active:
|
||||
Session.objects.filter(
|
||||
authenticatedsession__user__in=agents.filter(is_active=True)
|
||||
).delete()
|
||||
agents.update(is_active=instance.is_active)
|
||||
|
||||
|
||||
@receiver(pre_save)
|
||||
def backchannel_provider_pre_save(sender: type[Model], instance: Model, **_):
|
||||
"""Ensure backchannel providers have is_backchannel set to true"""
|
||||
|
||||
82
authentik/core/tests/test_agent_session.py
Normal file
82
authentik/core/tests/test_agent_session.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Test agent token-to-session exchange"""
|
||||
|
||||
from django.urls.base import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK,
|
||||
Token,
|
||||
TokenIntents,
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
|
||||
class TestAgentSession(APITestCase):
|
||||
"""Test agent token-to-session exchange"""
|
||||
|
||||
def _create_agent_with_token(self):
|
||||
owner = create_test_user()
|
||||
agent = User.objects.create(
|
||||
username=generate_id(),
|
||||
type=UserTypes.INTERNAL,
|
||||
attributes={USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk)},
|
||||
)
|
||||
agent.set_unusable_password()
|
||||
agent.save()
|
||||
token = Token.objects.create(
|
||||
identifier=generate_id(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
user=agent,
|
||||
expiring=True,
|
||||
)
|
||||
return owner, agent, token
|
||||
|
||||
def test_session_exchange_success(self):
|
||||
"""Valid agent token creates a session"""
|
||||
_owner, _agent, token = self._create_agent_with_token()
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agent-session"),
|
||||
data={"key": token.key},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
def test_session_exchange_invalid_token(self):
|
||||
"""Invalid token key is rejected"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agent-session"),
|
||||
data={"key": "nonexistent-key"},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_session_exchange_non_agent(self):
|
||||
"""Token belonging to a non-agent user is rejected"""
|
||||
user = create_test_user()
|
||||
token = Token.objects.create(
|
||||
identifier=generate_id(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
user=user,
|
||||
expiring=True,
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agent-session"),
|
||||
data={"key": token.key},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_session_exchange_inactive_agent(self):
|
||||
"""Inactive agent is rejected"""
|
||||
_owner, agent, token = self._create_agent_with_token()
|
||||
agent.is_active = False
|
||||
agent.save(update_fields=["is_active"])
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:agent-session"),
|
||||
data={"key": token.key},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
@@ -199,6 +199,50 @@ class TestTokenAPI(APITestCase):
|
||||
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
|
||||
self.assertEqual(body["results"][1]["identifier"], token_should_not.identifier)
|
||||
|
||||
def test_token_rotate_by_owner(self):
|
||||
"""Token owner can rotate their own token"""
|
||||
token = Token.objects.create(
|
||||
identifier=generate_id(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
user=self.user,
|
||||
expiring=True,
|
||||
)
|
||||
original_key = token.key
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:token-rotate", kwargs={"identifier": token.identifier}),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
token.refresh_from_db()
|
||||
self.assertNotEqual(token.key, original_key)
|
||||
self.assertEqual(token.key, loads(response.content)["key"])
|
||||
|
||||
def test_token_rotate_by_superuser(self):
|
||||
"""Superuser can rotate any token"""
|
||||
token = Token.objects.create(
|
||||
identifier=generate_id(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
user=self.user,
|
||||
expiring=True,
|
||||
)
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:token-rotate", kwargs={"identifier": token.identifier}),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_token_rotate_unauthorized(self):
|
||||
"""Non-owner cannot rotate another user's token"""
|
||||
token = Token.objects.create(
|
||||
identifier=generate_id(),
|
||||
intent=TokenIntents.INTENT_API,
|
||||
user=self.admin,
|
||||
expiring=True,
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:token-rotate", kwargs={"identifier": token.identifier}),
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_serializer_no_request(self):
|
||||
"""Test serializer without request"""
|
||||
self.assertTrue(
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
from django.test.testcases import TestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK,
|
||||
AuthenticatedSession,
|
||||
Session,
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.events.models import Event
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
@@ -33,3 +39,92 @@ class TestUsers(TestCase):
|
||||
self.assertEqual(Event.objects.count(), 1)
|
||||
user.ak_groups.all()
|
||||
self.assertEqual(Event.objects.count(), 1)
|
||||
|
||||
|
||||
class TestAgentUserSignals(TestCase):
|
||||
"""Test signals related to agent user lifecycle"""
|
||||
|
||||
def _create_owner(self):
|
||||
owner = User.objects.create(username=generate_id())
|
||||
owner.set_unusable_password()
|
||||
owner.save()
|
||||
return owner
|
||||
|
||||
def _create_agent(self, owner):
|
||||
agent = User.objects.create(
|
||||
username=generate_id(),
|
||||
type=UserTypes.INTERNAL,
|
||||
attributes={USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk)},
|
||||
)
|
||||
agent.set_unusable_password()
|
||||
agent.save()
|
||||
return agent
|
||||
|
||||
def test_delete_owner_cascades_to_agents(self):
|
||||
"""Deleting an owner also deletes all their agent users"""
|
||||
owner = self._create_owner()
|
||||
agent1 = self._create_agent(owner)
|
||||
agent2 = self._create_agent(owner)
|
||||
other_owner = self._create_owner()
|
||||
other_agent = self._create_agent(other_owner)
|
||||
|
||||
owner.delete()
|
||||
|
||||
self.assertFalse(User.objects.filter(pk=agent1.pk).exists())
|
||||
self.assertFalse(User.objects.filter(pk=agent2.pk).exists())
|
||||
self.assertTrue(User.objects.filter(pk=other_agent.pk).exists())
|
||||
|
||||
def test_deactivate_owner_deactivates_agents(self):
|
||||
"""Setting an owner inactive also marks all their agents inactive"""
|
||||
owner = self._create_owner()
|
||||
agent = self._create_agent(owner)
|
||||
|
||||
owner.is_active = False
|
||||
owner.save(update_fields=["is_active"])
|
||||
|
||||
agent.refresh_from_db()
|
||||
self.assertFalse(agent.is_active)
|
||||
|
||||
def test_reactivate_owner_reactivates_agents(self):
|
||||
"""Setting an owner active again also re-activates their agents"""
|
||||
owner = self._create_owner()
|
||||
owner.is_active = False
|
||||
owner.save(update_fields=["is_active"])
|
||||
agent = self._create_agent(owner)
|
||||
agent.is_active = False
|
||||
agent.save(update_fields=["is_active"])
|
||||
|
||||
owner.is_active = True
|
||||
owner.save(update_fields=["is_active"])
|
||||
|
||||
agent.refresh_from_db()
|
||||
self.assertTrue(agent.is_active)
|
||||
|
||||
def test_unrelated_owner_save_does_not_affect_agents(self):
|
||||
"""Saving an owner without changing is_active does not touch agents"""
|
||||
owner = self._create_owner()
|
||||
agent = self._create_agent(owner)
|
||||
agent.is_active = False
|
||||
agent.save(update_fields=["is_active"])
|
||||
|
||||
owner.name = generate_id()
|
||||
owner.save(update_fields=["name"])
|
||||
|
||||
agent.refresh_from_db()
|
||||
self.assertFalse(agent.is_active)
|
||||
|
||||
def test_deactivate_owner_clears_agent_sessions(self):
|
||||
"""Deactivating an owner removes authenticated sessions for their agents"""
|
||||
owner = self._create_owner()
|
||||
agent = self._create_agent(owner)
|
||||
session = Session.objects.create(
|
||||
session_key=generate_id(),
|
||||
last_ip="255.255.255.255",
|
||||
last_user_agent="",
|
||||
)
|
||||
AuthenticatedSession.objects.create(user=agent, session=session)
|
||||
|
||||
owner.is_active = False
|
||||
owner.save(update_fields=["is_active"])
|
||||
|
||||
self.assertFalse(Session.objects.filter(pk=session.pk).exists())
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from json import loads
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.urls.base import reverse
|
||||
from django.utils.timezone import now
|
||||
@@ -9,10 +10,14 @@ from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_AGENT_ALLOWED_APPS,
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK,
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||
Application,
|
||||
AuthenticatedSession,
|
||||
Session,
|
||||
Token,
|
||||
TokenIntents,
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
@@ -878,3 +883,244 @@ class TestUsersAPI(APITestCase):
|
||||
self.assertIn(user2.pk, pks)
|
||||
# Verify user2 comes before user1 in descending order
|
||||
self.assertLess(pks.index(user2.pk), pks.index(user1.pk))
|
||||
|
||||
|
||||
class TestAgentUserAPI(APITestCase):
|
||||
"""Test agent user API"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.admin = create_test_admin_user()
|
||||
self.user = create_test_user()
|
||||
self.owner = create_test_user()
|
||||
self.owner.assign_perms_to_managed_role("authentik_core.add_agent_user")
|
||||
self.owner.assign_perms_to_managed_role("authentik_core.add_user")
|
||||
self.owner.assign_perms_to_managed_role("authentik_core.add_token")
|
||||
|
||||
def _create_agent(self, name="test-agent", owner=None):
|
||||
owner = owner or self.admin
|
||||
agent = User.objects.create(
|
||||
username=name,
|
||||
name=name,
|
||||
type=UserTypes.INTERNAL,
|
||||
attributes={
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk),
|
||||
USER_ATTRIBUTE_AGENT_ALLOWED_APPS: [],
|
||||
},
|
||||
)
|
||||
agent.set_unusable_password()
|
||||
agent.save()
|
||||
return agent
|
||||
|
||||
def test_agent_create(self):
|
||||
"""Non-admin owner with correct permissions can create an agent"""
|
||||
self.client.force_login(self.owner)
|
||||
with patch(
|
||||
"authentik.enterprise.license.LicenseKey.cached_summary",
|
||||
MagicMock(return_value=MagicMock(status=MagicMock(is_valid=True))),
|
||||
):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-agent"),
|
||||
data={"name": "test-agent"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
agent = User.objects.get(username="test-agent")
|
||||
self.assertEqual(agent.type, UserTypes.INTERNAL)
|
||||
self.assertEqual(agent.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK), str(self.owner.pk))
|
||||
self.assertEqual(agent.attributes.get(USER_ATTRIBUTE_AGENT_ALLOWED_APPS), [])
|
||||
self.assertFalse(agent.has_usable_password())
|
||||
token = Token.objects.filter(user=agent, intent=TokenIntents.INTENT_API).first()
|
||||
self.assertIsNotNone(token)
|
||||
self.assertTrue(token.expiring)
|
||||
|
||||
def test_agent_create_no_license(self):
|
||||
"""Agent creation is rejected without a valid enterprise license"""
|
||||
self.client.force_login(self.owner)
|
||||
with patch(
|
||||
"authentik.enterprise.license.LicenseKey.cached_summary",
|
||||
MagicMock(return_value=MagicMock(status=MagicMock(is_valid=False))),
|
||||
):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-agent"),
|
||||
data={"name": "test-agent"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_agent_create_non_internal_user(self):
|
||||
"""Only internal users can create agent users"""
|
||||
self.owner.type = UserTypes.EXTERNAL
|
||||
self.owner.save(update_fields=["type"])
|
||||
self.client.force_login(self.owner)
|
||||
with patch(
|
||||
"authentik.enterprise.license.LicenseKey.cached_summary",
|
||||
MagicMock(return_value=MagicMock(status=MagicMock(is_valid=True))),
|
||||
):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-agent"),
|
||||
data={"name": "test-agent"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_agent_create_no_permission(self):
|
||||
"""User without add_agent_user permission is rejected"""
|
||||
self.client.force_login(self.user)
|
||||
with patch(
|
||||
"authentik.enterprise.license.LicenseKey.cached_summary",
|
||||
MagicMock(return_value=MagicMock(status=MagicMock(is_valid=True))),
|
||||
):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-agent"),
|
||||
data={"name": "test-agent"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_agent_create_duplicate(self):
|
||||
"""Duplicate agent username returns a user-friendly error"""
|
||||
self._create_agent("test-agent-dup")
|
||||
self.client.force_login(self.owner)
|
||||
with patch(
|
||||
"authentik.enterprise.license.LicenseKey.cached_summary",
|
||||
MagicMock(return_value=MagicMock(status=MagicMock(is_valid=True))),
|
||||
):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-agent"),
|
||||
data={"name": "test-agent-dup"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_agent_type_cannot_be_changed(self):
|
||||
"""Agent user type cannot be changed via the users API"""
|
||||
agent = self._create_agent()
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.patch(
|
||||
reverse("authentik_api:user-detail", kwargs={"pk": agent.pk}),
|
||||
data={"type": UserTypes.EXTERNAL},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_agent_owner_cannot_be_changed(self):
|
||||
"""Agent owner cannot be changed via the users API"""
|
||||
agent = self._create_agent()
|
||||
other = create_test_user()
|
||||
self.client.force_login(self.admin)
|
||||
new_attrs = dict(agent.attributes)
|
||||
new_attrs[USER_ATTRIBUTE_AGENT_OWNER_PK] = str(other.pk)
|
||||
response = self.client.patch(
|
||||
reverse("authentik_api:user-detail", kwargs={"pk": agent.pk}),
|
||||
data={"attributes": new_attrs},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_agent_marker_cannot_be_removed(self):
|
||||
"""Removing the agent owner attribute is rejected"""
|
||||
agent = self._create_agent()
|
||||
self.client.force_login(self.admin)
|
||||
new_attrs = dict(agent.attributes)
|
||||
del new_attrs[USER_ATTRIBUTE_AGENT_OWNER_PK]
|
||||
response = self.client.patch(
|
||||
reverse("authentik_api:user-detail", kwargs={"pk": agent.pk}),
|
||||
data={"attributes": new_attrs},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_agent_allowed_apps_update(self):
|
||||
"""Owner can update the agent's allowed apps list"""
|
||||
agent = self._create_agent(owner=self.admin)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.put(
|
||||
reverse("authentik_api:user-agent-allowed-apps", kwargs={"pk": agent.pk}),
|
||||
data={"allowed_apps": [str(app.pk)]},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
agent.refresh_from_db()
|
||||
self.assertIn(str(app.pk), agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS])
|
||||
|
||||
def test_agent_allowed_apps_update_unauthorized(self):
|
||||
"""Non-owner, non-superuser is rejected when updating allowed apps"""
|
||||
other = create_test_user()
|
||||
agent = self._create_agent(owner=other)
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.put(
|
||||
reverse("authentik_api:user-agent-allowed-apps", kwargs={"pk": agent.pk}),
|
||||
data={"allowed_apps": []},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_agent_allowed_apps_update_non_agent(self):
|
||||
"""Endpoint rejects non-agent users"""
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.put(
|
||||
reverse("authentik_api:user-agent-allowed-apps", kwargs={"pk": self.user.pk}),
|
||||
data={"allowed_apps": []},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_agent_allowed_app_add(self):
|
||||
"""PATCH add: owner can add a single app to agent's allowed list"""
|
||||
agent = self._create_agent(owner=self.admin)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.patch(
|
||||
reverse("authentik_api:user-agent-allowed-app", kwargs={"pk": agent.pk}),
|
||||
data={"app": str(app.pk), "action": "add"},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
agent.refresh_from_db()
|
||||
self.assertIn(str(app.pk), agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS])
|
||||
|
||||
def test_agent_allowed_app_remove(self):
|
||||
"""PATCH remove: owner can remove a single app from agent's allowed list"""
|
||||
agent = self._create_agent(owner=self.admin)
|
||||
app = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||
agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS] = [str(app.pk)]
|
||||
agent.save(update_fields=["attributes"])
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.patch(
|
||||
reverse("authentik_api:user-agent-allowed-app", kwargs={"pk": agent.pk}),
|
||||
data={"app": str(app.pk), "action": "remove"},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
agent.refresh_from_db()
|
||||
self.assertNotIn(str(app.pk), agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS])
|
||||
|
||||
def test_agent_allowed_app_add_nonexistent(self):
|
||||
"""PATCH add: nonexistent app UUID is rejected"""
|
||||
agent = self._create_agent(owner=self.admin)
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.patch(
|
||||
reverse("authentik_api:user-agent-allowed-app", kwargs={"pk": agent.pk}),
|
||||
data={"app": "00000000-0000-0000-0000-000000000000", "action": "add"},
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_token_rotate_by_agent_owner(self):
|
||||
"""Non-admin owner can rotate the agent's token"""
|
||||
self.client.force_login(self.owner)
|
||||
with patch(
|
||||
"authentik.enterprise.license.LicenseKey.cached_summary",
|
||||
MagicMock(return_value=MagicMock(status=MagicMock(is_valid=True))),
|
||||
):
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-agent"),
|
||||
data={"name": "rotate-test-agent"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
token = Token.objects.get(
|
||||
user__username="rotate-test-agent", intent=TokenIntents.INTENT_API
|
||||
)
|
||||
original_key = token.key
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:token-rotate", kwargs={"identifier": token.identifier}),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
token.refresh_from_db()
|
||||
self.assertNotEqual(token.key, original_key)
|
||||
|
||||
@@ -19,6 +19,7 @@ from authentik.core.api.sources import (
|
||||
from authentik.core.api.tokens import TokenViewSet
|
||||
from authentik.core.api.transactional_applications import TransactionalApplicationView
|
||||
from authentik.core.api.users import UserViewSet
|
||||
from authentik.core.views.agent_session import AgentSessionView
|
||||
from authentik.core.views.apps import RedirectToAppLaunch
|
||||
from authentik.core.views.debug import AccessDeniedView
|
||||
from authentik.core.views.interface import (
|
||||
@@ -79,6 +80,11 @@ api_urlpatterns = [
|
||||
TransactionalApplicationView.as_view(),
|
||||
name="core-transactional-application",
|
||||
),
|
||||
path(
|
||||
"core/agent/session/",
|
||||
AgentSessionView.as_view(),
|
||||
name="agent-session",
|
||||
),
|
||||
("core/groups", GroupViewSet),
|
||||
("core/users", UserViewSet),
|
||||
("core/tokens", TokenViewSet),
|
||||
|
||||
55
authentik/core/views/agent_session.py
Normal file
55
authentik/core/views/agent_session.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Agent token-to-session exchange view"""
|
||||
|
||||
from django.contrib.auth import login
|
||||
from rest_framework.authentication import BaseAuthentication
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK,
|
||||
AuthenticatedSession,
|
||||
Token,
|
||||
TokenIntents,
|
||||
)
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
|
||||
|
||||
class NoAuthentication(BaseAuthentication):
|
||||
"""Explicitly skip DRF authentication; the view authenticates via the request body."""
|
||||
|
||||
def authenticate(self, request):
|
||||
return None
|
||||
|
||||
|
||||
class AgentSessionView(APIView):
|
||||
"""Exchange an agent's API token for an authenticated session."""
|
||||
|
||||
authentication_classes = [NoAuthentication]
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request: Request) -> Response:
|
||||
key = request.data.get("key")
|
||||
if not key:
|
||||
return Response({"detail": "Key is required."}, status=400)
|
||||
|
||||
token = (
|
||||
Token.objects.filter(key=key, intent=TokenIntents.INTENT_API)
|
||||
.select_related("user")
|
||||
.first()
|
||||
)
|
||||
if not token:
|
||||
return Response({"detail": "Invalid token."}, status=400)
|
||||
if token.is_expired:
|
||||
return Response({"detail": "Token has expired."}, status=403)
|
||||
if not token.user.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK):
|
||||
return Response({"detail": "Token does not belong to an agent user."}, status=400)
|
||||
if not token.user.is_active:
|
||||
return Response({"detail": "Agent user is inactive."}, status=403)
|
||||
|
||||
login(request._request, token.user, backend=BACKEND_INBUILT)
|
||||
session = AuthenticatedSession.from_request(request._request, token.user)
|
||||
if session:
|
||||
session.save()
|
||||
return Response(status=204)
|
||||
@@ -14,7 +14,7 @@ from authentik.admin.tasks import LOCAL_VERSION
|
||||
from authentik.api.v3.config import ConfigView
|
||||
from authentik.brands.api import CurrentBrandSerializer
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import UserTypes
|
||||
from authentik.core.models import USER_ATTRIBUTE_AGENT_OWNER_PK, UserTypes
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.policies.denied import AccessDeniedResponse
|
||||
|
||||
@@ -26,10 +26,14 @@ class RootRedirectView(RedirectView):
|
||||
query_string = True
|
||||
|
||||
def redirect_to_app(self, request: HttpRequest):
|
||||
if request.user.is_authenticated and request.user.type in (
|
||||
UserTypes.EXTERNAL,
|
||||
UserTypes.SERVICE_ACCOUNT,
|
||||
UserTypes.INTERNAL_SERVICE_ACCOUNT,
|
||||
if request.user.is_authenticated and (
|
||||
request.user.type
|
||||
in (
|
||||
UserTypes.EXTERNAL,
|
||||
UserTypes.SERVICE_ACCOUNT,
|
||||
UserTypes.INTERNAL_SERVICE_ACCOUNT,
|
||||
)
|
||||
or request.user.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
):
|
||||
brand: Brand = request.brand
|
||||
if brand.default_application:
|
||||
@@ -66,10 +70,14 @@ class BrandDefaultRedirectView(InterfaceView):
|
||||
"""By default redirect to default app"""
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
if request.user.is_authenticated and request.user.type in (
|
||||
UserTypes.EXTERNAL,
|
||||
UserTypes.SERVICE_ACCOUNT,
|
||||
UserTypes.INTERNAL_SERVICE_ACCOUNT,
|
||||
if request.user.is_authenticated and (
|
||||
request.user.type
|
||||
in (
|
||||
UserTypes.EXTERNAL,
|
||||
UserTypes.SERVICE_ACCOUNT,
|
||||
UserTypes.INTERNAL_SERVICE_ACCOUNT,
|
||||
)
|
||||
or request.user.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
):
|
||||
brand: Brand = request.brand
|
||||
if brand.default_application:
|
||||
|
||||
@@ -144,10 +144,14 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
|
||||
def forecast(self, request: Request) -> Response:
|
||||
"""Forecast how many users will be required in a year"""
|
||||
last_month = now() - timedelta(days=30)
|
||||
# Forecast for internal users
|
||||
internal_in_last_month = User.objects.filter(
|
||||
type=UserTypes.INTERNAL, date_joined__gte=last_month
|
||||
).count()
|
||||
# Forecast for internal users (excluding agents)
|
||||
from authentik.core.models import USER_ATTRIBUTE_AGENT_OWNER_PK
|
||||
|
||||
internal_in_last_month = (
|
||||
User.objects.filter(type=UserTypes.INTERNAL, date_joined__gte=last_month)
|
||||
.exclude(attributes__has_key=USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
.count()
|
||||
)
|
||||
# Forecast for external users
|
||||
external_in_last_month = LicenseKey.get_external_user_count()
|
||||
forecast_for_months = 12
|
||||
|
||||
@@ -154,8 +154,15 @@ class LicenseKey:
|
||||
|
||||
@staticmethod
|
||||
def base_user_qs() -> QuerySet:
|
||||
"""Base query set for all users"""
|
||||
return User.objects.all().exclude_anonymous().exclude(is_active=False)
|
||||
"""Base query set for all users (excludes agents from license counting)"""
|
||||
from authentik.core.models import USER_ATTRIBUTE_AGENT_OWNER_PK
|
||||
|
||||
return (
|
||||
User.objects.all()
|
||||
.exclude_anonymous()
|
||||
.exclude(is_active=False)
|
||||
.exclude(attributes__has_key=USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_internal_user_count():
|
||||
|
||||
@@ -141,8 +141,12 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
# according to the provider's settings
|
||||
base = User.objects.all().exclude_anonymous().filter(**kwargs)
|
||||
if self.exclude_users_service_account:
|
||||
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
|
||||
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
from authentik.core.models import USER_ATTRIBUTE_AGENT_OWNER_PK
|
||||
|
||||
base = (
|
||||
base.exclude(type=UserTypes.SERVICE_ACCOUNT)
|
||||
.exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
|
||||
.exclude(attributes__has_key=USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
)
|
||||
if self.filter_group:
|
||||
base = base.filter(groups__in=[self.filter_group])
|
||||
|
||||
@@ -130,8 +130,12 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
# according to the provider's settings
|
||||
base = User.objects.all().exclude_anonymous().filter(**kwargs)
|
||||
if self.exclude_users_service_account:
|
||||
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
|
||||
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
from authentik.core.models import USER_ATTRIBUTE_AGENT_OWNER_PK
|
||||
|
||||
base = (
|
||||
base.exclude(type=UserTypes.SERVICE_ACCOUNT)
|
||||
.exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
|
||||
.exclude(attributes__has_key=USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
)
|
||||
if self.filter_group:
|
||||
base = base.filter(groups__in=[self.filter_group])
|
||||
|
||||
@@ -6,6 +6,25 @@ from dramatiq.actor import actor
|
||||
from authentik.enterprise.license import LicenseKey
|
||||
|
||||
|
||||
def _deactivate_agent_users():
|
||||
"""Mark all active agent users inactive and remove their sessions when the enterprise
|
||||
license is not valid. Called after each license usage recording."""
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK,
|
||||
Session,
|
||||
User,
|
||||
)
|
||||
|
||||
agents = User.objects.filter(
|
||||
attributes__has_key=USER_ATTRIBUTE_AGENT_OWNER_PK,
|
||||
is_active=True,
|
||||
)
|
||||
Session.objects.filter(authenticatedsession__user__in=agents).delete()
|
||||
agents.update(is_active=False)
|
||||
|
||||
|
||||
@actor(description=_("Update enterprise license status."))
|
||||
def enterprise_update_usage():
|
||||
LicenseKey.get_total().record_usage()
|
||||
usage = LicenseKey.get_total().record_usage()
|
||||
if not usage.status.is_valid:
|
||||
_deactivate_agent_users()
|
||||
|
||||
55
authentik/enterprise/tests/test_tasks.py
Normal file
55
authentik/enterprise/tests/test_tasks.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Enterprise task tests"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK,
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
|
||||
class TestDeactivateAgentUsers(TestCase):
|
||||
"""Tests for _deactivate_agent_users enterprise task"""
|
||||
|
||||
def _create_agent(self, owner):
|
||||
agent = User.objects.create(
|
||||
username=generate_id(),
|
||||
type=UserTypes.INTERNAL,
|
||||
attributes={USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk)},
|
||||
is_active=True,
|
||||
)
|
||||
agent.set_unusable_password()
|
||||
agent.save()
|
||||
return agent
|
||||
|
||||
def test_deactivates_all_active_agents(self):
|
||||
"""_deactivate_agent_users marks all active agent users inactive"""
|
||||
from authentik.enterprise.tasks import _deactivate_agent_users
|
||||
|
||||
owner = User.objects.create(username=generate_id())
|
||||
agent1 = self._create_agent(owner)
|
||||
agent2 = self._create_agent(owner)
|
||||
|
||||
_deactivate_agent_users()
|
||||
|
||||
agent1.refresh_from_db()
|
||||
agent2.refresh_from_db()
|
||||
self.assertFalse(agent1.is_active)
|
||||
self.assertFalse(agent2.is_active)
|
||||
|
||||
def test_does_not_deactivate_non_agents(self):
|
||||
"""_deactivate_agent_users does not affect non-agent internal users"""
|
||||
from authentik.enterprise.tasks import _deactivate_agent_users
|
||||
|
||||
internal = User.objects.create(
|
||||
username=generate_id(),
|
||||
type=UserTypes.INTERNAL,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
_deactivate_agent_users()
|
||||
|
||||
internal.refresh_from_db()
|
||||
self.assertTrue(internal.is_active)
|
||||
@@ -31,7 +31,7 @@ from authentik.policies.types import PolicyRequest
|
||||
# Special keys which are *not* cleaned, even when the default filter
|
||||
# is matched
|
||||
ALLOWED_SPECIAL_KEYS = re.compile(
|
||||
r"passing|password_change_date|^auth_method(_args)?$",
|
||||
r"passing|password_change_date|^auth_method(_args)?$|^goauthentik\.io/agent/",
|
||||
flags=re.I,
|
||||
)
|
||||
|
||||
|
||||
@@ -202,9 +202,49 @@ class PolicyEngine:
|
||||
).observe(proc_info.result._exec_time)
|
||||
return self
|
||||
|
||||
def _check_agent_access(self) -> PolicyResult | None:
|
||||
"""For agent users accessing an Application, enforce allowed_apps + owner access.
|
||||
Returns a deny PolicyResult if the agent should be blocked, or None to continue
|
||||
with normal policy evaluation."""
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_AGENT_ALLOWED_APPS,
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK,
|
||||
Application,
|
||||
)
|
||||
|
||||
user = self.request.user
|
||||
if not hasattr(user, "attributes"):
|
||||
return None
|
||||
owner_pk = user.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
if not owner_pk:
|
||||
return None
|
||||
if not isinstance(self.__pbm, Application):
|
||||
return None
|
||||
|
||||
allowed_apps = user.attributes.get(USER_ATTRIBUTE_AGENT_ALLOWED_APPS, [])
|
||||
if str(self.__pbm.pk) not in allowed_apps:
|
||||
return PolicyResult(False, "Agent does not have access to this application.")
|
||||
|
||||
owner = User.objects.filter(pk=owner_pk).first()
|
||||
if not owner:
|
||||
return PolicyResult(False, "Agent owner does not exist.")
|
||||
|
||||
from authentik.core.apps import AppAccessWithoutBindings
|
||||
|
||||
owner_engine = PolicyEngine(self.__pbm, owner)
|
||||
owner_engine.empty_result = AppAccessWithoutBindings.get()
|
||||
owner_engine.use_cache = False
|
||||
owner_engine.build()
|
||||
if not owner_engine.passing:
|
||||
return PolicyResult(False, "Agent owner does not have access to this application.")
|
||||
return None
|
||||
|
||||
@property
|
||||
def result(self) -> PolicyResult:
|
||||
"""Get policy-checking result"""
|
||||
agent_result = self._check_agent_access()
|
||||
if agent_result is not None:
|
||||
return agent_result
|
||||
self.__processes.sort(key=lambda x: x.binding.order)
|
||||
process_results: list[PolicyResult] = [x.result for x in self.__processes if x.result]
|
||||
all_results = list(process_results + self.__cached_policies)
|
||||
|
||||
@@ -50,6 +50,50 @@ class PolicyEvaluator(BaseEvaluator):
|
||||
self._context["ak_client_ip"] = ip_address(
|
||||
request.obj.client_ip or ClientIPMiddleware.default_ip
|
||||
)
|
||||
from authentik.core.models import Application # noqa: PLC0415
|
||||
|
||||
if request.obj and isinstance(request.obj, Application):
|
||||
self._context["has_access_to_application"] = self._make_has_access_to_application(
|
||||
request
|
||||
)
|
||||
|
||||
def _make_has_access_to_application(self, request: PolicyRequest):
|
||||
"""Return a no-argument callable that checks whether the current agent user's owner
|
||||
has access to the application currently being evaluated (request.obj)."""
|
||||
|
||||
def has_access_to_application() -> bool:
|
||||
from authentik.core.apps import AppAccessWithoutBindings
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_AGENT_ALLOWED_APPS,
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK,
|
||||
User,
|
||||
)
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
|
||||
user = request.user
|
||||
app = request.obj
|
||||
|
||||
if not hasattr(user, "attributes"):
|
||||
return False
|
||||
owner_pk = user.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
if not owner_pk:
|
||||
return False
|
||||
|
||||
allowed_apps = user.attributes.get(USER_ATTRIBUTE_AGENT_ALLOWED_APPS, [])
|
||||
if str(app.pk) not in allowed_apps:
|
||||
return False
|
||||
|
||||
owner = User.objects.filter(pk=owner_pk).first()
|
||||
if not owner:
|
||||
return False
|
||||
|
||||
engine = PolicyEngine(app, owner)
|
||||
engine.empty_result = AppAccessWithoutBindings.get()
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
return engine.passing
|
||||
|
||||
return has_access_to_application
|
||||
|
||||
def set_http_request(self, request: HttpRequest):
|
||||
"""Update context based on http request"""
|
||||
|
||||
@@ -5,7 +5,13 @@ from guardian.shortcuts import get_anonymous_user
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_AGENT_ALLOWED_APPS,
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK,
|
||||
Application,
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.policies.exceptions import PolicyException
|
||||
from authentik.policies.expression.api import ExpressionPolicySerializer
|
||||
@@ -135,6 +141,75 @@ class TestEvaluator(TestCase):
|
||||
self.assertEqual(res.messages, ("/", "/", "/"))
|
||||
|
||||
|
||||
class TestHasAccessToApplication(TestCase):
|
||||
"""Tests for has_access_to_application policy context helper"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
self.app = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||
self.owner = User.objects.create(username=generate_id())
|
||||
|
||||
def _create_agent(self, allowed_apps=None):
|
||||
return User.objects.create(
|
||||
username=generate_id(),
|
||||
type=UserTypes.INTERNAL,
|
||||
attributes={
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK: str(self.owner.pk),
|
||||
USER_ATTRIBUTE_AGENT_ALLOWED_APPS: allowed_apps if allowed_apps is not None else [],
|
||||
},
|
||||
)
|
||||
|
||||
def _evaluator_with(self, user, obj=None):
|
||||
request = PolicyRequest(user=user)
|
||||
request.obj = obj or self.app
|
||||
request.http_request = self.factory.get("/")
|
||||
evaluator = PolicyEvaluator("test")
|
||||
evaluator.set_policy_request(request)
|
||||
return evaluator
|
||||
|
||||
def test_not_injected_for_non_application_obj(self):
|
||||
"""has_access_to_application is not injected when obj is not an Application"""
|
||||
agent = self._create_agent(allowed_apps=[str(self.app.pk)])
|
||||
request = PolicyRequest(user=agent)
|
||||
request.obj = None
|
||||
evaluator = PolicyEvaluator("test")
|
||||
evaluator.set_policy_request(request)
|
||||
self.assertNotIn("has_access_to_application", evaluator._context)
|
||||
|
||||
def test_injected_for_application_obj(self):
|
||||
"""has_access_to_application is injected when obj is an Application"""
|
||||
agent = self._create_agent(allowed_apps=[str(self.app.pk)])
|
||||
evaluator = self._evaluator_with(agent)
|
||||
self.assertIn("has_access_to_application", evaluator._context)
|
||||
|
||||
def test_non_agent_returns_false(self):
|
||||
"""Returns False when the current user is not an agent"""
|
||||
evaluator = self._evaluator_with(self.owner)
|
||||
result = evaluator._context["has_access_to_application"]()
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_app_not_in_allowed_list_returns_false(self):
|
||||
"""Returns False when the application is not in the agent's allowed apps list"""
|
||||
agent = self._create_agent(allowed_apps=[])
|
||||
evaluator = self._evaluator_with(agent)
|
||||
result = evaluator._context["has_access_to_application"]()
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_missing_owner_returns_false(self):
|
||||
"""Returns False when the owner pk points to a non-existent user"""
|
||||
agent = User.objects.create(
|
||||
username=generate_id(),
|
||||
type=UserTypes.INTERNAL,
|
||||
attributes={
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK: "999999",
|
||||
USER_ATTRIBUTE_AGENT_ALLOWED_APPS: [str(self.app.pk)],
|
||||
},
|
||||
)
|
||||
evaluator = self._evaluator_with(agent)
|
||||
result = evaluator._context["has_access_to_application"]()
|
||||
self.assertFalse(result)
|
||||
|
||||
|
||||
class TestExpressionPolicyAPI(APITestCase):
|
||||
"""Test expression policy's API"""
|
||||
|
||||
|
||||
@@ -5,7 +5,14 @@ from django.db import connections
|
||||
from django.test import TestCase
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
|
||||
from authentik.core.models import Group
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_AGENT_ALLOWED_APPS,
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK,
|
||||
Application,
|
||||
Group,
|
||||
User,
|
||||
UserTypes,
|
||||
)
|
||||
from authentik.core.tests.utils import create_test_user
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
@@ -209,3 +216,91 @@ class TestPolicyEngine(TestCase):
|
||||
engine.build()
|
||||
self.assertLess(ctx.final_queries, 1000)
|
||||
self.assertTrue(engine.result.passing)
|
||||
|
||||
def test_anonymous_user(self):
|
||||
"""AnonymousUser (no attributes) does not break policy evaluation"""
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
|
||||
pbm = PolicyBindingModel.objects.create()
|
||||
engine = PolicyEngine(pbm, AnonymousUser())
|
||||
engine.empty_result = True
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
self.assertTrue(engine.passing)
|
||||
|
||||
|
||||
class TestPolicyEngineAgent(TestCase):
|
||||
"""PolicyEngine agent access enforcement tests"""
|
||||
|
||||
def setUp(self):
|
||||
clear_policy_cache()
|
||||
self.owner = create_test_user()
|
||||
self.app = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||
|
||||
def _create_agent(self, allowed_apps=None):
|
||||
return User.objects.create(
|
||||
username=generate_id(),
|
||||
type=UserTypes.INTERNAL,
|
||||
attributes={
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK: str(self.owner.pk),
|
||||
USER_ATTRIBUTE_AGENT_ALLOWED_APPS: allowed_apps if allowed_apps is not None else [],
|
||||
},
|
||||
)
|
||||
|
||||
def test_agent_allowed_app_passes(self):
|
||||
"""Agent with app in allowed_apps and owner access passes"""
|
||||
agent = self._create_agent(allowed_apps=[str(self.app.pk)])
|
||||
engine = PolicyEngine(self.app, agent)
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
self.assertTrue(engine.passing)
|
||||
|
||||
def test_agent_disallowed_app_denied(self):
|
||||
"""Agent without app in allowed_apps is denied"""
|
||||
agent = self._create_agent(allowed_apps=[])
|
||||
engine = PolicyEngine(self.app, agent)
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
self.assertFalse(engine.passing)
|
||||
|
||||
def test_agent_empty_allowed_apps_denied(self):
|
||||
"""Agent with empty allowed_apps is denied even for unbound apps"""
|
||||
agent = self._create_agent()
|
||||
engine = PolicyEngine(self.app, agent)
|
||||
engine.empty_result = True
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
self.assertFalse(engine.passing)
|
||||
|
||||
def test_non_agent_unaffected(self):
|
||||
"""Non-agent users are not affected by agent access check"""
|
||||
engine = PolicyEngine(self.app, self.owner)
|
||||
engine.empty_result = True
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
self.assertTrue(engine.passing)
|
||||
|
||||
def test_agent_missing_owner_denied(self):
|
||||
"""Agent with non-existent owner is denied"""
|
||||
agent = User.objects.create(
|
||||
username=generate_id(),
|
||||
type=UserTypes.INTERNAL,
|
||||
attributes={
|
||||
USER_ATTRIBUTE_AGENT_OWNER_PK: "999999",
|
||||
USER_ATTRIBUTE_AGENT_ALLOWED_APPS: [str(self.app.pk)],
|
||||
},
|
||||
)
|
||||
engine = PolicyEngine(self.app, agent)
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
self.assertFalse(engine.passing)
|
||||
|
||||
def test_agent_non_application_target_unaffected(self):
|
||||
"""Agent check only applies to Application targets"""
|
||||
agent = self._create_agent(allowed_apps=[])
|
||||
pbm = PolicyBindingModel.objects.create()
|
||||
engine = PolicyEngine(pbm, agent)
|
||||
engine.empty_result = True
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
self.assertTrue(engine.passing)
|
||||
|
||||
@@ -188,8 +188,12 @@ class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
# according to the provider's settings
|
||||
base = User.objects.all().exclude_anonymous().filter(**kwargs)
|
||||
if self.exclude_users_service_account:
|
||||
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
|
||||
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
from authentik.core.models import USER_ATTRIBUTE_AGENT_OWNER_PK
|
||||
|
||||
base = (
|
||||
base.exclude(type=UserTypes.SERVICE_ACCOUNT)
|
||||
.exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
|
||||
.exclude(attributes__has_key=USER_ATTRIBUTE_AGENT_OWNER_PK)
|
||||
)
|
||||
|
||||
# Filter users by their access to the backchannel application if an application is set
|
||||
|
||||
@@ -5545,6 +5545,7 @@
|
||||
"authentik_brands.change_brand",
|
||||
"authentik_brands.delete_brand",
|
||||
"authentik_brands.view_brand",
|
||||
"authentik_core.add_agent_user",
|
||||
"authentik_core.add_application",
|
||||
"authentik_core.add_applicationentitlement",
|
||||
"authentik_core.add_authenticatedsession",
|
||||
@@ -6257,6 +6258,7 @@
|
||||
"permission": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"add_agent_user",
|
||||
"add_user",
|
||||
"change_user",
|
||||
"delete_user",
|
||||
@@ -11220,6 +11222,7 @@
|
||||
"authentik_brands.change_brand",
|
||||
"authentik_brands.delete_brand",
|
||||
"authentik_brands.view_brand",
|
||||
"authentik_core.add_agent_user",
|
||||
"authentik_core.add_application",
|
||||
"authentik_core.add_applicationentitlement",
|
||||
"authentik_core.add_authenticatedsession",
|
||||
|
||||
354
packages/client-ts/src/apis/CoreApi.ts
generated
354
packages/client-ts/src/apis/CoreApi.ts
generated
@@ -41,6 +41,7 @@ import type {
|
||||
PatchedBrandRequest,
|
||||
PatchedGroupRequest,
|
||||
PatchedTokenRequest,
|
||||
PatchedUserAgentAllowedAppRequest,
|
||||
PatchedUserRequest,
|
||||
PolicyTestResult,
|
||||
SessionUser,
|
||||
@@ -53,6 +54,10 @@ import type {
|
||||
UsedBy,
|
||||
User,
|
||||
UserAccountRequest,
|
||||
UserAgentAllowedApps,
|
||||
UserAgentAllowedAppsRequest,
|
||||
UserAgentRequest,
|
||||
UserAgentResponse,
|
||||
UserConsent,
|
||||
UserPasswordSetRequest,
|
||||
UserPath,
|
||||
@@ -91,6 +96,7 @@ import {
|
||||
PatchedBrandRequestToJSON,
|
||||
PatchedGroupRequestToJSON,
|
||||
PatchedTokenRequestToJSON,
|
||||
PatchedUserAgentAllowedAppRequestToJSON,
|
||||
PatchedUserRequestToJSON,
|
||||
PolicyTestResultFromJSON,
|
||||
SessionUserFromJSON,
|
||||
@@ -102,6 +108,10 @@ import {
|
||||
TransactionApplicationResponseFromJSON,
|
||||
UsedByFromJSON,
|
||||
UserAccountRequestToJSON,
|
||||
UserAgentAllowedAppsFromJSON,
|
||||
UserAgentAllowedAppsRequestToJSON,
|
||||
UserAgentRequestToJSON,
|
||||
UserAgentResponseFromJSON,
|
||||
UserConsentFromJSON,
|
||||
UserFromJSON,
|
||||
UserPasswordSetRequestToJSON,
|
||||
@@ -358,6 +368,10 @@ export interface CoreTokensRetrieveRequest {
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
export interface CoreTokensRotateCreateRequest {
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
export interface CoreTokensSetKeyCreateRequest {
|
||||
identifier: string;
|
||||
tokenSetKeyRequest: TokenSetKeyRequest;
|
||||
@@ -401,6 +415,20 @@ export interface CoreUserConsentUsedByListRequest {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface CoreUsersAgentAllowedAppPartialUpdateRequest {
|
||||
id: number;
|
||||
patchedUserAgentAllowedAppRequest?: PatchedUserAgentAllowedAppRequest;
|
||||
}
|
||||
|
||||
export interface CoreUsersAgentAllowedAppsUpdateRequest {
|
||||
id: number;
|
||||
userAgentAllowedAppsRequest: UserAgentAllowedAppsRequest;
|
||||
}
|
||||
|
||||
export interface CoreUsersAgentCreateRequest {
|
||||
userAgentRequest: UserAgentRequest;
|
||||
}
|
||||
|
||||
export interface CoreUsersCreateRequest {
|
||||
userRequest: UserRequest;
|
||||
}
|
||||
@@ -521,6 +549,45 @@ export interface CoreUsersUsedByListRequest {
|
||||
*
|
||||
*/
|
||||
export class CoreApi extends runtime.BaseAPI {
|
||||
/**
|
||||
* Creates request options for coreAgentSessionCreate without sending the request
|
||||
*/
|
||||
async coreAgentSessionCreateRequestOpts(): Promise<runtime.RequestOpts> {
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
let urlPath = `/core/agent/session/`;
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
method: "POST",
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange an agent\'s API token for an authenticated session.
|
||||
*/
|
||||
async coreAgentSessionCreateRaw(
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<void>> {
|
||||
const requestOptions = await this.coreAgentSessionCreateRequestOpts();
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.VoidApiResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange an agent\'s API token for an authenticated session.
|
||||
*/
|
||||
async coreAgentSessionCreate(
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<void> {
|
||||
await this.coreAgentSessionCreateRaw(initOverrides);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for coreApplicationEntitlementsCreate without sending the request
|
||||
*/
|
||||
@@ -3575,6 +3642,70 @@ export class CoreApi extends runtime.BaseAPI {
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for coreTokensRotateCreate without sending the request
|
||||
*/
|
||||
async coreTokensRotateCreateRequestOpts(
|
||||
requestParameters: CoreTokensRotateCreateRequest,
|
||||
): Promise<runtime.RequestOpts> {
|
||||
if (requestParameters["identifier"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"identifier",
|
||||
'Required parameter "identifier" was null or undefined when calling coreTokensRotateCreate().',
|
||||
);
|
||||
}
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("authentik", []);
|
||||
|
||||
if (tokenString) {
|
||||
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||
}
|
||||
}
|
||||
|
||||
let urlPath = `/core/tokens/{identifier}/rotate/`;
|
||||
urlPath = urlPath.replace(
|
||||
`{${"identifier"}}`,
|
||||
encodeURIComponent(String(requestParameters["identifier"])),
|
||||
);
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
method: "POST",
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the token key and reset the expiry to 24 hours. Only callable by the token owner, the owning agent\'s human owner, or a superuser.
|
||||
*/
|
||||
async coreTokensRotateCreateRaw(
|
||||
requestParameters: CoreTokensRotateCreateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<TokenView>> {
|
||||
const requestOptions = await this.coreTokensRotateCreateRequestOpts(requestParameters);
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) => TokenViewFromJSON(jsonValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the token key and reset the expiry to 24 hours. Only callable by the token owner, the owning agent\'s human owner, or a superuser.
|
||||
*/
|
||||
async coreTokensRotateCreate(
|
||||
requestParameters: CoreTokensRotateCreateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<TokenView> {
|
||||
const response = await this.coreTokensRotateCreateRaw(requestParameters, initOverrides);
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for coreTokensSetKeyCreate without sending the request
|
||||
*/
|
||||
@@ -4182,6 +4313,229 @@ export class CoreApi extends runtime.BaseAPI {
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for coreUsersAgentAllowedAppPartialUpdate without sending the request
|
||||
*/
|
||||
async coreUsersAgentAllowedAppPartialUpdateRequestOpts(
|
||||
requestParameters: CoreUsersAgentAllowedAppPartialUpdateRequest,
|
||||
): Promise<runtime.RequestOpts> {
|
||||
if (requestParameters["id"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"id",
|
||||
'Required parameter "id" was null or undefined when calling coreUsersAgentAllowedAppPartialUpdate().',
|
||||
);
|
||||
}
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
headerParameters["Content-Type"] = "application/json";
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("authentik", []);
|
||||
|
||||
if (tokenString) {
|
||||
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||
}
|
||||
}
|
||||
|
||||
let urlPath = `/core/users/{id}/agent_allowed_app/`;
|
||||
urlPath = urlPath.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters["id"])));
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
method: "PATCH",
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
body: PatchedUserAgentAllowedAppRequestToJSON(
|
||||
requestParameters["patchedUserAgentAllowedAppRequest"],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or remove a single application from an agent\'s allowed list. Caller must be the agent\'s owner or a superuser.
|
||||
*/
|
||||
async coreUsersAgentAllowedAppPartialUpdateRaw(
|
||||
requestParameters: CoreUsersAgentAllowedAppPartialUpdateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<UserAgentAllowedApps>> {
|
||||
const requestOptions =
|
||||
await this.coreUsersAgentAllowedAppPartialUpdateRequestOpts(requestParameters);
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||
UserAgentAllowedAppsFromJSON(jsonValue),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or remove a single application from an agent\'s allowed list. Caller must be the agent\'s owner or a superuser.
|
||||
*/
|
||||
async coreUsersAgentAllowedAppPartialUpdate(
|
||||
requestParameters: CoreUsersAgentAllowedAppPartialUpdateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<UserAgentAllowedApps | null | undefined> {
|
||||
const response = await this.coreUsersAgentAllowedAppPartialUpdateRaw(
|
||||
requestParameters,
|
||||
initOverrides,
|
||||
);
|
||||
switch (response.raw.status) {
|
||||
case 200:
|
||||
return await response.value();
|
||||
case 204:
|
||||
return null;
|
||||
default:
|
||||
return await response.value();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for coreUsersAgentAllowedAppsUpdate without sending the request
|
||||
*/
|
||||
async coreUsersAgentAllowedAppsUpdateRequestOpts(
|
||||
requestParameters: CoreUsersAgentAllowedAppsUpdateRequest,
|
||||
): Promise<runtime.RequestOpts> {
|
||||
if (requestParameters["id"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"id",
|
||||
'Required parameter "id" was null or undefined when calling coreUsersAgentAllowedAppsUpdate().',
|
||||
);
|
||||
}
|
||||
|
||||
if (requestParameters["userAgentAllowedAppsRequest"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"userAgentAllowedAppsRequest",
|
||||
'Required parameter "userAgentAllowedAppsRequest" was null or undefined when calling coreUsersAgentAllowedAppsUpdate().',
|
||||
);
|
||||
}
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
headerParameters["Content-Type"] = "application/json";
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("authentik", []);
|
||||
|
||||
if (tokenString) {
|
||||
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||
}
|
||||
}
|
||||
|
||||
let urlPath = `/core/users/{id}/agent_allowed_apps/`;
|
||||
urlPath = urlPath.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters["id"])));
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
method: "PUT",
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
body: UserAgentAllowedAppsRequestToJSON(
|
||||
requestParameters["userAgentAllowedAppsRequest"],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the allowed application list for an agent user. Caller must be the agent\'s owner or a superuser.
|
||||
*/
|
||||
async coreUsersAgentAllowedAppsUpdateRaw(
|
||||
requestParameters: CoreUsersAgentAllowedAppsUpdateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<UserAgentAllowedApps>> {
|
||||
const requestOptions =
|
||||
await this.coreUsersAgentAllowedAppsUpdateRequestOpts(requestParameters);
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||
UserAgentAllowedAppsFromJSON(jsonValue),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the allowed application list for an agent user. Caller must be the agent\'s owner or a superuser.
|
||||
*/
|
||||
async coreUsersAgentAllowedAppsUpdate(
|
||||
requestParameters: CoreUsersAgentAllowedAppsUpdateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<UserAgentAllowedApps> {
|
||||
const response = await this.coreUsersAgentAllowedAppsUpdateRaw(
|
||||
requestParameters,
|
||||
initOverrides,
|
||||
);
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for coreUsersAgentCreate without sending the request
|
||||
*/
|
||||
async coreUsersAgentCreateRequestOpts(
|
||||
requestParameters: CoreUsersAgentCreateRequest,
|
||||
): Promise<runtime.RequestOpts> {
|
||||
if (requestParameters["userAgentRequest"] == null) {
|
||||
throw new runtime.RequiredError(
|
||||
"userAgentRequest",
|
||||
'Required parameter "userAgentRequest" was null or undefined when calling coreUsersAgentCreate().',
|
||||
);
|
||||
}
|
||||
|
||||
const queryParameters: any = {};
|
||||
|
||||
const headerParameters: runtime.HTTPHeaders = {};
|
||||
|
||||
headerParameters["Content-Type"] = "application/json";
|
||||
|
||||
if (this.configuration && this.configuration.accessToken) {
|
||||
const token = this.configuration.accessToken;
|
||||
const tokenString = await token("authentik", []);
|
||||
|
||||
if (tokenString) {
|
||||
headerParameters["Authorization"] = `Bearer ${tokenString}`;
|
||||
}
|
||||
}
|
||||
|
||||
let urlPath = `/core/users/agent/`;
|
||||
|
||||
return {
|
||||
path: urlPath,
|
||||
method: "POST",
|
||||
headers: headerParameters,
|
||||
query: queryParameters,
|
||||
body: UserAgentRequestToJSON(requestParameters["userAgentRequest"]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new agent user. Enterprise only. Caller must be an internal user. Agent users are internal users with an owner attribute that grants scoped application access on behalf of the owner.
|
||||
*/
|
||||
async coreUsersAgentCreateRaw(
|
||||
requestParameters: CoreUsersAgentCreateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<runtime.ApiResponse<UserAgentResponse>> {
|
||||
const requestOptions = await this.coreUsersAgentCreateRequestOpts(requestParameters);
|
||||
const response = await this.request(requestOptions, initOverrides);
|
||||
|
||||
return new runtime.JSONApiResponse(response, (jsonValue) =>
|
||||
UserAgentResponseFromJSON(jsonValue),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new agent user. Enterprise only. Caller must be an internal user. Agent users are internal users with an owner attribute that grants scoped application access on behalf of the owner.
|
||||
*/
|
||||
async coreUsersAgentCreate(
|
||||
requestParameters: CoreUsersAgentCreateRequest,
|
||||
initOverrides?: RequestInit | runtime.InitOverrideFunction,
|
||||
): Promise<UserAgentResponse> {
|
||||
const response = await this.coreUsersAgentCreateRaw(requestParameters, initOverrides);
|
||||
return await response.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates request options for coreUsersCreate without sending the request
|
||||
*/
|
||||
|
||||
90
packages/client-ts/src/models/PatchedUserAgentAllowedAppRequest.ts
generated
Normal file
90
packages/client-ts/src/models/PatchedUserAgentAllowedAppRequest.ts
generated
Normal file
@@ -0,0 +1,90 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import type { UserAgentAllowedAppActionEnum } from "./UserAgentAllowedAppActionEnum";
|
||||
import {
|
||||
UserAgentAllowedAppActionEnumFromJSON,
|
||||
UserAgentAllowedAppActionEnumToJSON,
|
||||
} from "./UserAgentAllowedAppActionEnum";
|
||||
|
||||
/**
|
||||
* Payload to add or remove a single allowed application
|
||||
* @export
|
||||
* @interface PatchedUserAgentAllowedAppRequest
|
||||
*/
|
||||
export interface PatchedUserAgentAllowedAppRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PatchedUserAgentAllowedAppRequest
|
||||
*/
|
||||
app?: string;
|
||||
/**
|
||||
*
|
||||
* @type {UserAgentAllowedAppActionEnum}
|
||||
* @memberof PatchedUserAgentAllowedAppRequest
|
||||
*/
|
||||
action?: UserAgentAllowedAppActionEnum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the PatchedUserAgentAllowedAppRequest interface.
|
||||
*/
|
||||
export function instanceOfPatchedUserAgentAllowedAppRequest(
|
||||
value: object,
|
||||
): value is PatchedUserAgentAllowedAppRequest {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function PatchedUserAgentAllowedAppRequestFromJSON(
|
||||
json: any,
|
||||
): PatchedUserAgentAllowedAppRequest {
|
||||
return PatchedUserAgentAllowedAppRequestFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function PatchedUserAgentAllowedAppRequestFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): PatchedUserAgentAllowedAppRequest {
|
||||
if (json == null) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
app: json["app"] == null ? undefined : json["app"],
|
||||
action:
|
||||
json["action"] == null
|
||||
? undefined
|
||||
: UserAgentAllowedAppActionEnumFromJSON(json["action"]),
|
||||
};
|
||||
}
|
||||
|
||||
export function PatchedUserAgentAllowedAppRequestToJSON(
|
||||
json: any,
|
||||
): PatchedUserAgentAllowedAppRequest {
|
||||
return PatchedUserAgentAllowedAppRequestToJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function PatchedUserAgentAllowedAppRequestToJSONTyped(
|
||||
value?: PatchedUserAgentAllowedAppRequest | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return {
|
||||
app: value["app"],
|
||||
action: UserAgentAllowedAppActionEnumToJSON(value["action"]),
|
||||
};
|
||||
}
|
||||
63
packages/client-ts/src/models/UserAgentAllowedAppActionEnum.ts
generated
Normal file
63
packages/client-ts/src/models/UserAgentAllowedAppActionEnum.ts
generated
Normal file
@@ -0,0 +1,63 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const UserAgentAllowedAppActionEnum = {
|
||||
Add: "add",
|
||||
Remove: "remove",
|
||||
UnknownDefaultOpenApi: "11184809",
|
||||
} as const;
|
||||
export type UserAgentAllowedAppActionEnum =
|
||||
(typeof UserAgentAllowedAppActionEnum)[keyof typeof UserAgentAllowedAppActionEnum];
|
||||
|
||||
export function instanceOfUserAgentAllowedAppActionEnum(value: any): boolean {
|
||||
for (const key in UserAgentAllowedAppActionEnum) {
|
||||
if (Object.prototype.hasOwnProperty.call(UserAgentAllowedAppActionEnum, key)) {
|
||||
if (
|
||||
UserAgentAllowedAppActionEnum[key as keyof typeof UserAgentAllowedAppActionEnum] ===
|
||||
value
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function UserAgentAllowedAppActionEnumFromJSON(json: any): UserAgentAllowedAppActionEnum {
|
||||
return UserAgentAllowedAppActionEnumFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function UserAgentAllowedAppActionEnumFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): UserAgentAllowedAppActionEnum {
|
||||
return json as UserAgentAllowedAppActionEnum;
|
||||
}
|
||||
|
||||
export function UserAgentAllowedAppActionEnumToJSON(
|
||||
value?: UserAgentAllowedAppActionEnum | null,
|
||||
): any {
|
||||
return value as any;
|
||||
}
|
||||
|
||||
export function UserAgentAllowedAppActionEnumToJSONTyped(
|
||||
value: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): UserAgentAllowedAppActionEnum {
|
||||
return value as UserAgentAllowedAppActionEnum;
|
||||
}
|
||||
68
packages/client-ts/src/models/UserAgentAllowedApps.ts
generated
Normal file
68
packages/client-ts/src/models/UserAgentAllowedApps.ts
generated
Normal file
@@ -0,0 +1,68 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Payload to replace an agent's allowed applications
|
||||
* @export
|
||||
* @interface UserAgentAllowedApps
|
||||
*/
|
||||
export interface UserAgentAllowedApps {
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof UserAgentAllowedApps
|
||||
*/
|
||||
allowedApps: Array<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the UserAgentAllowedApps interface.
|
||||
*/
|
||||
export function instanceOfUserAgentAllowedApps(value: object): value is UserAgentAllowedApps {
|
||||
if (!("allowedApps" in value) || value["allowedApps"] === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function UserAgentAllowedAppsFromJSON(json: any): UserAgentAllowedApps {
|
||||
return UserAgentAllowedAppsFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function UserAgentAllowedAppsFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): UserAgentAllowedApps {
|
||||
if (json == null) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
allowedApps: json["allowed_apps"],
|
||||
};
|
||||
}
|
||||
|
||||
export function UserAgentAllowedAppsToJSON(json: any): UserAgentAllowedApps {
|
||||
return UserAgentAllowedAppsToJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function UserAgentAllowedAppsToJSONTyped(
|
||||
value?: UserAgentAllowedApps | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return {
|
||||
allowed_apps: value["allowedApps"],
|
||||
};
|
||||
}
|
||||
70
packages/client-ts/src/models/UserAgentAllowedAppsRequest.ts
generated
Normal file
70
packages/client-ts/src/models/UserAgentAllowedAppsRequest.ts
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Payload to replace an agent's allowed applications
|
||||
* @export
|
||||
* @interface UserAgentAllowedAppsRequest
|
||||
*/
|
||||
export interface UserAgentAllowedAppsRequest {
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof UserAgentAllowedAppsRequest
|
||||
*/
|
||||
allowedApps: Array<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the UserAgentAllowedAppsRequest interface.
|
||||
*/
|
||||
export function instanceOfUserAgentAllowedAppsRequest(
|
||||
value: object,
|
||||
): value is UserAgentAllowedAppsRequest {
|
||||
if (!("allowedApps" in value) || value["allowedApps"] === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function UserAgentAllowedAppsRequestFromJSON(json: any): UserAgentAllowedAppsRequest {
|
||||
return UserAgentAllowedAppsRequestFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function UserAgentAllowedAppsRequestFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): UserAgentAllowedAppsRequest {
|
||||
if (json == null) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
allowedApps: json["allowed_apps"],
|
||||
};
|
||||
}
|
||||
|
||||
export function UserAgentAllowedAppsRequestToJSON(json: any): UserAgentAllowedAppsRequest {
|
||||
return UserAgentAllowedAppsRequestToJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function UserAgentAllowedAppsRequestToJSONTyped(
|
||||
value?: UserAgentAllowedAppsRequest | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return {
|
||||
allowed_apps: value["allowedApps"],
|
||||
};
|
||||
}
|
||||
76
packages/client-ts/src/models/UserAgentRequest.ts
generated
Normal file
76
packages/client-ts/src/models/UserAgentRequest.ts
generated
Normal file
@@ -0,0 +1,76 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Payload to create an agent user
|
||||
* @export
|
||||
* @interface UserAgentRequest
|
||||
*/
|
||||
export interface UserAgentRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserAgentRequest
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof UserAgentRequest
|
||||
*/
|
||||
owner?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the UserAgentRequest interface.
|
||||
*/
|
||||
export function instanceOfUserAgentRequest(value: object): value is UserAgentRequest {
|
||||
if (!("name" in value) || value["name"] === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function UserAgentRequestFromJSON(json: any): UserAgentRequest {
|
||||
return UserAgentRequestFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function UserAgentRequestFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): UserAgentRequest {
|
||||
if (json == null) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
name: json["name"],
|
||||
owner: json["owner"] == null ? undefined : json["owner"],
|
||||
};
|
||||
}
|
||||
|
||||
export function UserAgentRequestToJSON(json: any): UserAgentRequest {
|
||||
return UserAgentRequestToJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function UserAgentRequestToJSONTyped(
|
||||
value?: UserAgentRequest | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return {
|
||||
name: value["name"],
|
||||
owner: value["owner"],
|
||||
};
|
||||
}
|
||||
95
packages/client-ts/src/models/UserAgentResponse.ts
generated
Normal file
95
packages/client-ts/src/models/UserAgentResponse.ts
generated
Normal file
@@ -0,0 +1,95 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* authentik
|
||||
* Making authentication simple.
|
||||
*
|
||||
* The version of the OpenAPI document: 2026.5.0-rc1
|
||||
* Contact: hello@goauthentik.io
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface UserAgentResponse
|
||||
*/
|
||||
export interface UserAgentResponse {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserAgentResponse
|
||||
*/
|
||||
username: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserAgentResponse
|
||||
*/
|
||||
token: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof UserAgentResponse
|
||||
*/
|
||||
userUid: string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof UserAgentResponse
|
||||
*/
|
||||
userPk: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given object implements the UserAgentResponse interface.
|
||||
*/
|
||||
export function instanceOfUserAgentResponse(value: object): value is UserAgentResponse {
|
||||
if (!("username" in value) || value["username"] === undefined) return false;
|
||||
if (!("token" in value) || value["token"] === undefined) return false;
|
||||
if (!("userUid" in value) || value["userUid"] === undefined) return false;
|
||||
if (!("userPk" in value) || value["userPk"] === undefined) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function UserAgentResponseFromJSON(json: any): UserAgentResponse {
|
||||
return UserAgentResponseFromJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function UserAgentResponseFromJSONTyped(
|
||||
json: any,
|
||||
ignoreDiscriminator: boolean,
|
||||
): UserAgentResponse {
|
||||
if (json == null) {
|
||||
return json;
|
||||
}
|
||||
return {
|
||||
username: json["username"],
|
||||
token: json["token"],
|
||||
userUid: json["user_uid"],
|
||||
userPk: json["user_pk"],
|
||||
};
|
||||
}
|
||||
|
||||
export function UserAgentResponseToJSON(json: any): UserAgentResponse {
|
||||
return UserAgentResponseToJSONTyped(json, false);
|
||||
}
|
||||
|
||||
export function UserAgentResponseToJSONTyped(
|
||||
value?: UserAgentResponse | null,
|
||||
ignoreDiscriminator: boolean = false,
|
||||
): any {
|
||||
if (value == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return {
|
||||
username: value["username"],
|
||||
token: value["token"],
|
||||
user_uid: value["userUid"],
|
||||
user_pk: value["userPk"],
|
||||
};
|
||||
}
|
||||
6
packages/client-ts/src/models/index.ts
generated
6
packages/client-ts/src/models/index.ts
generated
@@ -628,6 +628,7 @@ export * from "./PatchedTelegramSourceRequest";
|
||||
export * from "./PatchedTenantRequest";
|
||||
export * from "./PatchedTokenRequest";
|
||||
export * from "./PatchedUniquePasswordPolicyRequest";
|
||||
export * from "./PatchedUserAgentAllowedAppRequest";
|
||||
export * from "./PatchedUserDeleteStageRequest";
|
||||
export * from "./PatchedUserKerberosSourceConnectionRequest";
|
||||
export * from "./PatchedUserLDAPSourceConnectionRequest";
|
||||
@@ -818,6 +819,11 @@ export * from "./UsedByActionEnum";
|
||||
export * from "./User";
|
||||
export * from "./UserAccountRequest";
|
||||
export * from "./UserAccountSerializerForRoleRequest";
|
||||
export * from "./UserAgentAllowedAppActionEnum";
|
||||
export * from "./UserAgentAllowedApps";
|
||||
export * from "./UserAgentAllowedAppsRequest";
|
||||
export * from "./UserAgentRequest";
|
||||
export * from "./UserAgentResponse";
|
||||
export * from "./UserAttributeEnum";
|
||||
export * from "./UserConsent";
|
||||
export * from "./UserCreationModeEnum";
|
||||
|
||||
205
schema.yml
205
schema.yml
@@ -2528,6 +2528,21 @@ paths:
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/core/agent/session/:
|
||||
post:
|
||||
operationId: core_agent_session_create
|
||||
description: Exchange an agent's API token for an authenticated session.
|
||||
tags:
|
||||
- core
|
||||
security:
|
||||
- {}
|
||||
responses:
|
||||
'200':
|
||||
description: No response body
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/core/application_entitlements/:
|
||||
get:
|
||||
operationId: core_application_entitlements_list
|
||||
@@ -3896,6 +3911,35 @@ paths:
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/core/tokens/{identifier}/rotate/:
|
||||
post:
|
||||
operationId: core_tokens_rotate_create
|
||||
description: |-
|
||||
Rotate the token key and reset the expiry to 24 hours. Only callable by the token
|
||||
owner, the owning agent's human owner, or a superuser.
|
||||
parameters:
|
||||
- in: path
|
||||
name: identifier
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
tags:
|
||||
- core
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenView'
|
||||
description: ''
|
||||
'403':
|
||||
description: Not the token owner, agent owner, or superuser
|
||||
'404':
|
||||
description: Token not found
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
/core/tokens/{identifier}/set_key/:
|
||||
post:
|
||||
operationId: core_tokens_set_key_create
|
||||
@@ -4406,6 +4450,75 @@ paths:
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/core/users/{id}/agent_allowed_app/:
|
||||
patch:
|
||||
operationId: core_users_agent_allowed_app_partial_update
|
||||
description: |-
|
||||
Add or remove a single application from an agent's allowed list.
|
||||
Caller must be the agent's owner or a superuser.
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this User.
|
||||
required: true
|
||||
tags:
|
||||
- core
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedUserAgentAllowedAppRequest'
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserAgentAllowedApps'
|
||||
description: ''
|
||||
'204':
|
||||
description: Application removed
|
||||
'400':
|
||||
description: Invalid app UUID or owner lacks access
|
||||
'403':
|
||||
description: Not the agent's owner or superuser
|
||||
/core/users/{id}/agent_allowed_apps/:
|
||||
put:
|
||||
operationId: core_users_agent_allowed_apps_update
|
||||
description: |-
|
||||
Replace the allowed application list for an agent user.
|
||||
Caller must be the agent's owner or a superuser.
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this User.
|
||||
required: true
|
||||
tags:
|
||||
- core
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserAgentAllowedAppsRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserAgentAllowedApps'
|
||||
description: ''
|
||||
'400':
|
||||
description: Invalid app UUIDs or owner lacks access
|
||||
'403':
|
||||
description: Not the agent's owner or superuser
|
||||
/core/users/{id}/impersonate/:
|
||||
post:
|
||||
operationId: core_users_impersonate_create
|
||||
@@ -4550,6 +4663,34 @@ paths:
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/core/users/agent/:
|
||||
post:
|
||||
operationId: core_users_agent_create
|
||||
description: |-
|
||||
Create a new agent user. Enterprise only. Caller must be an internal user.
|
||||
Agent users are internal users with an owner attribute that grants scoped
|
||||
application access on behalf of the owner.
|
||||
tags:
|
||||
- core
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserAgentRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserAgentResponse'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/responses/ValidationErrorResponse'
|
||||
'403':
|
||||
$ref: '#/components/responses/GenericErrorResponse'
|
||||
/core/users/export/:
|
||||
post:
|
||||
operationId: core_users_export_create
|
||||
@@ -50637,6 +50778,15 @@ components:
|
||||
maximum: 2147483647
|
||||
minimum: 0
|
||||
description: Number of passwords to check against.
|
||||
PatchedUserAgentAllowedAppRequest:
|
||||
type: object
|
||||
description: Payload to add or remove a single allowed application
|
||||
properties:
|
||||
app:
|
||||
type: string
|
||||
format: uuid
|
||||
action:
|
||||
$ref: '#/components/schemas/UserAgentAllowedAppActionEnum'
|
||||
PatchedUserDeleteStageRequest:
|
||||
type: object
|
||||
description: UserDeleteStage Serializer
|
||||
@@ -56816,6 +56966,61 @@ components:
|
||||
type: integer
|
||||
required:
|
||||
- pk
|
||||
UserAgentAllowedAppActionEnum:
|
||||
enum:
|
||||
- add
|
||||
- remove
|
||||
type: string
|
||||
UserAgentAllowedApps:
|
||||
type: object
|
||||
description: Payload to replace an agent's allowed applications
|
||||
properties:
|
||||
allowed_apps:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
required:
|
||||
- allowed_apps
|
||||
UserAgentAllowedAppsRequest:
|
||||
type: object
|
||||
description: Payload to replace an agent's allowed applications
|
||||
properties:
|
||||
allowed_apps:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
required:
|
||||
- allowed_apps
|
||||
UserAgentRequest:
|
||||
type: object
|
||||
description: Payload to create an agent user
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 150
|
||||
owner:
|
||||
type: integer
|
||||
required:
|
||||
- name
|
||||
UserAgentResponse:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
token:
|
||||
type: string
|
||||
user_uid:
|
||||
type: string
|
||||
user_pk:
|
||||
type: integer
|
||||
required:
|
||||
- token
|
||||
- user_pk
|
||||
- user_uid
|
||||
- username
|
||||
UserAttributeEnum:
|
||||
enum:
|
||||
- username
|
||||
|
||||
74
web/src/admin/users/AgentAddApplicationForm.ts
Normal file
74
web/src/admin/users/AgentAddApplicationForm.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { Form } from "#elements/forms/Form";
|
||||
|
||||
import { Application, CoreApi, CoreApplicationsListRequest, User } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
const USER_ATTRIBUTE_AGENT_OWNER_PK = "goauthentik.io/agent/owner-pk";
|
||||
|
||||
@customElement("ak-agent-add-application-form")
|
||||
export class AgentAddApplicationForm extends Form<{ app: string }> {
|
||||
public override headline = msg("Add Application");
|
||||
public override submitLabel = msg("Add");
|
||||
|
||||
@property({ attribute: false })
|
||||
public agent: User | null = null;
|
||||
|
||||
public override getSuccessMessage(): string {
|
||||
return msg("Successfully added application.");
|
||||
}
|
||||
|
||||
async send(data: { app: string }): Promise<{ app: string }> {
|
||||
if (!this.agent) throw new Error("Agent not set");
|
||||
await new CoreApi(DEFAULT_CONFIG).coreUsersAgentAllowedAppPartialUpdate({
|
||||
id: this.agent.pk,
|
||||
patchedUserAgentAllowedAppRequest: { app: data.app, action: "add" },
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
protected override renderForm(): TemplateResult {
|
||||
const ownerPk = this.agent?.attributes?.[USER_ATTRIBUTE_AGENT_OWNER_PK];
|
||||
|
||||
return html`<ak-form-element-horizontal label=${msg("Application")} required name="app">
|
||||
<ak-search-select
|
||||
placeholder=${msg("Select an application...")}
|
||||
.fetchObjects=${async (query?: string): Promise<Application[]> => {
|
||||
const args: CoreApplicationsListRequest = {
|
||||
ordering: "name",
|
||||
pageSize: 20,
|
||||
forUser: ownerPk ? Number(ownerPk) : undefined,
|
||||
};
|
||||
if (query) {
|
||||
args.search = query;
|
||||
}
|
||||
const result = await new CoreApi(DEFAULT_CONFIG).coreApplicationsList(args);
|
||||
return result.results;
|
||||
}}
|
||||
.renderElement=${(app: Application): string => {
|
||||
return app.name;
|
||||
}}
|
||||
.value=${(app: Application | undefined): string | undefined => {
|
||||
return app?.pk;
|
||||
}}
|
||||
.renderDescription=${(app: Application): TemplateResult => {
|
||||
return html`${app.group || msg("No group")}`;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-agent-add-application-form": AgentAddApplicationForm;
|
||||
}
|
||||
}
|
||||
107
web/src/admin/users/AgentForm.ts
Normal file
107
web/src/admin/users/AgentForm.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import "#components/ak-hidden-text-input";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
import "#components/ak-text-input";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { Form } from "#elements/forms/Form";
|
||||
import { ModalForm } from "#elements/forms/ModalForm";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { CoreApi, UserAgentRequest, UserAgentResponse } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@customElement("ak-user-agent-form")
|
||||
export class AgentForm extends Form<UserAgentRequest> {
|
||||
public static override verboseName = msg("Agent");
|
||||
public static override verboseNamePlural = msg("Agents");
|
||||
public override cancelButtonLabel = msg("Close");
|
||||
|
||||
@property({ attribute: false })
|
||||
result: UserAgentResponse | null = null;
|
||||
|
||||
getSuccessMessage(): string {
|
||||
return msg("Successfully created agent user.");
|
||||
}
|
||||
|
||||
async send(data: UserAgentRequest): Promise<UserAgentResponse> {
|
||||
const result = await new CoreApi(DEFAULT_CONFIG).coreUsersAgentCreate({
|
||||
userAgentRequest: data,
|
||||
});
|
||||
this.result = result;
|
||||
if (this.parentElement instanceof ModalForm) {
|
||||
this.parentElement.showSubmitButton = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public override reset(): void {
|
||||
super.reset();
|
||||
this.result = null;
|
||||
if (this.parentElement instanceof ModalForm) {
|
||||
this.parentElement.showSubmitButton = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override renderForm(): TemplateResult {
|
||||
return html`<ak-text-input
|
||||
name="name"
|
||||
label=${msg("Username")}
|
||||
placeholder=${msg("Type a username for the agent...")}
|
||||
value=""
|
||||
input-hint="code"
|
||||
required
|
||||
maxlength=${150}
|
||||
autofocus
|
||||
help=${msg(
|
||||
"The agent's primary identifier used for authentication. 150 characters or fewer.",
|
||||
)}
|
||||
></ak-text-input>`;
|
||||
}
|
||||
|
||||
protected renderResponseForm(): SlottedTemplateResult {
|
||||
return html`<p>
|
||||
${msg(
|
||||
"Use the username and token below to authenticate. The token expires in 24 hours and must be rotated before expiry.",
|
||||
)}
|
||||
</p>
|
||||
<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-text-input
|
||||
name="name"
|
||||
label=${msg("Username")}
|
||||
autocomplete="off"
|
||||
value=${ifDefined(this.result?.username)}
|
||||
input-hint="code"
|
||||
readonly
|
||||
></ak-text-input>
|
||||
|
||||
<ak-hidden-text-input
|
||||
label=${msg("Token")}
|
||||
value="${this.result?.token ?? ""}"
|
||||
input-hint="code"
|
||||
readonly
|
||||
.help=${msg(
|
||||
"Valid for 24 hours. The agent must rotate the token before it expires. If the rotation window is missed, the owner must issue a new token.",
|
||||
)}
|
||||
>
|
||||
</ak-hidden-text-input>
|
||||
</form>`;
|
||||
}
|
||||
|
||||
protected override renderFormWrapper(): SlottedTemplateResult {
|
||||
if (this.result) {
|
||||
return this.renderResponseForm();
|
||||
}
|
||||
return super.renderFormWrapper();
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-user-agent-form": AgentForm;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import "#admin/users/AgentAddApplicationForm";
|
||||
import "#elements/AppIcon";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { renderModal } from "#elements/dialogs";
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
@@ -15,6 +17,8 @@ import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
const USER_ATTRIBUTE_AGENT_OWNER_PK = "goauthentik.io/agent/owner-pk";
|
||||
|
||||
@customElement("ak-user-application-table")
|
||||
export class UserApplicationTable extends Table<Application> {
|
||||
@property({ attribute: false })
|
||||
@@ -22,6 +26,10 @@ export class UserApplicationTable extends Table<Application> {
|
||||
|
||||
static styles: CSSResult[] = [...super.styles, applicationListStyle];
|
||||
|
||||
private get isAgent(): boolean {
|
||||
return !!this.user?.attributes?.[USER_ATTRIBUTE_AGENT_OWNER_PK];
|
||||
}
|
||||
|
||||
async apiEndpoint(): Promise<PaginatedResponse<Application>> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationsList({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
@@ -38,6 +46,35 @@ export class UserApplicationTable extends Table<Application> {
|
||||
[msg("Actions"), null, msg("Row Actions")],
|
||||
];
|
||||
|
||||
private async removeApplication(app: Application): Promise<void> {
|
||||
if (!this.user) return;
|
||||
await new CoreApi(DEFAULT_CONFIG).coreUsersAgentAllowedAppPartialUpdate({
|
||||
id: this.user.pk,
|
||||
patchedUserAgentAllowedAppRequest: { app: String(app.pk), action: "remove" },
|
||||
});
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
protected openAddApplicationModal = () => {
|
||||
renderModal(
|
||||
html`<ak-agent-add-application-form
|
||||
.agent=${this.user}
|
||||
></ak-agent-add-application-form>`,
|
||||
).then(() => {
|
||||
this.fetch();
|
||||
});
|
||||
};
|
||||
|
||||
protected override renderToolbar(): SlottedTemplateResult {
|
||||
if (!this.isAgent) {
|
||||
return super.renderToolbar();
|
||||
}
|
||||
return html`<button class="pf-c-button pf-m-primary" @click=${this.openAddApplicationModal}>
|
||||
${msg("Add Application")}
|
||||
</button>
|
||||
${super.renderToolbar()}`;
|
||||
}
|
||||
|
||||
row(item: Application): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<ak-app-icon name=${item.name} icon=${ifPresent(item.metaIconUrl)}></ak-app-icon>`,
|
||||
@@ -71,6 +108,16 @@ export class UserApplicationTable extends Table<Application> {
|
||||
</pf-tooltip>
|
||||
</a>`
|
||||
: nothing}
|
||||
${this.isAgent
|
||||
? html`<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
@click=${() => this.removeApplication(item)}
|
||||
>
|
||||
<pf-tooltip position="top" content=${msg("Remove")}>
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>`
|
||||
: nothing}
|
||||
</div>`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import { css, CSSResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
const USER_ATTRIBUTE_AGENT_OWNER_PK = "goauthentik.io/agent/owner-pk";
|
||||
|
||||
const UserTypeOptions: readonly RadioOption<UserTypeEnum>[] = [
|
||||
{
|
||||
label: msg("Internal"),
|
||||
@@ -86,9 +88,19 @@ export class UserForm extends ModelForm<User, number> {
|
||||
});
|
||||
}
|
||||
|
||||
private get isAgent(): boolean {
|
||||
return !!this.instance?.attributes?.[USER_ATTRIBUTE_AGENT_OWNER_PK];
|
||||
}
|
||||
|
||||
protected override assignInstance(instance: User): void {
|
||||
super.assignInstance(instance);
|
||||
|
||||
if (this.isAgent) {
|
||||
this.verboseName = msg("Agent User");
|
||||
this.verboseNamePlural = msg("Agent Users");
|
||||
return;
|
||||
}
|
||||
|
||||
const { verboseName, verboseNamePlural } = match(instance.type)
|
||||
.with(UserTypeEnum.Internal, () => ({
|
||||
verboseName: msg("Internal User"),
|
||||
@@ -203,27 +215,44 @@ export class UserForm extends ModelForm<User, number> {
|
||||
|
||||
${this.userType
|
||||
? null
|
||||
: html`<ak-radio-input
|
||||
label=${msg("User type")}
|
||||
required
|
||||
name="type"
|
||||
.value=${this.instance?.type}
|
||||
.options=${[
|
||||
...UserTypeOptions,
|
||||
...(this.instance
|
||||
? [
|
||||
{
|
||||
label: msg("Internal Service account"),
|
||||
value: UserTypeEnum.InternalServiceAccount,
|
||||
disabled: true,
|
||||
description: html`${msg(
|
||||
"Managed by authentik and cannot be assigned manually.",
|
||||
)}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
] satisfies RadioOption<UserTypeEnum>[]}
|
||||
></ak-radio-input>`}
|
||||
: this.isAgent
|
||||
? html`<ak-radio-input
|
||||
label=${msg("User type")}
|
||||
required
|
||||
name="type"
|
||||
.value=${UserTypeEnum.Internal}
|
||||
.options=${[
|
||||
{
|
||||
label: msg("Agent"),
|
||||
value: UserTypeEnum.Internal,
|
||||
disabled: true,
|
||||
description: html`${msg(
|
||||
"Agent users are managed by their owner and cannot change type.",
|
||||
)}`,
|
||||
},
|
||||
] satisfies RadioOption<UserTypeEnum>[]}
|
||||
></ak-radio-input>`
|
||||
: html`<ak-radio-input
|
||||
label=${msg("User type")}
|
||||
required
|
||||
name="type"
|
||||
.value=${this.instance?.type}
|
||||
.options=${[
|
||||
...UserTypeOptions,
|
||||
...(this.instance
|
||||
? [
|
||||
{
|
||||
label: msg("Internal Service account"),
|
||||
value: UserTypeEnum.InternalServiceAccount,
|
||||
disabled: true,
|
||||
description: html`${msg(
|
||||
"Managed by authentik and cannot be assigned manually.",
|
||||
)}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
] satisfies RadioOption<UserTypeEnum>[]}
|
||||
></ak-radio-input>`}
|
||||
<ak-text-input
|
||||
name="email"
|
||||
label=${msg("Email Address")}
|
||||
|
||||
@@ -15,7 +15,7 @@ import "#elements/forms/ModalForm";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { userTypeToLabel } from "#common/labels";
|
||||
import { userDisplayLabel } from "#common/labels";
|
||||
import { DefaultUIConfig } from "#common/ui/config";
|
||||
import { formatUserDisplayName } from "#common/users";
|
||||
|
||||
@@ -258,7 +258,7 @@ export class UserListPage extends WithBrandConfig(
|
||||
</a>`,
|
||||
html`<ak-status-label ?good=${item.isActive}></ak-status-label>`,
|
||||
Timestamp(item.lastLogin),
|
||||
html`${userTypeToLabel(item.type)}`,
|
||||
html`${userDisplayLabel(item)}`,
|
||||
html`<div class="ak-c-table__actions">
|
||||
${IconEditButton(UserForm, item.pk, displayName)}
|
||||
${showImpersonation
|
||||
|
||||
@@ -28,7 +28,7 @@ import "./UserDevicesTable.js";
|
||||
import "#elements/ak-mdx/ak-mdx";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { userTypeToLabel } from "#common/labels";
|
||||
import { userDisplayLabel } from "#common/labels";
|
||||
import { formatUserDisplayName } from "#common/users";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
@@ -118,7 +118,7 @@ export class UserViewPage extends WithBrandConfig(WithCapabilitiesConfig(WithSes
|
||||
[msg("Last login"), Timestamp(user.lastLogin)],
|
||||
[msg("Last password change"), Timestamp(user.passwordChangeDate)],
|
||||
[msg("Active"), html`<ak-status-label ?good=${user.isActive}></ak-status-label>`],
|
||||
[msg("Type"), userTypeToLabel(user.type)],
|
||||
[msg("Type"), userDisplayLabel(user)],
|
||||
[msg("Superuser"), html`<ak-status-label type="warning" ?good=${user.isSuperuser}></ak-status-label>`],
|
||||
[msg("Actions"), this.renderActionButtons(user)],
|
||||
[msg("Recovery"), this.renderRecoveryButtons(user)],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "#admin/users/AgentForm";
|
||||
import "#admin/users/ServiceAccountForm";
|
||||
import "#admin/users/UserForm";
|
||||
import "#components/ak-hidden-text-input";
|
||||
@@ -13,7 +14,12 @@ import { WizardPage } from "#elements/wizard/WizardPage";
|
||||
|
||||
import { UserForm } from "#admin/users/UserForm";
|
||||
|
||||
import { TypeCreate, UserServiceAccountResponse, UserTypeEnum } from "@goauthentik/api";
|
||||
import {
|
||||
TypeCreate,
|
||||
UserAgentResponse,
|
||||
UserServiceAccountResponse,
|
||||
UserTypeEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html } from "lit";
|
||||
@@ -22,6 +28,10 @@ import { customElement, property, state } from "lit/decorators.js";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
|
||||
const AGENT_MODEL_NAME = "agent";
|
||||
const AGENT_FORM_SLOT = `type-ak-user-agent-form-${AGENT_MODEL_NAME}` as const;
|
||||
const AGENT_RESULT_SLOT = `${AGENT_FORM_SLOT}-result` as const;
|
||||
|
||||
const SERVICE_ACCOUNT_FORM_SLOT =
|
||||
`type-ak-user-service-account-form-${UserTypeEnum.ServiceAccount}` as const;
|
||||
const SERVICE_ACCOUNT_RESULT_SLOT = `${SERVICE_ACCOUNT_FORM_SLOT}-result` as const;
|
||||
@@ -41,6 +51,13 @@ const DEFAULT_USER_TYPES: TypeCreate[] = [
|
||||
"External consultants or B2C customers without access to enterprise features.",
|
||||
),
|
||||
},
|
||||
{
|
||||
component: "ak-user-agent-form",
|
||||
modelName: AGENT_MODEL_NAME,
|
||||
name: msg("Agent"),
|
||||
description: msg("Machine user owned by an internal user, with scoped application access."),
|
||||
requiresEnterprise: true,
|
||||
},
|
||||
{
|
||||
component: "ak-user-service-account-form",
|
||||
modelName: UserTypeEnum.ServiceAccount,
|
||||
@@ -50,6 +67,7 @@ const DEFAULT_USER_TYPES: TypeCreate[] = [
|
||||
];
|
||||
|
||||
export interface UserWizardState {
|
||||
[AGENT_FORM_SLOT]?: UserAgentResponse;
|
||||
[SERVICE_ACCOUNT_FORM_SLOT]?: UserServiceAccountResponse;
|
||||
}
|
||||
|
||||
@@ -110,6 +128,63 @@ export class ServiceAccountResultPage extends WizardPage<UserWizardState> {
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ak-user-agent-result-page")
|
||||
export class AgentResultPage extends WizardPage<UserWizardState> {
|
||||
public static styles: CSSResult[] = [PFForm, PFFormControl];
|
||||
|
||||
public override headline = msg("Review Credentials");
|
||||
|
||||
@state()
|
||||
protected result: UserAgentResponse | null = null;
|
||||
|
||||
public override activeCallback = async (): Promise<void> => {
|
||||
const result = this.host.state[AGENT_FORM_SLOT];
|
||||
|
||||
if (!result) {
|
||||
throw new TypeError("Expected agent creation result in wizard state.");
|
||||
}
|
||||
|
||||
this.result = result;
|
||||
|
||||
this.host.valid = true;
|
||||
this.host.cancelable = false;
|
||||
};
|
||||
|
||||
public override nextCallback = async (): Promise<boolean> => true;
|
||||
|
||||
protected override render(): SlottedTemplateResult {
|
||||
if (!this.result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { username, token } = this.result;
|
||||
|
||||
return html`<h3 class="pf-c-wizard__main-title">${msg("Review Credentials")}</h3>
|
||||
<h4 class="pf-c-title pf-m-md">
|
||||
${msg(
|
||||
"Use the username and token below to authenticate. The token expires in 24 hours and must be rotated before expiry.",
|
||||
)}
|
||||
</h4>
|
||||
<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-text-input
|
||||
label=${msg("Username")}
|
||||
value=${username}
|
||||
input-hint="code"
|
||||
readonly
|
||||
></ak-text-input>
|
||||
<ak-hidden-text-input
|
||||
label=${msg("Token")}
|
||||
value="${token}"
|
||||
input-hint="code"
|
||||
readonly
|
||||
.help=${msg(
|
||||
"Valid for 24 hours. The agent must rotate the token before it expires.",
|
||||
)}
|
||||
></ak-hidden-text-input>
|
||||
</form>`;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ak-user-wizard")
|
||||
export class AKUserWizard extends CreateWizard {
|
||||
/**
|
||||
@@ -128,20 +203,28 @@ export class AKUserWizard extends CreateWizard {
|
||||
|
||||
protected override selectSteps(type: TypeCreate, currentSteps: string[]): string[] {
|
||||
const { modelName } = type;
|
||||
const serviceAccount = modelName === UserTypeEnum.ServiceAccount;
|
||||
|
||||
if (!serviceAccount) {
|
||||
return super.selectSteps(type, currentSteps);
|
||||
if (modelName === AGENT_MODEL_NAME) {
|
||||
return [AGENT_FORM_SLOT, AGENT_RESULT_SLOT];
|
||||
}
|
||||
|
||||
return [
|
||||
// ---
|
||||
SERVICE_ACCOUNT_FORM_SLOT,
|
||||
SERVICE_ACCOUNT_RESULT_SLOT,
|
||||
];
|
||||
if (modelName === UserTypeEnum.ServiceAccount) {
|
||||
return [SERVICE_ACCOUNT_FORM_SLOT, SERVICE_ACCOUNT_RESULT_SLOT];
|
||||
}
|
||||
|
||||
return super.selectSteps(type, currentSteps);
|
||||
}
|
||||
|
||||
protected override renderWizardStep(type: TypeCreate): SlottedTemplateResult {
|
||||
if (type.modelName === AGENT_MODEL_NAME) {
|
||||
return [
|
||||
super.renderWizardStep(type),
|
||||
html`<ak-user-agent-result-page
|
||||
slot=${AGENT_RESULT_SLOT}
|
||||
></ak-user-agent-result-page>`,
|
||||
];
|
||||
}
|
||||
|
||||
if (type.modelName === UserTypeEnum.ServiceAccount) {
|
||||
return [
|
||||
super.renderWizardStep(type),
|
||||
@@ -155,7 +238,7 @@ export class AKUserWizard extends CreateWizard {
|
||||
}
|
||||
|
||||
protected override assembleFormProps(type: TypeCreate): LitPropertyRecord<UserForm | object> {
|
||||
if (type.modelName === UserTypeEnum.ServiceAccount) {
|
||||
if (type.modelName === AGENT_MODEL_NAME || type.modelName === UserTypeEnum.ServiceAccount) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -171,6 +254,7 @@ export class AKUserWizard extends CreateWizard {
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-user-wizard": AKUserWizard;
|
||||
"ak-user-agent-result-page": AgentResultPage;
|
||||
"ak-user-service-account-result-page": ServiceAccountResultPage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,14 @@ import {
|
||||
EventActions,
|
||||
IntentEnum,
|
||||
SeverityEnum,
|
||||
User,
|
||||
UserTypeEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
|
||||
const USER_ATTRIBUTE_AGENT_OWNER_PK = "goauthentik.io/agent/owner-pk";
|
||||
|
||||
/* Various tables in the API for which we need to supply labels */
|
||||
|
||||
export const intentEnumToLabel = new Map<IntentEnum, string>([
|
||||
@@ -122,3 +125,6 @@ const _userTypeToLabel = new Map<UserTypeEnum | undefined, string>([
|
||||
|
||||
export const userTypeToLabel = (type?: UserTypeEnum): string =>
|
||||
_userTypeToLabel.get(type) ?? type ?? "";
|
||||
|
||||
export const userDisplayLabel = (user: User): string =>
|
||||
user.attributes?.[USER_ATTRIBUTE_AGENT_OWNER_PK] ? msg("Agent") : userTypeToLabel(user.type);
|
||||
|
||||
Reference in New Issue
Block a user