Skip to content

Commit

Permalink
Merge branch 'web/element/ak-select-table' into web/policy-wizard
Browse files Browse the repository at this point in the history
* web/element/ak-select-table:
  Provide unit test accessibility to Firefox and Safari; wrap calls to manipulate test DOMs directly in a browser.exec call so they run in the proper context and be await()ed properly
  web: finalize testing for tables
  web: added basic unit testing to API-free tables
  website/docs: cve release notes (#11026)
  security: fix CVE-2024-42490 (#11022)
  web: bump API Client version (#11021)
  providers/scim: optimize sending all members within a group (#9968)
  providers/scim: add API endpoint to sync single user (#8486)
  web: bump chromedriver from 127.0.3 to 128.0.0 in /tests/wdio (#11017)
  web: dual-select uses, part 2: dual-select harder (#9377)
  web: fix flash of unstructured content, add tests for it (#11013)
  core: bump drf-orjson-renderer from 1.7.2 to 1.7.3 (#11015)
  core: bump github.com/gorilla/sessions from 1.3.0 to 1.4.0 (#11002)
  web: interim commit of the basic sortable & selectable table.
  website/docs: Correct the forward authentication configuration template for Caddy (#11012)
  web: test for flash of unstructured content
  web: comment on state management in API layer, move file to point to correct component under test.
  web: fix Flash of Unstructured Content while SearchSelect is loading from the backend
  • Loading branch information
kensternberg-authentik committed Aug 26, 2024
2 parents efba1b1 + 484ebe9 commit 1f1c15b
Show file tree
Hide file tree
Showing 73 changed files with 1,667 additions and 715 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci-outpost.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.54.2
version: latest
args: --timeout 5000s --verbose
skip-cache: true
test-unittest:
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build

# Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.22-fips-bookworm AS go-builder
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS go-builder

ARG TARGETOS
ARG TARGETARCH
Expand Down
3 changes: 2 additions & 1 deletion authentik/core/api/used_by.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from rest_framework.response import Response

from authentik.core.api.utils import PassiveSerializer
from authentik.rbac.filters import ObjectFilter


class DeleteAction(Enum):
Expand Down Expand Up @@ -53,7 +54,7 @@ class UsedByMixin:
@extend_schema(
responses={200: UsedBySerializer(many=True)},
)
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def used_by(self, request: Request, *args, **kwargs) -> Response:
"""Get a list of all objects that use this object"""
model: Model = self.get_object()
Expand Down
5 changes: 3 additions & 2 deletions authentik/crypto/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
from authentik.rbac.decorators import permission_required
from authentik.rbac.filters import ObjectFilter

LOGGER = get_logger()

Expand Down Expand Up @@ -265,7 +266,7 @@ def generate(self, request: Request) -> Response:
],
responses={200: CertificateDataSerializer(many=False)},
)
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def view_certificate(self, request: Request, pk: str) -> Response:
"""Return certificate-key pairs certificate and log access"""
certificate: CertificateKeyPair = self.get_object()
Expand Down Expand Up @@ -295,7 +296,7 @@ def view_certificate(self, request: Request, pk: str) -> Response:
],
responses={200: CertificateDataSerializer(many=False)},
)
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def view_private_key(self, request: Request, pk: str) -> Response:
"""Return certificate-key pairs private key and log access"""
certificate: CertificateKeyPair = self.get_object()
Expand Down
60 changes: 60 additions & 0 deletions authentik/crypto/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,46 @@ def test_private_key_download(self):
self.assertEqual(200, response.status_code)
self.assertIn("Content-Disposition", response)

def test_certificate_download_denied(self):
"""Test certificate export (download)"""
self.client.logout()
keypair = create_test_cert()
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-certificate",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(403, response.status_code)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-certificate",
kwargs={"pk": keypair.pk},
),
data={"download": True},
)
self.assertEqual(403, response.status_code)

def test_private_key_download_denied(self):
"""Test private_key export (download)"""
self.client.logout()
keypair = create_test_cert()
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-private-key",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(403, response.status_code)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-private-key",
kwargs={"pk": keypair.pk},
),
data={"download": True},
)
self.assertEqual(403, response.status_code)

def test_used_by(self):
"""Test used_by endpoint"""
self.client.force_login(create_test_admin_user())
Expand Down Expand Up @@ -246,6 +286,26 @@ def test_used_by(self):
],
)

def test_used_by_denied(self):
"""Test used_by endpoint"""
self.client.logout()
keypair = create_test_cert()
OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://localhost",
signing_key=keypair,
)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-used-by",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(403, response.status_code)

def test_discovery(self):
"""Test certificate discovery"""
name = generate_id()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
from authentik.enterprise.providers.google_workspace.tasks import google_workspace_sync
from authentik.enterprise.providers.google_workspace.tasks import (
google_workspace_sync,
google_workspace_sync_objects,
)
from authentik.lib.sync.outgoing.api import OutgoingSyncProviderStatusMixin


Expand Down Expand Up @@ -52,3 +55,4 @@ class GoogleWorkspaceProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixi
search_fields = ["name"]
ordering = ["name"]
sync_single_task = google_workspace_sync
sync_objects_task = google_workspace_sync_objects
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider
from authentik.enterprise.providers.microsoft_entra.tasks import microsoft_entra_sync
from authentik.enterprise.providers.microsoft_entra.tasks import (
microsoft_entra_sync,
microsoft_entra_sync_objects,
)
from authentik.lib.sync.outgoing.api import OutgoingSyncProviderStatusMixin


Expand Down Expand Up @@ -50,3 +53,4 @@ class MicrosoftEntraProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixin
search_fields = ["name"]
ordering = ["name"]
sync_single_task = microsoft_entra_sync
sync_objects_task = microsoft_entra_sync_objects
3 changes: 2 additions & 1 deletion authentik/flows/api/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
)
from authentik.lib.views import bad_request_message
from authentik.rbac.decorators import permission_required
from authentik.rbac.filters import ObjectFilter

