Skip to content

Commit

Permalink
Merge branch 'main' into dev
Browse files Browse the repository at this point in the history
* main: (34 commits)
  web: bump API Client version (#9299)
  core: fix api schema for users and groups (#9298)
  providers/oauth2: fix refresh_token grant returning incorrect id_token (#9275)
  web: bump @sentry/browser from 7.110.0 to 7.110.1 in /web in the sentry group (#9278)
  core, web: update translations (#9277)
  web: bump the rollup group in /web with 3 updates (#9280)
  web: bump lit from 3.1.2 to 3.1.3 in /web (#9282)
  web: bump @lit/context from 1.1.0 to 1.1.1 in /web (#9281)
  website: bump @types/react from 18.2.78 to 18.2.79 in /website (#9286)
  core: bump goauthentik.io/api/v3 from 3.2024022.10 to 3.2024022.11 (#9285)
  core: bump sqlparse from 0.4.4 to 0.5.0 (#9276)
  lifecycle: gunicorn: fix app preload (#9274)
  events: add indexes (#9272)
  web/flows: fix passwordless hidden without input (#9273)
  root: fix geoipupdate arguments (#9271)
  website/docs: cleanup more (#9249)
  web: bump API Client version (#9270)
  sources: add SCIM source (#3051)
  core: delegated group member management (#9254)
  web: bump API Client version (#9269)
  ...
  • Loading branch information
kensternberg-authentik committed Apr 16, 2024
2 parents 085debf + fbf0e4a commit f19ed14
Show file tree
Hide file tree
Showing 121 changed files with 6,740 additions and 1,038 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ jobs:
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
- name: radius
glob: tests/e2e/test_provider_radius*
- name: scim
glob: tests/e2e/test_source_scim*
- name: flows
glob: tests/e2e/test_flows*
steps:
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/gen-update-webauthn-mds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ on:
workflow_dispatch:
schedule:
- cron: '30 1 1,15 * *'

env:
POSTGRES_DB: authentik
POSTGRES_USER: authentik
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"

jobs:
build:
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
GOARM="${TARGETVARIANT#v}" go build -o /go/authentik ./cmd/server

# Stage 4: MaxMind GeoIP
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.0 as geoip
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.0.1 as geoip

ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
ENV GEOIPUPDATE_VERBOSE="true"
ENV GEOIPUPDATE_VERBOSE="1"
ENV GEOIPUPDATE_ACCOUNT_ID_FILE="/run/secrets/GEOIPUPDATE_ACCOUNT_ID"
ENV GEOIPUPDATE_LICENSE_KEY_FILE="/run/secrets/GEOIPUPDATE_LICENSE_KEY"

Expand Down
10 changes: 10 additions & 0 deletions authentik/api/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from drf_spectacular.types import OpenApiTypes
from rest_framework.settings import api_settings

from authentik.api.apps import AuthentikAPIConfig
from authentik.api.pagination import PAGINATION_COMPONENT_NAME, PAGINATION_SCHEMA


Expand Down Expand Up @@ -101,3 +102,12 @@ def postprocess_schema_responses(result, generator: SchemaGenerator, **kwargs):
comp = result["components"]["schemas"][component]
comp["additionalProperties"] = {}
return result


def preprocess_schema_exclude_non_api(endpoints, **kwargs):
"""Filter out all API Views which are not mounted under /api"""
return [
(path, path_regex, method, callback)
for path, path_regex, method, callback in endpoints
if path.startswith("/" + AuthentikAPIConfig.mountpoint)
]
3 changes: 3 additions & 0 deletions authentik/blueprints/v1/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from authentik.policies.reputation.models import Reputation
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
from authentik.providers.scim.models import SCIMGroup, SCIMUser
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
from authentik.stages.authenticator_webauthn.models import WebAuthnDeviceType
from authentik.tenants.models import Tenant

Expand Down Expand Up @@ -97,6 +98,8 @@ def excluded_models() -> list[type[Model]]:
RefreshToken,
Reputation,
WebAuthnDeviceType,
SCIMSourceUser,
SCIMSourceGroup,
)


Expand Down
65 changes: 50 additions & 15 deletions authentik/core/api/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
from django.http import Http404
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
from django_filters.filterset import FilterSet
from drf_spectacular.utils import OpenApiResponse, extend_schema
from drf_spectacular.utils import (
OpenApiParameter,
OpenApiResponse,
extend_schema,
extend_schema_field,
)
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import CharField, IntegerField
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
Expand Down Expand Up @@ -45,9 +50,7 @@ class GroupSerializer(ModelSerializer):
"""Group Serializer"""

attributes = JSONDictField(required=False)
users_obj = ListSerializer(
child=GroupMemberSerializer(), read_only=True, source="users", required=False
)
users_obj = SerializerMethodField(allow_null=True)
roles_obj = ListSerializer(
child=RoleSerializer(),
read_only=True,
Expand All @@ -58,6 +61,19 @@ class GroupSerializer(ModelSerializer):

num_pk = IntegerField(read_only=True)

@property
def _should_include_users(self) -> bool:
request: Request = self.context.get("request", None)
if not request:
return True
return str(request.query_params.get("include_users", "true")).lower() == "true"

@extend_schema_field(GroupMemberSerializer(many=True))
def get_users_obj(self, instance: Group) -> list[GroupMemberSerializer] | None:
if not self._should_include_users:
return None
return GroupMemberSerializer(instance.users, many=True).data

def validate_parent(self, parent: Group | None):
"""Validate group parent (if set), ensuring the parent isn't itself"""
if not self.instance or not parent:
Expand Down Expand Up @@ -130,30 +146,43 @@ class Meta:
fields = ["name", "is_superuser", "members_by_pk", "attributes", "members_by_username"]


class UserAccountSerializer(PassiveSerializer):
"""Account adding/removing operations"""

pk = IntegerField(required=True)


class GroupViewSet(UsedByMixin, ModelViewSet):
"""Group Viewset"""

class UserAccountSerializer(PassiveSerializer):
"""Account adding/removing operations"""

pk = IntegerField(required=True)

queryset = Group.objects.all().select_related("parent").prefetch_related("users")
serializer_class = GroupSerializer
search_fields = ["name", "is_superuser"]
filterset_class = GroupFilter
ordering = ["name"]

@permission_required(None, ["authentik_core.add_user"])
@extend_schema(
parameters=[
OpenApiParameter("include_users", bool, default=True),
]
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)

@permission_required("authentik_core.add_user_to_group")
@extend_schema(
request=UserAccountSerializer,
responses={
204: OpenApiResponse(description="User added"),
404: OpenApiResponse(description="User not found"),
},
)
@action(detail=True, methods=["POST"], pagination_class=None, filter_backends=[])
@action(
detail=True,
methods=["POST"],
pagination_class=None,
filter_backends=[],
permission_classes=[],
)
def add_user(self, request: Request, pk: str) -> Response:
"""Add user to group"""
group: Group = self.get_object()
Expand All @@ -169,15 +198,21 @@ def add_user(self, request: Request, pk: str) -> Response:
group.users.add(user)
return Response(status=204)

@permission_required(None, ["authentik_core.add_user"])
@permission_required("authentik_core.remove_user_from_group")
@extend_schema(
request=UserAccountSerializer,
responses={
204: OpenApiResponse(description="User added"),
404: OpenApiResponse(description="User not found"),
},
)
@action(detail=True, methods=["POST"], pagination_class=None, filter_backends=[])
@action(
detail=True,
methods=["POST"],
pagination_class=None,
filter_backends=[],
permission_classes=[],
)
def remove_user(self, request: Request, pk: str) -> Response:
"""Add user to group"""
group: Group = self.get_object()
Expand Down
23 changes: 22 additions & 1 deletion authentik/core/api/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,26 @@ class UserSerializer(ModelSerializer):
queryset=Group.objects.all().order_by("name"),
default=list,
)
groups_obj = ListSerializer(child=UserGroupSerializer(), read_only=True, source="ak_groups")
groups_obj = SerializerMethodField(allow_null=True)
uid = CharField(read_only=True)
username = CharField(
max_length=150,
validators=[UniqueValidator(queryset=User.objects.all().order_by("username"))],
)

@property
def _should_include_groups(self) -> bool:
request: Request = self.context.get("request", None)
if not request:
return True
return str(request.query_params.get("include_groups", "true")).lower() == "true"

@extend_schema_field(UserGroupSerializer(many=True))
def get_groups_obj(self, instance: User) -> list[UserGroupSerializer] | None:
if not self._should_include_groups:
return None
return UserGroupSerializer(instance.ak_groups, many=True).data

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
Expand Down Expand Up @@ -397,6 +410,14 @@ class UserViewSet(UsedByMixin, ModelViewSet):
def get_queryset(self): # pragma: no cover
return User.objects.all().exclude_anonymous().prefetch_related("ak_groups")

@extend_schema(
parameters=[
OpenApiParameter("include_groups", bool, default=True),
]
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)

def _create_recovery_link(self) -> tuple[str, Token]:
"""Create a recovery link (when the current brand has a recovery flow set),
that can either be shown to an admin or sent to the user directly"""
Expand Down
52 changes: 52 additions & 0 deletions authentik/core/migrations/0035_alter_group_options_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Generated by Django 5.0.4 on 2024-04-15 11:28

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("authentik_core", "0034_alter_authenticatedsession_expires_and_more"),
("authentik_rbac", "0003_alter_systempermission_options"),
]

operations = [
migrations.AlterModelOptions(
name="group",
options={
"permissions": [
("add_user_to_group", "Add user to group"),
("remove_user_from_group", "Remove user from group"),
],
"verbose_name": "Group",
"verbose_name_plural": "Groups",
},
),
migrations.AddIndex(
model_name="group",
index=models.Index(fields=["name"], name="authentik_c_name_9ba8e4_idx"),
),
migrations.AddIndex(
model_name="user",
index=models.Index(fields=["last_login"], name="authentik_c_last_lo_f0179a_idx"),
),
migrations.AddIndex(
model_name="user",
index=models.Index(
fields=["password_change_date"], name="authentik_c_passwor_eec915_idx"
),
),
migrations.AddIndex(
model_name="user",
index=models.Index(fields=["uuid"], name="authentik_c_uuid_3dae2f_idx"),
),
migrations.AddIndex(
model_name="user",
index=models.Index(fields=["path"], name="authentik_c_path_b1f502_idx"),
),
migrations.AddIndex(
model_name="user",
index=models.Index(fields=["type"], name="authentik_c_type_ecf60d_idx"),
),
]
14 changes: 13 additions & 1 deletion authentik/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,13 @@ class Meta:
"parent",
),
)
indexes = [models.Index(fields=["name"])]
verbose_name = _("Group")
verbose_name_plural = _("Groups")
permissions = [
("add_user_to_group", _("Add user to group")),
("remove_user_from_group", _("Remove user from group")),
]


class UserQuerySet(models.QuerySet):
Expand Down Expand Up @@ -323,6 +328,13 @@ class Meta:
("preview_user", _("Can preview user data sent to providers")),
("view_user_applications", _("View applications the user has access to")),
]
indexes = [
models.Index(fields=["last_login"]),
models.Index(fields=["password_change_date"]),
models.Index(fields=["uuid"]),
models.Index(fields=["path"]),
models.Index(fields=["type"]),
]
authentik_signals_ignored_fields = [
# Logged by the events `password_set`
# the `password_set` action/signal doesn't currently convey which user
Expand Down Expand Up @@ -659,7 +671,7 @@ def expire_action(self, *args, **kwargs):
return self.delete(*args, **kwargs)

@classmethod
def filter_not_expired(cls, **kwargs) -> QuerySet:
def filter_not_expired(cls, **kwargs) -> QuerySet["Token"]:
"""Filer for tokens which are not expired yet or are not expiring,
and match filters in `kwargs`"""
for obj in cls.objects.filter(**kwargs).filter(Q(expires__lt=now(), expiring=True)):
Expand Down

0 comments on commit f19ed14

Please sign in to comment.