enterprise/providers/google: initial account sync to google workspace (#9384)
* providers/google: initial account sync to google workspace Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start separating scim sync client Signed-off-by: Jens Langhammer <jens@goauthentik.io> * generalize more...ish Signed-off-by: Jens Langhammer <jens@goauthentik.io> * set dispatch_uid Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start generalizing task Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fully separate tasks Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix signals...? Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start google dedupe Signed-off-by: Jens Langhammer <jens@goauthentik.io> * drawing the rest of the owl Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * juse use a whole lot less magic Signed-off-by: Jens Langhammer <jens@goauthentik.io> * member sync, better implement conflict/retry-able exceptions Signed-off-by: Jens Langhammer <jens@goauthentik.io> * max wizards taller Signed-off-by: Jens Langhammer <jens@goauthentik.io> * gen api, basic UI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix some bugs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix a bunch more bugs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * generalize sync status API Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rework sync chart Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add slugify to evaluator Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add test property mappings Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rename to google workspace Signed-off-by: Jens Langhammer <jens@goauthentik.io> * handle existing objects Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix credential render Signed-off-by: Jens Langhammer <jens@goauthentik.io> * verify email has correct domain before syncing user Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix missing docstring Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix lock not being used Signed-off-by: Jens Langhammer <jens@goauthentik.io> * abstract more common stuff away Signed-off-by: Jens Langhammer <jens@goauthentik.io> * backport time limit fix https://github.com/goauthentik/authentik/pull/9546 Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start discovery Signed-off-by: Jens Langhammer <jens@goauthentik.io> * implement discover for google Signed-off-by: Jens Langhammer <jens@goauthentik.io> * prevent same issue as with https://github.com/goauthentik/authentik/pull/9557 Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix sync status Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make group name unique in API Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix reference to old wrapper Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start adding tests man this api client is awful Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add SkipObject Signed-off-by: Jens Langhammer <jens@goauthentik.io> * dont use weak ref Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add group tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add user and group delete options Signed-off-by: Jens Langhammer <jens@goauthentik.io> * set user agent Signed-off-by: Jens Langhammer <jens@goauthentik.io> * if the api's testing tools are awful, let's just make our own Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add more tests and already fix some more bugs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add discover Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add preview banner Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add group import test Signed-off-by: Jens Langhammer <jens@goauthentik.io> * only import users/groups in the correct parent group Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix conflicting args Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix missing schedule Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix web ui Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add default_group_email_domain Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
		| @ -1,4 +1,4 @@ | ||||
| import { SyncStatus } from "@goauthentik/admin/admin-overview/charts/SyncStatusChart"; | ||||
| import { SummarizedSyncStatus } from "@goauthentik/admin/admin-overview/charts/SyncStatusChart"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKChart } from "@goauthentik/elements/charts/Chart"; | ||||
| import "@goauthentik/elements/forms/ConfirmationForm"; | ||||
| @ -10,7 +10,7 @@ import { customElement } from "lit/decorators.js"; | ||||
| import { OutpostsApi } from "@goauthentik/api"; | ||||
|  | ||||
| @customElement("ak-admin-status-chart-outpost") | ||||
| export class OutpostStatusChart extends AKChart<SyncStatus[]> { | ||||
| export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> { | ||||
|     getChartType(): string { | ||||
|         return "doughnut"; | ||||
|     } | ||||
| @ -26,16 +26,16 @@ export class OutpostStatusChart extends AKChart<SyncStatus[]> { | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     async apiRequest(): Promise<SyncStatus[]> { | ||||
|     async apiRequest(): Promise<SummarizedSyncStatus[]> { | ||||
|         const api = new OutpostsApi(DEFAULT_CONFIG); | ||||
|         const outposts = await api.outpostsInstancesList({}); | ||||
|         const outpostStats: SyncStatus[] = []; | ||||
|         const outpostStats: SummarizedSyncStatus[] = []; | ||||
|         await Promise.all( | ||||
|             outposts.results.map(async (element) => { | ||||
|                 const health = await api.outpostsInstancesHealthList({ | ||||
|                     uuid: element.pk || "", | ||||
|                 }); | ||||
|                 const singleStats: SyncStatus = { | ||||
|                 const singleStats: SummarizedSyncStatus = { | ||||
|                     unsynced: 0, | ||||
|                     healthy: 0, | ||||
|                     failed: 0, | ||||
| @ -59,7 +59,7 @@ export class OutpostStatusChart extends AKChart<SyncStatus[]> { | ||||
|         return outpostStats; | ||||
|     } | ||||
|  | ||||
|     getChartData(data: SyncStatus[]): ChartData { | ||||
|     getChartData(data: SummarizedSyncStatus[]): ChartData { | ||||
|         return { | ||||
|             labels: [msg("Healthy outposts"), msg("Outdated outposts"), msg("Unhealthy outposts")], | ||||
|             datasets: data.map((d) => { | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { PaginatedResponse } from "@goauthentik/authentik/elements/table/Table"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { AKChart } from "@goauthentik/elements/charts/Chart"; | ||||
| import "@goauthentik/elements/forms/ConfirmationForm"; | ||||
| @ -6,9 +7,9 @@ import { ChartData, ChartOptions } from "chart.js"; | ||||
| import { msg } from "@lit/localize"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
|  | ||||
| import { ProvidersApi, SourcesApi, SystemTaskStatusEnum } from "@goauthentik/api"; | ||||
| import { ProvidersApi, SourcesApi, SyncStatus, SystemTaskStatusEnum } from "@goauthentik/api"; | ||||
|  | ||||
| export interface SyncStatus { | ||||
| export interface SummarizedSyncStatus { | ||||
|     healthy: number; | ||||
|     failed: number; | ||||
|     unsynced: number; | ||||
| @ -17,7 +18,7 @@ export interface SyncStatus { | ||||
| } | ||||
|  | ||||
| @customElement("ak-admin-status-chart-sync") | ||||
| export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> { | ||||
| export class LDAPSyncStatusChart extends AKChart<SummarizedSyncStatus[]> { | ||||
|     getChartType(): string { | ||||
|         return "doughnut"; | ||||
|     } | ||||
| @ -33,99 +34,91 @@ export class LDAPSyncStatusChart extends AKChart<SyncStatus[]> { | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     async ldapStatus(): Promise<SyncStatus> { | ||||
|         const api = new SourcesApi(DEFAULT_CONFIG); | ||||
|         const sources = await api.sourcesLdapList({}); | ||||
|     async fetchStatus<T>( | ||||
|         listObjects: () => Promise<PaginatedResponse<T>>, | ||||
|         fetchSyncStatus: (element: T) => Promise<SyncStatus>, | ||||
|         label: string, | ||||
|     ): Promise<SummarizedSyncStatus> { | ||||
|         const objects = await listObjects(); | ||||
|         const metrics: { [key: string]: number } = { | ||||
|             healthy: 0, | ||||
|             failed: 0, | ||||
|             unsynced: 0, | ||||
|         }; | ||||
|         await Promise.all( | ||||
|             sources.results.map(async (element) => { | ||||
|             objects.results.map(async (element) => { | ||||
|                 // Each source should have 3 successful tasks, so the worst task overwrites | ||||
|                 let objectKey = "healthy"; | ||||
|                 try { | ||||
|                     const health = await api.sourcesLdapSyncStatusRetrieve({ | ||||
|                         slug: element.slug, | ||||
|                     }); | ||||
|  | ||||
|                     health.tasks.forEach((task) => { | ||||
|                     const status = await fetchSyncStatus(element); | ||||
|                     status.tasks.forEach((task) => { | ||||
|                         if (task.status !== SystemTaskStatusEnum.Successful) { | ||||
|                             metrics.failed += 1; | ||||
|                             objectKey = "failed"; | ||||
|                         } | ||||
|                         const now = new Date().getTime(); | ||||
|                         const maxDelta = 3600000; // 1 hour | ||||
|                         if (!health || now - task.finishTimestamp.getTime() > maxDelta) { | ||||
|                             metrics.unsynced += 1; | ||||
|                         } else { | ||||
|                             metrics.healthy += 1; | ||||
|                         if (!status || now - task.finishTimestamp.getTime() > maxDelta) { | ||||
|                             objectKey = "unsynced"; | ||||
|                         } | ||||
|                     }); | ||||
|                     if (health.tasks.length < 1) { | ||||
|                         metrics.unsynced += 1; | ||||
|                     } | ||||
|                 } catch { | ||||
|                     metrics.unsynced += 1; | ||||
|                     objectKey = "unsynced"; | ||||
|                 } | ||||
|                 metrics[objectKey] += 1; | ||||
|             }), | ||||
|         ); | ||||
|         return { | ||||
|             healthy: metrics.healthy, | ||||
|             failed: metrics.failed, | ||||
|             unsynced: sources.pagination.count === 0 ? 1 : metrics.unsynced, | ||||
|             total: sources.pagination.count, | ||||
|             label: msg("LDAP Source"), | ||||
|             unsynced: objects.pagination.count === 0 ? 1 : metrics.unsynced, | ||||
|             total: objects.pagination.count, | ||||
|             label: label, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     async scimStatus(): Promise<SyncStatus> { | ||||
|         const api = new ProvidersApi(DEFAULT_CONFIG); | ||||
|         const providers = await api.providersScimList({}); | ||||
|         const metrics: { [key: string]: number } = { | ||||
|             healthy: 0, | ||||
|             failed: 0, | ||||
|             unsynced: 0, | ||||
|         }; | ||||
|         await Promise.all( | ||||
|             providers.results.map(async (element) => { | ||||
|                 // Each source should have 3 successful tasks, so the worst task overwrites | ||||
|                 let sourceKey = "healthy"; | ||||
|                 try { | ||||
|                     const health = await api.providersScimSyncStatusRetrieve({ | ||||
|     async apiRequest(): Promise<SummarizedSyncStatus[]> { | ||||
|         const statuses = [ | ||||
|             await this.fetchStatus( | ||||
|                 () => { | ||||
|                     return new ProvidersApi(DEFAULT_CONFIG).providersScimList(); | ||||
|                 }, | ||||
|                 (element) => { | ||||
|                     return new ProvidersApi(DEFAULT_CONFIG).providersScimSyncStatusRetrieve({ | ||||
|                         id: element.pk, | ||||
|                     }); | ||||
|                     health.tasks.forEach((task) => { | ||||
|                         if (task.status !== SystemTaskStatusEnum.Successful) { | ||||
|                             sourceKey = "failed"; | ||||
|                         } | ||||
|                         const now = new Date().getTime(); | ||||
|                         const maxDelta = 3600000; // 1 hour | ||||
|                         if (!health || now - task.finishTimestamp.getTime() > maxDelta) { | ||||
|                             sourceKey = "unsynced"; | ||||
|                         } | ||||
|                 }, | ||||
|                 msg("SCIM Provider"), | ||||
|             ), | ||||
|             await this.fetchStatus( | ||||
|                 () => { | ||||
|                     return new ProvidersApi(DEFAULT_CONFIG).providersGoogleWorkspaceList(); | ||||
|                 }, | ||||
|                 (element) => { | ||||
|                     return new ProvidersApi( | ||||
|                         DEFAULT_CONFIG, | ||||
|                     ).providersGoogleWorkspaceSyncStatusRetrieve({ | ||||
|                         id: element.pk, | ||||
|                     }); | ||||
|                 } catch { | ||||
|                     sourceKey = "unsynced"; | ||||
|                 } | ||||
|                 metrics[sourceKey] += 1; | ||||
|             }), | ||||
|         ); | ||||
|         return { | ||||
|             healthy: metrics.healthy, | ||||
|             failed: metrics.failed, | ||||
|             unsynced: providers.pagination.count === 0 ? 1 : metrics.unsynced, | ||||
|             total: providers.pagination.count, | ||||
|             label: msg("SCIM Provider"), | ||||
|         }; | ||||
|                 }, | ||||
|                 msg("Google Workspace Provider"), | ||||
|             ), | ||||
|             await this.fetchStatus( | ||||
|                 () => { | ||||
|                     return new SourcesApi(DEFAULT_CONFIG).sourcesLdapList(); | ||||
|                 }, | ||||
|                 (element) => { | ||||
|                     return new SourcesApi(DEFAULT_CONFIG).sourcesLdapSyncStatusRetrieve({ | ||||
|                         slug: element.slug, | ||||
|                     }); | ||||
|                 }, | ||||
|                 msg("LDAP Source"), | ||||
|             ), | ||||
|         ]; | ||||
|         this.centerText = statuses.reduce((total, el) => (total += el.total), 0).toString(); | ||||
|         return statuses; | ||||
|     } | ||||
|  | ||||
|     async apiRequest(): Promise<SyncStatus[]> { | ||||
|         const ldapStatus = await this.ldapStatus(); | ||||
|         const scimStatus = await this.scimStatus(); | ||||
|         this.centerText = (ldapStatus.total + scimStatus.total).toString(); | ||||
|         return [ldapStatus, scimStatus]; | ||||
|     } | ||||
|  | ||||
|     getChartData(data: SyncStatus[]): ChartData { | ||||
|     getChartData(data: SummarizedSyncStatus[]): ChartData { | ||||
|         return { | ||||
|             labels: [msg("Healthy"), msg("Failed"), msg("Unsynced / N/A")], | ||||
|             datasets: data.map((d) => { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens L
					Jens L