@chidchanun/bcp 0.2.5 → 0.2.6
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/README.md +140 -66
- package/docs/README.md +54 -48
- package/docs/api-manifest.json +5 -3
- package/docs/api-reference.md +42 -6
- package/docs/auth-route-guards.md +58 -50
- package/docs/authorization-security.md +298 -0
- package/docs/docs-web-manifest.json +6 -4
- package/docs/platform-manifest.json +11 -4
- package/docs/releases/0.2.6.md +194 -0
- package/docs/security.md +55 -1
- package/package.json +1 -1
- package/packages/client/src/auth.ts +20 -0
- package/packages/client/src/server.ts +16 -0
- package/packages/server/src/auth-guard.ts +111 -15
- package/packages/server/src/authorization.ts +368 -0
- package/packages/server/src/request-security.ts +596 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
export type AuthorizationMatch =
|
|
2
|
+
| "any"
|
|
3
|
+
| "all";
|
|
4
|
+
|
|
5
|
+
export type PermissionRequirement =
|
|
6
|
+
| string
|
|
7
|
+
| readonly string[];
|
|
8
|
+
|
|
9
|
+
export interface PermissionCheckOptions {
|
|
10
|
+
field?: string;
|
|
11
|
+
match?: AuthorizationMatch;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AuthorizationContext<
|
|
15
|
+
TUser extends object = Record<string, unknown>,
|
|
16
|
+
TResource = unknown,
|
|
17
|
+
TData = unknown
|
|
18
|
+
> {
|
|
19
|
+
user: TUser;
|
|
20
|
+
resource?: TResource;
|
|
21
|
+
data?: TData;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type AuthorizationPolicy<
|
|
25
|
+
TUser extends object = Record<string, unknown>,
|
|
26
|
+
TResource = unknown,
|
|
27
|
+
TData = unknown
|
|
28
|
+
> = (
|
|
29
|
+
context: AuthorizationContext<
|
|
30
|
+
TUser,
|
|
31
|
+
TResource,
|
|
32
|
+
TData
|
|
33
|
+
>
|
|
34
|
+
) => boolean | Promise<boolean>;
|
|
35
|
+
|
|
36
|
+
export class AuthorizationError
|
|
37
|
+
extends Error {
|
|
38
|
+
readonly status = 403;
|
|
39
|
+
readonly code = "FORBIDDEN";
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
message = "Forbidden"
|
|
43
|
+
) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name =
|
|
46
|
+
"AuthorizationError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getUserPermissions(
|
|
51
|
+
user: object,
|
|
52
|
+
field = "permissions"
|
|
53
|
+
): string[] {
|
|
54
|
+
const normalizedField =
|
|
55
|
+
normalizeField(
|
|
56
|
+
field
|
|
57
|
+
);
|
|
58
|
+
const value =
|
|
59
|
+
(
|
|
60
|
+
user as
|
|
61
|
+
Record<string, unknown>
|
|
62
|
+
)[normalizedField];
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
typeof value === "string"
|
|
66
|
+
) {
|
|
67
|
+
const permission =
|
|
68
|
+
value.trim();
|
|
69
|
+
|
|
70
|
+
return permission
|
|
71
|
+
? [permission]
|
|
72
|
+
: [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
!Array.isArray(
|
|
77
|
+
value
|
|
78
|
+
)
|
|
79
|
+
) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return Array.from(
|
|
84
|
+
new Set(
|
|
85
|
+
value
|
|
86
|
+
.filter(
|
|
87
|
+
(
|
|
88
|
+
permission
|
|
89
|
+
): permission is string =>
|
|
90
|
+
typeof permission ===
|
|
91
|
+
"string"
|
|
92
|
+
)
|
|
93
|
+
.map(
|
|
94
|
+
(permission) =>
|
|
95
|
+
permission.trim()
|
|
96
|
+
)
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function hasPermission(
|
|
103
|
+
user: object,
|
|
104
|
+
required:
|
|
105
|
+
PermissionRequirement,
|
|
106
|
+
options:
|
|
107
|
+
PermissionCheckOptions = {}
|
|
108
|
+
): boolean {
|
|
109
|
+
const requiredPermissions =
|
|
110
|
+
normalizePermissions(
|
|
111
|
+
required
|
|
112
|
+
);
|
|
113
|
+
const assignedPermissions =
|
|
114
|
+
getUserPermissions(
|
|
115
|
+
user,
|
|
116
|
+
options.field
|
|
117
|
+
);
|
|
118
|
+
const match =
|
|
119
|
+
options.match ??
|
|
120
|
+
"any";
|
|
121
|
+
|
|
122
|
+
assertAuthorizationMatch(
|
|
123
|
+
match
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return match === "all"
|
|
127
|
+
? requiredPermissions.every(
|
|
128
|
+
(permission) =>
|
|
129
|
+
assignedPermissions.includes(
|
|
130
|
+
permission
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
: requiredPermissions.some(
|
|
134
|
+
(permission) =>
|
|
135
|
+
assignedPermissions.includes(
|
|
136
|
+
permission
|
|
137
|
+
)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function assertPermission(
|
|
142
|
+
user: object,
|
|
143
|
+
required:
|
|
144
|
+
PermissionRequirement,
|
|
145
|
+
options:
|
|
146
|
+
PermissionCheckOptions = {}
|
|
147
|
+
): void {
|
|
148
|
+
if (
|
|
149
|
+
!hasPermission(
|
|
150
|
+
user,
|
|
151
|
+
required,
|
|
152
|
+
options
|
|
153
|
+
)
|
|
154
|
+
) {
|
|
155
|
+
throw new AuthorizationError();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function defineAuthorizationPolicy<
|
|
160
|
+
TUser extends object,
|
|
161
|
+
TResource = unknown,
|
|
162
|
+
TData = unknown
|
|
163
|
+
>(
|
|
164
|
+
policy:
|
|
165
|
+
AuthorizationPolicy<
|
|
166
|
+
TUser,
|
|
167
|
+
TResource,
|
|
168
|
+
TData
|
|
169
|
+
>
|
|
170
|
+
): AuthorizationPolicy<
|
|
171
|
+
TUser,
|
|
172
|
+
TResource,
|
|
173
|
+
TData
|
|
174
|
+
> {
|
|
175
|
+
if (
|
|
176
|
+
typeof policy !==
|
|
177
|
+
"function"
|
|
178
|
+
) {
|
|
179
|
+
throw new TypeError(
|
|
180
|
+
"BCP Authorization: policy must be a function."
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return policy;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function can<
|
|
188
|
+
TUser extends object,
|
|
189
|
+
TResource = unknown,
|
|
190
|
+
TData = unknown
|
|
191
|
+
>(
|
|
192
|
+
policy:
|
|
193
|
+
AuthorizationPolicy<
|
|
194
|
+
TUser,
|
|
195
|
+
TResource,
|
|
196
|
+
TData
|
|
197
|
+
>,
|
|
198
|
+
context:
|
|
199
|
+
AuthorizationContext<
|
|
200
|
+
TUser,
|
|
201
|
+
TResource,
|
|
202
|
+
TData
|
|
203
|
+
>
|
|
204
|
+
): Promise<boolean> {
|
|
205
|
+
assertAuthorizationContext(
|
|
206
|
+
context
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return Boolean(
|
|
210
|
+
await policy(
|
|
211
|
+
context
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function cannot<
|
|
217
|
+
TUser extends object,
|
|
218
|
+
TResource = unknown,
|
|
219
|
+
TData = unknown
|
|
220
|
+
>(
|
|
221
|
+
policy:
|
|
222
|
+
AuthorizationPolicy<
|
|
223
|
+
TUser,
|
|
224
|
+
TResource,
|
|
225
|
+
TData
|
|
226
|
+
>,
|
|
227
|
+
context:
|
|
228
|
+
AuthorizationContext<
|
|
229
|
+
TUser,
|
|
230
|
+
TResource,
|
|
231
|
+
TData
|
|
232
|
+
>
|
|
233
|
+
): Promise<boolean> {
|
|
234
|
+
return !await can(
|
|
235
|
+
policy,
|
|
236
|
+
context
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function authorize<
|
|
241
|
+
TUser extends object,
|
|
242
|
+
TResource = unknown,
|
|
243
|
+
TData = unknown
|
|
244
|
+
>(
|
|
245
|
+
policy:
|
|
246
|
+
AuthorizationPolicy<
|
|
247
|
+
TUser,
|
|
248
|
+
TResource,
|
|
249
|
+
TData
|
|
250
|
+
>,
|
|
251
|
+
context:
|
|
252
|
+
AuthorizationContext<
|
|
253
|
+
TUser,
|
|
254
|
+
TResource,
|
|
255
|
+
TData
|
|
256
|
+
>,
|
|
257
|
+
message = "Forbidden"
|
|
258
|
+
): Promise<void> {
|
|
259
|
+
if (
|
|
260
|
+
!await can(
|
|
261
|
+
policy,
|
|
262
|
+
context
|
|
263
|
+
)
|
|
264
|
+
) {
|
|
265
|
+
throw new AuthorizationError(
|
|
266
|
+
message
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function normalizePermissions(
|
|
272
|
+
value:
|
|
273
|
+
PermissionRequirement
|
|
274
|
+
): string[] {
|
|
275
|
+
const permissions =
|
|
276
|
+
typeof value === "string"
|
|
277
|
+
? [value]
|
|
278
|
+
: [
|
|
279
|
+
...value,
|
|
280
|
+
];
|
|
281
|
+
const normalized =
|
|
282
|
+
permissions.map(
|
|
283
|
+
(permission) =>
|
|
284
|
+
permission.trim()
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (
|
|
288
|
+
normalized.length === 0 ||
|
|
289
|
+
normalized.some(
|
|
290
|
+
(permission) =>
|
|
291
|
+
permission.length === 0
|
|
292
|
+
)
|
|
293
|
+
) {
|
|
294
|
+
throw new TypeError(
|
|
295
|
+
"BCP Authorization: required permissions must contain non-empty strings."
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return Array.from(
|
|
300
|
+
new Set(
|
|
301
|
+
normalized
|
|
302
|
+
)
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function normalizeField(
|
|
307
|
+
value: string
|
|
308
|
+
): string {
|
|
309
|
+
const field =
|
|
310
|
+
value.trim();
|
|
311
|
+
|
|
312
|
+
if (!field) {
|
|
313
|
+
throw new TypeError(
|
|
314
|
+
"BCP Authorization: permission field must be a non-empty string."
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return field;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function assertAuthorizationMatch(
|
|
322
|
+
value: string
|
|
323
|
+
): asserts value is AuthorizationMatch {
|
|
324
|
+
if (
|
|
325
|
+
value !== "any" &&
|
|
326
|
+
value !== "all"
|
|
327
|
+
) {
|
|
328
|
+
throw new TypeError(
|
|
329
|
+
"BCP Authorization: match must be either \"any\" or \"all\"."
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function assertAuthorizationContext(
|
|
335
|
+
value: unknown
|
|
336
|
+
): asserts value is AuthorizationContext<object> {
|
|
337
|
+
if (
|
|
338
|
+
!value ||
|
|
339
|
+
typeof value !== "object" ||
|
|
340
|
+
Array.isArray(
|
|
341
|
+
value
|
|
342
|
+
) ||
|
|
343
|
+
!(
|
|
344
|
+
"user" in value
|
|
345
|
+
) ||
|
|
346
|
+
!(
|
|
347
|
+
value as {
|
|
348
|
+
user?: unknown;
|
|
349
|
+
}
|
|
350
|
+
).user ||
|
|
351
|
+
typeof (
|
|
352
|
+
value as {
|
|
353
|
+
user: unknown;
|
|
354
|
+
}
|
|
355
|
+
).user !== "object" ||
|
|
356
|
+
Array.isArray(
|
|
357
|
+
(
|
|
358
|
+
value as {
|
|
359
|
+
user: unknown;
|
|
360
|
+
}
|
|
361
|
+
).user
|
|
362
|
+
)
|
|
363
|
+
) {
|
|
364
|
+
throw new TypeError(
|
|
365
|
+
"BCP Authorization: context must contain a user object."
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|