diff --git a/docs/core/services/auth-guard-sso-role.service.md b/docs/core/services/auth-guard-sso-role.service.md index 3d8917caed..ae2f09edde 100644 --- a/docs/core/services/auth-guard-sso-role.service.md +++ b/docs/core/services/auth-guard-sso-role.service.md @@ -31,9 +31,10 @@ const appRoutes: Routes = [ ``` If the user now clicks on a link or button that follows this route, they will be not able to access this content if they do not have the Realms roles. +
**Note**: An additional role ALFRESCO_ADMINISTRATORS can be used in the roles array, which will result in checking whether the logged in user has Content Admin capabilities or not, as this role is not part of the JWT token it will call a Content API to determine it. -Client role Example +Client role Example ```ts const appRoutes: Routes = [ ... diff --git a/e2e/protractor.excludes.json b/e2e/protractor.excludes.json index 2cd61e8d20..bf3f9244f0 100644 --- a/e2e/protractor.excludes.json +++ b/e2e/protractor.excludes.json @@ -5,5 +5,7 @@ "C279931": "login problem APS not basic", "C279930": "login problem APS not basic", "C593560": "https://alfresco.atlassian.net/browse/ADF-5366", + "C269081": "https://alfresco.atlassian.net/browse/ADF-5385", + "C272819": "https://alfresco.atlassian.net/browse/ADF-5385", "C290069": "https://alfresco.atlassian.net/browse/ADF-5387" } diff --git a/lib/core/mock/ecm-user.service.mock.ts b/lib/core/mock/ecm-user.service.mock.ts index a968b3cdd2..813c0a3bc1 100644 --- a/lib/core/mock/ecm-user.service.mock.ts +++ b/lib/core/mock/ecm-user.service.mock.ts @@ -16,6 +16,7 @@ */ import { EcmCompanyModel } from '../models/ecm-company.model'; +import { PersonEntry, Person } from '@alfresco/js-api'; export let fakeEcmCompany: EcmCompanyModel = { organization: 'company-fake-name', @@ -99,3 +100,25 @@ export const createNewPersonMock = { password: 'fake-avatar-id', email: 'fakeEcm@ecmUser.com' }; + +export function getFakeUserWithContentAdminCapability(): PersonEntry { + const fakeEcmUserWithAdminCapabilities = { + ...fakeEcmUser, + capabilities: { + isAdmin: true + } + }; + const mockPerson = new Person(fakeEcmUserWithAdminCapabilities); + return { entry: mockPerson }; +} + +export function getFakeUserWithContentUserCapability(): PersonEntry { + const fakeEcmUserWithAdminCapabilities = { + ...fakeEcmUser, + capabilities: { + isAdmin: false + } + }; + const mockPerson = new Person(fakeEcmUserWithAdminCapabilities); + return { entry: mockPerson }; +} diff --git a/lib/core/services/auth-guard-sso-role.service.spec.ts b/lib/core/services/auth-guard-sso-role.service.spec.ts index fd0fd5b294..12a810903d 100644 --- a/lib/core/services/auth-guard-sso-role.service.spec.ts +++ b/lib/core/services/auth-guard-sso-role.service.spec.ts @@ -23,12 +23,16 @@ import { AuthGuardSsoRoleService } from './auth-guard-sso-role.service'; import { JwtHelperService } from './jwt-helper.service'; import { MatDialog } from '@angular/material/dialog'; import { TranslateModule } from '@ngx-translate/core'; +import { PeopleContentService } from './people-content.service'; +import { of } from 'rxjs'; +import { getFakeUserWithContentAdminCapability, getFakeUserWithContentUserCapability } from '../mock/ecm-user.service.mock'; describe('Auth Guard SSO role service', () => { let authGuard: AuthGuardSsoRoleService; let jwtHelperService: JwtHelperService; let routerService: Router; + let peopleContentService: PeopleContentService; setupTestBed({ imports: [ @@ -42,6 +46,7 @@ describe('Auth Guard SSO role service', () => { authGuard = TestBed.inject(AuthGuardSsoRoleService); jwtHelperService = TestBed.inject(JwtHelperService); routerService = TestBed.inject(Router); + peopleContentService = TestBed.inject(PeopleContentService); }); it('Should canActivate be true if the Role is present int the JWT token', async(async () => { @@ -185,4 +190,39 @@ describe('Auth Guard SSO role service', () => { expect(await authGuard.canActivate(route)).toBeFalsy(); expect(materialDialog.closeAll).toHaveBeenCalled(); }); + + describe('Content Admin', () => { + + afterEach(() => { + peopleContentService.hasCheckedIsContentAdmin = false; + }); + + it('Should give access to a content section (ALFRESCO_ADMINISTRATORS) when the user has content admin capability', async () => { + spyOn(peopleContentService, 'getCurrentPerson').and.returnValue(of(getFakeUserWithContentAdminCapability())); + + const router: ActivatedRouteSnapshot = new ActivatedRouteSnapshot(); + router.data = { 'roles': ['ALFRESCO_ADMINISTRATORS'] }; + + expect(await authGuard.canActivate(router)).toBeTruthy(); + }); + + it('Should not give access to a content section (ALFRESCO_ADMINISTRATORS) when the user does not have content admin capability', async () => { + spyOn(peopleContentService, 'getCurrentPerson').and.returnValue(of(getFakeUserWithContentUserCapability())); + + const router: ActivatedRouteSnapshot = new ActivatedRouteSnapshot(); + router.data = { 'roles': ['ALFRESCO_ADMINISTRATORS'] }; + + expect(await authGuard.canActivate(router)).toBeFalsy(); + }); + + it('Should not call the service to check if the user has content admin capability when the roles do not contain ALFRESCO_ADMINISTRATORS', async () => { + const getCurrentPersonSpy = spyOn(peopleContentService, 'getCurrentPerson').and.returnValue(of(getFakeUserWithContentAdminCapability())); + const router: ActivatedRouteSnapshot = new ActivatedRouteSnapshot(); + router.data = { 'roles': ['fakeRole'] }; + + await authGuard.canActivate(router); + + expect(getCurrentPersonSpy).not.toHaveBeenCalled(); + }); + }); }); diff --git a/lib/core/services/auth-guard-sso-role.service.ts b/lib/core/services/auth-guard-sso-role.service.ts index 648e188ee1..60c1087b0c 100644 --- a/lib/core/services/auth-guard-sso-role.service.ts +++ b/lib/core/services/auth-guard-sso-role.service.ts @@ -19,24 +19,28 @@ import { Injectable } from '@angular/core'; import { JwtHelperService } from './jwt-helper.service'; import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; +import { ContentGroups, PeopleContentService } from './people-content.service'; @Injectable({ providedIn: 'root' }) export class AuthGuardSsoRoleService implements CanActivate { - - constructor(private jwtHelperService: JwtHelperService, private router: Router, private dialog: MatDialog) { + constructor(private jwtHelperService: JwtHelperService, + private router: Router, + private dialog: MatDialog, + private peopleContentService: PeopleContentService) { } - canActivate(route: ActivatedRouteSnapshot): boolean { + async canActivate(route: ActivatedRouteSnapshot): Promise { let hasRole; let hasRealmRole = false; let hasClientRole = true; if (route.data) { if (route.data['roles']) { - const rolesToCheck = route.data['roles']; - hasRealmRole = this.jwtHelperService.hasRealmRoles(rolesToCheck); + const rolesToCheck: string[] = route.data['roles']; + const isContentAdmin = rolesToCheck.includes(ContentGroups.ALFRESCO_ADMINISTRATORS) ? await this.peopleContentService.isContentAdmin() : false; + hasRealmRole = this.jwtHelperService.hasRealmRoles(rolesToCheck) || isContentAdmin; } if (route.data['clientRoles']) { diff --git a/lib/core/services/people-content.service.spec.ts b/lib/core/services/people-content.service.spec.ts index cef4ff4ca5..5258c51df9 100644 --- a/lib/core/services/people-content.service.spec.ts +++ b/lib/core/services/people-content.service.spec.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { fakeEcmUser, createNewPersonMock } from '../mock/ecm-user.service.mock'; +import { fakeEcmUser, createNewPersonMock, getFakeUserWithContentAdminCapability } from '../mock/ecm-user.service.mock'; import { AlfrescoApiServiceMock } from '../mock/alfresco-api.service.mock'; import { CoreTestingModule } from '../testing/core.testing.module'; import { PeopleContentService } from './people-content.service'; @@ -24,6 +24,7 @@ import { setupTestBed } from '../testing/setup-test-bed'; import { TranslateModule } from '@ngx-translate/core'; import { TestBed } from '@angular/core/testing'; import { LogService } from './log.service'; +import { of } from 'rxjs'; describe('PeopleContentService', () => { @@ -101,4 +102,16 @@ describe('PeopleContentService', () => { done(); }); }); + + it('Should make the api call to check if the user is a content admin only once', async () => { + const getCurrentPersonSpy = spyOn(service.peopleApi, 'getPerson').and.returnValue(of(getFakeUserWithContentAdminCapability())); + + expect(await service.isContentAdmin()).toBe(true); + expect(getCurrentPersonSpy.calls.count()).toEqual(1); + + await service.isContentAdmin(); + + expect(await service.isContentAdmin()).toBe(true); + expect(getCurrentPersonSpy.calls.count()).toEqual(1); + }); }); diff --git a/lib/core/services/people-content.service.ts b/lib/core/services/people-content.service.ts index 883b6f30a8..827f52c946 100644 --- a/lib/core/services/people-content.service.ts +++ b/lib/core/services/people-content.service.ts @@ -23,10 +23,16 @@ import { PersonEntry, PeopleApi, PersonBodyCreate } from '@alfresco/js-api'; import { EcmUserModel } from '../models/ecm-user.model'; import { LogService } from './log.service'; +export enum ContentGroups { + ALFRESCO_ADMINISTRATORS = 'ALFRESCO_ADMINISTRATORS' +} + @Injectable({ providedIn: 'root' }) export class PeopleContentService { + private hasContentAdminRole: boolean = false; + hasCheckedIsContentAdmin: boolean = false; private _peopleApi: PeopleApi; @@ -60,6 +66,7 @@ export class PeopleContentService { /** * Creates new person. * @param newPerson Object containing the new person details. + * @param opts Optional parameters * @returns Created new person */ createPerson(newPerson: PersonBodyCreate, opts?: any): Observable { @@ -69,6 +76,15 @@ export class PeopleContentService { ); } + async isContentAdmin(): Promise { + if (!this.hasCheckedIsContentAdmin) { + const user: PersonEntry = await this.getCurrentPerson().toPromise(); + this.hasContentAdminRole = user?.entry?.capabilities?.isAdmin; + this.hasCheckedIsContentAdmin = true; + } + return this.hasContentAdminRole; + } + private handleError(error: any) { this.logService.error(error); return throwError(error || 'Server error');