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`Set custom attributes using YAML or JSON. Any attributes set here will be inherited by users, if the request is handled by this tenant.`} +
+