@@ -2,13 +2,18 @@ import * as roles from "app/common/roles";
2
2
import { AclRule } from "app/gen-server/entity/AclRule" ;
3
3
import { Document } from "app/gen-server/entity/Document" ;
4
4
import { Group } from "app/gen-server/entity/Group" ;
5
- import { GroupDescriptor , NonGuestGroup , Resource } from "app/gen-server/lib/homedb/Interfaces" ;
5
+ import { GroupWithMembersDescriptor , NonGuestGroup ,
6
+ Resource , RoleGroupDescriptor , RunInTransaction } from "app/gen-server/lib/homedb/Interfaces" ;
6
7
import { Organization } from "app/gen-server/entity/Organization" ;
7
8
import { Permissions } from 'app/gen-server/lib/Permissions' ;
8
9
import { User } from "app/gen-server/entity/User" ;
9
10
import { Workspace } from "app/gen-server/entity/Workspace" ;
10
11
11
12
import { EntityManager } from "typeorm" ;
13
+ import { UsersManager } from "./UsersManager" ;
14
+ import { ApiError } from "app/common/ApiError" ;
15
+
16
+ export type GroupTypes = typeof Group . ROLE_TYPE | typeof Group . TEAM_TYPE ;
12
17
13
18
/**
14
19
* Class responsible for Groups and Roles Management.
@@ -18,18 +23,18 @@ import { EntityManager } from "typeorm";
18
23
*/
19
24
export class GroupsManager {
20
25
// All groups.
21
- public get defaultGroups ( ) : GroupDescriptor [ ] {
26
+ public get defaultGroups ( ) : RoleGroupDescriptor [ ] {
22
27
return this . _defaultGroups ;
23
28
}
24
29
25
30
// Groups whose permissions are inherited from parent resource to child resources.
26
- public get defaultBasicGroups ( ) : GroupDescriptor [ ] {
31
+ public get defaultBasicGroups ( ) : RoleGroupDescriptor [ ] {
27
32
return this . _defaultGroups
28
33
. filter ( _grpDesc => _grpDesc . nestParent ) ;
29
34
}
30
35
31
36
// Groups that are common to all resources.
32
- public get defaultCommonGroups ( ) : GroupDescriptor [ ] {
37
+ public get defaultCommonGroups ( ) : RoleGroupDescriptor [ ] {
33
38
return this . _defaultGroups
34
39
. filter ( _grpDesc => ! _grpDesc . orgOnly ) ;
35
40
}
@@ -87,7 +92,7 @@ export class GroupsManager {
87
92
* TODO: app/common/roles already contains an ordering of the default roles. Usage should
88
93
* be consolidated.
89
94
*/
90
- private readonly _defaultGroups : GroupDescriptor [ ] = [ {
95
+ private readonly _defaultGroups : RoleGroupDescriptor [ ] = [ {
91
96
name : roles . OWNER ,
92
97
permissions : Permissions . OWNER ,
93
98
nestParent : true
@@ -110,6 +115,8 @@ export class GroupsManager {
110
115
orgOnly : true
111
116
} ] ;
112
117
118
+ public constructor ( private _usersManager : UsersManager , private _runInTransaction : RunInTransaction ) { }
119
+
113
120
/**
114
121
* Helper for adjusting acl inheritance rules. Given an array of top-level groups from the
115
122
* resource of interest, and an array of inherited groups belonging to the parent resource,
@@ -207,8 +214,10 @@ export class GroupsManager {
207
214
this . defaultGroups . forEach ( groupProps => {
208
215
if ( ! groupProps . orgOnly || ! inherit ) {
209
216
// Skip this group if it's an org only group and the resource inherits from a parent.
210
- const group = new Group ( ) ;
211
- group . name = groupProps . name ;
217
+ const group = Group . create ( {
218
+ name : groupProps . name ,
219
+ type : Group . ROLE_TYPE ,
220
+ } ) ;
212
221
if ( inherit ) {
213
222
this . setInheritance ( group , inherit ) ;
214
223
}
@@ -268,4 +277,234 @@ export class GroupsManager {
268
277
}
269
278
return roles . getEffectiveRole ( maxInheritedRole ) ;
270
279
}
280
+
281
+ /**
282
+ * Create a Group.
283
+ * @param groupDescriptor - The descriptor for the group to be created.
284
+ * @param optManager - Optional EntityManager to use for the transaction.
285
+ * @returns The created Group.
286
+ */
287
+ public async createGroup ( groupDescriptor : GroupWithMembersDescriptor , optManager ?: EntityManager ) {
288
+ return await this . _runInTransaction ( optManager , async ( manager ) => {
289
+ if ( groupDescriptor . type === Group . TEAM_TYPE ) {
290
+ await this . _throwIfTeamNameCollision ( groupDescriptor . name , manager ) ;
291
+ }
292
+ const group = Group . create ( {
293
+ type : groupDescriptor . type ,
294
+ name : groupDescriptor . name ,
295
+ memberUsers : await this . _usersManager . getUsersByIdsStrict ( groupDescriptor . memberUsers ?? [ ] , manager ) ,
296
+ memberGroups : await this . _getGroupsByIdsStrict ( groupDescriptor . memberGroups ?? [ ] , manager ) ,
297
+ } ) ;
298
+ return await manager . save ( group ) ;
299
+ } ) ;
300
+ }
301
+
302
+ /**
303
+ * Overwrite a Role Group.
304
+ * @param id - The id of the Role Group to be overwritten.
305
+ * @param groupDescriptor - The descriptor to overwrite the role with.
306
+ * @param optManager - Optional EntityManager to use for the transaction.
307
+ *
308
+ * @returns The overwritten Role Group
309
+ */
310
+ public async overwriteRoleGroup (
311
+ id : number , groupDescriptor : GroupWithMembersDescriptor , optManager ?: EntityManager
312
+ ) {
313
+ return await this . _runInTransaction ( optManager , async ( manager ) => {
314
+ const existingGroup = await this . getGroupWithMembersById ( id , { } , manager ) ;
315
+ if ( ! existingGroup || ( existingGroup . type !== Group . ROLE_TYPE ) ) {
316
+ throw new ApiError ( `Role with id ${ id } not found` , 404 ) ;
317
+ }
318
+ return await this . _overwriteGroup ( existingGroup , groupDescriptor , manager ) ;
319
+ } ) ;
320
+ }
321
+
322
+ /**
323
+ * Overwrite a Team Group.
324
+ * @param id - The id of the Team Group to be overwritten.
325
+ * @param groupDescriptor - The descriptor to overwrite the role with.
326
+ * @param optManager - Optional EntityManager to use for the transaction.
327
+ *
328
+ * @returns The overwritten Team Group
329
+ */
330
+ public async overwriteTeamGroup (
331
+ id : number , groupDescriptor : GroupWithMembersDescriptor , optManager ?: EntityManager
332
+ ) {
333
+ return await this . _runInTransaction ( optManager , async ( manager ) => {
334
+ const existingGroup = await this . getGroupWithMembersById ( id , { } , manager ) ;
335
+ if ( ! existingGroup || ( existingGroup . type !== Group . TEAM_TYPE ) ) {
336
+ throw new ApiError ( `Group with id ${ id } not found` , 404 ) ;
337
+ }
338
+ await this . _throwIfTeamNameCollision ( groupDescriptor . name , manager , id ) ;
339
+ return await this . _overwriteGroup ( existingGroup , groupDescriptor , manager ) ;
340
+ } ) ;
341
+ }
342
+
343
+ /**
344
+ * Delete a Group.
345
+ *
346
+ * @param id - The id of the Group to be deleted.
347
+ * @param expectedType - The expected type of the Group to be deleted. If the type is specified,
348
+ * the Group will only be deleted if it has the expected type.
349
+ * If the type is not specified, the Group will be deleted regardless of its type.
350
+ * @param optManager - Optional EntityManager to use for the transaction.
351
+ */
352
+ public async deleteGroup ( id : number , expectedType ?: GroupTypes , optManager ?: EntityManager ) {
353
+ return await this . _runInTransaction ( optManager , async ( manager ) => {
354
+ const group = await this . getGroupWithMembersById ( id , { } , manager ) ;
355
+ if ( ! group || ( expectedType && expectedType !== group . type ) ) {
356
+ throw new ApiError ( `Group with id ${ id } not found` , 404 ) ;
357
+ }
358
+ await manager . createQueryBuilder ( )
359
+ . delete ( )
360
+ . from ( 'group_groups' )
361
+ . where ( 'subgroup_id = :id' , { id } )
362
+ . execute ( ) ;
363
+ await manager . remove ( group ) ;
364
+ } ) ;
365
+ }
366
+
367
+ /**
368
+ * Get all the groups with their members.
369
+ * @param optManager - Optional EntityManager to use for the transaction.
370
+ */
371
+ public getGroupsWithMembers ( optManager ?: EntityManager ) : Promise < Group [ ] > {
372
+ return this . _runInTransaction ( optManager , async ( manager : EntityManager ) => {
373
+ return this . _getGroupsQueryBuilder ( manager )
374
+ . getMany ( ) ;
375
+ } ) ;
376
+ }
377
+
378
+ /**
379
+ * Get all the groups with their members of the given type.
380
+ *
381
+ * @param type - The type of the groups to be fetched.
382
+ * @param opts - Optional options to be used for the query.
383
+ * @param opts.aclRule - Whether to include the aclRule in the query.
384
+ * @param optManager - Optional EntityManager to use for the transaction.
385
+ *
386
+ * @returns A Promise for an array of Group entities.
387
+ */
388
+ public getGroupsWithMembersByType (
389
+ type : GroupTypes , opts ?: { aclRule ?: boolean } , optManager ?: EntityManager
390
+ ) : Promise < Group [ ] > {
391
+ return this . _runInTransaction ( optManager , async ( manager : EntityManager ) => {
392
+ return this . _getGroupsQueryBuilder ( manager , opts )
393
+ . where ( 'groups.type = :type' , { type} )
394
+ . getMany ( ) ;
395
+ } ) ;
396
+ }
397
+
398
+ /**
399
+ * Get a Group with its members by id.
400
+ *
401
+ * @param id - The id of the Group to be fetched.
402
+ * @param opts - Optional options to be used for the query.
403
+ * @param opts.aclRule - Whether to include the aclRule in the query.
404
+ * @param optManager - Optional EntityManager to use for the transaction.
405
+ *
406
+ * @returns A Promise for the Group entity.
407
+ */
408
+ public async getGroupWithMembersById (
409
+ id : number , opts ?: { aclRule ?: boolean } , optManager ?: EntityManager
410
+ ) : Promise < Group | null > {
411
+ return await this . _runInTransaction ( optManager , async ( manager ) => {
412
+ return await this . _getGroupsQueryBuilder ( manager , opts )
413
+ . andWhere ( 'groups.id = :groupId' , { groupId : id } )
414
+ . getOne ( ) ;
415
+ } ) ;
416
+ }
417
+
418
+ /**
419
+ * Common method to overwrite groups of any type.
420
+ * @param existing - The existing group to be overwritten.
421
+ * @param groupDescriptor - The descriptor to overwrite the group with.
422
+ * @param optManager - The EntityManager to use for the transaction.
423
+ * @returns The overwritten Group.
424
+ */
425
+ private async _overwriteGroup (
426
+ existing : Group , groupDescriptor : GroupWithMembersDescriptor , optManager : EntityManager
427
+ ) {
428
+ if ( existing . type !== groupDescriptor . type ) {
429
+ throw new ApiError ( "cannot change type of group" , 400 ) ;
430
+ }
431
+ const updatedGroup = Group . create ( {
432
+ id : existing . id ,
433
+ type : groupDescriptor . type ,
434
+ name : groupDescriptor . name ,
435
+ memberUsers : await this . _usersManager . getUsersByIdsStrict ( groupDescriptor . memberUsers ?? [ ] , optManager ) ,
436
+ memberGroups : await this . _getGroupsByIdsStrict ( groupDescriptor . memberGroups ?? [ ] , optManager ) ,
437
+ } ) ;
438
+ return await optManager . save ( updatedGroup ) ;
439
+ }
440
+
441
+ /**
442
+ * Returns a Promise for an array of Groups for the given groupIds.
443
+ *
444
+ * @param groupIds - The ids of the Groups to be fetched.
445
+ * @param optManager - Optional EntityManager to use for the transaction.
446
+ * @returns A Promise for an array of Group entities.
447
+ */
448
+ private async _getGroupsByIds ( groupIds : number [ ] , optManager ?: EntityManager ) : Promise < Group [ ] > {
449
+ if ( groupIds . length === 0 ) {
450
+ return [ ] ;
451
+ }
452
+ return await this . _runInTransaction ( optManager , async ( manager ) => {
453
+ const queryBuilder = this . _getGroupsQueryBuilder ( manager )
454
+ . where ( 'groups.id IN (:...groupIds)' , { groupIds} ) ;
455
+ return await queryBuilder . getMany ( ) ;
456
+ } ) ;
457
+ }
458
+
459
+ /**
460
+ * Returns a Promise for an array of Groups for the given groupIds.
461
+ * Throws an ApiError if any of the groups are not found.
462
+ *
463
+ * @param groupIds - The ids of the Groups to be fetched.
464
+ * @param optManager - Optional EntityManager to use for the transaction.
465
+ */
466
+ private async _getGroupsByIdsStrict ( groupIds : number [ ] , optManager ?: EntityManager ) : Promise < Group [ ] > {
467
+ const groups = await this . _getGroupsByIds ( groupIds , optManager ) ;
468
+ if ( groups . length !== groupIds . length ) {
469
+ const foundGroupIds = new Set ( groups . map ( group => group . id ) ) ;
470
+ const missingGroupIds = groupIds . filter ( id => ! foundGroupIds . has ( id ) ) ;
471
+ throw new ApiError ( 'Groups not found: ' + missingGroupIds . join ( ', ' ) , 404 ) ;
472
+ }
473
+ return groups ;
474
+ }
475
+
476
+ /**
477
+ * Returns a QueryBuilder for fetching groups with their members.
478
+ * @param optManager - The EntityManager to use for the query.
479
+ * @param opts - Optional options to be used for the query.
480
+ * @param opts.aclRule - Whether to include the aclRule in the query.
481
+ * @returns The QueryBuilder for fetching groups with their members.
482
+ */
483
+ private _getGroupsQueryBuilder ( optManager : EntityManager , opts : { aclRule ?: boolean } = { } ) {
484
+ let queryBuilder = optManager . createQueryBuilder ( )
485
+ . select ( 'groups' )
486
+ . addSelect ( 'groups.type' )
487
+ . addSelect ( 'memberGroups.type' )
488
+ . from ( Group , 'groups' )
489
+ . leftJoinAndSelect ( 'groups.memberUsers' , 'memberUsers' )
490
+ . leftJoinAndSelect ( 'groups.memberGroups' , 'memberGroups' ) ;
491
+ if ( opts . aclRule ) {
492
+ queryBuilder = queryBuilder
493
+ . leftJoinAndSelect ( 'groups.aclRule' , 'aclRule' ) ;
494
+ }
495
+ return queryBuilder ;
496
+ }
497
+
498
+ private async _throwIfTeamNameCollision ( name : string , manager : EntityManager , existingId ?: number ) {
499
+ const query = this . _getGroupsQueryBuilder ( manager )
500
+ . where ( 'groups.name = :name' , { name} )
501
+ . andWhere ( 'groups.type = :type' , { type : Group . TEAM_TYPE } ) ;
502
+ if ( existingId !== undefined ) {
503
+ query . andWhere ( 'groups.id != :id' , { id : existingId } ) ;
504
+ }
505
+ const group = await query . getOne ( ) ;
506
+ if ( group ) {
507
+ throw new ApiError ( `Group with name "${ name } " already exists` , 409 ) ;
508
+ }
509
+ }
271
510
}
0 commit comments