Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

root: RBAC permission cleanup #12707

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions authentik/core/api/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def check_access(self, request: Request, slug: str) -> Response:
@extend_schema(
parameters=[
OpenApiParameter(
name="superuser_full_list",
name="list_rbac",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
),
Expand All @@ -229,10 +229,8 @@ def list(self, request: Request) -> Response:
"""Custom list method that checks Policy based access instead of guardian"""
should_cache = request.query_params.get("search", "") == ""

superuser_full_list = (
str(request.query_params.get("superuser_full_list", "false")).lower() == "true"
)
if superuser_full_list and request.user.is_superuser:
list_rbac = str(request.query_params.get("list_rbac", "false")).lower() == "true"
if list_rbac:
return super().list(request)

only_with_launch_url = str(
Expand Down
9 changes: 2 additions & 7 deletions authentik/core/api/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from django.utils.timezone import now
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import assign_perm, get_anonymous_user
from guardian.shortcuts import assign_perm
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField
Expand Down Expand Up @@ -138,13 +138,8 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
owner_field = "user"
rbac_allow_create_without_perm = True

def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)

def perform_create(self, serializer: TokenSerializer):
# TODO: better permission check
if not self.request.user.is_superuser:
instance = serializer.save(
user=self.request.user,
Expand Down
57 changes: 57 additions & 0 deletions authentik/core/migrations/0043_alter_user_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Generated by Django 5.0.10 on 2025-01-08 17:39

from django.db import migrations
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor


def migrate_user_debug_attribute(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from django.apps import apps as real_apps
from django.contrib.auth.management import create_permissions

db_alias = schema_editor.connection.alias

User = apps.get_model("authentik_core", "User")
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"

# Permissions are only created _after_ migrations are run
# - https://github.com/django/django/blob/43cdfa8b20e567a801b7d0a09ec67ddd062d5ea4/django/contrib/auth/apps.py#L19
# - https://stackoverflow.com/a/72029063/1870445
create_permissions(real_apps.get_app_config("authentik_core"), using=db_alias)

Permission = apps.get_model("auth", "Permission")

new_prem = Permission.objects.using(db_alias).get(codename="user_view_debug")

db_alias = schema_editor.connection.alias
for user in User.objects.using(db_alias).filter(
**{f"attributes__{USER_ATTRIBUTE_DEBUG}": True}
):
user.permissions.add(new_prem)


class Migration(migrations.Migration):

dependencies = [
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
]

operations = [
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"),
("preview_user", "Can preview user data sent to providers"),
("view_user_applications", "View applications the user has access to"),
("user_view_debug", "User receives additional details for error messages"),
],
"verbose_name": "User",
"verbose_name_plural": "Users",
},
),
migrations.RunPython(migrate_user_debug_attribute),
]
2 changes: 1 addition & 1 deletion authentik/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
from authentik.tenants.utils import get_current_tenant, get_unique_identifier

LOGGER = get_logger()
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated"
USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires"
USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
Expand Down Expand Up @@ -282,6 +281,7 @@ class Meta:
("unassign_user_permissions", _("Can unassign permissions from users")),
("preview_user", _("Can preview user data sent to providers")),
("view_user_applications", _("View applications the user has access to")),
("user_view_debug", _("User receives additional details for error messages")),
]
indexes = [
models.Index(fields=["last_login"]),
Expand Down
6 changes: 3 additions & 3 deletions authentik/enterprise/providers/rac/api/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def _get_allowed_endpoints(self, queryset: QuerySet) -> list[Endpoint]:
OpenApiTypes.STR,
),
OpenApiParameter(
name="superuser_full_list",
name="list_rbac",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
),
Expand All @@ -110,8 +110,8 @@ def list(self, request: Request, *args, **kwargs) -> Response:
"""List accessible endpoints"""
should_cache = request.GET.get("search", "") == ""

superuser_full_list = str(request.GET.get("superuser_full_list", "false")).lower() == "true"
if superuser_full_list and request.user.is_superuser:
list_rbac = str(request.GET.get("list_rbac", "false")).lower() == "true"
if list_rbac:
return super().list(request)

