@twinfinity/permission 6.0.0 → 6.0.1-ci.28952
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/dist/BuildInfo.js +3 -3
- package/dist/PermissionClient.d.ts +13 -5
- package/dist/PermissionClient.d.ts.map +1 -1
- package/dist/PermissionClient.js +129 -9
- package/dist/PermissionClient.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/PermissionClient.html +6 -2
- package/docs/classes/PermissionConflictError.html +1 -1
- package/docs/classes/PermissionError.html +1 -1
- package/docs/classes/PermissionForbiddenError.html +1 -1
- package/docs/classes/PermissionNotFoundError.html +1 -1
- package/docs/classes/PermissionValidationError.html +1 -1
- package/docs/hierarchy.html +1 -1
- package/docs/index.html +1 -1
- package/docs/interfaces/Group.html +7 -2
- package/docs/interfaces/GroupMember.html +10 -0
- package/docs/interfaces/IPermissionClient.html +6 -2
- package/docs/interfaces/PaginatedResponse.html +8 -2
- package/docs/interfaces/PutGroupRequest.html +4 -3
- package/docs/interfaces/User.html +1 -1
- package/docs/types/PrincipalPermission.html +9 -0
- package/package.json +2 -2
- package/src/PermissionClient.ts +212 -10
- package/src/types.ts +36 -0
package/src/PermissionClient.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { TwinfinityHttpClient, HttpMethod } from '@twinfinity/authentication';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
PaginatedResponse,
|
|
4
|
+
User,
|
|
5
|
+
Group,
|
|
6
|
+
GroupMember,
|
|
7
|
+
PutGroupRequest,
|
|
8
|
+
DeleteGroupRequest,
|
|
9
|
+
PermissionErrorBody
|
|
10
|
+
} from './types';
|
|
3
11
|
import {
|
|
4
12
|
PermissionValidationError,
|
|
5
13
|
PermissionError,
|
|
@@ -9,10 +17,24 @@ import {
|
|
|
9
17
|
} from './errors';
|
|
10
18
|
|
|
11
19
|
export interface IPermissionClient {
|
|
12
|
-
listUsers(page?: string, limit?: number, signal?: AbortSignal): Promise<PaginatedResponse<User>>;
|
|
13
|
-
listGroups(page?: string, limit?: number, signal?: AbortSignal): Promise<PaginatedResponse<Group>>;
|
|
20
|
+
listUsers(page?: string, limit?: number, q?: string, signal?: AbortSignal): Promise<PaginatedResponse<User>>;
|
|
21
|
+
listGroups(page?: string, limit?: number, q?: string, signal?: AbortSignal): Promise<PaginatedResponse<Group>>;
|
|
14
22
|
putGroup(id: string, request: PutGroupRequest, signal?: AbortSignal): Promise<Group>;
|
|
15
23
|
deleteGroup(id: string, request: DeleteGroupRequest, signal?: AbortSignal): Promise<Group>;
|
|
24
|
+
listGroupMembers(
|
|
25
|
+
groupId: string,
|
|
26
|
+
page?: string,
|
|
27
|
+
limit?: number,
|
|
28
|
+
signal?: AbortSignal
|
|
29
|
+
): Promise<PaginatedResponse<User>>;
|
|
30
|
+
addGroupMember(groupId: string, userId: string, signal?: AbortSignal): Promise<GroupMember>;
|
|
31
|
+
removeGroupMember(groupId: string, userId: string, signal?: AbortSignal): Promise<GroupMember>;
|
|
32
|
+
listUserGroups(
|
|
33
|
+
userId: string,
|
|
34
|
+
page?: string,
|
|
35
|
+
limit?: number,
|
|
36
|
+
signal?: AbortSignal
|
|
37
|
+
): Promise<PaginatedResponse<Group>>;
|
|
16
38
|
}
|
|
17
39
|
|
|
18
40
|
function isErrorBody(value: unknown): value is PermissionErrorBody {
|
|
@@ -23,11 +45,21 @@ async function readErrorBody(response: Response): Promise<PermissionErrorBody |
|
|
|
23
45
|
try {
|
|
24
46
|
const json = await response.json();
|
|
25
47
|
return isErrorBody(json) ? json : undefined;
|
|
26
|
-
} catch {
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err instanceof Error && err.name === 'AbortError') throw err;
|
|
27
50
|
return undefined;
|
|
28
51
|
}
|
|
29
52
|
}
|
|
30
53
|
|
|
54
|
+
async function readBodyText(response: Response): Promise<string> {
|
|
55
|
+
try {
|
|
56
|
+
return await response.text();
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err instanceof Error && err.name === 'AbortError') throw err;
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
31
63
|
export class PermissionClient implements IPermissionClient {
|
|
32
64
|
private readonly _usersUrl: string;
|
|
33
65
|
private readonly _groupsUrl: string;
|
|
@@ -44,11 +76,13 @@ export class PermissionClient implements IPermissionClient {
|
|
|
44
76
|
async listUsers(
|
|
45
77
|
page: string | undefined = undefined,
|
|
46
78
|
limit = 500,
|
|
79
|
+
q?: string,
|
|
47
80
|
signal?: AbortSignal
|
|
48
81
|
): Promise<PaginatedResponse<User>> {
|
|
49
82
|
const params = new URLSearchParams();
|
|
50
83
|
if (page !== undefined) params.set('page', page);
|
|
51
84
|
params.set('limit', limit.toString());
|
|
85
|
+
if (q) params.set('q', q);
|
|
52
86
|
|
|
53
87
|
const url = `${this._usersUrl}?${params}`;
|
|
54
88
|
|
|
@@ -70,7 +104,7 @@ export class PermissionClient implements IPermissionClient {
|
|
|
70
104
|
throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list users', body);
|
|
71
105
|
}
|
|
72
106
|
|
|
73
|
-
const errorText = await response
|
|
107
|
+
const errorText = await readBodyText(response);
|
|
74
108
|
throw new PermissionError(
|
|
75
109
|
`Failed to list users: ${response.status} ${response.statusText} - ${errorText}`,
|
|
76
110
|
response.status,
|
|
@@ -81,11 +115,13 @@ export class PermissionClient implements IPermissionClient {
|
|
|
81
115
|
async listGroups(
|
|
82
116
|
page: string | undefined = undefined,
|
|
83
117
|
limit = 500,
|
|
118
|
+
q?: string,
|
|
84
119
|
signal?: AbortSignal
|
|
85
120
|
): Promise<PaginatedResponse<Group>> {
|
|
86
121
|
const params = new URLSearchParams();
|
|
87
122
|
if (page !== undefined) params.set('page', page);
|
|
88
123
|
params.set('limit', limit.toString());
|
|
124
|
+
if (q) params.set('q', q);
|
|
89
125
|
|
|
90
126
|
const url = `${this._groupsUrl}?${params}`;
|
|
91
127
|
|
|
@@ -107,7 +143,7 @@ export class PermissionClient implements IPermissionClient {
|
|
|
107
143
|
throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list groups', body);
|
|
108
144
|
}
|
|
109
145
|
|
|
110
|
-
const errorText = await response
|
|
146
|
+
const errorText = await readBodyText(response);
|
|
111
147
|
throw new PermissionError(
|
|
112
148
|
`Failed to list groups: ${response.status} ${response.statusText} - ${errorText}`,
|
|
113
149
|
response.status,
|
|
@@ -136,7 +172,8 @@ export class PermissionClient implements IPermissionClient {
|
|
|
136
172
|
let json: unknown;
|
|
137
173
|
try {
|
|
138
174
|
json = await response.json();
|
|
139
|
-
} catch {
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (err instanceof Error && err.name === 'AbortError') throw err;
|
|
140
177
|
throw new PermissionConflictError('Group conflict', undefined);
|
|
141
178
|
}
|
|
142
179
|
const body = isErrorBody(json) ? json : undefined;
|
|
@@ -160,7 +197,7 @@ export class PermissionClient implements IPermissionClient {
|
|
|
160
197
|
throw new PermissionValidationError(body?.detail ?? 'Group validation failed', body);
|
|
161
198
|
}
|
|
162
199
|
|
|
163
|
-
const errorText = await response
|
|
200
|
+
const errorText = await readBodyText(response);
|
|
164
201
|
throw new PermissionError(
|
|
165
202
|
`Failed to put group: ${response.status} ${response.statusText} - ${errorText}`,
|
|
166
203
|
response.status,
|
|
@@ -168,6 +205,170 @@ export class PermissionClient implements IPermissionClient {
|
|
|
168
205
|
);
|
|
169
206
|
}
|
|
170
207
|
|
|
208
|
+
async listGroupMembers(
|
|
209
|
+
groupId: string,
|
|
210
|
+
page: string | undefined = undefined,
|
|
211
|
+
limit = 500,
|
|
212
|
+
signal?: AbortSignal
|
|
213
|
+
): Promise<PaginatedResponse<User>> {
|
|
214
|
+
const params = new URLSearchParams();
|
|
215
|
+
if (page !== undefined) params.set('page', page);
|
|
216
|
+
params.set('limit', limit.toString());
|
|
217
|
+
|
|
218
|
+
const url = `${this._groupsUrl}/${encodeURIComponent(groupId)}/members?${params}`;
|
|
219
|
+
|
|
220
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, { signal });
|
|
221
|
+
|
|
222
|
+
if (response.ok) {
|
|
223
|
+
return (await response.json()) as PaginatedResponse<User>;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (response.status === 404) {
|
|
227
|
+
const body = await readErrorBody(response);
|
|
228
|
+
throw new PermissionNotFoundError(body?.detail ?? `Group '${groupId}' not found`, body);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (response.status === 400) {
|
|
232
|
+
const body = await readErrorBody(response);
|
|
233
|
+
throw new PermissionValidationError(body?.detail ?? 'Group member listing validation failed', body);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (response.status === 403) {
|
|
237
|
+
const body = await readErrorBody(response);
|
|
238
|
+
throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list group members', body);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const errorText = await readBodyText(response);
|
|
242
|
+
throw new PermissionError(
|
|
243
|
+
`Failed to list group members: ${response.status} ${response.statusText} - ${errorText}`,
|
|
244
|
+
response.status,
|
|
245
|
+
response.statusText
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async addGroupMember(groupId: string, userId: string, signal?: AbortSignal): Promise<GroupMember> {
|
|
250
|
+
const url = `${this._groupsUrl}/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}`;
|
|
251
|
+
|
|
252
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, url, { signal });
|
|
253
|
+
|
|
254
|
+
if (response.ok) {
|
|
255
|
+
return (await response.json()) as GroupMember;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (response.status === 404) {
|
|
259
|
+
const body = await readErrorBody(response);
|
|
260
|
+
throw new PermissionNotFoundError(body?.detail ?? 'Group or user not found', body);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (response.status === 400) {
|
|
264
|
+
const body = await readErrorBody(response);
|
|
265
|
+
throw new PermissionValidationError(body?.detail ?? 'Cannot modify this group', body);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (response.status === 403) {
|
|
269
|
+
const body = await readErrorBody(response);
|
|
270
|
+
throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to add group member', body);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (response.status === 409) {
|
|
274
|
+
const body = await readErrorBody(response);
|
|
275
|
+
throw new PermissionConflictError(
|
|
276
|
+
body?.detail ?? 'Group membership was modified concurrently, please retry',
|
|
277
|
+
undefined,
|
|
278
|
+
body
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const errorText = await readBodyText(response);
|
|
283
|
+
throw new PermissionError(
|
|
284
|
+
`Failed to add group member: ${response.status} ${response.statusText} - ${errorText}`,
|
|
285
|
+
response.status,
|
|
286
|
+
response.statusText
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async removeGroupMember(groupId: string, userId: string, signal?: AbortSignal): Promise<GroupMember> {
|
|
291
|
+
const url = `${this._groupsUrl}/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}`;
|
|
292
|
+
|
|
293
|
+
const response = await this._httpClient.fetch(HttpMethod.Delete, url, { signal });
|
|
294
|
+
|
|
295
|
+
if (response.ok) {
|
|
296
|
+
return (await response.json()) as GroupMember;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (response.status === 404) {
|
|
300
|
+
const body = await readErrorBody(response);
|
|
301
|
+
throw new PermissionNotFoundError(body?.detail ?? 'Group, user, or membership not found', body);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (response.status === 400) {
|
|
305
|
+
const body = await readErrorBody(response);
|
|
306
|
+
throw new PermissionValidationError(body?.detail ?? 'Cannot modify this group', body);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (response.status === 403) {
|
|
310
|
+
const body = await readErrorBody(response);
|
|
311
|
+
throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to remove group member', body);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (response.status === 409) {
|
|
315
|
+
const body = await readErrorBody(response);
|
|
316
|
+
throw new PermissionConflictError(
|
|
317
|
+
body?.detail ?? 'Group membership was modified concurrently, please retry',
|
|
318
|
+
undefined,
|
|
319
|
+
body
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const errorText = await readBodyText(response);
|
|
324
|
+
throw new PermissionError(
|
|
325
|
+
`Failed to remove group member: ${response.status} ${response.statusText} - ${errorText}`,
|
|
326
|
+
response.status,
|
|
327
|
+
response.statusText
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async listUserGroups(
|
|
332
|
+
userId: string,
|
|
333
|
+
page: string | undefined = undefined,
|
|
334
|
+
limit = 500,
|
|
335
|
+
signal?: AbortSignal
|
|
336
|
+
): Promise<PaginatedResponse<Group>> {
|
|
337
|
+
const params = new URLSearchParams();
|
|
338
|
+
if (page !== undefined) params.set('page', page);
|
|
339
|
+
params.set('limit', limit.toString());
|
|
340
|
+
|
|
341
|
+
const url = `${this._usersUrl}/${encodeURIComponent(userId)}/groups?${params}`;
|
|
342
|
+
|
|
343
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, { signal });
|
|
344
|
+
|
|
345
|
+
if (response.ok) {
|
|
346
|
+
return (await response.json()) as PaginatedResponse<Group>;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (response.status === 404) {
|
|
350
|
+
const body = await readErrorBody(response);
|
|
351
|
+
throw new PermissionNotFoundError(body?.detail ?? `User '${userId}' not found`, body);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (response.status === 400) {
|
|
355
|
+
const body = await readErrorBody(response);
|
|
356
|
+
throw new PermissionValidationError(body?.detail ?? 'User group listing validation failed', body);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (response.status === 403) {
|
|
360
|
+
const body = await readErrorBody(response);
|
|
361
|
+
throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list user groups', body);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const errorText = await readBodyText(response);
|
|
365
|
+
throw new PermissionError(
|
|
366
|
+
`Failed to list user groups: ${response.status} ${response.statusText} - ${errorText}`,
|
|
367
|
+
response.status,
|
|
368
|
+
response.statusText
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
171
372
|
async deleteGroup(id: string, request: DeleteGroupRequest, signal?: AbortSignal): Promise<Group> {
|
|
172
373
|
const url = `${this._groupsUrl}/${encodeURIComponent(id)}`;
|
|
173
374
|
const response = await this._httpClient.fetch(HttpMethod.Delete, url, {
|
|
@@ -188,7 +389,8 @@ export class PermissionClient implements IPermissionClient {
|
|
|
188
389
|
let json: unknown;
|
|
189
390
|
try {
|
|
190
391
|
json = await response.json();
|
|
191
|
-
} catch {
|
|
392
|
+
} catch (err) {
|
|
393
|
+
if (err instanceof Error && err.name === 'AbortError') throw err;
|
|
192
394
|
throw new PermissionConflictError('Group was modified by another user', undefined);
|
|
193
395
|
}
|
|
194
396
|
const body = isErrorBody(json) ? json : undefined;
|
|
@@ -209,7 +411,7 @@ export class PermissionClient implements IPermissionClient {
|
|
|
209
411
|
throw new PermissionValidationError(body?.detail ?? 'Group deletion validation failed', body);
|
|
210
412
|
}
|
|
211
413
|
|
|
212
|
-
const errorText = await response
|
|
414
|
+
const errorText = await readBodyText(response);
|
|
213
415
|
throw new PermissionError(
|
|
214
416
|
`Failed to delete group: ${response.status} ${response.statusText} - ${errorText}`,
|
|
215
417
|
response.status,
|
package/src/types.ts
CHANGED
|
@@ -7,6 +7,14 @@ export interface Pagination {
|
|
|
7
7
|
export interface PaginatedResponse<T> {
|
|
8
8
|
data: T[];
|
|
9
9
|
pagination: Pagination;
|
|
10
|
+
/**
|
|
11
|
+
* The caller's effective permissions on the parent resource that governs every item in the
|
|
12
|
+
* page — populated by endpoints where the authorization subject is shared across all rows
|
|
13
|
+
* (currently `GET /groups/{id}/members`, where this reflects the caller's mask on the
|
|
14
|
+
* group's management scope). Absent for endpoints whose items each have their own
|
|
15
|
+
* authorization subject.
|
|
16
|
+
*/
|
|
17
|
+
callerPermissions?: PrincipalPermission[];
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
export interface User {
|
|
@@ -17,26 +25,54 @@ export interface User {
|
|
|
17
25
|
lastName: string | null;
|
|
18
26
|
}
|
|
19
27
|
|
|
28
|
+
/**
|
|
29
|
+
* A user returned from group-membership endpoints. Extends {@link User} with the target user's
|
|
30
|
+
* effective permissions on the containing group's management scope, reflecting the post-mutation
|
|
31
|
+
* state after an add/remove.
|
|
32
|
+
*/
|
|
33
|
+
export interface GroupMember extends User {
|
|
34
|
+
permissions: PrincipalPermission[];
|
|
35
|
+
}
|
|
36
|
+
|
|
20
37
|
export enum PrincipalType {
|
|
21
38
|
Unknown = 'Unknown',
|
|
22
39
|
DomainGroup = 'DomainGroup',
|
|
23
40
|
Group = 'Group'
|
|
24
41
|
}
|
|
25
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The permission bits a subject can hold on a group's management scope.
|
|
45
|
+
*
|
|
46
|
+
* - `View` — can see the group and its basic metadata (title, description, display name).
|
|
47
|
+
* - `Edit` — can create and modify groups.
|
|
48
|
+
* - `Delete` — can delete the group.
|
|
49
|
+
* - `ViewMembers` — can list the group's members.
|
|
50
|
+
* - `ManagePermissions` — can update permissions connected to the group (e.g. grant another user Edit on it).
|
|
51
|
+
*/
|
|
52
|
+
export type PrincipalPermission = 'View' | 'Edit' | 'Delete' | 'ViewMembers' | 'ManagePermissions';
|
|
53
|
+
|
|
26
54
|
export interface Group {
|
|
27
55
|
id: string;
|
|
28
56
|
ownerSystem: string;
|
|
29
57
|
principalType: PrincipalType;
|
|
30
58
|
title: string;
|
|
31
59
|
displayName: string | null;
|
|
60
|
+
description: string | null;
|
|
32
61
|
etag: string;
|
|
33
62
|
isEditable: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* The effective permission bits the relevant subject holds on this group's management scope.
|
|
65
|
+
* The subject is the caller on most endpoints, and the target user on `GET /users/{id}/groups`.
|
|
66
|
+
* An empty array means no permissions are granted.
|
|
67
|
+
*/
|
|
68
|
+
permissions: PrincipalPermission[];
|
|
34
69
|
}
|
|
35
70
|
|
|
36
71
|
export interface PutGroupRequest {
|
|
37
72
|
/** Title for the group. Used on create; ignored on update (title is immutable). */
|
|
38
73
|
title: string;
|
|
39
74
|
displayName?: string | null;
|
|
75
|
+
description?: string | null;
|
|
40
76
|
etag?: string | null;
|
|
41
77
|
}
|
|
42
78
|
|