diff --git a/demo-shell/src/app/components/cloud/people-groups-cloud-demo.component.ts b/demo-shell/src/app/components/cloud/people-groups-cloud-demo.component.ts index 60bf38f6d9..d353e2079d 100644 --- a/demo-shell/src/app/components/cloud/people-groups-cloud-demo.component.ts +++ b/demo-shell/src/app/components/cloud/people-groups-cloud-demo.component.ts @@ -16,8 +16,9 @@ */ import { Component, ViewEncapsulation } from '@angular/core'; -import { PeopleCloudComponent, GroupCloudComponent, GroupModel } from '@alfresco/adf-process-services-cloud'; +import { PeopleCloudComponent, GroupCloudComponent } from '@alfresco/adf-process-services-cloud'; import { MatRadioChange, MatCheckboxChange } from '@angular/material'; +import { IdentityGroupModel } from '@alfresco/adf-core'; @Component({ selector: 'app-people-groups-cloud', @@ -39,8 +40,8 @@ export class PeopleGroupCloudDemoComponent { peoplePreselectValidation: Boolean = false; groupMode: string = GroupCloudComponent.MODE_SINGLE; - preSelectGroup: GroupModel[] = []; - selectedGroupList: GroupModel[] = []; + preSelectGroup: IdentityGroupModel[] = []; + selectedGroupList: IdentityGroupModel[] = []; groupRoles: string[]; groupAppName: string; groupFilterMode: string = this.DEFAULT_FILTER_MODE; @@ -144,11 +145,11 @@ export class PeopleGroupCloudDemoComponent { return this.groupMode === GroupCloudComponent.MODE_MULTIPLE; } - onRemoveGroup(group: GroupModel) { + onRemoveGroup(group: IdentityGroupModel) { this.preSelectGroup = this.preSelectGroup.filter((value: any) => value.id !== group.id); } - onSelectGroup(group: GroupModel) { + onSelectGroup(group: IdentityGroupModel) { if (this.groupMode === GroupCloudComponent.MODE_MULTIPLE) { this.preSelectGroup.push(group); } diff --git a/docs/core/services/identity-group.service.md b/docs/core/services/identity-group.service.md new file mode 100644 index 0000000000..29e7622a26 --- /dev/null +++ b/docs/core/services/identity-group.service.md @@ -0,0 +1,75 @@ +--- +Title: Identity Group service +Added: v3.4.0 +Status: Active +Last reviewed: 2019-07-13 +--- + +# [Identity Group service](../../../lib/core/userinfo/services/identity-group.service.ts "Defined in identity-group.service.ts") + +Performs CRUD operations on identity groups. + +## Class members + +### Methods + +- **checkGroupHasAnyClientAppRole**(groupId: `string`, clientId: `string`, roleNames: `string[]`): [`Observable`](http://reactivex.io/documentation/observable.html)``
+ Check if a group has any of the client app roles in the supplied list. + - _groupId:_ `string` - Id of the target group + - _clientId:_ `string` - Id of the client + - _roleNames:_ `string[]` - Array of role names to check + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`` - True if the group has one or more of the roles, false otherwise +- **checkGroupHasClientApp**(groupId: `string`, clientId: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)``
+ Checks if a group has a client app. + - _groupId:_ `string` - Id of the target group + - _clientId:_ `string` - Id of the client + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`` - True if the group has the client app, false otherwise +- **checkGroupHasRole**(groupId: `string`, roleNames: `string[]`): [`Observable`](http://reactivex.io/documentation/observable.html)``
+ Check that a group has one or more roles from the supplied list. + - _groupId:_ `string` - Id of the target group + - _roleNames:_ `string[]` - Array of role names + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`` - True if the group has one or more of the roles, false otherwise +- **createGroup**(newGroup: [`IdentityGroupModel`](../../../lib/core/userinfo/models/identity-group.model.ts)): [`Observable`](http://reactivex.io/documentation/observable.html)``
+ Creates new group. + - _newGroup:_ [`IdentityGroupModel`](../../../lib/core/userinfo/models/identity-group.model.ts) - Object of containing the new group details. + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`` - Empty response when the group created. +- **deleteGroup**(groupId: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)``
+ Deletes Group. + - _groupId:_ `string` - Id of the group. + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`` - Empty response when the group deleted. +- **findGroupsByName**(searchParams: [`IdentityGroupSearchParam`](../../../lib/core/userinfo/models/identity-group.model.ts)): [`Observable`](http://reactivex.io/documentation/observable.html)``
+ Finds groups filtered by name. + - _searchParams:_ [`IdentityGroupSearchParam`](../../../lib/core/userinfo/models/identity-group.model.ts) - Object containing the name filter string + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`` - List of group information +- **getClientIdByApplicationName**(applicationName: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)``
+ Gets the client ID using the app name. + - _applicationName:_ `string` - Name of the app + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`` - client ID string +- **getClientRoles**(groupId: `string`, clientId: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`IdentityRoleModel`](../../../lib/core/userinfo/models/identity-role.model.ts)`[]>`
+ Gets client roles. + - _groupId:_ `string` - ID of the target group + - _clientId:_ `string` - ID of the client + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`IdentityRoleModel`](../../../lib/core/userinfo/models/identity-role.model.ts)`[]>` - List of roles +- **getGroupRoles**(groupId: `string`): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`IdentityRoleModel`](../../../lib/core/userinfo/models/identity-role.model.ts)`[]>`
+ Gets details for a specified group. + - _groupId:_ `string` - ID of the target group + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`IdentityRoleModel`](../../../lib/core/userinfo/models/identity-role.model.ts)`[]>` - Group details +- **getGroups**(): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`IdentityGroupModel`](../../../lib/core/userinfo/models/identity-group.model.ts)`[]>`
+ Gets all groups. + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`IdentityGroupModel`](../../../lib/core/userinfo/models/identity-group.model.ts)`[]>` - Array of group information objects +- **getTotalGroupsCount**(): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`IdentityGroupCountModel`](../../../lib/core/userinfo/models/identity-group.model.ts)`>`
+ Gets groups total count. + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`IdentityGroupCountModel`](../../../lib/core/userinfo/models/identity-group.model.ts)`>` - Number of groups count. +- **queryGroups**(requestQuery: [`IdentityGroupQueryCloudRequestModel`](../../../lib/core/userinfo/models/identity-group.model.ts)): [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`IdentityGroupQueryResponse`](../../../lib/core/userinfo/models/identity-group.model.ts)`>`
+ Queries groups. + - _requestQuery:_ [`IdentityGroupQueryCloudRequestModel`](../../../lib/core/userinfo/models/identity-group.model.ts) - + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`<`[`IdentityGroupQueryResponse`](../../../lib/core/userinfo/models/identity-group.model.ts)`>` - Array of user information objects +- **updateGroup**(groupId: `string`, updatedGroup: [`IdentityGroupModel`](../../../lib/core/userinfo/models/identity-group.model.ts)): [`Observable`](http://reactivex.io/documentation/observable.html)``
+ Updates group details. + - _groupId:_ `string` - Id of the targeted group. + - _updatedGroup:_ [`IdentityGroupModel`](../../../lib/core/userinfo/models/identity-group.model.ts) - Object of containing the group details + - **Returns** [`Observable`](http://reactivex.io/documentation/observable.html)`` - Empty response when the group updated. + +## See also + +- [Identity user service](../../core/userInfo/services/identity-user.service.md) diff --git a/docs/process-services-cloud/components/group-cloud.component.md b/docs/process-services-cloud/components/group-cloud.component.md index 8a8ec1b2e1..5c960c09fb 100644 --- a/docs/process-services-cloud/components/group-cloud.component.md +++ b/docs/process-services-cloud/components/group-cloud.component.md @@ -28,7 +28,7 @@ Searches Groups. | ---- | ---- | ------------- | ----------- | | appName | `string` | | Name of the application. If specified this shows the users who have access to the app. | | mode | `string` | | User selection mode (single/multiple). | -| preSelectGroups | [`GroupModel`](../../../lib/process-services-cloud/src/lib/group/models/group.model.ts)`[]` | \[] | Array of users to be pre-selected. This pre-selects all users in multi selection mode and only the first user of the array in single selection mode. | +| preSelectGroups | [`IdentityGroupModel`](../../../lib/core/userinfo/models/identity-group.model.ts)`[]` | \[] | Array of users to be pre-selected. This pre-selects all users in multi selection mode and only the first user of the array in single selection mode. | | roles | `string[]` | \[] | Role names of the groups to be listed. | | searchGroupsControl | `FormControl` | new FormControl() | FormControl to search the group | | title | `string` | | Title of the field | @@ -37,8 +37,8 @@ Searches Groups. | Name | Type | Description | | ---- | ---- | ----------- | -| removeGroup | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`GroupModel`](../../../lib/process-services-cloud/src/lib/group/models/group.model.ts)`>` | Emitted when a group is removed. | -| selectGroup | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`GroupModel`](../../../lib/process-services-cloud/src/lib/group/models/group.model.ts)`>` | Emitted when a group is selected. | +| removeGroup | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`IdentityGroupModel`](../../../lib/core/userinfo/models/identity-group.model.ts)`>` | Emitted when a group is removed. | +| selectGroup | [`EventEmitter`](https://angular.io/api/core/EventEmitter)`<`[`IdentityGroupModel`](../../../lib/core/userinfo/models/identity-group.model.ts)`>` | Emitted when a group is selected. | ## Details diff --git a/docs/process-services-cloud/pipes/group-initial.pipe.md b/docs/process-services-cloud/pipes/group-initial.pipe.md index 253196f0b3..e8772ea872 100644 --- a/docs/process-services-cloud/pipes/group-initial.pipe.md +++ b/docs/process-services-cloud/pipes/group-initial.pipe.md @@ -23,7 +23,7 @@ Extracts the initial character from a group name. ## Details -This pipe takes a [`GroupModel`](../../../lib/process-services-cloud/src/lib/group/models/group.model.ts) +This pipe takes a [`IdentityGroupModel`](../../../lib/core/userinfo/models/identity-group.model.ts) object as its parameter and extracts the initial character from the `name` property. The initial is a handy way to identify the group in lists and other situations where there is limited screen space available. diff --git a/lib/core/mock/identity-group.service.mock.ts b/lib/core/mock/identity-group.service.mock.ts new file mode 100644 index 0000000000..5903ffafa2 --- /dev/null +++ b/lib/core/mock/identity-group.service.mock.ts @@ -0,0 +1,163 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IdentityGroupModel, IdentityGroupCountModel } from '../userinfo/models/identity-group.model'; +import { IdentityRoleModel } from '../userinfo/models/identity-role.model'; + +export let mockIdentityGroup1 = new IdentityGroupModel({ + id: 'mock-group-id-1', name: 'Mock Group 1', path: '/mock', subGroups: [] +}); + +export let mockIdentityGroup2 = new IdentityGroupModel({ + id: 'mock-group-id-2', name: 'Mock Group 2', path: '', subGroups: [] +}); + +export let mockIdentityGroup3 = new IdentityGroupModel({ + id: 'mock-group-id-3', name: 'Mock Group 3', path: '', subGroups: [] +}); + +export let mockIdentityGroup4 = new IdentityGroupModel({ + id: 'mock-group-id-4', name: 'Mock Group 4', path: '', subGroups: [] +}); + +export let mockIdentityGroup5 = new IdentityGroupModel({ + id: 'mock-group-id-5', name: 'Mock Group 5', path: '', subGroups: [] +}); + +export let mockIdentityGroupsCount = { count: 10 }; + +export let mockIdentityGroups = [ + mockIdentityGroup1, mockIdentityGroup2, mockIdentityGroup3, mockIdentityGroup5, mockIdentityGroup5 +]; + +export let mockApplicationDetails = {id: 'mock-app-id', name: 'mock-app-name'}; + +export let groupAPIMockError = { + error: { + errorKey: 'failed', + statusCode: 400, + stackTrace: 'For security reasons the stack trace is no longer displayed, but the property is kept for previous versions.' + } +}; + +export let mockApiError = { + oauth2Auth: { + callCustomApi: () => { + return Promise.reject(groupAPIMockError); + } + } +}; + +export let roleMappingMock = [ + { id: 'role-id-1', name: 'role-name-1' }, { id: 'role-id-2', name: 'role-name-2' } +]; + +export let roleMappingApi = { + oauth2Auth: { + callCustomApi: () => { + return Promise.resolve(roleMappingMock); + } + } +}; + +export let noRoleMappingApi = { + oauth2Auth: { + callCustomApi: () => { + return Promise.resolve([]); + } + } +}; + +export let groupsMockApi = { + oauth2Auth: { + callCustomApi: () => { + return Promise.resolve(mockIdentityGroups); + } + } +}; + +export let getGroupsCountMockApi = { + oauth2Auth: { + callCustomApi: () => { + return Promise.resolve(10); + } + } +}; + +export let queryGroupsMockApi = { + oauth2Auth: { + callCustomApi: () => { + return Promise.resolve(mockIdentityGroups); + } + } +}; + +export let createGroupMappingApi = { + oauth2Auth: { + callCustomApi: () => { + return Promise.resolve(); + } + } +}; + +export let updateGroupMappingApi = { + oauth2Auth: { + callCustomApi: () => { + return Promise.resolve(); + } + } +}; + +export let deleteGroupMappingApi = { + oauth2Auth: { + callCustomApi: () => { + return Promise.resolve(); + } + } +}; + +export let returnCallQueryParameters = { + oauth2Auth: { + callCustomApi: (queryUrl, operation, context, queryParams) => { + return Promise.resolve(queryParams); + } + } +}; + +export let returnCallUrl = { + oauth2Auth: { + callCustomApi: (queryUrl, operation, context, queryParams) => { + return Promise.resolve(queryUrl); + } + } +}; + +export let applicationDetailsMockApi = { + oauth2Auth: { + callCustomApi: () => { + return Promise.resolve([mockApplicationDetails]); + } + } +}; + +export let mockIdentityRoles = [ + new IdentityRoleModel({id: 'mock-role-id', name: 'MOCK-ADMIN-ROLE'}), + new IdentityRoleModel({id: 'mock-role-id', name: 'MOCK-USER-ROLE'}), + new IdentityRoleModel({id: 'mock-role-id', name: 'MOCK-ROLE-1'}) +]; + +export let clientRoles = [ 'MOCK-ADMIN-ROLE', 'MOCK-USER-ROLE']; diff --git a/lib/core/mock/public-api.ts b/lib/core/mock/public-api.ts index 2adcedb278..758ec9ea81 100644 --- a/lib/core/mock/public-api.ts +++ b/lib/core/mock/public-api.ts @@ -37,3 +37,4 @@ export * from './form/start-form.component.mock'; export * from './form/form.service.mock'; export * from './form/widget-visibility.service.mock'; export * from './jwt-helper.service.spec'; +export * from './identity-group.service.mock'; diff --git a/lib/core/userinfo/models/identity-group.model.ts b/lib/core/userinfo/models/identity-group.model.ts index bffe11335a..7e852440fd 100644 --- a/lib/core/userinfo/models/identity-group.model.ts +++ b/lib/core/userinfo/models/identity-group.model.ts @@ -14,6 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { Pagination } from '@alfresco/js-api'; + export class IdentityGroupModel { id: string; @@ -36,3 +39,30 @@ export class IdentityGroupModel { } } } + +export interface IdentityGroupSearchParam { + name?: string; +} + +export interface IdentityGroupQueryResponse { + + entries: IdentityGroupModel[]; + pagination: Pagination; +} + +export class IdentityGroupQueryCloudRequestModel { + + first: number; + max: number; + + constructor(obj?: any) { + if (obj) { + this.first = obj.first; + this.max = obj.max; + } + } +} + +export interface IdentityGroupCountModel { + count: number; +} diff --git a/lib/core/userinfo/public-api.ts b/lib/core/userinfo/public-api.ts index 708508f84f..04c4893f95 100644 --- a/lib/core/userinfo/public-api.ts +++ b/lib/core/userinfo/public-api.ts @@ -19,8 +19,10 @@ export * from './components/user-info.component'; export * from './services/bpm-user.service'; export * from './services/ecm-user.service'; export * from './services/identity-user.service'; +export * from './services/identity-group.service'; export * from './models/bpm-user.model'; export * from './models/ecm-user.model'; +export * from './models/identity-group.model'; export * from './models/identity-user.model'; export * from './models/identity-role.model'; export * from './models/identity-group.model'; diff --git a/lib/process-services-cloud/src/lib/group/services/group-cloud.service.spec.ts b/lib/core/userinfo/services/identity-group.service.spec.ts similarity index 52% rename from lib/process-services-cloud/src/lib/group/services/group-cloud.service.spec.ts rename to lib/core/userinfo/services/identity-group.service.spec.ts index 89a9b12eec..ebf8e875c6 100644 --- a/lib/process-services-cloud/src/lib/group/services/group-cloud.service.spec.ts +++ b/lib/core/userinfo/services/identity-group.service.spec.ts @@ -17,32 +17,37 @@ import { async } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing'; -import { GroupCloudService } from './group-cloud.service'; import { AlfrescoApiServiceMock, CoreModule, setupTestBed, AlfrescoApiService, - LogService + LogService, + IdentityGroupService, + IdentityGroupSearchParam, + groupAPIMockError } from '@alfresco/adf-core'; -import { - applicationDetailsMockApi, - groupsMockApi, - returnCallQueryParameters, - returnCallUrl, - mockApiError, - mockError, - roleMappingApi, - noRoleMappingApi, - groupRoles, - clientRoles -} from '../mock/group-cloud.mock'; -import { GroupSearchParam } from '../models/group.model'; import { HttpErrorResponse } from '@angular/common/http'; import { throwError, of } from 'rxjs'; +import { + noRoleMappingApi, + mockIdentityRoles, + groupsMockApi, + roleMappingApi, + clientRoles, + returnCallQueryParameters, + returnCallUrl, + applicationDetailsMockApi, + mockApiError, + mockIdentityGroup1, + createGroupMappingApi, + updateGroupMappingApi, + deleteGroupMappingApi, + mockIdentityGroupsCount +} from '../../mock/identity-group.service.mock'; -describe('GroupCloudService', () => { - let service: GroupCloudService; +describe('IdentityGroupService', () => { + let service: IdentityGroupService; let apiService: AlfrescoApiService; let logService: LogService; @@ -54,23 +59,23 @@ describe('GroupCloudService', () => { }); beforeEach(async(() => { - service = TestBed.get(GroupCloudService); + service = TestBed.get(IdentityGroupService); apiService = TestBed.get(AlfrescoApiService); logService = TestBed.get(LogService); })); - it('should be able to fetch groups', (done) => { + it('should be able to fetch groups based on group name', (done) => { spyOn(apiService, 'getInstance').and.returnValue(groupsMockApi); - service.findGroupsByName( {name: 'mock'}).subscribe((res) => { + service.findGroupsByName( {name: 'mock'}).subscribe((res) => { expect(res).toBeDefined(); expect(res).not.toBeNull(); - expect(res.length).toBe(3); - expect(res[0].id).toBe('mock-id-1'); + expect(res.length).toBe(5); + expect(res[0].id).toBe('mock-group-id-1'); expect(res[0].name).toBe('Mock Group 1'); - expect(res[1].id).toBe('mock-id-2'); + expect(res[1].id).toBe('mock-group-id-2'); expect(res[1].name).toBe('Mock Group 2'); - expect(res[2].id).toBe('mock-id-3'); - expect(res[2].name).toBe('Fake Group 3'); + expect(res[2].id).toBe('mock-group-id-3'); + expect(res[2].name).toBe('Mock Group 3'); done(); }); }); @@ -94,7 +99,7 @@ describe('GroupCloudService', () => { }); it('should able to fetch group roles by groupId', (done) => { - spyOn(service, 'getGroupRoles').and.returnValue(of(groupRoles)); + spyOn(service, 'getGroupRoles').and.returnValue(of(mockIdentityRoles)); service.getGroupRoles('mock-group-id').subscribe( (res: any) => { expect(res).toBeDefined(); @@ -130,7 +135,7 @@ describe('GroupCloudService', () => { }); it('should return true if group has given role', (done) => { - spyOn(service, 'getGroupRoles').and.returnValue(of(groupRoles)); + spyOn(service, 'getGroupRoles').and.returnValue(of(mockIdentityRoles)); service.checkGroupHasRole('mock-group-id', ['MOCK-ADMIN-ROLE']).subscribe( (res: boolean) => { expect(res).toBeDefined(); @@ -141,7 +146,7 @@ describe('GroupCloudService', () => { }); it('should return false if group does not have given role', (done) => { - spyOn(service, 'getGroupRoles').and.returnValue(of(groupRoles)); + spyOn(service, 'getGroupRoles').and.returnValue(of(mockIdentityRoles)); service.checkGroupHasRole('mock-group-id', ['MOCK-ADMIN-MODELER']).subscribe( (res: boolean) => { expect(res).toBeDefined(); @@ -231,7 +236,7 @@ describe('GroupCloudService', () => { it('should append to the call all the parameters', (done) => { spyOn(apiService, 'getInstance').and.returnValue(returnCallQueryParameters); - service.findGroupsByName( {name: 'mock'}).subscribe((res) => { + service.findGroupsByName( {name: 'mock'}).subscribe((res) => { expect(res).toBeDefined(); expect(res).not.toBeNull(); expect(res.search).toBe('mock'); @@ -241,7 +246,7 @@ describe('GroupCloudService', () => { it('should request groups api url', (done) => { spyOn(apiService, 'getInstance').and.returnValue(returnCallUrl); - service.findGroupsByName( {name: 'mock'}).subscribe((requestUrl) => { + service.findGroupsByName( {name: 'mock'}).subscribe((requestUrl) => { expect(requestUrl).toBeDefined(); expect(requestUrl).not.toBeNull(); expect(requestUrl).toContain('/groups'); @@ -262,14 +267,184 @@ describe('GroupCloudService', () => { it('should notify errors returned from the API', (done) => { const logServiceSpy = spyOn(logService, 'error').and.callThrough(); spyOn(apiService, 'getInstance').and.returnValue(mockApiError); - service.findGroupsByName( {name: 'mock'}).subscribe( + service.findGroupsByName( {name: 'mock'}).subscribe( () => {}, (res: any) => { expect(res).toBeDefined(); - expect(res).toEqual(mockError); + expect(res).toEqual(groupAPIMockError); expect(logServiceSpy).toHaveBeenCalled(); done(); } ); }); + + it('should be able to all fetch groups', (done) => { + spyOn(apiService, 'getInstance').and.returnValue(groupsMockApi); + service.getGroups().subscribe((res) => { + expect(res).toBeDefined(); + expect(res).not.toBeNull(); + expect(res.length).toBe(5); + expect(res[0].id).toBe('mock-group-id-1'); + expect(res[0].name).toBe('Mock Group 1'); + expect(res[1].id).toBe('mock-group-id-2'); + expect(res[1].name).toBe('Mock Group 2'); + expect(res[2].id).toBe('mock-group-id-3'); + expect(res[2].name).toBe('Mock Group 3'); + done(); + }); + }); + + it('Should not able to fetch all group if error occurred', (done) => { + const errorResponse = new HttpErrorResponse({ + error: 'Mock Error', + status: 404, statusText: 'Not Found' + }); + + spyOn(service, 'getGroups').and.returnValue(throwError(errorResponse)); + + service.getGroups() + .subscribe( + () => { + fail('expected an error, not groups'); + }, + (error) => { + expect(error.status).toEqual(404); + expect(error.statusText).toEqual('Not Found'); + expect(error.error).toEqual('Mock Error'); + done(); + } + ); + }); + + it('should be able to query groups based on first & max params', (done) => { + spyOn(service, 'getTotalGroupsCount').and.returnValue(of(mockIdentityGroupsCount)); + spyOn(apiService, 'getInstance').and.returnValue(groupsMockApi); + service.queryGroups({first: 0, max: 5}).subscribe((res) => { + expect(res).toBeDefined(); + expect(res).not.toBeNull(); + expect(res.entries.length).toBe(5); + expect(res.entries[0].id).toBe('mock-group-id-1'); + expect(res.entries[0].name).toBe('Mock Group 1'); + expect(res.entries[1].id).toBe('mock-group-id-2'); + expect(res.entries[1].name).toBe('Mock Group 2'); + expect(res.entries[2].id).toBe('mock-group-id-3'); + expect(res.entries[2].name).toBe('Mock Group 3'); + expect(res.pagination.totalItems).toBe(10); + expect(res.pagination.skipCount).toBe(0); + expect(res.pagination.maxItems).toBe(5); + done(); + }); + }); + + it('Should not able to query groups if error occurred', (done) => { + const errorResponse = new HttpErrorResponse({ + error: 'Mock Error', + status: 404, statusText: 'Not Found' + }); + + spyOn(service, 'queryGroups').and.returnValue(throwError(errorResponse)); + + service.queryGroups({first: 0, max: 5}) + .subscribe( + () => { + fail('expected an error, not query groups'); + }, + (error) => { + expect(error.status).toEqual(404); + expect(error.statusText).toEqual('Not Found'); + expect(error.error).toEqual('Mock Error'); + done(); + } + ); + }); + + it('should be able to create group', (done) => { + const createCustomApiSpy = spyOn(apiService, 'getInstance').and.returnValue(createGroupMappingApi); + service.createGroup(mockIdentityGroup1).subscribe((res) => { + expect(createCustomApiSpy).toHaveBeenCalled(); + done(); + }); + }); + + it('Should not able to create group if error occurred', (done) => { + const errorResponse = new HttpErrorResponse({ + error: 'Mock Error', + status: 404, statusText: 'Not Found' + }); + + spyOn(service, 'createGroup').and.returnValue(throwError(errorResponse)); + + service.createGroup(mockIdentityGroup1) + .subscribe( + () => { + fail('expected an error, not to create group'); + }, + (error) => { + expect(error.status).toEqual(404); + expect(error.statusText).toEqual('Not Found'); + expect(error.error).toEqual('Mock Error'); + done(); + } + ); + }); + + it('should be able to update group', (done) => { + const updateCustomApiSpy = spyOn(apiService, 'getInstance').and.returnValue(updateGroupMappingApi); + service.updateGroup('mock-group-id', mockIdentityGroup1).subscribe((res) => { + expect(updateCustomApiSpy).toHaveBeenCalled(); + done(); + }); + }); + + it('Should not able to update group if error occurred', (done) => { + const errorResponse = new HttpErrorResponse({ + error: 'Mock Error', + status: 404, statusText: 'Not Found' + }); + + spyOn(service, 'updateGroup').and.returnValue(throwError(errorResponse)); + + service.updateGroup('mock-group-id', mockIdentityGroup1) + .subscribe( + () => { + fail('expected an error, not to update group'); + }, + (error) => { + expect(error.status).toEqual(404); + expect(error.statusText).toEqual('Not Found'); + expect(error.error).toEqual('Mock Error'); + done(); + } + ); + }); + + it('should be able to delete group', (done) => { + const deleteCustomApiSpy = spyOn(apiService, 'getInstance').and.returnValue(deleteGroupMappingApi); + service.deleteGroup('mock-group-id').subscribe((res) => { + expect(deleteCustomApiSpy).toHaveBeenCalled(); + done(); + }); + }); + + it('Should not able to delete group if error occurred', (done) => { + const errorResponse = new HttpErrorResponse({ + error: 'Mock Error', + status: 404, statusText: 'Not Found' + }); + + spyOn(service, 'deleteGroup').and.returnValue(throwError(errorResponse)); + + service.deleteGroup('mock-group-id') + .subscribe( + () => { + fail('expected an error, not to delete group'); + }, + (error) => { + expect(error.status).toEqual(404); + expect(error.statusText).toEqual('Not Found'); + expect(error.error).toEqual('Mock Error'); + done(); + } + ); + }); }); diff --git a/lib/core/userinfo/services/identity-group.service.ts b/lib/core/userinfo/services/identity-group.service.ts new file mode 100644 index 0000000000..11151c263a --- /dev/null +++ b/lib/core/userinfo/services/identity-group.service.ts @@ -0,0 +1,347 @@ +/*! + * @license + * Copyright 2019 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Injectable } from '@angular/core'; +import { Observable, of, from, throwError } from 'rxjs'; +import { catchError, map, switchMap } from 'rxjs/operators'; +import { AppConfigService } from '../../app-config/app-config.service'; +import { AlfrescoApiService } from '../../services/alfresco-api.service'; +import { LogService } from '../../services/log.service'; +import { + IdentityGroupSearchParam, + IdentityGroupQueryCloudRequestModel, + IdentityGroupModel, + IdentityGroupQueryResponse, + IdentityGroupCountModel +} from '../models/identity-group.model'; +import { IdentityRoleModel } from '../models/identity-role.model'; + +@Injectable({ + providedIn: 'root' +}) +export class IdentityGroupService { + + constructor( + private alfrescoApiService: AlfrescoApiService, + private appConfigService: AppConfigService, + private logService: LogService + ) {} + + /** + * Gets all groups. + * @returns Array of group information objects + */ + getGroups(): Observable { + const url = this.getGroupsApi(); + const httpMethod = 'GET', pathParams = {}, + queryParams = {}, bodyParam = {}, headerParams = {}, + formParams = {}, authNames = [], contentTypes = ['application/json']; + + return from(this.alfrescoApiService.getInstance().oauth2Auth.callCustomApi( + url, httpMethod, pathParams, queryParams, + headerParams, formParams, bodyParam, authNames, + contentTypes, null, null, null + )).pipe( + catchError((error) => this.handleError(error)) + ); + } + + /** + * Queries groups. + * @returns Array of user information objects + */ + queryGroups(requestQuery: IdentityGroupQueryCloudRequestModel): Observable { + const url = this.getGroupsApi(); + const httpMethod = 'GET', pathParams = {}, + queryParams = { first: requestQuery.first || 0, max: requestQuery.max || 5 }, bodyParam = {}, headerParams = {}, + formParams = {}, authNames = [], contentTypes = ['application/json']; + return this.getTotalGroupsCount().pipe( + switchMap((totalCount: IdentityGroupCountModel) => + from(this.alfrescoApiService.getInstance().oauth2Auth.callCustomApi( + url, httpMethod, pathParams, queryParams, + headerParams, formParams, bodyParam, authNames, + contentTypes, null, null, null) + ).pipe( + map((response: any[]) => { + return { + entries: response, + pagination: { + skipCount: requestQuery.first, + maxItems: requestQuery.max, + count: totalCount.count, + hasMoreItems: false, + totalItems: totalCount.count + } + }; + }), + catchError((error) => this.handleError(error)) + )) + ); + } + + /** + * Gets groups total count. + * @returns Number of groups count. + */ + getTotalGroupsCount(): Observable { + const url = this.getGroupsApi() + `/count`; + const contentTypes = ['application/json'], accepts = ['application/json']; + return from(this.alfrescoApiService.getInstance() + .oauth2Auth.callCustomApi(url, 'GET', + null, null, null, + null, null, contentTypes, + accepts, null, null, null)).pipe( + catchError((error) => this.handleError(error)) + ); + } + + /** + * Creates new group. + * @param newGroup Object of containing the new group details. + * @returns Empty response when the group created. + */ + createGroup(newGroup: IdentityGroupModel): Observable { + const url = this.getGroupsApi(); + const httpMethod = 'POST', pathParams = {}, queryParams = {}, bodyParam = newGroup, headerParams = {}, + formParams = {}, contentTypes = ['application/json'], accepts = ['application/json']; + + return from(this.alfrescoApiService.getInstance().oauth2Auth.callCustomApi( + url, httpMethod, pathParams, queryParams, + headerParams, formParams, bodyParam, + contentTypes, accepts, null, null, null + )).pipe( + catchError((error) => this.handleError(error)) + ); + } + + /** + * Updates group details. + * @param groupId Id of the targeted group. + * @param updatedGroup Object of containing the group details + * @returns Empty response when the group updated. + */ + updateGroup(groupId: string, updatedGroup: IdentityGroupModel): Observable { + const url = this.getGroupsApi() + `/${groupId}`; + const request = JSON.stringify(updatedGroup); + const httpMethod = 'PUT', pathParams = {} , queryParams = {}, bodyParam = request, headerParams = {}, + formParams = {}, contentTypes = ['application/json'], accepts = ['application/json']; + + return from(this.alfrescoApiService.getInstance().oauth2Auth.callCustomApi( + url, httpMethod, pathParams, queryParams, + headerParams, formParams, bodyParam, + contentTypes, accepts, null, null, null + )).pipe( + catchError((error) => this.handleError(error)) + ); + } + + /** + * Deletes Group. + * @param groupId Id of the group. + * @returns Empty response when the group deleted. + */ + deleteGroup(groupId: string): Observable { + const url = this.getGroupsApi() + `/${groupId}`; + const httpMethod = 'DELETE', pathParams = {} , queryParams = {}, bodyParam = {}, headerParams = {}, + formParams = {}, contentTypes = ['application/json'], accepts = ['application/json']; + + return from(this.alfrescoApiService.getInstance().oauth2Auth.callCustomApi( + url, httpMethod, pathParams, queryParams, + headerParams, formParams, bodyParam, + contentTypes, accepts, null, null, null + )).pipe( + catchError((error) => this.handleError(error)) + ); + } + + /** + * Finds groups filtered by name. + * @param searchParams Object containing the name filter string + * @returns List of group information + */ + findGroupsByName(searchParams: IdentityGroupSearchParam): Observable { + if (searchParams.name === '') { + return of([]); + } + const url = this.getGroupsApi(); + const httpMethod = 'GET', pathParams = {}, queryParams = {search: searchParams.name}, bodyParam = {}, headerParams = {}, + formParams = {}, contentTypes = ['application/json'], accepts = ['application/json']; + + return (from(this.alfrescoApiService.getInstance().oauth2Auth.callCustomApi( + url, httpMethod, pathParams, queryParams, + headerParams, formParams, bodyParam, + contentTypes, accepts, Object, null, null) + )).pipe( + catchError((error) => this.handleError(error)) + ); + } + + /** + * Gets details for a specified group. + * @param groupId Id of the target group + * @returns Group details + */ + getGroupRoles(groupId: string): Observable { + const url = this.buildRolesUrl(groupId); + const httpMethod = 'GET', pathParams = {}, queryParams = {}, bodyParam = {}, headerParams = {}, + formParams = {}, contentTypes = ['application/json'], accepts = ['application/json']; + + return (from(this.alfrescoApiService.getInstance().oauth2Auth.callCustomApi( + url, httpMethod, pathParams, queryParams, + headerParams, formParams, bodyParam, + contentTypes, accepts, Object, null, null) + )).pipe( + catchError((error) => this.handleError(error)) + ); + } + + /** + * Check that a group has one or more roles from the supplied list. + * @param groupId Id of the target group + * @param roleNames Array of role names + * @returns True if the group has one or more of the roles, false otherwise + */ + checkGroupHasRole(groupId: string, roleNames: string[]): Observable { + return this.getGroupRoles(groupId).pipe(map((groupRoles: IdentityRoleModel[]) => { + let hasRole = false; + if (groupRoles && groupRoles.length > 0) { + roleNames.forEach((roleName: string) => { + const role = groupRoles.find((groupRole) => { + return roleName === groupRole.name; + }); + if (role) { + hasRole = true; + return; + } + }); + } + return hasRole; + })); + } + + /** + * Gets the client Id using the app name. + * @param applicationName Name of the app + * @returns client Id string + */ + getClientIdByApplicationName(applicationName: string): Observable { + const url = this.getApplicationIdApi(); + const httpMethod = 'GET', pathParams = {}, queryParams = {clientId: applicationName}, bodyParam = {}, headerParams = {}, formParams = {}, + contentTypes = ['application/json'], accepts = ['application/json']; + return from(this.alfrescoApiService.getInstance() + .oauth2Auth.callCustomApi(url, httpMethod, pathParams, queryParams, headerParams, + formParams, bodyParam, contentTypes, + accepts, Object, null, null) + ).pipe( + map((response: any[]) => { + const clientId = response && response.length > 0 ? response[0].id : ''; + return clientId; + }), + catchError((error) => this.handleError(error)) + ); + } + + /** + * Gets client roles. + * @param groupId Id of the target group + * @param clientId Id of the client + * @returns List of roles + */ + getClientRoles(groupId: string, clientId: string): Observable { + const url = this.groupClientRoleMappingApi(groupId, clientId); + const httpMethod = 'GET', pathParams = {}, queryParams = {}, bodyParam = {}, headerParams = {}, + formParams = {}, contentTypes = ['application/json'], accepts = ['application/json']; + + return from(this.alfrescoApiService.getInstance().oauth2Auth.callCustomApi( + url, httpMethod, pathParams, queryParams, + headerParams, formParams, bodyParam, + contentTypes, accepts, Object, null, null) + ); + } + + /** + * Checks if a group has a client app. + * @param groupId Id of the target group + * @param clientId Id of the client + * @returns True if the group has the client app, false otherwise + */ + checkGroupHasClientApp(groupId: string, clientId: string): Observable { + return this.getClientRoles(groupId, clientId).pipe( + map((response: any[]) => { + if (response && response.length > 0) { + return true; + } + return false; + }), + catchError((error) => this.handleError(error)) + ); + } + + /** + * Check if a group has any of the client app roles in the supplied list. + * @param groupId Id of the target group + * @param clientId Id of the client + * @param roleNames Array of role names to check + * @returns True if the group has one or more of the roles, false otherwise + */ + checkGroupHasAnyClientAppRole(groupId: string, clientId: string, roleNames: string[]): Observable { + return this.getClientRoles(groupId, clientId).pipe( + map((clientRoles: any[]) => { + let hasRole = false; + if (clientRoles.length > 0) { + roleNames.forEach((roleName) => { + const role = clientRoles.find((availableRole) => { + return availableRole.name === roleName; + }); + + if (role) { + hasRole = true; + return; + } + }); + } + return hasRole; + }), + catchError((error) => this.handleError(error)) + ); + } + + private groupClientRoleMappingApi(groupId: string, clientId: string): string { + return `${this.appConfigService.get('identityHost')}/groups/${groupId}/role-mappings/clients/${clientId}`; + } + + private getApplicationIdApi(): string { + return `${this.appConfigService.get('identityHost')}/clients`; + } + + private getGroupsApi(): string { + return `${this.appConfigService.get('identityHost')}/groups`; + } + + private buildRolesUrl(groupId: string): string { + return `${this.appConfigService.get('identityHost')}/groups/${groupId}/role-mappings/realm/composite`; + } + + /** + * Throw the error + * @param error + */ + private handleError(error: Response) { + this.logService.error(error); + return throwError(error || 'Server error'); + } +} diff --git a/lib/core/userinfo/services/identity-user.service.ts b/lib/core/userinfo/services/identity-user.service.ts index a362219920..805ef1061f 100644 --- a/lib/core/userinfo/services/identity-user.service.ts +++ b/lib/core/userinfo/services/identity-user.service.ts @@ -667,19 +667,19 @@ export class IdentityUserService { ); } - private buildUserUrl(): any { + private buildUserUrl(): string { return `${this.appConfigService.get('identityHost')}/users`; } - private buildUserClientRoleMapping(userId: string, clientId: string): any { + private buildUserClientRoleMapping(userId: string, clientId: string): string { return `${this.appConfigService.get('identityHost')}/users/${userId}/role-mappings/clients/${clientId}`; } - private buildRolesUrl(userId: string): any { + private buildRolesUrl(userId: string): string { return `${this.appConfigService.get('identityHost')}/users/${userId}/role-mappings/realm/composite`; } - private buildGetClientsUrl() { + private buildGetClientsUrl(): string { return `${this.appConfigService.get('identityHost')}/clients`; } diff --git a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.spec.ts b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.spec.ts index eccbf58420..ca11b19f03 100644 --- a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.spec.ts +++ b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.spec.ts @@ -22,17 +22,20 @@ import { ProcessServiceCloudTestingModule } from './../../testing/process-servic import { GroupCloudModule } from '../group-cloud.module'; import { GroupCloudComponent } from './group-cloud.component'; -import { GroupCloudService } from '../services/group-cloud.service'; -import { setupTestBed, AlfrescoApiServiceMock } from '@alfresco/adf-core'; -import { mockGroups } from '../mock/group-cloud.mock'; -import { GroupModel } from '../models/group.model'; +import { + setupTestBed, + AlfrescoApiServiceMock, + IdentityGroupService, + IdentityGroupModel, + mockIdentityGroups +} from '@alfresco/adf-core'; import { SimpleChange } from '@angular/core'; describe('GroupCloudComponent', () => { let component: GroupCloudComponent; let fixture: ComponentFixture; let element: HTMLElement; - let service: GroupCloudService; + let service: IdentityGroupService; let findGroupsByNameSpy: jasmine.Spy; let getClientIdByApplicationNameSpy: jasmine.Spy; let checkGroupHasAccessSpy: jasmine.Spy; @@ -40,15 +43,15 @@ describe('GroupCloudComponent', () => { setupTestBed({ imports: [ProcessServiceCloudTestingModule, GroupCloudModule], - providers: [AlfrescoApiServiceMock, GroupCloudService] + providers: [AlfrescoApiServiceMock, IdentityGroupService] }); beforeEach(() => { fixture = TestBed.createComponent(GroupCloudComponent); component = fixture.componentInstance; element = fixture.nativeElement; - service = TestBed.get(GroupCloudService); - findGroupsByNameSpy = spyOn(service, 'findGroupsByName').and.returnValue(of(mockGroups)); + service = TestBed.get(IdentityGroupService); + findGroupsByNameSpy = spyOn(service, 'findGroupsByName').and.returnValue(of(mockIdentityGroups)); getClientIdByApplicationNameSpy = spyOn(service, 'getClientIdByApplicationName').and.returnValue(of('mock-client-id')); checkGroupHasAccessSpy = spyOn(service, 'checkGroupHasClientApp').and.returnValue(of(true)); checkGroupHasGivenRoleSpy = spyOn(service, 'checkGroupHasRole').and.returnValue(of(true)); @@ -72,7 +75,7 @@ describe('GroupCloudComponent', () => { it('should show the groups if the typed result match', async(() => { fixture.detectChanges(); - component.searchGroups$ = of( mockGroups); + component.searchGroups$.next( mockIdentityGroups); const inputHTMLElement: HTMLInputElement = element.querySelector('input'); inputHTMLElement.focus(); inputHTMLElement.dispatchEvent(new Event('input')); @@ -103,7 +106,7 @@ describe('GroupCloudComponent', () => { it('should emit selectedGroup if option is valid', async(() => { fixture.detectChanges(); const selectEmitSpy = spyOn(component.selectGroup, 'emit'); - component.onSelect(new GroupModel({ name: 'group name'})); + component.onSelect(new IdentityGroupModel({ name: 'group name'})); fixture.whenStable().then(() => { expect(selectEmitSpy).toHaveBeenCalled(); }); @@ -147,7 +150,7 @@ describe('GroupCloudComponent', () => { it('should pre-select all preSelectGroups when mode=multiple', async(() => { component.mode = 'multiple'; - component.preSelectGroups = [{id: mockGroups[1].id}, {id: mockGroups[2].id}]; + component.preSelectGroups = [{id: mockIdentityGroups[1].id}, {id: mockIdentityGroups[2].id}]; fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); @@ -168,11 +171,11 @@ describe('GroupCloudComponent', () => { it('should pre-select preSelectGroups[0] when mode=single', async(() => { component.mode = 'single'; - component.preSelectGroups = [{id: mockGroups[1].id}, {id: mockGroups[2].id}]; + component.preSelectGroups = [{id: mockIdentityGroups[1].id}, {id: mockIdentityGroups[2].id}]; fixture.detectChanges(); fixture.whenStable().then(() => { const selectedGroup = component.searchGroupsControl.value; - expect(selectedGroup.id).toBe(mockGroups[1].id); + expect(selectedGroup.id).toBe(mockIdentityGroups[1].id); }); })); @@ -189,7 +192,7 @@ describe('GroupCloudComponent', () => { const removeGroupSpy = spyOn(component.removeGroup, 'emit'); component.mode = 'multiple'; - component.preSelectGroups = [{id: mockGroups[1].id}, {id: mockGroups[2].id}]; + component.preSelectGroups = [{id: mockIdentityGroups[1].id}, {id: mockIdentityGroups[2].id}]; fixture.detectChanges(); fixture.whenStable().then(() => { @@ -197,7 +200,7 @@ describe('GroupCloudComponent', () => { const removeIcon = fixture.debugElement.query(By.css('mat-chip mat-icon')); removeIcon.nativeElement.click(); - expect(removeGroupSpy).toHaveBeenCalledWith({ id: mockGroups[1].id }); + expect(removeGroupSpy).toHaveBeenCalledWith({ id: mockIdentityGroups[1].id }); }); })); @@ -213,7 +216,7 @@ describe('GroupCloudComponent', () => { fixture.whenStable().then(() => { fixture.detectChanges(); const groupsList = fixture.debugElement.queryAll(By.css('mat-option')); - expect(groupsList.length).toBe(mockGroups.length); + expect(groupsList.length).toBe(mockIdentityGroups.length); }); })); @@ -246,7 +249,7 @@ describe('GroupCloudComponent', () => { fixture.whenStable().then(() => { fixture.detectChanges(); const groupsList = fixture.debugElement.queryAll(By.css('mat-option')); - expect(groupsList.length).toBe(mockGroups.length); + expect(groupsList.length).toBe(mockIdentityGroups.length); expect(checkGroupHasGivenRoleSpy).toHaveBeenCalled(); }); })); @@ -263,13 +266,13 @@ describe('GroupCloudComponent', () => { fixture.whenStable().then(() => { fixture.detectChanges(); const groupsList = fixture.debugElement.queryAll(By.css('mat-option')); - expect(groupsList.length).toBe(mockGroups.length); + expect(groupsList.length).toBe(mockIdentityGroups.length); expect(checkGroupHasGivenRoleSpy).not.toHaveBeenCalled(); }); })); it('should validate access to the app when appName is specified', async(() => { - findGroupsByNameSpy.and.returnValue(of(mockGroups)); + findGroupsByNameSpy.and.returnValue(of(mockIdentityGroups)); checkGroupHasAccessSpy.and.returnValue(of(true)); fixture.detectChanges(); const inputHTMLElement: HTMLInputElement = element.querySelector('input'); @@ -279,7 +282,7 @@ describe('GroupCloudComponent', () => { fixture.detectChanges(); fixture.whenStable().then(() => { fixture.detectChanges(); - expect(checkGroupHasAccessSpy).toHaveBeenCalledTimes(mockGroups.length); + expect(checkGroupHasAccessSpy).toHaveBeenCalledTimes(mockIdentityGroups.length); }); })); diff --git a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.ts b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.ts index de411748e7..d81430680b 100644 --- a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.ts +++ b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.ts @@ -25,15 +25,15 @@ import { ViewEncapsulation, Input, SimpleChanges, - OnChanges + OnChanges, + OnDestroy } from '@angular/core'; import { FormControl } from '@angular/forms'; import { trigger, state, style, transition, animate } from '@angular/animations'; -import { Observable, of, BehaviorSubject } from 'rxjs'; -import { GroupModel, GroupSearchParam } from '../models/group.model'; -import { GroupCloudService } from '../services/group-cloud.service'; +import { Observable, of, BehaviorSubject, Subject } from 'rxjs'; import { debounceTime } from 'rxjs/internal/operators/debounceTime'; -import { distinctUntilChanged, switchMap, mergeMap, filter, tap, map } from 'rxjs/operators'; +import { distinctUntilChanged, switchMap, mergeMap, filter, tap, map, takeUntil } from 'rxjs/operators'; +import { IdentityGroupModel, IdentityGroupSearchParam, IdentityGroupService } from '@alfresco/adf-core'; @Component({ selector: 'adf-cloud-group', @@ -50,7 +50,7 @@ import { distinctUntilChanged, switchMap, mergeMap, filter, tap, map } from 'rxj ], encapsulation: ViewEncapsulation.None }) -export class GroupCloudComponent implements OnInit, OnChanges { +export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy { static MODE_SINGLE = 'single'; static MODE_MULTIPLE = 'multiple'; @@ -69,7 +69,7 @@ export class GroupCloudComponent implements OnInit, OnChanges { /** Array of users to be pre-selected. This pre-selects all users in multi selection mode and only the first user of the array in single selection mode. */ @Input() - preSelectGroups: GroupModel[] = []; + preSelectGroups: IdentityGroupModel[] = []; /** FormControl to search the group */ @Input() @@ -81,26 +81,22 @@ export class GroupCloudComponent implements OnInit, OnChanges { /** Emitted when a group is selected. */ @Output() - selectGroup: EventEmitter = new EventEmitter(); + selectGroup = new EventEmitter(); /** Emitted when a group is removed. */ @Output() - removeGroup: EventEmitter = new EventEmitter(); + removeGroup = new EventEmitter(); @ViewChild('groupInput') private groupInput: ElementRef; - private selectedGroups: GroupModel[] = []; + private selectedGroups: IdentityGroupModel[] = []; - private searchGroups: GroupModel[] = []; + private searchGroups: IdentityGroupModel[] = []; - private searchGroupsSubject: BehaviorSubject; + searchGroups$ = new BehaviorSubject([]); - private selectedGroupsSubject: BehaviorSubject; - - searchGroups$: Observable; - - selectedGroups$: Observable; + selectedGroups$ = new BehaviorSubject([]); _subscriptAnimationState = 'enter'; @@ -112,12 +108,9 @@ export class GroupCloudComponent implements OnInit, OnChanges { isDisabled: boolean; - constructor(private groupService: GroupCloudService) { - this.selectedGroupsSubject = new BehaviorSubject(this.selectedGroups); - this.searchGroupsSubject = new BehaviorSubject(this.searchGroups); - this.selectedGroups$ = this.selectedGroupsSubject.asObservable(); - this.searchGroups$ = this.searchGroupsSubject.asObservable(); - } + private onDestroy$ = new Subject(); + + constructor(private identityGroupService: IdentityGroupService) { } ngOnInit() { this.initSearch(); @@ -136,12 +129,12 @@ export class GroupCloudComponent implements OnInit, OnChanges { } } - private isAppNameChanged(change) { + private isAppNameChanged(change): boolean { return change.previousValue !== change.currentValue && this.appName && this.appName.length > 0; } private async loadClientId() { - this.clientId = await this.groupService.getClientIdByApplicationName(this.appName).toPromise(); + this.clientId = await this.identityGroupService.getClientIdByApplicationName(this.appName).toPromise(); if (this.clientId) { this.enableSearch(); } @@ -165,7 +158,7 @@ export class GroupCloudComponent implements OnInit, OnChanges { }), switchMap((inputValue) => { const queryParams = this.createSearchParam(inputValue); - return this.groupService.findGroupsByName(queryParams); + return this.identityGroupService.findGroupsByName(queryParams); }), mergeMap((groups) => { return groups; @@ -185,23 +178,24 @@ export class GroupCloudComponent implements OnInit, OnChanges { } else { return of(group); } - }) - ).subscribe((searchedGroup) => { + }), + takeUntil(this.onDestroy$) + ).subscribe((searchedGroup: any) => { this.searchGroups.push(searchedGroup); - this.searchGroupsSubject.next(this.searchGroups); + this.searchGroups$.next(this.searchGroups); }); } checkGroupHasAccess(groupId: string): Observable { if (this.hasRoles()) { - return this.groupService.checkGroupHasAnyClientAppRole(groupId, this.clientId, this.roles); + return this.identityGroupService.checkGroupHasAnyClientAppRole(groupId, this.clientId, this.roles); } else { - return this.groupService.checkGroupHasClientApp(groupId, this.clientId); + return this.identityGroupService.checkGroupHasClientApp(groupId, this.clientId); } } - isGroupAlreadySelected(group: GroupModel): boolean { - const result = this.selectedGroups.find((selectedGroup: GroupModel) => { + isGroupAlreadySelected(group: IdentityGroupModel): boolean { + const result = this.selectedGroups.find((selectedGroup: IdentityGroupModel) => { return selectedGroup.id === group.id; }); @@ -211,30 +205,30 @@ export class GroupCloudComponent implements OnInit, OnChanges { private loadPreSelectGroups() { if (this.isMultipleMode()) { this.selectedGroups = []; - this.preSelectGroups.forEach((group: GroupModel) => { + this.preSelectGroups.forEach((group: IdentityGroupModel) => { this.selectedGroups.push(group); }); - this.selectedGroupsSubject.next(this.selectedGroups); + this.selectedGroups$.next(this.selectedGroups); } else { this.searchGroupsControl.setValue(this.preSelectGroups[0]); this.onSelect(this.preSelectGroups[0]); } } - filterGroupsByRoles(group: GroupModel): Observable { - return this.groupService.checkGroupHasRole(group.id, this.roles).pipe( + filterGroupsByRoles(group: IdentityGroupModel): Observable { + return this.identityGroupService.checkGroupHasRole(group.id, this.roles).pipe( map((hasRole: boolean) => ({ hasRole: hasRole, group: group })), - filter((filteredGroup: { hasRole: boolean, group: GroupModel }) => filteredGroup.hasRole), - map((filteredGroup: { hasRole: boolean, group: GroupModel }) => filteredGroup.group)); + filter((filteredGroup: { hasRole: boolean, group: IdentityGroupModel }) => filteredGroup.hasRole), + map((filteredGroup: { hasRole: boolean, group: IdentityGroupModel }) => filteredGroup.group)); } - onSelect(selectedGroup: GroupModel) { + onSelect(selectedGroup: IdentityGroupModel) { if (this.isMultipleMode()) { if (!this.isGroupAlreadySelected(selectedGroup)) { this.selectedGroups.push(selectedGroup); - this.selectedGroupsSubject.next(this.selectedGroups); + this.selectedGroups$.next(this.selectedGroups); this.selectGroup.emit(selectedGroup); - this.searchGroupsSubject.next([]); + this.searchGroups$.next([]); } this.groupInput.nativeElement.value = ''; this.searchGroupsControl.setValue(''); @@ -246,25 +240,25 @@ export class GroupCloudComponent implements OnInit, OnChanges { this.resetSearchGroups(); } - onRemove(selectedGroup: GroupModel) { + onRemove(selectedGroup: IdentityGroupModel) { this.removeGroup.emit(selectedGroup); - const indexToRemove = this.selectedGroups.findIndex((group: GroupModel) => { + const indexToRemove = this.selectedGroups.findIndex((group: IdentityGroupModel) => { return group.id === selectedGroup.id; }); this.selectedGroups.splice(indexToRemove, 1); - this.selectedGroupsSubject.next(this.selectedGroups); + this.selectedGroups$.next(this.selectedGroups); } private resetSearchGroups() { this.searchGroups = []; - this.searchGroupsSubject.next([]); + this.searchGroups$.next([]); } isMultipleMode(): boolean { return this.mode === GroupCloudComponent.MODE_MULTIPLE; } - getDisplayName(group: GroupModel): string { + getDisplayName(group: IdentityGroupModel): string { return group ? group.name : ''; } @@ -272,8 +266,8 @@ export class GroupCloudComponent implements OnInit, OnChanges { return this.preSelectGroups && this.preSelectGroups.length > 0; } - private createSearchParam(value: string): GroupSearchParam { - const queryParams: GroupSearchParam = { name: value }; + private createSearchParam(value: string): IdentityGroupSearchParam { + const queryParams: IdentityGroupSearchParam = { name: value }; return queryParams; } @@ -310,4 +304,9 @@ export class GroupCloudComponent implements OnInit, OnChanges { hasErrorMessage(): boolean { return !this.isFocused && this.hasError(); } + + ngOnDestroy() { + this.onDestroy$.next(true); + this.onDestroy$.complete(); + } } diff --git a/lib/process-services-cloud/src/lib/group/mock/group-cloud.mock.ts b/lib/process-services-cloud/src/lib/group/mock/group-cloud.mock.ts deleted file mode 100644 index a0f75bcc23..0000000000 --- a/lib/process-services-cloud/src/lib/group/mock/group-cloud.mock.ts +++ /dev/null @@ -1,112 +0,0 @@ -/*! - * @license - * Copyright 2019 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { GroupModel, GroupRoleModel } from '../models/group.model'; - -export let mockGroup1 = new GroupModel({ - id: 'mock-id-1', name: 'Mock Group 1', path: '/mock', subGroups: [] -}); - -export let mockGroup2 = new GroupModel({ - id: 'mock-id-2', name: 'Mock Group 2', path: '', subGroups: [] -}); - -export let mockGroup3 = new GroupModel({ - id: 'mock-id-3', name: 'Fake Group 3', path: '', subGroups: [] -}); - -export let mockGroups = [ - mockGroup1, mockGroup2, mockGroup3 -]; - -export let mockApplicationDetails = {id: 'mock-app-id', name: 'mock-app-name'}; - -export let mockError = { - error: { - errorKey: 'failed', - statusCode: 400, - stackTrace: 'For security reasons the stack trace is no longer displayed, but the property is kept for previous versions.' - } -}; - -export let mockApiError = { - oauth2Auth: { - callCustomApi: () => { - return Promise.reject(mockError); - } - } -}; - -export let roleMappingMock = [ - { id: 'role-id-1', name: 'role-name-1' }, { id: 'role-id-2', name: 'role-name-2' } -]; - -export let roleMappingApi = { - oauth2Auth: { - callCustomApi: () => { - return Promise.resolve(roleMappingMock); - } - } -}; - -export let noRoleMappingApi = { - oauth2Auth: { - callCustomApi: () => { - return Promise.resolve([]); - } - } -}; - -export let groupsMockApi = { - oauth2Auth: { - callCustomApi: () => { - return Promise.resolve(mockGroups); - } - } -}; - -export let returnCallQueryParameters = { - oauth2Auth: { - callCustomApi: (queryUrl, operation, context, queryParams) => { - return Promise.resolve(queryParams); - } - } -}; - -export let returnCallUrl = { - oauth2Auth: { - callCustomApi: (queryUrl, operation, context, queryParams) => { - return Promise.resolve(queryUrl); - } - } -}; - -export let applicationDetailsMockApi = { - oauth2Auth: { - callCustomApi: () => { - return Promise.resolve([mockApplicationDetails]); - } - } -}; - -export let groupRoles = [ - new GroupRoleModel({id: 'mock-id', name: 'MOCK-ADMIN-ROLE'}), - new GroupRoleModel({id: 'mock-id', name: 'MOCK-USER-ROLE'}), - new GroupRoleModel({id: 'mock-id', name: 'MOCK-ROLE-1'}) -]; - -export let clientRoles = [ 'MOCK-ADMIN-ROLE', 'MOCK-USER-ROLE']; diff --git a/lib/process-services-cloud/src/lib/group/models/group.model.ts b/lib/process-services-cloud/src/lib/group/models/group.model.ts deleted file mode 100644 index c8fb245897..0000000000 --- a/lib/process-services-cloud/src/lib/group/models/group.model.ts +++ /dev/null @@ -1,54 +0,0 @@ -/*! - * @license - * Copyright 2019 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export class GroupModel { - - id: string; - name: string; - path: string; - realmRoles: string[]; - access: any; - attributes: any; - clientRoles: any; - - constructor(obj?: any) { - this.id = obj.id || null; - this.name = obj.name || null; - this.path = obj.path || null; - this.realmRoles = obj.realmRoles || null; - this.access = obj.access || null; - this.attributes = obj.attributes || null; - this.clientRoles = obj.clientRoles || null; - } -} - -export interface GroupSearchParam { - name?: string; -} - -export class GroupRoleModel { - - id?: string; - name: string; - - constructor(obj?: any) { - if (obj) { - this.id = obj.id || null; - this.name = obj.name || null; - } - } -} diff --git a/lib/process-services-cloud/src/lib/group/pipe/group-initial.pipe.spec.ts b/lib/process-services-cloud/src/lib/group/pipe/group-initial.pipe.spec.ts index b33b1de4a4..d6b1073b34 100644 --- a/lib/process-services-cloud/src/lib/group/pipe/group-initial.pipe.spec.ts +++ b/lib/process-services-cloud/src/lib/group/pipe/group-initial.pipe.spec.ts @@ -16,16 +16,16 @@ */ import { InitialGroupNamePipe } from './group-initial.pipe'; -import { GroupModel } from '../models/group.model'; +import { IdentityGroupModel } from '@alfresco/adf-core'; describe('InitialGroupNamePipe', () => { let pipe: InitialGroupNamePipe; - let fakeGroup: GroupModel; + let fakeGroup: IdentityGroupModel; beforeEach(() => { pipe = new InitialGroupNamePipe(); - fakeGroup = new GroupModel({name: 'mock'}); + fakeGroup = new IdentityGroupModel({name: 'mock'}); }); it('should return with the group initial', () => { diff --git a/lib/process-services-cloud/src/lib/group/pipe/group-initial.pipe.ts b/lib/process-services-cloud/src/lib/group/pipe/group-initial.pipe.ts index 88ef8cecad..f3078683ba 100644 --- a/lib/process-services-cloud/src/lib/group/pipe/group-initial.pipe.ts +++ b/lib/process-services-cloud/src/lib/group/pipe/group-initial.pipe.ts @@ -16,7 +16,7 @@ */ import { Pipe, PipeTransform } from '@angular/core'; -import { GroupModel } from '../models/group.model'; +import { IdentityGroupModel } from '@alfresco/adf-core'; @Pipe({ name: 'groupNameInitial' @@ -25,7 +25,7 @@ export class InitialGroupNamePipe implements PipeTransform { constructor() {} - transform(group: GroupModel): string { + transform(group: IdentityGroupModel): string { let result = ''; if (group) { result = this.getInitialGroupName(group.name).toUpperCase(); diff --git a/lib/process-services-cloud/src/lib/group/public-api.ts b/lib/process-services-cloud/src/lib/group/public-api.ts index 7765d4e7a7..94cbce1bc1 100644 --- a/lib/process-services-cloud/src/lib/group/public-api.ts +++ b/lib/process-services-cloud/src/lib/group/public-api.ts @@ -15,7 +15,5 @@ * limitations under the License. */ -export * from './models/group.model'; -export * from './services/group-cloud.service'; export * from './components/group-cloud.component'; export * from './group-cloud.module'; diff --git a/lib/process-services-cloud/src/lib/group/services/group-cloud.service.ts b/lib/process-services-cloud/src/lib/group/services/group-cloud.service.ts deleted file mode 100644 index 265b24e4ad..0000000000 --- a/lib/process-services-cloud/src/lib/group/services/group-cloud.service.ts +++ /dev/null @@ -1,212 +0,0 @@ -/*! - * @license - * Copyright 2019 Alfresco Software, Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Injectable } from '@angular/core'; -import { from, of, Observable, throwError } from 'rxjs'; -import { map, catchError } from 'rxjs/operators'; - -import { AlfrescoApiService, AppConfigService, LogService } from '@alfresco/adf-core'; -import { GroupSearchParam, GroupRoleModel } from '../models/group.model'; - -@Injectable({ - providedIn: 'root' -}) -export class GroupCloudService { - - constructor( - private apiService: AlfrescoApiService, - private appConfigService: AppConfigService, - private logService: LogService - ) {} - - /** - * Finds groups filtered by name. - * @param searchParams Object containing the name filter string - * @returns List of group information - */ - findGroupsByName(searchParams: GroupSearchParam): Observable { - if (searchParams.name === '') { - return of([]); - } - const url = this.getGroupsApi(); - const httpMethod = 'GET', pathParams = {}, queryParams = {search: searchParams.name}, bodyParam = {}, headerParams = {}, - formParams = {}, contentTypes = ['application/json'], accepts = ['application/json']; - - return (from(this.apiService.getInstance().oauth2Auth.callCustomApi( - url, httpMethod, pathParams, queryParams, - headerParams, formParams, bodyParam, - contentTypes, accepts, Object, null, null) - )).pipe( - catchError((err) => this.handleError(err)) - ); - } - - /** - * Gets details for a specified group. - * @param groupId ID of the target group - * @returns Group details - */ - getGroupRoles(groupId: string): Observable { - const url = this.buildRolesUrl(groupId); - const httpMethod = 'GET', pathParams = {}, queryParams = {}, bodyParam = {}, headerParams = {}, - formParams = {}, contentTypes = ['application/json'], accepts = ['application/json']; - - return (from(this.apiService.getInstance().oauth2Auth.callCustomApi( - url, httpMethod, pathParams, queryParams, - headerParams, formParams, bodyParam, - contentTypes, accepts, Object, null, null) - )).pipe( - catchError((err) => this.handleError(err)) - ); - } - - /** - * Check that a group has one or more roles from the supplied list. - * @param groupId ID of the target group - * @param roleNames Array of role names - * @returns True if the group has one or more of the roles, false otherwise - */ - checkGroupHasRole(groupId: string, roleNames: string[]): Observable { - return this.getGroupRoles(groupId).pipe(map((groupRoles: GroupRoleModel[]) => { - let hasRole = false; - if (groupRoles && groupRoles.length > 0) { - roleNames.forEach((roleName: string) => { - const role = groupRoles.find((groupRole) => { - return roleName === groupRole.name; - }); - if (role) { - hasRole = true; - return; - } - }); - } - return hasRole; - })); - } - - /** - * Gets the client ID using the app name. - * @param applicationName Name of the app - * @returns client ID string - */ - getClientIdByApplicationName(applicationName: string): Observable { - const url = this.getApplicationIdApi(); - const httpMethod = 'GET', pathParams = {}, queryParams = {clientId: applicationName}, bodyParam = {}, headerParams = {}, formParams = {}, - contentTypes = ['application/json'], accepts = ['application/json']; - return from(this.apiService.getInstance() - .oauth2Auth.callCustomApi(url, httpMethod, pathParams, queryParams, headerParams, - formParams, bodyParam, contentTypes, - accepts, Object, null, null) - ).pipe( - map((response: any[]) => { - const clientId = response && response.length > 0 ? response[0].id : ''; - return clientId; - }), - catchError((err) => this.handleError(err)) - ); - } - - /** - * Gets client roles. - * @param groupId ID of the target group - * @param clientId ID of the client - * @returns List of roles - */ - getClientRoles(groupId: string, clientId: string): Observable { - const url = this.groupClientRoleMappingApi(groupId, clientId); - const httpMethod = 'GET', pathParams = {}, queryParams = {}, bodyParam = {}, headerParams = {}, - formParams = {}, contentTypes = ['application/json'], accepts = ['application/json']; - - return from(this.apiService.getInstance().oauth2Auth.callCustomApi( - url, httpMethod, pathParams, queryParams, - headerParams, formParams, bodyParam, - contentTypes, accepts, Object, null, null) - ); - } - - /** - * Checks if a group has a client app. - * @param groupId ID of the target group - * @param clientId ID of the client - * @returns True if the group has the client app, false otherwise - */ - checkGroupHasClientApp(groupId: string, clientId: string): Observable { - return this.getClientRoles(groupId, clientId).pipe( - map((response: any[]) => { - if (response && response.length > 0) { - return true; - } - return false; - }), - catchError((err) => this.handleError(err)) - ); - } - - /** - * Check if a group has any of the client app roles in the supplied list. - * @param groupId ID of the target group - * @param clientId ID of the client - * @param roleNames Array of role names to check - * @returns True if the group has one or more of the roles, false otherwise - */ - checkGroupHasAnyClientAppRole(groupId: string, clientId: string, roleNames: string[]): Observable { - return this.getClientRoles(groupId, clientId).pipe( - map((clientRoles: any[]) => { - let hasRole = false; - if (clientRoles.length > 0) { - roleNames.forEach((roleName) => { - const role = clientRoles.find((availableRole) => { - return availableRole.name === roleName; - }); - - if (role) { - hasRole = true; - return; - } - }); - } - return hasRole; - }), - catchError((err) => this.handleError(err)) - ); - } - - private groupClientRoleMappingApi(groupId: string, clientId: string): any { - return `${this.appConfigService.get('identityHost')}/groups/${groupId}/role-mappings/clients/${clientId}`; - } - - private getApplicationIdApi() { - return `${this.appConfigService.get('identityHost')}/clients`; - } - - private getGroupsApi() { - return `${this.appConfigService.get('identityHost')}/groups`; - } - - private buildRolesUrl(groupId: string): any { - return `${this.appConfigService.get('identityHost')}/groups/${groupId}/role-mappings/realm/composite`; - } - - /** - * Throw the error - * @param error - */ - private handleError(error: Response) { - this.logService.error(error); - return throwError(error || 'Server error'); - } -}