queryset = self._filter_queryset_for_list(self.get_queryset())
Expand Down
5 changes: 1 addition & 4 deletions authentik/flows/challenge.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,9 @@ def __init__(self, request: Request | None = None, error: Exception | None = Non
if not request or not error:
return
self.initial_data["request_id"] = request.request_id
from authentik.core.models import USER_ATTRIBUTE_DEBUG

if request.user and request.user.is_authenticated:
if request.user.is_superuser or request.user.group_attributes(request).get(
USER_ATTRIBUTE_DEBUG, False
):
if request.user.has_perm("authentik_core.user_view_debug"):
self.initial_data["error"] = str(error)
self.initial_data["traceback"] = exception_to_string(error)

Expand Down
15 changes: 11 additions & 4 deletions authentik/outposts/controllers/docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from structlog.stdlib import get_logger
from yaml import safe_dump

from authentik import __version__
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
Expand Down Expand Up @@ -182,10 +183,16 @@ def try_pull_image(self):
`outposts.container_image_base`, but fall back to known-good images"""
image = self.get_container_image()
try:
self.client.images.pull(image)
except DockerException: # pragma: no cover
image = f"ghcr.io/goauthentik/{self.outpost.type}:latest"
self.client.images.pull(image)
# See if the image exists...
self.client.images.get(image)
except DockerException:
try:
# ...otherwise try to pull it...
self.client.images.pull(image)
except DockerException:
# ...and as a fallback to that default to a sane standard
image = f"ghcr.io/goauthentik/{self.outpost.type}:{__version__}"
self.client.images.pull(image)
return image

def _get_container(self) -> tuple[Container, bool]:
Expand Down
9 changes: 4 additions & 5 deletions authentik/policies/denied.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.urls import reverse
from django.utils.translation import gettext as _

from authentik.core.models import USER_ATTRIBUTE_DEBUG
from authentik.core.models import User
from authentik.policies.types import PolicyResult


Expand All @@ -31,12 +31,11 @@ def resolve_context(self, context: dict[str, Any] | None) -> dict[str, Any] | No
if self.error_message:
context["error"] = self.error_message
# Only show policy result if user is authenticated and
# either superuser or has USER_ATTRIBUTE_DEBUG set
# has permissions to see them
if self.policy_result:
if self._request.user and self._request.user.is_authenticated:
if self._request.user.is_superuser or self._request.user.group_attributes(
self._request
).get(USER_ATTRIBUTE_DEBUG, False):
user: User = self._request.user
if user.has_perm("authentik_core.user_view_debug"):
context["policy_result"] = self.policy_result
context["cancel"] = reverse("authentik_flows:cancel")
return context
39 changes: 3 additions & 36 deletions authentik/providers/oauth2/api/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@

from json import dumps

from django_filters.rest_framework import DjangoFilterBackend
from guardian.utils import get_anonymous_user
from rest_framework import mixins
from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet

from authentik.core.api.used_by import UsedByMixin
Expand Down Expand Up @@ -66,17 +63,7 @@ class AuthorizationCodeViewSet(
serializer_class = ExpiringBaseGrantModelSerializer
filterset_fields = ["user", "provider"]
ordering = ["provider", "expires"]
filter_backends = [
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]

def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)
owner_field = "user"


class RefreshTokenViewSet(
Expand All @@ -92,17 +79,7 @@ class RefreshTokenViewSet(
serializer_class = TokenModelSerializer
filterset_fields = ["user", "provider"]
ordering = ["provider", "expires"]
filter_backends = [
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]

def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)
owner_field = "user"


class AccessTokenViewSet(
Expand All @@ -118,14 +95,4 @@ class AccessTokenViewSet(
serializer_class = TokenModelSerializer
filterset_fields = ["user", "provider"]
ordering = ["provider", "expires"]
filter_backends = [
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]

def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)
owner_field = "user"
3 changes: 3 additions & 0 deletions blueprints/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6445,6 +6445,7 @@
"authentik_core.remove_user_from_group",
"authentik_core.reset_user_password",
"authentik_core.unassign_user_permissions",
"authentik_core.user_view_debug",
"authentik_core.view_application",
"authentik_core.view_applicationentitlement",
"authentik_core.view_authenticatedsession",
Expand Down Expand Up @@ -12694,6 +12695,7 @@
"authentik_core.remove_user_from_group",
"authentik_core.reset_user_password",
"authentik_core.unassign_user_permissions",
"authentik_core.user_view_debug",
"authentik_core.view_application",
"authentik_core.view_applicationentitlement",
"authentik_core.view_authenticatedsession",
Expand Down Expand Up @@ -13202,6 +13204,7 @@
"unassign_user_permissions",
"preview_user",
"view_user_applications",
"user_view_debug",
"add_user",
"change_user",
"delete_user",
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ version = "2024.12.2"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]

[tool.poetry.requires-plugins]
poetry-plugin-export = ">1.8"

[tool.black]
line-length = 100
target-version = ['py312']
Expand Down
16 changes: 8 additions & 8 deletions schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3391,6 +3391,10 @@ paths:
name: group
schema:
type: string
- in: query
name: list_rbac
schema:
type: boolean
- in: query
name: meta_description
schema:
Expand Down Expand Up @@ -3439,10 +3443,6 @@ paths:
name: slug
schema:
type: string
- in: query
name: superuser_full_list
schema:
type: boolean
tags:
- core
security:
Expand Down Expand Up @@ -23204,6 +23204,10 @@ paths:
operationId: rac_endpoints_list
description: List accessible endpoints
parameters:
- in: query
name: list_rbac
schema:
type: boolean
- in: query
name: name
schema:
Expand Down Expand Up @@ -23234,10 +23238,6 @@ paths:
name: search
schema:
type: string
- in: query
name: superuser_full_list
schema:
type: boolean
tags:
- rac
security:
Expand Down
2 changes: 1 addition & 1 deletion web/src/admin/applications/ApplicationListPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
async apiEndpoint(): Promise<PaginatedResponse<Application>> {
return new CoreApi(DEFAULT_CONFIG).coreApplicationsList({
...(await this.defaultEndpointConfig()),
superuserFullList: true,
listRbac: true,
});
}

Expand Down
2 changes: 1 addition & 1 deletion web/src/admin/brands/BrandForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export class BrandForm extends ModelForm<Brand, string> {
.fetchObjects=${async (query?: string): Promise<Application[]> => {
const args: CoreApplicationsListRequest = {
ordering: "name",
superuserFullList: true,
listRbac: true,
};
if (query !== undefined) {
args.search = query;
Expand Down
2 changes: 1 addition & 1 deletion web/src/admin/providers/rac/EndpointList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class EndpointListPage extends Table<Endpoint> {
return new RacApi(DEFAULT_CONFIG).racEndpointsList({
...(await this.defaultEndpointConfig()),
provider: this.provider?.pk,
superuserFullList: true,
listRbac: true,
});
}

Expand Down
Loading