core: Initial RBAC (#6806)

* rename consent permission

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* the user version

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

t

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* initial role

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start form

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* some minor table refactoring

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix user, add assign

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add roles ui

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix backend

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add assign API for roles

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start adding toggle buttons

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start view page

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* exclude add_ permission for per-object perms

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* small cleanup

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add permission list for roles

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make sidebar update

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix page header not re-rendering?

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fixup

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add search

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* show first category in table groupBy except when its empty

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make model and object PK optional but required together

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* allow for setting global perms

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* exclude non-authentik permissions

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* exclude models which aren't allowed (base models etc)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* ensure all models have verbose_name set, exclude some more internal objects

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* lint fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix role perm assign

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add unasign for global perms

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add meta changes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* clear modal state after submit

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add roles to our group

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix duplicate url names

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make recursive group query more usable

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add name field to role itself and move group creation to signal

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start sync

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* move rbac stuff to separate django app

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix lint and such

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix go

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start API changes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add more API tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make admin interface not require superuser for now, improve error handling

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* replace some IsAdminUser where applicable

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* migrate flow inspector perms to actual permission

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix license not being a serializermodel

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add permission modal to models without view page

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add additional permissions to assign/unassign permissions

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add action to unassign user permissions

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add permissions tab to remaining view pages

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix flow inspector permission check

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix codecov config?

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add more API tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* ensure viewsets have an order set

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* hopefully the last api name change

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make perm modal less confusing

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start user view permission page

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* only make delete bulk form expandable if usedBy is set

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* expand permission tables

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add more things

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add user global permission table

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix lint

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests' url names

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add tests for assign perms

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add unassign tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rebuild permissions

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* prevent assigning/unassigning permissions to internal service accounts

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* only enable default api browser in debug

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix role object permissions showing duplicate

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix role link on role object permissions table

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix object permission modal having duplicate close buttons

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* return error if user has no global perm and no object perms

also improve error display on table

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* small optimisation

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* optimise even more

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update locale

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add system permission for non-object permissions

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* allow access to admin interface based on perm

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* clean

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* don't exclude base models

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L 2023-10-16 17:31:50 +02:00 committed by GitHub
parent dce913496e
commit e28babb0b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
139 changed files with 6563 additions and 425 deletions

4
.github/codecov.yml vendored
View file

@ -6,5 +6,5 @@ coverage:
# adjust accordingly based on how flaky your tests are # adjust accordingly based on how flaky your tests are
# this allows a 1% drop from the previous base commit coverage # this allows a 1% drop from the previous base commit coverage
threshold: 1% threshold: 1%
notify: comment:
after_n_builds: 3 after_n_builds: 3

View file

@ -62,8 +62,9 @@ lint-fix: ## Lint and automatically fix errors in the python source code. Repor
codespell -w $(CODESPELL_ARGS) codespell -w $(CODESPELL_ARGS)
lint: ## Lint the python and golang sources lint: ## Lint the python and golang sources
pylint $(PY_SOURCES)
bandit -r $(PY_SOURCES) -x node_modules bandit -r $(PY_SOURCES) -x node_modules
./web/node_modules/.bin/pyright $(PY_SOURCES)
pylint $(PY_SOURCES)
golangci-lint run -v golangci-lint run -v
migrate: ## Run the Authentik Django server's migrations migrate: ## Run the Authentik Django server's migrations

View file

@ -1,7 +1,7 @@
"""Meta API""" """Meta API"""
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from rest_framework.fields import CharField from rest_framework.fields import CharField
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ViewSet from rest_framework.viewsets import ViewSet
@ -21,7 +21,7 @@ class AppSerializer(PassiveSerializer):
class AppsViewSet(ViewSet): class AppsViewSet(ViewSet):
"""Read-only view list all installed apps""" """Read-only view list all installed apps"""
permission_classes = [IsAdminUser] permission_classes = [IsAuthenticated]
@extend_schema(responses={200: AppSerializer(many=True)}) @extend_schema(responses={200: AppSerializer(many=True)})
def list(self, request: Request) -> Response: def list(self, request: Request) -> Response:
@ -35,7 +35,7 @@ class AppsViewSet(ViewSet):
class ModelViewSet(ViewSet): class ModelViewSet(ViewSet):
"""Read-only view list all installed models""" """Read-only view list all installed models"""
permission_classes = [IsAdminUser] permission_classes = [IsAuthenticated]
@extend_schema(responses={200: AppSerializer(many=True)}) @extend_schema(responses={200: AppSerializer(many=True)})
def list(self, request: Request) -> Response: def list(self, request: Request) -> Response:

View file

@ -5,7 +5,7 @@ from django.db.models.functions import ExtractHour
from drf_spectacular.utils import extend_schema, extend_schema_field from drf_spectacular.utils import extend_schema, extend_schema_field
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from rest_framework.fields import IntegerField, SerializerMethodField from rest_framework.fields import IntegerField, SerializerMethodField
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
@ -68,7 +68,7 @@ class LoginMetricsSerializer(PassiveSerializer):
class AdministrationMetricsViewSet(APIView): class AdministrationMetricsViewSet(APIView):
"""Login Metrics per 1h""" """Login Metrics per 1h"""
permission_classes = [IsAdminUser] permission_classes = [IsAuthenticated]
@extend_schema(responses={200: LoginMetricsSerializer(many=False)}) @extend_schema(responses={200: LoginMetricsSerializer(many=False)})
def get(self, request: Request) -> Response: def get(self, request: Request) -> Response:

View file

@ -8,7 +8,6 @@ from django.utils.timezone import now
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from gunicorn import version_info as gunicorn_version from gunicorn import version_info as gunicorn_version
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
@ -17,6 +16,7 @@ from authentik.core.api.utils import PassiveSerializer
from authentik.lib.utils.reflection import get_env from authentik.lib.utils.reflection import get_env
from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost from authentik.outposts.models import Outpost
from authentik.rbac.permissions import HasPermission
class RuntimeDict(TypedDict): class RuntimeDict(TypedDict):
@ -88,7 +88,7 @@ class SystemSerializer(PassiveSerializer):
class SystemView(APIView): class SystemView(APIView):
"""Get system information.""" """Get system information."""
permission_classes = [IsAdminUser] permission_classes = [HasPermission("authentik_rbac.view_system_info")]
pagination_class = None pagination_class = None
filter_backends = [] filter_backends = []
serializer_class = SystemSerializer serializer_class = SystemSerializer

View file

@ -14,14 +14,15 @@ from rest_framework.fields import (
ListField, ListField,
SerializerMethodField, SerializerMethodField,
) )
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ViewSet from rest_framework.viewsets import ViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
from authentik.rbac.permissions import HasPermission
LOGGER = get_logger() LOGGER = get_logger()
@ -63,7 +64,7 @@ class TaskSerializer(PassiveSerializer):
class TaskViewSet(ViewSet): class TaskViewSet(ViewSet):
"""Read-only view set that returns all background tasks""" """Read-only view set that returns all background tasks"""
permission_classes = [IsAdminUser] permission_classes = [HasPermission("authentik_rbac.view_system_tasks")]
serializer_class = TaskSerializer serializer_class = TaskSerializer
@extend_schema( @extend_schema(
@ -93,6 +94,7 @@ class TaskViewSet(ViewSet):
tasks = sorted(TaskInfo.all().values(), key=lambda task: task.task_name) tasks = sorted(TaskInfo.all().values(), key=lambda task: task.task_name)
return Response(TaskSerializer(tasks, many=True).data) return Response(TaskSerializer(tasks, many=True).data)
@permission_required(None, ["authentik_rbac.run_system_tasks"])
@extend_schema( @extend_schema(
request=OpenApiTypes.NONE, request=OpenApiTypes.NONE,
responses={ responses={

View file

@ -2,18 +2,18 @@
from django.conf import settings from django.conf import settings
from drf_spectacular.utils import extend_schema, inline_serializer from drf_spectacular.utils import extend_schema, inline_serializer
from rest_framework.fields import IntegerField from rest_framework.fields import IntegerField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from authentik.rbac.permissions import HasPermission
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
class WorkerView(APIView): class WorkerView(APIView):
"""Get currently connected worker count.""" """Get currently connected worker count."""
permission_classes = [IsAdminUser] permission_classes = [HasPermission("authentik_rbac.view_system_info")]
@extend_schema(responses=inline_serializer("Workers", fields={"count": IntegerField()})) @extend_schema(responses=inline_serializer("Workers", fields={"count": IntegerField()}))
def get(self, request: Request) -> Response: def get(self, request: Request) -> Response:

View file

@ -7,9 +7,9 @@ from rest_framework.authentication import get_authorization_header
from rest_framework.filters import BaseFilterBackend from rest_framework.filters import BaseFilterBackend
from rest_framework.permissions import BasePermission from rest_framework.permissions import BasePermission
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.api.authentication import validate_auth from authentik.api.authentication import validate_auth
from authentik.rbac.filters import ObjectFilter
class OwnerFilter(BaseFilterBackend): class OwnerFilter(BaseFilterBackend):
@ -26,14 +26,14 @@ class OwnerFilter(BaseFilterBackend):
class SecretKeyFilter(DjangoFilterBackend): class SecretKeyFilter(DjangoFilterBackend):
"""Allow access to all objects when authenticated with secret key as token. """Allow access to all objects when authenticated with secret key as token.
Replaces both DjangoFilterBackend and ObjectPermissionsFilter""" Replaces both DjangoFilterBackend and ObjectFilter"""
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet: def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
auth_header = get_authorization_header(request) auth_header = get_authorization_header(request)
token = validate_auth(auth_header) token = validate_auth(auth_header)
if token and token == settings.SECRET_KEY: if token and token == settings.SECRET_KEY:
return queryset return queryset
queryset = ObjectPermissionsFilter().filter_queryset(request, queryset, view) queryset = ObjectFilter().filter_queryset(request, queryset, view)
return super().filter_queryset(request, queryset, view) return super().filter_queryset(request, queryset, view)

View file

@ -10,7 +10,7 @@ from structlog.stdlib import get_logger
LOGGER = get_logger() LOGGER = get_logger()
def permission_required(perm: Optional[str] = None, other_perms: Optional[list[str]] = None): def permission_required(obj_perm: Optional[str] = None, global_perms: Optional[list[str]] = None):
"""Check permissions for a single custom action""" """Check permissions for a single custom action"""
def wrapper_outter(func: Callable): def wrapper_outter(func: Callable):
@ -18,15 +18,17 @@ def permission_required(perm: Optional[str] = None, other_perms: Optional[list[s
@wraps(func) @wraps(func)
def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response: def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response:
if perm: if obj_perm:
obj = self.get_object() obj = self.get_object()
if not request.user.has_perm(perm, obj): if not request.user.has_perm(obj_perm, obj):
LOGGER.debug("denying access for object", user=request.user, perm=perm, obj=obj) LOGGER.debug(
"denying access for object", user=request.user, perm=obj_perm, obj=obj
)
return self.permission_denied(request) return self.permission_denied(request)
if other_perms: if global_perms:
for other_perm in other_perms: for other_perm in global_perms:
if not request.user.has_perm(other_perm): if not request.user.has_perm(other_perm):
LOGGER.debug("denying access for other", user=request.user, perm=perm) LOGGER.debug("denying access for other", user=request.user, perm=other_perm)
return self.permission_denied(request) return self.permission_denied(request)
return func(self, request, *args, **kwargs) return func(self, request, *args, **kwargs)

View file

@ -77,3 +77,10 @@ class Pagination(pagination.PageNumberPagination):
}, },
"required": ["pagination", "results"], "required": ["pagination", "results"],
} }
class SmallerPagination(Pagination):
"""Smaller pagination for objects which might require a lot of queries
to retrieve all data for."""
max_page_size = 10

View file

@ -16,6 +16,7 @@ def viewset_tester_factory(test_viewset: type[ModelViewSet]) -> Callable:
def tester(self: TestModelViewSets): def tester(self: TestModelViewSets):
self.assertIsNotNone(getattr(test_viewset, "search_fields", None)) self.assertIsNotNone(getattr(test_viewset, "search_fields", None))
self.assertIsNotNone(getattr(test_viewset, "ordering", None))
filterset_class = getattr(test_viewset, "filterset_class", None) filterset_class = getattr(test_viewset, "filterset_class", None)
if not filterset_class: if not filterset_class:
self.assertIsNotNone(getattr(test_viewset, "filterset_fields", None)) self.assertIsNotNone(getattr(test_viewset, "filterset_fields", None))

View file

@ -4,7 +4,6 @@ from drf_spectacular.utils import extend_schema, inline_serializer
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, DateTimeField, JSONField from rest_framework.fields import CharField, DateTimeField, JSONField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ModelSerializer from rest_framework.serializers import ListSerializer, ModelSerializer
@ -87,11 +86,11 @@ class BlueprintInstanceSerializer(ModelSerializer):
class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet): class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
"""Blueprint instances""" """Blueprint instances"""
permission_classes = [IsAdminUser]
serializer_class = BlueprintInstanceSerializer serializer_class = BlueprintInstanceSerializer
queryset = BlueprintInstance.objects.all() queryset = BlueprintInstance.objects.all()
search_fields = ["name", "path"] search_fields = ["name", "path"]
filterset_fields = ["name", "path"] filterset_fields = ["name", "path"]
ordering = ["name"]
@extend_schema( @extend_schema(
responses={ responses={

View file

@ -35,25 +35,28 @@ from authentik.core.models import (
Source, Source,
UserSourceConnection, UserSourceConnection,
) )
from authentik.enterprise.models import LicenseUsage
from authentik.events.utils import cleanse_dict from authentik.events.utils import cleanse_dict
from authentik.flows.models import FlowToken, Stage from authentik.flows.models import FlowToken, Stage
from authentik.lib.models import SerializerModel from authentik.lib.models import SerializerModel
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.outposts.models import OutpostServiceConnection from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel from authentik.policies.models import Policy, PolicyBindingModel
from authentik.providers.scim.models import SCIMGroup, SCIMUser
# Context set when the serializer is created in a blueprint context # Context set when the serializer is created in a blueprint context
# Update website/developer-docs/blueprints/v1/models.md when used # Update website/developer-docs/blueprints/v1/models.md when used
SERIALIZER_CONTEXT_BLUEPRINT = "blueprint_entry" SERIALIZER_CONTEXT_BLUEPRINT = "blueprint_entry"
def is_model_allowed(model: type[Model]) -> bool: def excluded_models() -> list[type[Model]]:
"""Check if model is allowed""" """Return a list of all excluded models that shouldn't be exposed via API
or other means (internal only, base classes, non-used objects, etc)"""
# pylint: disable=imported-auth-user # pylint: disable=imported-auth-user
from django.contrib.auth.models import Group as DjangoGroup from django.contrib.auth.models import Group as DjangoGroup
from django.contrib.auth.models import User as DjangoUser from django.contrib.auth.models import User as DjangoUser
excluded_models = ( return (
DjangoUser, DjangoUser,
DjangoGroup, DjangoGroup,
# Base classes # Base classes
@ -69,8 +72,15 @@ def is_model_allowed(model: type[Model]) -> bool:
AuthenticatedSession, AuthenticatedSession,
# Classes which are only internally managed # Classes which are only internally managed
FlowToken, FlowToken,
LicenseUsage,
SCIMGroup,
SCIMUser,
) )
return model not in excluded_models and issubclass(model, (SerializerModel, BaseMetaModel))
def is_model_allowed(model: type[Model]) -> bool:
"""Check if model is allowed"""
return model not in excluded_models() and issubclass(model, (SerializerModel, BaseMetaModel))
class DoRollback(SentryIgnoredException): class DoRollback(SentryIgnoredException):

View file

@ -17,7 +17,6 @@ from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from structlog.testing import capture_logs from structlog.testing import capture_logs
@ -38,6 +37,7 @@ from authentik.lib.utils.file import (
from authentik.policies.api.exec import PolicyTestResultSerializer from authentik.policies.api.exec import PolicyTestResultSerializer
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult from authentik.policies.types import PolicyResult
from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger() LOGGER = get_logger()
@ -122,7 +122,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
"""Custom filter_queryset method which ignores guardian, but still supports sorting""" """Custom filter_queryset method which ignores guardian, but still supports sorting"""
for backend in list(self.filter_backends): for backend in list(self.filter_backends):
if backend == ObjectPermissionsFilter: if backend == ObjectFilter:
continue continue
queryset = backend().filter_queryset(self.request, queryset, self) queryset = backend().filter_queryset(self.request, queryset, self)
return queryset return queryset

View file

@ -2,7 +2,6 @@
from json import loads from json import loads
from typing import Optional from typing import Optional
from django.db.models.query import QuerySet
from django.http import Http404 from django.http import Http404
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
from django_filters.filterset import FilterSet from django_filters.filterset import FilterSet
@ -14,12 +13,12 @@ from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer, is_dict from authentik.core.api.utils import PassiveSerializer, is_dict
from authentik.core.models import Group, User from authentik.core.models import Group, User
from authentik.rbac.api.roles import RoleSerializer
class GroupMemberSerializer(ModelSerializer): class GroupMemberSerializer(ModelSerializer):
@ -49,6 +48,12 @@ class GroupSerializer(ModelSerializer):
users_obj = ListSerializer( users_obj = ListSerializer(
child=GroupMemberSerializer(), read_only=True, source="users", required=False child=GroupMemberSerializer(), read_only=True, source="users", required=False
) )
roles_obj = ListSerializer(
child=RoleSerializer(),
read_only=True,
source="roles",
required=False,
)
parent_name = CharField(source="parent.name", read_only=True, allow_null=True) parent_name = CharField(source="parent.name", read_only=True, allow_null=True)
num_pk = IntegerField(read_only=True) num_pk = IntegerField(read_only=True)
@ -71,8 +76,10 @@ class GroupSerializer(ModelSerializer):
"parent", "parent",
"parent_name", "parent_name",
"users", "users",
"attributes",
"users_obj", "users_obj",
"attributes",
"roles",
"roles_obj",
] ]
extra_kwargs = { extra_kwargs = {
"users": { "users": {
@ -138,19 +145,6 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
filterset_class = GroupFilter filterset_class = GroupFilter
ordering = ["name"] ordering = ["name"]
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
for backend in list(self.filter_backends):
if backend == ObjectPermissionsFilter:
continue
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
def filter_queryset(self, queryset):
if self.request.user.has_perm("authentik_core.view_group"):
return self._filter_queryset_for_list(queryset)
return super().filter_queryset(queryset)
@permission_required(None, ["authentik_core.add_user"]) @permission_required(None, ["authentik_core.add_user"])
@extend_schema( @extend_schema(
request=UserAccountSerializer, request=UserAccountSerializer,

View file

@ -119,6 +119,7 @@ class TransactionApplicationResponseSerializer(PassiveSerializer):
class TransactionalApplicationView(APIView): class TransactionalApplicationView(APIView):
"""Create provider and application and attach them in a single transaction""" """Create provider and application and attach them in a single transaction"""
# TODO: Migrate to a more specific permission
permission_classes = [IsAdminUser] permission_classes = [IsAdminUser]
@extend_schema( @extend_schema(

View file

@ -73,6 +73,11 @@ class UsedByMixin:
# but so we only apply them once, have a simple flag for the first object # but so we only apply them once, have a simple flag for the first object
first_object = True first_object = True
# TODO: This will only return the used-by references that the user can see
# Either we have to leak model information here to not make the list
# useless if the user doesn't have all permissions, or we need to double
# query and check if there is a difference between modes the user can see
# and can't see and add a warning
for obj in get_objects_for_user( for obj in get_objects_for_user(
request.user, f"{app}.view_{model_name}", manager request.user, f"{app}.view_{model_name}", manager
).all(): ).all():

View file

@ -7,7 +7,6 @@ from django.contrib.auth import update_session_auth_hash
from django.contrib.sessions.backends.cache import KEY_PREFIX from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache from django.core.cache import cache
from django.db.models.functions import ExtractHour from django.db.models.functions import ExtractHour
from django.db.models.query import QuerySet
from django.db.transaction import atomic from django.db.transaction import atomic
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -52,7 +51,6 @@ from rest_framework.serializers import (
) )
from rest_framework.validators import UniqueValidator from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer from authentik.admin.api.metrics import CoordinateSerializer
@ -205,6 +203,7 @@ class UserSelfSerializer(ModelSerializer):
groups = SerializerMethodField() groups = SerializerMethodField()
uid = CharField(read_only=True) uid = CharField(read_only=True)
settings = SerializerMethodField() settings = SerializerMethodField()
system_permissions = SerializerMethodField()
@extend_schema_field( @extend_schema_field(
ListSerializer( ListSerializer(
@ -226,6 +225,14 @@ class UserSelfSerializer(ModelSerializer):
"""Get user settings with tenant and group settings applied""" """Get user settings with tenant and group settings applied"""
return user.group_attributes(self._context["request"]).get("settings", {}) return user.group_attributes(self._context["request"]).get("settings", {})
def get_system_permissions(self, user: User) -> list[str]:
"""Get all system permissions assigned to the user"""
return list(
user.user_permissions.filter(
content_type__app_label="authentik_rbac", content_type__model="systempermission"
).values_list("codename", flat=True)
)
class Meta: class Meta:
model = User model = User
fields = [ fields = [
@ -240,6 +247,7 @@ class UserSelfSerializer(ModelSerializer):
"uid", "uid",
"settings", "settings",
"type", "type",
"system_permissions",
] ]
extra_kwargs = { extra_kwargs = {
"is_active": {"read_only": True}, "is_active": {"read_only": True},
@ -654,19 +662,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
return Response(status=204) return Response(status=204)
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
for backend in list(self.filter_backends):
if backend == ObjectPermissionsFilter:
continue
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
def filter_queryset(self, queryset):
if self.request.user.has_perm("authentik_core.view_user"):
return self._filter_queryset_for_list(queryset)
return super().filter_queryset(queryset)
@extend_schema( @extend_schema(
responses={ responses={
200: inline_serializer( 200: inline_serializer(

View file

@ -0,0 +1,45 @@
# Generated by Django 4.2.6 on 2023-10-11 13:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0031_alter_user_type"),
("authentik_rbac", "0001_initial"),
]
operations = [
migrations.AlterModelOptions(
name="group",
options={"verbose_name": "Group", "verbose_name_plural": "Groups"},
),
migrations.AlterModelOptions(
name="token",
options={
"permissions": [("view_token_key", "View token's key")],
"verbose_name": "Token",
"verbose_name_plural": "Tokens",
},
),
migrations.AlterModelOptions(
name="user",
options={
"permissions": [
("reset_user_password", "Reset Password"),
("impersonate", "Can impersonate other users"),
("assign_user_permissions", "Can assign permissions to users"),
("unassign_user_permissions", "Can unassign permissions from users"),
],
"verbose_name": "User",
"verbose_name_plural": "Users",
},
),
migrations.AddField(
model_name="group",
name="roles",
field=models.ManyToManyField(
blank=True, related_name="ak_groups", to="authentik_rbac.role"
),
),
]

View file

@ -1,7 +1,7 @@
"""authentik core models""" """authentik core models"""
from datetime import timedelta from datetime import timedelta
from hashlib import sha256 from hashlib import sha256
from typing import Any, Optional from typing import Any, Optional, Self
from uuid import uuid4 from uuid import uuid4
from deepmerge import always_merger from deepmerge import always_merger
@ -88,6 +88,8 @@ class Group(SerializerModel):
default=False, help_text=_("Users added to this group will be superusers.") default=False, help_text=_("Users added to this group will be superusers.")
) )
roles = models.ManyToManyField("authentik_rbac.Role", related_name="ak_groups", blank=True)
parent = models.ForeignKey( parent = models.ForeignKey(
"Group", "Group",
blank=True, blank=True,
@ -115,6 +117,38 @@ class Group(SerializerModel):
"""Recursively check if `user` is member of us, or any parent.""" """Recursively check if `user` is member of us, or any parent."""
return user.all_groups().filter(group_uuid=self.group_uuid).exists() return user.all_groups().filter(group_uuid=self.group_uuid).exists()
def children_recursive(self: Self | QuerySet["Group"]) -> QuerySet["Group"]:
"""Recursively get all groups that have this as parent or are indirectly related"""
direct_groups = []
if isinstance(self, QuerySet):
direct_groups = list(x for x in self.all().values_list("pk", flat=True).iterator())
else:
direct_groups = [self.pk]
if len(direct_groups) < 1:
return Group.objects.none()
query = """
WITH RECURSIVE parents AS (
SELECT authentik_core_group.*, 0 AS relative_depth
FROM authentik_core_group
WHERE authentik_core_group.group_uuid = ANY(%s)
UNION ALL
SELECT authentik_core_group.*, parents.relative_depth + 1
FROM authentik_core_group, parents
WHERE (
authentik_core_group.group_uuid = parents.parent_id and
parents.relative_depth < 20
)
)
SELECT group_uuid
FROM parents
GROUP BY group_uuid, name
ORDER BY name;
"""
group_pks = [group.pk for group in Group.objects.raw(query, [direct_groups]).iterator()]
return Group.objects.filter(pk__in=group_pks)
def __str__(self): def __str__(self):
return f"Group {self.name}" return f"Group {self.name}"
@ -125,6 +159,8 @@ class Group(SerializerModel):
"parent", "parent",
), ),
) )
verbose_name = _("Group")
verbose_name_plural = _("Groups")
class UserManager(DjangoUserManager): class UserManager(DjangoUserManager):
@ -160,33 +196,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
"""Recursively get all groups this user is a member of. """Recursively get all groups this user is a member of.
At least one query is done to get the direct groups of the user, with groups At least one query is done to get the direct groups of the user, with groups
there are at most 3 queries done""" there are at most 3 queries done"""
direct_groups = list( return Group.children_recursive(self.ak_groups.all())
x for x in self.ak_groups.all().values_list("pk", flat=True).iterator()
)
if len(direct_groups) < 1:
return Group.objects.none()
query = """
WITH RECURSIVE parents AS (
SELECT authentik_core_group.*, 0 AS relative_depth
FROM authentik_core_group
WHERE authentik_core_group.group_uuid = ANY(%s)
UNION ALL
SELECT authentik_core_group.*, parents.relative_depth + 1
FROM authentik_core_group, parents
WHERE (
authentik_core_group.group_uuid = parents.parent_id and
parents.relative_depth < 20
)
)
SELECT group_uuid
FROM parents
GROUP BY group_uuid, name
ORDER BY name;
"""
group_pks = [group.pk for group in Group.objects.raw(query, [direct_groups]).iterator()]
return Group.objects.filter(pk__in=group_pks)
def group_attributes(self, request: Optional[HttpRequest] = None) -> 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, """Get a dictionary containing the attributes from all groups the user belongs to,
@ -261,12 +271,14 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
return get_avatar(self) return get_avatar(self)
class Meta: class Meta:
permissions = (
("reset_user_password", "Reset Password"),
("impersonate", "Can impersonate other users"),
)
verbose_name = _("User") verbose_name = _("User")
verbose_name_plural = _("Users") verbose_name_plural = _("Users")
permissions = [
("reset_user_password", _("Reset Password")),
("impersonate", _("Can impersonate other users")),
("assign_user_permissions", _("Can assign permissions to users")),
("unassign_user_permissions", _("Can unassign permissions from users")),
]
class Provider(SerializerModel): class Provider(SerializerModel):
@ -675,7 +687,7 @@ class Token(SerializerModel, ManagedModel, ExpiringModel):
models.Index(fields=["identifier"]), models.Index(fields=["identifier"]),
models.Index(fields=["key"]), models.Index(fields=["key"]),
] ]
permissions = (("view_token_key", "View token's key"),) permissions = [("view_token_key", _("View token's key"))]
class PropertyMapping(SerializerModel, ManagedModel): class PropertyMapping(SerializerModel, ManagedModel):

View file

@ -7,6 +7,7 @@ from django.db.models import Model
from django.db.models.signals import post_save, pre_delete, pre_save from django.db.models.signals import post_save, pre_delete, pre_save
from django.dispatch import receiver from django.dispatch import receiver
from django.http.request import HttpRequest from django.http.request import HttpRequest
from structlog.stdlib import get_logger
from authentik.core.models import Application, AuthenticatedSession, BackchannelProvider, User from authentik.core.models import Application, AuthenticatedSession, BackchannelProvider, User
@ -15,6 +16,8 @@ password_changed = Signal()
# Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage # Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage
login_failed = Signal() login_failed = Signal()
LOGGER = get_logger()
@receiver(post_save, sender=Application) @receiver(post_save, sender=Application)
def post_save_application(sender: type[Model], instance, created: bool, **_): def post_save_application(sender: type[Model], instance, created: bool, **_):

View file

@ -21,10 +21,9 @@ def create_test_flow(
) )
def create_test_admin_user(name: Optional[str] = None, **kwargs) -> User: def create_test_user(name: Optional[str] = None, **kwargs) -> User:
"""Generate a test-admin user""" """Generate a test user"""
uid = generate_id(20) if not name else name uid = generate_id(20) if not name else name
group = Group.objects.create(name=uid, is_superuser=True)
kwargs.setdefault("email", f"{uid}@goauthentik.io") kwargs.setdefault("email", f"{uid}@goauthentik.io")
kwargs.setdefault("username", uid) kwargs.setdefault("username", uid)
user: User = User.objects.create( user: User = User.objects.create(
@ -33,6 +32,13 @@ def create_test_admin_user(name: Optional[str] = None, **kwargs) -> User:
) )
user.set_password(uid) user.set_password(uid)
user.save() user.save()
return user
def create_test_admin_user(name: Optional[str] = None, **kwargs) -> User:
"""Generate a test-admin user"""
user = create_test_user(name, **kwargs)
group = Group.objects.create(name=user.name or name, is_superuser=True)
group.users.add(user) group.users.add(user)
return user return user

View file

@ -6,7 +6,7 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, inline_serializer from drf_spectacular.utils import extend_schema, inline_serializer
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, DateTimeField, IntegerField from rest_framework.fields import BooleanField, CharField, DateTimeField, IntegerField
from rest_framework.permissions import IsAdminUser, IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
@ -84,7 +84,7 @@ class LicenseViewSet(UsedByMixin, ModelViewSet):
200: inline_serializer("InstallIDSerializer", {"install_id": CharField(required=True)}), 200: inline_serializer("InstallIDSerializer", {"install_id": CharField(required=True)}),
}, },
) )
@action(detail=False, methods=["GET"], permission_classes=[IsAdminUser]) @action(detail=False, methods=["GET"])
def get_install_id(self, request: Request) -> Response: def get_install_id(self, request: Request) -> Response:
"""Get install_id""" """Get install_id"""
return Response( return Response(

View file

@ -33,4 +33,8 @@ class Migration(migrations.Migration):
"verbose_name_plural": "License Usage Records", "verbose_name_plural": "License Usage Records",
}, },
), ),
migrations.AlterModelOptions(
name="license",
options={"verbose_name": "License", "verbose_name_plural": "Licenses"},
),
] ]

View file

@ -19,8 +19,10 @@ from django.utils.translation import gettext as _
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
from jwt import PyJWTError, decode, get_unverified_header from jwt import PyJWTError, decode, get_unverified_header
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.serializers import BaseSerializer
from authentik.core.models import ExpiringModel, User, UserTypes from authentik.core.models import ExpiringModel, User, UserTypes
from authentik.lib.models import SerializerModel
from authentik.root.install_id import get_install_id from authentik.root.install_id import get_install_id
@ -151,7 +153,7 @@ class LicenseKey:
return usage.record_date return usage.record_date
class License(models.Model): class License(SerializerModel):
"""An authentik enterprise license""" """An authentik enterprise license"""
license_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) license_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@ -162,6 +164,12 @@ class License(models.Model):
internal_users = models.BigIntegerField() internal_users = models.BigIntegerField()
external_users = models.BigIntegerField() external_users = models.BigIntegerField()
@property
def serializer(self) -> type[BaseSerializer]:
from authentik.enterprise.api import LicenseSerializer
return LicenseSerializer
@property @property
def status(self) -> LicenseKey: def status(self) -> LicenseKey:
"""Get parsed license status""" """Get parsed license status"""
@ -169,6 +177,8 @@ class License(models.Model):
class Meta: class Meta:
indexes = (HashIndex(fields=("key",)),) indexes = (HashIndex(fields=("key",)),)
verbose_name = _("License")
verbose_name_plural = _("Licenses")
def usage_expiry(): def usage_expiry():

View file

@ -45,3 +45,4 @@ class FlowStageBindingViewSet(UsedByMixin, ModelViewSet):
serializer_class = FlowStageBindingSerializer serializer_class = FlowStageBindingSerializer
filterset_fields = "__all__" filterset_fields = "__all__"
search_fields = ["stage__name"] search_fields = ["stage__name"]
ordering = ["order"]

View file

@ -132,13 +132,6 @@ class PermissionDict(TypedDict):
name: str name: str
class PermissionSerializer(PassiveSerializer):
"""Permission used for consent"""
name = CharField(allow_blank=True)
id = CharField()
class ChallengeResponse(PassiveSerializer): class ChallengeResponse(PassiveSerializer):
"""Base class for all challenge responses""" """Base class for all challenge responses"""

View file

@ -0,0 +1,25 @@
# Generated by Django 4.2.6 on 2023-10-10 17:18
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="flow",
options={
"permissions": [
("export_flow", "Can export a Flow"),
("inspect_flow", "Can inspect a Flow's execution"),
("view_flow_cache", "View Flow's cache metrics"),
("clear_flow_cache", "Clear Flow's cache metrics"),
],
"verbose_name": "Flow",
"verbose_name_plural": "Flows",
},
),
]

View file

@ -194,9 +194,10 @@ class Flow(SerializerModel, PolicyBindingModel):
verbose_name_plural = _("Flows") verbose_name_plural = _("Flows")
permissions = [ permissions = [
("export_flow", "Can export a Flow"), ("export_flow", _("Can export a Flow")),
("view_flow_cache", "View Flow's cache metrics"), ("inspect_flow", _("Can inspect a Flow's execution")),
("clear_flow_cache", "Clear Flow's cache metrics"), ("view_flow_cache", _("View Flow's cache metrics")),
("clear_flow_cache", _("Clear Flow's cache metrics")),
] ]

View file

@ -3,6 +3,7 @@ from hashlib import sha256
from typing import Any from typing import Any
from django.conf import settings from django.conf import settings
from django.http import Http404
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
@ -11,7 +12,6 @@ from django.views.decorators.clickjacking import xframe_options_sameorigin
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.fields import BooleanField, ListField, SerializerMethodField from rest_framework.fields import BooleanField, ListField, SerializerMethodField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
@ -68,21 +68,19 @@ class FlowInspectionSerializer(PassiveSerializer):
class FlowInspectorView(APIView): class FlowInspectorView(APIView):
"""Flow inspector API""" """Flow inspector API"""
permission_classes = [IsAdminUser]
flow: Flow flow: Flow
_logger: BoundLogger _logger: BoundLogger
permission_classes = []
def check_permissions(self, request):
"""Always allow access when in debug mode"""
if settings.DEBUG:
return None
return super().check_permissions(request)
def setup(self, request: HttpRequest, flow_slug: str): def setup(self, request: HttpRequest, flow_slug: str):
super().setup(request, flow_slug=flow_slug) super().setup(request, flow_slug=flow_slug)
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
self._logger = get_logger().bind(flow_slug=flow_slug) self._logger = get_logger().bind(flow_slug=flow_slug)
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
if settings.DEBUG:
return
if request.user.has_perm("authentik_flow.inspect_flow", self.flow):
return
raise Http404
@extend_schema( @extend_schema(
responses={ responses={

View file

@ -0,0 +1,32 @@
"""Serializer validators"""
from typing import Optional
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import Serializer
from rest_framework.utils.representation import smart_repr
class RequiredTogetherValidator:
"""Serializer-level validator that ensures all fields in `fields` are only
used together"""
fields: list[str]
requires_context = True
message = _("The fields {field_names} must be used together.")
def __init__(self, fields: list[str], message: Optional[str] = None) -> None:
self.fields = fields
self.message = message or self.message
def __call__(self, attrs: dict, serializer: Serializer):
"""Check that if any of the fields in `self.fields` are set, all of them must be set"""
if any(field in attrs for field in self.fields) and not all(
field in attrs for field in self.fields
):
field_names = ", ".join(self.fields)
message = self.message.format(field_names=field_names)
raise ValidationError(message, code="required")
def __repr__(self):
return "<%s(fields=%s)>" % (self.__class__.__name__, smart_repr(self.fields))

View file

@ -28,4 +28,8 @@ class Migration(migrations.Migration):
verbose_name="Managed by authentik", verbose_name="Managed by authentik",
), ),
), ),
migrations.AlterModelOptions(
name="outpost",
options={"verbose_name": "Outpost", "verbose_name_plural": "Outposts"},
),
] ]

View file

@ -405,6 +405,10 @@ class Outpost(SerializerModel, ManagedModel):
def __str__(self) -> str: def __str__(self) -> str:
return f"Outpost {self.name}" return f"Outpost {self.name}"
class Meta:
verbose_name = _("Outpost")
verbose_name_plural = _("Outposts")
@dataclass @dataclass
class OutpostState: class OutpostState:

View file

@ -190,8 +190,8 @@ class Policy(SerializerModel, CreatedUpdatedModel):
verbose_name_plural = _("Policies") verbose_name_plural = _("Policies")
permissions = [ permissions = [
("view_policy_cache", "View Policy's cache metrics"), ("view_policy_cache", _("View Policy's cache metrics")),
("clear_policy_cache", "Clear Policy's cache metrics"), ("clear_policy_cache", _("Clear Policy's cache metrics")),
] ]
class PolicyMeta: class PolicyMeta:

View file

View file

130
authentik/rbac/api/rbac.py Normal file
View file

@ -0,0 +1,130 @@
"""common RBAC serializers"""
from django.apps import apps
from django.contrib.auth.models import Permission
from django.db.models import QuerySet
from django_filters.filters import ModelChoiceFilter
from django_filters.filterset import FilterSet
from rest_framework.exceptions import ValidationError
from rest_framework.fields import (
CharField,
ChoiceField,
ListField,
ReadOnlyField,
SerializerMethodField,
)
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ReadOnlyModelViewSet
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User
from authentik.lib.validators import RequiredTogetherValidator
from authentik.policies.event_matcher.models import model_choices
from authentik.rbac.models import Role
class PermissionSerializer(ModelSerializer):
"""Global permission"""
app_label = ReadOnlyField(source="content_type.app_label")
app_label_verbose = SerializerMethodField()
model = ReadOnlyField(source="content_type.model")
model_verbose = SerializerMethodField()
def get_app_label_verbose(self, instance: Permission) -> str:
"""Human-readable app label"""
return apps.get_app_config(instance.content_type.app_label).verbose_name
def get_model_verbose(self, instance: Permission) -> str:
"""Human-readable model name"""
return apps.get_model(
instance.content_type.app_label, instance.content_type.model
)._meta.verbose_name
class Meta:
model = Permission
fields = [
"id",
"name",
"codename",
"model",
"app_label",
"app_label_verbose",
"model_verbose",
]
class PermissionFilter(FilterSet):
"""Filter permissions"""
role = ModelChoiceFilter(queryset=Role.objects.all(), method="filter_role")
user = ModelChoiceFilter(queryset=User.objects.all())
def filter_role(self, queryset: QuerySet, name, value: Role) -> QuerySet:
"""Filter permissions based on role"""
return queryset.filter(group__role=value)
class Meta:
model = Permission
fields = [
"codename",
"content_type__model",
"content_type__app_label",
"role",
"user",
]
class RBACPermissionViewSet(ReadOnlyModelViewSet):
"""Read-only list of all permissions, filterable by model and app"""
queryset = Permission.objects.none()
serializer_class = PermissionSerializer
ordering = ["name"]
filterset_class = PermissionFilter
search_fields = [
"codename",
"content_type__model",
"content_type__app_label",
]
def get_queryset(self) -> QuerySet:
return (
Permission.objects.all()
.select_related("content_type")
.filter(
content_type__app_label__startswith="authentik",
)
)
class PermissionAssignSerializer(PassiveSerializer):
"""Request to assign a new permission"""
permissions = ListField(child=CharField())
model = ChoiceField(choices=model_choices(), required=False)
object_pk = CharField(required=False)
validators = [RequiredTogetherValidator(fields=["model", "object_pk"])]
def validate(self, attrs: dict) -> dict:
model_instance = None
# Check if we're setting an object-level perm or global
model = attrs.get("model")
object_pk = attrs.get("object_pk")
if model and object_pk:
model = apps.get_model(attrs["model"])
model_instance = model.objects.filter(pk=attrs["object_pk"]).first()
attrs["model_instance"] = model_instance
if attrs.get("model"):
return attrs
permissions = attrs.get("permissions", [])
if not all("." in perm for perm in permissions):
raise ValidationError(
{
"permissions": (
"When assigning global permissions, codename must be given as "
"app_label.codename"
)
}
)
return attrs

View file

@ -0,0 +1,123 @@
"""common RBAC serializers"""
from django.db.models import Q, QuerySet
from django.db.transaction import atomic
from django_filters.filters import CharFilter, ChoiceFilter
from django_filters.filterset import FilterSet
from drf_spectacular.utils import OpenApiResponse, extend_schema
from guardian.models import GroupObjectPermission
from guardian.shortcuts import assign_perm, remove_perm
from rest_framework.decorators import action
from rest_framework.fields import CharField, ReadOnlyField
from rest_framework.mixins import ListModelMixin
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.utils import PassiveSerializer
from authentik.policies.event_matcher.models import model_choices
from authentik.rbac.api.rbac import PermissionAssignSerializer
from authentik.rbac.models import Role
class RoleObjectPermissionSerializer(ModelSerializer):
"""Role-bound object level permission"""
app_label = ReadOnlyField(source="content_type.app_label")
model = ReadOnlyField(source="content_type.model")
codename = ReadOnlyField(source="permission.codename")
name = ReadOnlyField(source="permission.name")
object_pk = ReadOnlyField()
class Meta:
model = GroupObjectPermission
fields = ["id", "codename", "model", "app_label", "object_pk", "name"]
class RoleAssignedObjectPermissionSerializer(PassiveSerializer):
"""Roles assigned object permission serializer"""
role_pk = CharField(source="group.role.pk", read_only=True)
name = CharField(source="group.name", read_only=True)
permissions = RoleObjectPermissionSerializer(
many=True, source="group.groupobjectpermission_set"
)
class Meta:
model = Role
fields = ["role_pk", "name", "permissions"]
class RoleAssignedPermissionFilter(FilterSet):
"""Role Assigned permission filter"""
model = ChoiceFilter(choices=model_choices(), method="filter_model", required=True)
object_pk = CharFilter(method="filter_object_pk")
def filter_model(self, queryset: QuerySet, name, value: str) -> QuerySet:
"""Filter by object type"""
app, _, model = value.partition(".")
return queryset.filter(
Q(
group__permissions__content_type__app_label=app,
group__permissions__content_type__model=model,
)
| Q(
group__groupobjectpermission__permission__content_type__app_label=app,
group__groupobjectpermission__permission__content_type__model=model,
)
).distinct()
def filter_object_pk(self, queryset: QuerySet, name, value: str) -> QuerySet:
"""Filter by object primary key"""
return queryset.filter(Q(group__groupobjectpermission__object_pk=value)).distinct()
class RoleAssignedPermissionViewSet(ListModelMixin, GenericViewSet):
"""Get assigned object permissions for a single object"""
serializer_class = RoleAssignedObjectPermissionSerializer
ordering = ["name"]
# The filtering is done in the filterset,
# which has a required filter that does the heavy lifting
queryset = Role.objects.all()
filterset_class = RoleAssignedPermissionFilter
@permission_required("authentik_rbac.assign_role_permissions")
@extend_schema(
request=PermissionAssignSerializer(),
responses={
204: OpenApiResponse(description="Successfully assigned"),
},
)
@action(methods=["POST"], detail=True, pagination_class=None, filter_backends=[])
def assign(self, request: Request, *args, **kwargs) -> Response:
"""Assign permission(s) to role. When `object_pk` is set, the permissions
are only assigned to the specific object, otherwise they are assigned globally."""
role: Role = self.get_object()
data = PermissionAssignSerializer(data=request.data)
data.is_valid(raise_exception=True)
with atomic():
for perm in data.validated_data["permissions"]:
assign_perm(perm, role.group, data.validated_data["model_instance"])
return Response(status=204)
@permission_required("authentik_rbac.unassign_role_permissions")
@extend_schema(
request=PermissionAssignSerializer(),
responses={
204: OpenApiResponse(description="Successfully unassigned"),
},
)
@action(methods=["PATCH"], detail=True, pagination_class=None, filter_backends=[])
def unassign(self, request: Request, *args, **kwargs) -> Response:
"""Unassign permission(s) to role. When `object_pk` is set, the permissions
are only assigned to the specific object, otherwise they are assigned globally."""
role: Role = self.get_object()
data = PermissionAssignSerializer(data=request.data)
data.is_valid(raise_exception=True)
with atomic():
for perm in data.validated_data["permissions"]:
remove_perm(perm, role.group, data.validated_data["model_instance"])
return Response(status=204)

View file

@ -0,0 +1,129 @@
"""common RBAC serializers"""
from django.db.models import Q, QuerySet
from django.db.transaction import atomic
from django_filters.filters import CharFilter, ChoiceFilter
from django_filters.filterset import FilterSet
from drf_spectacular.utils import OpenApiResponse, extend_schema
from guardian.models import UserObjectPermission
from guardian.shortcuts import assign_perm, remove_perm
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import BooleanField, ReadOnlyField
from rest_framework.mixins import ListModelMixin
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.models import User, UserTypes
from authentik.policies.event_matcher.models import model_choices
from authentik.rbac.api.rbac import PermissionAssignSerializer
class UserObjectPermissionSerializer(ModelSerializer):
"""User-bound object level permission"""
app_label = ReadOnlyField(source="content_type.app_label")
model = ReadOnlyField(source="content_type.model")
codename = ReadOnlyField(source="permission.codename")
name = ReadOnlyField(source="permission.name")
object_pk = ReadOnlyField()
class Meta:
model = UserObjectPermission
fields = ["id", "codename", "model", "app_label", "object_pk", "name"]
class UserAssignedObjectPermissionSerializer(GroupMemberSerializer):
"""Users assigned object permission serializer"""
permissions = UserObjectPermissionSerializer(many=True, source="userobjectpermission_set")
is_superuser = BooleanField()
class Meta:
model = GroupMemberSerializer.Meta.model
fields = GroupMemberSerializer.Meta.fields + ["permissions", "is_superuser"]
class UserAssignedPermissionFilter(FilterSet):
"""Assigned permission filter"""
model = ChoiceFilter(choices=model_choices(), method="filter_model", required=True)
object_pk = CharFilter(method="filter_object_pk")
def filter_model(self, queryset: QuerySet, name, value: str) -> QuerySet:
"""Filter by object type"""
app, _, model = value.partition(".")
return queryset.filter(
Q(
user_permissions__content_type__app_label=app,
user_permissions__content_type__model=model,
)
| Q(
userobjectpermission__permission__content_type__app_label=app,
userobjectpermission__permission__content_type__model=model,
)
| Q(ak_groups__is_superuser=True)
).distinct()
def filter_object_pk(self, queryset: QuerySet, name, value: str) -> QuerySet:
"""Filter by object primary key"""
return queryset.filter(
Q(userobjectpermission__object_pk=value) | Q(ak_groups__is_superuser=True),
).distinct()
class UserAssignedPermissionViewSet(ListModelMixin, GenericViewSet):
"""Get assigned object permissions for a single object"""
serializer_class = UserAssignedObjectPermissionSerializer
ordering = ["username"]
# The filtering is done in the filterset,
# which has a required filter that does the heavy lifting
queryset = User.objects.all()
filterset_class = UserAssignedPermissionFilter
@permission_required("authentik_core.assign_user_permissions")
@extend_schema(
request=PermissionAssignSerializer(),
responses={
204: OpenApiResponse(description="Successfully assigned"),
},
)
@action(methods=["POST"], detail=True, pagination_class=None, filter_backends=[])
def assign(self, request: Request, *args, **kwargs) -> Response:
"""Assign permission(s) to user"""
user: User = self.get_object()
if user.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
raise ValidationError("Permissions cannot be assigned to an internal service account.")
data = PermissionAssignSerializer(data=request.data)
data.is_valid(raise_exception=True)
with atomic():
for perm in data.validated_data["permissions"]:
assign_perm(perm, user, data.validated_data["model_instance"])
return Response(status=204)
@permission_required("authentik_core.unassign_user_permissions")
@extend_schema(
request=PermissionAssignSerializer(),
responses={
204: OpenApiResponse(description="Successfully unassigned"),
},
)
@action(methods=["PATCH"], detail=True, pagination_class=None, filter_backends=[])
def unassign(self, request: Request, *args, **kwargs) -> Response:
"""Unassign permission(s) to user. When `object_pk` is set, the permissions
are only assigned to the specific object, otherwise they are assigned globally."""
user: User = self.get_object()
if user.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
raise ValidationError(
"Permissions cannot be unassigned from an internal service account."
)
data = PermissionAssignSerializer(data=request.data)
data.is_valid(raise_exception=True)
with atomic():
for perm in data.validated_data["permissions"]:
remove_perm(perm, user, data.validated_data["model_instance"])
return Response(status=204)

View file

@ -0,0 +1,71 @@
"""common RBAC serializers"""
from typing import Optional
from django.apps import apps
from django_filters.filters import UUIDFilter
from django_filters.filterset import FilterSet
from guardian.models import GroupObjectPermission
from guardian.shortcuts import get_objects_for_group
from rest_framework.fields import SerializerMethodField
from rest_framework.mixins import ListModelMixin
from rest_framework.viewsets import GenericViewSet
from authentik.api.pagination import SmallerPagination
from authentik.rbac.api.rbac_assigned_by_roles import RoleObjectPermissionSerializer
class ExtraRoleObjectPermissionSerializer(RoleObjectPermissionSerializer):
"""User permission with additional object-related data"""
app_label_verbose = SerializerMethodField()
model_verbose = SerializerMethodField()
object_description = SerializerMethodField()
def get_app_label_verbose(self, instance: GroupObjectPermission) -> str:
"""Get app label from permission's model"""
return apps.get_app_config(instance.content_type.app_label).verbose_name
def get_model_verbose(self, instance: GroupObjectPermission) -> str:
"""Get model label from permission's model"""
return apps.get_model(
instance.content_type.app_label, instance.content_type.model
)._meta.verbose_name
def get_object_description(self, instance: GroupObjectPermission) -> Optional[str]:
"""Get model description from attached model. This operation takes at least
one additional query, and the description is only shown if the user/role has the
view_ permission on the object"""
app_label = instance.content_type.app_label
model = instance.content_type.model
model_class = apps.get_model(app_label, model)
objects = get_objects_for_group(instance.group, f"{app_label}.view_{model}", model_class)
obj = objects.first()
if not obj:
return None
return str(obj)
class Meta(RoleObjectPermissionSerializer.Meta):
fields = RoleObjectPermissionSerializer.Meta.fields + [
"app_label_verbose",
"model_verbose",
"object_description",
]
class RolePermissionFilter(FilterSet):
"""Role permission filter"""
uuid = UUIDFilter("group__role__uuid", required=True)
class RolePermissionViewSet(ListModelMixin, GenericViewSet):
"""Get a role's assigned object permissions"""
serializer_class = ExtraRoleObjectPermissionSerializer
ordering = ["group__role__name"]
pagination_class = SmallerPagination
# The filtering is done in the filterset,
# which has a required filter that does the heavy lifting
queryset = GroupObjectPermission.objects.select_related("content_type", "group__role").all()
filterset_class = RolePermissionFilter

View file

@ -0,0 +1,71 @@
"""common RBAC serializers"""
from typing import Optional
from django.apps import apps
from django_filters.filters import NumberFilter
from django_filters.filterset import FilterSet
from guardian.models import UserObjectPermission
from guardian.shortcuts import get_objects_for_user
from rest_framework.fields import SerializerMethodField
from rest_framework.mixins import ListModelMixin
from rest_framework.viewsets import GenericViewSet
from authentik.api.pagination import SmallerPagination
from authentik.rbac.api.rbac_assigned_by_users import UserObjectPermissionSerializer
class ExtraUserObjectPermissionSerializer(UserObjectPermissionSerializer):
"""User permission with additional object-related data"""
app_label_verbose = SerializerMethodField()
model_verbose = SerializerMethodField()
object_description = SerializerMethodField()
def get_app_label_verbose(self, instance: UserObjectPermission) -> str:
"""Get app label from permission's model"""
return apps.get_app_config(instance.content_type.app_label).verbose_name
def get_model_verbose(self, instance: UserObjectPermission) -> str:
"""Get model label from permission's model"""
return apps.get_model(
instance.content_type.app_label, instance.content_type.model
)._meta.verbose_name
def get_object_description(self, instance: UserObjectPermission) -> Optional[str]:
"""Get model description from attached model. This operation takes at least
one additional query, and the description is only shown if the user/role has the
view_ permission on the object"""
app_label = instance.content_type.app_label
model = instance.content_type.model
model_class = apps.get_model(app_label, model)
objects = get_objects_for_user(instance.user, f"{app_label}.view_{model}", model_class)
obj = objects.first()
if not obj:
return None
return str(obj)
class Meta(UserObjectPermissionSerializer.Meta):
fields = UserObjectPermissionSerializer.Meta.fields + [
"app_label_verbose",
"model_verbose",
"object_description",
]
class UserPermissionFilter(FilterSet):
"""User-assigned permission filter"""
user_id = NumberFilter("user__id", required=True)
class UserPermissionViewSet(ListModelMixin, GenericViewSet):
"""Get a users's assigned object permissions"""
serializer_class = ExtraUserObjectPermissionSerializer
ordering = ["user__username"]
pagination_class = SmallerPagination
# The filtering is done in the filterset,
# which has a required filter that does the heavy lifting
queryset = UserObjectPermission.objects.select_related("content_type", "user").all()
filterset_class = UserPermissionFilter

View file

@ -0,0 +1,24 @@
"""RBAC Roles"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.rbac.models import Role
class RoleSerializer(ModelSerializer):
"""Role serializer"""
class Meta:
model = Role
fields = ["pk", "name"]
class RoleViewSet(UsedByMixin, ModelViewSet):
"""Role viewset"""
serializer_class = RoleSerializer
queryset = Role.objects.all()
search_fields = ["group__name"]
ordering = ["group__name"]
filterset_fields = ["group__name"]

15
authentik/rbac/apps.py Normal file
View file

@ -0,0 +1,15 @@
"""authentik rbac app config"""
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikRBACConfig(ManagedAppConfig):
"""authentik rbac app config"""
name = "authentik.rbac"
label = "authentik_rbac"
verbose_name = "authentik RBAC"
default = True
def reconcile_load_rbac_signals(self):
"""Load rbac signals"""
self.import_module("authentik.rbac.signals")

26
authentik/rbac/filters.py Normal file
View file

@ -0,0 +1,26 @@
"""RBAC API Filter"""
from django.db.models import QuerySet
from rest_framework.exceptions import PermissionDenied
from rest_framework.request import Request
from rest_framework_guardian.filters import ObjectPermissionsFilter
class ObjectFilter(ObjectPermissionsFilter):
"""Object permission filter that grants global permission higher priority than
per-object permissions"""
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
permission = self.perm_format % {
"app_label": queryset.model._meta.app_label,
"model_name": queryset.model._meta.model_name,
}
# having the global permission set on a user has higher priority than
# per-object permissions
if request.user.has_perm(permission):
return queryset
queryset = super().filter_queryset(request, queryset, view)
if not queryset.exists():
# User doesn't have direct permission to all objects
# and also no object permissions assigned (directly or via role)
raise PermissionDenied()
return queryset

View file

@ -0,0 +1,47 @@
# Generated by Django 4.2.6 on 2023-10-11 13:37
import uuid
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.CreateModel(
name="Role",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("name", models.TextField(max_length=150, unique=True)),
(
"group",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE, to="auth.group"
),
),
],
options={
"verbose_name": "Role",
"verbose_name_plural": "Roles",
"permissions": [
("assign_role_permissions", "Can assign permissions to users"),
("unassign_role_permissions", "Can unassign permissions from users"),
],
},
),
]

View file

@ -0,0 +1,33 @@
# Generated by Django 4.2.6 on 2023-10-12 15:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_rbac", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="SystemPermission",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
],
options={
"permissions": [
("view_system_info", "Can view system info"),
("view_system_tasks", "Can view system tasks"),
("run_system_tasks", "Can run system tasks"),
("access_admin_interface", "Can access admin interface"),
],
"managed": False,
"default_permissions": (),
},
),
]

View file

73
authentik/rbac/models.py Normal file
View file

@ -0,0 +1,73 @@
"""RBAC models"""
from typing import Optional
from uuid import uuid4
from django.db import models
from django.db.transaction import atomic
from django.utils.translation import gettext_lazy as _
from guardian.shortcuts import assign_perm
from rest_framework.serializers import BaseSerializer
from authentik.lib.models import SerializerModel
class Role(SerializerModel):
"""RBAC role, which can have different permissions (both global and per-object) attached
to it."""
uuid = models.UUIDField(default=uuid4, editable=False, unique=True, primary_key=True)
# Due to the way django and django-guardian work, this is somewhat of a hack.
# Django and django-guardian allow for setting permissions on users and groups, but they
# only allow for a custom user object, not a custom group object, which is why
# we have both authentik and django groups. With this model, we use the inbuilt group system
# for RBAC. This means that every Role needs a single django group that its assigned to
# which will hold all of the actual permissions
# The main advantage of that is that all the permission checking just works out of the box,
# as these permissions are checked by default by django and most other libraries that build
# on top of django
group = models.OneToOneField("auth.Group", on_delete=models.CASCADE)
# name field has the same constraints as the group model
name = models.TextField(max_length=150, unique=True)
def assign_permission(self, *perms: str, obj: Optional[models.Model] = None):
"""Assign permission to role, can handle multiple permissions,
but when assigning multiple permissions to an object the permissions
must all belong to the object given"""
with atomic():
for perm in perms:
assign_perm(perm, self.group, obj)
@property
def serializer(self) -> type[BaseSerializer]:
from authentik.rbac.api.roles import RoleSerializer
return RoleSerializer
def __str__(self) -> str:
return f"Role {self.name}"
class Meta:
verbose_name = _("Role")
verbose_name_plural = _("Roles")
permissions = [
("assign_role_permissions", _("Can assign permissions to users")),
("unassign_role_permissions", _("Can unassign permissions from users")),
]
class SystemPermission(models.Model):
"""System-wide permissions that are not related to any direct
database model"""
class Meta:
managed = False
default_permissions = ()
verbose_name = _("System permission")
verbose_name_plural = _("System permissions")
permissions = [
("view_system_info", _("Can view system info")),
("view_system_tasks", _("Can view system tasks")),
("run_system_tasks", _("Can run system tasks")),
("access_admin_interface", _("Can access admin interface")),
]

View file

@ -0,0 +1,30 @@
"""RBAC Permissions"""
from django.db.models import Model
from rest_framework.permissions import BasePermission, DjangoObjectPermissions
from rest_framework.request import Request
class ObjectPermissions(DjangoObjectPermissions):
"""RBAC Permissions"""
def has_object_permission(self, request: Request, view, obj: Model):
queryset = self._queryset(view)
model_cls = queryset.model
perms = self.get_required_object_permissions(request.method, model_cls)
# Rank global permissions higher than per-object permissions
if request.user.has_perms(perms):
return True
return super().has_object_permission(request, view, obj)
# pylint: disable=invalid-name
def HasPermission(*perm: str) -> type[BasePermission]:
"""Permission checker for any non-object permissions, returns
a BasePermission class that can be used with rest_framework"""
# pylint: disable=missing-class-docstring, invalid-name
class checker(BasePermission):
def has_permission(self, request: Request, view):
return bool(request.user and request.user.has_perms(perm))
return checker

67
authentik/rbac/signals.py Normal file
View file

@ -0,0 +1,67 @@
"""rbac signals"""
from django.contrib.auth.models import Group as DjangoGroup
from django.db.models.signals import m2m_changed, pre_save
from django.db.transaction import atomic
from django.dispatch import receiver
from structlog.stdlib import get_logger
from authentik.core.models import Group
from authentik.rbac.models import Role
LOGGER = get_logger()
@receiver(pre_save, sender=Role)
def rbac_role_pre_save(sender: type[Role], instance: Role, **_):
"""Ensure role has a group object created for it"""
if hasattr(instance, "group"):
return
group, _ = DjangoGroup.objects.get_or_create(name=instance.name)
instance.group = group
@receiver(m2m_changed, sender=Group.roles.through)
def rbac_group_role_m2m(sender: type[Group], action: str, instance: Group, reverse: bool, **_):
"""RBAC: Sync group members into roles when roles are assigned"""
if action not in ["post_add", "post_remove", "post_clear"]:
return
with atomic():
group_users = list(
instance.children_recursive()
.exclude(users__isnull=True)
.values_list("users", flat=True)
)
if not group_users:
return
for role in instance.roles.all():
role: Role
role.group.user_set.set(group_users)
LOGGER.debug("Updated users in group", group=instance)
# pylint: disable=no-member
@receiver(m2m_changed, sender=Group.users.through)
def rbac_group_users_m2m(
sender: type[Group], action: str, instance: Group, pk_set: set, reverse: bool, **_
):
"""Handle Group/User m2m and mirror it to roles"""
if action not in ["post_add", "post_remove"]:
return
# reverse: instance is a Group, pk_set is a list of user pks
# non-reverse: instance is a User, pk_set is a list of groups
with atomic():
if reverse:
for role in instance.roles.all():
role: Role
if action == "post_add":
role.group.user_set.add(*pk_set)
elif action == "post_remove":
role.group.user_set.remove(*pk_set)
else:
for group in Group.objects.filter(pk__in=pk_set):
for role in group.roles.all():
role: Role
if action == "post_add":
role.group.user_set.add(instance)
elif action == "post_remove":
role.group.user_set.remove(instance)

View file

View file

@ -0,0 +1,151 @@
"""Test RoleAssignedPermissionViewSet api"""
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Group
from authentik.core.tests.utils import create_test_admin_user, create_test_user
from authentik.lib.generators import generate_id
from authentik.rbac.api.rbac_assigned_by_roles import RoleAssignedObjectPermissionSerializer
from authentik.rbac.models import Role
from authentik.stages.invitation.models import Invitation
class TestRBACRoleAPI(APITestCase):
"""Test RoleAssignedPermissionViewSet api"""
def setUp(self) -> None:
self.superuser = create_test_admin_user()
self.user = create_test_user()
self.role = Role.objects.create(name=generate_id())
self.group = Group.objects.create(name=generate_id())
self.group.roles.add(self.role)
self.group.users.add(self.user)
def test_filter_assigned(self):
"""Test RoleAssignedPermissionViewSet's filters"""
inv = Invitation.objects.create(
name=generate_id(),
created_by=self.superuser,
)
self.role.assign_permission("authentik_stages_invitation.view_invitation", obj=inv)
# self.user doesn't have permissions to see their (object) permissions
self.client.force_login(self.superuser)
res = self.client.get(
reverse("authentik_api:permissions-assigned-by-roles-list"),
{
"model": "authentik_stages_invitation.invitation",
"object_pk": str(inv.pk),
"ordering": "pk",
},
)
self.assertEqual(res.status_code, 200)
self.assertJSONEqual(
res.content.decode(),
{
"pagination": {
"next": 0,
"previous": 0,
"count": 1,
"current": 1,
"total_pages": 1,
"start_index": 1,
"end_index": 1,
},
"results": [
RoleAssignedObjectPermissionSerializer(instance=self.role).data,
],
},
)
def test_assign_global(self):
"""Test permission assign"""
self.client.force_login(self.superuser)
res = self.client.post(
reverse(
"authentik_api:permissions-assigned-by-roles-assign",
kwargs={
"pk": self.role.pk,
},
),
{
"permissions": ["authentik_stages_invitation.view_invitation"],
},
)
self.assertEqual(res.status_code, 204)
self.assertTrue(self.user.has_perm("authentik_stages_invitation.view_invitation"))
def test_assign_object(self):
"""Test permission assign (object)"""
inv = Invitation.objects.create(
name=generate_id(),
created_by=self.superuser,
)
self.client.force_login(self.superuser)
res = self.client.post(
reverse(
"authentik_api:permissions-assigned-by-roles-assign",
kwargs={
"pk": self.role.pk,
},
),
{
"permissions": ["authentik_stages_invitation.view_invitation"],
"model": "authentik_stages_invitation.invitation",
"object_pk": str(inv.pk),
},
)
self.assertEqual(res.status_code, 204)
self.assertTrue(
self.user.has_perm(
"authentik_stages_invitation.view_invitation",
inv,
)
)
def test_unassign_global(self):
"""Test permission unassign"""
self.role.assign_permission("authentik_stages_invitation.view_invitation")
self.client.force_login(self.superuser)
res = self.client.patch(
reverse(
"authentik_api:permissions-assigned-by-roles-unassign",
kwargs={
"pk": str(self.role.pk),
},
),
{
"permissions": ["authentik_stages_invitation.view_invitation"],
},
)
self.assertEqual(res.status_code, 204)
self.assertFalse(self.user.has_perm("authentik_stages_invitation.view_invitation"))
def test_unassign_object(self):
"""Test permission unassign (object)"""
inv = Invitation.objects.create(
name=generate_id(),
created_by=self.superuser,
)
self.role.assign_permission("authentik_stages_invitation.view_invitation", obj=inv)
self.client.force_login(self.superuser)
res = self.client.patch(
reverse(
"authentik_api:permissions-assigned-by-roles-unassign",
kwargs={
"pk": str(self.role.pk),
},
),
{
"permissions": ["authentik_stages_invitation.view_invitation"],
"model": "authentik_stages_invitation.invitation",
"object_pk": str(inv.pk),
},
)
self.assertEqual(res.status_code, 204)
self.assertFalse(
self.user.has_perm(
"authentik_stages_invitation.view_invitation",
inv,
)
)

View file

@ -0,0 +1,196 @@
"""Test UserAssignedPermissionViewSet api"""
from django.urls import reverse
from guardian.shortcuts import assign_perm
from rest_framework.test import APITestCase
from authentik.core.models import Group, UserTypes
from authentik.core.tests.utils import create_test_admin_user, create_test_user
from authentik.lib.generators import generate_id
from authentik.rbac.api.rbac_assigned_by_users import UserAssignedObjectPermissionSerializer
from authentik.rbac.models import Role
from authentik.stages.invitation.models import Invitation
class TestRBACUserAPI(APITestCase):
"""Test UserAssignedPermissionViewSet api"""
def setUp(self) -> None:
self.superuser = create_test_admin_user()
self.user = create_test_user()
self.role = Role.objects.create(name=generate_id())
self.group = Group.objects.create(name=generate_id())
self.group.roles.add(self.role)
self.group.users.add(self.user)
def test_filter_assigned(self):
"""Test UserAssignedPermissionViewSet's filters"""
inv = Invitation.objects.create(
name=generate_id(),
created_by=self.superuser,
)
assign_perm("authentik_stages_invitation.view_invitation", self.user, inv)
# self.user doesn't have permissions to see their (object) permissions
self.client.force_login(self.superuser)
res = self.client.get(
reverse("authentik_api:permissions-assigned-by-users-list"),
{
"model": "authentik_stages_invitation.invitation",
"object_pk": str(inv.pk),
"ordering": "pk",
},
)
self.assertEqual(res.status_code, 200)
self.assertJSONEqual(
res.content.decode(),
{
"pagination": {
"next": 0,
"previous": 0,
"count": 2,
"current": 1,
"total_pages": 1,
"start_index": 1,
"end_index": 2,
},
"results": sorted(
[
UserAssignedObjectPermissionSerializer(instance=self.user).data,
UserAssignedObjectPermissionSerializer(instance=self.superuser).data,
],
key=lambda u: u["pk"],
),
},
)
def test_assign_global(self):
"""Test permission assign"""
self.client.force_login(self.superuser)
res = self.client.post(
reverse(
"authentik_api:permissions-assigned-by-users-assign",
kwargs={
"pk": self.user.pk,
},
),
{
"permissions": ["authentik_stages_invitation.view_invitation"],
},
)
self.assertEqual(res.status_code, 204)
self.assertTrue(self.user.has_perm("authentik_stages_invitation.view_invitation"))
def test_assign_global_internal_sa(self):
"""Test permission assign (to internal service account)"""
self.client.force_login(self.superuser)
self.user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
self.user.save()
res = self.client.post(
reverse(
"authentik_api:permissions-assigned-by-users-assign",
kwargs={
"pk": self.user.pk,
},
),
{
"permissions": ["authentik_stages_invitation.view_invitation"],
},
)
self.assertEqual(res.status_code, 400)
self.assertFalse(self.user.has_perm("authentik_stages_invitation.view_invitation"))
def test_assign_object(self):
"""Test permission assign (object)"""
inv = Invitation.objects.create(
name=generate_id(),
created_by=self.superuser,
)
self.client.force_login(self.superuser)
res = self.client.post(
reverse(
"authentik_api:permissions-assigned-by-users-assign",
kwargs={
"pk": self.user.pk,
},
),
{
"permissions": ["authentik_stages_invitation.view_invitation"],
"model": "authentik_stages_invitation.invitation",
"object_pk": str(inv.pk),
},
)
self.assertEqual(res.status_code, 204)
self.assertTrue(
self.user.has_perm(
"authentik_stages_invitation.view_invitation",
inv,
)
)
def test_unassign_global(self):
"""Test permission unassign"""
assign_perm("authentik_stages_invitation.view_invitation", self.user)
self.client.force_login(self.superuser)
res = self.client.patch(
reverse(
"authentik_api:permissions-assigned-by-users-unassign",
kwargs={
"pk": self.user.pk,
},
),
{
"permissions": ["authentik_stages_invitation.view_invitation"],
},
)
self.assertEqual(res.status_code, 204)
self.assertFalse(self.user.has_perm("authentik_stages_invitation.view_invitation"))
def test_unassign_global_internal_sa(self):
"""Test permission unassign (from internal service account)"""
self.client.force_login(self.superuser)
self.user.type = UserTypes.INTERNAL_SERVICE_ACCOUNT
self.user.save()
assign_perm("authentik_stages_invitation.view_invitation", self.user)
self.client.force_login(self.superuser)
res = self.client.patch(
reverse(
"authentik_api:permissions-assigned-by-users-unassign",
kwargs={
"pk": self.user.pk,
},
),
{
"permissions": ["authentik_stages_invitation.view_invitation"],
},
)
self.assertEqual(res.status_code, 400)
self.assertTrue(self.user.has_perm("authentik_stages_invitation.view_invitation"))
def test_unassign_object(self):
"""Test permission unassign (object)"""
inv = Invitation.objects.create(
name=generate_id(),
created_by=self.superuser,
)
assign_perm("authentik_stages_invitation.view_invitation", self.user, inv)
self.client.force_login(self.superuser)
res = self.client.patch(
reverse(
"authentik_api:permissions-assigned-by-users-unassign",
kwargs={
"pk": self.user.pk,
},
),
{
"permissions": ["authentik_stages_invitation.view_invitation"],
"model": "authentik_stages_invitation.invitation",
"object_pk": str(inv.pk),
},
)
self.assertEqual(res.status_code, 204)
self.assertFalse(
self.user.has_perm(
"authentik_stages_invitation.view_invitation",
inv,
)
)

View file

@ -0,0 +1,122 @@
"""RBAC role tests"""
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Group
from authentik.core.tests.utils import create_test_admin_user, create_test_user
from authentik.lib.generators import generate_id
from authentik.rbac.models import Role
from authentik.stages.invitation.api import InvitationSerializer
from authentik.stages.invitation.models import Invitation
class TestAPIPerms(APITestCase):
"""Test API Permission and filtering"""
def setUp(self) -> None:
self.superuser = create_test_admin_user()
self.user = create_test_user()
self.role = Role.objects.create(name=generate_id())
self.group = Group.objects.create(name=generate_id())
self.group.roles.add(self.role)
self.group.users.add(self.user)
def test_list_simple(self):
"""Test list (single object, role has global permission)"""
self.client.force_login(self.user)
self.role.assign_permission("authentik_stages_invitation.view_invitation")
Invitation.objects.all().delete()
inv = Invitation.objects.create(
name=generate_id(),
created_by=self.superuser,
)
res = self.client.get(reverse("authentik_api:invitation-list"))
self.assertEqual(res.status_code, 200)
self.assertJSONEqual(
res.content.decode(),
{
"pagination": {
"next": 0,
"previous": 0,
"count": 1,
"current": 1,
"total_pages": 1,
"start_index": 1,
"end_index": 1,
},
"results": [
InvitationSerializer(instance=inv).data,
],
},
)
def test_list_object_perm(self):
"""Test list"""
self.client.force_login(self.user)
Invitation.objects.all().delete()
Invitation.objects.create(
name=generate_id(),
created_by=self.superuser,
)
inv2 = Invitation.objects.create(
name=generate_id(),
created_by=self.superuser,
)
self.role.assign_permission("authentik_stages_invitation.view_invitation", obj=inv2)
res = self.client.get(reverse("authentik_api:invitation-list"))
self.assertEqual(res.status_code, 200)
self.assertJSONEqual(
res.content.decode(),
{
"pagination": {
"next": 0,
"previous": 0,
"count": 1,
"current": 1,
"total_pages": 1,
"start_index": 1,
"end_index": 1,
},
"results": [
InvitationSerializer(instance=inv2).data,
],
},
)
def test_list_denied(self):
"""Test list without adding permission"""
self.client.force_login(self.user)
res = self.client.get(reverse("authentik_api:invitation-list"))
self.assertEqual(res.status_code, 403)
self.assertJSONEqual(
res.content.decode(),
{"detail": "You do not have permission to perform this action."},
)
def test_create_simple(self):
"""Test create with permission"""
self.client.force_login(self.user)
self.role.assign_permission("authentik_stages_invitation.add_invitation")
res = self.client.post(
reverse("authentik_api:invitation-list"),
data={
"name": generate_id(),
},
)
self.assertEqual(res.status_code, 201)
def test_create_simple_denied(self):
"""Test create without assigning permission"""
self.client.force_login(self.user)
res = self.client.post(
reverse("authentik_api:invitation-list"),
data={
"name": generate_id(),
},
)
self.assertEqual(res.status_code, 403)

View file

@ -0,0 +1,35 @@
"""RBAC role tests"""
from rest_framework.test import APITestCase
from authentik.core.models import Group
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
from authentik.rbac.models import Role
class TestRoles(APITestCase):
"""Test roles"""
def test_role_create(self):
"""Test creation"""
user = create_test_admin_user()
group = Group.objects.create(name=generate_id())
role = Role.objects.create(name=generate_id())
role.assign_permission("authentik_core.view_application")
group.roles.add(role)
group.users.add(user)
self.assertEqual(list(role.group.user_set.all()), [user])
self.assertTrue(user.has_perm("authentik_core.view_application"))
def test_role_create_remove(self):
"""Test creation and remove"""
user = create_test_admin_user()
group = Group.objects.create(name=generate_id())
role = Role.objects.create(name=generate_id())
role.assign_permission("authentik_core.view_application")
group.roles.add(role)
group.users.add(user)
self.assertEqual(list(role.group.user_set.all()), [user])
self.assertTrue(user.has_perm("authentik_core.view_application"))
user.delete()
self.assertEqual(list(role.group.user_set.all()), [])

24
authentik/rbac/urls.py Normal file
View file

@ -0,0 +1,24 @@
"""RBAC API urls"""
from authentik.rbac.api.rbac import RBACPermissionViewSet
from authentik.rbac.api.rbac_assigned_by_roles import RoleAssignedPermissionViewSet
from authentik.rbac.api.rbac_assigned_by_users import UserAssignedPermissionViewSet
from authentik.rbac.api.rbac_roles import RolePermissionViewSet
from authentik.rbac.api.rbac_users import UserPermissionViewSet
from authentik.rbac.api.roles import RoleViewSet
api_urlpatterns = [
(
"rbac/permissions/assigned_by_users",
UserAssignedPermissionViewSet,
"permissions-assigned-by-users",
),
(
"rbac/permissions/assigned_by_roles",
RoleAssignedPermissionViewSet,
"permissions-assigned-by-roles",
),
("rbac/permissions/users", UserPermissionViewSet, "permissions-users"),
("rbac/permissions/roles", RolePermissionViewSet, "permissions-roles"),
("rbac/permissions", RBACPermissionViewSet),
("rbac/roles", RoleViewSet),
]

View file

@ -77,6 +77,7 @@ INSTALLED_APPS = [
"authentik.providers.radius", "authentik.providers.radius",
"authentik.providers.saml", "authentik.providers.saml",
"authentik.providers.scim", "authentik.providers.scim",
"authentik.rbac",
"authentik.recovery", "authentik.recovery",
"authentik.sources.ldap", "authentik.sources.ldap",
"authentik.sources.oauth", "authentik.sources.oauth",
@ -156,7 +157,7 @@ REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "authentik.api.pagination.Pagination", "DEFAULT_PAGINATION_CLASS": "authentik.api.pagination.Pagination",
"PAGE_SIZE": 100, "PAGE_SIZE": 100,
"DEFAULT_FILTER_BACKENDS": [ "DEFAULT_FILTER_BACKENDS": [
"rest_framework_guardian.filters.ObjectPermissionsFilter", "authentik.rbac.filters.ObjectFilter",
"django_filters.rest_framework.DjangoFilterBackend", "django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.OrderingFilter", "rest_framework.filters.OrderingFilter",
"rest_framework.filters.SearchFilter", "rest_framework.filters.SearchFilter",
@ -164,7 +165,7 @@ REST_FRAMEWORK = {
"DEFAULT_PARSER_CLASSES": [ "DEFAULT_PARSER_CLASSES": [
"rest_framework.parsers.JSONParser", "rest_framework.parsers.JSONParser",
], ],
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.DjangoObjectPermissions",), "DEFAULT_PERMISSION_CLASSES": ("authentik.rbac.permissions.ObjectPermissions",),
"DEFAULT_AUTHENTICATION_CLASSES": ( "DEFAULT_AUTHENTICATION_CLASSES": (
"authentik.api.authentication.TokenAuthentication", "authentik.api.authentication.TokenAuthentication",
"rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.SessionAuthentication",
@ -410,6 +411,9 @@ if DEBUG:
INSTALLED_APPS.append("silk") INSTALLED_APPS.append("silk")
SILKY_PYTHON_PROFILER = True SILKY_PYTHON_PROFILER = True
MIDDLEWARE = ["silk.middleware.SilkyMiddleware"] + MIDDLEWARE MIDDLEWARE = ["silk.middleware.SilkyMiddleware"] + MIDDLEWARE
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"].append(
"rest_framework.renderers.BrowsableAPIRenderer"
)
INSTALLED_APPS.append("authentik.core") INSTALLED_APPS.append("authentik.core")

View file

@ -30,4 +30,12 @@ class Migration(migrations.Migration):
name="staticdevice", name="staticdevice",
options={"verbose_name": "Static device", "verbose_name_plural": "Static devices"}, options={"verbose_name": "Static device", "verbose_name_plural": "Static devices"},
), ),
migrations.AlterModelOptions(
name="staticdevice",
options={"verbose_name": "Static Device", "verbose_name_plural": "Static Devices"},
),
migrations.AlterModelOptions(
name="statictoken",
options={"verbose_name": "Static Token", "verbose_name_plural": "Static Tokens"},
),
] ]

View file

@ -95,8 +95,8 @@ class StaticDevice(SerializerModel, ThrottlingMixin, Device):
return match is not None return match is not None
class Meta(Device.Meta): class Meta(Device.Meta):
verbose_name = _("Static device") verbose_name = _("Static Device")
verbose_name_plural = _("Static devices") verbose_name_plural = _("Static Devices")
class StaticToken(models.Model): class StaticToken(models.Model):
@ -124,3 +124,7 @@ class StaticToken(models.Model):
""" """
return b32encode(urandom(5)).decode("utf-8").lower() return b32encode(urandom(5)).decode("utf-8").lower()
class Meta:
verbose_name = _("Static Token")
verbose_name_plural = _("Static Tokens")

View file

@ -25,4 +25,8 @@ class Migration(migrations.Migration):
name="totpdevice", name="totpdevice",
options={"verbose_name": "TOTP device", "verbose_name_plural": "TOTP devices"}, options={"verbose_name": "TOTP device", "verbose_name_plural": "TOTP devices"},
), ),
migrations.AlterModelOptions(
name="totpdevice",
options={"verbose_name": "TOTP Device", "verbose_name_plural": "TOTP Devices"},
),
] ]

View file

@ -241,5 +241,5 @@ class TOTPDevice(SerializerModel, ThrottlingMixin, Device):
return None return None
class Meta(Device.Meta): class Meta(Device.Meta):
verbose_name = _("TOTP device") verbose_name = _("TOTP Device")
verbose_name_plural = _("TOTP devices") verbose_name_plural = _("TOTP Devices")

View file

@ -6,11 +6,11 @@ from django.http import HttpRequest, HttpResponse
from django.utils.timezone import now from django.utils.timezone import now
from rest_framework.fields import CharField from rest_framework.fields import CharField
from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import ( from authentik.flows.challenge import (
Challenge, Challenge,
ChallengeResponse, ChallengeResponse,
ChallengeTypes, ChallengeTypes,
PermissionSerializer,
WithUserInfoChallenge, WithUserInfoChallenge,
) )
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER
@ -25,12 +25,19 @@ PLAN_CONTEXT_CONSENT_EXTRA_PERMISSIONS = "consent_additional_permissions"
SESSION_KEY_CONSENT_TOKEN = "authentik/stages/consent/token" # nosec SESSION_KEY_CONSENT_TOKEN = "authentik/stages/consent/token" # nosec
class ConsentPermissionSerializer(PassiveSerializer):
"""Permission used for consent"""
name = CharField(allow_blank=True)
id = CharField()
class ConsentChallenge(WithUserInfoChallenge): class ConsentChallenge(WithUserInfoChallenge):
"""Challenge info for consent screens""" """Challenge info for consent screens"""
header_text = CharField(required=False) header_text = CharField(required=False)
permissions = PermissionSerializer(many=True) permissions = ConsentPermissionSerializer(many=True)
additional_permissions = PermissionSerializer(many=True) additional_permissions = ConsentPermissionSerializer(many=True)
component = CharField(default="ak-stage-consent") component = CharField(default="ak-stage-consent")
token = CharField(required=True) token = CharField(required=True)

View file

@ -71,6 +71,7 @@ class PromptViewSet(UsedByMixin, ModelViewSet):
queryset = Prompt.objects.all().prefetch_related("promptstage_set") queryset = Prompt.objects.all().prefetch_related("promptstage_set")
serializer_class = PromptSerializer serializer_class = PromptSerializer
ordering = ["field_key"]
filterset_fields = ["field_key", "name", "label", "type", "placeholder"] filterset_fields = ["field_key", "name", "label", "type", "placeholder"]
search_fields = ["field_key", "name", "label", "type", "placeholder"] search_fields = ["field_key", "name", "label", "type", "placeholder"]

View file

@ -1188,6 +1188,43 @@
} }
} }
}, },
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_rbac.role"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"attrs": {
"$ref": "#/$defs/model_authentik_rbac.role"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_rbac.role"
}
}
},
{ {
"type": "object", "type": "object",
"required": [ "required": [
@ -2705,6 +2742,43 @@
} }
} }
}, },
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_enterprise.license"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"attrs": {
"$ref": "#/$defs/model_authentik_enterprise.license"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_enterprise.license"
}
}
},
{ {
"type": "object", "type": "object",
"required": [ "required": [
@ -3372,6 +3446,7 @@
"authentik.providers.radius", "authentik.providers.radius",
"authentik.providers.saml", "authentik.providers.saml",
"authentik.providers.scim", "authentik.providers.scim",
"authentik.rbac",
"authentik.recovery", "authentik.recovery",
"authentik.sources.ldap", "authentik.sources.ldap",
"authentik.sources.oauth", "authentik.sources.oauth",
@ -3443,6 +3518,7 @@
"authentik_providers_saml.samlpropertymapping", "authentik_providers_saml.samlpropertymapping",
"authentik_providers_scim.scimprovider", "authentik_providers_scim.scimprovider",
"authentik_providers_scim.scimmapping", "authentik_providers_scim.scimmapping",
"authentik_rbac.role",
"authentik_sources_ldap.ldapsource", "authentik_sources_ldap.ldapsource",
"authentik_sources_ldap.ldappropertymapping", "authentik_sources_ldap.ldappropertymapping",
"authentik_sources_oauth.oauthsource", "authentik_sources_oauth.oauthsource",
@ -3483,7 +3559,8 @@
"authentik_core.group", "authentik_core.group",
"authentik_core.user", "authentik_core.user",
"authentik_core.application", "authentik_core.application",
"authentik_core.token" "authentik_core.token",
"authentik_enterprise.license"
], ],
"title": "Model", "title": "Model",
"description": "Match events created by selected model. When left empty, all models are matched. When an app is selected, all the application's models are matched." "description": "Match events created by selected model. When left empty, all models are matched. When an app is selected, all the application's models are matched."
@ -4944,6 +5021,18 @@
}, },
"required": [] "required": []
}, },
"model_authentik_rbac.role": {
"type": "object",
"properties": {
"name": {
"type": "string",
"maxLength": 150,
"minLength": 1,
"title": "Name"
}
},
"required": []
},
"model_authentik_sources_ldap.ldapsource": { "model_authentik_sources_ldap.ldapsource": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -8405,6 +8494,13 @@
"type": "object", "type": "object",
"additionalProperties": true, "additionalProperties": true,
"title": "Attributes" "title": "Attributes"
},
"roles": {
"type": "array",
"items": {
"type": "integer"
},
"title": "Roles"
} }
}, },
"required": [] "required": []
@ -8599,6 +8695,17 @@
}, },
"required": [] "required": []
}, },
"model_authentik_enterprise.license": {
"type": "object",
"properties": {
"key": {
"type": "string",
"minLength": 1,
"title": "Key"
}
},
"required": []
},
"model_authentik_blueprints.metaapplyblueprint": { "model_authentik_blueprints.metaapplyblueprint": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -162,7 +162,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
for _, u := range g.UsersObj { for _, u := range g.UsersObj {
if flag.UserPk == u.Pk { if flag.UserPk == u.Pk {
//TODO: Is there a better way to clone this object? //TODO: Is there a better way to clone this object?
fg := api.NewGroup(g.Pk, g.NumPk, g.Name, g.ParentName, []api.GroupMember{u}) fg := api.NewGroup(g.Pk, g.NumPk, g.Name, g.ParentName, []api.GroupMember{u}, []api.Role{})
fg.SetUsers([]int32{flag.UserPk}) fg.SetUsers([]int32{flag.UserPk})
if g.Parent.IsSet() { if g.Parent.IsSet() {
fg.SetParent(*g.Parent.Get()) fg.SetParent(*g.Parent.Get())

1624
schema.yml

File diff suppressed because it is too large Load diff

View file

@ -116,7 +116,11 @@ export class AdminInterface extends Interface {
configureSentry(true); configureSentry(true);
this.version = await new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve(); this.version = await new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve();
this.user = await me(); this.user = await me();
if (!this.user.user.isSuperuser && this.user.user.pk > 0) { const canAccessAdmin =
this.user.user.isSuperuser ||
// TODO: somehow add `access_admin_interface` to the API schema
this.user.user.systemPermissions.includes("access_admin_interface");
if (!canAccessAdmin && this.user.user.pk > 0) {
window.location.assign("/if/user"); window.location.assign("/if/user");
} }
} }
@ -211,6 +215,7 @@ export class AdminInterface extends Interface {
[null, msg("Directory"), null, [ [null, msg("Directory"), null, [
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]], ["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]], ["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]], ["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`]],
["/core/tokens", msg("Tokens and App passwords")], ["/core/tokens", msg("Tokens and App passwords")],
["/flow/stages/invitations", msg("Invitations")]]], ["/flow/stages/invitations", msg("Invitations")]]],

View file

@ -80,6 +80,14 @@ export const ROUTES: Route[] = [
await import("@goauthentik/admin/users/UserViewPage"); await import("@goauthentik/admin/users/UserViewPage");
return html`<ak-user-view .userId=${parseInt(args.id, 10)}></ak-user-view>`; return html`<ak-user-view .userId=${parseInt(args.id, 10)}></ak-user-view>`;
}), }),
new Route(new RegExp("^/identity/roles$"), async () => {
await import("@goauthentik/admin/roles/RoleListPage");
return html`<ak-role-list></ak-role-list>`;
}),
new Route(new RegExp(`^/identity/roles/(?<id>${UUID_REGEX})$`), async (args) => {
await import("@goauthentik/admin/roles/RoleViewPage");
return html`<ak-role-view roleId=${args.id}></ak-role-view>`;
}),
new Route(new RegExp("^/flow/stages/invitations$"), async () => { new Route(new RegExp("^/flow/stages/invitations$"), async () => {
await import("@goauthentik/admin/stages/invitation/InvitationListPage"); await import("@goauthentik/admin/stages/invitation/InvitationListPage");
return html`<ak-stage-invitation-list></ak-stage-invitation-list>`; return html`<ak-stage-invitation-list></ak-stage-invitation-list>`;

View file

@ -2,9 +2,12 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { PFSize } from "@goauthentik/elements/Spinner"; import { PFSize } from "@goauthentik/elements/Spinner";
import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard"; import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { until } from "lit/directives/until.js"; import { until } from "lit/directives/until.js";
import { ResponseError } from "@goauthentik/api";
export interface AdminStatus { export interface AdminStatus {
icon: string; icon: string;
message?: TemplateResult; message?: TemplateResult;
@ -41,6 +44,12 @@ export abstract class AdminStatusCard<T> extends AggregateCard {
${status.message ${status.message
? html`<p class="subtext">${status.message}</p>` ? html`<p class="subtext">${status.message}</p>`
: html``}`; : html``}`;
})
.catch((exc: ResponseError) => {
return html` <p>
<i class="fa fa-times"></i>&nbsp;${exc.response.statusText}
</p>
<p class="subtext">${msg("Failed to fetch")}</p>`;
}), }),
html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`, html`<ak-spinner size="${PFSize.Large}"></ak-spinner>`,
)} )}

View file

@ -3,6 +3,7 @@ import "@goauthentik/admin/applications/ApplicationCheckAccessForm";
import "@goauthentik/admin/applications/ApplicationForm"; import "@goauthentik/admin/applications/ApplicationForm";
import "@goauthentik/admin/policies/BoundPoliciesList"; import "@goauthentik/admin/policies/BoundPoliciesList";
import { PFSize } from "@goauthentik/app/elements/Spinner"; import { PFSize } from "@goauthentik/app/elements/Spinner";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-app-icon"; import "@goauthentik/components/ak-app-icon";
import "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
@ -27,7 +28,12 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { Application, CoreApi, OutpostsApi } from "@goauthentik/api"; import {
Application,
CoreApi,
OutpostsApi,
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api";
@customElement("ak-application-view") @customElement("ak-application-view")
export class ApplicationViewPage extends AKElement { export class ApplicationViewPage extends AKElement {
@ -299,6 +305,12 @@ export class ApplicationViewPage extends AKElement {
</ak-bound-policies-list> </ak-bound-policies-list>
</div> </div>
</section> </section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.CoreApplication}
objectPk=${this.application.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`; </ak-tabs>`;
} }
} }

View file

@ -7,6 +7,7 @@ import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/SpinnerButton"; import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/rbac/ObjectPermissionModal";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage"; import { TablePage } from "@goauthentik/elements/table/TablePage";
@ -18,7 +19,12 @@ import { customElement, property } from "lit/decorators.js";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import { BlueprintInstance, BlueprintInstanceStatusEnum, ManagedApi } from "@goauthentik/api"; import {
BlueprintInstance,
BlueprintInstanceStatusEnum,
ManagedApi,
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api";
export function BlueprintStatus(blueprint?: BlueprintInstance): string { export function BlueprintStatus(blueprint?: BlueprintInstance): string {
if (!blueprint) return ""; if (!blueprint) return "";
@ -151,6 +157,11 @@ export class BlueprintListPage extends TablePage<BlueprintInstance> {
</pf-tooltip> </pf-tooltip>
</button> </button>
</ak-forms-modal> </ak-forms-modal>
<ak-rbac-object-permission-modal
model=${RbacPermissionsAssignedByUsersListModelEnum.BlueprintsBlueprintinstance}
objectPk=${item.pk}
>
</ak-rbac-object-permission-modal>
<ak-action-button <ak-action-button
class="pf-m-plain" class="pf-m-plain"
.apiRequest=${() => { .apiRequest=${() => {

View file

@ -6,6 +6,7 @@ import { PFColor } from "@goauthentik/elements/Label";
import "@goauthentik/elements/buttons/SpinnerButton"; import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/rbac/ObjectPermissionModal";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage"; import { TablePage } from "@goauthentik/elements/table/TablePage";
@ -17,7 +18,11 @@ import { customElement, property } from "lit/decorators.js";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import { CertificateKeyPair, CryptoApi } from "@goauthentik/api"; import {
CertificateKeyPair,
CryptoApi,
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api";
@customElement("ak-crypto-certificate-list") @customElement("ak-crypto-certificate-list")
export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> { export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
@ -119,16 +124,21 @@ export class CertificateKeyPairListPage extends TablePage<CertificateKeyPair> {
</ak-label>`, </ak-label>`,
html`<ak-label color=${color}> ${item.certExpiry?.toLocaleString()} </ak-label>`, html`<ak-label color=${color}> ${item.certExpiry?.toLocaleString()} </ak-label>`,
html`<ak-forms-modal> html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span> <span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Certificate-Key Pair")} </span> <span slot="header"> ${msg("Update Certificate-Key Pair")} </span>
<ak-crypto-certificate-form slot="form" .instancePk=${item.pk}> <ak-crypto-certificate-form slot="form" .instancePk=${item.pk}>
</ak-crypto-certificate-form> </ak-crypto-certificate-form>
<button slot="trigger" class="pf-c-button pf-m-plain"> <button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}> <pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</pf-tooltip> </pf-tooltip>
</button> </button>
</ak-forms-modal>`, </ak-forms-modal>
<ak-rbac-object-permission-modal
model=${RbacPermissionsAssignedByUsersListModelEnum.CryptoCertificatekeypair}
objectPk=${item.pk}
>
</ak-rbac-object-permission-modal>`,
]; ];
} }

View file

@ -7,6 +7,7 @@ import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/cards/AggregateCard"; import "@goauthentik/elements/cards/AggregateCard";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/rbac/ObjectPermissionModal";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage"; import { TablePage } from "@goauthentik/elements/table/TablePage";
@ -23,7 +24,13 @@ import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css"; import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import { EnterpriseApi, License, LicenseForecast, LicenseSummary } from "@goauthentik/api"; import {
EnterpriseApi,
License,
LicenseForecast,
LicenseSummary,
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api";
@customElement("ak-enterprise-license-list") @customElement("ak-enterprise-license-list")
export class EnterpriseLicenseListPage extends TablePage<License> { export class EnterpriseLicenseListPage extends TablePage<License> {
@ -221,16 +228,21 @@ export class EnterpriseLicenseListPage extends TablePage<License> {
<div>${msg(str`External: ${item.externalUsers}`)}</div>`, <div>${msg(str`External: ${item.externalUsers}`)}</div>`,
html`<ak-label color=${color}> ${item.expiry?.toLocaleString()} </ak-label>`, html`<ak-label color=${color}> ${item.expiry?.toLocaleString()} </ak-label>`,
html`<ak-forms-modal> html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span> <span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update License")} </span> <span slot="header"> ${msg("Update License")} </span>
<ak-enterprise-license-form slot="form" .instancePk=${item.licenseUuid}> <ak-enterprise-license-form slot="form" .instancePk=${item.licenseUuid}>
</ak-enterprise-license-form> </ak-enterprise-license-form>
<button slot="trigger" class="pf-c-button pf-m-plain"> <button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}> <pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</pf-tooltip> </pf-tooltip>
</button> </button>
</ak-forms-modal>`, </ak-forms-modal>
<ak-rbac-object-permission-modal
model=${RbacPermissionsAssignedByUsersListModelEnum.EnterpriseLicense}
objectPk=${item.licenseUuid}
>
</ak-rbac-object-permission-modal> `,
]; ];
} }

View file

@ -6,6 +6,8 @@ import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/buttons/SpinnerButton"; import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/rbac/ObjectPermissionModal";
import "@goauthentik/elements/rbac/ObjectPermissionModal";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage"; import { TablePage } from "@goauthentik/elements/table/TablePage";
@ -15,7 +17,11 @@ import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { EventsApi, NotificationRule } from "@goauthentik/api"; import {
EventsApi,
NotificationRule,
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api";
@customElement("ak-event-rule-list") @customElement("ak-event-rule-list")
export class RuleListPage extends TablePage<NotificationRule> { export class RuleListPage extends TablePage<NotificationRule> {
@ -88,15 +94,21 @@ export class RuleListPage extends TablePage<NotificationRule> {
? html`<a href="#/identity/groups/${item.groupObj.pk}">${item.groupObj.name}</a>` ? html`<a href="#/identity/groups/${item.groupObj.pk}">${item.groupObj.name}</a>`
: msg("None (rule disabled)")}`, : msg("None (rule disabled)")}`,
html`<ak-forms-modal> html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span> <span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Notification Rule")} </span> <span slot="header"> ${msg("Update Notification Rule")} </span>
<ak-event-rule-form slot="form" .instancePk=${item.pk}> </ak-event-rule-form> <ak-event-rule-form slot="form" .instancePk=${item.pk}> </ak-event-rule-form>
<button slot="trigger" class="pf-c-button pf-m-plain"> <button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}> <pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</pf-tooltip> </pf-tooltip>
</button> </button>
</ak-forms-modal>`, </ak-forms-modal>
<ak-rbac-object-permission-modal
model=${RbacPermissionsAssignedByUsersListModelEnum.EventsNotificationrule}
objectPk=${item.pk}
>
</ak-rbac-object-permission-modal>`,
]; ];
} }

