diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index e172f9717b..53a9ecb2ab 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -531,14 +531,17 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { * Create menu sections dependent on whether or not the current user can manage access control groups */ createAccessControlMenuSections() { - this.authorizationService.isAuthorized(FeatureID.CanManageGroups).subscribe((authorized) => { + observableCombineLatest( + this.authorizationService.isAuthorized(FeatureID.AdministratorOf), + this.authorizationService.isAuthorized(FeatureID.CanManageGroups) + ).subscribe(([isSiteAdmin, canManageGroups]) => { const menuList = [ /* Access Control */ { id: 'access_control_people', parentID: 'access_control', active: false, - visible: authorized, + visible: isSiteAdmin, model: { type: MenuItemType.LINK, text: 'menu.section.access_control_people', @@ -549,7 +552,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { id: 'access_control_groups', parentID: 'access_control', active: false, - visible: authorized, + visible: canManageGroups, model: { type: MenuItemType.LINK, text: 'menu.section.access_control_groups', @@ -571,7 +574,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { { id: 'access_control', active: false, - visible: authorized, + visible: canManageGroups || isSiteAdmin, model: { type: MenuItemType.TEXT, text: 'menu.section.access_control' diff --git a/src/app/access-control/access-control-routing-paths.ts b/src/app/access-control/access-control-routing-paths.ts index d229d12bd2..259aa311e7 100644 --- a/src/app/access-control/access-control-routing-paths.ts +++ b/src/app/access-control/access-control-routing-paths.ts @@ -3,6 +3,10 @@ import { getAccessControlModuleRoute } from '../app-routing-paths'; export const GROUP_EDIT_PATH = 'groups'; +export function getGroupsRoute() { + return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH).toString(); +} + export function getGroupEditRoute(id: string) { return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH, id).toString(); } diff --git a/src/app/access-control/access-control-routing.module.ts b/src/app/access-control/access-control-routing.module.ts index cac49938a9..e64b0d170a 100644 --- a/src/app/access-control/access-control-routing.module.ts +++ b/src/app/access-control/access-control-routing.module.ts @@ -5,6 +5,9 @@ import { GroupFormComponent } from './group-registry/group-form/group-form.compo import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; import { GROUP_EDIT_PATH } from './access-control-routing-paths'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { GroupPageGuard } from './group-registry/group-page.guard'; +import { GroupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; +import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; @NgModule({ imports: [ @@ -15,7 +18,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso resolve: { breadcrumb: I18nBreadcrumbResolver }, - data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' } + data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' }, + canActivate: [SiteAdministratorGuard] }, { path: GROUP_EDIT_PATH, @@ -23,7 +27,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso resolve: { breadcrumb: I18nBreadcrumbResolver }, - data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' } + data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' }, + canActivate: [GroupAdministratorGuard] }, { path: `${GROUP_EDIT_PATH}/newGroup`, @@ -31,7 +36,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso resolve: { breadcrumb: I18nBreadcrumbResolver }, - data: { title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup' } + data: { title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup' }, + canActivate: [GroupAdministratorGuard] }, { path: `${GROUP_EDIT_PATH}/:groupId`, @@ -39,7 +45,8 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso resolve: { breadcrumb: I18nBreadcrumbResolver }, - data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' } + data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' }, + canActivate: [GroupPageGuard] } ]) ] diff --git a/src/app/access-control/group-registry/group-page.guard.spec.ts b/src/app/access-control/group-registry/group-page.guard.spec.ts new file mode 100644 index 0000000000..48fa124c07 --- /dev/null +++ b/src/app/access-control/group-registry/group-page.guard.spec.ts @@ -0,0 +1,83 @@ +import { GroupPageGuard } from './group-page.guard'; +import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of as observableOf } from 'rxjs'; +import { AuthService } from '../../core/auth/auth.service'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; + +describe('GroupPageGuard', () => { + const groupsEndpointUrl = 'https://test.org/api/eperson/groups'; + const groupUuid = '0d6f89df-f95a-4829-943c-f21f434fb892'; + const groupEndpointUrl = `${groupsEndpointUrl}/${groupUuid}`; + const routeSnapshotWithGroupId = { + params: { + groupId: groupUuid, + } + } as unknown as ActivatedRouteSnapshot; + + let guard: GroupPageGuard; + let halEndpointService: HALEndpointService; + let authorizationService: AuthorizationDataService; + let router: Router; + let authService: AuthService; + + beforeEach(() => { + halEndpointService = jasmine.createSpyObj(['getEndpoint']); + (halEndpointService as any).getEndpoint.and.returnValue(observableOf(groupsEndpointUrl)); + + authorizationService = jasmine.createSpyObj(['isAuthorized']); + // NOTE: value is set in beforeEach + + router = jasmine.createSpyObj(['parseUrl']); + (router as any).parseUrl.and.returnValue = {}; + + authService = jasmine.createSpyObj(['isAuthenticated']); + (authService as any).isAuthenticated.and.returnValue(observableOf(true)); + + guard = new GroupPageGuard(halEndpointService, authorizationService, router, authService); + }); + + it('should be created', () => { + expect(guard).toBeTruthy(); + }); + + describe('canActivate', () => { + describe('when the current user can manage the group', () => { + beforeEach(() => { + (authorizationService as any).isAuthorized.and.returnValue(observableOf(true)); + }); + + it('should return true', (done) => { + guard.canActivate( + routeSnapshotWithGroupId, { url: 'current-url'} as any + ).subscribe((result) => { + expect(authorizationService.isAuthorized).toHaveBeenCalledWith( + FeatureID.CanManageGroup, groupEndpointUrl, undefined + ); + expect(result).toBeTrue(); + done(); + }); + }); + }); + + describe('when the current user can not manage the group', () => { + beforeEach(() => { + (authorizationService as any).isAuthorized.and.returnValue(observableOf(false)); + }); + + it('should not return true', (done) => { + guard.canActivate( + routeSnapshotWithGroupId, { url: 'current-url'} as any + ).subscribe((result) => { + expect(authorizationService.isAuthorized).toHaveBeenCalledWith( + FeatureID.CanManageGroup, groupEndpointUrl, undefined + ); + expect(result).not.toBeTrue(); + done(); + }); + }); + }); + }); + +}); diff --git a/src/app/access-control/group-registry/group-page.guard.ts b/src/app/access-control/group-registry/group-page.guard.ts new file mode 100644 index 0000000000..057f67ddeb --- /dev/null +++ b/src/app/access-control/group-registry/group-page.guard.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { Observable, of as observableOf } from 'rxjs'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { AuthService } from '../../core/auth/auth.service'; +import { SomeFeatureAuthorizationGuard } from '../../core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard'; +import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; +import { map } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class GroupPageGuard extends SomeFeatureAuthorizationGuard { + + protected groupsEndpoint = 'groups'; + + constructor(protected halEndpointService: HALEndpointService, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService) { + super(authorizationService, router, authService); + } + + getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf([FeatureID.CanManageGroup]); + } + + getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.halEndpointService.getEndpoint(this.groupsEndpoint).pipe( + map(groupsUrl => `${groupsUrl}/${route?.params?.groupId}`) + ); + } + +} diff --git a/src/app/access-control/group-registry/groups-registry.component.html b/src/app/access-control/group-registry/groups-registry.component.html index e5e25ae944..4135f4cc95 100644 --- a/src/app/access-control/group-registry/groups-registry.component.html +++ b/src/app/access-control/group-registry/groups-registry.component.html @@ -33,9 +33,9 @@ - + {{groupDto.epersons?.totalElements + groupDto.subgroups?.totalElements}}
- + + + +