Skip to content

Commit

Permalink
Merge branch 'main' into web/lab-upgrades
Browse files Browse the repository at this point in the history
* main:
  web: the return of pseudolocalization (#7190)
  rbac: revisions (#7188)
  website: bump @babel/traverse from 7.21.4 to 7.23.2 in /website (#7187)
  web: bump API Client version (#7186)
  core: Initial RBAC (#6806)
  lifecycle: re-fix system migrations (#7185)
  outposts: use channel groups instead of saving channel names (#7183)
  sources/ldap: made ldap_sync_single calls from ldap_sync_all asynchronous (#6862)
  website/docs: fix API OAuth token usage (#7159)
  web: bump rollup from 4.1.3 to 4.1.4 in /web (#7181)
  web: bump @formatjs/intl-listformat from 7.4.2 to 7.5.0 in /web (#7182)
  web: bump @rollup/plugin-replace from 5.0.3 to 5.0.4 in /web (#7177)
  web: bump the sentry group in /web with 2 updates (#7175)
  web: bump @rollup/plugin-commonjs from 25.0.5 to 25.0.7 in /web (#7178)
  web: bump yaml from 2.3.2 to 2.3.3 in /web (#7176)
  web: bump rollup from 4.0.2 to 4.1.3 in /web (#7179)
  web: bump the wdio group in /tests/wdio with 3 updates (#7180)
  • Loading branch information
kensternberg-authentik committed Oct 16, 2023
2 parents 01e6793 + 9e568e1 commit 29fc42f
Show file tree
Hide file tree
Showing 164 changed files with 10,701 additions and 4,546 deletions.
4 changes: 2 additions & 2 deletions .github/codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ coverage:
# adjust accordingly based on how flaky your tests are
# this allows a 1% drop from the previous base commit coverage
threshold: 1%
notify:
after_n_builds: 3
comment:
after_n_builds: 3
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ lint-fix: ## Lint and automatically fix errors in the python source code. Repor
codespell -w $(CODESPELL_ARGS)

lint: ## Lint the python and golang sources
pylint $(PY_SOURCES)
bandit -r $(PY_SOURCES) -x node_modules
./web/node_modules/.bin/pyright $(PY_SOURCES)
pylint $(PY_SOURCES)
golangci-lint run -v

migrate: ## Run the Authentik Django server's migrations
Expand Down
6 changes: 3 additions & 3 deletions authentik/admin/api/meta.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Meta API"""
from drf_spectacular.utils import extend_schema
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.response import Response
from rest_framework.viewsets import ViewSet
Expand All @@ -21,7 +21,7 @@ class AppSerializer(PassiveSerializer):
class AppsViewSet(ViewSet):
"""Read-only view list all installed apps"""

permission_classes = [IsAdminUser]
permission_classes = [IsAuthenticated]

@extend_schema(responses={200: AppSerializer(many=True)})
def list(self, request: Request) -> Response:
Expand All @@ -35,7 +35,7 @@ def list(self, request: Request) -> Response:
class ModelViewSet(ViewSet):
"""Read-only view list all installed models"""

permission_classes = [IsAdminUser]
permission_classes = [IsAuthenticated]

@extend_schema(responses={200: AppSerializer(many=True)})
def list(self, request: Request) -> Response:
Expand Down
4 changes: 2 additions & 2 deletions authentik/admin/api/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from drf_spectacular.utils import extend_schema, extend_schema_field
from guardian.shortcuts import get_objects_for_user
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.response import Response
from rest_framework.views import APIView
Expand Down Expand Up @@ -68,7 +68,7 @@ def get_authorizations(self, _):
class AdministrationMetricsViewSet(APIView):
"""Login Metrics per 1h"""

permission_classes = [IsAdminUser]
permission_classes = [IsAuthenticated]

@extend_schema(responses={200: LoginMetricsSerializer(many=False)})
def get(self, request: Request) -> Response:
Expand Down
4 changes: 2 additions & 2 deletions authentik/admin/api/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from drf_spectacular.utils import extend_schema
from gunicorn import version_info as gunicorn_version
from rest_framework.fields import SerializerMethodField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
Expand All @@ -17,6 +16,7 @@
from authentik.lib.utils.reflection import get_env
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost
from authentik.rbac.permissions import HasPermission


class RuntimeDict(TypedDict):
Expand Down Expand Up @@ -88,7 +88,7 @@ def get_embedded_outpost_host(self, request: Request) -> str:
class SystemView(APIView):
"""Get system information."""

permission_classes = [IsAdminUser]
permission_classes = [HasPermission("authentik_rbac.view_system_info")]
pagination_class = None
filter_backends = []
serializer_class = SystemSerializer
Expand Down
6 changes: 4 additions & 2 deletions authentik/admin/api/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
ListField,
SerializerMethodField,
)
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
from structlog.stdlib import get_logger

from authentik.api.decorators import permission_required
from authentik.core.api.utils import PassiveSerializer
from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus
from authentik.rbac.permissions import HasPermission

LOGGER = get_logger()

Expand Down Expand Up @@ -63,7 +64,7 @@ def to_representation(self, instance: TaskInfo):
class TaskViewSet(ViewSet):
"""Read-only view set that returns all background tasks"""

permission_classes = [IsAdminUser]
permission_classes = [HasPermission("authentik_rbac.view_system_tasks")]
serializer_class = TaskSerializer

@extend_schema(
Expand Down Expand Up @@ -93,6 +94,7 @@ def list(self, request: Request) -> Response:
tasks = sorted(TaskInfo.all().values(), key=lambda task: task.task_name)
return Response(TaskSerializer(tasks, many=True).data)

@permission_required(None, ["authentik_rbac.run_system_tasks"])
@extend_schema(
request=OpenApiTypes.NONE,
responses={
Expand Down
4 changes: 2 additions & 2 deletions authentik/admin/api/workers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
from django.conf import settings
from drf_spectacular.utils import extend_schema, inline_serializer
from rest_framework.fields import IntegerField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

from authentik.rbac.permissions import HasPermission
from authentik.root.celery import CELERY_APP


class WorkerView(APIView):
"""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()}))
def get(self, request: Request) -> Response:
Expand Down
6 changes: 3 additions & 3 deletions authentik/api/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from rest_framework.filters import BaseFilterBackend
from rest_framework.permissions import BasePermission
from rest_framework.request import Request
from rest_framework_guardian.filters import ObjectPermissionsFilter

from authentik.api.authentication import validate_auth
from authentik.rbac.filters import ObjectFilter


class OwnerFilter(BaseFilterBackend):
Expand All @@ -26,14 +26,14 @@ def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySe
class SecretKeyFilter(DjangoFilterBackend):
"""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:
auth_header = get_authorization_header(request)
token = validate_auth(auth_header)
if token and token == settings.SECRET_KEY:
return queryset
queryset = ObjectPermissionsFilter().filter_queryset(request, queryset, view)
queryset = ObjectFilter().filter_queryset(request, queryset, view)
return super().filter_queryset(request, queryset, view)


Expand Down
16 changes: 9 additions & 7 deletions authentik/api/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,25 @@
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"""

def wrapper_outter(func: Callable):
"""Check permissions for a single custom action"""

@wraps(func)
def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response:
if perm:
if obj_perm:
obj = self.get_object()
if not request.user.has_perm(perm, obj):
LOGGER.debug("denying access for object", user=request.user, perm=perm, obj=obj)
if not request.user.has_perm(obj_perm, obj):
LOGGER.debug(
"denying access for object", user=request.user, perm=obj_perm, obj=obj
)
return self.permission_denied(request)
if other_perms:
for other_perm in other_perms:
if global_perms:
for other_perm in global_perms:
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 func(self, request, *args, **kwargs)

Expand Down
7 changes: 7 additions & 0 deletions authentik/api/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,10 @@ def get_paginated_response_schema(self, schema):
},
"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
1 change: 1 addition & 0 deletions authentik/api/tests/test_viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def viewset_tester_factory(test_viewset: type[ModelViewSet]) -> Callable:

def tester(self: TestModelViewSets):
self.assertIsNotNone(getattr(test_viewset, "search_fields", None))
self.assertIsNotNone(getattr(test_viewset, "ordering", None))
filterset_class = getattr(test_viewset, "filterset_class", None)
if not filterset_class:
self.assertIsNotNone(getattr(test_viewset, "filterset_fields", None))
Expand Down
3 changes: 1 addition & 2 deletions authentik/blueprints/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, DateTimeField, JSONField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ModelSerializer
Expand Down Expand Up @@ -87,11 +86,11 @@ class Meta:
class BlueprintInstanceViewSet(UsedByMixin, ModelViewSet):
"""Blueprint instances"""

permission_classes = [IsAdminUser]
serializer_class = BlueprintInstanceSerializer
queryset = BlueprintInstance.objects.all()
search_fields = ["name", "path"]
filterset_fields = ["name", "path"]
ordering = ["name"]

@extend_schema(
responses={
Expand Down
18 changes: 14 additions & 4 deletions authentik/blueprints/v1/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,28 @@
Source,
UserSourceConnection,
)
from authentik.enterprise.models import LicenseUsage
from authentik.events.utils import cleanse_dict
from authentik.flows.models import FlowToken, Stage
from authentik.lib.models import SerializerModel
from authentik.lib.sentry import SentryIgnoredException
from authentik.outposts.models import OutpostServiceConnection
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
# Update website/developer-docs/blueprints/v1/models.md when used
SERIALIZER_CONTEXT_BLUEPRINT = "blueprint_entry"


def is_model_allowed(model: type[Model]) -> bool:
"""Check if model is allowed"""
def excluded_models() -> list[type[Model]]:
"""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
from django.contrib.auth.models import Group as DjangoGroup
from django.contrib.auth.models import User as DjangoUser

excluded_models = (
return (
DjangoUser,
DjangoGroup,
# Base classes
Expand All @@ -69,8 +72,15 @@ def is_model_allowed(model: type[Model]) -> bool:
AuthenticatedSession,
# Classes which are only internally managed
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):
Expand Down
4 changes: 2 additions & 2 deletions authentik/core/api/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from structlog.stdlib import get_logger
from structlog.testing import capture_logs

Expand All @@ -38,6 +37,7 @@
from authentik.policies.api.exec import PolicyTestResultSerializer
from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult
from authentik.rbac.filters import ObjectFilter

LOGGER = get_logger()

Expand Down Expand Up @@ -122,7 +122,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
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:
if backend == ObjectFilter:
continue
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
Expand Down
26 changes: 10 additions & 16 deletions authentik/core/api/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from json import loads
from typing import Optional

from django.db.models.query import QuerySet
from django.http import Http404
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
from django_filters.filterset import FilterSet
Expand All @@ -14,12 +13,12 @@
from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter

from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer, is_dict
from authentik.core.models import Group, User
from authentik.rbac.api.roles import RoleSerializer


class GroupMemberSerializer(ModelSerializer):
Expand Down Expand Up @@ -49,6 +48,12 @@ class GroupSerializer(ModelSerializer):
users_obj = ListSerializer(
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)

num_pk = IntegerField(read_only=True)
Expand All @@ -71,8 +76,10 @@ class Meta:
"parent",
"parent_name",
"users",
"attributes",
"users_obj",
"attributes",
"roles",
"roles_obj",
]
extra_kwargs = {
"users": {
Expand Down Expand Up @@ -138,19 +145,6 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
filterset_class = GroupFilter
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"])
@extend_schema(
request=UserAccountSerializer,
Expand Down
1 change: 1 addition & 0 deletions authentik/core/api/transactional_applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class TransactionApplicationResponseSerializer(PassiveSerializer):
class TransactionalApplicationView(APIView):
"""Create provider and application and attach them in a single transaction"""

# TODO: Migrate to a more specific permission
permission_classes = [IsAdminUser]

@extend_schema(
Expand Down
Loading

0 comments on commit 29fc42f

Please sign in to comment.