Compare commits

...

24 Commits

Author SHA1 Message Date
Fletcher Heisler
6d22c39e8e lint 2026-04-13 21:33:20 -04:00
Fletcher Heisler
e939d60e18 explicit perms to add user + token, non-admin owners 2026-04-13 20:44:21 -04:00
Fletcher Heisler
88c458ba3b owners must be admins, for now 2026-04-13 20:22:53 -04:00
Fletcher Heisler
014e0516c3 token lookup, no owner token viewing 2026-04-13 19:21:10 -04:00
Fletcher Heisler
4647294541 more realistic test 2026-04-13 18:49:25 -04:00
Fletcher Heisler
084194207d Merge branch 'main' into enterprise/agent-users 2026-04-13 15:02:25 -07:00
Fletcher Heisler
685684c89a test fix, wish I could run these 2026-04-13 17:59:09 -04:00
Fletcher Heisler
f495747b2a fix test 2026-04-13 17:45:04 -04:00
Fletcher Heisler
6222531f2d split migration 2026-04-13 17:31:38 -04:00
Fletcher Heisler
787cdfb8f2 test fix 2026-04-13 17:28:28 -04:00
Fletcher Heisler
b761fd8c6d remake web, gen 2026-04-13 16:55:31 -04:00
Fletcher Heisler
3ce09b96d7 lint 2026-04-13 16:50:28 -04:00
Fletcher Heisler
0f3925bb4e auth in body 2026-04-13 16:08:23 -04:00
Fletcher Heisler
498ebe8840 test fixes 2026-04-13 16:04:34 -04:00
Fletcher Heisler
1aa57f732b explicit return 2026-04-13 15:56:15 -04:00
Fletcher Heisler
77123ffce7 dedupe logic 2026-04-13 15:52:54 -04:00
Fletcher Heisler
7919569056 add agent session view + tests 2026-04-13 15:34:08 -04:00
Fletcher Heisler
4e09e82b2f agent token session API view 2026-04-13 13:40:53 -04:00
Fletcher Heisler
1687949d71 is this how to frontend 2026-04-13 13:08:56 -04:00
Fletcher Heisler
5ed8444840 owner add/remove application from agent access 2026-04-13 12:33:18 -04:00
Fletcher Heisler
8447fad9c8 enterprise gate on agent creation 2026-04-13 10:48:34 -04:00
Fletcher Heisler
dd9c5cde6e check for anonymous user 2026-04-13 10:42:20 -04:00
Fletcher Heisler
66716b8296 exchange token for session 2026-04-12 23:01:25 -04:00
Fletcher Heisler
7c65b8a8d3 enterprise/agent user types + tests first pass 2026-04-12 22:07:38 -04:00
58 changed files with 4950 additions and 45 deletions

View File

@@ -1,5 +1,6 @@
"""Tokens API Viewset"""
from datetime import timedelta
from typing import Any
from django.utils.timezone import now
@@ -18,12 +19,15 @@ 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,
UserTypes,
default_token_duration,
default_token_key,
)
from authentik.events.models import Event, EventAction
from authentik.events.utils import model_to_dict
@@ -171,6 +175,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"),
},
)
@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
is_agent_owner = token.user.type == UserTypes.AGENT and str(
request.user.pk
) == token.user.attributes.get(USER_ATTRIBUTE_AGENT_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(),

View File

@@ -75,9 +75,13 @@ 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_AGENT,
USER_PATH_SERVICE_ACCOUNT,
USERNAME_MAX_LENGTH,
Application,
Group,
Session,
Token,
@@ -88,6 +92,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 +254,25 @@ 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.type == UserTypes.AGENT
and user_type != UserTypes.AGENT.value
):
raise ValidationError(_("Can't change agent user type."))
return user_type
def validate_attributes(self, attrs: dict) -> dict:
"""Prevent changes to agent owner"""
if not self.instance:
return attrs
if self.instance.type == UserTypes.AGENT:
existing_owner = self.instance.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
new_owner = attrs.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
if existing_owner is not None and 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 +427,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 +733,260 @@ 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."""
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.AGENT,
attributes={
USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk),
USER_ATTRIBUTE_AGENT_ALLOWED_APPS: [],
},
path=USER_PATH_AGENT,
)
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 and the caller is authorized."""
agent: User = self.get_object()
if agent.type != UserTypes.AGENT:
raise ValidationError(_("User is not an agent user."))
owner_pk = agent.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
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",

View File

@@ -18,6 +18,7 @@ class Command(TenantCommand):
User.objects.exclude_anonymous()
.exclude(type=UserTypes.SERVICE_ACCOUNT)
.exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
.exclude(type=UserTypes.AGENT)
)
if options["usernames"] and options["all"]:
self.stderr.write("--all and usernames specified, only one can be specified")

View File

@@ -0,0 +1,41 @@
# Generated by Django 5.2.13 on 2026-04-13 21:29
from django.db import migrations, models
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",
},
),
migrations.AlterField(
model_name="user",
name="type",
field=models.TextField(
choices=[
("internal", "Internal"),
("external", "External"),
("service_account", "Service Account"),
("internal_service_account", "Internal Service Account"),
("agent", "Agent"),
],
default="internal",
),
),
]

View File

@@ -67,6 +67,10 @@ 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"
USER_PATH_AGENT = f"{USER_PATH_SYSTEM_PREFIX}/agents"
options.DEFAULT_NAMES = options.DEFAULT_NAMES + (
# used_by API that allows models to specify if they shadow an object
@@ -126,6 +130,9 @@ class UserTypes(models.TextChoices):
# accounts, such as outpost users
INTERNAL_SERVICE_ACCOUNT = "internal_service_account"
# Enterprise-gated agent users owned by an internal user
AGENT = "agent"
class AttributesMixin(models.Model):
"""Adds an attributes property to a model"""
@@ -385,6 +392,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"]),

View File

@@ -11,12 +11,14 @@ 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,
ExpiringModel,
Session,
User,
UserTypes,
default_token_duration,
)
from authentik.flows.apps import RefreshOtherFlowsAfterAuthentication
@@ -69,6 +71,35 @@ 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(
type=UserTypes.AGENT,
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"""

View File

@@ -0,0 +1,84 @@
"""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,
USER_PATH_AGENT,
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.AGENT,
attributes={USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk)},
path=USER_PATH_AGENT,
)
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)

View File

@@ -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(

View File

@@ -2,7 +2,14 @@
from django.test.testcases import TestCase
from authentik.core.models import User
from authentik.core.models import (
USER_ATTRIBUTE_AGENT_OWNER_PK,
USER_PATH_AGENT,
AuthenticatedSession,
Session,
User,
UserTypes,
)
from authentik.events.models import Event
from authentik.lib.generators import generate_id
@@ -33,3 +40,93 @@ 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.AGENT,
attributes={USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk)},
path=USER_PATH_AGENT,
)
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())

View File

@@ -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,15 @@ 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,
USER_PATH_AGENT,
Application,
AuthenticatedSession,
Session,
Token,
TokenIntents,
User,
UserTypes,
)
@@ -878,3 +884,249 @@ 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.AGENT,
attributes={
USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk),
USER_ATTRIBUTE_AGENT_ALLOWED_APPS: [],
},
path=USER_PATH_AGENT,
)
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.AGENT)
self.assertEqual(agent.path, USER_PATH_AGENT)
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.INTERNAL},
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_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_add_duplicate(self):
"""PATCH add: adding an already-allowed app is idempotent"""
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": "add"},
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
agent.refresh_from_db()
self.assertEqual(agent.attributes[USER_ATTRIBUTE_AGENT_ALLOWED_APPS].count(str(app.pk)), 1)
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)

View File

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

View File

