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:
parent
fcd9c58a73
commit
5861d41ad3
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}"""
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
18
authentik/tenants/migrations/0003_tenant_attributes.py
Normal file
18
authentik/tenants/migrations/0003_tenant_attributes.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -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"
|
||||
|
|
15
schema.yml
15
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:
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in a new issue