diff --git a/authentik/core/api/tokens.py b/authentik/core/api/tokens.py index 8529c677e187..0b10f861ceac 100644 --- a/authentik/core/api/tokens.py +++ b/authentik/core/api/tokens.py @@ -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.users import UserSerializer from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.core.models import ( + USER_ATTRIBUTE_AGENT_OWNER_PK, + USER_ATTRIBUTE_IS_AGENT, USER_ATTRIBUTE_TOKEN_EXPIRING, USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME, Token, TokenIntents, User, default_token_duration, + default_token_key, ) from authentik.events.models import Event, EventAction from authentik.events.utils import model_to_dict @@ -171,6 +175,33 @@ def view_key(self, request: Request, identifier: str) -> Response: 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 = self.get_object() + + if not request.user.is_superuser: + is_token_owner = token.user_id == request.user.pk + is_agent_owner = token.user.attributes.get(USER_ATTRIBUTE_IS_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(), diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index eaa23b99ac3e..cd4fddab6acc 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -75,9 +75,14 @@ SESSION_KEY_IMPERSONATE_USER, ) from authentik.core.models import ( + USER_ATTRIBUTE_AGENT_ALLOWED_APPS, + USER_ATTRIBUTE_AGENT_OWNER_PK, + USER_ATTRIBUTE_IS_AGENT, USER_ATTRIBUTE_TOKEN_EXPIRING, + USER_PATH_AGENT, USER_PATH_SERVICE_ACCOUNT, USERNAME_MAX_LENGTH, + Application, Group, Session, Token, @@ -86,6 +91,7 @@ UserTypes, default_token_duration, ) +from authentik.core.apps import AppAccessWithoutBindings from authentik.endpoints.connectors.agent.auth import AgentAuth from authentik.events.models import Event, EventAction from authentik.flows.exceptions import FlowNonApplicableException @@ -95,6 +101,7 @@ from authentik.lib.avatars import get_avatar from authentik.lib.utils.reflection import ConditionalInheritance from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator +from authentik.policies.engine import PolicyEngine from authentik.rbac.api.roles import RoleSerializer from authentik.rbac.decorators import permission_required from authentik.rbac.models import Role, get_permission_choices @@ -249,8 +256,27 @@ def validate_type(self, user_type: str) -> str: raise ValidationError(_("Can't change internal service account to other user type.")) if not self.instance and user_type == UserTypes.INTERNAL_SERVICE_ACCOUNT.value: raise ValidationError(_("Setting a user to internal service account is not allowed.")) + if ( + self.instance + and self.instance.attributes.get(USER_ATTRIBUTE_IS_AGENT) + and user_type != UserTypes.SERVICE_ACCOUNT.value + ): + raise ValidationError(_("Can't change agent user to other user type.")) return user_type + def validate_attributes(self, attrs: dict) -> dict: + """Prevent removal of agent marker or change of agent owner""" + if not self.instance: + return attrs + if self.instance.attributes.get(USER_ATTRIBUTE_IS_AGENT): + if not attrs.get(USER_ATTRIBUTE_IS_AGENT): + raise ValidationError(_("Can't remove agent marker from agent user.")) + 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 +431,24 @@ class UserServiceAccountSerializer(PassiveSerializer): ) +class UserAgentSerializer(PassiveSerializer): + """Payload to create an agent user""" + + name = CharField( + required=True, + validators=[UniqueValidator(queryset=User.objects.all().order_by("username"))], + ) + + +class UserAgentAllowedAppsSerializer(PassiveSerializer): + """Payload to replace the allowed application list for an agent user""" + + allowed_apps = ListField( + child=UUIDField(), + help_text="List of application UUIDs the agent is permitted to access.", + ) + + class UserRecoveryLinkSerializer(PassiveSerializer): """Payload to create a recovery link""" @@ -691,6 +735,175 @@ def service_account(self, request: Request, body: UserServiceAccountSerializer) status=500, ) + @permission_required(None, ["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.")) + + username = body.validated_data["name"] + with atomic(): + try: + user: User = User.objects.create( + username=username, + name=username, + type=UserTypes.SERVICE_ACCOUNT, + attributes={ + USER_ATTRIBUTE_IS_AGENT: True, + USER_ATTRIBUTE_AGENT_OWNER_PK: str(request.user.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, + ) + # Allow the agent to read its own rotated token key + user.assign_perms_to_managed_role("authentik_core.view_token_key", token) + + 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. Each supplied application UUID + is validated against the owner's current access.""" + agent: User = self.get_object() + + if not agent.attributes.get(USER_ATTRIBUTE_IS_AGENT): + return Response( + data={"non_field_errors": [_("User is not an agent user.")]}, + status=400, + ) + + 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: + return Response(status=403) + + try: + owner = User.objects.get(pk=owner_pk) + except User.DoesNotExist: + return Response( + data={"non_field_errors": [_("Agent owner not found.")]}, + status=400, + ) + + 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(responses={200: SessionUserSerializer(many=False)}) @action( url_path="me", diff --git a/authentik/core/migrations/0058_user_add_agent_user_permission.py b/authentik/core/migrations/0058_user_add_agent_user_permission.py new file mode 100644 index 000000000000..e3b4cb19242d --- /dev/null +++ b/authentik/core/migrations/0058_user_add_agent_user_permission.py @@ -0,0 +1,27 @@ +"""Add add_agent_user permission to User model""" + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_core", "0057_remove_user_groups_remove_user_user_permissions_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="user", + options={ + "permissions": [ + ("reset_user_password", "Reset Password"), + ("impersonate", "Can impersonate other users"), + ("preview_user", "Can preview user data sent to providers"), + ("view_user_applications", "View applications the user has access to"), + ("add_agent_user", "Can create agent users"), + ], + "verbose_name": "User", + "verbose_name_plural": "Users", + }, + ), + ] diff --git a/authentik/core/models.py b/authentik/core/models.py index 77c759fbba0a..08e2e51a5bd8 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -66,7 +66,12 @@ 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_ATTR_AGENT_PREFIX = f"{USER_PATH_SYSTEM_PREFIX}/agent" +USER_ATTRIBUTE_IS_AGENT = f"{_USER_ATTR_AGENT_PREFIX}/is-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_SERVICE_ACCOUNT = f"{USER_PATH_SYSTEM_PREFIX}/service-accounts" +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 @@ -385,6 +390,7 @@ class Meta: ("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"]), diff --git a/authentik/core/signals.py b/authentik/core/signals.py index 05cfaf2eb347..5b4c6b1ae199 100644 --- a/authentik/core/signals.py +++ b/authentik/core/signals.py @@ -11,12 +11,15 @@ from structlog.stdlib import get_logger from authentik.core.models import ( + USER_ATTRIBUTE_AGENT_OWNER_PK, + USER_ATTRIBUTE_IS_AGENT, Application, AuthenticatedSession, BackchannelProvider, ExpiringModel, Session, User, + UserTypes, default_token_duration, ) from authentik.flows.apps import RefreshOtherFlowsAfterAuthentication @@ -69,6 +72,38 @@ 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.SERVICE_ACCOUNT, + attributes__contains={ + USER_ATTRIBUTE_IS_AGENT: True, + 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""" diff --git a/authentik/core/tests/test_token_api.py b/authentik/core/tests/test_token_api.py index 5354ecae84d4..75a579cf8a97 100644 --- a/authentik/core/tests/test_token_api.py +++ b/authentik/core/tests/test_token_api.py @@ -199,6 +199,50 @@ def test_list_with_permission(self): 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( diff --git a/authentik/core/tests/test_users.py b/authentik/core/tests/test_users.py index 8900058e4871..e2a7d9fecfa0 100644 --- a/authentik/core/tests/test_users.py +++ b/authentik/core/tests/test_users.py @@ -2,7 +2,15 @@ from django.test.testcases import TestCase -from authentik.core.models import User +from authentik.core.models import ( + USER_ATTRIBUTE_AGENT_OWNER_PK, + USER_ATTRIBUTE_IS_AGENT, + USER_PATH_AGENT, + AuthenticatedSession, + Session, + User, + UserTypes, +) from authentik.events.models import Event from authentik.lib.generators import generate_id @@ -33,3 +41,92 @@ def test_user_ak_groups_event(self): 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.SERVICE_ACCOUNT, + attributes={ + USER_ATTRIBUTE_IS_AGENT: True, + 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(), session_data="{}") + 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()) diff --git a/authentik/core/tests/test_users_api.py b/authentik/core/tests/test_users_api.py index ef7069827f15..2dbbaf790f45 100644 --- a/authentik/core/tests/test_users_api.py +++ b/authentik/core/tests/test_users_api.py @@ -3,16 +3,24 @@ 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 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_IS_AGENT, USER_ATTRIBUTE_TOKEN_EXPIRING, + USER_PATH_AGENT, + Application, AuthenticatedSession, Session, Token, + TokenIntents, User, UserTypes, ) @@ -878,3 +886,189 @@ def test_sort_by_last_login(self): 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.admin.assign_perms_to_managed_role("authentik_core.add_agent_user") + self.user = create_test_user() + + def _create_agent(self, name="test-agent", owner=None): + """Helper to create an agent user directly in the database""" + owner = owner or self.admin + agent = User.objects.create( + username=name, + name=name, + type=UserTypes.SERVICE_ACCOUNT, + attributes={ + USER_ATTRIBUTE_IS_AGENT: True, + 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): + """Agent user creation""" + self.client.force_login(self.admin) + 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.SERVICE_ACCOUNT) + self.assertEqual(agent.path, USER_PATH_AGENT) + self.assertTrue(agent.attributes.get(USER_ATTRIBUTE_IS_AGENT)) + self.assertEqual(agent.attributes.get(USER_ATTRIBUTE_AGENT_OWNER_PK), str(self.admin.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.admin) + 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""" + external = create_test_user() + external.type = UserTypes.EXTERNAL + external.save() + external.assign_perms_to_managed_role("authentik_core.add_agent_user") + self.client.force_login(external) + 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_duplicate(self): + """Duplicate agent username returns a user-friendly error""" + self._create_agent("test-agent-dup") + self.client.force_login(self.admin) + 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_marker_cannot_be_removed(self): + """Agent is-agent attribute cannot be removed via the users API""" + agent = self._create_agent() + self.client.force_login(self.admin) + new_attrs = dict(agent.attributes) + del new_attrs[USER_ATTRIBUTE_IS_AGENT] + 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_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 cannot update the agent's allowed apps list""" + other = create_test_user() + agent = self._create_agent(owner=other) + self.client.force_login(self.admin) + 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_token_rotate_by_agent_owner(self): + """Agent owner can rotate the agent's token""" + agent = self._create_agent(owner=self.user) + token = Token.objects.create( + identifier=generate_id(), + intent=TokenIntents.INTENT_API, + user=agent, + expiring=True, + ) + original_key = token.key + self.client.force_login(self.user) + 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) diff --git a/authentik/enterprise/tasks.py b/authentik/enterprise/tasks.py index 7c5a3bbea0a2..84bbdd6cf601 100644 --- a/authentik/enterprise/tasks.py +++ b/authentik/enterprise/tasks.py @@ -6,6 +6,27 @@ from authentik.enterprise.license import LicenseKey +def _deactivate_agent_users(): + """Mark all active agent users inactive and remove their sessions when the enterprise + license is not valid. Called after each license usage recording.""" + from authentik.core.models import ( + USER_ATTRIBUTE_IS_AGENT, + Session, + User, + UserTypes, + ) + + agents = User.objects.filter( + type=UserTypes.SERVICE_ACCOUNT, + attributes__contains={USER_ATTRIBUTE_IS_AGENT: True}, + 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() diff --git a/authentik/enterprise/tests/test_license.py b/authentik/enterprise/tests/test_license.py index 6ab2ced0c78d..a54f6ba7878c 100644 --- a/authentik/enterprise/tests/test_license.py +++ b/authentik/enterprise/tests/test_license.py @@ -8,7 +8,6 @@ from django.utils.timezone import now from rest_framework.exceptions import ValidationError -from authentik.core.models import User from authentik.enterprise.license import LicenseKey from authentik.enterprise.models import ( THRESHOLD_READ_ONLY_WEEKS, diff --git a/authentik/enterprise/tests/test_tasks.py b/authentik/enterprise/tests/test_tasks.py new file mode 100644 index 000000000000..588e33dcc044 --- /dev/null +++ b/authentik/enterprise/tests/test_tasks.py @@ -0,0 +1,68 @@ +"""Enterprise task tests""" + +from django.test import TestCase + +from authentik.core.models import ( + USER_ATTRIBUTE_AGENT_OWNER_PK, + USER_ATTRIBUTE_IS_AGENT, + 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.SERVICE_ACCOUNT, + attributes={ + USER_ATTRIBUTE_IS_AGENT: True, + 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) diff --git a/authentik/events/utils.py b/authentik/events/utils.py index 526e7cd9b68c..1e3cbc83a44e 100644 --- a/authentik/events/utils.py +++ b/authentik/events/utils.py @@ -31,7 +31,7 @@ # 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, ) diff --git a/authentik/policies/expression/evaluator.py b/authentik/policies/expression/evaluator.py index 1561ed2aa42c..bf84d1b943e1 100644 --- a/authentik/policies/expression/evaluator.py +++ b/authentik/policies/expression/evaluator.py @@ -50,6 +50,13 @@ def set_policy_request(self, request: PolicyRequest): 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): + # update website/docs/expressions/_functions.md + 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 +65,51 @@ def set_http_request(self, request: HttpRequest): 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: + """Check if the current agent user's owner has access to the current application. + Returns False when called outside an application-access policy context, or when + the current user is not an agent user.""" + from authentik.core.apps import AppAccessWithoutBindings + from authentik.core.models import ( + USER_ATTRIBUTE_AGENT_ALLOWED_APPS, + USER_ATTRIBUTE_AGENT_OWNER_PK, + USER_ATTRIBUTE_IS_AGENT, + User, + ) + from authentik.policies.engine import PolicyEngine + + user = request.user + app = request.obj + + if not getattr(user, "attributes", None): + return False + if not user.attributes.get(USER_ATTRIBUTE_IS_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) diff --git a/authentik/policies/expression/tests.py b/authentik/policies/expression/tests.py index ed8fac2c6535..cc38029f7084 100644 --- a/authentik/policies/expression/tests.py +++ b/authentik/policies/expression/tests.py @@ -5,7 +5,15 @@ 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_ATTRIBUTE_IS_AGENT, + 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 +143,87 @@ def test_call_policy_test_like(self): 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): + agent = User.objects.create( + username=generate_id(), + type=UserTypes.SERVICE_ACCOUNT, + attributes={ + USER_ATTRIBUTE_IS_AGENT: True, + 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, + ) + return 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.SERVICE_ACCOUNT, + attributes={ + USER_ATTRIBUTE_IS_AGENT: True, + 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""" diff --git a/blueprints/schema.json b/blueprints/schema.json index 663af89beefe..d662bea79f6c 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -5545,6 +5545,7 @@ "authentik_brands.change_brand", "authentik_brands.delete_brand", "authentik_brands.view_brand", + "authentik_core.add_agent_user", "authentik_core.add_application", "authentik_core.add_applicationentitlement", "authentik_core.add_authenticatedsession", @@ -6257,6 +6258,7 @@ "permission": { "type": "string", "enum": [ + "add_agent_user", "add_user", "change_user", "delete_user", @@ -11212,6 +11214,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", diff --git a/packages/client-go/api_core.go b/packages/client-go/api_core.go index bd05db13cef4..d9c8b7653ea7 100644 --- a/packages/client-go/api_core.go +++ b/packages/client-go/api_core.go @@ -6066,6 +6066,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 +7355,255 @@ func (a *CoreAPIService) CoreUserConsentUsedByListExecute(r ApiCoreUserConsentUs 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. Each supplied application UUID +is validated against the owner's current access. + + @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 diff --git a/packages/client-go/model_user_agent_allowed_apps.go b/packages/client-go/model_user_agent_allowed_apps.go new file mode 100644 index 000000000000..ed2ea552618a --- /dev/null +++ b/packages/client-go/model_user_agent_allowed_apps.go @@ -0,0 +1,168 @@ +/* +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 the allowed application list for an agent user +type UserAgentAllowedApps struct { + // List of application UUIDs the agent is permitted to access. + 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) +} diff --git a/packages/client-go/model_user_agent_allowed_apps_request.go b/packages/client-go/model_user_agent_allowed_apps_request.go new file mode 100644 index 000000000000..7952cb83dd13 --- /dev/null +++ b/packages/client-go/model_user_agent_allowed_apps_request.go @@ -0,0 +1,168 @@ +/* +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 the allowed application list for an agent user +type UserAgentAllowedAppsRequest struct { + // List of application UUIDs the agent is permitted to access. + 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) +} diff --git a/packages/client-go/model_user_agent_request.go b/packages/client-go/model_user_agent_request.go new file mode 100644 index 000000000000..b1775071ebc7 --- /dev/null +++ b/packages/client-go/model_user_agent_request.go @@ -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 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"` + 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 +} + +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 + + 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") + 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) +} diff --git a/packages/client-go/model_user_agent_response.go b/packages/client-go/model_user_agent_response.go new file mode 100644 index 000000000000..6d496ae63d6c --- /dev/null +++ b/packages/client-go/model_user_agent_response.go @@ -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) +} diff --git a/packages/client-rust/src/apis/core_api.rs b/packages/client-rust/src/apis/core_api.rs index 15884aafb3cb..7ea63a42f4c0 100644 --- a/packages/client-rust/src/apis/core_api.rs +++ b/packages/client-rust/src/apis/core_api.rs @@ -392,6 +392,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 +484,24 @@ pub enum CoreUserConsentUsedByListError { 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)] @@ -3481,6 +3508,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> { + // 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 = 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 +4115,132 @@ pub async fn core_user_consent_used_by_list( } } +/// Replace the allowed application list for an agent user. Caller must be the agent's owner or a +/// superuser. Each supplied application UUID is validated against the owner's current access. +pub async fn core_users_agent_allowed_apps_update( + configuration: &configuration::Configuration, + id: i32, + user_agent_allowed_apps_request: models::UserAgentAllowedAppsRequest, +) -> Result> { + // 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 = + 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> { + // 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 = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { + status, + content, + entity, + })) + } +} + /// User Viewset pub async fn core_users_create( configuration: &configuration::Configuration, diff --git a/packages/client-rust/src/models/mod.rs b/packages/client-rust/src/models/mod.rs index f91673481755..0955672b1cbb 100644 --- a/packages/client-rust/src/models/mod.rs +++ b/packages/client-rust/src/models/mod.rs @@ -1634,6 +1634,14 @@ 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_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; diff --git a/packages/client-rust/src/models/user_agent_allowed_apps.rs b/packages/client-rust/src/models/user_agent_allowed_apps.rs new file mode 100644 index 000000000000..7c9e11196194 --- /dev/null +++ b/packages/client-rust/src/models/user_agent_allowed_apps.rs @@ -0,0 +1,26 @@ +// 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 the allowed application list for an agent user +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct UserAgentAllowedApps { + /// List of application UUIDs the agent is permitted to access. + #[serde(rename = "allowed_apps")] + pub allowed_apps: Vec, +} + +impl UserAgentAllowedApps { + /// Payload to replace the allowed application list for an agent user + pub fn new(allowed_apps: Vec) -> UserAgentAllowedApps { + UserAgentAllowedApps { allowed_apps } + } +} diff --git a/packages/client-rust/src/models/user_agent_allowed_apps_request.rs b/packages/client-rust/src/models/user_agent_allowed_apps_request.rs new file mode 100644 index 000000000000..54a595eacdc4 --- /dev/null +++ b/packages/client-rust/src/models/user_agent_allowed_apps_request.rs @@ -0,0 +1,26 @@ +// 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 the allowed application list for an agent user +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct UserAgentAllowedAppsRequest { + /// List of application UUIDs the agent is permitted to access. + #[serde(rename = "allowed_apps")] + pub allowed_apps: Vec, +} + +impl UserAgentAllowedAppsRequest { + /// Payload to replace the allowed application list for an agent user + pub fn new(allowed_apps: Vec) -> UserAgentAllowedAppsRequest { + UserAgentAllowedAppsRequest { allowed_apps } + } +} diff --git a/packages/client-rust/src/models/user_agent_request.rs b/packages/client-rust/src/models/user_agent_request.rs new file mode 100644 index 000000000000..0fecca8f7799 --- /dev/null +++ b/packages/client-rust/src/models/user_agent_request.rs @@ -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; + +/// UserAgentRequest : Payload to create an agent user +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct UserAgentRequest { + #[serde(rename = "name")] + pub name: String, +} + +impl UserAgentRequest { + /// Payload to create an agent user + pub fn new(name: String) -> UserAgentRequest { + UserAgentRequest { name } + } +} diff --git a/packages/client-rust/src/models/user_agent_response.rs b/packages/client-rust/src/models/user_agent_response.rs new file mode 100644 index 000000000000..252e5901c2d4 --- /dev/null +++ b/packages/client-rust/src/models/user_agent_response.rs @@ -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, + } + } +} diff --git a/packages/client-ts/src/apis/CoreApi.ts b/packages/client-ts/src/apis/CoreApi.ts index 4c05001ec7ec..1a9b2ebda5d2 100644 --- a/packages/client-ts/src/apis/CoreApi.ts +++ b/packages/client-ts/src/apis/CoreApi.ts @@ -53,6 +53,10 @@ import type { UsedBy, User, UserAccountRequest, + UserAgentAllowedApps, + UserAgentAllowedAppsRequest, + UserAgentRequest, + UserAgentResponse, UserConsent, UserPasswordSetRequest, UserPath, @@ -102,6 +106,10 @@ import { TransactionApplicationResponseFromJSON, UsedByFromJSON, UserAccountRequestToJSON, + UserAgentAllowedAppsFromJSON, + UserAgentAllowedAppsRequestToJSON, + UserAgentRequestToJSON, + UserAgentResponseFromJSON, UserConsentFromJSON, UserFromJSON, UserPasswordSetRequestToJSON, @@ -358,6 +366,10 @@ export interface CoreTokensRetrieveRequest { identifier: string; } +export interface CoreTokensRotateCreateRequest { + identifier: string; +} + export interface CoreTokensSetKeyCreateRequest { identifier: string; tokenSetKeyRequest: TokenSetKeyRequest; @@ -401,6 +413,15 @@ export interface CoreUserConsentUsedByListRequest { id: number; } +export interface CoreUsersAgentAllowedAppsUpdateRequest { + id: number; + userAgentAllowedAppsRequest: UserAgentAllowedAppsRequest; +} + +export interface CoreUsersAgentCreateRequest { + userAgentRequest: UserAgentRequest; +} + export interface CoreUsersCreateRequest { userRequest: UserRequest; } @@ -3575,6 +3596,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 { + 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> { + 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 { + const response = await this.coreTokensRotateCreateRaw(requestParameters, initOverrides); + return await response.value(); + } + /** * Creates request options for coreTokensSetKeyCreate without sending the request */ @@ -4182,6 +4267,150 @@ export class CoreApi extends runtime.BaseAPI { return await response.value(); } + /** + * Creates request options for coreUsersAgentAllowedAppsUpdate without sending the request + */ + async coreUsersAgentAllowedAppsUpdateRequestOpts( + requestParameters: CoreUsersAgentAllowedAppsUpdateRequest, + ): Promise { + 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. Each supplied application UUID is validated against the owner\'s current access. + */ + async coreUsersAgentAllowedAppsUpdateRaw( + requestParameters: CoreUsersAgentAllowedAppsUpdateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + 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. Each supplied application UUID is validated against the owner\'s current access. + */ + async coreUsersAgentAllowedAppsUpdate( + requestParameters: CoreUsersAgentAllowedAppsUpdateRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + 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 { + 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> { + 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 { + const response = await this.coreUsersAgentCreateRaw(requestParameters, initOverrides); + return await response.value(); + } + /** * Creates request options for coreUsersCreate without sending the request */ diff --git a/packages/client-ts/src/models/UserAgentAllowedApps.ts b/packages/client-ts/src/models/UserAgentAllowedApps.ts new file mode 100644 index 000000000000..3bf8d7a29caa --- /dev/null +++ b/packages/client-ts/src/models/UserAgentAllowedApps.ts @@ -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 the allowed application list for an agent user + * @export + * @interface UserAgentAllowedApps + */ +export interface UserAgentAllowedApps { + /** + * List of application UUIDs the agent is permitted to access. + * @type {Array} + * @memberof UserAgentAllowedApps + */ + allowedApps: Array; +} + +/** + * 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"], + }; +} diff --git a/packages/client-ts/src/models/UserAgentAllowedAppsRequest.ts b/packages/client-ts/src/models/UserAgentAllowedAppsRequest.ts new file mode 100644 index 000000000000..55c635a8799a --- /dev/null +++ b/packages/client-ts/src/models/UserAgentAllowedAppsRequest.ts @@ -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 the allowed application list for an agent user + * @export + * @interface UserAgentAllowedAppsRequest + */ +export interface UserAgentAllowedAppsRequest { + /** + * List of application UUIDs the agent is permitted to access. + * @type {Array} + * @memberof UserAgentAllowedAppsRequest + */ + allowedApps: Array; +} + +/** + * 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"], + }; +} diff --git a/packages/client-ts/src/models/UserAgentRequest.ts b/packages/client-ts/src/models/UserAgentRequest.ts new file mode 100644 index 000000000000..b32c12515828 --- /dev/null +++ b/packages/client-ts/src/models/UserAgentRequest.ts @@ -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 create an agent user + * @export + * @interface UserAgentRequest + */ +export interface UserAgentRequest { + /** + * + * @type {string} + * @memberof UserAgentRequest + */ + name: string; +} + +/** + * 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"], + }; +} + +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"], + }; +} diff --git a/packages/client-ts/src/models/UserAgentResponse.ts b/packages/client-ts/src/models/UserAgentResponse.ts new file mode 100644 index 000000000000..f853256a7cfc --- /dev/null +++ b/packages/client-ts/src/models/UserAgentResponse.ts @@ -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"], + }; +} diff --git a/packages/client-ts/src/models/index.ts b/packages/client-ts/src/models/index.ts index e24455ffaf20..655914a4ebb6 100644 --- a/packages/client-ts/src/models/index.ts +++ b/packages/client-ts/src/models/index.ts @@ -818,6 +818,10 @@ export * from "./UsedByActionEnum"; export * from "./User"; export * from "./UserAccountRequest"; export * from "./UserAccountSerializerForRoleRequest"; +export * from "./UserAgentAllowedApps"; +export * from "./UserAgentAllowedAppsRequest"; +export * from "./UserAgentRequest"; +export * from "./UserAgentResponse"; export * from "./UserAttributeEnum"; export * from "./UserConsent"; export * from "./UserCreationModeEnum"; diff --git a/schema.yml b/schema.yml index 49ef628cf12d..8df230d2d5e8 100644 --- a/schema.yml +++ b/schema.yml @@ -3896,6 +3896,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 +4433,41 @@ paths: $ref: '#/components/responses/ValidationErrorResponse' '403': $ref: '#/components/responses/GenericErrorResponse' + /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. Each supplied application UUID + is validated against the owner's current access. + 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 +4612,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 @@ -56801,6 +56889,55 @@ components: type: integer required: - pk + UserAgentAllowedApps: + type: object + description: Payload to replace the allowed application list for an agent user + properties: + allowed_apps: + type: array + items: + type: string + format: uuid + description: List of application UUIDs the agent is permitted to access. + required: + - allowed_apps + UserAgentAllowedAppsRequest: + type: object + description: Payload to replace the allowed application list for an agent user + properties: + allowed_apps: + type: array + items: + type: string + format: uuid + description: List of application UUIDs the agent is permitted to access. + required: + - allowed_apps + UserAgentRequest: + type: object + description: Payload to create an agent user + properties: + name: + type: string + minLength: 1 + 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 diff --git a/web/src/admin/users/AgentForm.ts b/web/src/admin/users/AgentForm.ts new file mode 100644 index 000000000000..411e222d9f53 --- /dev/null +++ b/web/src/admin/users/AgentForm.ts @@ -0,0 +1,114 @@ +import "#components/ak-hidden-text-input"; +import "#components/ak-text-input"; +import "#elements/forms/HorizontalFormElement"; + +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 { + public static override verboseName = msg("Agent User"); + public static override verboseNamePlural = msg("Agent Users"); + 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 { + 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; + } + } + + //#region Rendering + + protected override renderForm(): TemplateResult { + return html``; + } + + protected renderResponseForm(): SlottedTemplateResult { + return html`

+ ${msg( + "Use the token below to authenticate the agent. The token expires after 24 hours " + + "and must be rotated before expiry via the token rotate endpoint.", + )} +

+
+ + + + +
`; + } + + protected override renderFormWrapper(): SlottedTemplateResult { + if (this.result) { + return this.renderResponseForm(); + } + return super.renderFormWrapper(); + } + + //#endregion +} + +declare global { + interface HTMLElementTagNameMap { + "ak-user-agent-form": AgentForm; + } +} diff --git a/web/src/admin/users/UserForm.ts b/web/src/admin/users/UserForm.ts index 5b696c9dfaea..48414f1e2d08 100644 --- a/web/src/admin/users/UserForm.ts +++ b/web/src/admin/users/UserForm.ts @@ -22,6 +22,8 @@ import { css, CSSResult, html } from "lit"; import { customElement, property } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; +const USER_ATTRIBUTE_IS_AGENT = "goauthentik.io/agent/is-agent"; + const UserTypeOptions: readonly RadioOption[] = [ { label: msg("Internal"), @@ -64,6 +66,10 @@ export class UserForm extends ModelForm { @property({ attribute: false }) public userType: UserTypeEnum | null = null; + private get isAgent(): boolean { + return !!this.instance?.attributes?.[USER_ATTRIBUTE_IS_AGENT]; + } + static get defaultUserAttributes(): { [key: string]: unknown } { return {}; } @@ -89,23 +95,25 @@ export class UserForm extends ModelForm { protected override assignInstance(instance: User): void { super.assignInstance(instance); - const { verboseName, verboseNamePlural } = match(instance.type) - .with(UserTypeEnum.Internal, () => ({ - verboseName: msg("Internal User"), - verboseNamePlural: msg("Internal Users"), - })) - .with(UserTypeEnum.External, () => ({ - verboseName: msg("External User"), - verboseNamePlural: msg("External Users"), - })) - .with(UserTypeEnum.ServiceAccount, () => ({ - verboseName: msg("Service Account"), - verboseNamePlural: msg("Service Accounts"), - })) - .otherwise(() => ({ - verboseName: msg("User"), - verboseNamePlural: msg("Users"), - })); + const { verboseName, verboseNamePlural } = this.isAgent + ? { verboseName: msg("Agent User"), verboseNamePlural: msg("Agent Users") } + : match(instance.type) + .with(UserTypeEnum.Internal, () => ({ + verboseName: msg("Internal User"), + verboseNamePlural: msg("Internal Users"), + })) + .with(UserTypeEnum.External, () => ({ + verboseName: msg("External User"), + verboseNamePlural: msg("External Users"), + })) + .with(UserTypeEnum.ServiceAccount, () => ({ + verboseName: msg("Service Account"), + verboseNamePlural: msg("Service Accounts"), + })) + .otherwise(() => ({ + verboseName: msg("User"), + verboseNamePlural: msg("Users"), + })); this.verboseName = verboseName; this.verboseNamePlural = verboseNamePlural; @@ -208,21 +216,32 @@ export class UserForm extends ModelForm { 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[]} + .options=${(this.isAgent + ? [ + { + label: msg("Agent account"), + value: UserTypeEnum.ServiceAccount, + disabled: true, + description: html`${msg( + "AI agent acting on behalf of an internal user. Type cannot be changed.", + )}`, + }, + ] + : [ + ...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[]} >`} `, html``, Timestamp(item.lastLogin), - html`${userTypeToLabel(item.type)}`, + html`${userDisplayTypeLabel(item)}`, html`
${IconEditButton(UserForm, item.pk, displayName)} ${showImpersonation diff --git a/web/src/admin/users/UserViewPage.ts b/web/src/admin/users/UserViewPage.ts index ead0b48935a0..496a409a5584 100644 --- a/web/src/admin/users/UserViewPage.ts +++ b/web/src/admin/users/UserViewPage.ts @@ -28,7 +28,7 @@ import "./UserDevicesTable.js"; import "#elements/ak-mdx/ak-mdx"; import { DEFAULT_CONFIG } from "#common/api/config"; -import { userTypeToLabel } from "#common/labels"; +import { userDisplayTypeLabel } from "#common/labels"; import { formatUserDisplayName } from "#common/users"; import { AKElement } from "#elements/Base"; @@ -118,7 +118,7 @@ export class UserViewPage extends WithBrandConfig(WithCapabilitiesConfig(WithSes [msg("Last login"), Timestamp(user.lastLogin)], [msg("Last password change"), Timestamp(user.passwordChangeDate)], [msg("Active"), html``], - [msg("Type"), userTypeToLabel(user.type)], + [msg("Type"), userDisplayTypeLabel(user)], [msg("Superuser"), html``], [msg("Actions"), this.renderActionButtons(user)], [msg("Recovery"), this.renderRecoveryButtons(user)], diff --git a/web/src/admin/users/ak-user-wizard.ts b/web/src/admin/users/ak-user-wizard.ts index 0d62239d64e7..bcd7d2843e1a 100644 --- a/web/src/admin/users/ak-user-wizard.ts +++ b/web/src/admin/users/ak-user-wizard.ts @@ -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,7 @@ 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"; @@ -26,6 +27,10 @@ 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; +const AGENT_FORM_SLOT = "type-ak-user-agent-form-agent" as const; +const AGENT_RESULT_SLOT = `${AGENT_FORM_SLOT}-result` as const; +const AGENT_MODEL_NAME = "agent" as const; + const DEFAULT_USER_TYPES: TypeCreate[] = [ { component: "ak-user-form", @@ -41,6 +46,15 @@ const DEFAULT_USER_TYPES: TypeCreate[] = [ "External consultants or B2C customers without access to enterprise features.", ), }, + { + component: "ak-user-agent-form", + modelName: AGENT_MODEL_NAME, + name: msg("Agent User"), + description: msg( + "AI agent acting on behalf of an internal user, with scoped application access.", + ), + requiresEnterprise: true, + }, { component: "ak-user-service-account-form", modelName: UserTypeEnum.ServiceAccount, @@ -51,6 +65,7 @@ const DEFAULT_USER_TYPES: TypeCreate[] = [ export interface UserWizardState { [SERVICE_ACCOUNT_FORM_SLOT]?: UserServiceAccountResponse; + [AGENT_FORM_SLOT]?: UserAgentResponse; } @customElement("ak-user-service-account-result-page") @@ -110,6 +125,64 @@ export class ServiceAccountResultPage extends WizardPage { } } +@customElement("ak-user-agent-result-page") +export class AgentResultPage extends WizardPage { + public static styles: CSSResult[] = [PFForm, PFFormControl]; + + public override headline = msg("Review Credentials"); + + @state() + protected result: UserAgentResponse | null = null; + + public override activeCallback = async (): Promise => { + 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 => true; + + protected override render(): SlottedTemplateResult { + if (!this.result) { + return null; + } + + const { username, token } = this.result; + + return html`

${msg("Review Credentials")}

+

+ ${msg( + "Use the username and token below to authenticate the agent. The token expires " + + "after 24 hours and must be rotated before expiry.", + )} +

+
+ + +
`; + } +} + @customElement("ak-user-wizard") export class AKUserWizard extends CreateWizard { /** @@ -128,17 +201,16 @@ 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.ServiceAccount) { + return [SERVICE_ACCOUNT_FORM_SLOT, SERVICE_ACCOUNT_RESULT_SLOT]; } - return [ - // --- - SERVICE_ACCOUNT_FORM_SLOT, - SERVICE_ACCOUNT_RESULT_SLOT, - ]; + if (modelName === AGENT_MODEL_NAME) { + return [AGENT_FORM_SLOT, AGENT_RESULT_SLOT]; + } + + return super.selectSteps(type, currentSteps); } protected override renderWizardStep(type: TypeCreate): SlottedTemplateResult { @@ -151,11 +223,23 @@ export class AKUserWizard extends CreateWizard { ]; } + if (type.modelName === AGENT_MODEL_NAME) { + return [ + super.renderWizardStep(type), + html``, + ]; + } + return super.renderWizardStep(type); } protected override assembleFormProps(type: TypeCreate): LitPropertyRecord { - if (type.modelName === UserTypeEnum.ServiceAccount) { + if ( + type.modelName === UserTypeEnum.ServiceAccount || + type.modelName === AGENT_MODEL_NAME + ) { return {}; } @@ -172,5 +256,7 @@ declare global { interface HTMLElementTagNameMap { "ak-user-wizard": AKUserWizard; "ak-user-service-account-result-page": ServiceAccountResultPage; + "ak-user-agent-result-page": AgentResultPage; + "ak-user-agent-form": AgentForm; } } diff --git a/web/src/common/labels.ts b/web/src/common/labels.ts index be5b8d917abf..6404a16c779d 100644 --- a/web/src/common/labels.ts +++ b/web/src/common/labels.ts @@ -5,6 +5,7 @@ import { EventActions, IntentEnum, SeverityEnum, + User, UserTypeEnum, } from "@goauthentik/api"; @@ -122,3 +123,8 @@ const _userTypeToLabel = new Map([ export const userTypeToLabel = (type?: UserTypeEnum): string => _userTypeToLabel.get(type) ?? type ?? ""; + +const USER_ATTRIBUTE_IS_AGENT = "goauthentik.io/agent/is-agent"; + +export const userDisplayTypeLabel = (user: User): string => + user.attributes?.[USER_ATTRIBUTE_IS_AGENT] ? msg("Agent account") : userTypeToLabel(user.type);