[AAE-8061] Add groups restriction to people widget (#7586)

* [AAE-8061] add groups restriction to people widget

* Trigger travis

* [AAE-8061] fix lint

* [AAE-8061] fix unit tests
This commit is contained in:
Tomasz Gnyp
2022-04-20 15:51:32 +02:00
committed by GitHub
parent 6963bef092
commit 694d71a103
6 changed files with 159 additions and 20 deletions

View File

@@ -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;

View File

@@ -12,6 +12,7 @@
(changedUsers)="onChangedUser($event)"
[roles]="roles"
[mode]="mode"
[groupsRestriction]="groupsRestriction"
(blur)="markAsTouched()"
[matTooltip]="field.tooltip"
matTooltipPosition="above"

View File

@@ -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}, []),

View File

@@ -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<T = HTMLElement>(selector: string): T {
return fixture.nativeElement.querySelector(selector);
}
const typeInputValue = (value: string) => {
const input = getElement<HTMLInputElement>('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 }]);

View File

@@ -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<IdentityUserModel> {
if (this.groupsRestriction?.length) {
return this.isUserPartOfAllRestrictedGroups(user).pipe(
mergeMap(isPartOfAllGroups => isPartOfAllGroups ? of(user) : of())
);
}
return of(user);
}
private findUsers(search: string): Observable<IdentityUserModel[]> {
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<boolean> {
return this.getUserGroups(user.id).pipe(
map(userGroups => userGroups.filter(
restrictedGroup => userGroups.includes(restrictedGroup)
).length >= this.groupsRestriction.length)
);
}
private getUserGroups(userId: string): Observable<IdentityGroupModel[]> {
return this.identityUserService.getInvolvedGroups(userId).pipe(
map(groups => groups.map(({id, name}) => ({id, name})))
);
}
getSelectedUsers(): IdentityUserModel[] {
return this.selectedUsers;
}

View File

@@ -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
];