mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[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:
@@ -12,6 +12,7 @@
|
||||
(changedUsers)="onChangedUser($event)"
|
||||
[roles]="roles"
|
||||
[mode]="mode"
|
||||
[groupsRestriction]="groupsRestriction"
|
||||
(blur)="markAsTouched()"
|
||||
[matTooltip]="field.tooltip"
|
||||
matTooltipPosition="above"
|
||||
|
@@ -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}, []),
|
||||
|
@@ -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 }]);
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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
|
||||
];
|
||||
|
Reference in New Issue
Block a user