acl-next 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +174 -0
- package/dist/index.cjs +679 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +346 -0
- package/dist/index.d.ts +346 -0
- package/dist/index.js +667 -0
- package/dist/index.js.map +1 -0
- package/package.json +90 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core domain types and the storage Backend interface.
|
|
3
|
+
*
|
|
4
|
+
* This is the typed, promise-native successor to the legacy lib/backend.js
|
|
5
|
+
* contract. Backends are namespaced (bucketed) key -> set-of-values stores
|
|
6
|
+
* with batched, committed writes.
|
|
7
|
+
*/
|
|
8
|
+
/** A user identifier. Users are created implicitly by assigning them roles. */
|
|
9
|
+
type UserId = string | number;
|
|
10
|
+
/** A role name. `"*"` is reserved to mean "all permissions". */
|
|
11
|
+
type Role = string;
|
|
12
|
+
/** A resource name (e.g. a URL or logical resource). */
|
|
13
|
+
type Resource = string;
|
|
14
|
+
/** A permission/action name (e.g. `"get"`, `"edit"`). */
|
|
15
|
+
type Permission = string;
|
|
16
|
+
/** A key within a bucket. */
|
|
17
|
+
type Key = string | number;
|
|
18
|
+
/** A value stored inside a bucket's set. */
|
|
19
|
+
type StoredValue = string | number;
|
|
20
|
+
/** One or many of `T` — mirrors the original "accepts string or array" API. */
|
|
21
|
+
type OneOrMany<T> = T | T[];
|
|
22
|
+
/** Optional logger; when provided, the Acl instance emits debug output. */
|
|
23
|
+
interface Logger {
|
|
24
|
+
debug(...args: unknown[]): void;
|
|
25
|
+
}
|
|
26
|
+
/** Names of the internal storage buckets. Overridable via {@link AclOptions}. */
|
|
27
|
+
interface Buckets {
|
|
28
|
+
meta: string;
|
|
29
|
+
parents: string;
|
|
30
|
+
permissions: string;
|
|
31
|
+
resources: string;
|
|
32
|
+
roles: string;
|
|
33
|
+
users: string;
|
|
34
|
+
}
|
|
35
|
+
/** Options accepted by the Acl constructor. */
|
|
36
|
+
interface AclOptions {
|
|
37
|
+
buckets?: Partial<Buckets>;
|
|
38
|
+
}
|
|
39
|
+
/** A single entry of the array form of `allow(...)`. */
|
|
40
|
+
interface AllowRule {
|
|
41
|
+
roles: OneOrMany<Role>;
|
|
42
|
+
allows: Array<{
|
|
43
|
+
resources: OneOrMany<Resource>;
|
|
44
|
+
permissions: OneOrMany<Permission>;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Storage backend interface.
|
|
49
|
+
*
|
|
50
|
+
* Writes are not applied immediately: callers obtain a transaction with
|
|
51
|
+
* {@link Backend.begin}, queue mutations with {@link Backend.add},
|
|
52
|
+
* {@link Backend.del} and {@link Backend.remove}, then commit them with
|
|
53
|
+
* {@link Backend.end}. Backends that support it commit atomically.
|
|
54
|
+
*
|
|
55
|
+
* @typeParam T - the backend-specific transaction type produced by `begin`.
|
|
56
|
+
*/
|
|
57
|
+
interface Backend<T = unknown> {
|
|
58
|
+
/** Start a transaction (a queue of pending mutations). */
|
|
59
|
+
begin(): T;
|
|
60
|
+
/** Commit a transaction. */
|
|
61
|
+
end(transaction: T): Promise<void>;
|
|
62
|
+
/** Remove all stored data. */
|
|
63
|
+
clean(): Promise<void>;
|
|
64
|
+
/** Get the set of values stored at `bucket`/`key` (empty array if none). */
|
|
65
|
+
get(bucket: string, key: Key): Promise<string[]>;
|
|
66
|
+
/** Union of the sets stored at the given `keys` within one `bucket`. */
|
|
67
|
+
union(bucket: string, keys: Key[]): Promise<string[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Per-bucket union of `keys` across multiple `buckets`, keyed by bucket.
|
|
70
|
+
* Optional — Acl falls back to repeated {@link Backend.union} calls when a
|
|
71
|
+
* backend does not implement it.
|
|
72
|
+
*/
|
|
73
|
+
unions?(buckets: string[], keys: Key[]): Promise<Record<string, string[]>>;
|
|
74
|
+
/** Queue: add `values` to the set at `bucket`/`key`. */
|
|
75
|
+
add(transaction: T, bucket: string, key: Key, values: OneOrMany<StoredValue>): void;
|
|
76
|
+
/** Queue: delete the given `keys` from `bucket`. */
|
|
77
|
+
del(transaction: T, bucket: string, keys: OneOrMany<Key>): void;
|
|
78
|
+
/** Queue: remove `values` from the set at `bucket`/`key`. */
|
|
79
|
+
remove(transaction: T, bucket: string, key: Key, values: OneOrMany<StoredValue>): void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Minimal structural request shape the middleware needs. Compatible with
|
|
84
|
+
* Express (and most HTTP frameworks) without depending on their types.
|
|
85
|
+
*/
|
|
86
|
+
interface AclRequest {
|
|
87
|
+
originalUrl?: string;
|
|
88
|
+
url?: string;
|
|
89
|
+
method: string;
|
|
90
|
+
session?: {
|
|
91
|
+
userId?: UserId;
|
|
92
|
+
};
|
|
93
|
+
user?: {
|
|
94
|
+
id?: UserId;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
interface AclResponse {
|
|
98
|
+
status(code: number): {
|
|
99
|
+
end(body?: string): unknown;
|
|
100
|
+
json(body: unknown): unknown;
|
|
101
|
+
send(body: unknown): unknown;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
type AclNext = (err?: unknown) => void;
|
|
105
|
+
type AclMiddleware = (req: AclRequest, res: AclResponse, next: AclNext) => void;
|
|
106
|
+
/** Resolves the userId for a request when not supplied statically. */
|
|
107
|
+
type UserIdResolver = (req: AclRequest, res: AclResponse) => UserId | undefined;
|
|
108
|
+
/** Error thrown by the middleware; pair it with {@link aclErrorHandler}. */
|
|
109
|
+
declare class HttpError extends Error {
|
|
110
|
+
readonly errorCode: number;
|
|
111
|
+
readonly name = "HttpError";
|
|
112
|
+
constructor(errorCode: number, message: string);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build an Express-style middleware that authorizes the current request.
|
|
116
|
+
*
|
|
117
|
+
* @param acl The Acl instance to check against.
|
|
118
|
+
* @param numPathComponents How many leading URL path components form the
|
|
119
|
+
* resource name (default: the whole path).
|
|
120
|
+
* @param userId A static user id, or a resolver `(req, res) => id`.
|
|
121
|
+
* Defaults to `req.session.userId` / `req.user.id`.
|
|
122
|
+
* @param actions Permission(s) to check (default: the HTTP method).
|
|
123
|
+
*/
|
|
124
|
+
declare function aclMiddleware(acl: Acl, numPathComponents?: number, userId?: UserId | UserIdResolver, actions?: OneOrMany<Permission>): AclMiddleware;
|
|
125
|
+
/**
|
|
126
|
+
* Express error handler that renders {@link HttpError}s. Pass `"json"` or
|
|
127
|
+
* `"html"` to choose the response format (defaults to plain text).
|
|
128
|
+
*/
|
|
129
|
+
declare function aclErrorHandler(contentType?: "json" | "html"): (err: unknown, req: AclRequest, res: AclResponse, next: AclNext) => void;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Access Control List. Models authorization as users -> roles -> resources ->
|
|
133
|
+
* permissions, with role hierarchies (parents).
|
|
134
|
+
*
|
|
135
|
+
* Promise-native: every method returns a Promise (the legacy callback API is
|
|
136
|
+
* intentionally dropped). Storage is delegated to a {@link Backend}.
|
|
137
|
+
*
|
|
138
|
+
* @typeParam T - the backend transaction type.
|
|
139
|
+
*/
|
|
140
|
+
declare class Acl<T = unknown> {
|
|
141
|
+
readonly backend: Backend<T>;
|
|
142
|
+
readonly logger: Logger | undefined;
|
|
143
|
+
readonly buckets: Buckets;
|
|
144
|
+
constructor(backend: Backend<T>, logger?: Logger, options?: AclOptions);
|
|
145
|
+
/** Adds roles to a given user id. */
|
|
146
|
+
addUserRoles(userId: UserId, roles: OneOrMany<Role>): Promise<void>;
|
|
147
|
+
/** Removes roles from a given user id. */
|
|
148
|
+
removeUserRoles(userId: UserId, roles: OneOrMany<Role>): Promise<void>;
|
|
149
|
+
/** Returns all the roles assigned to a given user id. */
|
|
150
|
+
userRoles(userId: UserId): Promise<Role[]>;
|
|
151
|
+
/** Returns all users that have the given role. */
|
|
152
|
+
roleUsers(roleName: Role): Promise<UserId[]>;
|
|
153
|
+
/** Returns whether the user has the given role. */
|
|
154
|
+
hasRole(userId: UserId, role: Role): Promise<boolean>;
|
|
155
|
+
/** Adds one or more parent roles to a role. */
|
|
156
|
+
addRoleParents(role: Role, parents: OneOrMany<Role>): Promise<void>;
|
|
157
|
+
/** Removes parent role(s) from a role. Omit `parents` to remove all of them. */
|
|
158
|
+
removeRoleParents(role: Role, parents?: OneOrMany<Role>): Promise<void>;
|
|
159
|
+
/** Removes a role from the system, including all its permissions. */
|
|
160
|
+
removeRole(role: Role): Promise<void>;
|
|
161
|
+
/** Removes a resource from the system. */
|
|
162
|
+
removeResource(resource: Resource): Promise<void>;
|
|
163
|
+
/** Adds permissions to roles over resources (compact array form). */
|
|
164
|
+
allow(rules: AllowRule[]): Promise<void>;
|
|
165
|
+
/** Adds the given permissions to the given roles over the given resources. */
|
|
166
|
+
allow(roles: OneOrMany<Role>, resources: OneOrMany<Resource>, permissions: OneOrMany<Permission>): Promise<void>;
|
|
167
|
+
/** Removes permissions from a role over resources. Omit `permissions` to remove all. */
|
|
168
|
+
removeAllow(role: Role, resources: OneOrMany<Resource>, permissions?: OneOrMany<Permission>): Promise<void>;
|
|
169
|
+
/**
|
|
170
|
+
* Removes permissions from a role over the given resources. When
|
|
171
|
+
* `permissions` is null the resource is fully revoked for the role.
|
|
172
|
+
*
|
|
173
|
+
* Note: loses atomicity when pruning emptied role/resource links.
|
|
174
|
+
*/
|
|
175
|
+
removePermissions(role: Role, resources: Resource[], permissions: Permission[] | null): Promise<void>;
|
|
176
|
+
/**
|
|
177
|
+
* Returns, per resource, the permissions a user has. Uses the backend's
|
|
178
|
+
* `unions` optimization when available.
|
|
179
|
+
*/
|
|
180
|
+
allowedPermissions(userId: UserId, resources: OneOrMany<Resource>): Promise<Record<Resource, Permission[]>>;
|
|
181
|
+
/** `allowedPermissions` variant using the backend `unions` bulk query. */
|
|
182
|
+
optimizedAllowedPermissions(userId: UserId, resources: OneOrMany<Resource>): Promise<Record<Resource, Permission[]>>;
|
|
183
|
+
/** Checks if a user is allowed all of the given permissions on a resource. */
|
|
184
|
+
isAllowed(userId: UserId, resource: Resource, permissions: OneOrMany<Permission>): Promise<boolean>;
|
|
185
|
+
/** Returns true if any of the roles has all of the given permissions. */
|
|
186
|
+
areAnyRolesAllowed(roles: OneOrMany<Role>, resource: Resource, permissions: OneOrMany<Permission>): Promise<boolean>;
|
|
187
|
+
/** Returns a map of resource -> permissions the roles have. */
|
|
188
|
+
whatResources(roles: OneOrMany<Role>): Promise<Record<Resource, Permission[]>>;
|
|
189
|
+
/** Returns the resources the roles have all of the given permissions over. */
|
|
190
|
+
whatResources(roles: OneOrMany<Role>, permissions: OneOrMany<Permission>): Promise<Resource[]>;
|
|
191
|
+
/** Backing implementation for {@link whatResources}. */
|
|
192
|
+
permittedResources(roles: OneOrMany<Role>, permissions?: Permission[]): Promise<Record<Resource, Permission[]> | Resource[]>;
|
|
193
|
+
/**
|
|
194
|
+
* Express-style middleware that authorizes the current request against this
|
|
195
|
+
* Acl. See {@link aclMiddleware} for parameter semantics. Pair with
|
|
196
|
+
* {@link aclErrorHandler} to render the resulting 401/403 errors.
|
|
197
|
+
*/
|
|
198
|
+
middleware(numPathComponents?: number, userId?: UserId | UserIdResolver, actions?: OneOrMany<Permission>): AclMiddleware;
|
|
199
|
+
/** Compact array form of {@link allow}. */
|
|
200
|
+
private allowEx;
|
|
201
|
+
/** Direct parents of the given roles. */
|
|
202
|
+
private rolesParents;
|
|
203
|
+
/** All roles in the hierarchy, including the given roles. */
|
|
204
|
+
private allRoles;
|
|
205
|
+
/** All roles in the hierarchy of the given user. */
|
|
206
|
+
private allUserRoles;
|
|
207
|
+
/** All resources reachable by the given roles (through the hierarchy). */
|
|
208
|
+
private rolesResources;
|
|
209
|
+
/** Permissions the given roles (and their parents) have over a resource. */
|
|
210
|
+
private resourcePermissions;
|
|
211
|
+
/**
|
|
212
|
+
* Whether the roles (and their parents) satisfy all permissions on a resource.
|
|
213
|
+
*
|
|
214
|
+
* NOTE: does not handle circular role hierarchies.
|
|
215
|
+
*/
|
|
216
|
+
private checkPermissions;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** A queued mutation. The memory transaction is simply a list of these. */
|
|
220
|
+
type Mutation = () => void;
|
|
221
|
+
/** In-memory transaction: an ordered list of pending mutations. */
|
|
222
|
+
type MemoryTransaction = Mutation[];
|
|
223
|
+
/**
|
|
224
|
+
* In-memory storage backend. No external dependencies — ideal for tests and
|
|
225
|
+
* single-process apps. Data lives in a `Map<bucket, Map<key, values[]>>`.
|
|
226
|
+
*/
|
|
227
|
+
declare class MemoryBackend implements Backend<MemoryTransaction> {
|
|
228
|
+
private buckets;
|
|
229
|
+
begin(): MemoryTransaction;
|
|
230
|
+
end(transaction: MemoryTransaction): Promise<void>;
|
|
231
|
+
clean(): Promise<void>;
|
|
232
|
+
get(bucket: string, key: Key): Promise<string[]>;
|
|
233
|
+
unions(buckets: string[], keys: Key[]): Promise<Record<string, string[]>>;
|
|
234
|
+
union(bucket: string, keys: Key[]): Promise<string[]>;
|
|
235
|
+
add(transaction: MemoryTransaction, bucket: string, key: Key, values: OneOrMany<StoredValue>): void;
|
|
236
|
+
del(transaction: MemoryTransaction, bucket: string, keys: OneOrMany<Key>): void;
|
|
237
|
+
remove(transaction: MemoryTransaction, bucket: string, key: Key, values: OneOrMany<StoredValue>): void;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Minimal structural type for a node-redis v4+ client. Declared here (rather
|
|
242
|
+
* than importing from `redis`) so this module stays decoupled from the driver:
|
|
243
|
+
* any compatible client works, and consumers without `redis` installed can
|
|
244
|
+
* still type-check the rest of the package.
|
|
245
|
+
*/
|
|
246
|
+
interface RedisClientLike {
|
|
247
|
+
multi(): RedisMultiLike;
|
|
248
|
+
sMembers(key: string): Promise<string[]>;
|
|
249
|
+
sUnion(keys: string[]): Promise<string[]>;
|
|
250
|
+
keys(pattern: string): Promise<string[]>;
|
|
251
|
+
del(keys: string | string[]): Promise<number>;
|
|
252
|
+
}
|
|
253
|
+
interface RedisMultiLike {
|
|
254
|
+
sAdd(key: string, members: string | string[]): RedisMultiLike;
|
|
255
|
+
sRem(key: string, members: string | string[]): RedisMultiLike;
|
|
256
|
+
del(keys: string | string[]): RedisMultiLike;
|
|
257
|
+
exec(): Promise<unknown[]>;
|
|
258
|
+
}
|
|
259
|
+
/** Redis storage backend (node-redis v4+). Sets map directly to Redis sets. */
|
|
260
|
+
declare class RedisBackend implements Backend<RedisMultiLike> {
|
|
261
|
+
private readonly redis;
|
|
262
|
+
private readonly prefix;
|
|
263
|
+
constructor(redis: RedisClientLike, prefix?: string);
|
|
264
|
+
begin(): RedisMultiLike;
|
|
265
|
+
end(transaction: RedisMultiLike): Promise<void>;
|
|
266
|
+
clean(): Promise<void>;
|
|
267
|
+
get(bucket: string, key: Key): Promise<string[]>;
|
|
268
|
+
unions(buckets: string[], keys: Key[]): Promise<Record<string, string[]>>;
|
|
269
|
+
union(bucket: string, keys: Key[]): Promise<string[]>;
|
|
270
|
+
add(transaction: RedisMultiLike, bucket: string, key: Key, values: OneOrMany<StoredValue>): void;
|
|
271
|
+
del(transaction: RedisMultiLike, bucket: string, keys: OneOrMany<Key>): void;
|
|
272
|
+
remove(transaction: RedisMultiLike, bucket: string, key: Key, values: OneOrMany<StoredValue>): void;
|
|
273
|
+
private bucketKey;
|
|
274
|
+
private bucketKeys;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Minimal structural types for the mongodb v4+ driver, declared here so this
|
|
279
|
+
* module stays decoupled from the driver (mongodb is an optional peer dep).
|
|
280
|
+
*/
|
|
281
|
+
interface MongoCollectionLike {
|
|
282
|
+
findOne(filter: object, options?: object): Promise<Record<string, unknown> | null>;
|
|
283
|
+
find(filter: object, options?: object): {
|
|
284
|
+
toArray(): Promise<Record<string, unknown>[]>;
|
|
285
|
+
};
|
|
286
|
+
updateOne(filter: object, update: object, options?: object): Promise<unknown>;
|
|
287
|
+
deleteMany(filter: object): Promise<unknown>;
|
|
288
|
+
createIndex(spec: object): Promise<string>;
|
|
289
|
+
drop(): Promise<boolean>;
|
|
290
|
+
}
|
|
291
|
+
interface MongoDbLike {
|
|
292
|
+
collection(name: string): MongoCollectionLike;
|
|
293
|
+
collections(): Promise<MongoCollectionLike[]>;
|
|
294
|
+
}
|
|
295
|
+
/** Each queued mutation is an async function; the transaction runs them in series. */
|
|
296
|
+
type MongoTransaction = Array<() => Promise<void>>;
|
|
297
|
+
interface MongoDBBackendOptions {
|
|
298
|
+
prefix?: string;
|
|
299
|
+
/** Store every bucket in one collection (distinguished by `_bucketname`). */
|
|
300
|
+
useSingle?: boolean;
|
|
301
|
+
/** Use bucket names verbatim as collection names (skip sanitization). */
|
|
302
|
+
useRawCollectionNames?: boolean;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* MongoDB storage backend (mongodb v4+ driver).
|
|
306
|
+
*
|
|
307
|
+
* Each (bucket, key) pair is a document whose field names are the set members
|
|
308
|
+
* (stored as `{ <member>: true }`), since MongoDB has no native set type.
|
|
309
|
+
*/
|
|
310
|
+
declare class MongoDBBackend implements Backend<MongoTransaction> {
|
|
311
|
+
private readonly db;
|
|
312
|
+
private readonly prefix;
|
|
313
|
+
private readonly useSingle;
|
|
314
|
+
private readonly useRawCollectionNames;
|
|
315
|
+
constructor(db: MongoDbLike, options?: MongoDBBackendOptions);
|
|
316
|
+
begin(): MongoTransaction;
|
|
317
|
+
end(transaction: MongoTransaction): Promise<void>;
|
|
318
|
+
clean(): Promise<void>;
|
|
319
|
+
get(bucket: string, key: Key): Promise<string[]>;
|
|
320
|
+
union(bucket: string, keys: Key[]): Promise<string[]>;
|
|
321
|
+
add(transaction: MongoTransaction, bucket: string, key: Key, values: OneOrMany<StoredValue>): void;
|
|
322
|
+
del(transaction: MongoTransaction, bucket: string, keys: OneOrMany<Key>): void;
|
|
323
|
+
remove(transaction: MongoTransaction, bucket: string, key: Key, values: OneOrMany<StoredValue>): void;
|
|
324
|
+
private collection;
|
|
325
|
+
private filter;
|
|
326
|
+
/** Build a `{ <encoded member>: true }` doc from one or many values. */
|
|
327
|
+
private buildDoc;
|
|
328
|
+
/** Decode a stored document's field names back into set members. */
|
|
329
|
+
private members;
|
|
330
|
+
private sanitizeCollectionName;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* acl-next — modern TypeScript ACL / RBAC.
|
|
335
|
+
*
|
|
336
|
+
* Fork of optimalbits/node_acl (MIT, (c) 2011-2013 Manuel Astudillo).
|
|
337
|
+
*
|
|
338
|
+
* Public surface is filled in across the modernization phases:
|
|
339
|
+
* - Phase 2: types & Backend interface
|
|
340
|
+
* - Phase 3: backends (memory, redis, mongodb)
|
|
341
|
+
* - Phase 4: the Acl class
|
|
342
|
+
* - Phase 5: express middleware
|
|
343
|
+
*/
|
|
344
|
+
declare const VERSION = "1.0.0-alpha.0";
|
|
345
|
+
|
|
346
|
+
export { Acl, type AclMiddleware, type AclNext, type AclOptions, type AclRequest, type AclResponse, type AllowRule, type Backend, type Buckets, HttpError, type Key, type Logger, MemoryBackend, type MemoryTransaction, type MongoCollectionLike, MongoDBBackend, type MongoDBBackendOptions, type MongoDbLike, type MongoTransaction, type OneOrMany, type Permission, RedisBackend, type RedisClientLike, type RedisMultiLike, type Resource, type Role, type StoredValue, type UserId, type UserIdResolver, VERSION, aclErrorHandler, aclMiddleware, Acl as default };
|