@k2b/cloud 0.44.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -2
- package/src/_internal/app-queries.ts +73 -0
- package/src/_internal/capabilities.ts +137 -9
- package/src/_internal/define-app.ts +2 -0
- package/src/_internal/registry-validation.ts +17 -4
- package/src/_internal/registry.ts +32 -4
- package/src/_internal/resource-principals.ts +36 -0
- package/src/access/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +11 -2
- package/src/access/PrincipalAvatar.tsx +30 -21
- package/src/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -0
- package/src/ai/capabilities.ts +4 -2
- package/src/ai/chat/map-messages.ts +270 -0
- package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
- package/src/ai/chat/map-notebook.tsx +301 -0
- package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
- package/src/ai/chat/map-widget.css +107 -0
- package/src/ai/chat/map-widget.tsx +1279 -0
- package/src/ai/chat/message-utils.ts +1 -0
- package/src/ai/chat/tool-names.ts +2 -0
- package/src/ai/chat/turn-layout.ts +2 -1
- package/src/ai/chat/turn-view.tsx +17 -1
- package/src/ai/default-tools.ts +3 -0
- package/src/ai/dictation-runtime.ts +6 -2
- package/src/ai/executor.ts +22 -4
- package/src/ai/index.ts +1 -0
- package/src/ai/map-contracts.ts +209 -0
- package/src/ai/map-execution.ts +14 -0
- package/src/ai/map-notebook-save.ts +276 -0
- package/src/ai/map-notebook.ts +32 -0
- package/src/ai/map-opening-hours.ts +40 -0
- package/src/ai/map-routes.ts +84 -0
- package/src/ai/map-service.ts +288 -0
- package/src/ai/map-store.ts +137 -0
- package/src/ai/map-tool.ts +73 -0
- package/src/ai/migrate.ts +3 -0
- package/src/ai/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +56 -46
- package/src/ai/projects.ts +100 -79
- package/src/ai/protocol.ts +3 -1
- package/src/ai/recent-resources.ts +3 -3
- package/src/ai/routes.ts +32 -30
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- package/src/ai/store.ts +19 -6
- package/src/ai/tools.ts +14 -0
- package/src/ai/types.ts +3 -0
- package/src/api/accounts-entities.ts +6 -3
- package/src/api/admin-ai-skills.ts +4 -1
- package/src/api/auth.ts +9 -2
- package/src/api/capabilities.ts +90 -12
- package/src/api/mcp.ts +4 -2
- package/src/browser/notifications.ts +25 -8
- package/src/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -0
- package/src/capabilities/discussion-providers.ts +99 -0
- package/src/capabilities/executions.ts +2 -2
- package/src/capabilities/migrate.ts +10 -1
- package/src/capabilities/resource-readers.ts +58 -0
- package/src/capabilities/server-invoke.ts +23 -13
- package/src/capabilities/server.ts +17 -6
- package/src/capabilities/testing.ts +13 -0
- package/src/contracts/capabilities.ts +137 -4
- package/src/contracts/discussions.ts +91 -0
- package/src/contracts/geo-document.ts +60 -0
- package/src/contracts/geo-snapshot.ts +86 -0
- package/src/contracts/geo.ts +14 -0
- package/src/contracts/index.ts +2 -0
- package/src/contracts/outgoing-mail.ts +1 -1
- package/src/contracts/resource-types.ts +21 -0
- package/src/contracts/shared.ts +2 -1
- package/src/geo/AddressAutocomplete.tsx +213 -0
- package/src/geo/messages.ts +29 -0
- package/src/geo/navigation.ts +25 -0
- package/src/geo/ui.ts +2 -0
- package/src/server/api/respond.ts +10 -2
- package/src/server/index.ts +1 -0
- package/src/server/services/access.ts +23 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +56 -17
- package/src/services/accounts/entities.ts +36 -16
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +26 -17
- package/src/services/accounts/identities.ts +8 -1
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +141 -27
- package/src/services/accounts/machine-group-management.ts +94 -0
- package/src/services/accounts/users.ts +38 -8
- package/src/services/geo/cache.ts +2 -1
- package/src/services/geo/geoapify.ts +3 -0
- package/src/services/identity/invocation-token.ts +110 -0
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +1 -0
- package/src/services/pdf/geo-snapshot.ts +107 -0
- package/src/services/pdf/index.ts +2 -0
- package/src/services/session/index.ts +12 -3
- package/src/shared/email-html.ts +1 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +428 -77
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/types.ts +24 -5
- package/src/ssr/AdminArea.tsx +2 -2
- package/src/ssr/AdminLayout.tsx +1 -1
- package/src/ssr/AdminSidebar.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/CloudShell.island.tsx +19 -3
- package/src/ssr/Layout.tsx +1 -1
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-navigation.ts +1 -11
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/platform-messages.ts +2 -0
- package/src/ssr/profile-actions.ts +3 -6
- package/src/ssr/shell-data.tsx +1 -1
- package/src/styles/global.css +2 -0
- package/src/workflows/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +2 -1
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
1
|
+
import { type SQLQuery, sql } from "bun";
|
|
2
2
|
import type { EntityKind, EntityListItem, UserProfile, UserProvider } from "../../contracts/shared";
|
|
3
3
|
import { getFreeIpaConfig } from "../freeipa-config";
|
|
4
4
|
import { escapeLikePattern, toPgTextArray, toPgUuidArray } from "../postgres";
|
|
5
5
|
import type { ServiceAccountKind, ServiceAccountStatus } from "../service-accounts";
|
|
6
6
|
import { buildBaseGroup, personalOwnerJoin } from "./base-group";
|
|
7
7
|
import { buildBaseUser } from "./base-user";
|
|
8
|
-
import { buildManagedGroupScopeCondition, recursiveGroupIdsSubquery } from "./group-sql";
|
|
8
|
+
import { buildManagedGroupScopeCondition, recursiveGroupIdsSubquery, recursiveServiceAccountGroupIdsSubquery } from "./group-sql";
|
|
9
9
|
|
|
10
10
|
type DbRow = Record<string, unknown>;
|
|
11
|
-
type SqlFragment =
|
|
11
|
+
type SqlFragment = SQLQuery;
|
|
12
12
|
|
|
13
13
|
export type EntityListParams = {
|
|
14
14
|
visibility:
|
|
@@ -21,6 +21,7 @@ export type EntityListParams = {
|
|
|
21
21
|
profile?: UserProfile;
|
|
22
22
|
userIds?: string[];
|
|
23
23
|
groupIds?: string[];
|
|
24
|
+
serviceAccountIds?: string[];
|
|
24
25
|
serviceAccountKinds?: ServiceAccountKind[];
|
|
25
26
|
serviceAccountStatus?: ServiceAccountStatus;
|
|
26
27
|
excludeUserIds?: string[];
|
|
@@ -32,11 +33,6 @@ export type EntityListParams = {
|
|
|
32
33
|
parentGroupId?: string;
|
|
33
34
|
managedByUserId?: string;
|
|
34
35
|
recursive?: boolean;
|
|
35
|
-
/**
|
|
36
|
-
* Personal Linux groups are left out of directory browsing and search by
|
|
37
|
-
* default. Exact `groupIds` lookups and relation filters always return them.
|
|
38
|
-
*/
|
|
39
|
-
includePersonal?: boolean;
|
|
40
36
|
page?: number;
|
|
41
37
|
perPage?: number;
|
|
42
38
|
};
|
|
@@ -429,11 +425,7 @@ export const list = async (
|
|
|
429
425
|
const groupsAdmin = (await getFreeIpaConfig()).groupsAdmin;
|
|
430
426
|
const groupsAdminLiteral = toPgTextArray(groupsAdmin);
|
|
431
427
|
const spec = buildQuerySpec(params);
|
|
432
|
-
const
|
|
433
|
-
params.includePersonal === true ||
|
|
434
|
-
params.groupIds !== undefined ||
|
|
435
|
-
Boolean(params.memberOfGroupId || params.managerOfGroupId || params.parentGroupId || params.managedByUserId);
|
|
436
|
-
const personalCondition = includePersonal ? sql`TRUE` : sql`(kind <> 'group' OR personal_owner_id IS NULL)`;
|
|
428
|
+
const personalCondition = sql`(kind <> 'group' OR NOT EXISTS (SELECT 1 FROM auth.personal_groups p WHERE p.group_id = entity_rows.id))`;
|
|
437
429
|
const visibilityCondition =
|
|
438
430
|
params.visibility.type === "directory"
|
|
439
431
|
? sql`TRUE`
|
|
@@ -455,12 +447,13 @@ export const list = async (
|
|
|
455
447
|
)
|
|
456
448
|
)`;
|
|
457
449
|
const kindsCondition = (params.kinds?.length ?? 0) === 0 ? sql`TRUE` : sql`kind = ANY(${toPgTextArray(params.kinds ?? [])}::text[])`;
|
|
458
|
-
const hasIdFilter = params.userIds !== undefined || params.groupIds !== undefined;
|
|
450
|
+
const hasIdFilter = params.userIds !== undefined || params.groupIds !== undefined || params.serviceAccountIds !== undefined;
|
|
459
451
|
const includeIdCondition = !hasIdFilter
|
|
460
452
|
? sql`TRUE`
|
|
461
453
|
: sql`(
|
|
462
454
|
(kind = 'user' AND id = ANY(${toPgUuidArray(params.userIds ?? [])}::uuid[]))
|
|
463
455
|
OR (kind = 'group' AND id = ANY(${toPgUuidArray(params.groupIds ?? [])}::uuid[]))
|
|
456
|
+
OR (kind = 'service_account' AND id = ANY(${toPgUuidArray(params.serviceAccountIds ?? [])}::uuid[]))
|
|
464
457
|
)`;
|
|
465
458
|
const excludeUserCondition =
|
|
466
459
|
(params.excludeUserIds?.length ?? 0) === 0
|
|
@@ -491,11 +484,37 @@ export const list = async (
|
|
|
491
484
|
AND ug.group_id = ANY(${toPgUuidArray(params.userMemberOfGroupIds ?? [])}::uuid[])
|
|
492
485
|
))`;
|
|
493
486
|
|
|
487
|
+
// Machine management is direct only; membership never implies management.
|
|
488
|
+
const otherRelation = Boolean(params.parentGroupId || params.managedByUserId);
|
|
489
|
+
const machineRelation = params.memberOfGroupId
|
|
490
|
+
? sql`sa.kind IN ('standalone', 'agent') AND EXISTS (
|
|
491
|
+
SELECT 1 FROM auth.groups target
|
|
492
|
+
WHERE target.id = ${params.memberOfGroupId}::uuid AND target.provider = 'local'
|
|
493
|
+
AND ${
|
|
494
|
+
params.recursive === true
|
|
495
|
+
? sql`target.id IN (${recursiveServiceAccountGroupIdsSubquery(sql`sa.id`)})`
|
|
496
|
+
: sql`EXISTS (SELECT 1 FROM auth.service_account_groups sg
|
|
497
|
+
WHERE sg.service_account_id = sa.id AND sg.group_id = target.id)`
|
|
498
|
+
}
|
|
499
|
+
)`
|
|
500
|
+
: params.managerOfGroupId
|
|
501
|
+
? sql`sa.kind IN ('standalone', 'agent') AND EXISTS (
|
|
502
|
+
SELECT 1 FROM auth.group_manager_service_accounts gm JOIN auth.groups g ON g.id = gm.group_id
|
|
503
|
+
WHERE gm.service_account_id = sa.id AND gm.group_id = ${params.managerOfGroupId}::uuid AND g.provider = 'local'
|
|
504
|
+
)`
|
|
505
|
+
: sql`TRUE`;
|
|
506
|
+
const machineDirect = params.memberOfGroupId
|
|
507
|
+
? sql`EXISTS (SELECT 1 FROM auth.service_account_groups sg
|
|
508
|
+
WHERE sg.service_account_id = sa.id AND sg.group_id = ${params.memberOfGroupId}::uuid)`
|
|
509
|
+
: params.managerOfGroupId
|
|
510
|
+
? sql`TRUE`
|
|
511
|
+
: sql`NULL::boolean`;
|
|
494
512
|
const where = sql`
|
|
495
513
|
${visibilityCondition}
|
|
496
514
|
AND ${kindsCondition}
|
|
497
515
|
AND ${includeIdCondition}
|
|
498
|
-
AND (${params.provider ?? null}::text IS NULL OR provider = ${params.provider ?? null}
|
|
516
|
+
AND (${params.provider ?? null}::text IS NULL OR provider = ${params.provider ?? null}
|
|
517
|
+
OR (kind = 'service_account' AND ${params.provider ?? null}::text = 'local'))
|
|
499
518
|
AND (${params.profile ?? null}::text IS NULL OR kind = 'group' OR profile = ${params.profile ?? null})
|
|
500
519
|
AND ${excludeUserCondition}
|
|
501
520
|
AND ${excludeGroupCondition}
|
|
@@ -612,7 +631,7 @@ export const list = async (
|
|
|
612
631
|
service_account_rows AS (
|
|
613
632
|
SELECT
|
|
614
633
|
'service_account'::text AS kind,
|
|
615
|
-
|
|
634
|
+
${machineDirect} AS direct,
|
|
616
635
|
sa.id,
|
|
617
636
|
NULL::text AS provider,
|
|
618
637
|
NULL::text AS profile,
|
|
@@ -644,6 +663,7 @@ export const list = async (
|
|
|
644
663
|
NULL::text AS personal_owner_display_name,
|
|
645
664
|
LOWER(sa.name) AS sort_label
|
|
646
665
|
FROM auth.service_accounts sa
|
|
666
|
+
WHERE NOT ${otherRelation} AND ${machineRelation}
|
|
647
667
|
),
|
|
648
668
|
entity_rows AS (
|
|
649
669
|
SELECT * FROM user_rows
|
|
@@ -104,3 +104,26 @@ export const buildManagedGroupScopeCondition = (params: { userId: SqlValue; grou
|
|
|
104
104
|
})}
|
|
105
105
|
)
|
|
106
106
|
`;
|
|
107
|
+
|
|
108
|
+
/** Local recipient membership only; never supplies user roles or directory identity. */
|
|
109
|
+
export const recursiveServiceAccountGroupIdsSubquery = (serviceAccountId: SqlValue) => sql`
|
|
110
|
+
WITH RECURSIVE account_subject AS (
|
|
111
|
+
-- Resolve a correlated column before introducing aliases used by directory callers (for example sa.id).
|
|
112
|
+
SELECT ${serviceAccountId}::uuid AS id
|
|
113
|
+
), account_groups AS (
|
|
114
|
+
SELECT sg.group_id
|
|
115
|
+
FROM auth.service_account_groups sg
|
|
116
|
+
JOIN auth.service_accounts sa ON sa.id = sg.service_account_id
|
|
117
|
+
JOIN account_subject subject ON subject.id = sa.id
|
|
118
|
+
JOIN auth.groups g ON g.id = sg.group_id
|
|
119
|
+
WHERE sa.status = 'active' AND sa.kind IN ('standalone', 'agent')
|
|
120
|
+
AND g.provider = 'local'
|
|
121
|
+
UNION
|
|
122
|
+
SELECT gg.parent_group_id
|
|
123
|
+
FROM auth.group_groups_v2 gg
|
|
124
|
+
JOIN auth.groups parent ON parent.id = gg.parent_group_id
|
|
125
|
+
JOIN account_groups ag ON ag.group_id = gg.child_group_id
|
|
126
|
+
WHERE parent.provider = 'local'
|
|
127
|
+
)
|
|
128
|
+
SELECT group_id FROM account_groups
|
|
129
|
+
`;
|
|
@@ -31,8 +31,6 @@ const listCanonical = async (params: {
|
|
|
31
31
|
scope?: GroupListScope;
|
|
32
32
|
search?: string;
|
|
33
33
|
provider?: UserProvider;
|
|
34
|
-
/** Personal Linux groups are left out of browsing and search unless requested; an explicit `ids` lookup always returns them. */
|
|
35
|
-
includePersonal?: boolean;
|
|
36
34
|
page?: number;
|
|
37
35
|
perPage?: number;
|
|
38
36
|
}): Promise<{
|
|
@@ -48,7 +46,6 @@ const listCanonical = async (params: {
|
|
|
48
46
|
const scope = params.scope ?? (params.userId ? "member" : "all");
|
|
49
47
|
const scopeUserId = params.userId ?? "00000000-0000-0000-0000-000000000000";
|
|
50
48
|
const idsCondition = ids.length === 0 ? sql`TRUE` : sql`g.id = ANY(${toPgUuidArray(ids)}::uuid[])`;
|
|
51
|
-
const includePersonal = params.includePersonal === true || params.ids !== undefined;
|
|
52
49
|
|
|
53
50
|
if (params.ids && params.ids.length === 0) {
|
|
54
51
|
return {
|
|
@@ -69,7 +66,7 @@ const listCanonical = async (params: {
|
|
|
69
66
|
${personalOwnerJoin()}
|
|
70
67
|
WHERE (${params.provider ?? null}::text IS NULL OR g.provider = ${params.provider ?? null})
|
|
71
68
|
AND ${idsCondition}
|
|
72
|
-
AND (
|
|
69
|
+
AND NOT EXISTS (SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id)
|
|
73
70
|
AND (
|
|
74
71
|
${scope === "all"} = true
|
|
75
72
|
OR ${params.userId ?? null}::uuid IS NULL
|
|
@@ -107,8 +104,6 @@ export const list = async (params: {
|
|
|
107
104
|
scope?: GroupListScope;
|
|
108
105
|
search?: string;
|
|
109
106
|
provider?: UserProvider;
|
|
110
|
-
/** Personal Linux groups are left out of browsing and search unless requested; an explicit `ids` lookup always returns them. */
|
|
111
|
-
includePersonal?: boolean;
|
|
112
107
|
page?: number;
|
|
113
108
|
perPage?: number;
|
|
114
109
|
}) => {
|
|
@@ -122,13 +117,13 @@ export const get = async (params: { id: string }): Promise<BaseGroup | null> =>
|
|
|
122
117
|
export const getMembers = async (params: {
|
|
123
118
|
id: string;
|
|
124
119
|
provider?: UserProvider;
|
|
125
|
-
type?: "
|
|
120
|
+
type?: GroupMember["type"];
|
|
126
121
|
recursive?: boolean;
|
|
127
122
|
}): Promise<GroupMember[]> => {
|
|
128
123
|
const provider = params.provider ?? (await getGroup(params.id))?.provider;
|
|
129
124
|
if (provider === "local") return localGroups.getMembers(params);
|
|
130
|
-
if (!provider) return [];
|
|
131
|
-
return providers.ipa.groups.getMembers(params);
|
|
125
|
+
if (!provider || params.type === "service_account") return [];
|
|
126
|
+
return providers.ipa.groups.getMembers({ ...params, type: params.type });
|
|
132
127
|
};
|
|
133
128
|
|
|
134
129
|
export const getManagers = async (params: {
|
|
@@ -225,9 +220,13 @@ export const addMember = async (params: {
|
|
|
225
220
|
provider?: UserProvider;
|
|
226
221
|
user?: string;
|
|
227
222
|
group?: string;
|
|
223
|
+
serviceAccount?: string;
|
|
228
224
|
}): Promise<MutationResult<void>> => {
|
|
229
|
-
const provider =
|
|
230
|
-
if (provider === "local")
|
|
225
|
+
const provider = (await getGroup(params.id))?.provider;
|
|
226
|
+
if (provider === "local")
|
|
227
|
+
return localGroups.addMember({ id: params.id, user: params.user, group: params.group, serviceAccount: params.serviceAccount });
|
|
228
|
+
if (params.serviceAccount)
|
|
229
|
+
return { ok: false, error: "Service accounts can only join local Cloud groups, not directory groups", status: 400 };
|
|
231
230
|
const serviceSession = await getServiceIpaSession();
|
|
232
231
|
if (!serviceSession.ok) return serviceSession;
|
|
233
232
|
return providers.ipa.groups.addMember({
|
|
@@ -243,9 +242,13 @@ export const removeMember = async (params: {
|
|
|
243
242
|
provider?: UserProvider;
|
|
244
243
|
user?: string;
|
|
245
244
|
group?: string;
|
|
245
|
+
serviceAccount?: string;
|
|
246
246
|
}): Promise<MutationResult<void>> => {
|
|
247
|
-
const provider =
|
|
248
|
-
if (provider === "local")
|
|
247
|
+
const provider = (await getGroup(params.id))?.provider;
|
|
248
|
+
if (provider === "local")
|
|
249
|
+
return localGroups.removeMember({ id: params.id, user: params.user, group: params.group, serviceAccount: params.serviceAccount });
|
|
250
|
+
if (params.serviceAccount)
|
|
251
|
+
return { ok: false, error: "Service accounts can only join local Cloud groups, not directory groups", status: 400 };
|
|
249
252
|
const serviceSession = await getServiceIpaSession();
|
|
250
253
|
if (!serviceSession.ok) return serviceSession;
|
|
251
254
|
return providers.ipa.groups.removeMember({
|
|
@@ -261,9 +264,12 @@ export const addManager = async (params: {
|
|
|
261
264
|
provider?: UserProvider;
|
|
262
265
|
user?: string;
|
|
263
266
|
group?: string;
|
|
267
|
+
serviceAccount?: string;
|
|
264
268
|
}): Promise<MutationResult<void>> => {
|
|
265
|
-
const provider =
|
|
266
|
-
if (provider === "local")
|
|
269
|
+
const provider = (await getGroup(params.id))?.provider;
|
|
270
|
+
if (provider === "local")
|
|
271
|
+
return localGroups.addManager({ id: params.id, user: params.user, group: params.group, serviceAccount: params.serviceAccount });
|
|
272
|
+
if (params.serviceAccount) return { ok: false, error: "Machine management requires a local group", status: 400 };
|
|
267
273
|
const serviceSession = await getServiceIpaSession();
|
|
268
274
|
if (!serviceSession.ok) return serviceSession;
|
|
269
275
|
return providers.ipa.groups.addManager({
|
|
@@ -279,9 +285,12 @@ export const removeManager = async (params: {
|
|
|
279
285
|
provider?: UserProvider;
|
|
280
286
|
user?: string;
|
|
281
287
|
group?: string;
|
|
288
|
+
serviceAccount?: string;
|
|
282
289
|
}): Promise<MutationResult<void>> => {
|
|
283
|
-
const provider =
|
|
284
|
-
if (provider === "local")
|
|
290
|
+
const provider = (await getGroup(params.id))?.provider;
|
|
291
|
+
if (provider === "local")
|
|
292
|
+
return localGroups.removeManager({ id: params.id, user: params.user, group: params.group, serviceAccount: params.serviceAccount });
|
|
293
|
+
if (params.serviceAccount) return { ok: false, error: "Machine management requires a local group", status: 400 };
|
|
285
294
|
const serviceSession = await getServiceIpaSession();
|
|
286
295
|
if (!serviceSession.ok) return serviceSession;
|
|
287
296
|
return providers.ipa.groups.removeManager({
|
|
@@ -189,18 +189,25 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
|
|
|
189
189
|
eligible: current.profile === "user",
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
|
+
const [ownership] = await db<{ personal: boolean }[]>`SELECT EXISTS (
|
|
193
|
+
SELECT 1 FROM auth.personal_groups p
|
|
194
|
+
WHERE (${input.identityId ?? null}::uuid IS NOT NULL AND p.group_id = ${input.identityId ?? null}::uuid)
|
|
195
|
+
OR (${input.identityId ?? null}::uuid IS NULL AND p.group_name = ${input.name})
|
|
196
|
+
) AS personal`;
|
|
197
|
+
const personal = ownership?.personal === true;
|
|
192
198
|
const rows = await groupRows(sql`(g.provider = 'local' AND g.name = ${input.name}) OR g.id = ${input.identityId ?? null}::uuid`);
|
|
193
199
|
const expected = rows.find((row) => row.id === input.identityId);
|
|
194
200
|
if (expected && (expected.provider !== "local" || expected.name !== input.name))
|
|
195
201
|
return { state: "unknown", reason: "identity_conflict" };
|
|
196
202
|
const matches = rows.filter((row) => row.provider === "local" && row.name === input.name);
|
|
197
|
-
if (!matches.length) return { state: "absent" };
|
|
203
|
+
if (!matches.length) return personal ? { state: "absent", personal: true } : { state: "absent" };
|
|
198
204
|
if (matches.length !== 1) return { state: "unknown", reason: "invalid_response" };
|
|
199
205
|
const current = matches[0]!;
|
|
200
206
|
return {
|
|
201
207
|
state: "present",
|
|
202
208
|
identity: { id: current.id, name: current.name, uidNumber: null, gidNumber: current.gidNumber },
|
|
203
209
|
eligible: current.gidNumber !== null,
|
|
210
|
+
...(personal ? { personal: true } : {}),
|
|
204
211
|
};
|
|
205
212
|
};
|
|
206
213
|
const validateLookup = (input: { kind: "users" | "groups"; name: string }) => {
|
|
@@ -9,8 +9,10 @@ export type AccountIdentityReconciliation =
|
|
|
9
9
|
state: "present";
|
|
10
10
|
identity: { id: string | null; name: string; uidNumber: number | null; gidNumber: number | null };
|
|
11
11
|
eligible: boolean;
|
|
12
|
+
/** Proven Cloud personal-group ownership, including retained directory bindings. */
|
|
13
|
+
personal?: boolean;
|
|
12
14
|
}
|
|
13
|
-
| { state: "absent" }
|
|
15
|
+
| { state: "absent"; personal?: boolean }
|
|
14
16
|
| { state: "unknown"; reason: "provider_disabled" | "provider_unavailable" | "invalid_response" | "identity_conflict" };
|
|
15
17
|
export type IdentityLookup = { kind: "users" | "groups"; name: string; signal?: AbortSignal };
|
|
16
18
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
1
|
+
import { sql, type TransactionSQL } from "bun";
|
|
2
2
|
import type { BaseGroup, GroupMember, MutationResult, UserProvider } from "../../contracts/shared";
|
|
3
3
|
import { escapeLikePattern, isUniqueViolation } from "../postgres";
|
|
4
4
|
import { buildBaseGroup, personalOwnerJoin } from "./base-group";
|
|
5
|
+
import { recursiveServiceAccountGroupIdsSubquery } from "./group-sql";
|
|
5
6
|
|
|
6
7
|
type DbRow = Record<string, unknown>;
|
|
7
8
|
|
|
@@ -22,8 +23,8 @@ const toBaseGroup = (row: LocalGroupRow): BaseGroup => ({
|
|
|
22
23
|
personalOwner: null,
|
|
23
24
|
});
|
|
24
25
|
|
|
25
|
-
const getLocalGroupById = async (id: string): Promise<LocalGroupRow | null> => {
|
|
26
|
-
const [row] = await
|
|
26
|
+
const getLocalGroupById = async (id: string, db: typeof sql | TransactionSQL = sql): Promise<LocalGroupRow | null> => {
|
|
27
|
+
const [row] = await db<DbRow[]>`
|
|
27
28
|
SELECT id, provider, name, description, gid_number
|
|
28
29
|
FROM auth.groups
|
|
29
30
|
WHERE id = ${id}::uuid AND provider = 'local'
|
|
@@ -38,20 +39,23 @@ const getLocalGroupById = async (id: string): Promise<LocalGroupRow | null> => {
|
|
|
38
39
|
};
|
|
39
40
|
};
|
|
40
41
|
|
|
41
|
-
const getUserProvider = async (userId: string): Promise<UserProvider | null> => {
|
|
42
|
-
const [row] = await
|
|
42
|
+
const getUserProvider = async (userId: string, db: typeof sql | TransactionSQL = sql): Promise<UserProvider | null> => {
|
|
43
|
+
const [row] = await db<DbRow[]>`SELECT provider FROM auth.users WHERE id = ${userId}`;
|
|
43
44
|
return (row?.provider as UserProvider | undefined) ?? null;
|
|
44
45
|
};
|
|
45
46
|
|
|
46
|
-
const ensureLocalGroupTreeMember = async (groupId: string): Promise<boolean> => {
|
|
47
|
-
const [row] = await
|
|
47
|
+
const ensureLocalGroupTreeMember = async (groupId: string, db: typeof sql | TransactionSQL = sql): Promise<boolean> => {
|
|
48
|
+
const [row] = await db<DbRow[]>`SELECT 1 FROM auth.groups WHERE id = ${groupId} AND provider = 'local'`;
|
|
48
49
|
return Boolean(row);
|
|
49
50
|
};
|
|
50
51
|
|
|
51
|
-
const wouldCreateLocalGroupCycle = async (
|
|
52
|
+
const wouldCreateLocalGroupCycle = async (
|
|
53
|
+
params: { parentGroupId: string; childGroupId: string },
|
|
54
|
+
db: typeof sql | TransactionSQL = sql,
|
|
55
|
+
): Promise<boolean> => {
|
|
52
56
|
if (params.parentGroupId === params.childGroupId) return true;
|
|
53
57
|
|
|
54
|
-
const [row] = await
|
|
58
|
+
const [row] = await db<DbRow[]>`
|
|
55
59
|
WITH RECURSIVE descendants AS (
|
|
56
60
|
SELECT gg.child_group_id
|
|
57
61
|
FROM auth.group_groups_v2 gg
|
|
@@ -122,6 +126,7 @@ export const list = async (params: { page?: number; perPage?: number; search?: s
|
|
|
122
126
|
SELECT COUNT(*)::int AS count
|
|
123
127
|
FROM auth.groups g
|
|
124
128
|
WHERE g.provider = 'local'
|
|
129
|
+
AND NOT EXISTS (SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id)
|
|
125
130
|
AND (${pattern}::text IS NULL OR LOWER(g.name) LIKE ${pattern} ESCAPE '\\' OR LOWER(g.description) LIKE ${pattern} ESCAPE '\\')
|
|
126
131
|
`;
|
|
127
132
|
const total = Number(countRow?.count ?? 0);
|
|
@@ -130,6 +135,7 @@ export const list = async (params: { page?: number; perPage?: number; search?: s
|
|
|
130
135
|
FROM auth.groups g
|
|
131
136
|
${personalOwnerJoin()}
|
|
132
137
|
WHERE g.provider = 'local'
|
|
138
|
+
AND NOT EXISTS (SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id)
|
|
133
139
|
AND (${pattern}::text IS NULL OR LOWER(g.name) LIKE ${pattern} ESCAPE '\\' OR LOWER(g.description) LIKE ${pattern} ESCAPE '\\')
|
|
134
140
|
ORDER BY g.name
|
|
135
141
|
LIMIT ${perPage} OFFSET ${offset}
|
|
@@ -177,7 +183,7 @@ export const remove = async (params: { id: string }): Promise<MutationResult<voi
|
|
|
177
183
|
return { ok: true, data: undefined };
|
|
178
184
|
};
|
|
179
185
|
|
|
180
|
-
export const getMembers = async (params: { id: string; type?: "
|
|
186
|
+
export const getMembers = async (params: { id: string; type?: GroupMember["type"]; recursive?: boolean }): Promise<GroupMember[]> => {
|
|
181
187
|
const group = await getLocalGroupById(params.id);
|
|
182
188
|
if (!group) return [];
|
|
183
189
|
const members: GroupMember[] = [];
|
|
@@ -231,6 +237,7 @@ export const getMembers = async (params: { id: string; type?: "user" | "group";
|
|
|
231
237
|
SELECT DISTINCT g.id, g.name
|
|
232
238
|
FROM local_group_tree tree
|
|
233
239
|
JOIN auth.groups g ON g.id = tree.child_group_id
|
|
240
|
+
WHERE NOT EXISTS (SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id)
|
|
234
241
|
ORDER BY g.name
|
|
235
242
|
`
|
|
236
243
|
: await sql<DbRow[]>`
|
|
@@ -238,6 +245,7 @@ export const getMembers = async (params: { id: string; type?: "user" | "group";
|
|
|
238
245
|
FROM auth.group_groups_v2 gg
|
|
239
246
|
JOIN auth.groups g ON g.id = gg.child_group_id
|
|
240
247
|
WHERE gg.parent_group_id = ${group.id} AND g.provider = 'local'
|
|
248
|
+
AND NOT EXISTS (SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id)
|
|
241
249
|
ORDER BY g.name
|
|
242
250
|
`;
|
|
243
251
|
for (const row of groupRows) {
|
|
@@ -245,10 +253,33 @@ export const getMembers = async (params: { id: string; type?: "user" | "group";
|
|
|
245
253
|
}
|
|
246
254
|
}
|
|
247
255
|
|
|
256
|
+
if (!params.type || params.type === "service_account") {
|
|
257
|
+
const rows = await sql<{ id: string; name: string; kind: "standalone" | "agent" }[]>`
|
|
258
|
+
SELECT sa.id, sa.name, sa.kind
|
|
259
|
+
FROM auth.service_accounts sa
|
|
260
|
+
WHERE sa.kind IN ('standalone', 'agent') AND ${
|
|
261
|
+
params.recursive === true
|
|
262
|
+
? sql`${group.id}::uuid IN (${recursiveServiceAccountGroupIdsSubquery(sql`sa.id`)})`
|
|
263
|
+
: sql`EXISTS (
|
|
264
|
+
SELECT 1 FROM auth.service_account_groups sg
|
|
265
|
+
WHERE sg.service_account_id = sa.id AND sg.group_id = ${group.id}::uuid
|
|
266
|
+
)`
|
|
267
|
+
}
|
|
268
|
+
ORDER BY sa.name, sa.id
|
|
269
|
+
`;
|
|
270
|
+
members.push(
|
|
271
|
+
...rows.map((row) => ({ type: "service_account" as const, id: row.id, displayName: row.name, serviceAccountKind: row.kind })),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
248
275
|
return members;
|
|
249
276
|
};
|
|
250
277
|
|
|
251
|
-
export const getManagers = async (params: {
|
|
278
|
+
export const getManagers = async (params: {
|
|
279
|
+
id: string;
|
|
280
|
+
type?: "user" | "group" | "service_account";
|
|
281
|
+
recursive?: boolean;
|
|
282
|
+
}): Promise<GroupMember[]> => {
|
|
252
283
|
const group = await getLocalGroupById(params.id);
|
|
253
284
|
if (!group) return [];
|
|
254
285
|
|
|
@@ -310,6 +341,7 @@ export const getManagers = async (params: { id: string; type?: "user" | "group";
|
|
|
310
341
|
FROM auth.group_manager_groups_v2 gmg
|
|
311
342
|
JOIN auth.groups g ON g.id = gmg.manager_group_id
|
|
312
343
|
WHERE gmg.group_id = ${group.id} AND g.provider = 'local'
|
|
344
|
+
AND NOT EXISTS (SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id)
|
|
313
345
|
ORDER BY g.name
|
|
314
346
|
`;
|
|
315
347
|
for (const row of groupRows) {
|
|
@@ -317,6 +349,16 @@ export const getManagers = async (params: { id: string; type?: "user" | "group";
|
|
|
317
349
|
}
|
|
318
350
|
}
|
|
319
351
|
|
|
352
|
+
if (!params.type || params.type === "service_account") {
|
|
353
|
+
const rows = await sql<{ id: string; name: string; kind: "standalone" | "agent" }[]>`
|
|
354
|
+
SELECT sa.id, sa.name, sa.kind FROM auth.group_manager_service_accounts gm
|
|
355
|
+
JOIN auth.service_accounts sa ON sa.id = gm.service_account_id
|
|
356
|
+
WHERE gm.group_id = ${group.id}::uuid AND sa.kind IN ('standalone', 'agent')
|
|
357
|
+
ORDER BY sa.name, sa.id`;
|
|
358
|
+
managers.push(
|
|
359
|
+
...rows.map((row) => ({ type: "service_account" as const, id: row.id, displayName: row.name, serviceAccountKind: row.kind })),
|
|
360
|
+
);
|
|
361
|
+
}
|
|
320
362
|
return managers;
|
|
321
363
|
};
|
|
322
364
|
|
|
@@ -340,7 +382,8 @@ export const getParents = async (params: { id: string; recursive?: boolean }): P
|
|
|
340
382
|
WHERE g.provider = 'local'
|
|
341
383
|
)
|
|
342
384
|
SELECT DISTINCT parent_group_id
|
|
343
|
-
FROM local_parent_tree
|
|
385
|
+
FROM local_parent_tree tree
|
|
386
|
+
WHERE NOT EXISTS (SELECT 1 FROM auth.personal_groups p WHERE p.group_id = tree.parent_group_id)
|
|
344
387
|
`
|
|
345
388
|
: await sql<DbRow[]>`
|
|
346
389
|
SELECT gg.parent_group_id
|
|
@@ -348,6 +391,7 @@ export const getParents = async (params: { id: string; recursive?: boolean }): P
|
|
|
348
391
|
JOIN auth.groups g ON g.id = gg.parent_group_id
|
|
349
392
|
WHERE gg.child_group_id = ${group.id}
|
|
350
393
|
AND g.provider = 'local'
|
|
394
|
+
AND NOT EXISTS (SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id)
|
|
351
395
|
ORDER BY g.name
|
|
352
396
|
`;
|
|
353
397
|
|
|
@@ -374,6 +418,7 @@ export const getManagedGroups = async (params: { id: string; recursive?: boolean
|
|
|
374
418
|
JOIN auth.group_manager_groups_v2 gmg ON gmg.manager_group_id = tree.manager_group_id
|
|
375
419
|
JOIN auth.groups g ON g.id = gmg.group_id
|
|
376
420
|
WHERE g.provider = 'local'
|
|
421
|
+
AND NOT EXISTS (SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id)
|
|
377
422
|
`
|
|
378
423
|
: await sql<DbRow[]>`
|
|
379
424
|
SELECT gmg.group_id
|
|
@@ -381,20 +426,46 @@ export const getManagedGroups = async (params: { id: string; recursive?: boolean
|
|
|
381
426
|
JOIN auth.groups g ON g.id = gmg.group_id
|
|
382
427
|
WHERE gmg.manager_group_id = ${group.id}
|
|
383
428
|
AND g.provider = 'local'
|
|
429
|
+
AND NOT EXISTS (SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id)
|
|
384
430
|
ORDER BY g.name
|
|
385
431
|
`;
|
|
386
432
|
|
|
387
433
|
return rows.map((row) => row.group_id as string);
|
|
388
434
|
};
|
|
389
435
|
|
|
390
|
-
export const addMember = async (
|
|
391
|
-
|
|
436
|
+
export const addMember = async (
|
|
437
|
+
params: {
|
|
438
|
+
id: string;
|
|
439
|
+
user?: string;
|
|
440
|
+
group?: string;
|
|
441
|
+
serviceAccount?: string;
|
|
442
|
+
},
|
|
443
|
+
db: typeof sql | TransactionSQL = sql,
|
|
444
|
+
): Promise<MutationResult<void>> => {
|
|
445
|
+
const group = await getLocalGroupById(params.id, db);
|
|
392
446
|
if (!group) return { ok: false, error: "Group not found", status: 404 };
|
|
447
|
+
const [personal] = await sql`SELECT 1 FROM auth.personal_groups
|
|
448
|
+
WHERE group_id = ${group.id}::uuid OR group_id = ${params.group ?? null}::uuid`;
|
|
449
|
+
if (personal) return { ok: false, error: "Personal Linux groups cannot be used in group relationships", status: 409 };
|
|
450
|
+
|
|
451
|
+
if (params.serviceAccount) {
|
|
452
|
+
const [account] = await db<{ id: string }[]>`
|
|
453
|
+
SELECT id FROM auth.service_accounts
|
|
454
|
+
WHERE id = ${params.serviceAccount}::uuid AND status = 'active' AND kind IN ('standalone', 'agent')
|
|
455
|
+
`;
|
|
456
|
+
if (!account) return { ok: false, error: "An active standalone or agent account is required", status: 400 };
|
|
457
|
+
const inserted = await db`
|
|
458
|
+
INSERT INTO auth.service_account_groups (service_account_id, group_id)
|
|
459
|
+
VALUES (${account.id}::uuid, ${group.id}::uuid) ON CONFLICT DO NOTHING
|
|
460
|
+
`;
|
|
461
|
+
if (inserted.count === 0) return { ok: false, error: "Service account is already a direct member of this group", status: 409 };
|
|
462
|
+
return { ok: true, data: undefined };
|
|
463
|
+
}
|
|
393
464
|
|
|
394
465
|
if (params.user) {
|
|
395
|
-
const provider = await getUserProvider(params.user);
|
|
466
|
+
const provider = await getUserProvider(params.user, db);
|
|
396
467
|
if (!provider) return { ok: false, error: "User not found", status: 404 };
|
|
397
|
-
const [existing] = await
|
|
468
|
+
const [existing] = await db<DbRow[]>`
|
|
398
469
|
SELECT 1
|
|
399
470
|
FROM auth.user_groups_v2
|
|
400
471
|
WHERE user_id = ${params.user}::uuid
|
|
@@ -402,14 +473,14 @@ export const addMember = async (params: { id: string; user?: string; group?: str
|
|
|
402
473
|
LIMIT 1
|
|
403
474
|
`;
|
|
404
475
|
if (existing) return { ok: false, error: "User is already a direct member of this group", status: 409 };
|
|
405
|
-
await
|
|
476
|
+
await db`INSERT INTO auth.user_groups_v2 (user_id, group_id) VALUES (${params.user}, ${group.id}) ON CONFLICT DO NOTHING`;
|
|
406
477
|
return { ok: true, data: undefined };
|
|
407
478
|
}
|
|
408
479
|
|
|
409
480
|
if (params.group) {
|
|
410
|
-
const isLocal = await ensureLocalGroupTreeMember(params.group);
|
|
481
|
+
const isLocal = await ensureLocalGroupTreeMember(params.group, db);
|
|
411
482
|
if (!isLocal) return { ok: false, error: "Only local groups can be nested into local groups", status: 400 };
|
|
412
|
-
const [existing] = await
|
|
483
|
+
const [existing] = await db<DbRow[]>`
|
|
413
484
|
SELECT 1
|
|
414
485
|
FROM auth.group_groups_v2
|
|
415
486
|
WHERE parent_group_id = ${group.id}::uuid
|
|
@@ -417,36 +488,68 @@ export const addMember = async (params: { id: string; user?: string; group?: str
|
|
|
417
488
|
LIMIT 1
|
|
418
489
|
`;
|
|
419
490
|
if (existing) return { ok: false, error: "Group is already a direct member of this group", status: 409 };
|
|
420
|
-
if (await wouldCreateLocalGroupCycle({ parentGroupId: group.id, childGroupId: params.group })) {
|
|
491
|
+
if (await wouldCreateLocalGroupCycle({ parentGroupId: group.id, childGroupId: params.group }, db)) {
|
|
421
492
|
return { ok: false, error: "Local group nesting cannot create cycles", status: 400 };
|
|
422
493
|
}
|
|
423
|
-
await
|
|
494
|
+
await db`INSERT INTO auth.group_groups_v2 (parent_group_id, child_group_id) VALUES (${group.id}, ${params.group}) ON CONFLICT DO NOTHING`;
|
|
424
495
|
return { ok: true, data: undefined };
|
|
425
496
|
}
|
|
426
497
|
|
|
427
498
|
return { ok: false, error: "Missing member", status: 400 };
|
|
428
499
|
};
|
|
429
500
|
|
|
430
|
-
export const removeMember = async (
|
|
431
|
-
|
|
501
|
+
export const removeMember = async (
|
|
502
|
+
params: {
|
|
503
|
+
id: string;
|
|
504
|
+
user?: string;
|
|
505
|
+
group?: string;
|
|
506
|
+
serviceAccount?: string;
|
|
507
|
+
},
|
|
508
|
+
db: typeof sql | TransactionSQL = sql,
|
|
509
|
+
): Promise<MutationResult<void>> => {
|
|
510
|
+
const group = await getLocalGroupById(params.id, db);
|
|
432
511
|
if (!group) return { ok: false, error: "Group not found", status: 404 };
|
|
433
512
|
|
|
513
|
+
if (params.serviceAccount) {
|
|
514
|
+
await db`DELETE FROM auth.service_account_groups
|
|
515
|
+
WHERE service_account_id = ${params.serviceAccount}::uuid AND group_id = ${group.id}::uuid`;
|
|
516
|
+
return { ok: true, data: undefined };
|
|
517
|
+
}
|
|
518
|
+
|
|
434
519
|
if (params.user) {
|
|
435
|
-
await
|
|
520
|
+
await db`DELETE FROM auth.user_groups_v2 WHERE user_id = ${params.user}::uuid AND group_id = ${group.id}::uuid`;
|
|
436
521
|
return { ok: true, data: undefined };
|
|
437
522
|
}
|
|
438
523
|
|
|
439
524
|
if (params.group) {
|
|
440
|
-
await
|
|
525
|
+
await db`DELETE FROM auth.group_groups_v2 WHERE parent_group_id = ${group.id}::uuid AND child_group_id = ${params.group}::uuid`;
|
|
441
526
|
return { ok: true, data: undefined };
|
|
442
527
|
}
|
|
443
528
|
|
|
444
529
|
return { ok: false, error: "Missing member", status: 400 };
|
|
445
530
|
};
|
|
446
531
|
|
|
447
|
-
export const addManager = async (params: {
|
|
532
|
+
export const addManager = async (params: {
|
|
533
|
+
id: string;
|
|
534
|
+
user?: string;
|
|
535
|
+
group?: string;
|
|
536
|
+
serviceAccount?: string;
|
|
537
|
+
}): Promise<MutationResult<void>> => {
|
|
448
538
|
const group = await getLocalGroupById(params.id);
|
|
449
539
|
if (!group) return { ok: false, error: "Group not found", status: 404 };
|
|
540
|
+
const [personal] = await sql`SELECT 1 FROM auth.personal_groups
|
|
541
|
+
WHERE group_id = ${group.id}::uuid OR group_id = ${params.group ?? null}::uuid`;
|
|
542
|
+
if (personal) return { ok: false, error: "Personal Linux groups cannot be used in group relationships", status: 409 };
|
|
543
|
+
|
|
544
|
+
if (params.serviceAccount) {
|
|
545
|
+
const [account] = await sql`SELECT id FROM auth.service_accounts
|
|
546
|
+
WHERE id = ${params.serviceAccount}::uuid AND status = 'active' AND kind IN ('standalone', 'agent')`;
|
|
547
|
+
if (!account) return { ok: false, error: "An active standalone or agent account is required", status: 400 };
|
|
548
|
+
const inserted = await sql`INSERT INTO auth.group_manager_service_accounts(group_id, service_account_id)
|
|
549
|
+
VALUES (${group.id}::uuid, ${params.serviceAccount}::uuid) ON CONFLICT DO NOTHING`;
|
|
550
|
+
if (inserted.count === 0) return { ok: false, error: "Service account already manages this group", status: 409 };
|
|
551
|
+
return { ok: true, data: undefined };
|
|
552
|
+
}
|
|
450
553
|
|
|
451
554
|
if (params.user) {
|
|
452
555
|
const provider = await getUserProvider(params.user);
|
|
@@ -481,10 +584,21 @@ export const addManager = async (params: { id: string; user?: string; group?: st
|
|
|
481
584
|
return { ok: false, error: "Missing manager", status: 400 };
|
|
482
585
|
};
|
|
483
586
|
|
|
484
|
-
export const removeManager = async (params: {
|
|
587
|
+
export const removeManager = async (params: {
|
|
588
|
+
id: string;
|
|
589
|
+
user?: string;
|
|
590
|
+
group?: string;
|
|
591
|
+
serviceAccount?: string;
|
|
592
|
+
}): Promise<MutationResult<void>> => {
|
|
485
593
|
const group = await getLocalGroupById(params.id);
|
|
486
594
|
if (!group) return { ok: false, error: "Group not found", status: 404 };
|
|
487
595
|
|
|
596
|
+
if (params.serviceAccount) {
|
|
597
|
+
await sql`DELETE FROM auth.group_manager_service_accounts
|
|
598
|
+
WHERE group_id = ${group.id}::uuid AND service_account_id = ${params.serviceAccount}::uuid`;
|
|
599
|
+
return { ok: true, data: undefined };
|
|
600
|
+
}
|
|
601
|
+
|
|
488
602
|
if (params.user) {
|
|
489
603
|
await sql`DELETE FROM auth.group_manager_users_v2 WHERE group_id = ${group.id}::uuid AND user_id = ${params.user}::uuid`;
|
|
490
604
|
return { ok: true, data: undefined };
|