@@ -0,0 +1,50 @@
"""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 AuthenticatedSession, Token, TokenIntents, UserTypes
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 token.user.type != UserTypes.AGENT:
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)

View File

@@ -30,6 +30,7 @@ class RootRedirectView(RedirectView):
UserTypes.EXTERNAL,
UserTypes.SERVICE_ACCOUNT,
UserTypes.INTERNAL_SERVICE_ACCOUNT,
UserTypes.AGENT,
):
brand: Brand = request.brand
if brand.default_application:
@@ -70,6 +71,7 @@ class BrandDefaultRedirectView(InterfaceView):
UserTypes.EXTERNAL,
UserTypes.SERVICE_ACCOUNT,
UserTypes.INTERNAL_SERVICE_ACCOUNT,
UserTypes.AGENT,
):
brand: Brand = request.brand
if brand.default_application:

View File

@@ -141,8 +141,10 @@ 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
base = (
base.exclude(type=UserTypes.SERVICE_ACCOUNT)
.exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
.exclude(type=UserTypes.AGENT)
)
if self.filter_group:
base = base.filter(groups__in=[self.filter_group])

View File

@@ -130,8 +130,10 @@ 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
base = (
base.exclude(type=UserTypes.SERVICE_ACCOUNT)
.exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
.exclude(type=UserTypes.AGENT)
)
if self.filter_group:
base = base.filter(groups__in=[self.filter_group])

View File

@@ -6,6 +6,18 @@ 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 Session, User, UserTypes
agents = User.objects.filter(type=UserTypes.AGENT, 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()

View File

@@ -0,0 +1,64 @@
"""Enterprise task tests"""
from django.test import TestCase
from authentik.core.models import (
USER_ATTRIBUTE_AGENT_OWNER_PK,
USER_PATH_AGENT,
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.AGENT,
attributes={USER_ATTRIBUTE_AGENT_OWNER_PK: str(owner.pk)},
path=USER_PATH_AGENT,
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 service accounts"""
from authentik.enterprise.tasks import _deactivate_agent_users
sa = User.objects.create(
username=generate_id(),
type=UserTypes.SERVICE_ACCOUNT,
is_active=True,
)
internal = User.objects.create(
username=generate_id(),
type=UserTypes.INTERNAL,
is_active=True,
)
_deactivate_agent_users()
sa.refresh_from_db()
internal.refresh_from_db()
self.assertTrue(sa.is_active)
self.assertTrue(internal.is_active)

View File

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

View File

@@ -202,9 +202,51 @@ 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,
UserTypes,
)
user = self.request.user
if not hasattr(user, "type") or user.type != UserTypes.AGENT:
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_pk = user.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK)
if not owner_pk:
return PolicyResult(False, "Agent has no owner configured.")
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)

View File

@@ -50,6 +50,12 @@ 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 set_http_request(self, request: HttpRequest):
"""Update context based on http request"""
@@ -58,6 +64,46 @@ class PolicyEvaluator(BaseEvaluator):
self._context["ak_client_ip"] = ip_address(ClientIPMiddleware.get_client_ip(request))
self._context["http_request"] = 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,
UserTypes,
)
from authentik.policies.engine import PolicyEngine
user = request.user
app = request.obj
if user.type != UserTypes.AGENT:
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 handle_error(self, exc: Exception, expression_source: str):
"""Exception Handler"""
raise PolicyException(exc)

View File

@@ -5,7 +5,14 @@ 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,
USER_PATH_AGENT,
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 +142,84 @@ 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.AGENT,
attributes={
USER_ATTRIBUTE_AGENT_OWNER_PK: str(self.owner.pk),
USER_ATTRIBUTE_AGENT_ALLOWED_APPS: allowed_apps if allowed_apps is not None else [],
},
path=USER_PATH_AGENT,
)
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.AGENT,
attributes={
USER_ATTRIBUTE_AGENT_OWNER_PK: "999999",
USER_ATTRIBUTE_AGENT_ALLOWED_APPS: [str(self.app.pk)],
},
path=USER_PATH_AGENT,
)
evaluator = self._evaluator_with(agent)
result = evaluator._context["has_access_to_application"]()
self.assertFalse(result)
def test_no_attributes_returns_false(self):
"""Returns False when the user has no attributes"""
user = get_anonymous_user()
evaluator = self._evaluator_with(user)
result = evaluator._context["has_access_to_application"]()
self.assertFalse(result)
class TestExpressionPolicyAPI(APITestCase):
"""Test expression policy's API"""

View File

