Lightweight replacement for the upstream La Suite Numérique drive (Django/Celery/Next.js) built as a single Deno binary. Server (Deno + Hono): - S3 file operations via AWS SigV4 (no SDK) with pre-signed URLs - WOPI host for Collabora Online (CheckFileInfo, GetFile, PutFile, locks) - Ory Kratos session auth + CSRF protection - Ory Keto permission model (OPL namespaces, not yet wired to routes) - PostgreSQL metadata with recursive folder sizes - S3 backfill API for registering files uploaded outside the UI - OpenTelemetry tracing + metrics (opt-in via OTEL_ENABLED) Frontend (React 19 + Cunningham v4 + react-aria): - File browser with GridList, keyboard nav, multi-select - Collabora editor iframe (full-screen, form POST, postMessage) - Profile menu, waffle menu, drag-drop upload, asset type badges - La Suite integration service theming (runtime CSS) Testing (549 tests): - 235 server unit tests (Deno) — 90%+ coverage - 278 UI unit tests (Vitest) — 90%+ coverage - 11 E2E tests (Playwright) - 12 integration service tests (Playwright) - 13 WOPI integration tests (Playwright + Docker Compose + Collabora) MIT licensed.
121 lines
3.5 KiB
TypeScript
121 lines
3.5 KiB
TypeScript
/**
|
|
* Ory Keto OPL (Ory Permission Language) namespace definitions.
|
|
*
|
|
* This file defines the permission model deployed to Keto.
|
|
* It is NOT executed by Deno — it uses Keto's OPL type system
|
|
* (Namespace, Context, SubjectSet) and is consumed by the Keto
|
|
* server at deploy time.
|
|
*
|
|
* Hierarchy: Bucket → Folder → File
|
|
* Permissions cascade downward through `parents` relations.
|
|
*/
|
|
|
|
// deno-lint-ignore-file no-unused-vars
|
|
|
|
/* ── Namespace / Context / SubjectSet are Keto OPL built-ins ── */
|
|
/* They are declared here so the file type-checks as valid TS. */
|
|
|
|
interface Namespace {
|
|
related?: Record<string, unknown[]>;
|
|
permits?: Record<string, (ctx: Context) => boolean>;
|
|
}
|
|
|
|
interface Context {
|
|
subject: unknown;
|
|
}
|
|
|
|
type SubjectSet<N, R extends string> = unknown;
|
|
|
|
// ─── Namespaces ──────────────────────────────────────────────────────────────
|
|
|
|
class User implements Namespace {}
|
|
|
|
class Group implements Namespace {
|
|
related: {
|
|
members: (User | Group)[];
|
|
};
|
|
}
|
|
|
|
class Bucket implements Namespace {
|
|
related: {
|
|
owners: User[];
|
|
editors: (User | SubjectSet<Group, "members">)[];
|
|
viewers: (User | SubjectSet<Group, "members">)[];
|
|
};
|
|
|
|
permits = {
|
|
write: (ctx: Context): boolean =>
|
|
this.related.owners.includes(ctx.subject) ||
|
|
this.related.editors.includes(ctx.subject),
|
|
|
|
read: (ctx: Context): boolean =>
|
|
this.permits.write(ctx) ||
|
|
this.related.viewers.includes(ctx.subject),
|
|
|
|
delete: (ctx: Context): boolean =>
|
|
this.related.owners.includes(ctx.subject),
|
|
};
|
|
}
|
|
|
|
class Folder implements Namespace {
|
|
related: {
|
|
owners: User[];
|
|
editors: (User | SubjectSet<Group, "members">)[];
|
|
viewers: (User | SubjectSet<Group, "members">)[];
|
|
parents: (Folder | Bucket)[];
|
|
};
|
|
|
|
permits = {
|
|
write: (ctx: Context): boolean =>
|
|
this.related.owners.includes(ctx.subject) ||
|
|
this.related.editors.includes(ctx.subject) ||
|
|
// @ts-ignore — Keto OPL traverse
|
|
this.related.parents.traverse((p: Folder | Bucket) =>
|
|
p.permits.write(ctx),
|
|
),
|
|
|
|
read: (ctx: Context): boolean =>
|
|
this.permits.write(ctx) ||
|
|
this.related.viewers.includes(ctx.subject) ||
|
|
// @ts-ignore — Keto OPL traverse
|
|
this.related.parents.traverse((p: Folder | Bucket) =>
|
|
p.permits.read(ctx),
|
|
),
|
|
|
|
delete: (ctx: Context): boolean =>
|
|
this.related.owners.includes(ctx.subject) ||
|
|
// @ts-ignore — Keto OPL traverse
|
|
this.related.parents.traverse((p: Folder | Bucket) =>
|
|
p.permits.delete(ctx),
|
|
),
|
|
};
|
|
}
|
|
|
|
class File implements Namespace {
|
|
related: {
|
|
owners: User[];
|
|
editors: (User | SubjectSet<Group, "members">)[];
|
|
viewers: (User | SubjectSet<Group, "members">)[];
|
|
parents: Folder[];
|
|
};
|
|
|
|
permits = {
|
|
write: (ctx: Context): boolean =>
|
|
this.related.owners.includes(ctx.subject) ||
|
|
this.related.editors.includes(ctx.subject) ||
|
|
// @ts-ignore — Keto OPL traverse
|
|
this.related.parents.traverse((p: Folder) => p.permits.write(ctx)),
|
|
|
|
read: (ctx: Context): boolean =>
|
|
this.permits.write(ctx) ||
|
|
this.related.viewers.includes(ctx.subject) ||
|
|
// @ts-ignore — Keto OPL traverse
|
|
this.related.parents.traverse((p: Folder) => p.permits.read(ctx)),
|
|
|
|
delete: (ctx: Context): boolean =>
|
|
this.related.owners.includes(ctx.subject) ||
|
|
// @ts-ignore — Keto OPL traverse
|
|
this.related.parents.traverse((p: Folder) => p.permits.delete(ctx)),
|
|
};
|
|
}
|