View file

@ -5,6 +5,8 @@ import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/SpinnerButton"; import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/rbac/ObjectPermissionModal";
import "@goauthentik/elements/rbac/ObjectPermissionModal";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage"; import { TablePage } from "@goauthentik/elements/table/TablePage";
@ -14,7 +16,11 @@ import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { EventsApi, NotificationTransport } from "@goauthentik/api"; import {
EventsApi,
NotificationTransport,
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api";
@customElement("ak-event-transport-list") @customElement("ak-event-transport-list")
export class TransportListPage extends TablePage<NotificationTransport> { export class TransportListPage extends TablePage<NotificationTransport> {
@ -90,6 +96,12 @@ export class TransportListPage extends TablePage<NotificationTransport> {
</pf-tooltip> </pf-tooltip>
</button> </button>
</ak-forms-modal> </ak-forms-modal>
<ak-rbac-object-permission-modal
model=${RbacPermissionsAssignedByUsersListModelEnum.EventsNotificationtransport}
objectPk=${item.pk}
>
</ak-rbac-object-permission-modal>
<ak-action-button <ak-action-button
class="pf-m-plain" class="pf-m-plain"
.apiRequest=${() => { .apiRequest=${() => {

View file

@ -3,6 +3,7 @@ import "@goauthentik/admin/flows/FlowDiagram";
import "@goauthentik/admin/flows/FlowForm"; import "@goauthentik/admin/flows/FlowForm";
import "@goauthentik/admin/policies/BoundPoliciesList"; import "@goauthentik/admin/policies/BoundPoliciesList";
import { DesignationToLabel } from "@goauthentik/app/admin/flows/utils"; import { DesignationToLabel } from "@goauthentik/app/admin/flows/utils";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
@ -22,7 +23,12 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { Flow, FlowsApi, ResponseError } from "@goauthentik/api"; import {
Flow,
FlowsApi,
RbacPermissionsAssignedByUsersListModelEnum,
ResponseError,
} from "@goauthentik/api";
@customElement("ak-flow-view") @customElement("ak-flow-view")
export class FlowViewPage extends AKElement { export class FlowViewPage extends AKElement {
@ -267,6 +273,12 @@ export class FlowViewPage extends AKElement {
</div> </div>
</div> </div>
</div> </div>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.FlowsFlow}
objectPk=${this.flow.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`; </ak-tabs>`;
} }
} }

View file

@ -11,13 +11,22 @@ import YAML from "yaml";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { CoreApi, CoreGroupsListRequest, Group } from "@goauthentik/api"; import {
CoreApi,
CoreGroupsListRequest,
Group,
PaginatedRoleList,
RbacApi,
} from "@goauthentik/api";
@customElement("ak-group-form") @customElement("ak-group-form")
export class GroupForm extends ModelForm<Group, string> { export class GroupForm extends ModelForm<Group, string> {
@state()
roles?: PaginatedRoleList;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return super.styles.concat(css` return super.styles.concat(css`
.pf-c-button.pf-m-control { .pf-c-button.pf-m-control {
@ -43,6 +52,12 @@ export class GroupForm extends ModelForm<Group, string> {
} }
} }
async load(): Promise<void> {
this.roles = await new RbacApi(DEFAULT_CONFIG).rbacRolesList({
ordering: "name",
});
}
async send(data: Group): Promise<Group> { async send(data: Group): Promise<Group> {
if (this.instance?.pk) { if (this.instance?.pk) {
return new CoreApi(DEFAULT_CONFIG).coreGroupsPartialUpdate({ return new CoreApi(DEFAULT_CONFIG).coreGroupsPartialUpdate({
@ -112,6 +127,26 @@ export class GroupForm extends ModelForm<Group, string> {
> >
</ak-search-select> </ak-search-select>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Roles")} name="roles">
<select class="pf-c-form-control" multiple>
${this.roles?.results.map((role) => {
const selected = Array.from(this.instance?.roles || []).some((sp) => {
return sp == role.pk;
});
return html`<option value=${role.pk} ?selected=${selected}>
${role.name}
</option>`;
})}
</select>
<p class="pf-c-form__helper-text">
${msg(
"Select roles to grant this groups' users' permissions from the selected roles.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Attributes")} label=${msg("Attributes")}
?required=${true} ?required=${true}

View file

@ -1,5 +1,6 @@
import "@goauthentik/admin/groups/GroupForm"; import "@goauthentik/admin/groups/GroupForm";
import "@goauthentik/admin/users/RelatedUserList"; import "@goauthentik/admin/users/RelatedUserList";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
@ -20,13 +21,14 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css"; import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css"; import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css"; import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import PFSizing from "@patternfly/patternfly/utilities/Sizing/sizing.css"; import PFSizing from "@patternfly/patternfly/utilities/Sizing/sizing.css";
import { CoreApi, Group } from "@goauthentik/api"; import { CoreApi, Group, RbacPermissionsAssignedByUsersListModelEnum } from "@goauthentik/api";
@customElement("ak-group-view") @customElement("ak-group-view")
export class GroupViewPage extends AKElement { export class GroupViewPage extends AKElement {
@ -51,6 +53,7 @@ export class GroupViewPage extends AKElement {
PFButton, PFButton,
PFDisplay, PFDisplay,
PFGrid, PFGrid,
PFList,
PFContent, PFContent,
PFCard, PFCard,
PFDescriptionList, PFDescriptionList,
@ -92,7 +95,7 @@ export class GroupViewPage extends AKElement {
> >
<div class="pf-c-card__title">${msg("Group Info")}</div> <div class="pf-c-card__title">${msg("Group Info")}</div>
<div class="pf-c-card__body"> <div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col"> <dl class="pf-c-description-list">
<div class="pf-c-description-list__group"> <div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term"> <dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text" <span class="pf-c-description-list__text"
@ -121,6 +124,26 @@ export class GroupViewPage extends AKElement {
</div> </div>
</dd> </dd>
</div> </div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Roles")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ul class="pf-c-list">
${this.group.rolesObj.map((role) => {
return html`<li>
<a href=${`#/identity/roles/${role.pk}`}
>${role.name}
</a>
</li>`;
})}
</ul>
</div>
</dd>
</div>
</dl> </dl>
</div> </div>
<div class="pf-c-card__footer"> <div class="pf-c-card__footer">
@ -177,6 +200,13 @@ export class GroupViewPage extends AKElement {
</div> </div>
</div> </div>
</section> </section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.CoreGroup}
objectPk=${this.group.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`; </ak-tabs>`;
} }
} }

View file

@ -10,6 +10,7 @@ import { PFSize } from "@goauthentik/elements/Spinner";
import "@goauthentik/elements/buttons/SpinnerButton"; import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/rbac/ObjectPermissionModal";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage"; import { TablePage } from "@goauthentik/elements/table/TablePage";
@ -23,7 +24,13 @@ import { ifDefined } from "lit/directives/if-defined.js";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import { Outpost, OutpostHealth, OutpostTypeEnum, OutpostsApi } from "@goauthentik/api"; import {
Outpost,
OutpostHealth,
OutpostTypeEnum,
OutpostsApi,
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api";
export function TypeToLabel(type?: OutpostTypeEnum): string { export function TypeToLabel(type?: OutpostTypeEnum): string {
if (!type) return ""; if (!type) return "";
@ -141,6 +148,11 @@ export class OutpostListPage extends TablePage<Outpost> {
</pf-tooltip> </pf-tooltip>
</button> </button>
</ak-forms-modal> </ak-forms-modal>
<ak-rbac-object-permission-modal
model=${RbacPermissionsAssignedByUsersListModelEnum.OutpostsOutpost}
objectPk=${item.pk}
>
</ak-rbac-object-permission-modal>
${item.managed !== "goauthentik.io/outposts/embedded" ${item.managed !== "goauthentik.io/outposts/embedded"
? html`<ak-outpost-deployment-modal .outpost=${item} size=${PFSize.Medium}> ? html`<ak-outpost-deployment-modal .outpost=${item} size=${PFSize.Medium}>
<button slot="trigger" class="pf-c-button pf-m-tertiary"> <button slot="trigger" class="pf-c-button pf-m-tertiary">

View file

@ -9,6 +9,7 @@ import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/forms/ProxyForm"; import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/rbac/ObjectPermissionModal";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage"; import { TablePage } from "@goauthentik/elements/table/TablePage";
@ -88,23 +89,27 @@ export class OutpostServiceConnectionListPage extends TablePage<ServiceConnectio
html`${itemState?.healthy html`${itemState?.healthy
? html`<ak-label color=${PFColor.Green}>${ifDefined(itemState.version)}</ak-label>` ? html`<ak-label color=${PFColor.Green}>${ifDefined(itemState.version)}</ak-label>`
: html`<ak-label color=${PFColor.Red}>${msg("Unhealthy")}</ak-label>`}`, : html`<ak-label color=${PFColor.Red}>${msg("Unhealthy")}</ak-label>`}`,
html` <ak-forms-modal> html`
<span slot="submit"> ${msg("Update")} </span> <ak-forms-modal>
<span slot="header"> ${msg(str`Update ${item.verboseName}`)} </span> <span slot="submit"> ${msg("Update")} </span>
<ak-proxy-form <span slot="header"> ${msg(str`Update ${item.verboseName}`)} </span>
slot="form" <ak-proxy-form
.args=${{ slot="form"
instancePk: item.pk, .args=${{
}} instancePk: item.pk,
type=${ifDefined(item.component)} }}
> type=${ifDefined(item.component)}
</ak-proxy-form> >
<button slot="trigger" class="pf-c-button pf-m-plain"> </ak-proxy-form>
<pf-tooltip position="top" content=${msg("Edit")}> <button slot="trigger" class="pf-c-button pf-m-plain">
<i class="fas fa-edit"></i> <pf-tooltip position="top" content=${msg("Edit")}>
</pf-tooltip> <i class="fas fa-edit"></i>
</button> </pf-tooltip>
</ak-forms-modal>`, </button>
</ak-forms-modal>
<ak-rbac-object-permission-modal model=${item.metaModelName} objectPk=${item.pk}>
</ak-rbac-object-permission-modal>
`,
]; ];
} }

View file

@ -13,6 +13,7 @@ import "@goauthentik/elements/forms/ConfirmationForm";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/forms/ProxyForm"; import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/rbac/ObjectPermissionModal";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage"; import { TablePage } from "@goauthentik/elements/table/TablePage";
@ -92,6 +93,9 @@ export class PolicyListPage extends TablePage<Policy> {
</pf-tooltip> </pf-tooltip>
</button> </button>
</ak-forms-modal> </ak-forms-modal>
<ak-rbac-object-permission-modal model=${item.metaModelName} objectPk=${item.pk}>
</ak-rbac-object-permission-modal>
<ak-forms-modal .closeAfterSuccessfulSubmit=${false}> <ak-forms-modal .closeAfterSuccessfulSubmit=${false}>
<span slot="submit"> ${msg("Test")} </span> <span slot="submit"> ${msg("Test")} </span>
<span slot="header"> ${msg("Test Policy")} </span> <span slot="header"> ${msg("Test Policy")} </span>

View file

@ -4,6 +4,7 @@ import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/SpinnerButton"; import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/rbac/ObjectPermissionModal";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage"; import { TablePage } from "@goauthentik/elements/table/TablePage";
@ -13,7 +14,11 @@ import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { PoliciesApi, Reputation } from "@goauthentik/api"; import {
PoliciesApi,
RbacPermissionsAssignedByUsersListModelEnum,
Reputation,
} from "@goauthentik/api";
@customElement("ak-policy-reputation-list") @customElement("ak-policy-reputation-list")
export class ReputationListPage extends TablePage<Reputation> { export class ReputationListPage extends TablePage<Reputation> {
@ -52,6 +57,7 @@ export class ReputationListPage extends TablePage<Reputation> {
new TableColumn(msg("IP"), "ip"), new TableColumn(msg("IP"), "ip"),
new TableColumn(msg("Score"), "score"), new TableColumn(msg("Score"), "score"),
new TableColumn(msg("Updated"), "updated"), new TableColumn(msg("Updated"), "updated"),
new TableColumn(msg("Actions")),
]; ];
} }
@ -86,6 +92,13 @@ export class ReputationListPage extends TablePage<Reputation> {
${item.ip}`, ${item.ip}`,
html`${item.score}`, html`${item.score}`,
html`${item.updated.toLocaleString()}`, html`${item.updated.toLocaleString()}`,
html`
<ak-rbac-object-permission-modal
model=${RbacPermissionsAssignedByUsersListModelEnum.PoliciesReputationReputation}
objectPk=${item.pk || ""}
>
</ak-rbac-object-permission-modal>
`,
]; ];
} }
} }

View file

@ -10,6 +10,7 @@ import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/forms/ProxyForm"; import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/rbac/ObjectPermissionModal";
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch"; import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table";
@ -107,6 +108,8 @@ export class PropertyMappingListPage extends TablePage<PropertyMapping> {
</pf-tooltip> </pf-tooltip>
</button> </button>
</ak-forms-modal> </ak-forms-modal>
<ak-rbac-object-permission-modal model=${item.metaModelName} objectPk=${item.pk}>
</ak-rbac-object-permission-modal>
<ak-forms-modal .closeAfterSuccessfulSubmit=${false}> <ak-forms-modal .closeAfterSuccessfulSubmit=${false}>
<span slot="submit"> ${msg("Test")} </span> <span slot="submit"> ${msg("Test")} </span>
<span slot="header"> ${msg("Test Property Mapping")} </span> <span slot="header"> ${msg("Test Property Mapping")} </span>

View file

@ -1,5 +1,6 @@
import "@goauthentik/admin/providers/RelatedApplicationButton"; import "@goauthentik/admin/providers/RelatedApplicationButton";
import "@goauthentik/admin/providers/ldap/LDAPProviderForm"; import "@goauthentik/admin/providers/ldap/LDAPProviderForm";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
@ -27,7 +28,12 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { LDAPProvider, ProvidersApi, SessionUser } from "@goauthentik/api"; import {
LDAPProvider,
ProvidersApi,
RbacPermissionsAssignedByUsersListModelEnum,
SessionUser,
} from "@goauthentik/api";
@customElement("ak-provider-ldap-view") @customElement("ak-provider-ldap-view")
export class LDAPProviderViewPage extends AKElement { export class LDAPProviderViewPage extends AKElement {
@ -101,6 +107,12 @@ export class LDAPProviderViewPage extends AKElement {
</div> </div>
</div> </div>
</section> </section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.ProvidersLdapLdapprovider}
objectPk=${this.provider.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`; </ak-tabs>`;
} }

View file

@ -33,6 +33,7 @@ import {
OAuth2ProviderSetupURLs, OAuth2ProviderSetupURLs,
PropertyMappingPreview, PropertyMappingPreview,
ProvidersApi, ProvidersApi,
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-provider-oauth2-view") @customElement("ak-provider-oauth2-view")
@ -128,6 +129,12 @@ export class OAuth2ProviderViewPage extends AKElement {
</div> </div>
</div> </div>
</section> </section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.ProvidersOauth2Oauth2provider}
objectPk=${this.provider.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`; </ak-tabs>`;
} }

View file

@ -1,5 +1,6 @@
import "@goauthentik/admin/providers/RelatedApplicationButton"; import "@goauthentik/admin/providers/RelatedApplicationButton";
import "@goauthentik/admin/providers/proxy/ProxyProviderForm"; import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { convertToSlug } from "@goauthentik/common/utils"; import { convertToSlug } from "@goauthentik/common/utils";
@ -39,7 +40,12 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { ProvidersApi, ProxyMode, ProxyProvider } from "@goauthentik/api"; import {
ProvidersApi,
ProxyMode,
ProxyProvider,
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api";
export function ModeToLabel(action?: ProxyMode): string { export function ModeToLabel(action?: ProxyMode): string {
if (!action) return ""; if (!action) return "";
@ -208,6 +214,12 @@ export class ProxyProviderViewPage extends AKElement {
</div> </div>
</div> </div>
</section> </section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.ProvidersProxyProxyprovider}
objectPk=${this.provider.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`; </ak-tabs>`;
} }

View file

@ -1,5 +1,6 @@
import "@goauthentik/admin/providers/RelatedApplicationButton"; import "@goauthentik/admin/providers/RelatedApplicationButton";
import "@goauthentik/admin/providers/radius/RadiusProviderForm"; import "@goauthentik/admin/providers/radius/RadiusProviderForm";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
@ -21,10 +22,13 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGallery from "@patternfly/patternfly/layouts/Gallery/gallery.css"; import PFGallery from "@patternfly/patternfly/layouts/Gallery/gallery.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css"; import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import PFFlex from "@patternfly/patternfly/utilities/Flex/flex.css";
import PFSizing from "@patternfly/patternfly/utilities/Sizing/sizing.css"; import PFSizing from "@patternfly/patternfly/utilities/Sizing/sizing.css";
import { ProvidersApi, RadiusProvider } from "@goauthentik/api"; import {
ProvidersApi,
RadiusProvider,
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api";
@customElement("ak-provider-radius-view") @customElement("ak-provider-radius-view")
export class RadiusProviderViewPage extends AKElement { export class RadiusProviderViewPage extends AKElement {
@ -50,7 +54,6 @@ export class RadiusProviderViewPage extends AKElement {
PFBase, PFBase,
PFButton, PFButton,
PFPage, PFPage,
PFFlex,
PFDisplay, PFDisplay,
PFGallery, PFGallery,
PFContent, PFContent,
@ -162,6 +165,12 @@ export class RadiusProviderViewPage extends AKElement {
</div> </div>
</div> </div>
</section> </section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.ProvidersRadiusRadiusprovider}
objectPk=${this.provider.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`; </ak-tabs>`;
} }
} }

View file

@ -1,5 +1,6 @@
import "@goauthentik/admin/providers/RelatedApplicationButton"; import "@goauthentik/admin/providers/RelatedApplicationButton";
import "@goauthentik/admin/providers/saml/SAMLProviderForm"; import "@goauthentik/admin/providers/saml/SAMLProviderForm";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { MessageLevel } from "@goauthentik/common/messages"; import { MessageLevel } from "@goauthentik/common/messages";
@ -34,6 +35,7 @@ import {
CertificateKeyPair, CertificateKeyPair,
CryptoApi, CryptoApi,
ProvidersApi, ProvidersApi,
RbacPermissionsAssignedByUsersListModelEnum,
SAMLMetadata, SAMLMetadata,
SAMLProvider, SAMLProvider,
} from "@goauthentik/api"; } from "@goauthentik/api";
@ -226,6 +228,12 @@ export class SAMLProviderViewPage extends AKElement {
</div> </div>
</div> </div>
</section> </section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.ProvidersSamlSamlprovider}
objectPk=${this.provider.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`; </ak-tabs>`;
} }

View file

@ -1,4 +1,5 @@
import "@goauthentik/admin/providers/scim/SCIMProviderForm"; import "@goauthentik/admin/providers/scim/SCIMProviderForm";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
@ -26,7 +27,12 @@ import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFStack from "@patternfly/patternfly/layouts/Stack/stack.css"; import PFStack from "@patternfly/patternfly/layouts/Stack/stack.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { ProvidersApi, SCIMProvider, Task } from "@goauthentik/api"; import {
ProvidersApi,
RbacPermissionsAssignedByUsersListModelEnum,
SCIMProvider,
Task,
} from "@goauthentik/api";
@customElement("ak-provider-scim-view") @customElement("ak-provider-scim-view")
export class SCIMProviderViewPage extends AKElement { export class SCIMProviderViewPage extends AKElement {
@ -113,6 +119,12 @@ export class SCIMProviderViewPage extends AKElement {
</div> </div>
</div> </div>
</section> </section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.ProvidersScimScimprovider}
objectPk=${this.provider.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`; </ak-tabs>`;
} }

View file

@ -0,0 +1,56 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/chips/Chip";
import "@goauthentik/elements/chips/ChipGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { RbacApi, Role } from "@goauthentik/api";
@customElement("ak-role-form")
export class RoleForm extends ModelForm<Role, string> {
loadInstance(pk: string): Promise<Role> {
return new RbacApi(DEFAULT_CONFIG).rbacRolesRetrieve({
uuid: pk,
});
}
getSuccessMessage(): string {
if (this.instance) {
return msg("Successfully updated role.");
} else {
return msg("Successfully created role.");
}
}
async send(data: Role): Promise<Role> {
if (this.instance?.pk) {
return new RbacApi(DEFAULT_CONFIG).rbacRolesPartialUpdate({
uuid: this.instance.pk,
patchedRoleRequest: data,
});
} else {
return new RbacApi(DEFAULT_CONFIG).rbacRolesCreate({
roleRequest: data,
});
}
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
</form>`;
}
}

View file

@ -0,0 +1,98 @@
import "@goauthentik/admin/roles/RoleForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { RbacApi, Role } from "@goauthentik/api";
@customElement("ak-role-list")
export class RoleListPage extends TablePage<Role> {
checkbox = true;
searchEnabled(): boolean {
return true;
}
pageTitle(): string {
return msg("Roles");
}
pageDescription(): string {
return msg("Manage roles which grant permissions to objects within authentik.");
}
pageIcon(): string {
return "fa fa-lock";
}
@property()
order = "name";
async apiEndpoint(page: number): Promise<PaginatedResponse<Role>> {
return new RbacApi(DEFAULT_CONFIG).rbacRolesList({
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [new TableColumn(msg("Name"), "name"), new TableColumn(msg("Actions"))];
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Role(s)")}
.objects=${this.selectedElements}
.usedBy=${(item: Role) => {
return new RbacApi(DEFAULT_CONFIG).rbacRolesUsedByList({
uuid: item.pk,
});
}}
.delete=${(item: Role) => {
return new RbacApi(DEFAULT_CONFIG).rbacRolesDestroy({
uuid: item.pk,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
row(item: Role): TemplateResult[] {
return [
html`<a href="#/identity/roles/${item.pk}">${item.name}</a>`,
html`<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Role")} </span>
<ak-role-form slot="form" .instancePk=${item.pk}> </ak-role-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit"></i>
</pf-tooltip>
</button>
</ak-forms-modal>`,
];
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Role")} </span>
<ak-role-form slot="form"> </ak-role-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
</ak-forms-modal>
`;
}
}

View file

@ -0,0 +1,88 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/chips/Chip";
import "@goauthentik/elements/chips/ChipGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/rbac/PermissionSelectModal";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { Permission, RbacApi } from "@goauthentik/api";
interface RolePermissionAssign {
permissions: string[];
}
@customElement("ak-role-permission-form")
export class RolePermissionForm extends ModelForm<RolePermissionAssign, number> {
@state()
permissionsToAdd: Permission[] = [];
@property()
roleUuid?: string;
async load(): Promise<void> {}
loadInstance(): Promise<RolePermissionAssign> {
throw new Error("Method not implemented.");
}
getSuccessMessage(): string {
return msg("Successfully assigned permission.");
}
async send(data: RolePermissionAssign): Promise<unknown> {
await new RbacApi(DEFAULT_CONFIG).rbacPermissionsAssignedByRolesAssignCreate({
uuid: this.roleUuid || "",
permissionAssignRequest: {
permissions: data.permissions,
},
});
this.permissionsToAdd = [];
return;
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-form-element-horizontal label=${msg("Permissions to add")} name="permissions">
<div class="pf-c-input-group">
<ak-rbac-permission-select-table
.confirm=${(items: Permission[]) => {
this.permissionsToAdd = items;
this.requestUpdate();
return Promise.resolve();
}}
>
<button slot="trigger" class="pf-c-button pf-m-control" type="button">
<pf-tooltip position="top" content=${msg("Select permissions")}>
<i class="fas fa-plus" aria-hidden="true"></i>
</pf-tooltip>
</button>
</ak-rbac-permission-select-table>
<div class="pf-c-form-control">
<ak-chip-group>
${this.permissionsToAdd.map((permission) => {
return html`<ak-chip
.removable=${true}
value=${`${permission.appLabel}.${permission.codename}`}
@remove=${() => {
const idx = this.permissionsToAdd.indexOf(permission);
this.permissionsToAdd.splice(idx, 1);
this.requestUpdate();
}}
>
${permission.name}
</ak-chip>`;
})}
</ak-chip-group>
</div>
</div>
</ak-form-element-horizontal>
</form>`;
}
}

View file

@ -0,0 +1,89 @@
import "@goauthentik/admin/roles/RolePermissionForm";
import { DEFAULT_CONFIG } from "@goauthentik/app/common/api/config";
import { groupBy } from "@goauthentik/app/common/utils";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/app/elements/table/Table";
import "@goauthentik/elements/forms/ModalForm";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { Permission, RbacApi } from "@goauthentik/api";
@customElement("ak-role-permissions-global-table")
export class RolePermissionGlobalTable extends Table<Permission> {
@property()
roleUuid?: string;
searchEnabled(): boolean {
return true;
}
checkbox = true;
order = "content_type__app_label,content_type__model";
apiEndpoint(page: number): Promise<PaginatedResponse<Permission>> {
return new RbacApi(DEFAULT_CONFIG).rbacPermissionsList({
role: this.roleUuid,
page: page,
ordering: this.order,
search: this.search,
});
}
groupBy(items: Permission[]): [string, Permission[]][] {
return groupBy(items, (obj) => {
return obj.appLabelVerbose;
});
}
columns(): TableColumn[] {
return [
new TableColumn("Model", "model"),
new TableColumn("Permission", ""),
new TableColumn(""),
];
}
renderObjectCreate(): TemplateResult {
return html`
<ak-forms-modal>
<span slot="submit"> ${msg("Assign")} </span>
<span slot="header"> ${msg("Assign permission to role")} </span>
<ak-role-permission-form roleUuid=${ifDefined(this.roleUuid)} slot="form">
</ak-role-permission-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Assign permission")}
</button>
</ak-forms-modal>
`;
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Permission(s)")}
.objects=${this.selectedElements}
.delete=${(item: Permission) => {
return new RbacApi(
DEFAULT_CONFIG,
).rbacPermissionsAssignedByRolesUnassignPartialUpdate({
uuid: this.roleUuid || "",
patchedPermissionAssignRequest: {
permissions: [`${item.appLabel}.${item.codename}`],
},
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
row(item: Permission): TemplateResult[] {
return [html`${item.modelVerbose}`, html`${item.name}`, html``];
}
}

View file

@ -0,0 +1,94 @@
import { DEFAULT_CONFIG } from "@goauthentik/app/common/api/config";
import { groupBy } from "@goauthentik/app/common/utils";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/app/elements/table/Table";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ExtraRoleObjectPermission, RbacApi } from "@goauthentik/api";
@customElement("ak-role-permissions-object-table")
export class RolePermissionObjectTable extends Table<ExtraRoleObjectPermission> {
@property()
roleUuid?: string;
searchEnabled(): boolean {
return true;
}
checkbox = true;
apiEndpoint(page: number): Promise<PaginatedResponse<ExtraRoleObjectPermission>> {
return new RbacApi(DEFAULT_CONFIG).rbacPermissionsRolesList({
uuid: this.roleUuid || "",
page: page,
ordering: this.order,
search: this.search,
});
}
groupBy(items: ExtraRoleObjectPermission[]): [string, ExtraRoleObjectPermission[]][] {
return groupBy(items, (obj) => {
return obj.appLabelVerbose;
});
}
columns(): TableColumn[] {
return [
new TableColumn("Model", "model"),
new TableColumn("Permission", ""),
new TableColumn("Object", ""),
new TableColumn(""),
];
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Permission(s)")}
.objects=${this.selectedElements}
.metadata=${(item: ExtraRoleObjectPermission) => {
return [
{ key: msg("Permission"), value: item.name },
{ key: msg("Object"), value: item.objectDescription || item.objectPk },
];
}}
.delete=${(item: ExtraRoleObjectPermission) => {
return new RbacApi(
DEFAULT_CONFIG,
).rbacPermissionsAssignedByRolesUnassignPartialUpdate({
uuid: this.roleUuid || "",
patchedPermissionAssignRequest: {
permissions: [`${item.appLabel}.${item.codename}`],
objectPk: item.objectPk,
},
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
row(item: ExtraRoleObjectPermission): TemplateResult[] {
return [
html`${item.modelVerbose}`,
html`${item.name}`,
html`${item.objectDescription
? html`${item.objectDescription}`
: html`<pf-tooltip
position="top"
content=${msg(
"Role doesn't have view permission so description cannot be retrieved.",
)}
>
<pre>${item.objectPk}</pre>
</pf-tooltip>`}`,
html``,
];
}
}

View file

@ -0,0 +1,144 @@
import "@goauthentik/admin/groups/RelatedGroupList";
import "@goauthentik/app/admin/roles/RolePermissionGlobalTable";
import "@goauthentik/app/admin/roles/RolePermissionObjectTable";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/components/events/ObjectChangelog";
import "@goauthentik/components/events/UserEvents";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/Tabs";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import { RbacApi, RbacPermissionsAssignedByUsersListModelEnum, Role } from "@goauthentik/api";
@customElement("ak-role-view")
export class RoleViewPage extends AKElement {
@property({ type: String })
set roleId(id: string) {
new RbacApi(DEFAULT_CONFIG)
.rbacRolesRetrieve({
uuid: id,
})
.then((role) => {
this._role = role;
});
}
@state()
_role?: Role;
static get styles(): CSSResult[] {
return [
PFBase,
PFPage,
PFButton,
PFDisplay,
PFGrid,
PFContent,
PFCard,
PFDescriptionList,
css`
.pf-c-description-list__description ak-action-button {
margin-right: 6px;
margin-bottom: 6px;
}
.ak-button-collection {
max-width: 12em;
}
`,
];
}
constructor() {
super();
this.addEventListener(EVENT_REFRESH, () => {
if (!this._role?.pk) return;
this.roleId = this._role?.pk;
});
}
render(): TemplateResult {
return html`<ak-page-header
icon="fa fa-lock"
header=${msg(str`Role ${this._role?.name || ""}`)}
>
</ak-page-header>
${this.renderBody()}`;
}
renderBody(): TemplateResult {
if (!this._role) {
return html``;
}
return html`<ak-tabs>
<section
slot="page-overview"
data-tab-title="${msg("Overview")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-l-grid pf-m-gutter">
<div
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-3-col-on-xl pf-m-3-col-on-2xl"
>
<div class="pf-c-card__title">${msg("Role Info")}</div>
<div class="pf-c-card__body">
<dl class="pf-c-description-list">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Name")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this._role.name}
</div>
</dd>
</div>
</dl>
</div>
</div>
<div
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-9-col-on-xl pf-m-9-col-on-2xl"
>
<div class="pf-c-card__title">${msg("Assigned global permissions")}</div>
<div class="pf-c-card__body">
<ak-role-permissions-global-table
roleUuid=${this._role.pk}
></ak-role-permissions-global-table>
</div>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">${msg("Assigned object permissions")}</div>
<div class="pf-c-card__body">
<ak-role-permissions-object-table
roleUuid=${this._role.pk}
></ak-role-permissions-object-table>
</div>
</div>
</div>
</section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.RbacRole}
objectPk=${this._role.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`;
}
}

View file

@ -1,4 +1,5 @@
import "@goauthentik/admin/sources/ldap/LDAPSourceForm"; import "@goauthentik/admin/sources/ldap/LDAPSourceForm";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
@ -22,7 +23,13 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { LDAPSource, SourcesApi, Task, TaskStatusEnum } from "@goauthentik/api"; import {
LDAPSource,
RbacPermissionsAssignedByUsersListModelEnum,
SourcesApi,
Task,
TaskStatusEnum,
} from "@goauthentik/api";
@customElement("ak-source-ldap-view") @customElement("ak-source-ldap-view")
export class LDAPSourceViewPage extends AKElement { export class LDAPSourceViewPage extends AKElement {
@ -206,6 +213,12 @@ export class LDAPSourceViewPage extends AKElement {
</div> </div>
</div> </div>
</section> </section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.SourcesLdapLdapsource}
objectPk=${this.source.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`; </ak-tabs>`;
} }
} }

View file

@ -1,6 +1,7 @@
import "@goauthentik/admin/policies/BoundPoliciesList"; import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/sources/oauth/OAuthSourceDiagram"; import "@goauthentik/admin/sources/oauth/OAuthSourceDiagram";
import "@goauthentik/admin/sources/oauth/OAuthSourceForm"; import "@goauthentik/admin/sources/oauth/OAuthSourceForm";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
@ -22,7 +23,12 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { OAuthSource, ProviderTypeEnum, SourcesApi } from "@goauthentik/api"; import {
OAuthSource,
ProviderTypeEnum,
RbacPermissionsAssignedByUsersListModelEnum,
SourcesApi,
} from "@goauthentik/api";
export function ProviderToLabel(provider?: ProviderTypeEnum): string { export function ProviderToLabel(provider?: ProviderTypeEnum): string {
switch (provider) { switch (provider) {
@ -238,6 +244,12 @@ export class OAuthSourceViewPage extends AKElement {
</div> </div>
</div> </div>
</div> </div>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.SourcesOauthOauthsource}
objectPk=${this.source.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`; </ak-tabs>`;
} }
} }

View file

@ -1,5 +1,6 @@
import "@goauthentik/admin/policies/BoundPoliciesList"; import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/sources/plex/PlexSourceForm"; import "@goauthentik/admin/sources/plex/PlexSourceForm";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
@ -21,7 +22,11 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { PlexSource, SourcesApi } from "@goauthentik/api"; import {
PlexSource,
RbacPermissionsAssignedByUsersListModelEnum,
SourcesApi,
} from "@goauthentik/api";
@customElement("ak-source-plex-view") @customElement("ak-source-plex-view")
export class PlexSourceViewPage extends AKElement { export class PlexSourceViewPage extends AKElement {
@ -131,6 +136,12 @@ export class PlexSourceViewPage extends AKElement {
</div> </div>
</div> </div>
</div> </div>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.SourcesPlexPlexsource}
objectPk=${this.source.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`; </ak-tabs>`;
} }
} }

View file

@ -1,5 +1,6 @@
import "@goauthentik/admin/policies/BoundPoliciesList"; import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/sources/saml/SAMLSourceForm"; import "@goauthentik/admin/sources/saml/SAMLSourceForm";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
@ -22,7 +23,12 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SAMLMetadata, SAMLSource, SourcesApi } from "@goauthentik/api"; import {
RbacPermissionsAssignedByUsersListModelEnum,
SAMLMetadata,
SAMLSource,
SourcesApi,
} from "@goauthentik/api";
@customElement("ak-source-saml-view") @customElement("ak-source-saml-view")
export class SAMLSourceViewPage extends AKElement { export class SAMLSourceViewPage extends AKElement {
@ -206,6 +212,12 @@ export class SAMLSourceViewPage extends AKElement {
</div> </div>
</div> </div>
</div> </div>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.SourcesSamlSamlsource}
objectPk=${this.source.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`; </ak-tabs>`;
} }
} }

View file

@ -24,6 +24,7 @@ import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/forms/ProxyForm"; import "@goauthentik/elements/forms/ProxyForm";
import "@goauthentik/elements/rbac/ObjectPermissionModal";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table"; import { TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage"; import { TablePage } from "@goauthentik/elements/table/TablePage";
@ -149,6 +150,8 @@ export class StageListPage extends TablePage<Stage> {
</pf-tooltip> </pf-tooltip>
</button> </button>
</ak-forms-modal> </ak-forms-modal>
<ak-rbac-object-permission-modal model=${item.metaModelName} objectPk=${item.pk}>
</ak-rbac-object-permission-modal>
${this.renderStageActions(item)}`, ${this.renderStageActions(item)}`,
]; ];
} }

Some files were not shown because too many files have changed in this diff Show more