@@ -5,7 +5,15 @@ 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,
USER_PATH_AGENT,
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 +217,93 @@ class TestPolicyEngine(TestCase):
engine.build()
self.assertLess(ctx.final_queries, 1000)
self.assertTrue(engine.result.passing)
def test_anonymous_user(self):
"""AnonymousUser (no type attribute) 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.AGENT,
attributes={
USER_ATTRIBUTE_AGENT_OWNER_PK: str(self.owner.pk),
USER_ATTRIBUTE_AGENT_ALLOWED_APPS: allowed_apps if allowed_apps is not None else [],
},
path=USER_PATH_AGENT,
)
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.AGENT,
attributes={
USER_ATTRIBUTE_AGENT_OWNER_PK: "999999",
USER_ATTRIBUTE_AGENT_ALLOWED_APPS: [str(self.app.pk)],
},
path=USER_PATH_AGENT,
)
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)

View File

@@ -188,8 +188,10 @@ 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
base = (
base.exclude(type=UserTypes.SERVICE_ACCOUNT)
.exclude(type=UserTypes.INTERNAL_SERVICE_ACCOUNT)
.exclude(type=UserTypes.AGENT)
)
# Filter users by their access to the backchannel application if an application is set

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.2.13 on 2026-04-13 21:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_user_write", "0008_userwritestage_user_type"),
]
operations = [
migrations.AlterField(
model_name="userwritestage",
name="user_type",
field=models.TextField(
choices=[
("internal", "Internal"),
("external", "External"),
("service_account", "Service Account"),
("internal_service_account", "Internal Service Account"),
("agent", "Agent"),
],
default="external",
),
),
]

View File

@@ -5520,7 +5520,8 @@
"internal",
"external",
"service_account",
"internal_service_account"
"internal_service_account",
"agent"
],
"title": "Type"
},
@@ -5545,6 +5546,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 +6259,7 @@
"permission": {
"type": "string",
"enum": [
"add_agent_user",
"add_user",
"change_user",
"delete_user",
@@ -11212,6 +11215,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",
@@ -15976,7 +15980,8 @@
"internal",
"external",
"service_account",
"internal_service_account"
"internal_service_account",
"agent"
],
"title": "User type"
},

View File

@@ -25,6 +25,115 @@ import (
// CoreAPIService CoreAPI service
type CoreAPIService service
type ApiCoreAgentSessionCreateRequest struct {
ctx context.Context
ApiService *CoreAPIService
}
func (r ApiCoreAgentSessionCreateRequest) Execute() (*http.Response, error) {
return r.ApiService.CoreAgentSessionCreateExecute(r)
}
/*
CoreAgentSessionCreate Method for CoreAgentSessionCreate
Exchange an agent's API token for an authenticated session.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@return ApiCoreAgentSessionCreateRequest
*/
func (a *CoreAPIService) CoreAgentSessionCreate(ctx context.Context) ApiCoreAgentSessionCreateRequest {
return ApiCoreAgentSessionCreateRequest{
ApiService: a,
ctx: ctx,
}
}
// Execute executes the request
func (a *CoreAPIService) CoreAgentSessionCreateExecute(r ApiCoreAgentSessionCreateRequest) (*http.Response, error) {
var (
localVarHTTPMethod = http.MethodPost
localVarPostBody interface{}
formFiles []formFile
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CoreAPIService.CoreAgentSessionCreate")
if err != nil {
return nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/core/agent/session/"
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 400 {
var v ValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
return localVarHTTPResponse, newErr
}
if localVarHTTPResponse.StatusCode == 403 {
var v GenericError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarHTTPResponse, newErr
}
return localVarHTTPResponse, nil
}
type ApiCoreApplicationEntitlementsCreateRequest struct {
ctx context.Context
ApiService *CoreAPIService
@@ -6066,6 +6175,121 @@ func (a *CoreAPIService) CoreTokensRetrieveExecute(r ApiCoreTokensRetrieveReques
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiCoreTokensRotateCreateRequest struct {
ctx context.Context
ApiService *CoreAPIService
identifier string
}
func (r ApiCoreTokensRotateCreateRequest) Execute() (*TokenView, *http.Response, error) {
return r.ApiService.CoreTokensRotateCreateExecute(r)
}
/*
CoreTokensRotateCreate Method for CoreTokensRotateCreate
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.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param identifier
@return ApiCoreTokensRotateCreateRequest
*/
func (a *CoreAPIService) CoreTokensRotateCreate(ctx context.Context, identifier string) ApiCoreTokensRotateCreateRequest {
return ApiCoreTokensRotateCreateRequest{
ApiService: a,
ctx: ctx,
identifier: identifier,
}
}
// Execute executes the request
//
// @return TokenView
func (a *CoreAPIService) CoreTokensRotateCreateExecute(r ApiCoreTokensRotateCreateRequest) (*TokenView, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodPost
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *TokenView
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CoreAPIService.CoreTokensRotateCreate")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/core/tokens/{identifier}/rotate/"
localVarPath = strings.Replace(localVarPath, "{"+"identifier"+"}", url.PathEscape(parameterValueToString(r.identifier, "identifier")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 400 {
var v ValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiCoreTokensSetKeyCreateRequest struct {
ctx context.Context
ApiService *CoreAPIService
@@ -7240,6 +7464,367 @@ func (a *CoreAPIService) CoreUserConsentUsedByListExecute(r ApiCoreUserConsentUs
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiCoreUsersAgentAllowedAppPartialUpdateRequest struct {
ctx context.Context
ApiService *CoreAPIService
id int32
patchedUserAgentAllowedAppRequest *PatchedUserAgentAllowedAppRequest
}
func (r ApiCoreUsersAgentAllowedAppPartialUpdateRequest) PatchedUserAgentAllowedAppRequest(patchedUserAgentAllowedAppRequest PatchedUserAgentAllowedAppRequest) ApiCoreUsersAgentAllowedAppPartialUpdateRequest {
r.patchedUserAgentAllowedAppRequest = &patchedUserAgentAllowedAppRequest
return r
}
func (r ApiCoreUsersAgentAllowedAppPartialUpdateRequest) Execute() (*UserAgentAllowedApps, *http.Response, error) {
return r.ApiService.CoreUsersAgentAllowedAppPartialUpdateExecute(r)
}
/*
CoreUsersAgentAllowedAppPartialUpdate Method for CoreUsersAgentAllowedAppPartialUpdate
Add or remove a single application from an agent's allowed list.
Caller must be the agent's owner or a superuser.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param id A unique integer value identifying this User.
@return ApiCoreUsersAgentAllowedAppPartialUpdateRequest
*/
func (a *CoreAPIService) CoreUsersAgentAllowedAppPartialUpdate(ctx context.Context, id int32) ApiCoreUsersAgentAllowedAppPartialUpdateRequest {
return ApiCoreUsersAgentAllowedAppPartialUpdateRequest{
ApiService: a,
ctx: ctx,
id: id,
}
}
// Execute executes the request
//
// @return UserAgentAllowedApps
func (a *CoreAPIService) CoreUsersAgentAllowedAppPartialUpdateExecute(r ApiCoreUsersAgentAllowedAppPartialUpdateRequest) (*UserAgentAllowedApps, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodPatch
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *UserAgentAllowedApps
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CoreAPIService.CoreUsersAgentAllowedAppPartialUpdate")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/core/users/{id}/agent_allowed_app/"
localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterValueToString(r.id, "id")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{"application/json"}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
// body params
localVarPostBody = r.patchedUserAgentAllowedAppRequest
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiCoreUsersAgentAllowedAppsUpdateRequest struct {
ctx context.Context
ApiService *CoreAPIService
id int32
userAgentAllowedAppsRequest *UserAgentAllowedAppsRequest
}
func (r ApiCoreUsersAgentAllowedAppsUpdateRequest) UserAgentAllowedAppsRequest(userAgentAllowedAppsRequest UserAgentAllowedAppsRequest) ApiCoreUsersAgentAllowedAppsUpdateRequest {
r.userAgentAllowedAppsRequest = &userAgentAllowedAppsRequest
return r
}
func (r ApiCoreUsersAgentAllowedAppsUpdateRequest) Execute() (*UserAgentAllowedApps, *http.Response, error) {
return r.ApiService.CoreUsersAgentAllowedAppsUpdateExecute(r)
}
/*
CoreUsersAgentAllowedAppsUpdate Method for CoreUsersAgentAllowedAppsUpdate
Replace the allowed application list for an agent user.
Caller must be the agent's owner or a superuser.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param id A unique integer value identifying this User.
@return ApiCoreUsersAgentAllowedAppsUpdateRequest
*/
func (a *CoreAPIService) CoreUsersAgentAllowedAppsUpdate(ctx context.Context, id int32) ApiCoreUsersAgentAllowedAppsUpdateRequest {
return ApiCoreUsersAgentAllowedAppsUpdateRequest{
ApiService: a,
ctx: ctx,
id: id,
}
}
// Execute executes the request
//
// @return UserAgentAllowedApps
func (a *CoreAPIService) CoreUsersAgentAllowedAppsUpdateExecute(r ApiCoreUsersAgentAllowedAppsUpdateRequest) (*UserAgentAllowedApps, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodPut
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *UserAgentAllowedApps
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CoreAPIService.CoreUsersAgentAllowedAppsUpdate")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/core/users/{id}/agent_allowed_apps/"
localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterValueToString(r.id, "id")), -1)
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
if r.userAgentAllowedAppsRequest == nil {
return localVarReturnValue, nil, reportError("userAgentAllowedAppsRequest is required and must be specified")
}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{"application/json"}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
// body params
localVarPostBody = r.userAgentAllowedAppsRequest
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiCoreUsersAgentCreateRequest struct {
ctx context.Context
ApiService *CoreAPIService
userAgentRequest *UserAgentRequest
}
func (r ApiCoreUsersAgentCreateRequest) UserAgentRequest(userAgentRequest UserAgentRequest) ApiCoreUsersAgentCreateRequest {
r.userAgentRequest = &userAgentRequest
return r
}
func (r ApiCoreUsersAgentCreateRequest) Execute() (*UserAgentResponse, *http.Response, error) {
return r.ApiService.CoreUsersAgentCreateExecute(r)
}
/*
CoreUsersAgentCreate Method for CoreUsersAgentCreate
Create a new agent user. Enterprise only. Caller must be an internal user.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@return ApiCoreUsersAgentCreateRequest
*/
func (a *CoreAPIService) CoreUsersAgentCreate(ctx context.Context) ApiCoreUsersAgentCreateRequest {
return ApiCoreUsersAgentCreateRequest{
ApiService: a,
ctx: ctx,
}
}
// Execute executes the request
//
// @return UserAgentResponse
func (a *CoreAPIService) CoreUsersAgentCreateExecute(r ApiCoreUsersAgentCreateRequest) (*UserAgentResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodPost
localVarPostBody interface{}
formFiles []formFile
localVarReturnValue *UserAgentResponse
)
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "CoreAPIService.CoreUsersAgentCreate")
if err != nil {
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
}
localVarPath := localBasePath + "/core/users/agent/"
localVarHeaderParams := make(map[string]string)
localVarQueryParams := url.Values{}
localVarFormParams := url.Values{}
if r.userAgentRequest == nil {
return localVarReturnValue, nil, reportError("userAgentRequest is required and must be specified")
}
// to determine the Content-Type header
localVarHTTPContentTypes := []string{"application/json"}
// set Content-Type header
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
if localVarHTTPContentType != "" {
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
}
// to determine the Accept header
localVarHTTPHeaderAccepts := []string{"application/json"}
// set Accept header
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
if localVarHTTPHeaderAccept != "" {
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
}
// body params
localVarPostBody = r.userAgentRequest
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
if err != nil {
return localVarReturnValue, nil, err
}
localVarHTTPResponse, err := a.client.callAPI(req)
if err != nil || localVarHTTPResponse == nil {
return localVarReturnValue, localVarHTTPResponse, err
}
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
localVarHTTPResponse.Body.Close()
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
if err != nil {
return localVarReturnValue, localVarHTTPResponse, err
}
if localVarHTTPResponse.StatusCode >= 300 {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: localVarHTTPResponse.Status,
}
if localVarHTTPResponse.StatusCode == 400 {
var v ValidationError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
return localVarReturnValue, localVarHTTPResponse, newErr
}
if localVarHTTPResponse.StatusCode == 403 {
var v GenericError
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr.error = err.Error()
return localVarReturnValue, localVarHTTPResponse, newErr
}
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
newErr.model = v
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
if err != nil {
newErr := &GenericOpenAPIError{
body: localVarBody,
error: err.Error(),
}
return localVarReturnValue, localVarHTTPResponse, newErr
}
return localVarReturnValue, localVarHTTPResponse, nil
}
type ApiCoreUsersCreateRequest struct {
ctx context.Context
ApiService *CoreAPIService

View File

@@ -0,0 +1,191 @@
/*
authentik
Making authentication simple.
API version: 2026.5.0-rc1
Contact: hello@goauthentik.io
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package api
import (
"encoding/json"
)
// checks if the PatchedUserAgentAllowedAppRequest type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &PatchedUserAgentAllowedAppRequest{}
// PatchedUserAgentAllowedAppRequest Payload to add or remove a single allowed application
type PatchedUserAgentAllowedAppRequest struct {
App *string `json:"app,omitempty"`
Action *UserAgentAllowedAppActionEnum `json:"action,omitempty"`
AdditionalProperties map[string]interface{}
}
type _PatchedUserAgentAllowedAppRequest PatchedUserAgentAllowedAppRequest
// NewPatchedUserAgentAllowedAppRequest instantiates a new PatchedUserAgentAllowedAppRequest object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewPatchedUserAgentAllowedAppRequest() *PatchedUserAgentAllowedAppRequest {
this := PatchedUserAgentAllowedAppRequest{}
return &this
}
// NewPatchedUserAgentAllowedAppRequestWithDefaults instantiates a new PatchedUserAgentAllowedAppRequest object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewPatchedUserAgentAllowedAppRequestWithDefaults() *PatchedUserAgentAllowedAppRequest {
this := PatchedUserAgentAllowedAppRequest{}
return &this
}
// GetApp returns the App field value if set, zero value otherwise.
func (o *PatchedUserAgentAllowedAppRequest) GetApp() string {
if o == nil || IsNil(o.App) {
var ret string
return ret
}
return *o.App
}
// GetAppOk returns a tuple with the App field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *PatchedUserAgentAllowedAppRequest) GetAppOk() (*string, bool) {
if o == nil || IsNil(o.App) {
return nil, false
}
return o.App, true
}
// HasApp returns a boolean if a field has been set.
func (o *PatchedUserAgentAllowedAppRequest) HasApp() bool {
if o != nil && !IsNil(o.App) {
return true
}
return false
}
// SetApp gets a reference to the given string and assigns it to the App field.
func (o *PatchedUserAgentAllowedAppRequest) SetApp(v string) {
o.App = &v
}
// GetAction returns the Action field value if set, zero value otherwise.
func (o *PatchedUserAgentAllowedAppRequest) GetAction() UserAgentAllowedAppActionEnum {
if o == nil || IsNil(o.Action) {
var ret UserAgentAllowedAppActionEnum
return ret
}
return *o.Action
}
// GetActionOk returns a tuple with the Action field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *PatchedUserAgentAllowedAppRequest) GetActionOk() (*UserAgentAllowedAppActionEnum, bool) {
if o == nil || IsNil(o.Action) {
return nil, false
}
return o.Action, true
}
// HasAction returns a boolean if a field has been set.
func (o *PatchedUserAgentAllowedAppRequest) HasAction() bool {
if o != nil && !IsNil(o.Action) {
return true
}
return false
}
// SetAction gets a reference to the given UserAgentAllowedAppActionEnum and assigns it to the Action field.
func (o *PatchedUserAgentAllowedAppRequest) SetAction(v UserAgentAllowedAppActionEnum) {
o.Action = &v
}
func (o PatchedUserAgentAllowedAppRequest) MarshalJSON() ([]byte, error) {
toSerialize, err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o PatchedUserAgentAllowedAppRequest) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
if !IsNil(o.App) {
toSerialize["app"] = o.App
}
if !IsNil(o.Action) {
toSerialize["action"] = o.Action
}
for key, value := range o.AdditionalProperties {
toSerialize[key] = value
}
return toSerialize, nil
}
func (o *PatchedUserAgentAllowedAppRequest) UnmarshalJSON(data []byte) (err error) {
varPatchedUserAgentAllowedAppRequest := _PatchedUserAgentAllowedAppRequest{}
err = json.Unmarshal(data, &varPatchedUserAgentAllowedAppRequest)
if err != nil {
return err
}
*o = PatchedUserAgentAllowedAppRequest(varPatchedUserAgentAllowedAppRequest)
additionalProperties := make(map[string]interface{})
if err = json.Unmarshal(data, &additionalProperties); err == nil {
delete(additionalProperties, "app")
delete(additionalProperties, "action")
o.AdditionalProperties = additionalProperties
}
return err
}
type NullablePatchedUserAgentAllowedAppRequest struct {
value *PatchedUserAgentAllowedAppRequest
isSet bool
}
func (v NullablePatchedUserAgentAllowedAppRequest) Get() *PatchedUserAgentAllowedAppRequest {
return v.value
}
func (v *NullablePatchedUserAgentAllowedAppRequest) Set(val *PatchedUserAgentAllowedAppRequest) {
v.value = val
v.isSet = true
}
func (v NullablePatchedUserAgentAllowedAppRequest) IsSet() bool {
return v.isSet
}
func (v *NullablePatchedUserAgentAllowedAppRequest) Unset() {
v.value = nil
v.isSet = false
}
func NewNullablePatchedUserAgentAllowedAppRequest(val *PatchedUserAgentAllowedAppRequest) *NullablePatchedUserAgentAllowedAppRequest {
return &NullablePatchedUserAgentAllowedAppRequest{value: val, isSet: true}
}
func (v NullablePatchedUserAgentAllowedAppRequest) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullablePatchedUserAgentAllowedAppRequest) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View File

@@ -0,0 +1,111 @@
/*
authentik
Making authentication simple.
API version: 2026.5.0-rc1
Contact: hello@goauthentik.io
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package api
import (
"encoding/json"
"fmt"
)
// UserAgentAllowedAppActionEnum the model 'UserAgentAllowedAppActionEnum'
type UserAgentAllowedAppActionEnum string
// List of UserAgentAllowedAppActionEnum
const (
USERAGENTALLOWEDAPPACTIONENUM_ADD UserAgentAllowedAppActionEnum = "add"
USERAGENTALLOWEDAPPACTIONENUM_REMOVE UserAgentAllowedAppActionEnum = "remove"
)
// All allowed values of UserAgentAllowedAppActionEnum enum
var AllowedUserAgentAllowedAppActionEnumEnumValues = []UserAgentAllowedAppActionEnum{
"add",
"remove",
}
func (v *UserAgentAllowedAppActionEnum) UnmarshalJSON(src []byte) error {
var value string
err := json.Unmarshal(src, &value)
if err != nil {
return err
}
enumTypeValue := UserAgentAllowedAppActionEnum(value)
for _, existing := range AllowedUserAgentAllowedAppActionEnumEnumValues {
if existing == enumTypeValue {
*v = enumTypeValue
return nil
}
}
return fmt.Errorf("%+v is not a valid UserAgentAllowedAppActionEnum", value)
}
// NewUserAgentAllowedAppActionEnumFromValue returns a pointer to a valid UserAgentAllowedAppActionEnum
// for the value passed as argument, or an error if the value passed is not allowed by the enum
func NewUserAgentAllowedAppActionEnumFromValue(v string) (*UserAgentAllowedAppActionEnum, error) {
ev := UserAgentAllowedAppActionEnum(v)
if ev.IsValid() {
return &ev, nil
} else {
return nil, fmt.Errorf("invalid value '%v' for UserAgentAllowedAppActionEnum: valid values are %v", v, AllowedUserAgentAllowedAppActionEnumEnumValues)
}
}
// IsValid return true if the value is valid for the enum, false otherwise
func (v UserAgentAllowedAppActionEnum) IsValid() bool {
for _, existing := range AllowedUserAgentAllowedAppActionEnumEnumValues {
if existing == v {
return true
}
}
return false
}
// Ptr returns reference to UserAgentAllowedAppActionEnum value
func (v UserAgentAllowedAppActionEnum) Ptr() *UserAgentAllowedAppActionEnum {
return &v
}
type NullableUserAgentAllowedAppActionEnum struct {
value *UserAgentAllowedAppActionEnum
isSet bool
}
func (v NullableUserAgentAllowedAppActionEnum) Get() *UserAgentAllowedAppActionEnum {
return v.value
}
func (v *NullableUserAgentAllowedAppActionEnum) Set(val *UserAgentAllowedAppActionEnum) {
v.value = val
v.isSet = true
}
func (v NullableUserAgentAllowedAppActionEnum) IsSet() bool {
return v.isSet
}
func (v *NullableUserAgentAllowedAppActionEnum) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableUserAgentAllowedAppActionEnum(val *UserAgentAllowedAppActionEnum) *NullableUserAgentAllowedAppActionEnum {
return &NullableUserAgentAllowedAppActionEnum{value: val, isSet: true}
}
func (v NullableUserAgentAllowedAppActionEnum) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableUserAgentAllowedAppActionEnum) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View File

@@ -0,0 +1,167 @@
/*
authentik
Making authentication simple.
API version: 2026.5.0-rc1
Contact: hello@goauthentik.io
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package api
import (
"encoding/json"
"fmt"
)
// checks if the UserAgentAllowedApps type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &UserAgentAllowedApps{}
// UserAgentAllowedApps Payload to replace an agent's allowed applications
type UserAgentAllowedApps struct {
AllowedApps []string `json:"allowed_apps"`
AdditionalProperties map[string]interface{}
}
type _UserAgentAllowedApps UserAgentAllowedApps
// NewUserAgentAllowedApps instantiates a new UserAgentAllowedApps object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewUserAgentAllowedApps(allowedApps []string) *UserAgentAllowedApps {
this := UserAgentAllowedApps{}
this.AllowedApps = allowedApps
return &this
}
// NewUserAgentAllowedAppsWithDefaults instantiates a new UserAgentAllowedApps object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewUserAgentAllowedAppsWithDefaults() *UserAgentAllowedApps {
this := UserAgentAllowedApps{}
return &this
}
// GetAllowedApps returns the AllowedApps field value
func (o *UserAgentAllowedApps) GetAllowedApps() []string {
if o == nil {
var ret []string
return ret
}
return o.AllowedApps
}
// GetAllowedAppsOk returns a tuple with the AllowedApps field value
// and a boolean to check if the value has been set.
func (o *UserAgentAllowedApps) GetAllowedAppsOk() ([]string, bool) {
if o == nil {
return nil, false
}
return o.AllowedApps, true
}
// SetAllowedApps sets field value
func (o *UserAgentAllowedApps) SetAllowedApps(v []string) {
o.AllowedApps = v
}
func (o UserAgentAllowedApps) MarshalJSON() ([]byte, error) {
toSerialize, err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o UserAgentAllowedApps) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["allowed_apps"] = o.AllowedApps
for key, value := range o.AdditionalProperties {
toSerialize[key] = value
}
return toSerialize, nil
}
func (o *UserAgentAllowedApps) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"allowed_apps",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err
}
for _, requiredProperty := range requiredProperties {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varUserAgentAllowedApps := _UserAgentAllowedApps{}
err = json.Unmarshal(data, &varUserAgentAllowedApps)
if err != nil {
return err
}
*o = UserAgentAllowedApps(varUserAgentAllowedApps)
additionalProperties := make(map[string]interface{})
if err = json.Unmarshal(data, &additionalProperties); err == nil {
delete(additionalProperties, "allowed_apps")
o.AdditionalProperties = additionalProperties
}
return err
}
type NullableUserAgentAllowedApps struct {
value *UserAgentAllowedApps
isSet bool
}
func (v NullableUserAgentAllowedApps) Get() *UserAgentAllowedApps {
return v.value
}
func (v *NullableUserAgentAllowedApps) Set(val *UserAgentAllowedApps) {
v.value = val
v.isSet = true
}
func (v NullableUserAgentAllowedApps) IsSet() bool {
return v.isSet
}
func (v *NullableUserAgentAllowedApps) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableUserAgentAllowedApps(val *UserAgentAllowedApps) *NullableUserAgentAllowedApps {
return &NullableUserAgentAllowedApps{value: val, isSet: true}
}
func (v NullableUserAgentAllowedApps) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableUserAgentAllowedApps) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View File

@@ -0,0 +1,167 @@
/*
authentik
Making authentication simple.
API version: 2026.5.0-rc1
Contact: hello@goauthentik.io
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package api
import (
"encoding/json"
"fmt"
)
// checks if the UserAgentAllowedAppsRequest type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &UserAgentAllowedAppsRequest{}
// UserAgentAllowedAppsRequest Payload to replace an agent's allowed applications
type UserAgentAllowedAppsRequest struct {
AllowedApps []string `json:"allowed_apps"`
AdditionalProperties map[string]interface{}
}
type _UserAgentAllowedAppsRequest UserAgentAllowedAppsRequest
// NewUserAgentAllowedAppsRequest instantiates a new UserAgentAllowedAppsRequest object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewUserAgentAllowedAppsRequest(allowedApps []string) *UserAgentAllowedAppsRequest {
this := UserAgentAllowedAppsRequest{}
this.AllowedApps = allowedApps
return &this
}
// NewUserAgentAllowedAppsRequestWithDefaults instantiates a new UserAgentAllowedAppsRequest object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewUserAgentAllowedAppsRequestWithDefaults() *UserAgentAllowedAppsRequest {
this := UserAgentAllowedAppsRequest{}
return &this
}
// GetAllowedApps returns the AllowedApps field value
func (o *UserAgentAllowedAppsRequest) GetAllowedApps() []string {
if o == nil {
var ret []string
return ret
}
return o.AllowedApps
}
// GetAllowedAppsOk returns a tuple with the AllowedApps field value
// and a boolean to check if the value has been set.
func (o *UserAgentAllowedAppsRequest) GetAllowedAppsOk() ([]string, bool) {
if o == nil {
return nil, false
}
return o.AllowedApps, true
}
// SetAllowedApps sets field value
func (o *UserAgentAllowedAppsRequest) SetAllowedApps(v []string) {
o.AllowedApps = v
}
func (o UserAgentAllowedAppsRequest) MarshalJSON() ([]byte, error) {
toSerialize, err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o UserAgentAllowedAppsRequest) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["allowed_apps"] = o.AllowedApps
for key, value := range o.AdditionalProperties {
toSerialize[key] = value
}
return toSerialize, nil
}
func (o *UserAgentAllowedAppsRequest) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"allowed_apps",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err
}
for _, requiredProperty := range requiredProperties {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varUserAgentAllowedAppsRequest := _UserAgentAllowedAppsRequest{}
err = json.Unmarshal(data, &varUserAgentAllowedAppsRequest)
if err != nil {
return err
}
*o = UserAgentAllowedAppsRequest(varUserAgentAllowedAppsRequest)
additionalProperties := make(map[string]interface{})
if err = json.Unmarshal(data, &additionalProperties); err == nil {
delete(additionalProperties, "allowed_apps")
o.AdditionalProperties = additionalProperties
}
return err
}
type NullableUserAgentAllowedAppsRequest struct {
value *UserAgentAllowedAppsRequest
isSet bool
}
func (v NullableUserAgentAllowedAppsRequest) Get() *UserAgentAllowedAppsRequest {
return v.value
}
func (v *NullableUserAgentAllowedAppsRequest) Set(val *UserAgentAllowedAppsRequest) {
v.value = val
v.isSet = true
}
func (v NullableUserAgentAllowedAppsRequest) IsSet() bool {
return v.isSet
}
func (v *NullableUserAgentAllowedAppsRequest) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableUserAgentAllowedAppsRequest(val *UserAgentAllowedAppsRequest) *NullableUserAgentAllowedAppsRequest {
return &NullableUserAgentAllowedAppsRequest{value: val, isSet: true}
}
func (v NullableUserAgentAllowedAppsRequest) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableUserAgentAllowedAppsRequest) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View File

@@ -0,0 +1,204 @@
/*
authentik
Making authentication simple.
API version: 2026.5.0-rc1
Contact: hello@goauthentik.io
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package api
import (
"encoding/json"
"fmt"
)
// checks if the UserAgentRequest type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &UserAgentRequest{}
// UserAgentRequest Payload to create an agent user
type UserAgentRequest struct {
Name string `json:"name"`
Owner *int32 `json:"owner,omitempty"`
AdditionalProperties map[string]interface{}
}
type _UserAgentRequest UserAgentRequest
// NewUserAgentRequest instantiates a new UserAgentRequest object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewUserAgentRequest(name string) *UserAgentRequest {
this := UserAgentRequest{}
this.Name = name
return &this
}
// NewUserAgentRequestWithDefaults instantiates a new UserAgentRequest object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewUserAgentRequestWithDefaults() *UserAgentRequest {
this := UserAgentRequest{}
return &this
}
// GetName returns the Name field value
func (o *UserAgentRequest) GetName() string {
if o == nil {
var ret string
return ret
}
return o.Name
}
// GetNameOk returns a tuple with the Name field value
// and a boolean to check if the value has been set.
func (o *UserAgentRequest) GetNameOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Name, true
}
// SetName sets field value
func (o *UserAgentRequest) SetName(v string) {
o.Name = v
}
// GetOwner returns the Owner field value if set, zero value otherwise.
func (o *UserAgentRequest) GetOwner() int32 {
if o == nil || IsNil(o.Owner) {
var ret int32
return ret
}
return *o.Owner
}
// GetOwnerOk returns a tuple with the Owner field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *UserAgentRequest) GetOwnerOk() (*int32, bool) {
if o == nil || IsNil(o.Owner) {
return nil, false
}
return o.Owner, true
}
// HasOwner returns a boolean if a field has been set.
func (o *UserAgentRequest) HasOwner() bool {
if o != nil && !IsNil(o.Owner) {
return true
}
return false
}
// SetOwner gets a reference to the given int32 and assigns it to the Owner field.
func (o *UserAgentRequest) SetOwner(v int32) {
o.Owner = &v
}
func (o UserAgentRequest) MarshalJSON() ([]byte, error) {
toSerialize, err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o UserAgentRequest) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["name"] = o.Name
if !IsNil(o.Owner) {
toSerialize["owner"] = o.Owner
}
for key, value := range o.AdditionalProperties {
toSerialize[key] = value
}
return toSerialize, nil
}
func (o *UserAgentRequest) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"name",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err
}
for _, requiredProperty := range requiredProperties {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varUserAgentRequest := _UserAgentRequest{}
err = json.Unmarshal(data, &varUserAgentRequest)
if err != nil {
return err
}
*o = UserAgentRequest(varUserAgentRequest)
additionalProperties := make(map[string]interface{})
if err = json.Unmarshal(data, &additionalProperties); err == nil {
delete(additionalProperties, "name")
delete(additionalProperties, "owner")
o.AdditionalProperties = additionalProperties
}
return err
}
type NullableUserAgentRequest struct {
value *UserAgentRequest
isSet bool
}
func (v NullableUserAgentRequest) Get() *UserAgentRequest {
return v.value
}
func (v *NullableUserAgentRequest) Set(val *UserAgentRequest) {
v.value = val
v.isSet = true
}
func (v NullableUserAgentRequest) IsSet() bool {
return v.isSet
}
func (v *NullableUserAgentRequest) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableUserAgentRequest(val *UserAgentRequest) *NullableUserAgentRequest {
return &NullableUserAgentRequest{value: val, isSet: true}
}
func (v NullableUserAgentRequest) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableUserAgentRequest) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View File

@@ -0,0 +1,254 @@
/*
authentik
Making authentication simple.
API version: 2026.5.0-rc1
Contact: hello@goauthentik.io
*/
// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
package api
import (
"encoding/json"
"fmt"
)
// checks if the UserAgentResponse type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &UserAgentResponse{}
// UserAgentResponse struct for UserAgentResponse
type UserAgentResponse struct {
Username string `json:"username"`
Token string `json:"token"`
UserUid string `json:"user_uid"`
UserPk int32 `json:"user_pk"`
AdditionalProperties map[string]interface{}
}
type _UserAgentResponse UserAgentResponse
// NewUserAgentResponse instantiates a new UserAgentResponse object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewUserAgentResponse(username string, token string, userUid string, userPk int32) *UserAgentResponse {
this := UserAgentResponse{}
this.Username = username
this.Token = token
this.UserUid = userUid
this.UserPk = userPk
return &this
}
// NewUserAgentResponseWithDefaults instantiates a new UserAgentResponse object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewUserAgentResponseWithDefaults() *UserAgentResponse {
this := UserAgentResponse{}
return &this
}
// GetUsername returns the Username field value
func (o *UserAgentResponse) GetUsername() string {
if o == nil {
var ret string
return ret
}
return o.Username
}
// GetUsernameOk returns a tuple with the Username field value
// and a boolean to check if the value has been set.
func (o *UserAgentResponse) GetUsernameOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Username, true
}
// SetUsername sets field value
func (o *UserAgentResponse) SetUsername(v string) {
o.Username = v
}
// GetToken returns the Token field value
func (o *UserAgentResponse) GetToken() string {
if o == nil {
var ret string
return ret
}
return o.Token
}
// GetTokenOk returns a tuple with the Token field value
// and a boolean to check if the value has been set.
func (o *UserAgentResponse) GetTokenOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.Token, true
}
// SetToken sets field value
func (o *UserAgentResponse) SetToken(v string) {
o.Token = v
}
// GetUserUid returns the UserUid field value
func (o *UserAgentResponse) GetUserUid() string {
if o == nil {
var ret string
return ret
}
return o.UserUid
}
// GetUserUidOk returns a tuple with the UserUid field value
// and a boolean to check if the value has been set.
func (o *UserAgentResponse) GetUserUidOk() (*string, bool) {
if o == nil {
return nil, false
}
return &o.UserUid, true
}
// SetUserUid sets field value
func (o *UserAgentResponse) SetUserUid(v string) {
o.UserUid = v
}
// GetUserPk returns the UserPk field value
func (o *UserAgentResponse) GetUserPk() int32 {
if o == nil {
var ret int32
return ret
}
return o.UserPk
}
// GetUserPkOk returns a tuple with the UserPk field value
// and a boolean to check if the value has been set.
func (o *UserAgentResponse) GetUserPkOk() (*int32, bool) {
if o == nil {
return nil, false
}
return &o.UserPk, true
}
// SetUserPk sets field value
func (o *UserAgentResponse) SetUserPk(v int32) {
o.UserPk = v
}
func (o UserAgentResponse) MarshalJSON() ([]byte, error) {
toSerialize, err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o UserAgentResponse) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
toSerialize["username"] = o.Username
toSerialize["token"] = o.Token
toSerialize["user_uid"] = o.UserUid
toSerialize["user_pk"] = o.UserPk
for key, value := range o.AdditionalProperties {
toSerialize[key] = value
}
return toSerialize, nil
}
func (o *UserAgentResponse) UnmarshalJSON(data []byte) (err error) {
// This validates that all required properties are included in the JSON object
// by unmarshalling the object into a generic map with string keys and checking
// that every required field exists as a key in the generic map.
requiredProperties := []string{
"username",
"token",
"user_uid",
"user_pk",
}
allProperties := make(map[string]interface{})
err = json.Unmarshal(data, &allProperties)
if err != nil {
return err
}
for _, requiredProperty := range requiredProperties {
if _, exists := allProperties[requiredProperty]; !exists {
return fmt.Errorf("no value given for required property %v", requiredProperty)
}
}
varUserAgentResponse := _UserAgentResponse{}
err = json.Unmarshal(data, &varUserAgentResponse)
if err != nil {
return err
}
*o = UserAgentResponse(varUserAgentResponse)
additionalProperties := make(map[string]interface{})
if err = json.Unmarshal(data, &additionalProperties); err == nil {
delete(additionalProperties, "username")
delete(additionalProperties, "token")
delete(additionalProperties, "user_uid")
delete(additionalProperties, "user_pk")
o.AdditionalProperties = additionalProperties
}
return err
}
type NullableUserAgentResponse struct {
value *UserAgentResponse
isSet bool
}
func (v NullableUserAgentResponse) Get() *UserAgentResponse {
return v.value
}
func (v *NullableUserAgentResponse) Set(val *UserAgentResponse) {
v.value = val
v.isSet = true
}
func (v NullableUserAgentResponse) IsSet() bool {
return v.isSet
}
func (v *NullableUserAgentResponse) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableUserAgentResponse(val *UserAgentResponse) *NullableUserAgentResponse {
return &NullableUserAgentResponse{value: val, isSet: true}
}
func (v NullableUserAgentResponse) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableUserAgentResponse) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}

View File

@@ -25,6 +25,7 @@ const (
USERTYPEENUM_EXTERNAL UserTypeEnum = "external"
USERTYPEENUM_SERVICE_ACCOUNT UserTypeEnum = "service_account"
USERTYPEENUM_INTERNAL_SERVICE_ACCOUNT UserTypeEnum = "internal_service_account"
USERTYPEENUM_AGENT UserTypeEnum = "agent"
)
// All allowed values of UserTypeEnum enum
@@ -33,6 +34,7 @@ var AllowedUserTypeEnumEnumValues = []UserTypeEnum{
"external",
"service_account",
"internal_service_account",
"agent",
}
func (v *UserTypeEnum) UnmarshalJSON(src []byte) error {

View File

@@ -12,6 +12,15 @@ use serde::{Deserialize, Serialize, de::Error as _};
use super::{ContentType, Error, configuration};
use crate::{apis::ResponseContent, models};
/// struct for typed errors of method [`core_agent_session_create`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CoreAgentSessionCreateError {
Status400(models::ValidationError),
Status403(models::GenericError),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`core_application_entitlements_create`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
@@ -392,6 +401,15 @@ pub enum CoreTokensRetrieveError {
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`core_tokens_rotate_create`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CoreTokensRotateCreateError {
Status403(),
Status400(models::ValidationError),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`core_tokens_set_key_create`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
@@ -475,6 +493,33 @@ pub enum CoreUserConsentUsedByListError {
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`core_users_agent_allowed_app_partial_update`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CoreUsersAgentAllowedAppPartialUpdateError {
Status400(),
Status403(),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`core_users_agent_allowed_apps_update`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CoreUsersAgentAllowedAppsUpdateError {
Status400(),
Status403(),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`core_users_agent_create`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CoreUsersAgentCreateError {
Status400(models::ValidationError),
Status403(models::GenericError),
UnknownValue(serde_json::Value),
}
/// struct for typed errors of method [`core_users_create`]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
@@ -619,6 +664,37 @@ pub enum CoreUsersUsedByListError {
UnknownValue(serde_json::Value),
}
/// Exchange an agent's API token for an authenticated session.
pub async fn core_agent_session_create(
configuration: &configuration::Configuration,
) -> Result<(), Error<CoreAgentSessionCreateError>> {
let uri_str = format!("{}/core/agent/session/", configuration.base_path);
let mut req_builder = configuration
.client
.request(reqwest::Method::POST, &uri_str);
if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}
let req = req_builder.build()?;
let resp = configuration.client.execute(req).await?;
let status = resp.status();
if !status.is_client_error() && !status.is_server_error() {
Ok(())
} else {
let content = resp.text().await?;
let entity: Option<CoreAgentSessionCreateError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// ApplicationEntitlement Viewset
pub async fn core_application_entitlements_create(
configuration: &configuration::Configuration,
@@ -3481,6 +3557,70 @@ pub async fn core_tokens_retrieve(
}
}
/// 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.
pub async fn core_tokens_rotate_create(
configuration: &configuration::Configuration,
identifier: &str,
) -> Result<models::TokenView, Error<CoreTokensRotateCreateError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_path_identifier = identifier;
let uri_str = format!(
"{}/core/tokens/{identifier}/rotate/",
configuration.base_path,
identifier = crate::apis::urlencode(p_path_identifier)
);
let mut req_builder = configuration
.client
.request(reqwest::Method::POST, &uri_str);
if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}
if let Some(ref token) = configuration.bearer_access_token {
req_builder = req_builder.bearer_auth(token.to_owned());
};
let req = req_builder.build()?;
let resp = configuration.client.execute(req).await?;
let status = resp.status();
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream");
let content_type = super::ContentType::from(content_type);
if !status.is_client_error() && !status.is_server_error() {
let content = resp.text().await?;
match content_type {
ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
ContentType::Text => {
return Err(Error::from(serde_json::Error::custom(
"Received `text/plain` content type response that cannot be converted to \
`models::TokenView`",
)));
}
ContentType::Unsupported(unknown_type) => {
return Err(Error::from(serde_json::Error::custom(format!(
"Received `{unknown_type}` content type response that cannot be converted to \
`models::TokenView`"
))));
}
}
} else {
let content = resp.text().await?;
let entity: Option<CoreTokensRotateCreateError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// Set token key. Action is logged as event. `authentik_core.set_token_key` permission is required.
pub async fn core_tokens_set_key_create(
configuration: &configuration::Configuration,
@@ -4024,6 +4164,200 @@ pub async fn core_user_consent_used_by_list(
}
}
/// Add or remove a single application from an agent's allowed list. Caller must be the agent's
/// owner or a superuser.
pub async fn core_users_agent_allowed_app_partial_update(
configuration: &configuration::Configuration,
id: i32,
patched_user_agent_allowed_app_request: Option<models::PatchedUserAgentAllowedAppRequest>,
) -> Result<models::UserAgentAllowedApps, Error<CoreUsersAgentAllowedAppPartialUpdateError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_path_id = id;
let p_body_patched_user_agent_allowed_app_request = patched_user_agent_allowed_app_request;
let uri_str = format!(
"{}/core/users/{id}/agent_allowed_app/",
configuration.base_path,
id = p_path_id
);
let mut req_builder = configuration
.client
.request(reqwest::Method::PATCH, &uri_str);
if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}
if let Some(ref token) = configuration.bearer_access_token {
req_builder = req_builder.bearer_auth(token.to_owned());
};
req_builder = req_builder.json(&p_body_patched_user_agent_allowed_app_request);
let req = req_builder.build()?;
let resp = configuration.client.execute(req).await?;
let status = resp.status();
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream");
let content_type = super::ContentType::from(content_type);
if !status.is_client_error() && !status.is_server_error() {
let content = resp.text().await?;
match content_type {
ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
ContentType::Text => {
return Err(Error::from(serde_json::Error::custom(
"Received `text/plain` content type response that cannot be converted to \
`models::UserAgentAllowedApps`",
)));
}
ContentType::Unsupported(unknown_type) => {
return Err(Error::from(serde_json::Error::custom(format!(
"Received `{unknown_type}` content type response that cannot be converted to \
`models::UserAgentAllowedApps`"
))));
}
}
} else {
let content = resp.text().await?;
let entity: Option<CoreUsersAgentAllowedAppPartialUpdateError> =
serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// Replace the allowed application list for an agent user. Caller must be the agent's owner or a
/// superuser.
pub async fn core_users_agent_allowed_apps_update(
configuration: &configuration::Configuration,
id: i32,
user_agent_allowed_apps_request: models::UserAgentAllowedAppsRequest,
) -> Result<models::UserAgentAllowedApps, Error<CoreUsersAgentAllowedAppsUpdateError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_path_id = id;
let p_body_user_agent_allowed_apps_request = user_agent_allowed_apps_request;
let uri_str = format!(
"{}/core/users/{id}/agent_allowed_apps/",
configuration.base_path,
id = p_path_id
);
let mut req_builder = configuration.client.request(reqwest::Method::PUT, &uri_str);
if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}
if let Some(ref token) = configuration.bearer_access_token {
req_builder = req_builder.bearer_auth(token.to_owned());
};
req_builder = req_builder.json(&p_body_user_agent_allowed_apps_request);
let req = req_builder.build()?;
let resp = configuration.client.execute(req).await?;
let status = resp.status();
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream");
let content_type = super::ContentType::from(content_type);
if !status.is_client_error() && !status.is_server_error() {
let content = resp.text().await?;
match content_type {
ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
ContentType::Text => {
return Err(Error::from(serde_json::Error::custom(
"Received `text/plain` content type response that cannot be converted to \
`models::UserAgentAllowedApps`",
)));
}
ContentType::Unsupported(unknown_type) => {
return Err(Error::from(serde_json::Error::custom(format!(
"Received `{unknown_type}` content type response that cannot be converted to \
`models::UserAgentAllowedApps`"
))));
}
}
} else {
let content = resp.text().await?;
let entity: Option<CoreUsersAgentAllowedAppsUpdateError> =
serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// Create a new agent user. Enterprise only. Caller must be an internal user.
pub async fn core_users_agent_create(
configuration: &configuration::Configuration,
user_agent_request: models::UserAgentRequest,
) -> Result<models::UserAgentResponse, Error<CoreUsersAgentCreateError>> {
// add a prefix to parameters to efficiently prevent name collisions
let p_body_user_agent_request = user_agent_request;
let uri_str = format!("{}/core/users/agent/", configuration.base_path);
let mut req_builder = configuration
.client
.request(reqwest::Method::POST, &uri_str);
if let Some(ref user_agent) = configuration.user_agent {
req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
}
if let Some(ref token) = configuration.bearer_access_token {
req_builder = req_builder.bearer_auth(token.to_owned());
};
req_builder = req_builder.json(&p_body_user_agent_request);
let req = req_builder.build()?;
let resp = configuration.client.execute(req).await?;
let status = resp.status();
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream");
let content_type = super::ContentType::from(content_type);
if !status.is_client_error() && !status.is_server_error() {
let content = resp.text().await?;
match content_type {
ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
ContentType::Text => {
return Err(Error::from(serde_json::Error::custom(
"Received `text/plain` content type response that cannot be converted to \
`models::UserAgentResponse`",
)));
}
ContentType::Unsupported(unknown_type) => {
return Err(Error::from(serde_json::Error::custom(format!(
"Received `{unknown_type}` content type response that cannot be converted to \
`models::UserAgentResponse`"
))));
}
}
} else {
let content = resp.text().await?;
let entity: Option<CoreUsersAgentCreateError> = serde_json::from_str(&content).ok();
Err(Error::ResponseError(ResponseContent {
status,
content,
entity,
}))
}
}
/// User Viewset
pub async fn core_users_create(
configuration: &configuration::Configuration,

View File

@@ -1252,6 +1252,8 @@ pub mod patched_totp_device_request;
pub use self::patched_totp_device_request::PatchedTotpDeviceRequest;
pub mod patched_unique_password_policy_request;
pub use self::patched_unique_password_policy_request::PatchedUniquePasswordPolicyRequest;
pub mod patched_user_agent_allowed_app_request;
pub use self::patched_user_agent_allowed_app_request::PatchedUserAgentAllowedAppRequest;
pub mod patched_user_delete_stage_request;
pub use self::patched_user_delete_stage_request::PatchedUserDeleteStageRequest;
pub mod patched_user_kerberos_source_connection_request;
@@ -1634,6 +1636,16 @@ pub mod user_account_request;
pub use self::user_account_request::UserAccountRequest;
pub mod user_account_serializer_for_role_request;
pub use self::user_account_serializer_for_role_request::UserAccountSerializerForRoleRequest;
pub mod user_agent_allowed_app_action_enum;
pub use self::user_agent_allowed_app_action_enum::UserAgentAllowedAppActionEnum;
pub mod user_agent_allowed_apps;
pub use self::user_agent_allowed_apps::UserAgentAllowedApps;
pub mod user_agent_allowed_apps_request;
pub use self::user_agent_allowed_apps_request::UserAgentAllowedAppsRequest;
pub mod user_agent_request;
pub use self::user_agent_request::UserAgentRequest;
pub mod user_agent_response;
pub use self::user_agent_response::UserAgentResponse;
pub mod user_attribute_enum;
pub use self::user_attribute_enum::UserAttributeEnum;
pub mod user_consent;

View File

@@ -0,0 +1,30 @@
// authentik
//
// Making authentication simple.
//
// The version of the OpenAPI document: 2026.5.0-rc1
// Contact: hello@goauthentik.io
// Generated by: https://openapi-generator.tech
use serde::{Deserialize, Serialize};
use crate::models;
/// PatchedUserAgentAllowedAppRequest : Payload to add or remove a single allowed application
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct PatchedUserAgentAllowedAppRequest {
#[serde(rename = "app", skip_serializing_if = "Option::is_none")]
pub app: Option<uuid::Uuid>,
#[serde(rename = "action", skip_serializing_if = "Option::is_none")]
pub action: Option<models::UserAgentAllowedAppActionEnum>,
}
impl PatchedUserAgentAllowedAppRequest {
/// Payload to add or remove a single allowed application
pub fn new() -> PatchedUserAgentAllowedAppRequest {
PatchedUserAgentAllowedAppRequest {
app: None,
action: None,
}
}
}

View File

@@ -0,0 +1,35 @@
// authentik
//
// Making authentication simple.
//
// The version of the OpenAPI document: 2026.5.0-rc1
// Contact: hello@goauthentik.io
// Generated by: https://openapi-generator.tech
use serde::{Deserialize, Serialize};
use crate::models;
///
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
pub enum UserAgentAllowedAppActionEnum {
#[serde(rename = "add")]
Add,
#[serde(rename = "remove")]
Remove,
}
impl std::fmt::Display for UserAgentAllowedAppActionEnum {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Add => write!(f, "add"),
Self::Remove => write!(f, "remove"),
}
}
}
impl Default for UserAgentAllowedAppActionEnum {
fn default() -> UserAgentAllowedAppActionEnum {
Self::Add
}
}

View File

@@ -0,0 +1,25 @@
// authentik
//
// Making authentication simple.
//
// The version of the OpenAPI document: 2026.5.0-rc1
// Contact: hello@goauthentik.io
// Generated by: https://openapi-generator.tech
use serde::{Deserialize, Serialize};
use crate::models;
/// UserAgentAllowedApps : Payload to replace an agent's allowed applications
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct UserAgentAllowedApps {
#[serde(rename = "allowed_apps")]
pub allowed_apps: Vec<uuid::Uuid>,
}
impl UserAgentAllowedApps {
/// Payload to replace an agent's allowed applications
pub fn new(allowed_apps: Vec<uuid::Uuid>) -> UserAgentAllowedApps {
UserAgentAllowedApps { allowed_apps }
}
}

View File

@@ -0,0 +1,25 @@
// authentik
//
// Making authentication simple.
//
// The version of the OpenAPI document: 2026.5.0-rc1
// Contact: hello@goauthentik.io
// Generated by: https://openapi-generator.tech
use serde::{Deserialize, Serialize};
use crate::models;
/// UserAgentAllowedAppsRequest : Payload to replace an agent's allowed applications
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct UserAgentAllowedAppsRequest {
#[serde(rename = "allowed_apps")]
pub allowed_apps: Vec<uuid::Uuid>,
}
impl UserAgentAllowedAppsRequest {
/// Payload to replace an agent's allowed applications
pub fn new(allowed_apps: Vec<uuid::Uuid>) -> UserAgentAllowedAppsRequest {
UserAgentAllowedAppsRequest { allowed_apps }
}
}

View File

@@ -0,0 +1,27 @@
// authentik
//
// Making authentication simple.
//
// The version of the OpenAPI document: 2026.5.0-rc1
// Contact: hello@goauthentik.io
// Generated by: https://openapi-generator.tech
use serde::{Deserialize, Serialize};
use crate::models;
/// UserAgentRequest : Payload to create an agent user
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct UserAgentRequest {
#[serde(rename = "name")]
pub name: String,
#[serde(rename = "owner", skip_serializing_if = "Option::is_none")]
pub owner: Option<i32>,
}
impl UserAgentRequest {
/// Payload to create an agent user
pub fn new(name: String) -> UserAgentRequest {
UserAgentRequest { name, owner: None }
}
}

View File

@@ -0,0 +1,39 @@
// authentik
//
// Making authentication simple.
//
// The version of the OpenAPI document: 2026.5.0-rc1
// Contact: hello@goauthentik.io
// Generated by: https://openapi-generator.tech
use serde::{Deserialize, Serialize};
use crate::models;
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct UserAgentResponse {
#[serde(rename = "username")]
pub username: String,
#[serde(rename = "token")]
pub token: String,
#[serde(rename = "user_uid")]
pub user_uid: String,
#[serde(rename = "user_pk")]
pub user_pk: i32,
}
impl UserAgentResponse {
pub fn new(
username: String,
token: String,
user_uid: String,
user_pk: i32,
) -> UserAgentResponse {
UserAgentResponse {
username,
token,
user_uid,
user_pk,
}
}
}

View File

@@ -21,6 +21,8 @@ pub enum UserTypeEnum {
ServiceAccount,
#[serde(rename = "internal_service_account")]
InternalServiceAccount,
#[serde(rename = "agent")]
Agent,
}
impl std::fmt::Display for UserTypeEnum {
@@ -30,6 +32,7 @@ impl std::fmt::Display for UserTypeEnum {
Self::External => write!(f, "external"),
Self::ServiceAccount => write!(f, "service_account"),
Self::InternalServiceAccount => write!(f, "internal_service_account"),
Self::Agent => write!(f, "agent"),
}
}
}

View File

@@ -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.
*/
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.
*/
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
*/

View 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"]),
};
}

View 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;
}

View 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"],
};
}

View 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"],
};
}

View 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"],
};
}

View 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"],
};
}

View File

@@ -21,6 +21,7 @@ export const UserTypeEnum = {
External: "external",
ServiceAccount: "service_account",
InternalServiceAccount: "internal_service_account",
Agent: "agent",
UnknownDefaultOpenApi: "11184809",
} as const;
export type UserTypeEnum = (typeof UserTypeEnum)[keyof typeof UserTypeEnum];

View File

@@ -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";

View File

@@ -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,33 @@ 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
'400':
$ref: '#/components/responses/ValidationErrorResponse'
/core/tokens/{identifier}/set_key/:
post:
operationId: core_tokens_set_key_create
@@ -4406,6 +4448,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 +4661,32 @@ 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.
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
@@ -50622,6 +50759,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
@@ -56801,6 +56947,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
@@ -57706,6 +57907,7 @@ components:
- external
- service_account
- internal_service_account
- agent
type: string
UserVerificationEnum:
enum:

View 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;
}
}

View 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;
}
}

View File

@@ -1,15 +1,17 @@
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";
import { applicationListStyle } from "#admin/applications/ApplicationListPage";
import { Application, CoreApi, User } from "@goauthentik/api";
import { Application, CoreApi, User, UserTypeEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { CSSResult, html, nothing } from "lit";
@@ -22,6 +24,10 @@ export class UserApplicationTable extends Table<Application> {
static styles: CSSResult[] = [...super.styles, applicationListStyle];
private get isAgent(): boolean {
return this.user?.type === UserTypeEnum.Agent;
}
async apiEndpoint(): Promise<PaginatedResponse<Application>> {
return new CoreApi(DEFAULT_CONFIG).coreApplicationsList({
...(await this.defaultEndpointConfig()),
@@ -38,6 +44,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 +106,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>`,
];
}

View File

@@ -102,6 +102,10 @@ export class UserForm extends ModelForm<User, number> {
verboseName: msg("Service Account"),
verboseNamePlural: msg("Service Accounts"),
}))
.with(UserTypeEnum.Agent, () => ({
verboseName: msg("Agent User"),
verboseNamePlural: msg("Agent Users"),
}))
.otherwise(() => ({
verboseName: msg("User"),
verboseNamePlural: msg("Users"),
@@ -203,27 +207,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.instance?.type === UserTypeEnum.Agent
? html`<ak-radio-input
label=${msg("User type")}
required
name="type"
.value=${UserTypeEnum.Agent}
.options=${[
{
label: msg("Agent"),
value: UserTypeEnum.Agent,
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")}

View File

@@ -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,9 @@ 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_FORM_SLOT = `type-ak-user-agent-form-${UserTypeEnum.Agent}` 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 +50,13 @@ const DEFAULT_USER_TYPES: TypeCreate[] = [
"External consultants or B2C customers without access to enterprise features.",
),
},
{
component: "ak-user-agent-form",
modelName: UserTypeEnum.Agent,
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 +66,7 @@ const DEFAULT_USER_TYPES: TypeCreate[] = [
];
export interface UserWizardState {
[AGENT_FORM_SLOT]?: UserAgentResponse;
[SERVICE_ACCOUNT_FORM_SLOT]?: UserServiceAccountResponse;
}
@@ -110,6 +127,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 +202,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 === UserTypeEnum.Agent) {
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 === UserTypeEnum.Agent) {
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 +237,10 @@ export class AKUserWizard extends CreateWizard {
}
protected override assembleFormProps(type: TypeCreate): LitPropertyRecord<UserForm | object> {
if (type.modelName === UserTypeEnum.ServiceAccount) {
if (
type.modelName === UserTypeEnum.Agent ||
type.modelName === UserTypeEnum.ServiceAccount
) {
return {};
}
@@ -171,6 +256,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;
}
}

View File

@@ -118,6 +118,7 @@ const _userTypeToLabel = new Map<UserTypeEnum | undefined, string>([
[UserTypeEnum.External, msg("External")],
[UserTypeEnum.ServiceAccount, msg("Service account")],
[UserTypeEnum.InternalServiceAccount, msg("Service account (internal)")],
[UserTypeEnum.Agent, msg("Agent")],
]);
export const userTypeToLabel = (type?: UserTypeEnum): string =>