Skip to content

Commit

Permalink
providers/scim: add API endpoint to sync single user (#8486)
Browse files Browse the repository at this point in the history
* add api

Signed-off-by: Jens Langhammer <[email protected]>

* add UI

Signed-off-by: Jens Langhammer <[email protected]>

* format

Signed-off-by: Jens Langhammer <[email protected]>

---------

Signed-off-by: Jens Langhammer <[email protected]>
  • Loading branch information
BeryJu authored Aug 22, 2024
1 parent e428e4c commit 46acab3
Show file tree
Hide file tree
Showing 11 changed files with 424 additions and 14 deletions.
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
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/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
148 changes: 148 additions & 0 deletions schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17853,6 +17853,46 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/providers/google_workspace/{id}/sync/object/:
post:
operationId: providers_google_workspace_sync_object_create
description: Sync/Re-sync a single user/group object
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this Google Workspace Provider.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SyncObjectRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/SyncObjectResult'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/providers/google_workspace/{id}/sync/status/:
get:
operationId: providers_google_workspace_sync_status_retrieve
Expand Down Expand Up @@ -18856,6 +18896,46 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/providers/microsoft_entra/{id}/sync/object/:
post:
operationId: providers_microsoft_entra_sync_object_create
description: Sync/Re-sync a single user/group object
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this Microsoft Entra Provider.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SyncObjectRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/SyncObjectResult'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/providers/microsoft_entra/{id}/sync/status/:
get:
operationId: providers_microsoft_entra_sync_status_retrieve
Expand Down Expand Up @@ -21346,6 +21426,46 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/providers/scim/{id}/sync/object/:
post:
operationId: providers_scim_sync_object_create
description: Sync/Re-sync a single user/group object
parameters:
- in: path
name: id
schema:
type: integer
description: A unique integer value identifying this SCIM Provider.
required: true
tags:
- providers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SyncObjectRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/SyncObjectResult'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/providers/scim/{id}/sync/status/:
get:
operationId: providers_scim_sync_status_retrieve
Expand Down Expand Up @@ -51354,6 +51474,34 @@ components:
- user_email
- user_upn
type: string
SyncObjectModelEnum:
enum:
- authentik.core.models.User
- authentik.core.models.Group
type: string
SyncObjectRequest:
type: object
description: Sync object serializer
properties:
sync_object_model:
$ref: '#/components/schemas/SyncObjectModelEnum'
sync_object_id:
type: string
minLength: 1
required:
- sync_object_id
- sync_object_model
SyncObjectResult:
type: object
description: Result of a single object sync
properties:
messages:
type: array
items:
$ref: '#/components/schemas/LogEvent'
readOnly: true
required:
- messages
SyncStatus:
type: object
description: Provider sync status
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/sync/SyncObjectForm";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";

import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";

import { GoogleWorkspaceProviderGroup, ProvidersApi } from "@goauthentik/api";
import { GoogleWorkspaceProviderGroup, ProvidersApi, SyncObjectModelEnum } from "@goauthentik/api";

@customElement("ak-provider-google-workspace-groups-list")
export class GoogleWorkspaceProviderGroupList extends Table<GoogleWorkspaceProviderGroup> {
Expand All @@ -22,6 +24,23 @@ export class GoogleWorkspaceProviderGroupList extends Table<GoogleWorkspaceProvi
checkbox = true;
clearOnRefresh = true;

renderToolbar(): TemplateResult {
return html`<ak-forms-modal cancelText=${msg("Close")} ?closeAfterSuccessfulSubmit=${false}>
<span slot="submit">${msg("Sync")}</span>
<span slot="header">${msg("Sync User")}</span>
<ak-sync-object-form
.provider=${this.providerId}
model=${SyncObjectModelEnum.Group}
.sync=${new ProvidersApi(DEFAULT_CONFIG)
.providersGoogleWorkspaceSyncObjectCreate}
slot="form"
>
</ak-sync-object-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Sync")}</button>
</ak-forms-modal>
${super.renderToolbar()}`;
}

renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
Expand Down
Loading

0 comments on commit 46acab3

Please sign in to comment.