providers/scim: add API endpoint to sync single user (#8486)
* add api Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add UI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * format Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
		@ -6,7 +6,10 @@ from authentik.core.api.providers import ProviderSerializer
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,10 @@ from authentik.core.api.providers import ProviderSerializer
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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):
 | 
			
		||||
@ -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={
 | 
			
		||||
@ -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"""
 | 
			
		||||
@ -55,6 +77,30 @@ class OutgoingSyncProviderStatusMixin:
 | 
			
		||||
            }
 | 
			
		||||
        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"""
 | 
			
		||||
 | 
			
		||||
@ -105,7 +105,7 @@ class SyncTasks:
 | 
			
		||||
                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),
 | 
			
		||||
@ -120,7 +120,7 @@ class SyncTasks:
 | 
			
		||||
            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()
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ from authentik.core.api.providers import ProviderSerializer
 | 
			
		||||
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):
 | 
			
		||||
@ -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
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										148
									
								
								schema.yml
									
									
									
									
									
								
							@ -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
 | 
			
		||||
@ -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
 | 
			
		||||
@ -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
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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> {
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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 { GoogleWorkspaceProviderUser, ProvidersApi } from "@goauthentik/api";
 | 
			
		||||
import { GoogleWorkspaceProviderUser, ProvidersApi, SyncObjectModelEnum } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-provider-google-workspace-users-list")
 | 
			
		||||
export class GoogleWorkspaceProviderUserList extends Table<GoogleWorkspaceProviderUser> {
 | 
			
		||||
@ -22,6 +24,23 @@ export class GoogleWorkspaceProviderUserList extends Table<GoogleWorkspaceProvid
 | 
			
		||||
    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.User}
 | 
			
		||||
                    .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
 | 
			
		||||
 | 
			
		||||
@ -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 { MicrosoftEntraProviderGroup, ProvidersApi } from "@goauthentik/api";
 | 
			
		||||
import { MicrosoftEntraProviderGroup, ProvidersApi, SyncObjectModelEnum } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-provider-microsoft-entra-groups-list")
 | 
			
		||||
export class MicrosoftEntraProviderGroupList extends Table<MicrosoftEntraProviderGroup> {
 | 
			
		||||
@ -19,6 +21,23 @@ export class MicrosoftEntraProviderGroupList extends Table<MicrosoftEntraProvide
 | 
			
		||||
        return 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)
 | 
			
		||||
                        .providersMicrosoftEntraSyncObjectCreate}
 | 
			
		||||
                    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
 | 
			
		||||
 | 
			
		||||
@ -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 { MicrosoftEntraProviderUser, ProvidersApi } from "@goauthentik/api";
 | 
			
		||||
import { MicrosoftEntraProviderUser, ProvidersApi, SyncObjectModelEnum } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-provider-microsoft-entra-users-list")
 | 
			
		||||
export class MicrosoftEntraProviderUserList extends Table<MicrosoftEntraProviderUser> {
 | 
			
		||||
@ -22,6 +24,23 @@ export class MicrosoftEntraProviderUserList extends Table<MicrosoftEntraProvider
 | 
			
		||||
    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.User}
 | 
			
		||||
                    .sync=${new ProvidersApi(DEFAULT_CONFIG)
 | 
			
		||||
                        .providersMicrosoftEntraSyncObjectCreate}
 | 
			
		||||
                    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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										131
									
								
								web/src/elements/sync/SyncObjectForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								web/src/elements/sync/SyncObjectForm.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,131 @@
 | 
			
		||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search-no-default";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { Form } from "@goauthentik/elements/forms/Form";
 | 
			
		||||
import "@goauthentik/elements/forms/HorizontalFormElement";
 | 
			
		||||
import "@goauthentik/elements/forms/SearchSelect";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { TemplateResult, html, nothing } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    CoreApi,
 | 
			
		||||
    CoreGroupsListRequest,
 | 
			
		||||
    CoreUsersListRequest,
 | 
			
		||||
    Group,
 | 
			
		||||
    InitOverrideFunction,
 | 
			
		||||
    SyncObjectModelEnum,
 | 
			
		||||
    SyncObjectRequest,
 | 
			
		||||
    SyncObjectResult,
 | 
			
		||||
    User,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-sync-object-form")
 | 
			
		||||
export class SyncObjectForm extends Form<SyncObjectRequest> {
 | 
			
		||||
    @property({ type: Number })
 | 
			
		||||
    provider?: number;
 | 
			
		||||
 | 
			
		||||
    @property()
 | 
			
