From 5861d41ad3f41b92153450569d00c15bf00f6706 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Wed, 6 Apr 2022 10:41:35 +0200 Subject: [PATCH] tenants: add tenant-level attributes, applied to users based on request Signed-off-by: Jens Langhammer --- authentik/core/api/users.py | 19 ++++++++++++------- authentik/core/models.py | 4 +++- authentik/flows/views/executor.py | 6 +++--- authentik/lib/utils/http.py | 2 +- authentik/policies/denied.py | 6 +++--- authentik/providers/proxy/managed.py | 2 +- authentik/tenants/api.py | 17 ++++++++++++++++- .../0002_tenant_flow_user_settings.py | 6 +++--- .../migrations/0003_tenant_attributes.py | 18 ++++++++++++++++++ authentik/tenants/models.py | 2 ++ schema.yml | 15 ++++++++++++--- web/src/pages/tenants/TenantForm.ts | 11 +++++++++++ 12 files changed, 85 insertions(+), 23 deletions(-) create mode 100644 authentik/tenants/migrations/0003_tenant_attributes.py diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 39e5a77a2..df9d69777 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -1,7 +1,7 @@ """User API Views""" from datetime import timedelta from json import loads -from typing import Optional +from typing import Any, Optional from django.contrib.auth import update_session_auth_hash from django.db.models.query import QuerySet @@ -23,7 +23,7 @@ from drf_spectacular.utils import ( ) from guardian.shortcuts import get_anonymous_user, get_objects_for_user from rest_framework.decorators import action -from rest_framework.fields import CharField, DictField, JSONField, SerializerMethodField +from rest_framework.fields import CharField, JSONField, SerializerMethodField from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ( @@ -96,14 +96,13 @@ class UserSerializer(ModelSerializer): class UserSelfSerializer(ModelSerializer): - """User Serializer for information a user can retrieve about themselves and - update about themselves""" + """User Serializer for information a user can retrieve about themselves""" is_superuser = BooleanField(read_only=True) avatar = CharField(read_only=True) groups = SerializerMethodField() uid = CharField(read_only=True) - settings = DictField(source="attributes.settings", default=dict) + settings = SerializerMethodField() @extend_schema_field( ListSerializer( @@ -121,6 +120,10 @@ class UserSelfSerializer(ModelSerializer): "pk": group.pk, } + def get_settings(self, user: User) -> dict[str, Any]: + """Get user settings with tenant and group settings applied""" + return user.group_attributes(self._context["request"]).get("settings", {}) + class Meta: model = User @@ -328,12 +331,14 @@ class UserViewSet(UsedByMixin, ModelViewSet): # pylint: disable=invalid-name def me(self, request: Request) -> Response: """Get information about current user""" + context = {"request": request} serializer = SessionUserSerializer( - data={"user": UserSelfSerializer(instance=request.user).data} + data={"user": UserSelfSerializer(instance=request.user, context=context).data} ) if SESSION_IMPERSONATE_USER in request._request.session: serializer.initial_data["original"] = UserSelfSerializer( - instance=request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER] + instance=request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER], + context=context, ).data return Response(serializer.initial_data) diff --git a/authentik/core/models.py b/authentik/core/models.py index 4ebe1b6a1..29e62c783 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -147,10 +147,12 @@ class User(GuardianUserMixin, AbstractUser): objects = UserManager() - def group_attributes(self) -> dict[str, Any]: + def group_attributes(self, request: Optional[HttpRequest] = None) -> dict[str, Any]: """Get a dictionary containing the attributes from all groups the user belongs to, including the users attributes""" final_attributes = {} + if request and hasattr(request, "tenant"): + always_merger.merge(final_attributes, request.tenant.attributes) for group in self.ak_groups.all().order_by("name"): always_merger.merge(final_attributes, group.attributes) always_merger.merge(final_attributes, self.attributes) diff --git a/authentik/flows/views/executor.py b/authentik/flows/views/executor.py index d41573876..e6e27a0f9 100644 --- a/authentik/flows/views/executor.py +++ b/authentik/flows/views/executor.py @@ -442,9 +442,9 @@ class FlowErrorResponse(TemplateResponse): context = {} context["error"] = self.error if self._request.user and self._request.user.is_authenticated: - if self._request.user.is_superuser or self._request.user.group_attributes().get( - USER_ATTRIBUTE_DEBUG, False - ): + if self._request.user.is_superuser or self._request.user.group_attributes( + self._request + ).get(USER_ATTRIBUTE_DEBUG, False): context["tb"] = "".join(format_tb(self.error.__traceback__)) return context diff --git a/authentik/lib/utils/http.py b/authentik/lib/utils/http.py index 6ef90e835..94c01c9b7 100644 --- a/authentik/lib/utils/http.py +++ b/authentik/lib/utils/http.py @@ -45,7 +45,7 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]: LOGGER.warning("Attempted remote-ip override without token", fake_ip=fake_ip) return None user = tokens.first().user - if not user.group_attributes().get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False): + if not user.group_attributes(request).get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False): LOGGER.warning( "Remote-IP override: user doesn't have permission", user=user, diff --git a/authentik/policies/denied.py b/authentik/policies/denied.py index 349a17ce5..b65688727 100644 --- a/authentik/policies/denied.py +++ b/authentik/policies/denied.py @@ -33,8 +33,8 @@ class AccessDeniedResponse(TemplateResponse): # either superuser or has USER_ATTRIBUTE_DEBUG set if self.policy_result: if self._request.user and self._request.user.is_authenticated: - if self._request.user.is_superuser or self._request.user.group_attributes().get( - USER_ATTRIBUTE_DEBUG, False - ): + if self._request.user.is_superuser or self._request.user.group_attributes( + self._request + ).get(USER_ATTRIBUTE_DEBUG, False): context["policy_result"] = self.policy_result return context diff --git a/authentik/providers/proxy/managed.py b/authentik/providers/proxy/managed.py index fb7cf35c2..ab908322e 100644 --- a/authentik/providers/proxy/managed.py +++ b/authentik/providers/proxy/managed.py @@ -8,7 +8,7 @@ SCOPE_AK_PROXY_EXPRESSION = """ # which are used for example for the HTTP-Basic Authentication mapping. return { "ak_proxy": { - "user_attributes": request.user.group_attributes() + "user_attributes": request.user.group_attributes(request) } }""" diff --git a/authentik/tenants/api.py b/authentik/tenants/api.py index 631ceb7ee..61911d8fe 100644 --- a/authentik/tenants/api.py +++ b/authentik/tenants/api.py @@ -53,6 +53,7 @@ class TenantSerializer(ModelSerializer): "flow_user_settings", "event_retention", "web_certificate", + "attributes", ] @@ -86,7 +87,21 @@ class TenantViewSet(UsedByMixin, ModelViewSet): "branding_title", "web_certificate__name", ] - filterset_fields = "__all__" + filterset_fields = [ + "tenant_uuid", + "domain", + "default", + "branding_title", + "branding_logo", + "branding_favicon", + "flow_authentication", + "flow_invalidation", + "flow_recovery", + "flow_unenrollment", + "flow_user_settings", + "event_retention", + "web_certificate", + ] ordering = ["domain"] @extend_schema( diff --git a/authentik/tenants/migrations/0002_tenant_flow_user_settings.py b/authentik/tenants/migrations/0002_tenant_flow_user_settings.py index 0fda85dd8..93e54dbf3 100644 --- a/authentik/tenants/migrations/0002_tenant_flow_user_settings.py +++ b/authentik/tenants/migrations/0002_tenant_flow_user_settings.py @@ -17,21 +17,21 @@ from authentik.core.models import ( ) prompt_data = request.context.get("prompt_data") -if not request.user.group_attributes().get( +if not request.user.group_attributes(request.http_request).get( USER_ATTRIBUTE_CHANGE_EMAIL, CONFIG.y_bool("default_user_change_email", True) ): if prompt_data.get("email") != request.user.email: ak_message("Not allowed to change email address.") return False -if not request.user.group_attributes().get( +if not request.user.group_attributes(request.http_request).get( USER_ATTRIBUTE_CHANGE_NAME, CONFIG.y_bool("default_user_change_name", True) ): if prompt_data.get("name") != request.user.name: ak_message("Not allowed to change name.") return False -if not request.user.group_attributes().get( +if not request.user.group_attributes(request.http_request).get( USER_ATTRIBUTE_CHANGE_USERNAME, CONFIG.y_bool("default_user_change_username", True) ): if prompt_data.get("username") != request.user.username: diff --git a/authentik/tenants/migrations/0003_tenant_attributes.py b/authentik/tenants/migrations/0003_tenant_attributes.py new file mode 100644 index 000000000..c467ac0c6 --- /dev/null +++ b/authentik/tenants/migrations/0003_tenant_attributes.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.3 on 2022-04-06 08:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_tenants", "0002_tenant_flow_user_settings"), + ] + + operations = [ + migrations.AddField( + model_name="tenant", + name="attributes", + field=models.JSONField(blank=True, default=dict), + ), + ] diff --git a/authentik/tenants/models.py b/authentik/tenants/models.py index 58b6de674..df4901c2a 100644 --- a/authentik/tenants/models.py +++ b/authentik/tenants/models.py @@ -63,6 +63,8 @@ class Tenant(models.Model): help_text=_(("Web Certificate used by the authentik Core webserver.")), ) + attributes = models.JSONField(default=dict, blank=True) + def __str__(self) -> str: if self.default: return "Default tenant" diff --git a/schema.yml b/schema.yml index 4d9490689..aa283ef2f 100644 --- a/schema.yml +++ b/schema.yml @@ -28229,6 +28229,9 @@ components: format: uuid nullable: true description: Web Certificate used by the authentik Core webserver. + attributes: + type: object + additionalProperties: {} PatchedTokenRequest: type: object description: Token Serializer @@ -30673,6 +30676,9 @@ components: format: uuid nullable: true description: Web Certificate used by the authentik Core webserver. + attributes: + type: object + additionalProperties: {} required: - domain - tenant_uuid @@ -30725,6 +30731,9 @@ components: format: uuid nullable: true description: Web Certificate used by the authentik Core webserver. + attributes: + type: object + additionalProperties: {} required: - domain Token: @@ -31211,9 +31220,7 @@ components: - username UserSelf: type: object - description: |- - User Serializer for information a user can retrieve about themselves and - update about themselves + description: User Serializer for information a user can retrieve about themselves properties: pk: type: integer @@ -31256,6 +31263,7 @@ components: settings: type: object additionalProperties: {} + readOnly: true required: - avatar - groups @@ -31263,6 +31271,7 @@ components: - is_superuser - name - pk + - settings - uid - username UserSelfGroups: diff --git a/web/src/pages/tenants/TenantForm.ts b/web/src/pages/tenants/TenantForm.ts index 9d3c90534..bb161f5b0 100644 --- a/web/src/pages/tenants/TenantForm.ts +++ b/web/src/pages/tenants/TenantForm.ts @@ -1,4 +1,5 @@ import { t } from "@lingui/macro"; +import YAML from "yaml"; import { TemplateResult, html } from "lit"; import { customElement } from "lit/decorators.js"; @@ -341,6 +342,16 @@ export class TenantForm extends ModelForm { ${t`Format: "weeks=3;days=2;hours=3,seconds=2".`}

+ + + +

+ ${t`Set custom attributes using YAML or JSON. Any attributes set here will be inherited by users, if the request is handled by this tenant.`} +

+