@k2b/cloud 0.43.0 → 0.45.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 +11 -2
- 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 +158 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +7 -5
- package/src/_internal/define-app.ts +57 -6
- 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/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +11 -1
- package/src/_internal/registry.ts +8 -1
- package/src/_internal/runtime-context.ts +8 -1
- package/src/access/AppAccessEditor.tsx +2 -0
- package/src/access/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +20 -5
- package/src/access/PrincipalAvatar.tsx +30 -21
- package/src/access/PrincipalPicker.tsx +14 -4
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-messages.ts +6 -0
- package/src/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -0
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +7 -0
- 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/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/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 +10 -6
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +40 -25
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +12 -5
- package/src/ai/inference-calls.ts +5 -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.ts +26 -9
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +58 -47
- package/src/ai/projects.ts +110 -88
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +55 -6
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider.ts +1 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +4 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +9 -3
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +47 -48
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- 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 +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +21 -44
- 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 +27 -22
- 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 +22 -3
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-skills.ts +4 -1
- 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/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 +35 -5
- package/src/api/capabilities.ts +28 -4
- package/src/api/index.ts +7 -1
- package/src/api/mcp.ts +175 -2
- package/src/api/me.ts +6 -4
- package/src/api/pwa-phone.ts +75 -26
- 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/app-pwa/contracts.ts +4 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +1 -0
- package/src/app-pwa/service.ts +91 -0
- package/src/app-pwa/transport.ts +22 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- 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/server-invoke.ts +2 -11
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- 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 +5 -2
- package/src/cli/apps.ts +89 -26
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +26 -2
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +21 -5
- package/src/contracts/capabilities.ts +143 -3
- package/src/contracts/index.ts +2 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/pwa.ts +17 -1
- package/src/contracts/registry.ts +6 -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 +6 -1
- 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 +1 -1
- package/src/server/api/respond.ts +17 -4
- package/src/server/index.ts +2 -0
- package/src/server/middleware/app-access.ts +1 -1
- package/src/server/middleware/auth.ts +21 -7
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +2 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +31 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +36 -13
- package/src/services/accounts/entities.ts +43 -19
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +19 -14
- package/src/services/accounts/identities.ts +70 -11
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +69 -4
- package/src/services/accounts/users.ts +45 -14
- package/src/services/app-access.ts +25 -7
- 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/ledger.ts +28 -10
- package/src/services/geo/service.ts +43 -24
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/invocation-authority.ts +4 -0
- package/src/services/identity/invocation-token.ts +238 -56
- package/src/services/identity/issue-invocation.ts +6 -1
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +13 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +20 -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/session/index.ts +26 -3
- package/src/services/settings/core-settings.ts +71 -43
- 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/email-html.ts +1 -0
- package/src/shared/index.ts +2 -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 +433 -65
- 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/text-diff.ts +75 -0
- package/src/shared-text/types.ts +24 -5
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +9 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +2 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +44 -7
- package/src/ssr/Layout.tsx +9 -7
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- package/src/ssr/PwaLayout.tsx +16 -1
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -31
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/index.ts +8 -0
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +11 -1
- package/src/ssr/profile-actions.ts +3 -6
- package/src/ssr/pwa-messages.ts +2 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +3 -1
- 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/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +134 -34
- 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
|
@@ -2,6 +2,7 @@ import type { Context, Env, MiddlewareHandler } from "hono";
|
|
|
2
2
|
import { createMiddleware } from "hono/factory";
|
|
3
3
|
import { matchedRoutes } from "hono/route";
|
|
4
4
|
import { findTargetHandler } from "hono/utils/handler";
|
|
5
|
+
import { bindAppAccessRequest } from "../../_internal/app-access-request";
|
|
5
6
|
import { measureServerPhase } from "../../_internal/server-timing";
|
|
6
7
|
import {
|
|
7
8
|
AI_INFERENCE_SCOPE,
|
|
@@ -58,6 +59,8 @@ export type ServiceAccountRequestActor =
|
|
|
58
59
|
delegatedUser: User;
|
|
59
60
|
scopes: string[];
|
|
60
61
|
credentialId?: string | null;
|
|
62
|
+
/** Public OAuth client identifier; safe to retain, never the bearer token. */
|
|
63
|
+
oauthClientId?: string | null;
|
|
61
64
|
credentialExpiresAt?: string | null;
|
|
62
65
|
delegation?: InvocationProvenance;
|
|
63
66
|
}
|
|
@@ -67,6 +70,8 @@ export type ServiceAccountRequestActor =
|
|
|
67
70
|
delegatedUser: null;
|
|
68
71
|
scopes: string[];
|
|
69
72
|
credentialId?: string | null;
|
|
73
|
+
/** Public OAuth client identifier; safe to retain, never the bearer token. */
|
|
74
|
+
oauthClientId?: string | null;
|
|
70
75
|
credentialExpiresAt?: string | null;
|
|
71
76
|
delegation?: InvocationProvenance;
|
|
72
77
|
};
|
|
@@ -248,6 +253,7 @@ const loadAuthenticatedActorUncached = async (
|
|
|
248
253
|
delegatedUser: authResult.delegatedUser,
|
|
249
254
|
scopes: authResult.scopes,
|
|
250
255
|
credentialId: null,
|
|
256
|
+
oauthClientId: authResult.oauthClientId,
|
|
251
257
|
credentialExpiresAt: typeof authResult.payload.exp === "number" ? new Date(authResult.payload.exp * 1_000).toISOString() : null,
|
|
252
258
|
};
|
|
253
259
|
c.set("actor", actor);
|
|
@@ -482,13 +488,21 @@ const getAuthority = <E extends AuthContext>(c: Context<E>): RequestAuthority =>
|
|
|
482
488
|
const accessSubject = c.get("accessSubject");
|
|
483
489
|
const credentialKind = c.get("credentialKind");
|
|
484
490
|
if (!actor || !accessSubject || !credentialKind) throw new Error("Request authority has not been resolved");
|
|
485
|
-
return
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
491
|
+
return bindAppAccessRequest(
|
|
492
|
+
{
|
|
493
|
+
actor,
|
|
494
|
+
accessSubject,
|
|
495
|
+
credentialKind,
|
|
496
|
+
scopes: [...(c.get("credentialScopes") ?? [])],
|
|
497
|
+
...(c.get("credentialProof") ? { credentialProof: c.get("credentialProof") } : {}),
|
|
498
|
+
},
|
|
499
|
+
c.req.raw,
|
|
500
|
+
credentialKind === "session"
|
|
501
|
+
? c.get("sessionToken")
|
|
502
|
+
: credentialKind !== "invocation"
|
|
503
|
+
? (session.getBearerToken(c) ?? undefined)
|
|
504
|
+
: undefined,
|
|
505
|
+
);
|
|
492
506
|
};
|
|
493
507
|
|
|
494
508
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Context, MiddlewareHandler } from "hono";
|
|
2
2
|
import { createMiddleware } from "hono/factory";
|
|
3
|
+
import { bindAppAccessRequest } from "../../_internal/app-access-request";
|
|
3
4
|
import { getApp } from "../../_internal/registry";
|
|
4
5
|
import type { AppMeta } from "../../contracts/app";
|
|
5
6
|
import { canUseApp } from "../../services/app-access";
|
|
@@ -53,6 +54,7 @@ export const requireInvocation = (
|
|
|
53
54
|
if (!claims) return reject(c);
|
|
54
55
|
const authority = await (dependencies.resolve ?? resolveInvocationAuthority)(claims);
|
|
55
56
|
if (!authority) return reject(c);
|
|
57
|
+
bindAppAccessRequest(authority, c.req.raw);
|
|
56
58
|
const app = await (dependencies.getApp ?? getApp)(expectation.targetAppId).catch(() => null);
|
|
57
59
|
if (!app || !(await canUseApp(authority, app))) {
|
|
58
60
|
if (app) recordAppAccessDenied(app.id);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { objectStoreBackend } from "../_internal/object-store-config";
|
|
2
|
+
import { validateObjectKey } from "../_internal/object-store-key";
|
|
3
|
+
import { get } from "../services/settings";
|
|
4
|
+
import { clientIpBucket } from "./client-ip";
|
|
5
|
+
import { ratelimit } from "./ratelimit";
|
|
6
|
+
|
|
7
|
+
const notFound = () => new Response(null, { status: 404, headers: { "Cache-Control": "no-store" } });
|
|
8
|
+
|
|
9
|
+
/** Platform-owned anonymous delivery. Never accepts a bucket, private prefix or listing. */
|
|
10
|
+
export const servePublicStorage = async (request: Request, clientAddress = "unknown"): Promise<Response> => {
|
|
11
|
+
let path: string;
|
|
12
|
+
try {
|
|
13
|
+
// Validate before application decoding; Bun has already resolved transport
|
|
14
|
+
// dot segments. The S3 key below always starts in the public area.
|
|
15
|
+
const raw = request.url.replace(/^[a-z]+:\/\/[^/]+/i, "").split(/[?#]/, 1)[0] ?? "";
|
|
16
|
+
if (/[\\]|%(?:2e|2f|5c)/i.test(raw)) return notFound();
|
|
17
|
+
const match = /^\/public\/storage\/([a-z][a-z0-9-]{0,79})\/(.+)$/.exec(raw);
|
|
18
|
+
if (!match || match[1] === "storage") return notFound();
|
|
19
|
+
const area = `public/${match[1]}/`;
|
|
20
|
+
path = area + validateObjectKey(decodeURIComponent(match[2] ?? ""), area);
|
|
21
|
+
} catch {
|
|
22
|
+
return notFound();
|
|
23
|
+
}
|
|
24
|
+
if (request.method !== "GET" && request.method !== "HEAD") return notFound();
|
|
25
|
+
try {
|
|
26
|
+
const perSecond = Math.max(1, Math.floor(await get<number>("security.rate_limit_per_second")));
|
|
27
|
+
const budget = await ratelimit({ id: "public-storage", limit: perSecond }).check(clientIpBucket(clientAddress));
|
|
28
|
+
if (budget.limited)
|
|
29
|
+
return new Response("Rate limit exceeded.", {
|
|
30
|
+
status: 429,
|
|
31
|
+
headers: { "Cache-Control": "no-store", "Retry-After": String(Math.max(1, Math.ceil(budget.resetIn / 1000))) },
|
|
32
|
+
});
|
|
33
|
+
const { client, limit } = await objectStoreBackend();
|
|
34
|
+
const stat = await client.stat(path);
|
|
35
|
+
if (stat.size > limit) return notFound();
|
|
36
|
+
const headers = new Headers({
|
|
37
|
+
"Content-Type": stat.type || "application/octet-stream",
|
|
38
|
+
"Content-Length": String(stat.size),
|
|
39
|
+
ETag: stat.etag,
|
|
40
|
+
"Last-Modified": stat.lastModified.toUTCString(),
|
|
41
|
+
"Cache-Control": "public, max-age=300",
|
|
42
|
+
"X-Content-Type-Options": "nosniff",
|
|
43
|
+
// Apply to every type, including mislabelled or future active formats.
|
|
44
|
+
"Content-Security-Policy": "sandbox; default-src 'none'; frame-ancestors 'none'",
|
|
45
|
+
});
|
|
46
|
+
const essence = stat.type.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
47
|
+
const inline =
|
|
48
|
+
/^(?:image\/(?:png|jpeg|gif|webp|avif|bmp|x-icon)|audio\/(?:mpeg|ogg|wav|webm|mp4)|video\/(?:mp4|webm|ogg)|text\/plain)$/;
|
|
49
|
+
if (!inline.test(essence)) headers.set("Content-Disposition", "attachment");
|
|
50
|
+
const ifNoneMatch = request.headers.get("if-none-match");
|
|
51
|
+
const modifiedSince = request.headers.get("if-modified-since");
|
|
52
|
+
if (
|
|
53
|
+
ifNoneMatch
|
|
54
|
+
? ifNoneMatch.split(",").some((value) => value.trim() === stat.etag || value.trim() === "*")
|
|
55
|
+
: modifiedSince && stat.lastModified.getTime() <= Date.parse(modifiedSince)
|
|
56
|
+
) {
|
|
57
|
+
headers.delete("Content-Length");
|
|
58
|
+
return new Response(null, { status: 304, headers });
|
|
59
|
+
}
|
|
60
|
+
if (request.method === "HEAD") return new Response(null, { headers });
|
|
61
|
+
const reader = client.file(path).stream().getReader();
|
|
62
|
+
let size = 0;
|
|
63
|
+
const abort = () => {
|
|
64
|
+
void reader.cancel(request.signal.reason).catch(() => {});
|
|
65
|
+
};
|
|
66
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
67
|
+
const release = () => {
|
|
68
|
+
request.signal.removeEventListener("abort", abort);
|
|
69
|
+
reader.releaseLock();
|
|
70
|
+
};
|
|
71
|
+
if (request.signal.aborted) abort();
|
|
72
|
+
const stream = new ReadableStream<Uint8Array>(
|
|
73
|
+
{
|
|
74
|
+
async pull(controller) {
|
|
75
|
+
try {
|
|
76
|
+
const next = await reader.read();
|
|
77
|
+
if (next.done) {
|
|
78
|
+
release();
|
|
79
|
+
controller.close();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
size += next.value.byteLength;
|
|
83
|
+
if (size > limit) throw new RangeError("Public object exceeds the configured size limit.");
|
|
84
|
+
controller.enqueue(next.value);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
await reader.cancel(error).catch(() => {});
|
|
87
|
+
release();
|
|
88
|
+
controller.error(error);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
async cancel(reason) {
|
|
92
|
+
try {
|
|
93
|
+
await reader.cancel(reason);
|
|
94
|
+
} finally {
|
|
95
|
+
release();
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{ highWaterMark: 0 },
|
|
100
|
+
);
|
|
101
|
+
return new Response(stream, { headers });
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error instanceof Error && "code" in error && ["NoSuchKey", "NotFound", "ENOENT"].includes(String(error.code))) return notFound();
|
|
104
|
+
return new Response("Storage is unavailable.", { status: 503, headers: { "Cache-Control": "no-store" } });
|
|
105
|
+
}
|
|
106
|
+
};
|
|
@@ -2,9 +2,10 @@ import { err, fail, ok, type Result } from "@k2b/stdlib";
|
|
|
2
2
|
import { type SQLQuery, sql } from "bun";
|
|
3
3
|
import { type ManagerCandidate, removesLastManager } from "../../access/managers";
|
|
4
4
|
import { accessMessages } from "../../access/messages";
|
|
5
|
-
import { recursiveGroupIdsSubquery } from "../../services/accounts/group-sql";
|
|
5
|
+
import { recursiveGroupIdsSubquery, recursiveServiceAccountGroupIdsSubquery } from "../../services/accounts/group-sql";
|
|
6
6
|
import { toPgUuidArray } from "../../services/postgres";
|
|
7
7
|
import type { ServiceAccountKind } from "../../services/service-accounts";
|
|
8
|
+
import type { RequestAuthority } from "../middleware/auth";
|
|
8
9
|
|
|
9
10
|
export type AccessDb = typeof sql;
|
|
10
11
|
|
|
@@ -265,19 +266,26 @@ export const ensureManagerRemains = (params: {
|
|
|
265
266
|
return fail({ code: "LAST_MANAGER", message: t.lastManager({ level: params.level ?? t.manage }), status: 409 });
|
|
266
267
|
};
|
|
267
268
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
type EffectiveGroupParams = { userId: string | null; serviceAccountId?: never } | { serviceAccountId: string | null; userId?: never };
|
|
270
|
+
|
|
271
|
+
/** Resolve authoritative memberships; machine recipients inherit local groups only. */
|
|
272
|
+
export const getEffectiveGroups = async (params: EffectiveGroupParams, db: AccessDb = sql): Promise<EffectiveGroup[]> => {
|
|
273
|
+
const groupIds = params.userId
|
|
274
|
+
? recursiveGroupIdsSubquery(params.userId)
|
|
275
|
+
: params.serviceAccountId
|
|
276
|
+
? recursiveServiceAccountGroupIdsSubquery(params.serviceAccountId)
|
|
277
|
+
: null;
|
|
278
|
+
if (!groupIds) return [];
|
|
271
279
|
const rows = await db<{ id: string; name: string }[]>`
|
|
272
280
|
SELECT g.id, g.name
|
|
273
281
|
FROM auth.groups g
|
|
274
|
-
WHERE g.id IN (${
|
|
282
|
+
WHERE g.id IN (${groupIds})
|
|
275
283
|
ORDER BY g.name
|
|
276
284
|
`;
|
|
277
285
|
return rows;
|
|
278
286
|
};
|
|
279
287
|
|
|
280
|
-
export const getEffectiveGroupIds = async (params:
|
|
288
|
+
export const getEffectiveGroupIds = async (params: EffectiveGroupParams, db: AccessDb = sql): Promise<string[]> =>
|
|
281
289
|
(await getEffectiveGroups(params, db)).map((group) => group.id);
|
|
282
290
|
|
|
283
291
|
/**
|
|
@@ -305,6 +313,13 @@ export const deleteAccess = async (params: { id: string }, db: AccessDb = sql):
|
|
|
305
313
|
* Each app implements this to connect their resources to auth.access.
|
|
306
314
|
*/
|
|
307
315
|
export type ResourceAccessAdapter<TResourceId = string> = {
|
|
316
|
+
/**
|
|
317
|
+
* Whether any resource in this adapter has an effective read/write/admin right.
|
|
318
|
+
* Use an existence query, never enumerate resources. Preserve domain eligibility,
|
|
319
|
+
* principal semantics, resource binding and credential ceilings. Use db and signal.
|
|
320
|
+
* Optional for existing adapters; required when selected by appAccess.resources().
|
|
321
|
+
*/
|
|
322
|
+
hasAnyRight?: (subject: AccessSubject, context: { authority: RequestAuthority; db: AccessDb; signal: AbortSignal }) => Promise<boolean>;
|
|
308
323
|
/** Get all access entries for a resource */
|
|
309
324
|
list: (resourceId: TResourceId) => Promise<AccessEntry[]>;
|
|
310
325
|
/** Add an access entry to a resource */
|
|
@@ -321,7 +336,7 @@ export type ResourceAccessAdapter<TResourceId = string> = {
|
|
|
321
336
|
*
|
|
322
337
|
* Apps provide column fragments because access rows are commonly joined under
|
|
323
338
|
* different aliases. The subject is authoritative: group membership is always
|
|
324
|
-
* resolved recursively from its user id and never accepted from a caller.
|
|
339
|
+
* resolved recursively from its user or service-account id and never accepted from a caller.
|
|
325
340
|
* Resource binding, credential scopes, and permission precedence remain the
|
|
326
341
|
* responsibility of the app using the predicate.
|
|
327
342
|
*/
|
|
@@ -338,8 +353,13 @@ export const buildAccessPrincipalTierConditions = (params: {
|
|
|
338
353
|
serviceAccount: sql`${columns.serviceAccountId} = ${serviceAccountId}::uuid`,
|
|
339
354
|
user: sql`${columns.userId} = ${userId}::uuid`,
|
|
340
355
|
group: sql`(
|
|
341
|
-
${
|
|
342
|
-
|
|
356
|
+
${columns.groupId} IN (${
|
|
357
|
+
userId
|
|
358
|
+
? recursiveGroupIdsSubquery(userId)
|
|
359
|
+
: serviceAccountId
|
|
360
|
+
? recursiveServiceAccountGroupIdsSubquery(serviceAccountId)
|
|
361
|
+
: sql`SELECT NULL::uuid WHERE FALSE`
|
|
362
|
+
})
|
|
343
363
|
)`,
|
|
344
364
|
authenticated: sql`${authenticated} AND ${columns.authenticatedOnly} = true`,
|
|
345
365
|
public: sql`(
|
|
@@ -361,7 +381,7 @@ export const buildAccessPrincipalCondition = (params: { subject: AccessSubject |
|
|
|
361
381
|
* Get the effective permission level for an actor on a resource.
|
|
362
382
|
* Returns the highest permission from:
|
|
363
383
|
* - Direct user or service-account access
|
|
364
|
-
* - Direct and nested
|
|
384
|
+
* - Direct and nested group memberships for users and active standalone/agent accounts
|
|
365
385
|
* - Authenticated access for every authenticated subject
|
|
366
386
|
* - Public access for every subject, including anonymous callers
|
|
367
387
|
*
|
|
@@ -457,6 +477,7 @@ export const getEffectivePermissions = async (params: {
|
|
|
457
477
|
a.service_account_id = s.service_account_id
|
|
458
478
|
OR a.user_id = s.user_id
|
|
459
479
|
OR (s.user_id IS NOT NULL AND a.group_id IN (${recursiveGroupIdsSubquery(sql`s.user_id`)}))
|
|
480
|
+
OR (s.service_account_id IS NOT NULL AND a.group_id IN (${recursiveServiceAccountGroupIdsSubquery(sql`s.service_account_id`)}))
|
|
460
481
|
OR a.authenticated_only = true
|
|
461
482
|
OR (a.user_id IS NULL AND a.group_id IS NULL AND a.service_account_id IS NULL AND a.authenticated_only = false)
|
|
462
483
|
)
|
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
EntityListItem,
|
|
9
9
|
GroupMember,
|
|
10
10
|
MutationResult,
|
|
11
|
+
ServiceAccountKind,
|
|
12
|
+
ServiceAccountStatus,
|
|
11
13
|
User,
|
|
12
14
|
UserProfile,
|
|
13
15
|
UserProvider,
|
|
@@ -413,7 +415,6 @@ export const accountsAppService = {
|
|
|
413
415
|
const userGroups = await users.getGroups({
|
|
414
416
|
id: config.userId,
|
|
415
417
|
recursive: config.recursive,
|
|
416
|
-
includePersonal: false,
|
|
417
418
|
});
|
|
418
419
|
const query = config.filter?.query?.trim().toLowerCase();
|
|
419
420
|
const filtered = query ? userGroups.filter((groupName) => groupName.toLowerCase().includes(query)) : userGroups;
|
|
@@ -968,7 +969,7 @@ export const accountsAppService = {
|
|
|
968
969
|
group: {
|
|
969
970
|
list: async (config: {
|
|
970
971
|
pagination?: PageParams;
|
|
971
|
-
filter?: { search?: string
|
|
972
|
+
filter?: { search?: string };
|
|
972
973
|
scope?: { userId?: string; ids?: string[]; provider?: UserProvider; mode?: "all" | "member" | "managed" };
|
|
973
974
|
}): Promise<Paginated<BaseGroup>> => {
|
|
974
975
|
const { page, perPage } = paginate(config.pagination);
|
|
@@ -980,7 +981,6 @@ export const accountsAppService = {
|
|
|
980
981
|
scope: config.scope?.mode,
|
|
981
982
|
ids: config.scope?.ids,
|
|
982
983
|
provider: config.scope?.provider,
|
|
983
|
-
includePersonal: config.filter?.includePersonal,
|
|
984
984
|
});
|
|
985
985
|
|
|
986
986
|
return {
|
|
@@ -991,7 +991,10 @@ export const accountsAppService = {
|
|
|
991
991
|
hasNext: result.pagination.hasNext,
|
|
992
992
|
};
|
|
993
993
|
},
|
|
994
|
-
get: async (config: { id: string }) =>
|
|
994
|
+
get: async (config: { id: string }) => {
|
|
995
|
+
const result = await groups.list({ ids: [config.id], perPage: 1 });
|
|
996
|
+
return result.groups[0] ?? null;
|
|
997
|
+
},
|
|
995
998
|
member: {
|
|
996
999
|
list: async (config: {
|
|
997
1000
|
id: string;
|
|
@@ -1014,7 +1017,14 @@ export const accountsAppService = {
|
|
|
1014
1017
|
});
|
|
1015
1018
|
return paginateItems(filtered, config.pagination);
|
|
1016
1019
|
},
|
|
1017
|
-
add: async (config: {
|
|
1020
|
+
add: async (config: {
|
|
1021
|
+
actor: AccountsActor;
|
|
1022
|
+
id: string;
|
|
1023
|
+
provider?: UserProvider;
|
|
1024
|
+
userId?: string;
|
|
1025
|
+
groupId?: string;
|
|
1026
|
+
serviceAccountId?: string;
|
|
1027
|
+
}) => {
|
|
1018
1028
|
const group = await groups.get({ id: config.id });
|
|
1019
1029
|
if (!group) {
|
|
1020
1030
|
const result = fail(err.notFound("Group not found"));
|
|
@@ -1033,17 +1043,25 @@ export const accountsAppService = {
|
|
|
1033
1043
|
provider: config.provider,
|
|
1034
1044
|
user: config.userId,
|
|
1035
1045
|
group: config.groupId,
|
|
1046
|
+
serviceAccount: config.serviceAccountId,
|
|
1036
1047
|
}),
|
|
1037
1048
|
);
|
|
1038
1049
|
return recordCompletedMutation({
|
|
1039
1050
|
action: "accounts.group.member.add",
|
|
1040
1051
|
actor: auditActor(config.actor),
|
|
1041
1052
|
target: groupTarget(group),
|
|
1042
|
-
metadata: { userId: config.userId ?? null, groupId: config.groupId ?? null },
|
|
1053
|
+
metadata: { userId: config.userId ?? null, groupId: config.groupId ?? null, serviceAccountId: config.serviceAccountId ?? null },
|
|
1043
1054
|
result,
|
|
1044
1055
|
});
|
|
1045
1056
|
},
|
|
1046
|
-
remove: async (config: {
|
|
1057
|
+
remove: async (config: {
|
|
1058
|
+
actor: AccountsActor;
|
|
1059
|
+
id: string;
|
|
1060
|
+
provider?: UserProvider;
|
|
1061
|
+
userId?: string;
|
|
1062
|
+
groupId?: string;
|
|
1063
|
+
serviceAccountId?: string;
|
|
1064
|
+
}) => {
|
|
1047
1065
|
const group = await groups.get({ id: config.id });
|
|
1048
1066
|
if (!group) {
|
|
1049
1067
|
const result = fail(err.notFound("Group not found"));
|
|
@@ -1062,13 +1080,14 @@ export const accountsAppService = {
|
|
|
1062
1080
|
provider: config.provider,
|
|
1063
1081
|
user: config.userId,
|
|
1064
1082
|
group: config.groupId,
|
|
1083
|
+
serviceAccount: config.serviceAccountId,
|
|
1065
1084
|
}),
|
|
1066
1085
|
);
|
|
1067
1086
|
return recordCompletedMutation({
|
|
1068
1087
|
action: "accounts.group.member.remove",
|
|
1069
1088
|
actor: auditActor(config.actor),
|
|
1070
1089
|
target: groupTarget(group),
|
|
1071
|
-
metadata: { userId: config.userId ?? null, groupId: config.groupId ?? null },
|
|
1090
|
+
metadata: { userId: config.userId ?? null, groupId: config.groupId ?? null, serviceAccountId: config.serviceAccountId ?? null },
|
|
1072
1091
|
result,
|
|
1073
1092
|
});
|
|
1074
1093
|
},
|
|
@@ -1253,6 +1272,9 @@ export const accountsAppService = {
|
|
|
1253
1272
|
profile?: UserProfile;
|
|
1254
1273
|
userIds?: string[];
|
|
1255
1274
|
groupIds?: string[];
|
|
1275
|
+
serviceAccountIds?: string[];
|
|
1276
|
+
serviceAccountKinds?: ServiceAccountKind[];
|
|
1277
|
+
serviceAccountStatus?: ServiceAccountStatus;
|
|
1256
1278
|
excludeUserIds?: string[];
|
|
1257
1279
|
excludeGroupIds?: string[];
|
|
1258
1280
|
excludeServiceAccountIds?: string[];
|
|
@@ -1262,7 +1284,6 @@ export const accountsAppService = {
|
|
|
1262
1284
|
parentGroupId?: string;
|
|
1263
1285
|
managedByUserId?: string;
|
|
1264
1286
|
recursive?: boolean;
|
|
1265
|
-
includePersonal?: boolean;
|
|
1266
1287
|
}): Promise<Paginated<EntityListItem>> => {
|
|
1267
1288
|
const canSearchDirectory = config.actor.roles.includes("user");
|
|
1268
1289
|
const usesRelationFilter = Boolean(
|
|
@@ -1284,6 +1305,9 @@ export const accountsAppService = {
|
|
|
1284
1305
|
profile: config.profile,
|
|
1285
1306
|
userIds: config.userIds,
|
|
1286
1307
|
groupIds: config.groupIds,
|
|
1308
|
+
serviceAccountIds: config.serviceAccountIds,
|
|
1309
|
+
serviceAccountKinds: config.serviceAccountKinds,
|
|
1310
|
+
serviceAccountStatus: config.serviceAccountStatus,
|
|
1287
1311
|
excludeUserIds: config.excludeUserIds,
|
|
1288
1312
|
excludeGroupIds: config.excludeGroupIds,
|
|
1289
1313
|
excludeServiceAccountIds: config.excludeServiceAccountIds,
|
|
@@ -1293,7 +1317,6 @@ export const accountsAppService = {
|
|
|
1293
1317
|
parentGroupId: config.parentGroupId,
|
|
1294
1318
|
managedByUserId: config.managedByUserId,
|
|
1295
1319
|
recursive: config.recursive,
|
|
1296
|
-
includePersonal: config.includePersonal,
|
|
1297
1320
|
page,
|
|
1298
1321
|
perPage,
|
|
1299
1322
|
});
|
|
@@ -1655,9 +1678,9 @@ export const accountsAppService = {
|
|
|
1655
1678
|
(SELECT COUNT(*)::int FROM auth.users WHERE provider = 'local') AS local_accounts_total,
|
|
1656
1679
|
(SELECT COUNT(*)::int FROM auth.users WHERE provider = 'local' AND profile = 'user') AS local_user_accounts_total,
|
|
1657
1680
|
(SELECT COUNT(*)::int FROM auth.users WHERE provider = 'local' AND profile = 'guest') AS local_guest_accounts_total,
|
|
1658
|
-
(SELECT COUNT(*)::int FROM auth.groups g WHERE NOT EXISTS (SELECT 1 FROM auth.
|
|
1659
|
-
(SELECT COUNT(*)::int FROM auth.groups g WHERE provider = 'ipa' AND NOT EXISTS (SELECT 1 FROM auth.
|
|
1660
|
-
(SELECT COUNT(*)::int FROM auth.groups g WHERE provider = 'local' AND NOT EXISTS (SELECT 1 FROM auth.
|
|
1681
|
+
(SELECT COUNT(*)::int FROM auth.groups g WHERE NOT EXISTS (SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id)) AS groups_total,
|
|
1682
|
+
(SELECT COUNT(*)::int FROM auth.groups g WHERE provider = 'ipa' AND NOT EXISTS (SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id)) AS ipa_groups_total,
|
|
1683
|
+
(SELECT COUNT(*)::int FROM auth.groups g WHERE provider = 'local' AND NOT EXISTS (SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id)) AS local_groups_total,
|
|
1661
1684
|
(SELECT COUNT(*)::int FROM auth.account_requests WHERE status = 'pending') AS open_requests,
|
|
1662
1685
|
(
|
|
1663
1686
|
SELECT COUNT(*)::int
|
|
@@ -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
|
-
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
|
-
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,9 @@ export type EntityListParams = {
|
|
|
21
21
|
profile?: UserProfile;
|
|
22
22
|
userIds?: string[];
|
|
23
23
|
groupIds?: string[];
|
|
24
|
+
serviceAccountIds?: string[];
|
|
25
|
+
serviceAccountKinds?: ServiceAccountKind[];
|
|
26
|
+
serviceAccountStatus?: ServiceAccountStatus;
|
|
24
27
|
excludeUserIds?: string[];
|
|
25
28
|
excludeGroupIds?: string[];
|
|
26
29
|
excludeServiceAccountIds?: string[];
|
|
@@ -30,11 +33,6 @@ export type EntityListParams = {
|
|
|
30
33
|
parentGroupId?: string;
|
|
31
34
|
managedByUserId?: string;
|
|
32
35
|
recursive?: boolean;
|
|
33
|
-
/**
|
|
34
|
-
* Personal Linux groups are left out of directory browsing and search by
|
|
35
|
-
* default. Exact `groupIds` lookups and relation filters always return them.
|
|
36
|
-
*/
|
|
37
|
-
includePersonal?: boolean;
|
|
38
36
|
page?: number;
|
|
39
37
|
perPage?: number;
|
|
40
38
|
};
|
|
@@ -427,11 +425,7 @@ export const list = async (
|
|
|
427
425
|
const groupsAdmin = (await getFreeIpaConfig()).groupsAdmin;
|
|
428
426
|
const groupsAdminLiteral = toPgTextArray(groupsAdmin);
|
|
429
427
|
const spec = buildQuerySpec(params);
|
|
430
|
-
const
|
|
431
|
-
params.includePersonal === true ||
|
|
432
|
-
params.groupIds !== undefined ||
|
|
433
|
-
Boolean(params.memberOfGroupId || params.managerOfGroupId || params.parentGroupId || params.managedByUserId);
|
|
434
|
-
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))`;
|
|
435
429
|
const visibilityCondition =
|
|
436
430
|
params.visibility.type === "directory"
|
|
437
431
|
? sql`TRUE`
|
|
@@ -453,12 +447,13 @@ export const list = async (
|
|
|
453
447
|
)
|
|
454
448
|
)`;
|
|
455
449
|
const kindsCondition = (params.kinds?.length ?? 0) === 0 ? sql`TRUE` : sql`kind = ANY(${toPgTextArray(params.kinds ?? [])}::text[])`;
|
|
456
|
-
const hasIdFilter = params.userIds !== undefined || params.groupIds !== undefined;
|
|
450
|
+
const hasIdFilter = params.userIds !== undefined || params.groupIds !== undefined || params.serviceAccountIds !== undefined;
|
|
457
451
|
const includeIdCondition = !hasIdFilter
|
|
458
452
|
? sql`TRUE`
|
|
459
453
|
: sql`(
|
|
460
454
|
(kind = 'user' AND id = ANY(${toPgUuidArray(params.userIds ?? [])}::uuid[]))
|
|
461
455
|
OR (kind = 'group' AND id = ANY(${toPgUuidArray(params.groupIds ?? [])}::uuid[]))
|
|
456
|
+
OR (kind = 'service_account' AND id = ANY(${toPgUuidArray(params.serviceAccountIds ?? [])}::uuid[]))
|
|
462
457
|
)`;
|
|
463
458
|
const excludeUserCondition =
|
|
464
459
|
(params.excludeUserIds?.length ?? 0) === 0
|
|
@@ -472,6 +467,13 @@ export const list = async (
|
|
|
472
467
|
(params.excludeServiceAccountIds?.length ?? 0) === 0
|
|
473
468
|
? sql`TRUE`
|
|
474
469
|
: sql`(kind <> 'service_account' OR id <> ALL(${toPgUuidArray(params.excludeServiceAccountIds ?? [])}::uuid[]))`;
|
|
470
|
+
const serviceAccountKindsCondition =
|
|
471
|
+
(params.serviceAccountKinds?.length ?? 0) === 0
|
|
472
|
+
? sql`TRUE`
|
|
473
|
+
: sql`(kind <> 'service_account' OR service_account_kind = ANY(${toPgTextArray(params.serviceAccountKinds ?? [])}::text[]))`;
|
|
474
|
+
const serviceAccountStatusCondition = params.serviceAccountStatus
|
|
475
|
+
? sql`(kind <> 'service_account' OR status = ${params.serviceAccountStatus})`
|
|
476
|
+
: sql`TRUE`;
|
|
475
477
|
const userMemberOfGroupCondition =
|
|
476
478
|
(params.userMemberOfGroupIds?.length ?? 0) === 0
|
|
477
479
|
? sql`TRUE`
|
|
@@ -482,15 +484,36 @@ export const list = async (
|
|
|
482
484
|
AND ug.group_id = ANY(${toPgUuidArray(params.userMemberOfGroupIds ?? [])}::uuid[])
|
|
483
485
|
))`;
|
|
484
486
|
|
|
487
|
+
// Machine recipients have local membership, but no directory management/parent relation.
|
|
488
|
+
const otherRelation = Boolean(params.managerOfGroupId || 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
|
+
: sql`TRUE`;
|
|
501
|
+
const machineDirect = params.memberOfGroupId
|
|
502
|
+
? sql`EXISTS (SELECT 1 FROM auth.service_account_groups sg
|
|
503
|
+
WHERE sg.service_account_id = sa.id AND sg.group_id = ${params.memberOfGroupId}::uuid)`
|
|
504
|
+
: sql`NULL::boolean`;
|
|
485
505
|
const where = sql`
|
|
486
506
|
${visibilityCondition}
|
|
487
507
|
AND ${kindsCondition}
|
|
488
508
|
AND ${includeIdCondition}
|
|
489
|
-
AND (${params.provider ?? null}::text IS NULL OR provider = ${params.provider ?? null}
|
|
509
|
+
AND (${params.provider ?? null}::text IS NULL OR provider = ${params.provider ?? null}
|
|
510
|
+
OR (kind = 'service_account' AND ${params.provider ?? null}::text = 'local'))
|
|
490
511
|
AND (${params.profile ?? null}::text IS NULL OR kind = 'group' OR profile = ${params.profile ?? null})
|
|
491
512
|
AND ${excludeUserCondition}
|
|
492
513
|
AND ${excludeGroupCondition}
|
|
493
514
|
AND ${excludeServiceAccountCondition}
|
|
515
|
+
AND ${serviceAccountKindsCondition}
|
|
516
|
+
AND ${serviceAccountStatusCondition}
|
|
494
517
|
AND ${userMemberOfGroupCondition}
|
|
495
518
|
AND ${personalCondition}
|
|
496
519
|
AND (
|
|
@@ -601,7 +624,7 @@ export const list = async (
|
|
|
601
624
|
service_account_rows AS (
|
|
602
625
|
SELECT
|
|
603
626
|
'service_account'::text AS kind,
|
|
604
|
-
|
|
627
|
+
${machineDirect} AS direct,
|
|
605
628
|
sa.id,
|
|
606
629
|
NULL::text AS provider,
|
|
607
630
|
NULL::text AS profile,
|
|
@@ -633,6 +656,7 @@ export const list = async (
|
|
|
633
656
|
NULL::text AS personal_owner_display_name,
|
|
634
657
|
LOWER(sa.name) AS sort_label
|
|
635
658
|
FROM auth.service_accounts sa
|
|
659
|
+
WHERE NOT ${otherRelation} AND ${machineRelation}
|
|
636
660
|
),
|
|
637
661
|
entity_rows AS (
|
|
638
662
|
SELECT * FROM user_rows
|
|
@@ -649,9 +673,9 @@ export const list = async (
|
|
|
649
673
|
OFFSET ${offset}
|
|
650
674
|
`;
|
|
651
675
|
|
|
676
|
+
// A page past the end has no rows to carry the window count; the total still counts every match.
|
|
652
677
|
let total = rows.length > 0 ? Number(rows[0]!.total) : 0;
|
|
653
|
-
if (
|
|
654
|
-
total = (await list({ ...params, page: 1, perPage: 1 })).total;
|
|
678
|
+
if (!rows.length && page > 1) total = (await list({ ...params, page: 1, perPage: 1 })).total;
|
|
655
679
|
return {
|
|
656
680
|
items: rows.map((row) => {
|
|
657
681
|
const entity = mapEntityRow(row);
|
|
@@ -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
|
+
`;
|