Files
0pt/src/document-workbench/project-access.ts
T
admin e3c4bc28c3
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 6s
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / security (push) Successful in 14s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 19s
CI (SIAX Cloud) / security (pull_request) Successful in 21s
Security Scan (SIAX Cloud) / security (push) Successful in 25s
CI (SIAX Cloud) / integration (pull_request) Successful in 29s
CI (SIAX Cloud) / quality (pull_request) Successful in 51s
CI (SIAX Cloud) / quality (push) Successful in 1m29s
feat(workbench): live LiteLLM gateway adapter + explicit project grants
- createInf0Gateway prefers the live OpenAI-compatible LiteLLM gateway and
  falls back to the native INF0 surface; admission stays local and fail-closed.
- Project authorization: OpenFGA when configured, otherwise a validated
  server-owned OPT_PROJECT_GRANTS list (deny by default).
- Tests for the gateway adapter, the factory and grants.
2026-11-02 14:50:50 +01:00

108 lines
3.4 KiB
TypeScript

/**
* 0pt project authorization for the document workbench.
*
* Two sources, in order:
*
* 1. OpenFGA via the universal `@siax/access` primitive (the same one DOC0
* uses) when `OPENFGA_API_URL/STORE_ID/MODEL_ID/AUTH_TOKEN` are set.
* 2. An explicit, server-owned grant list (`OPT_PROJECT_GRANTS`), a
* transitional source for environments where the central relationship
* store is not deployed yet.
*
* FAIL CLOSED: when neither source is configured, or the source is
* unavailable, the answer is "no". A grant is never derived from a request
* header or body.
*
* `OPT_PROJECT_GRANTS` shape (JSON array):
* [{"tenantId":"379976258701823235","projectId":"prj_alpha","actorId":"*"}]
* `actorId: "*"` grants every verified actor in the tenant; otherwise the exact
* actor must match.
*/
import { OpenFgaRelationships, isProjectAccessAllowed } from '@siax/access';
import { readDocumentWorkbenchRuntime } from './runtime-config';
export interface ProjectScope {
tenantId: string;
projectId: string;
actorId: string;
}
export type ProjectAuthorizer = (scope: ProjectScope) => Promise<boolean>;
interface ProjectGrant {
tenantId: string;
projectId: string;
actorId: string;
}
const scopeId = /^[\x21-\x7e]{1,256}$/;
export function parseProjectGrants(raw: string | undefined): ProjectGrant[] | undefined {
if (!raw || !raw.trim()) return undefined;
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return undefined;
}
if (!Array.isArray(parsed) || parsed.length === 0 || parsed.length > 1000) {
return undefined;
}
const grants: ProjectGrant[] = [];
for (const entry of parsed) {
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return undefined;
const grant = entry as Record<string, unknown>;
if (
typeof grant.tenantId !== 'string' ||
!scopeId.test(grant.tenantId) ||
typeof grant.projectId !== 'string' ||
!scopeId.test(grant.projectId) ||
typeof grant.actorId !== 'string' ||
(grant.actorId !== '*' && !scopeId.test(grant.actorId))
) {
return undefined;
}
grants.push({
tenantId: grant.tenantId,
projectId: grant.projectId,
actorId: grant.actorId,
});
}
return grants;
}
export function createProjectAuthorizer(env: NodeJS.ProcessEnv = process.env): ProjectAuthorizer {
const runtime = readDocumentWorkbenchRuntime(env);
let relationships: OpenFgaRelationships | undefined;
if (runtime.openfga) {
try {
relationships = new OpenFgaRelationships(runtime.openfga);
} catch {
relationships = undefined;
}
}
const grants = parseProjectGrants(env.OPT_PROJECT_GRANTS);
return async (scope) => {
if (!scopeId.test(scope.tenantId) || !scopeId.test(scope.projectId) || !scopeId.test(scope.actorId)) {
return false;
}
if (relationships) {
return isProjectAccessAllowed(scope, { relationships });
}
if (!grants) return false;
return grants.some(
(grant) =>
grant.tenantId === scope.tenantId &&
grant.projectId === scope.projectId &&
(grant.actorId === '*' || grant.actorId === scope.actorId),
);
};
}
/** True when the server has enough configuration to authorize at all. */
export function projectAuthorizationConfigured(env: NodeJS.ProcessEnv = process.env): boolean {
if (readDocumentWorkbenchRuntime(env).openfga) return true;
return Boolean(parseProjectGrants(env.OPT_PROJECT_GRANTS));
}