diff --git a/lib/core/form/components/widgets/core/form-field.model.ts b/lib/core/form/components/widgets/core/form-field.model.ts index 65817c90ee..d6d4c24be5 100644 --- a/lib/core/form/components/widgets/core/form-field.model.ts +++ b/lib/core/form/components/widgets/core/form-field.model.ts @@ -75,6 +75,7 @@ export class FormFieldModel extends FormWidgetModel { selectionType: 'single' | 'multiple' = null; rule?: FormFieldRule; selectLoggedUser: boolean; + groupsRestriction: string[]; // container model members numberOfColumns: number = 1; @@ -183,6 +184,7 @@ export class FormFieldModel extends FormWidgetModel { this.selectionType = json.selectionType; this.rule = json.rule; this.selectLoggedUser = json.selectLoggedUser; + this.groupsRestriction = json.groupsRestriction?.groups; if (json.placeholder && json.placeholder !== '' && json.placeholder !== 'null') { this.placeholder = json.placeholder; diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.html index 6b1da8b26f..320167cd72 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.html @@ -12,6 +12,7 @@ (changedUsers)="onChangedUser($event)" [roles]="roles" [mode]="mode" + [groupsRestriction]="groupsRestriction" (blur)="markAsTouched()" [matTooltip]="field.tooltip" matTooltipPosition="above" diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.ts index af2a0260d2..2630cd5ab5 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/people/people-cloud.widget.ts @@ -51,6 +51,7 @@ export class PeopleCloudWidgetComponent extends WidgetComponent implements OnIni title: string; preSelectUsers: IdentityUserModel[]; search: FormControl; + groupsRestriction: string[]; constructor(formService: FormService, private identityUserService: IdentityUserService) { super(formService); @@ -62,6 +63,7 @@ export class PeopleCloudWidgetComponent extends WidgetComponent implements OnIni this.mode = this.field.optionType as ComponentSelectionMode; this.title = this.field.placeholder; this.preSelectUsers = this.field.value ? this.field.value : []; + this.groupsRestriction = this.field.groupsRestriction; } // eslint-disable-next-line @typescript-eslint/no-unused-expressions this.search = new FormControl({value: '', disabled: this.field.readOnly}, []), diff --git a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.spec.ts index d823a538b4..d99ec3981b 100644 --- a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.spec.ts @@ -26,7 +26,7 @@ import { } from '@alfresco/adf-core'; import { ProcessServiceCloudTestingModule } from '../../testing/process-service-cloud.testing.module'; import { of } from 'rxjs'; -import { mockUsers } from '../mock/user-cloud.mock'; +import { mockInvolvedGroups, mockOAuth2, mockPreselectedUsers, mockUsers } from '../mock/user-cloud.mock'; import { PeopleCloudModule } from '../people-cloud.module'; import { SimpleChange } from '@angular/core'; import { By } from '@angular/platform-browser'; @@ -39,25 +39,21 @@ describe('PeopleCloudComponent', () => { let identityService: IdentityUserService; let alfrescoApiService: AlfrescoApiService; let findUsersByNameSpy: jasmine.Spy; - - const mock: any = { - oauth2Auth: { - callCustomApi: () => Promise.resolve(mockUsers) - }, - isEcmLoggedIn: () => false, - reply: jasmine.createSpy('reply') - }; - - const mockPreselectedUsers = [ - { id: mockUsers[1].id, username: mockUsers[1].username }, - { id: mockUsers[2].id, username: mockUsers[2].username } - ]; + let getInvolvedGroupsSpy: jasmine.Spy; // eslint-disable-next-line prefer-arrow/prefer-arrow-functions function getElement(selector: string): T { return fixture.nativeElement.querySelector(selector); } + const typeInputValue = (value: string) => { + const input = getElement('input'); + input.focus(); + input.value = value; + input.dispatchEvent(new Event('keyup')); + input.dispatchEvent(new Event('input')); + }; + setupTestBed({ imports: [ TranslateModule.forRoot(), @@ -74,7 +70,7 @@ describe('PeopleCloudComponent', () => { identityService = TestBed.inject(IdentityUserService); alfrescoApiService = TestBed.inject(AlfrescoApiService); - spyOn(alfrescoApiService, 'getInstance').and.returnValue(mock); + spyOn(alfrescoApiService, 'getInstance').and.returnValue(mockOAuth2); }); it('should populate placeholder when title is present', async () => { @@ -760,7 +756,8 @@ describe('PeopleCloudComponent', () => { message: 'INVALID_PRESELECTED_USERS', users: [{ id: mockPreselectedUsers[0].id, - username: mockPreselectedUsers[0].username + username: mockPreselectedUsers[0].username, + readonly: mockPreselectedUsers[0].readonly }] }; component.warning.subscribe(warning => { @@ -801,11 +798,13 @@ describe('PeopleCloudComponent', () => { users: [ { id: mockPreselectedUsers[0].id, - username: mockPreselectedUsers[0].username + username: mockPreselectedUsers[0].username, + readonly: mockPreselectedUsers[0].readonly }, { id: mockPreselectedUsers[1].id, - username: mockPreselectedUsers[1].username + username: mockPreselectedUsers[1].username, + readonly: mockPreselectedUsers[1].readonly } ] }; @@ -824,6 +823,75 @@ describe('PeopleCloudComponent', () => { }); }); + describe('Groups restriction', () => { + + beforeEach(() => { + fixture.detectChanges(); + findUsersByNameSpy = spyOn(identityService, 'findUsersByName').and.returnValue(of(mockUsers)); + }); + + it('Shoud display all users if groups restriction is empty', async () => { + getInvolvedGroupsSpy = spyOn(identityService, 'getInvolvedGroups').and.returnValue(of(mockInvolvedGroups)); + component.groupsRestriction = []; + typeInputValue('M'); + + await fixture.whenStable(); + fixture.detectChanges(); + + expect(getInvolvedGroupsSpy).toHaveBeenCalledTimes(0); + expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-people-cloud-row"]')).length).toEqual(mockUsers.length); + }); + + it('Should display users that belongs to restricted groups', async () => { + getInvolvedGroupsSpy = spyOn(identityService, 'getInvolvedGroups').and.returnValue(of(mockInvolvedGroups)); + component.groupsRestriction = [mockInvolvedGroups[0].name, mockInvolvedGroups[1].name]; + typeInputValue('M'); + + await fixture.whenStable(); + fixture.detectChanges(); + + expect(getInvolvedGroupsSpy).toHaveBeenCalledTimes(3); + expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-people-cloud-row"]')).length).toEqual(mockUsers.length); + }); + + it('Should not display users that not belongs to restricted groups', async () => { + getInvolvedGroupsSpy = spyOn(identityService, 'getInvolvedGroups').and.returnValue(of([mockInvolvedGroups[0]])); + component.groupsRestriction = [mockInvolvedGroups[0].name, mockInvolvedGroups[1].name]; + typeInputValue('M'); + + await fixture.whenStable(); + fixture.detectChanges(); + expect(getInvolvedGroupsSpy).toHaveBeenCalledTimes(3); + expect(fixture.debugElement.queryAll(By.css('[data-automation-id="adf-people-cloud-row"]')).length).toEqual(0); + }); + + it('Should mark as invalid preselected user if is not belongs to restricted groups', (done) => { + spyOn(identityService, 'findUserById').and.returnValue(of(mockPreselectedUsers[0])); + getInvolvedGroupsSpy = spyOn(identityService, 'getInvolvedGroups').and.returnValue(of([mockInvolvedGroups[0]])); + + const expectedWarning = { + message: 'INVALID_PRESELECTED_USERS', + users: [{ + id: mockPreselectedUsers[0].id, + username: mockPreselectedUsers[0].username, + readonly: mockPreselectedUsers[0].readonly + }] + }; + component.warning.subscribe(warning => { + expect(warning).toEqual(expectedWarning); + done(); + }); + + component.groupsRestriction = [mockInvolvedGroups[0].name, mockInvolvedGroups[1].name]; + component.mode = 'single'; + component.validate = true; + component.preSelectUsers = [mockPreselectedUsers[0]]; + component.ngOnChanges({ + preSelectUsers: new SimpleChange(null, [mockPreselectedUsers[0]], false) + }); + }); + }); + it('should removeDuplicateUsers return only unique users', () => { const duplicatedUsers = [{ id: mockUsers[0].id }, { id: mockUsers[0].id }]; expect(component.removeDuplicatedUsers(duplicatedUsers)).toEqual([{ id: mockUsers[0].id }]); diff --git a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts index f2779cb962..d1c810213f 100644 --- a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.ts @@ -33,6 +33,7 @@ import { Observable, of, BehaviorSubject, Subject } from 'rxjs'; import { switchMap, debounceTime, distinctUntilChanged, mergeMap, tap, filter, map, takeUntil } from 'rxjs/operators'; import { FullNamePipe, + IdentityGroupModel, IdentityUserModel, IdentityUserService, LogService @@ -103,6 +104,12 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy { @Input() excludedUsers: IdentityUserModel[] = []; + /** Array of groups to restrict user searches. + * Mandatory property is group id + */ + @Input() + groupsRestriction: string[] = []; + /** FormControl to list of users */ @Input() userChipsCtrl: FormControl = new FormControl({ value: '', disabled: false }); @@ -216,13 +223,15 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy { this.resetSearchUsers(); }), switchMap((search) => - this.identityUserService.findUsersByName(search.trim())), + this.findUsers(search) + ), mergeMap((users) => { this.resetSearchUsers(); this.searchLoading = false; return users; }), filter(user => !this.isUserAlreadySelected(user) && !this.isExcludedUser(user)), + mergeMap(user => this.filterUsersByGroupsRestriction(user)), mergeMap(user => { if (this.appName) { return this.checkUserHasAccess(user.id).pipe( @@ -275,6 +284,19 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy { map((filteredUser: { hasRole: boolean; user: IdentityUserModel }) => filteredUser.user)); } + private filterUsersByGroupsRestriction(user: IdentityUserModel): Observable { + if (this.groupsRestriction?.length) { + return this.isUserPartOfAllRestrictedGroups(user).pipe( + mergeMap(isPartOfAllGroups => isPartOfAllGroups ? of(user) : of()) + ); + } + return of(user); + } + + private findUsers(search: string): Observable { + return this.identityUserService.findUsersByName(search.trim()); + } + private isUserAlreadySelected(searchUser: IdentityUserModel): boolean { if (this.selectedUsers && this.selectedUsers.length > 0) { const result = this.selectedUsers.find((selectedUser) => this.compare(selectedUser, searchUser)); @@ -325,7 +347,17 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy { if (this.compare(user, validationResult)) { validationResult.readonly = user.readonly; - validUsers.push(validationResult); + if (this.groupsRestriction?.length) { + const isUserPartOfAllRestrictedGroups = await this.isUserPartOfAllRestrictedGroups(validationResult).toPromise(); + + if (isUserPartOfAllRestrictedGroups) { + validUsers.push(user); + } else { + this.invalidUsers.push(user); + } + } else { + validUsers.push(validationResult); + } } else { this.invalidUsers.push(user); } @@ -529,6 +561,20 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy { this.searchUsers$.next(this._searchUsers); } + private isUserPartOfAllRestrictedGroups(user: IdentityUserModel): Observable { + return this.getUserGroups(user.id).pipe( + map(userGroups => userGroups.filter( + restrictedGroup => userGroups.includes(restrictedGroup) + ).length >= this.groupsRestriction.length) + ); + } + + private getUserGroups(userId: string): Observable { + return this.identityUserService.getInvolvedGroups(userId).pipe( + map(groups => groups.map(({id, name}) => ({id, name}))) + ); + } + getSelectedUsers(): IdentityUserModel[] { return this.selectedUsers; } diff --git a/lib/process-services-cloud/src/lib/people/mock/user-cloud.mock.ts b/lib/process-services-cloud/src/lib/people/mock/user-cloud.mock.ts index b6d4148869..6a9c5535c5 100644 --- a/lib/process-services-cloud/src/lib/people/mock/user-cloud.mock.ts +++ b/lib/process-services-cloud/src/lib/people/mock/user-cloud.mock.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import { IdentityGroupModel } from '@alfresco/adf-core'; + export const mockUsers = [ { id: 'fake-id-1', username: 'first-name-1 last-name-1', firstName: 'first-name-1', lastName: 'last-name-1', email: 'abc@xyz.com' }, { id: 'fake-id-2', username: 'first-name-2 last-name-2', firstName: 'first-name-2', lastName: 'last-name-2', email: 'abcd@xyz.com'}, @@ -32,3 +34,21 @@ export const mockRoles = [ { id: 'id-4', name: 'MOCK-ROLE-1' }, { id: 'id-5', name: 'MOCK-ROLE-2'} ]; + +export const mockOAuth2: any = { + oauth2Auth: { + callCustomApi: () => Promise.resolve(mockUsers) + }, + isEcmLoggedIn: () => false, + reply: jasmine.createSpy('reply') +}; + +export const mockPreselectedUsers = [ + { id: mockUsers[1].id, username: mockUsers[1].username, readonly: false }, + { id: mockUsers[2].id, username: mockUsers[2].username, readonly: false } +]; + +export const mockInvolvedGroups = [ + { id: 'mock-group-id-1', name: 'Mock Group 1', path: '/mock', subGroups: [] } as IdentityGroupModel, + { id: 'mock-group-id-2', name: 'Mock Group 2', path: '', subGroups: [] } as IdentityGroupModel +];