@twinfinity/permission 6.0.1 → 6.0.2
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/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/BuildInfo.ts +28 -28
- package/src/PermissionClient.ts +421 -421
- package/src/errors.ts +55 -55
- package/src/index.ts +3 -3
- package/src/types.ts +87 -87
package/src/PermissionClient.ts
CHANGED
|
@@ -1,421 +1,421 @@
|
|
|
1
|
-
import { TwinfinityHttpClient, HttpMethod } from '@twinfinity/authentication';
|
|
2
|
-
import type {
|
|
3
|
-
PaginatedResponse,
|
|
4
|
-
User,
|
|
5
|
-
Group,
|
|
6
|
-
GroupMember,
|
|
7
|
-
PutGroupRequest,
|
|
8
|
-
DeleteGroupRequest,
|
|
9
|
-
PermissionErrorBody
|
|
10
|
-
} from './types';
|
|
11
|
-
import {
|
|
12
|
-
PermissionValidationError,
|
|
13
|
-
PermissionError,
|
|
14
|
-
PermissionForbiddenError,
|
|
15
|
-
PermissionConflictError,
|
|
16
|
-
PermissionNotFoundError
|
|
17
|
-
} from './errors';
|
|
18
|
-
|
|
19
|
-
export interface IPermissionClient {
|
|
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>>;
|
|
22
|
-
putGroup(id: string, request: PutGroupRequest, signal?: AbortSignal): Promise<Group>;
|
|
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>>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function isErrorBody(value: unknown): value is PermissionErrorBody {
|
|
41
|
-
return typeof value === 'object' && value !== null && 'detail' in value && 'cause' in value;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function readErrorBody(response: Response): Promise<PermissionErrorBody | undefined> {
|
|
45
|
-
try {
|
|
46
|
-
const json = await response.json();
|
|
47
|
-
return isErrorBody(json) ? json : undefined;
|
|
48
|
-
} catch (err) {
|
|
49
|
-
if (err instanceof Error && err.name === 'AbortError') throw err;
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
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
|
-
|
|
63
|
-
export class PermissionClient implements IPermissionClient {
|
|
64
|
-
private readonly _usersUrl: string;
|
|
65
|
-
private readonly _groupsUrl: string;
|
|
66
|
-
|
|
67
|
-
constructor(
|
|
68
|
-
baseUrl: string | URL,
|
|
69
|
-
private readonly _httpClient: TwinfinityHttpClient
|
|
70
|
-
) {
|
|
71
|
-
const base = String(baseUrl).replace(/\/+$/, '');
|
|
72
|
-
this._usersUrl = `${base}/_ps/users`;
|
|
73
|
-
this._groupsUrl = `${base}/_ps/groups`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async listUsers(
|
|
77
|
-
page: string | undefined = undefined,
|
|
78
|
-
limit = 500,
|
|
79
|
-
q?: string,
|
|
80
|
-
signal?: AbortSignal
|
|
81
|
-
): Promise<PaginatedResponse<User>> {
|
|
82
|
-
const params = new URLSearchParams();
|
|
83
|
-
if (page !== undefined) params.set('page', page);
|
|
84
|
-
params.set('limit', limit.toString());
|
|
85
|
-
if (q) params.set('q', q);
|
|
86
|
-
|
|
87
|
-
const url = `${this._usersUrl}?${params}`;
|
|
88
|
-
|
|
89
|
-
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
90
|
-
signal
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
if (response.ok) {
|
|
94
|
-
return (await response.json()) as PaginatedResponse<User>;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (response.status === 400) {
|
|
98
|
-
const body = await readErrorBody(response);
|
|
99
|
-
throw new PermissionValidationError(body?.detail ?? 'User listing validation failed', body);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (response.status === 403) {
|
|
103
|
-
const body = await readErrorBody(response);
|
|
104
|
-
throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list users', body);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const errorText = await readBodyText(response);
|
|
108
|
-
throw new PermissionError(
|
|
109
|
-
`Failed to list users: ${response.status} ${response.statusText} - ${errorText}`,
|
|
110
|
-
response.status,
|
|
111
|
-
response.statusText
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async listGroups(
|
|
116
|
-
page: string | undefined = undefined,
|
|
117
|
-
limit = 500,
|
|
118
|
-
q?: string,
|
|
119
|
-
signal?: AbortSignal
|
|
120
|
-
): Promise<PaginatedResponse<Group>> {
|
|
121
|
-
const params = new URLSearchParams();
|
|
122
|
-
if (page !== undefined) params.set('page', page);
|
|
123
|
-
params.set('limit', limit.toString());
|
|
124
|
-
if (q) params.set('q', q);
|
|
125
|
-
|
|
126
|
-
const url = `${this._groupsUrl}?${params}`;
|
|
127
|
-
|
|
128
|
-
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
129
|
-
signal
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
if (response.ok) {
|
|
133
|
-
return (await response.json()) as PaginatedResponse<Group>;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (response.status === 400) {
|
|
137
|
-
const body = await readErrorBody(response);
|
|
138
|
-
throw new PermissionValidationError(body?.detail ?? 'Group listing validation failed', body);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (response.status === 403) {
|
|
142
|
-
const body = await readErrorBody(response);
|
|
143
|
-
throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list groups', body);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const errorText = await readBodyText(response);
|
|
147
|
-
throw new PermissionError(
|
|
148
|
-
`Failed to list groups: ${response.status} ${response.statusText} - ${errorText}`,
|
|
149
|
-
response.status,
|
|
150
|
-
response.statusText
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async putGroup(id: string, request: PutGroupRequest, signal?: AbortSignal): Promise<Group> {
|
|
155
|
-
const url = `${this._groupsUrl}/${encodeURIComponent(id)}`;
|
|
156
|
-
const response = await this._httpClient.fetch(HttpMethod.Put, url, {
|
|
157
|
-
headers: { 'Content-Type': 'application/json' },
|
|
158
|
-
body: JSON.stringify(request),
|
|
159
|
-
signal
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
if (response.ok) {
|
|
163
|
-
return (await response.json()) as Group;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (response.status === 404) {
|
|
167
|
-
const body = await readErrorBody(response);
|
|
168
|
-
throw new PermissionNotFoundError(body?.detail ?? `Group '${id}' not found`, body);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (response.status === 409) {
|
|
172
|
-
let json: unknown;
|
|
173
|
-
try {
|
|
174
|
-
json = await response.json();
|
|
175
|
-
} catch (err) {
|
|
176
|
-
if (err instanceof Error && err.name === 'AbortError') throw err;
|
|
177
|
-
throw new PermissionConflictError('Group conflict', undefined);
|
|
178
|
-
}
|
|
179
|
-
const body = isErrorBody(json) ? json : undefined;
|
|
180
|
-
const data =
|
|
181
|
-
body !== undefined && typeof json === 'object' && json !== null && 'currentState' in json
|
|
182
|
-
? (json as { currentState: Group }).currentState
|
|
183
|
-
: undefined;
|
|
184
|
-
throw new PermissionConflictError(body?.detail ?? 'Group conflict', data, body);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (response.status === 403) {
|
|
188
|
-
const body = await readErrorBody(response);
|
|
189
|
-
throw new PermissionForbiddenError(
|
|
190
|
-
body?.detail ?? 'Insufficient permissions to create or update group',
|
|
191
|
-
body
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (response.status === 400) {
|
|
196
|
-
const body = await readErrorBody(response);
|
|
197
|
-
throw new PermissionValidationError(body?.detail ?? 'Group validation failed', body);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const errorText = await readBodyText(response);
|
|
201
|
-
throw new PermissionError(
|
|
202
|
-
`Failed to put group: ${response.status} ${response.statusText} - ${errorText}`,
|
|
203
|
-
response.status,
|
|
204
|
-
response.statusText
|
|
205
|
-
);
|
|
206
|
-
}
|
|
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
|
-
|
|
372
|
-
async deleteGroup(id: string, request: DeleteGroupRequest, signal?: AbortSignal): Promise<Group> {
|
|
373
|
-
const url = `${this._groupsUrl}/${encodeURIComponent(id)}`;
|
|
374
|
-
const response = await this._httpClient.fetch(HttpMethod.Delete, url, {
|
|
375
|
-
headers: { 'If-Match': request.etag },
|
|
376
|
-
signal
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
if (response.ok) {
|
|
380
|
-
return (await response.json()) as Group;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (response.status === 404) {
|
|
384
|
-
const body = await readErrorBody(response);
|
|
385
|
-
throw new PermissionNotFoundError(body?.detail ?? `Group '${id}' not found`, body);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (response.status === 409) {
|
|
389
|
-
let json: unknown;
|
|
390
|
-
try {
|
|
391
|
-
json = await response.json();
|
|
392
|
-
} catch (err) {
|
|
393
|
-
if (err instanceof Error && err.name === 'AbortError') throw err;
|
|
394
|
-
throw new PermissionConflictError('Group was modified by another user', undefined);
|
|
395
|
-
}
|
|
396
|
-
const body = isErrorBody(json) ? json : undefined;
|
|
397
|
-
const data =
|
|
398
|
-
body !== undefined && typeof json === 'object' && json !== null && 'currentState' in json
|
|
399
|
-
? (json as { currentState: Group }).currentState
|
|
400
|
-
: undefined;
|
|
401
|
-
throw new PermissionConflictError(body?.detail ?? 'Group was modified by another user', data, body);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (response.status === 403) {
|
|
405
|
-
const body = await readErrorBody(response);
|
|
406
|
-
throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to delete group', body);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (response.status === 400) {
|
|
410
|
-
const body = await readErrorBody(response);
|
|
411
|
-
throw new PermissionValidationError(body?.detail ?? 'Group deletion validation failed', body);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const errorText = await readBodyText(response);
|
|
415
|
-
throw new PermissionError(
|
|
416
|
-
`Failed to delete group: ${response.status} ${response.statusText} - ${errorText}`,
|
|
417
|
-
response.status,
|
|
418
|
-
response.statusText
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
1
|
+
import { TwinfinityHttpClient, HttpMethod } from '@twinfinity/authentication';
|
|
2
|
+
import type {
|
|
3
|
+
PaginatedResponse,
|
|
4
|
+
User,
|
|
5
|
+
Group,
|
|
6
|
+
GroupMember,
|
|
7
|
+
PutGroupRequest,
|
|
8
|
+
DeleteGroupRequest,
|
|
9
|
+
PermissionErrorBody
|
|
10
|
+
} from './types';
|
|
11
|
+
import {
|
|
12
|
+
PermissionValidationError,
|
|
13
|
+
PermissionError,
|
|
14
|
+
PermissionForbiddenError,
|
|
15
|
+
PermissionConflictError,
|
|
16
|
+
PermissionNotFoundError
|
|
17
|
+
} from './errors';
|
|
18
|
+
|
|
19
|
+
export interface IPermissionClient {
|
|
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>>;
|
|
22
|
+
putGroup(id: string, request: PutGroupRequest, signal?: AbortSignal): Promise<Group>;
|
|
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>>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isErrorBody(value: unknown): value is PermissionErrorBody {
|
|
41
|
+
return typeof value === 'object' && value !== null && 'detail' in value && 'cause' in value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function readErrorBody(response: Response): Promise<PermissionErrorBody | undefined> {
|
|
45
|
+
try {
|
|
46
|
+
const json = await response.json();
|
|
47
|
+
return isErrorBody(json) ? json : undefined;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err instanceof Error && err.name === 'AbortError') throw err;
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
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
|
+
|
|
63
|
+
export class PermissionClient implements IPermissionClient {
|
|
64
|
+
private readonly _usersUrl: string;
|
|
65
|
+
private readonly _groupsUrl: string;
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
baseUrl: string | URL,
|
|
69
|
+
private readonly _httpClient: TwinfinityHttpClient
|
|
70
|
+
) {
|
|
71
|
+
const base = String(baseUrl).replace(/\/+$/, '');
|
|
72
|
+
this._usersUrl = `${base}/_ps/users`;
|
|
73
|
+
this._groupsUrl = `${base}/_ps/groups`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async listUsers(
|
|
77
|
+
page: string | undefined = undefined,
|
|
78
|
+
limit = 500,
|
|
79
|
+
q?: string,
|
|
80
|
+
signal?: AbortSignal
|
|
81
|
+
): Promise<PaginatedResponse<User>> {
|
|
82
|
+
const params = new URLSearchParams();
|
|
83
|
+
if (page !== undefined) params.set('page', page);
|
|
84
|
+
params.set('limit', limit.toString());
|
|
85
|
+
if (q) params.set('q', q);
|
|
86
|
+
|
|
87
|
+
const url = `${this._usersUrl}?${params}`;
|
|
88
|
+
|
|
89
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
90
|
+
signal
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (response.ok) {
|
|
94
|
+
return (await response.json()) as PaginatedResponse<User>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (response.status === 400) {
|
|
98
|
+
const body = await readErrorBody(response);
|
|
99
|
+
throw new PermissionValidationError(body?.detail ?? 'User listing validation failed', body);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (response.status === 403) {
|
|
103
|
+
const body = await readErrorBody(response);
|
|
104
|
+
throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list users', body);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const errorText = await readBodyText(response);
|
|
108
|
+
throw new PermissionError(
|
|
109
|
+
`Failed to list users: ${response.status} ${response.statusText} - ${errorText}`,
|
|
110
|
+
response.status,
|
|
111
|
+
response.statusText
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async listGroups(
|
|
116
|
+
page: string | undefined = undefined,
|
|
117
|
+
limit = 500,
|
|
118
|
+
q?: string,
|
|
119
|
+
signal?: AbortSignal
|
|
120
|
+
): Promise<PaginatedResponse<Group>> {
|
|
121
|
+
const params = new URLSearchParams();
|
|
122
|
+
if (page !== undefined) params.set('page', page);
|
|
123
|
+
params.set('limit', limit.toString());
|
|
124
|
+
if (q) params.set('q', q);
|
|
125
|
+
|
|
126
|
+
const url = `${this._groupsUrl}?${params}`;
|
|
127
|
+
|
|
128
|
+
const response = await this._httpClient.fetch(HttpMethod.Get, url, {
|
|
129
|
+
signal
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (response.ok) {
|
|
133
|
+
return (await response.json()) as PaginatedResponse<Group>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (response.status === 400) {
|
|
137
|
+
const body = await readErrorBody(response);
|
|
138
|
+
throw new PermissionValidationError(body?.detail ?? 'Group listing validation failed', body);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (response.status === 403) {
|
|
142
|
+
const body = await readErrorBody(response);
|
|
143
|
+
throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to list groups', body);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const errorText = await readBodyText(response);
|
|
147
|
+
throw new PermissionError(
|
|
148
|
+
`Failed to list groups: ${response.status} ${response.statusText} - ${errorText}`,
|
|
149
|
+
response.status,
|
|
150
|
+
response.statusText
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async putGroup(id: string, request: PutGroupRequest, signal?: AbortSignal): Promise<Group> {
|
|
155
|
+
const url = `${this._groupsUrl}/${encodeURIComponent(id)}`;
|
|
156
|
+
const response = await this._httpClient.fetch(HttpMethod.Put, url, {
|
|
157
|
+
headers: { 'Content-Type': 'application/json' },
|
|
158
|
+
body: JSON.stringify(request),
|
|
159
|
+
signal
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (response.ok) {
|
|
163
|
+
return (await response.json()) as Group;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (response.status === 404) {
|
|
167
|
+
const body = await readErrorBody(response);
|
|
168
|
+
throw new PermissionNotFoundError(body?.detail ?? `Group '${id}' not found`, body);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (response.status === 409) {
|
|
172
|
+
let json: unknown;
|
|
173
|
+
try {
|
|
174
|
+
json = await response.json();
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (err instanceof Error && err.name === 'AbortError') throw err;
|
|
177
|
+
throw new PermissionConflictError('Group conflict', undefined);
|
|
178
|
+
}
|
|
179
|
+
const body = isErrorBody(json) ? json : undefined;
|
|
180
|
+
const data =
|
|
181
|
+
body !== undefined && typeof json === 'object' && json !== null && 'currentState' in json
|
|
182
|
+
? (json as { currentState: Group }).currentState
|
|
183
|
+
: undefined;
|
|
184
|
+
throw new PermissionConflictError(body?.detail ?? 'Group conflict', data, body);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (response.status === 403) {
|
|
188
|
+
const body = await readErrorBody(response);
|
|
189
|
+
throw new PermissionForbiddenError(
|
|
190
|
+
body?.detail ?? 'Insufficient permissions to create or update group',
|
|
191
|
+
body
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (response.status === 400) {
|
|
196
|
+
const body = await readErrorBody(response);
|
|
197
|
+
throw new PermissionValidationError(body?.detail ?? 'Group validation failed', body);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const errorText = await readBodyText(response);
|
|
201
|
+
throw new PermissionError(
|
|
202
|
+
`Failed to put group: ${response.status} ${response.statusText} - ${errorText}`,
|
|
203
|
+
response.status,
|
|
204
|
+
response.statusText
|
|
205
|
+
);
|
|
206
|
+
}
|
|
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
|
+
|
|
372
|
+
async deleteGroup(id: string, request: DeleteGroupRequest, signal?: AbortSignal): Promise<Group> {
|
|
373
|
+
const url = `${this._groupsUrl}/${encodeURIComponent(id)}`;
|
|
374
|
+
const response = await this._httpClient.fetch(HttpMethod.Delete, url, {
|
|
375
|
+
headers: { 'If-Match': request.etag },
|
|
376
|
+
signal
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
if (response.ok) {
|
|
380
|
+
return (await response.json()) as Group;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (response.status === 404) {
|
|
384
|
+
const body = await readErrorBody(response);
|
|
385
|
+
throw new PermissionNotFoundError(body?.detail ?? `Group '${id}' not found`, body);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (response.status === 409) {
|
|
389
|
+
let json: unknown;
|
|
390
|
+
try {
|
|
391
|
+
json = await response.json();
|
|
392
|
+
} catch (err) {
|
|
393
|
+
if (err instanceof Error && err.name === 'AbortError') throw err;
|
|
394
|
+
throw new PermissionConflictError('Group was modified by another user', undefined);
|
|
395
|
+
}
|
|
396
|
+
const body = isErrorBody(json) ? json : undefined;
|
|
397
|
+
const data =
|
|
398
|
+
body !== undefined && typeof json === 'object' && json !== null && 'currentState' in json
|
|
399
|
+
? (json as { currentState: Group }).currentState
|
|
400
|
+
: undefined;
|
|
401
|
+
throw new PermissionConflictError(body?.detail ?? 'Group was modified by another user', data, body);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (response.status === 403) {
|
|
405
|
+
const body = await readErrorBody(response);
|
|
406
|
+
throw new PermissionForbiddenError(body?.detail ?? 'Insufficient permissions to delete group', body);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (response.status === 400) {
|
|
410
|
+
const body = await readErrorBody(response);
|
|
411
|
+
throw new PermissionValidationError(body?.detail ?? 'Group deletion validation failed', body);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const errorText = await readBodyText(response);
|
|
415
|
+
throw new PermissionError(
|
|
416
|
+
`Failed to delete group: ${response.status} ${response.statusText} - ${errorText}`,
|
|
417
|
+
response.status,
|
|
418
|
+
response.statusText
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
}
|