@k2b/cloud 0.42.0 → 0.44.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 +13 -5
- package/scripts/app-favicon.ts +7 -6
- package/scripts/build.ts +3 -0
- package/scripts/installable-assets.ts +61 -0
- package/scripts/preload.ts +3 -0
- package/src/_internal/app-access-path.ts +12 -0
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +185 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +10 -5
- package/src/_internal/define-app.ts +159 -45
- package/src/_internal/installable-assets.ts +90 -0
- package/src/_internal/installable-head.ts +22 -0
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/page-responses.ts +34 -3
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +15 -1
- package/src/_internal/registry.ts +44 -1
- package/src/_internal/runtime-context.ts +9 -1
- package/src/_internal/runtime-watcher.ts +8 -0
- package/src/_internal/status-preserving-ssr.ts +12 -1
- package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
- package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
- package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
- package/src/access/AppAccessEditor.tsx +380 -0
- package/src/access/PermissionEditor.browser-harness.tsx +22 -2
- package/src/access/PermissionEditor.tsx +386 -77
- package/src/access/PrincipalAvatar.tsx +63 -0
- package/src/access/PrincipalPicker.tsx +29 -8
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-adapter.ts +51 -0
- package/src/access/app-access-messages.ts +172 -0
- package/src/access/messages.ts +14 -0
- package/src/access/ui.ts +4 -1
- package/src/account/Avatar.tsx +21 -3
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +8 -1
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/capability-execution.ts +5 -2
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/code-capability-routes.ts +1 -0
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +4 -4
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +38 -27
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +6 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api-slots.ts +10 -2
- package/src/ai/openai-api.ts +83 -16
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/projects-routes.ts +2 -1
- package/src/ai/projects.ts +13 -12
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +138 -0
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider-retry.ts +1 -1
- package/src/ai/provider.ts +2 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +11 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas-migrate.ts +1 -0
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +6 -0
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +17 -18
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +29 -46
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +37 -21
- package/src/ai/turn-failure.ts +24 -2
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +40 -16
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/admin-directory-shares.ts +55 -0
- package/src/api/admin-identity.ts +81 -2
- package/src/api/app-access.ts +85 -0
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +26 -3
- package/src/api/capabilities.ts +127 -15
- package/src/api/capability-streams.ts +2 -5
- package/src/api/identity-child-invocation.ts +145 -0
- package/src/api/index.ts +11 -2
- package/src/api/mcp.ts +180 -5
- package/src/api/me.ts +38 -17
- package/src/api/pwa-phone.ts +87 -48
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/api/widgets.ts +1 -0
- package/src/app-pwa/authority.ts +75 -0
- package/src/app-pwa/contracts.ts +177 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +23 -0
- package/src/app-pwa/protocol.ts +27 -0
- package/src/app-pwa/schema.ts +59 -0
- package/src/app-pwa/service.ts +861 -0
- package/src/app-pwa/transaction.ts +338 -0
- package/src/app-pwa/transport-input.ts +57 -0
- package/src/app-pwa/transport.ts +235 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/app-access-recovery.ts +44 -0
- package/src/browser/app-session.ts +14 -3
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/live-websocket.ts +5 -0
- package/src/browser/pwa-drafts.ts +57 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/resource-picker.tsx +3 -2
- package/src/capabilities/calendar-sources.ts +302 -0
- package/src/capabilities/executions.ts +10 -3
- package/src/capabilities/migrate.ts +4 -0
- package/src/capabilities/nested.ts +43 -0
- package/src/capabilities/server-invoke.ts +202 -0
- package/src/capabilities/server.ts +26 -187
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/account.ts +57 -0
- package/src/cli/admin/agents.ts +66 -0
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +7 -3
- package/src/cli/app-access.ts +60 -0
- package/src/cli/apps.ts +89 -26
- package/src/cli/avatar.ts +64 -0
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/cli/index.ts +1 -1
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +52 -5
- package/src/contracts/app-approval.ts +8 -1
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +24 -5
- package/src/contracts/calendar-source.ts +274 -0
- package/src/contracts/calendar-view.ts +35 -0
- package/src/contracts/capabilities.ts +164 -3
- package/src/contracts/geo-admin.ts +2 -0
- package/src/contracts/geo-route.ts +0 -11
- package/src/contracts/geo.ts +14 -2
- package/src/contracts/index.ts +6 -0
- package/src/contracts/installable.ts +40 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/profile.ts +32 -0
- package/src/contracts/pwa-builds.ts +14 -0
- package/src/contracts/pwa-location.ts +38 -0
- package/src/contracts/pwa.ts +24 -3
- package/src/contracts/registry.ts +8 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +4 -0
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +2 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +7 -6
- package/src/server/middleware/auth.ts +168 -66
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +4 -2
- package/src/server/middleware/workload.ts +3 -3
- package/src/server/oauth-scope.ts +10 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +35 -3
- package/src/services/accounts/avatar.ts +342 -35
- package/src/services/accounts/base-group.ts +4 -5
- package/src/services/accounts/directory-shares.ts +157 -0
- package/src/services/accounts/entities.ts +50 -6
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +64 -12
- package/src/services/accounts/local-groups.ts +1 -1
- package/src/services/accounts/users.ts +23 -15
- package/src/services/app-access-management.ts +519 -0
- package/src/services/app-access.ts +25 -7
- package/src/services/app-approval.ts +15 -4
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/admin.ts +4 -1
- package/src/services/geo/ledger.ts +47 -14
- package/src/services/geo/route-chain.ts +40 -18
- package/src/services/geo/route-estimate.ts +19 -0
- package/src/services/geo/route-plan.ts +0 -2
- package/src/services/geo/route-service.ts +57 -39
- package/src/services/geo/service.ts +51 -25
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/index.ts +2 -1
- package/src/services/identity/invocation-actor.ts +50 -2
- package/src/services/identity/invocation-authority.ts +38 -1
- package/src/services/identity/invocation-token.ts +255 -50
- package/src/services/identity/issue-invocation.ts +9 -1
- package/src/services/identity/models-only-auth.ts +13 -4
- package/src/services/identity/nested-invocation.ts +128 -0
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +24 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/policy.ts +19 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/service-account-credentials.ts +5 -2
- package/src/services/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +91 -47
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/index.ts +2 -0
- package/src/shared-text/documents.ts +25 -8
- package/src/shared-text/log.ts +1 -1
- package/src/shared-text/text-diff.ts +75 -0
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +11 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +114 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/AppPwaRuntime.island.tsx +82 -0
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +15 -7
- package/src/ssr/PwaLayout.tsx +73 -8
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/PwaRuntime.island.tsx +85 -6
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/ShellRuntime.island.tsx +5 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-pwa-document.tsx +16 -0
- package/src/ssr/app-pwa-messages.ts +17 -0
- package/src/ssr/index.ts +8 -0
- package/src/ssr/layout-context.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +10 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +2 -0
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/runtime/executor.ts +132 -33
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- package/src/shared/ai-model-request-settings.ts +0 -21
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { err, fail, ok, type Result } from "@k2b/stdlib";
|
|
2
|
+
import { sql } from "bun";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import type { User } from "../../contracts/shared";
|
|
5
|
+
import type { RequestActor } from "../../server/middleware/auth";
|
|
6
|
+
import { audit } from "../audit";
|
|
7
|
+
|
|
8
|
+
import * as entities from "./entities";
|
|
9
|
+
|
|
10
|
+
export const DirectorySharePrincipalSchema = z.discriminatedUnion("type", [
|
|
11
|
+
z.object({ type: z.literal("user"), userId: z.uuid() }).strict(),
|
|
12
|
+
z.object({ type: z.literal("group"), groupId: z.uuid() }).strict(),
|
|
13
|
+
z.object({ type: z.literal("service_account"), serviceAccountId: z.uuid() }).strict(),
|
|
14
|
+
]);
|
|
15
|
+
type DirectorySharePrincipal = z.infer<typeof DirectorySharePrincipalSchema>;
|
|
16
|
+
type Administrator = Pick<User, "id" | "uid" | "provider" | "roles">;
|
|
17
|
+
const administrator = (actor: Administrator) => actor.roles.includes("admin");
|
|
18
|
+
const auditActor = (actor: Administrator) => ({ userId: actor.id, uid: actor.uid, provider: actor.provider, roles: actor.roles });
|
|
19
|
+
const recipientExists = async (id: string) => {
|
|
20
|
+
const [row] = await sql`SELECT id FROM auth.service_accounts WHERE id=${id}::uuid AND kind IN ('standalone','agent')`;
|
|
21
|
+
return Boolean(row);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const directoryShares = {
|
|
25
|
+
list: async (params: { actor: Administrator; recipientId: string; page: number; perPage: number }) => {
|
|
26
|
+
if (!administrator(params.actor)) return fail(err.forbidden("Administrator access required"));
|
|
27
|
+
if (!(await recipientExists(params.recipientId))) return fail(err.notFound("Service account"));
|
|
28
|
+
const rows = await sql<
|
|
29
|
+
{
|
|
30
|
+
id: string;
|
|
31
|
+
user_id: string | null;
|
|
32
|
+
group_id: string | null;
|
|
33
|
+
service_account_id: string | null;
|
|
34
|
+
label: string;
|
|
35
|
+
service_account_kind: string | null;
|
|
36
|
+
total: number;
|
|
37
|
+
}[]
|
|
38
|
+
>`SELECT share.id, share.user_id, share.group_id, share.service_account_id,
|
|
39
|
+
COALESCE(NULLIF(u.display_name,''),u.uid,g.name,sa.name) AS label,
|
|
40
|
+
sa.kind AS service_account_kind, count(*) OVER()::int AS total
|
|
41
|
+
FROM auth.directory_shares share
|
|
42
|
+
LEFT JOIN auth.users u ON u.id=share.user_id
|
|
43
|
+
LEFT JOIN auth.groups g ON g.id=share.group_id
|
|
44
|
+
LEFT JOIN auth.service_accounts sa ON sa.id=share.service_account_id
|
|
45
|
+
WHERE share.recipient_id=${params.recipientId}::uuid
|
|
46
|
+
ORDER BY share.created_at, share.id
|
|
47
|
+
LIMIT ${params.perPage} OFFSET ${(params.page - 1) * params.perPage}`;
|
|
48
|
+
const total =
|
|
49
|
+
rows[0]?.total ??
|
|
50
|
+
(
|
|
51
|
+
await sql<
|
|
52
|
+
{ total: number }[]
|
|
53
|
+
>`SELECT count(*)::int AS total FROM auth.directory_shares WHERE recipient_id=${params.recipientId}::uuid`
|
|
54
|
+
)[0]!.total;
|
|
55
|
+
return ok({
|
|
56
|
+
items: rows.map((row) => ({
|
|
57
|
+
id: row.id,
|
|
58
|
+
label: row.label,
|
|
59
|
+
serviceAccountKind: row.service_account_kind,
|
|
60
|
+
principal: row.user_id
|
|
61
|
+
? { type: "user" as const, userId: row.user_id }
|
|
62
|
+
: row.group_id
|
|
63
|
+
? { type: "group" as const, groupId: row.group_id }
|
|
64
|
+
: { type: "service_account" as const, serviceAccountId: row.service_account_id! },
|
|
65
|
+
})),
|
|
66
|
+
total,
|
|
67
|
+
page: params.page,
|
|
68
|
+
perPage: params.perPage,
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
grant: async (params: {
|
|
72
|
+
actor: Administrator;
|
|
73
|
+
recipientId: string;
|
|
74
|
+
principal: DirectorySharePrincipal;
|
|
75
|
+
}): Promise<Result<{ id: string }>> => {
|
|
76
|
+
if (!administrator(params.actor)) return fail(err.forbidden("Administrator access required"));
|
|
77
|
+
const principal = DirectorySharePrincipalSchema.safeParse(params.principal);
|
|
78
|
+
if (!principal.success) return fail(err.badInput("Choose an exact directory entry"));
|
|
79
|
+
if (!(await recipientExists(params.recipientId))) return fail(err.notFound("Service account"));
|
|
80
|
+
const target = principal.data;
|
|
81
|
+
try {
|
|
82
|
+
return await sql.begin(async (db) => {
|
|
83
|
+
const [row] = await db<{ id: string }[]>`INSERT INTO auth.directory_shares(recipient_id,user_id,group_id,service_account_id)
|
|
84
|
+
VALUES(${params.recipientId}::uuid, ${target.type === "user" ? target.userId : null}::uuid,
|
|
85
|
+
${target.type === "group" ? target.groupId : null}::uuid,
|
|
86
|
+
${target.type === "service_account" ? target.serviceAccountId : null}::uuid)
|
|
87
|
+
ON CONFLICT (recipient_id,user_id,group_id,service_account_id) DO UPDATE SET recipient_id=EXCLUDED.recipient_id RETURNING id`;
|
|
88
|
+
if (!row) return fail(err.internal("Could not save directory access"));
|
|
89
|
+
await audit.record(
|
|
90
|
+
{
|
|
91
|
+
action: "accounts.directory.grant",
|
|
92
|
+
outcome: "allowed",
|
|
93
|
+
actor: auditActor(params.actor),
|
|
94
|
+
target: { type: "directory_share", id: row.id },
|
|
95
|
+
metadata: { serviceAccountId: params.recipientId, principal: target },
|
|
96
|
+
},
|
|
97
|
+
db,
|
|
98
|
+
);
|
|
99
|
+
return ok(row);
|
|
100
|
+
});
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (
|
|
103
|
+
z.object({ code: z.literal("23503") }).safeParse(error).success ||
|
|
104
|
+
z.object({ errno: z.literal("23503") }).safeParse(error).success
|
|
105
|
+
)
|
|
106
|
+
return fail(err.notFound("Directory entry"));
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
revoke: async (params: { actor: Administrator; recipientId: string; id: string }): Promise<Result<void>> => {
|
|
111
|
+
if (!administrator(params.actor)) return fail(err.forbidden("Administrator access required"));
|
|
112
|
+
return sql.begin(async (db) => {
|
|
113
|
+
await db`DELETE FROM auth.directory_shares WHERE id=${params.id}::uuid AND recipient_id=${params.recipientId}::uuid`;
|
|
114
|
+
await audit.record(
|
|
115
|
+
{
|
|
116
|
+
action: "accounts.directory.revoke",
|
|
117
|
+
outcome: "allowed",
|
|
118
|
+
actor: auditActor(params.actor),
|
|
119
|
+
target: { type: "directory_share", id: params.id },
|
|
120
|
+
metadata: { serviceAccountId: params.recipientId },
|
|
121
|
+
},
|
|
122
|
+
db,
|
|
123
|
+
);
|
|
124
|
+
return ok();
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
search: async (params: { actor: RequestActor; query: Omit<entities.EntityListParams, "visibility">; requestId?: string }) => {
|
|
128
|
+
const { actor, query } = params;
|
|
129
|
+
const result = await (async () => {
|
|
130
|
+
if (
|
|
131
|
+
actor.kind !== "service_account" ||
|
|
132
|
+
actor.delegatedUser ||
|
|
133
|
+
!["standalone", "agent"].includes(actor.serviceAccount.kind) ||
|
|
134
|
+
!actor.scopes.includes("read")
|
|
135
|
+
)
|
|
136
|
+
return fail(err.forbidden("Directory read access required"));
|
|
137
|
+
// Sharing an entry never shares its membership or management relationships.
|
|
138
|
+
if (
|
|
139
|
+
query.memberOfGroupId ||
|
|
140
|
+
query.managerOfGroupId ||
|
|
141
|
+
query.parentGroupId ||
|
|
142
|
+
query.managedByUserId ||
|
|
143
|
+
query.userMemberOfGroupIds?.length
|
|
144
|
+
)
|
|
145
|
+
return fail(err.forbidden("Directory shares do not include group relationships"));
|
|
146
|
+
return ok(await entities.list({ ...query, visibility: { type: "directory_shares", serviceAccountId: actor.serviceAccount.id } }));
|
|
147
|
+
})();
|
|
148
|
+
return audit.recordResult({
|
|
149
|
+
action: "accounts.directory.search",
|
|
150
|
+
result,
|
|
151
|
+
requestId: params.requestId,
|
|
152
|
+
target: actor.kind === "service_account" ? { type: "service_account", id: actor.serviceAccount.id } : null,
|
|
153
|
+
metadata:
|
|
154
|
+
actor.kind === "service_account" ? { serviceAccountId: actor.serviceAccount.id, credentialId: actor.credentialId ?? null } : {},
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
};
|
|
@@ -2,7 +2,7 @@ import { 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
|
-
import type { ServiceAccountKind } from "../service-accounts";
|
|
5
|
+
import type { ServiceAccountKind, ServiceAccountStatus } from "../service-accounts";
|
|
6
6
|
import { buildBaseGroup, personalOwnerJoin } from "./base-group";
|
|
7
7
|
import { buildBaseUser } from "./base-user";
|
|
8
8
|
import { buildManagedGroupScopeCondition, recursiveGroupIdsSubquery } from "./group-sql";
|
|
@@ -11,13 +11,18 @@ type DbRow = Record<string, unknown>;
|
|
|
11
11
|
type SqlFragment = any;
|
|
12
12
|
|
|
13
13
|
export type EntityListParams = {
|
|
14
|
-
visibility:
|
|
14
|
+
visibility:
|
|
15
|
+
| { type: "directory" }
|
|
16
|
+
| { type: "self_and_effective_groups"; userId: string }
|
|
17
|
+
| { type: "directory_shares"; serviceAccountId: string };
|
|
15
18
|
search?: string;
|
|
16
19
|
kinds?: EntityKind[];
|
|
17
20
|
provider?: UserProvider;
|
|
18
21
|
profile?: UserProfile;
|
|
19
22
|
userIds?: string[];
|
|
20
23
|
groupIds?: string[];
|
|
24
|
+
serviceAccountKinds?: ServiceAccountKind[];
|
|
25
|
+
serviceAccountStatus?: ServiceAccountStatus;
|
|
21
26
|
excludeUserIds?: string[];
|
|
22
27
|
excludeGroupIds?: string[];
|
|
23
28
|
excludeServiceAccountIds?: string[];
|
|
@@ -432,7 +437,17 @@ export const list = async (
|
|
|
432
437
|
const visibilityCondition =
|
|
433
438
|
params.visibility.type === "directory"
|
|
434
439
|
? sql`TRUE`
|
|
435
|
-
:
|
|
440
|
+
: params.visibility.type === "directory_shares"
|
|
441
|
+
? sql`EXISTS (
|
|
442
|
+
SELECT 1 FROM auth.directory_shares share
|
|
443
|
+
JOIN auth.service_accounts recipient ON recipient.id = share.recipient_id
|
|
444
|
+
WHERE recipient.id = ${params.visibility.serviceAccountId}::uuid
|
|
445
|
+
AND recipient.kind IN ('standalone', 'agent') AND recipient.status = 'active'
|
|
446
|
+
AND ((entity_rows.kind = 'user' AND share.user_id = entity_rows.id)
|
|
447
|
+
OR (entity_rows.kind = 'group' AND share.group_id = entity_rows.id)
|
|
448
|
+
OR (entity_rows.kind = 'service_account' AND share.service_account_id = entity_rows.id))
|
|
449
|
+
)`
|
|
450
|
+
: sql`(
|
|
436
451
|
(kind = 'user' AND id = ${params.visibility.userId}::uuid)
|
|
437
452
|
OR (
|
|
438
453
|
kind = 'group'
|
|
@@ -459,6 +474,13 @@ export const list = async (
|
|
|
459
474
|
(params.excludeServiceAccountIds?.length ?? 0) === 0
|
|
460
475
|
? sql`TRUE`
|
|
461
476
|
: sql`(kind <> 'service_account' OR id <> ALL(${toPgUuidArray(params.excludeServiceAccountIds ?? [])}::uuid[]))`;
|
|
477
|
+
const serviceAccountKindsCondition =
|
|
478
|
+
(params.serviceAccountKinds?.length ?? 0) === 0
|
|
479
|
+
? sql`TRUE`
|
|
480
|
+
: sql`(kind <> 'service_account' OR service_account_kind = ANY(${toPgTextArray(params.serviceAccountKinds ?? [])}::text[]))`;
|
|
481
|
+
const serviceAccountStatusCondition = params.serviceAccountStatus
|
|
482
|
+
? sql`(kind <> 'service_account' OR status = ${params.serviceAccountStatus})`
|
|
483
|
+
: sql`TRUE`;
|
|
462
484
|
const userMemberOfGroupCondition =
|
|
463
485
|
(params.userMemberOfGroupIds?.length ?? 0) === 0
|
|
464
486
|
? sql`TRUE`
|
|
@@ -478,6 +500,8 @@ export const list = async (
|
|
|
478
500
|
AND ${excludeUserCondition}
|
|
479
501
|
AND ${excludeGroupCondition}
|
|
480
502
|
AND ${excludeServiceAccountCondition}
|
|
503
|
+
AND ${serviceAccountKindsCondition}
|
|
504
|
+
AND ${serviceAccountStatusCondition}
|
|
481
505
|
AND ${userMemberOfGroupCondition}
|
|
482
506
|
AND ${personalCondition}
|
|
483
507
|
AND (
|
|
@@ -500,9 +524,10 @@ export const list = async (
|
|
|
500
524
|
OR (
|
|
501
525
|
kind = 'service_account' AND (
|
|
502
526
|
LOWER(name) LIKE ${pattern} ESCAPE '\\'
|
|
503
|
-
OR LOWER(COALESCE(app_id, '')) LIKE ${pattern} ESCAPE '\\'
|
|
527
|
+
OR (${params.visibility.type !== "directory_shares"} AND (LOWER(COALESCE(app_id, '')) LIKE ${pattern} ESCAPE '\\'
|
|
504
528
|
OR LOWER(COALESCE(resource_type, '')) LIKE ${pattern} ESCAPE '\\'
|
|
505
529
|
OR LOWER(COALESCE(resource_id, '')) LIKE ${pattern} ESCAPE '\\'
|
|
530
|
+
))
|
|
506
531
|
)
|
|
507
532
|
)
|
|
508
533
|
)
|
|
@@ -635,9 +660,28 @@ export const list = async (
|
|
|
635
660
|
OFFSET ${offset}
|
|
636
661
|
`;
|
|
637
662
|
|
|
638
|
-
|
|
663
|
+
// A page past the end has no rows to carry the window count; the total still counts every match.
|
|
664
|
+
let total = rows.length > 0 ? Number(rows[0]!.total) : 0;
|
|
665
|
+
if (!rows.length && page > 1) total = (await list({ ...params, page: 1, perPage: 1 })).total;
|
|
639
666
|
return {
|
|
640
|
-
items: rows.map(
|
|
667
|
+
items: rows.map((row) => {
|
|
668
|
+
const entity = mapEntityRow(row);
|
|
669
|
+
if (params.visibility.type !== "directory_shares") return entity;
|
|
670
|
+
if (entity.kind === "group") return { ...entity, group: { ...entity.group, personalOwner: null } };
|
|
671
|
+
if (entity.kind === "service_account")
|
|
672
|
+
return {
|
|
673
|
+
...entity,
|
|
674
|
+
serviceAccount: {
|
|
675
|
+
...entity.serviceAccount,
|
|
676
|
+
delegatedUserId: null,
|
|
677
|
+
createdBy: null,
|
|
678
|
+
appId: null,
|
|
679
|
+
resourceType: null,
|
|
680
|
+
resourceId: null,
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
return entity;
|
|
684
|
+
}),
|
|
641
685
|
total,
|
|
642
686
|
pagination: {
|
|
643
687
|
page,
|
|
@@ -86,7 +86,9 @@ const listCanonical = async (params: {
|
|
|
86
86
|
OFFSET ${offset}
|
|
87
87
|
`;
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
// A page past the end has no rows to carry the window count; the total still counts every match.
|
|
90
|
+
let total = rows.length > 0 ? Number((rows[0] as Record<string, unknown>).total) : 0;
|
|
91
|
+
if (!rows.length && page > 1) total = (await listCanonical({ ...params, page: 1, perPage: 1 })).total;
|
|
90
92
|
return {
|
|
91
93
|
groups: rows.map(buildBaseGroup),
|
|
92
94
|
total,
|
|
@@ -16,7 +16,7 @@ export type AccountIdentityUser = {
|
|
|
16
16
|
profile: UserProfile;
|
|
17
17
|
posix: { uidNumber: number; primaryGidNumber: number } | null;
|
|
18
18
|
};
|
|
19
|
-
/** `personal` marks a user's personal Linux group
|
|
19
|
+
/** `personal` marks a user's durably owned personal Linux group. */
|
|
20
20
|
export type AccountIdentityGroup = { id: string; provider: UserProvider; name: string; gidNumber: number | null; personal: boolean };
|
|
21
21
|
export type AccountIdentityPage<T> = { items: T[]; nextCursor: string | null };
|
|
22
22
|
export type AccountIdentityAvailability = { localLinuxEnabled: boolean; freeipaEnabled: boolean };
|
|
@@ -122,7 +122,7 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
|
|
|
122
122
|
const groupRows = async (filter: SQLQuery) => {
|
|
123
123
|
const rows = await db<{ id: string; provider: UserProvider; name: string; gid_number: number | null; personal: boolean }[]>`
|
|
124
124
|
SELECT g.id, g.provider, g.name, g.gid_number,
|
|
125
|
-
EXISTS(SELECT 1 FROM auth.
|
|
125
|
+
EXISTS(SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id) AS personal
|
|
126
126
|
FROM auth.groups g
|
|
127
127
|
WHERE ${filter} ORDER BY g.id LIMIT ${PAGE_SIZE + 1}
|
|
128
128
|
`;
|
|
@@ -207,22 +207,74 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
|
|
|
207
207
|
if ((input.kind !== "users" && input.kind !== "groups") || typeof input.name !== "string" || !input.name || input.name.includes("\0"))
|
|
208
208
|
throw new AccountIdentityError("invalid_identity_lookup", 400);
|
|
209
209
|
};
|
|
210
|
+
const reconcileIdentity = async (
|
|
211
|
+
input: { kind: "users" | "groups"; provider: UserProvider; name: string; identityId?: string; signal?: AbortSignal },
|
|
212
|
+
state: AccountIdentityAvailability,
|
|
213
|
+
): Promise<AccountIdentityReconciliation> => {
|
|
214
|
+
validateLookup(input);
|
|
215
|
+
if (input.provider !== "local" && input.provider !== "ipa") throw new AccountIdentityError("invalid_identity_lookup", 400);
|
|
216
|
+
if (input.identityId !== undefined) cursor(input.identityId);
|
|
217
|
+
if (input.signal?.aborted) return { state: "unknown", reason: "provider_unavailable" };
|
|
218
|
+
if (input.provider === "ipa") {
|
|
219
|
+
if (!state.freeipaEnabled) return { state: "unknown", reason: "provider_disabled" };
|
|
220
|
+
return upstreamIdentity(input);
|
|
221
|
+
}
|
|
222
|
+
const result = await readLocalIdentity(input);
|
|
223
|
+
return input.signal?.aborted ? { state: "unknown", reason: "provider_unavailable" } : result;
|
|
224
|
+
};
|
|
210
225
|
return {
|
|
226
|
+
/** Trusted application provisioning only; no profile, grants, credentials, or administrator impersonation. */
|
|
227
|
+
async provisioningUsers(input: { provider?: UserProvider; id?: string; after?: string }) {
|
|
228
|
+
if (input.provider !== undefined && input.provider !== "local" && input.provider !== "ipa")
|
|
229
|
+
throw new AccountIdentityError("invalid_provider", 400);
|
|
230
|
+
const id = cursor(input.id);
|
|
231
|
+
const after = cursor(input.after);
|
|
232
|
+
const state = await availability();
|
|
233
|
+
const [localAllowed, ipaAllowed] = await Promise.all([
|
|
234
|
+
isAccountCategoryAllowed({ provider: "local", profile: "user" }, db),
|
|
235
|
+
isAccountCategoryAllowed({ provider: "ipa", profile: "user" }, db),
|
|
236
|
+
]);
|
|
237
|
+
const rows = await db<
|
|
238
|
+
{
|
|
239
|
+
id: string;
|
|
240
|
+
provider: UserProvider;
|
|
241
|
+
uid: string;
|
|
242
|
+
managed_by: UserProvider | null;
|
|
243
|
+
uid_number: number | null;
|
|
244
|
+
primary_gid_number: number | null;
|
|
245
|
+
}[]
|
|
246
|
+
>`SELECT u.id,u.provider,u.uid,p.managed_by,p.uid_number,p.primary_gid_number
|
|
247
|
+
FROM auth.users u LEFT JOIN auth.user_posix p ON p.user_id=u.id
|
|
248
|
+
WHERE u.provider IN ('local','ipa') AND (${input.provider ?? null}::text IS NULL OR u.provider=${input.provider ?? null})
|
|
249
|
+
AND u.profile='user'
|
|
250
|
+
AND (u.account_expires IS NULL OR u.account_expires>now())
|
|
251
|
+
AND (${id}::uuid IS NULL OR u.id=${id}::uuid)
|
|
252
|
+
AND (${after}::uuid IS NULL OR u.id>${after}::uuid)
|
|
253
|
+
ORDER BY u.id LIMIT ${PAGE_SIZE + 1}`;
|
|
254
|
+
const source = page(rows);
|
|
255
|
+
const items = source.items
|
|
256
|
+
.filter((row) => (row.provider === "local" ? localAllowed : ipaAllowed && state.freeipaEnabled))
|
|
257
|
+
.map((row) => ({
|
|
258
|
+
id: row.id,
|
|
259
|
+
provider: row.provider,
|
|
260
|
+
username: row.uid,
|
|
261
|
+
posix:
|
|
262
|
+
row.managed_by === row.provider && positiveId(row.uid_number) && positiveId(row.primary_gid_number)
|
|
263
|
+
? { uidNumber: row.uid_number, primaryGidNumber: row.primary_gid_number }
|
|
264
|
+
: null,
|
|
265
|
+
}));
|
|
266
|
+
return { items, nextCursor: source.nextCursor, availability: state };
|
|
267
|
+
},
|
|
211
268
|
async reconcile(
|
|
212
269
|
actor: RequestActor,
|
|
213
270
|
input: { kind: "users" | "groups"; provider: UserProvider; name: string; identityId?: string; signal?: AbortSignal },
|
|
214
271
|
): Promise<AccountIdentityReconciliation> {
|
|
215
272
|
const { state } = await requireAdmin(actor);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (!state.freeipaEnabled) return { state: "unknown", reason: "provider_disabled" };
|
|
222
|
-
return upstreamIdentity(input);
|
|
223
|
-
}
|
|
224
|
-
const result = await readLocalIdentity(input);
|
|
225
|
-
return input.signal?.aborted ? { state: "unknown", reason: "provider_unavailable" } : result;
|
|
273
|
+
return reconcileIdentity(input, state);
|
|
274
|
+
},
|
|
275
|
+
/** Trusted provisioning services use the same live identity check as administrator provisioning. */
|
|
276
|
+
async provisioningIdentity(input: { kind: "users"; provider: UserProvider; name: string; identityId: string; signal?: AbortSignal }) {
|
|
277
|
+
return reconcileIdentity(input, await availability());
|
|
226
278
|
},
|
|
227
279
|
/** Trusted application jobs only; this read does not authorize an HTTP caller or a filesystem mutation. */
|
|
228
280
|
async localLifecycle(input: { kind: "users" | "groups"; identityId: string; name: string }) {
|
|
@@ -164,7 +164,7 @@ export const remove = async (params: { id: string }): Promise<MutationResult<voi
|
|
|
164
164
|
const group = await getLocalGroupById(params.id);
|
|
165
165
|
if (!group) return { ok: false, error: "Group not found", status: 404 };
|
|
166
166
|
|
|
167
|
-
const [primary] = await sql`SELECT 1 FROM auth.
|
|
167
|
+
const [primary] = await sql`SELECT 1 FROM auth.personal_groups WHERE group_id = ${params.id}::uuid LIMIT 1`;
|
|
168
168
|
if (primary) return { ok: false, error: "This group is a user's primary Linux group and cannot be deleted.", status: 409 };
|
|
169
169
|
try {
|
|
170
170
|
await sql`DELETE FROM auth.groups WHERE id = ${params.id}::uuid AND provider = 'local'`;
|
|
@@ -4,7 +4,7 @@ import { freeipa } from "../../server/services";
|
|
|
4
4
|
import { createAuthLoginUrl } from "../../shared/redirect";
|
|
5
5
|
import { isAccountCategoryAllowed } from "../account-category-policy";
|
|
6
6
|
import type { AuditActor } from "../audit";
|
|
7
|
-
import { getFreeIpaConfig } from "../freeipa-config";
|
|
7
|
+
import { getFreeIpaAdminGroups, getFreeIpaConfig } from "../freeipa-config";
|
|
8
8
|
import { getServiceIpaSession } from "../ipa/service-account";
|
|
9
9
|
import { toPgTextArray, toPgUuidArray } from "../postgres";
|
|
10
10
|
import { providers } from "../providers";
|
|
@@ -12,7 +12,7 @@ import { session } from "../session";
|
|
|
12
12
|
import * as settings from "../settings";
|
|
13
13
|
import { buildRoles } from "./authz";
|
|
14
14
|
import { buildBaseUser, resolveProviderProfile } from "./base-user";
|
|
15
|
-
import { managedGroupIdsSubquery, managedGroupsNamesSubquery, recursiveGroupIdsSubquery,
|
|
15
|
+
import { managedGroupIdsSubquery, managedGroupsNamesSubquery, recursiveGroupIdsSubquery, recursiveUserGroupsSubquery } from "./group-sql";
|
|
16
16
|
import { buildIpaUserData, emptyIpaUserData, userIpaDataColumns, userIpaDataJoin } from "./ipa-data";
|
|
17
17
|
import {
|
|
18
18
|
canPersistStoredAdmin,
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
import type { AccountsNotificationSender } from "./notification-sender";
|
|
26
26
|
import { transitionIpaUserToLocal } from "./switching";
|
|
27
27
|
|
|
28
|
-
export { clearAvatar, getAvatar, parseAvatarDataUrl, setAvatar } from "./avatar";
|
|
28
|
+
export { clearAvatar, getAvatar, getAvatarState, parseAvatarDataUrl, setAvatar, updateAvatar } from "./avatar";
|
|
29
29
|
|
|
30
30
|
type DbRow = Record<string, unknown>;
|
|
31
31
|
type UserMutationTarget = BaseUser & { accountExpires: string | null; storedAdmin: boolean };
|
|
@@ -168,11 +168,12 @@ const buildUser = (row: DbRow, groupsAdmin: string[]): User => {
|
|
|
168
168
|
};
|
|
169
169
|
};
|
|
170
170
|
|
|
171
|
-
export const get = async (params: { id: string } | { uid: string }): Promise<User | null> => {
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
const
|
|
171
|
+
export const get = async (params: { id: string } | { uid: string }, options: { db?: SQL } = {}): Promise<User | null> => {
|
|
172
|
+
const db = options.db ?? sql;
|
|
173
|
+
const whereClause = "id" in params ? db`u.id = ${params.id}` : db`u.uid = ${params.uid}`;
|
|
174
|
+
const userIdExpr = "id" in params ? db`${params.id}` : db`u.id`;
|
|
175
|
+
const groupsAdmin = options.db ? await getFreeIpaAdminGroups(db) : (await getFreeIpaConfig()).groupsAdmin;
|
|
176
|
+
const rows = await db<DbRow[]>`
|
|
176
177
|
SELECT u.*,
|
|
177
178
|
${userIpaDataColumns()},
|
|
178
179
|
CASE
|
|
@@ -346,17 +347,24 @@ export const list = async (params: {
|
|
|
346
347
|
};
|
|
347
348
|
};
|
|
348
349
|
|
|
349
|
-
|
|
350
|
+
/** Technical membership reads stay complete; human-facing lists exclude personal groups explicitly. */
|
|
351
|
+
export const getGroups = async (params: { id: string; recursive?: boolean; includePersonal?: boolean }): Promise<string[]> => {
|
|
350
352
|
if (params.recursive) {
|
|
351
|
-
const rows = await sql<DbRow[]>`${
|
|
353
|
+
const rows = await sql<DbRow[]>`${recursiveUserGroupsSubquery({
|
|
354
|
+
userId: params.id,
|
|
355
|
+
select: sql`
|
|
356
|
+
SELECT DISTINCT g.name FROM user_all_groups ag
|
|
357
|
+
JOIN auth.groups g ON g.id = ag.group_id
|
|
358
|
+
WHERE (${params.includePersonal !== false} OR NOT EXISTS (SELECT 1 FROM auth.user_posix p WHERE p.primary_group_id = g.id))
|
|
359
|
+
ORDER BY g.name
|
|
360
|
+
`,
|
|
361
|
+
})}`;
|
|
352
362
|
return rows.map((row) => row.name as string);
|
|
353
363
|
}
|
|
354
|
-
|
|
355
364
|
const rows = await sql<DbRow[]>`
|
|
356
|
-
SELECT g.name
|
|
357
|
-
FROM auth.user_groups_v2
|
|
358
|
-
|
|
359
|
-
WHERE ug.user_id = ${params.id}::uuid
|
|
365
|
+
SELECT g.name FROM auth.groups g
|
|
366
|
+
WHERE g.id IN (SELECT group_id FROM auth.user_groups_v2 WHERE user_id = ${params.id}::uuid)
|
|
367
|
+
AND (${params.includePersonal !== false} OR NOT EXISTS (SELECT 1 FROM auth.user_posix p WHERE p.primary_group_id = g.id))
|
|
360
368
|
ORDER BY g.name
|
|
361
369
|
`;
|
|
362
370
|
return rows.map((row) => row.name as string);
|