tenants: add tenant-level attributes, applied to users based on request

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2022-04-06 10:41:35 +02:00
parent fcd9c58a73
commit 5861d41ad3
12 changed files with 85 additions and 23 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
}
}"""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Tenant, string> {
${t`Format: "weeks=3;days=2;hours=3,seconds=2".`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Attributes`} name="attributes">
<ak-codemirror
mode="yaml"
value="${YAML.stringify(first(this.instance?.attributes, {}))}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
${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.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Web Certificate`} name="webCertificate">
<select class="pf-c-form-control">
<option