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:
Jens L
2024-05-07 19:52:20 +02:00
committed by GitHub
parent 18b4b2d7b2
commit aeb1b450eb
84 changed files with 4307 additions and 619 deletions

View File

@ -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) => {

View File

@ -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) => {