LOGGER = get_logger()

Expand Down Expand Up @@ -281,7 +282,7 @@ def set_background_url(self, request: Request, slug: str):
400: OpenApiResponse(description="Flow not applicable"),
},
)
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def execute(self, request: Request, slug: str):
"""Execute flow for current user"""
# Because we pre-plan the flow here, and not in the planner, we need to manually clear
Expand Down
56 changes: 51 additions & 5 deletions authentik/lib/sync/outgoing/api.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
from collections.abc import Callable

from celery import Task
from django.utils.text import slugify
from drf_spectacular.utils import OpenApiResponse, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import BooleanField
from rest_framework.fields import BooleanField, CharField, ChoiceField
from rest_framework.request import Request
from rest_framework.response import Response

from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.models import Group, User
from authentik.events.api.tasks import SystemTaskSerializer
from authentik.events.logs import LogEvent, LogEventSerializer
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
from authentik.lib.utils.reflection import class_to_path
from authentik.rbac.filters import ObjectFilter


class SyncStatusSerializer(PassiveSerializer):
Expand All @@ -20,10 +23,29 @@ class SyncStatusSerializer(PassiveSerializer):
tasks = SystemTaskSerializer(many=True, read_only=True)


class SyncObjectSerializer(PassiveSerializer):
"""Sync object serializer"""

sync_object_model = ChoiceField(
choices=(
(class_to_path(User), "user"),
(class_to_path(Group), "group"),
)
)
sync_object_id = CharField()


class SyncObjectResultSerializer(PassiveSerializer):
"""Result of a single object sync"""

messages = LogEventSerializer(many=True, read_only=True)


class OutgoingSyncProviderStatusMixin:
"""Common API Endpoints for Outgoing sync providers"""

sync_single_task: Callable = None
sync_single_task: type[Task] = None
sync_objects_task: type[Task] = None

@extend_schema(
responses={
Expand All @@ -36,7 +58,7 @@ class OutgoingSyncProviderStatusMixin:
detail=True,
pagination_class=None,
url_path="sync/status",
filter_backends=[],
filter_backends=[ObjectFilter],
)
def sync_status(self, request: Request, pk: int) -> Response:
"""Get provider's sync status"""
Expand All @@ -55,6 +77,30 @@ def sync_status(self, request: Request, pk: int) -> Response:
}
return Response(SyncStatusSerializer(status).data)

@extend_schema(
request=SyncObjectSerializer,
responses={200: SyncObjectResultSerializer()},
)
@action(
methods=["POST"],
detail=True,
pagination_class=None,
url_path="sync/object",
filter_backends=[ObjectFilter],
)
def sync_object(self, request: Request, pk: int) -> Response:
"""Sync/Re-sync a single user/group object"""
provider: OutgoingSyncProvider = self.get_object()
params = SyncObjectSerializer(data=request.data)
params.is_valid(raise_exception=True)
res: list[LogEvent] = self.sync_objects_task.delay(
params.validated_data["sync_object_model"],
page=1,
provider_pk=provider.pk,
pk=params.validated_data["sync_object_id"],
).get()
return Response(SyncObjectResultSerializer(instance={"messages": res}).data)


class OutgoingSyncConnectionCreateMixin:
"""Mixin for connection objects that fetches remote data upon creation"""
Expand Down
4 changes: 2 additions & 2 deletions authentik/lib/sync/outgoing/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def sync_single(
return
task.set_status(TaskStatus.SUCCESSFUL, *messages)

def sync_objects(self, object_type: str, page: int, provider_pk: int):
def sync_objects(self, object_type: str, page: int, provider_pk: int, **filter):
_object_type = path_to_class(object_type)
self.logger = get_logger().bind(
provider_type=class_to_path(self._provider_model),
Expand All @@ -120,7 +120,7 @@ def sync_objects(self, object_type: str, page: int, provider_pk: int):
client = provider.client_for_model(_object_type)
except TransientSyncException:
return messages
paginator = Paginator(provider.get_object_qs(_object_type), PAGE_SIZE)
paginator = Paginator(provider.get_object_qs(_object_type).filter(**filter), PAGE_SIZE)
if client.can_discover:
self.logger.debug("starting discover")
client.discover()
Expand Down
3 changes: 2 additions & 1 deletion authentik/outposts/api/service_connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
KubernetesServiceConnection,
OutpostServiceConnection,
)
from authentik.rbac.filters import ObjectFilter


class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
Expand Down Expand Up @@ -75,7 +76,7 @@ class ServiceConnectionViewSet(
filterset_fields = ["name"]

@extend_schema(responses={200: ServiceConnectionStateSerializer(many=False)})
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def state(self, request: Request, pk: str) -> Response:
"""Get the service connection's state"""
connection = self.get_object()
Expand Down
3 changes: 2 additions & 1 deletion authentik/providers/scim/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from authentik.core.api.used_by import UsedByMixin
from authentik.lib.sync.outgoing.api import OutgoingSyncProviderStatusMixin
from authentik.providers.scim.models import SCIMProvider
from authentik.providers.scim.tasks import scim_sync
from authentik.providers.scim.tasks import scim_sync, scim_sync_objects


class SCIMProviderSerializer(ProviderSerializer):
Expand Down Expand Up @@ -42,3 +42,4 @@ class SCIMProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixin, ModelVie
search_fields = ["name", "url"]
ordering = ["name", "url"]
sync_single_task = scim_sync
sync_objects_task = scim_sync_objects
Loading

0 comments on commit 1f1c15b

Please sign in to comment.