diff --git a/src/app/+collection-page/collection-page-administrator.guard.ts b/src/app/+collection-page/collection-page-administrator.guard.ts new file mode 100644 index 0000000000..dcb9f545a9 --- /dev/null +++ b/src/app/+collection-page/collection-page-administrator.guard.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { DsoPageAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-administrator.guard'; +import { Collection } from '../core/shared/collection.model'; +import { CollectionPageResolver } from './collection-page.resolver'; +import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Collection} pages requiring administrator rights + */ +export class CollectionPageAdministratorGuard extends DsoPageAdministratorGuard { + constructor(protected resolver: CollectionPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router) { + super(resolver, authorizationService, router); + } +} diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index 1ebd9b3630..ebe086375f 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -16,6 +16,7 @@ import { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-bre import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service'; import { LinkService } from '../core/cache/builders/link.service'; import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { CollectionPageAdministratorGuard } from './collection-page-administrator.guard'; export const COLLECTION_PARENT_PARAMETER = 'parent'; @@ -54,7 +55,7 @@ const ITEMTEMPLATE_PATH = 'itemtemplate'; { path: COLLECTION_EDIT_PATH, loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule', - canActivate: [AuthenticatedGuard] + canActivate: [CollectionPageAdministratorGuard] }, { path: 'delete', @@ -93,7 +94,8 @@ const ITEMTEMPLATE_PATH = 'itemtemplate'; CollectionBreadcrumbResolver, DSOBreadcrumbsService, LinkService, - CreateCollectionPageGuard + CreateCollectionPageGuard, + CollectionPageAdministratorGuard ] }) export class CollectionPageRoutingModule { diff --git a/src/app/+community-page/community-page-administrator.guard.ts b/src/app/+community-page/community-page-administrator.guard.ts new file mode 100644 index 0000000000..886a449951 --- /dev/null +++ b/src/app/+community-page/community-page-administrator.guard.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { DsoPageAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-administrator.guard'; +import { Community } from '../core/shared/community.model'; +import { CommunityPageResolver } from './community-page.resolver'; +import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Community} pages requiring administrator rights + */ +export class CommunityPageAdministratorGuard extends DsoPageAdministratorGuard { + constructor(protected resolver: CommunityPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router) { + super(resolver, authorizationService, router); + } +} diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index 9922bc2c01..384574d9be 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -12,6 +12,7 @@ import { getCommunityModulePath } from '../app-routing.module'; import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver'; import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service'; import { LinkService } from '../core/cache/builders/link.service'; +import { CommunityPageAdministratorGuard } from './community-page-administrator.guard'; export const COMMUNITY_PARENT_PARAMETER = 'parent'; @@ -49,7 +50,7 @@ const COMMUNITY_EDIT_PATH = 'edit'; { path: COMMUNITY_EDIT_PATH, loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule', - canActivate: [AuthenticatedGuard] + canActivate: [CommunityPageAdministratorGuard] }, { path: 'delete', @@ -71,7 +72,8 @@ const COMMUNITY_EDIT_PATH = 'edit'; CommunityBreadcrumbResolver, DSOBreadcrumbsService, LinkService, - CreateCommunityPageGuard + CreateCommunityPageGuard, + CommunityPageAdministratorGuard ] }) export class CommunityPageRoutingModule { diff --git a/src/app/+item-page/item-page-administrator.guard.ts b/src/app/+item-page/item-page-administrator.guard.ts new file mode 100644 index 0000000000..507f8fcdbe --- /dev/null +++ b/src/app/+item-page/item-page-administrator.guard.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { DsoPageAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-administrator.guard'; +import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; +import { ItemPageResolver } from './item-page.resolver'; +import { Item } from '../core/shared/item.model'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights + */ +export class ItemPageAdministratorGuard extends DsoPageAdministratorGuard { + constructor(protected resolver: ItemPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router) { + super(resolver, authorizationService, router); + } +} diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts index 52faf96236..fc5cfa3522 100644 --- a/src/app/+item-page/item-page-routing.module.ts +++ b/src/app/+item-page/item-page-routing.module.ts @@ -11,6 +11,7 @@ import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.reso import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service'; import { LinkService } from '../core/cache/builders/link.service'; import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component'; +import { ItemPageAdministratorGuard } from './item-page-administrator.guard'; export function getItemPageRoute(itemId: string) { return new URLCombiner(getItemModulePath(), itemId).toString(); @@ -46,7 +47,7 @@ const UPLOAD_BITSTREAM_PATH = 'bitstreams/new'; { path: ITEM_EDIT_PATH, loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule', - canActivate: [AuthenticatedGuard] + canActivate: [ItemPageAdministratorGuard] }, { path: UPLOAD_BITSTREAM_PATH, @@ -61,7 +62,8 @@ const UPLOAD_BITSTREAM_PATH = 'bitstreams/new'; ItemPageResolver, ItemBreadcrumbResolver, DSOBreadcrumbsService, - LinkService + LinkService, + ItemPageAdministratorGuard ] }) diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-administrator.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-administrator.guard.spec.ts new file mode 100644 index 0000000000..7fef2e5d4c --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-administrator.guard.spec.ts @@ -0,0 +1,56 @@ +import { DsoPageAdministratorGuard } from './dso-page-administrator.guard'; +import { AuthorizationDataService } from '../authorization-data.service'; +import { Resolve, Router } from '@angular/router'; +import { RemoteData } from '../../remote-data'; +import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { DSpaceObject } from '../../../shared/dspace-object.model'; + +/** + * Test implementation of abstract class DsoPageAdministratorGuard + */ +class DsoPageAdministratorGuardImpl extends DsoPageAdministratorGuard { + constructor(protected resolver: Resolve>, + protected authorizationService: AuthorizationDataService, + protected router: Router) { + super(resolver, authorizationService, router); + } +} + +describe('DsoPageAdministratorGuard', () => { + let guard: DsoPageAdministratorGuard; + let authorizationService: AuthorizationDataService; + let router: Router; + let resolver: Resolve>; + let object: DSpaceObject; + + function init() { + object = { + self: 'test-selflink' + } as DSpaceObject; + + authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true) + }); + router = jasmine.createSpyObj('router', { + parseUrl: {} + }); + resolver = jasmine.createSpyObj('resolver', { + resolve: createSuccessfulRemoteDataObject$(object) + }); + guard = new DsoPageAdministratorGuardImpl(resolver, authorizationService, router); + } + + beforeEach(() => { + init(); + }); + + describe('getObjectUrl', () => { + it('should return the resolved object\'s selflink', (done) => { + guard.getObjectUrl(undefined, undefined).subscribe((selflink) => { + expect(selflink).toEqual(object.self); + done(); + }); + }); + }); +}); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-administrator.guard.ts new file mode 100644 index 0000000000..868510eb4e --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-administrator.guard.ts @@ -0,0 +1,39 @@ +import { FeatureAuthorizationGuard } from './feature-authorization.guard'; +import { AuthorizationDataService } from '../authorization-data.service'; +import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; +import { DSpaceObject } from '../../../shared/dspace-object.model'; +import { FeatureID } from '../feature-id'; +import { of as observableOf } from 'rxjs'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from '../../remote-data'; +import { getAllSucceededRemoteDataPayload } from '../../../shared/operators'; +import { map } from 'rxjs/operators'; + +/** + * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require administrator rights + * This guard utilizes a resolver to retrieve the relevant object to check authorizations for + */ +export abstract class DsoPageAdministratorGuard extends FeatureAuthorizationGuard { + constructor(protected resolver: Resolve>, + protected authorizationService: AuthorizationDataService, + protected router: Router) { + super(authorizationService, router); + } + + /** + * Check administrator authorization rights + */ + getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.AdministratorOf); + } + + /** + * Check authorization rights for the object resolved using the provided resolver + */ + getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return (this.resolver.resolve(route, state) as Observable>).pipe( + getAllSucceededRemoteDataPayload(), + map((dso) => dso.self) + ); + } +} diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.spec.ts index bfd161bad2..829a246dcc 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.spec.ts @@ -2,7 +2,8 @@ import { FeatureAuthorizationGuard } from './feature-authorization.guard'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; import { of as observableOf } from 'rxjs'; -import { Router } from '@angular/router'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs/internal/Observable'; /** * Test implementation of abstract class FeatureAuthorizationGuard @@ -17,16 +18,16 @@ class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard { super(authorizationService, router); } - getFeatureID(): FeatureID { - return this.featureId; + getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.featureId); } - getObjectUrl(): string { - return this.objectUrl; + getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.objectUrl); } - getEPersonUuid(): string { - return this.ePersonUuid; + getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.ePersonUuid); } } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.ts index 7806d87b0c..d53e71e289 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.ts @@ -9,6 +9,8 @@ import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; import { Observable } from 'rxjs/internal/Observable'; import { returnUnauthorizedUrlTreeOnFalse } from '../../../shared/operators'; +import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; /** * Abstract Guard for preventing unauthorized activating and loading of routes when a user @@ -24,29 +26,32 @@ export abstract class FeatureAuthorizationGuard implements CanActivate { * True when user has authorization rights for the feature and object provided * Redirect the user to the unauthorized page when he/she's not authorized for the given feature */ - canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.authorizationService.isAuthorized(this.getFeatureID(), this.getObjectUrl(), this.getEPersonUuid()).pipe(returnUnauthorizedUrlTreeOnFalse(this.router)); + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableCombineLatest(this.getFeatureID(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe( + switchMap(([featureID, objectUrl, ePersonUuid]) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid)), + returnUnauthorizedUrlTreeOnFalse(this.router) + ); } /** * The type of feature to check authorization for * Override this method to define a feature */ - abstract getFeatureID(): FeatureID; + abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable; /** * The URL of the object to check if the user has authorized rights for * Override this method to define an object URL. If not provided, the {@link Site}'s URL will be used */ - getObjectUrl(): string { - return undefined; + getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(undefined); } /** * The UUID of the user to check authorization rights for * Override this method to define an {@link EPerson} UUID. If not provided, the authenticated user's UUID will be used. */ - getEPersonUuid(): string { - return undefined; + getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(undefined); } } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts index a64e40468d..a45049645a 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts @@ -2,7 +2,9 @@ import { Injectable } from '@angular/core'; import { FeatureAuthorizationGuard } from './feature-authorization.guard'; import { FeatureID } from '../feature-id'; import { AuthorizationDataService } from '../authorization-data.service'; -import { Router } from '@angular/router'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { of as observableOf } from 'rxjs'; +import { Observable } from 'rxjs/internal/Observable'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have administrator @@ -19,7 +21,7 @@ export class SiteAdministratorGuard extends FeatureAuthorizationGuard { /** * Check administrator authorization rights */ - getFeatureID(): FeatureID { - return FeatureID.AdministratorOf; + getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.AdministratorOf); } }