103 lines
2.4 KiB
Markdown
103 lines
2.4 KiB
Markdown
# Access Control
|
|
|
|
Operations declare their access requirements in `accessControl`. The registry and call handler enforce these before executing the handler.
|
|
|
|
## AccessControl Fields
|
|
|
|
```ts
|
|
interface AccessControl {
|
|
requiredScopes: string[]; // ALL must be present (AND)
|
|
requiredScopesAny?: string[]; // At least ONE must match (OR)
|
|
resourceType?: string; // e.g., "project", "tool"
|
|
resourceAction?: string; // e.g., "read", "write", "execute"
|
|
}
|
|
```
|
|
|
|
### requiredScopes (AND)
|
|
|
|
Every scope in the array must be present in the caller's identity:
|
|
|
|
```ts
|
|
accessControl: {
|
|
requiredScopes: ["task:read", "task:write"],
|
|
}
|
|
```
|
|
|
|
The caller must have **both** `task:read` and `task:write`.
|
|
|
|
### requiredScopesAny (OR)
|
|
|
|
At least one scope must match:
|
|
|
|
```ts
|
|
accessControl: {
|
|
requiredScopes: ["admin"],
|
|
requiredScopesAny: ["task:read", "task:write"],
|
|
}
|
|
```
|
|
|
|
The caller needs `admin` AND either `task:read` or `task:write`.
|
|
|
|
### Resource-based access
|
|
|
|
When both `resourceType` and `resourceAction` are set, the caller's `resources` map is checked:
|
|
|
|
```ts
|
|
accessControl: {
|
|
requiredScopes: [],
|
|
resourceType: "project",
|
|
resourceAction: "read",
|
|
}
|
|
```
|
|
|
|
The identity must have `resources` with a key matching `project:*` and `"read"` in the actions array:
|
|
|
|
```ts
|
|
identity: {
|
|
id: "user-1",
|
|
scopes: [],
|
|
resources: { "project:abc": ["read", "write"] },
|
|
}
|
|
```
|
|
|
|
## Identity
|
|
|
|
```ts
|
|
interface Identity {
|
|
id: string;
|
|
scopes: string[];
|
|
resources?: Record<string, string[]>;
|
|
}
|
|
```
|
|
|
|
## Enforcement
|
|
|
|
### enforceAccess()
|
|
|
|
Throws `CallError(ACCESS_DENIED)` if access is denied:
|
|
|
|
```ts
|
|
import { enforceAccess } from "@alkdev/operations";
|
|
|
|
enforceAccess(spec.accessControl, context.identity, operationId, context.trusted);
|
|
```
|
|
|
|
Used internally by `registry.execute()` and `subscribe()`. Passes automatically if `context.trusted` is `true`.
|
|
|
|
### checkAccess()
|
|
|
|
Returns a boolean without throwing:
|
|
|
|
```ts
|
|
import { checkAccess } from "@alkdev/operations";
|
|
|
|
if (!checkAccess(spec.accessControl, identity)) {
|
|
// deny access
|
|
}
|
|
```
|
|
|
|
## Trusted Contexts
|
|
|
|
When `buildEnv()` creates an `OperationEnv` for inter-operation calls, it sets `trusted: true` on the context. This bypasses all access control checks, allowing internal operations to call each other without needing every scope.
|
|
|
|
Direct `registry.execute()` calls within a process can also pass `trusted: true`, but **untrusted callers should go through the call protocol** which always enforces access control. |