		||||
    model: SyncObjectModelEnum = SyncObjectModelEnum.UnknownDefaultOpenApi;
 | 
			
		||||
 | 
			
		||||
    @property({ attribute: false })
 | 
			
		||||
    result?: SyncObjectResult;
 | 
			
		||||
 | 
			
		||||
    @property({ attribute: false })
 | 
			
		||||
    sync: (
 | 
			
		||||
        requestParameters: {
 | 
			
		||||
            id: number;
 | 
			
		||||
            syncObjectRequest: SyncObjectRequest;
 | 
			
		||||
        },
 | 
			
		||||
        initOverrides?: RequestInit | InitOverrideFunction,
 | 
			
		||||
    ) => Promise<SyncObjectResult> = (_, __) => {
 | 
			
		||||
        return Promise.reject();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    getSuccessMessage(): string {
 | 
			
		||||
        return msg("Successfully triggered sync.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async send(data: SyncObjectRequest): Promise<void> {
 | 
			
		||||
        data.syncObjectModel = this.model;
 | 
			
		||||
        this.result = await this.sync({
 | 
			
		||||
            id: this.provider || 0,
 | 
			
		||||
            syncObjectRequest: data,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderSelectUser() {
 | 
			
		||||
        return html`<ak-form-element-horizontal label=${msg("User")} name="syncObjectId">
 | 
			
		||||
            <ak-search-select
 | 
			
		||||
                .fetchObjects=${async (query?: string): Promise<User[]> => {
 | 
			
		||||
                    const args: CoreUsersListRequest = {
 | 
			
		||||
                        ordering: "username",
 | 
			
		||||
                    };
 | 
			
		||||
                    if (query !== undefined) {
 | 
			
		||||
                        args.search = query;
 | 
			
		||||
                    }
 | 
			
		||||
                    const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList(args);
 | 
			
		||||
                    return users.results;
 | 
			
		||||
                }}
 | 
			
		||||
                .renderElement=${(user: User): string => {
 | 
			
		||||
                    return user.username;
 | 
			
		||||
                }}
 | 
			
		||||
                .renderDescription=${(user: User): TemplateResult => {
 | 
			
		||||
                    return html`${user.name}`;
 | 
			
		||||
                }}
 | 
			
		||||
                .value=${(user: User | undefined): number | undefined => {
 | 
			
		||||
                    return user?.pk;
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
            </ak-search-select>
 | 
			
		||||
        </ak-form-element-horizontal>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderSelectGroup() {
 | 
			
		||||
        return html` <ak-form-element-horizontal label=${msg("Group")} name="syncObjectId">
 | 
			
		||||
            <ak-search-select
 | 
			
		||||
                .fetchObjects=${async (query?: string): Promise<Group[]> => {
 | 
			
		||||
                    const args: CoreGroupsListRequest = {
 | 
			
		||||
                        ordering: "name",
 | 
			
		||||
                    };
 | 
			
		||||
                    if (query !== undefined) {
 | 
			
		||||
                        args.search = query;
 | 
			
		||||
                    }
 | 
			
		||||
                    const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(args);
 | 
			
		||||
                    return groups.results;
 | 
			
		||||
                }}
 | 
			
		||||
                .renderElement=${(group: Group): string => {
 | 
			
		||||
                    return group.name;
 | 
			
		||||
                }}
 | 
			
		||||
                .value=${(group: Group | undefined): string | undefined => {
 | 
			
		||||
                    return group?.pk;
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
            </ak-search-select>
 | 
			
		||||
        </ak-form-element-horizontal>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderResult(): TemplateResult {
 | 
			
		||||
        return html`<ak-form-element-horizontal label=${msg("Log messages")}>
 | 
			
		||||
            <div class="pf-c-form__group-label">
 | 
			
		||||
                <div class="c-form__horizontal-group">
 | 
			
		||||
                    <dl class="pf-c-description-list pf-m-horizontal">
 | 
			
		||||
                        <ak-log-viewer .logs=${this.result?.messages}></ak-log-viewer>
 | 
			
		||||
                    </dl>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </ak-form-element-horizontal> `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    renderForm() {
 | 
			
		||||
        return html` ${this.model === SyncObjectModelEnum.User ? this.renderSelectUser() : nothing}
 | 
			
		||||
        ${this.model === SyncObjectModelEnum.Group ? this.renderSelectGroup() : nothing}
 | 
			
		||||
        ${this.result ? this.renderResult() : html``}`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
    interface HTMLElementTagNameMap {
 | 
			
		||||
        "ak-sync-object-form": SyncObjectForm;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user