Initial Commit
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
# `sharing/grants/grants.ts`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`GrantManager` is the deterministic in-memory grant store. It holds `Grant` records keyed by `GrantId` and answers permission queries via `check(grantId, userId, action)`. The check is exhaustive: existence, non-revocation, non-expiry, grantee identity, and permission inclusion are all verified, in that order, with the first failure short-circuiting the result. Every check increments `share.grant.check_total`; denials additionally increment `share.grant.denied_total` with a `reason` attribute so dashboards can split denials by cause.
|
||||
|
||||
## Features
|
||||
|
||||
- Synchronous `check` — sub-millisecond by design (no IO, no crypto).
|
||||
- Injectable `Clock` so expiry tests are deterministic.
|
||||
- `EventBusLike` integration: `grant:created` and `grant:revoked` events carry full context but never key material.
|
||||
- Hard cap on active grants (`GRANT_MANAGER_MAX_ENTRIES = 10_000_000`) per the spec scalability table.
|
||||
- Filtered `list(query)` supporting `granteeId`, `partitionId`, and `active` filters.
|
||||
- `revoke` is idempotent — already-revoked / missing grants are no-ops.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { GrantManager } from "@suaveplan/drive-sharing/sharing/grants";
|
||||
import { partitionId, userId } from "@suaveplan/drive-sharing/types/common";
|
||||
|
||||
const grants = new GrantManager();
|
||||
const grant = grants.create({
|
||||
grantorId: userId("alice"),
|
||||
granteeId: userId("bob"),
|
||||
partitionId: partitionId("p"),
|
||||
permissions: ["read"],
|
||||
});
|
||||
grants.check(grant.id, userId("bob"), "read"); // { allowed: true }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { GrantManager } from "@suaveplan/drive-sharing/sharing/grants";
|
||||
import { partitionId, userId } from "@suaveplan/drive-sharing/types/common";
|
||||
|
||||
let now = 0;
|
||||
const events = new Map<string, unknown[]>();
|
||||
const grants = new GrantManager({
|
||||
clock: () => now,
|
||||
events: {
|
||||
on: () => () => {},
|
||||
emit: (k, p) => {
|
||||
const arr = events.get(k) ?? [];
|
||||
arr.push(p);
|
||||
events.set(k, arr);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const g = grants.create({
|
||||
grantorId: userId("alice"),
|
||||
granteeId: userId("bob"),
|
||||
partitionId: partitionId("p"),
|
||||
permissions: ["read"],
|
||||
expiresAt: 1000,
|
||||
});
|
||||
|
||||
now = 999;
|
||||
console.log(grants.check(g.id, userId("bob"), "read")); // allowed
|
||||
now = 1001;
|
||||
console.log(grants.check(g.id, userId("bob"), "read")); // denied: expired
|
||||
|
||||
grants.revoke(g.id);
|
||||
console.log(grants.list({ active: true })); // []
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class GrantManager`
|
||||
|
||||
- `constructor(config?: GrantManagerConfig)`
|
||||
- `create(options: CreateGrantOptions): Grant`
|
||||
- `check(id, userId, action): GrantCheckResult`
|
||||
- `revoke(id): void`
|
||||
- `list(query?): Grant[]`
|
||||
- `get(id): Grant | undefined`
|
||||
|
||||
### `interface CreateGrantOptions`
|
||||
|
||||
`{ grantorId, granteeId, partitionId, permissions, expiresAt? }`.
|
||||
|
||||
### `interface Grant`
|
||||
|
||||
`{ id, grantorId, granteeId, partitionId, permissions, createdAt, expiresAt?, revoked }`.
|
||||
|
||||
### `interface GrantCheckResult`
|
||||
|
||||
`{ allowed: boolean; reason?: string }` — `reason` is one of `grant_not_found`, `revoked`, `expired`, `grantee_mismatch`, `permission_missing`.
|
||||
|
||||
### `interface GrantQuery`
|
||||
|
||||
`{ granteeId?, partitionId?, active? }`.
|
||||
|
||||
### `interface GrantManagerEvents`
|
||||
|
||||
- `grant:created` — full grant
|
||||
- `grant:revoked` — `{ id, partitionId, userId }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The denial-reason strings are stable contract — dashboards may key on them. Renaming a reason is a major-version bump.
|
||||
- `revoke` is intentionally idempotent so a duplicate "revoke this grant" message from a retry-loop does not trigger a second `grant:revoked` event.
|
||||
- `list({ active: true })` reads the clock once per grant; for very large grant stores callers should prefer `list({ partitionId, active: true })` so the filter narrows before the active-check fires.
|
||||
- The Zod schema (`CreateGrantOptionsSchema`) enforces non-empty `permissions`. An attacker who calls `create` with `permissions: []` is rejected at the boundary, never silently inserted.
|
||||
- `check` runs the action verb through `permissions.includes(action as SharePermission)` — the cast is safe because every `PolicyAction` is also a valid `SharePermission` in the canonical list.
|
||||
Reference in New Issue
Block a user