Initial commit — Drive, an S3 file browser with WOPI editing
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.
This commit is contained in:
120
keto/namespaces.ts
Normal file
120
keto/namespaces.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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)),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user