From bbbeddc875774d712c2df08a110aa7396eddab2a Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 18 Jun 2020 17:13:37 +0200 Subject: [PATCH] 71429: SiteAdministratorGuard on admin routes + authorization check on visibility of admin sidebar sections --- .../eperson-form/eperson-form.component.ts | 2 +- .../admin-sidebar/admin-sidebar.component.ts | 65 ++++++++++--------- src/app/app-routing.module.ts | 3 +- src/app/core/core.module.ts | 2 + .../authorization-data.service.ts | 6 +- .../authorization-utils.ts | 2 +- .../feature-authorization/feature-type.ts | 3 +- .../site-administrator.guard.ts | 31 +++++++++ 8 files changed, 77 insertions(+), 37 deletions(-) create mode 100644 src/app/core/data/feature-authorization/site-administrator.guard.ts diff --git a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts index e9e446ed34..6d1efd956c 100644 --- a/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/+admin/admin-access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -245,7 +245,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { }); })); this.canImpersonate$ = this.epersonService.getActiveEPerson().pipe( - switchMap((eperson) => this.authorizationService.isAuthenticated(eperson.self, undefined, FeatureType.LoginOnBehalfOf)) + switchMap((eperson) => this.authorizationService.isAuthenticated(FeatureType.LoginOnBehalfOf, eperson.self)) ); }); } diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index f11313fb6d..b009aacb33 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -2,7 +2,7 @@ import { Component, Injector, OnInit } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { combineLatest as combineLatestObservable } from 'rxjs'; import { Observable } from 'rxjs/internal/Observable'; -import { first, map } from 'rxjs/operators'; +import { first, map, take } from 'rxjs/operators'; import { AuthService } from '../../core/auth/auth.service'; import { slideHorizontal, slideSidebar } from '../../shared/animations/slide'; import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; @@ -18,6 +18,8 @@ import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model import { MenuComponent } from '../../shared/menu/menu.component'; import { MenuService } from '../../shared/menu/menu.service'; import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { FeatureType } from '../../core/data/feature-authorization/feature-type'; /** * Component representing the admin sidebar @@ -61,7 +63,8 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { protected injector: Injector, private variableService: CSSVariableService, private authService: AuthService, - private modalService: NgbModal + private modalService: NgbModal, + private authorizationService: AuthorizationDataService ) { super(menuService, injector); } @@ -70,30 +73,32 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { * Set and calculate all initial values of the instance variables */ ngOnInit(): void { - this.createMenu(); - super.ngOnInit(); - this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth'); - this.authService.isAuthenticated() - .subscribe((loggedIn: boolean) => { - if (loggedIn) { - this.menuService.showMenu(this.menuID); - } - }); - this.menuCollapsed.pipe(first()) - .subscribe((collapsed: boolean) => { - this.sidebarOpen = !collapsed; - this.sidebarClosed = collapsed; - }); - this.sidebarExpanded = combineLatestObservable(this.menuCollapsed, this.menuPreviewCollapsed) - .pipe( - map(([collapsed, previewCollapsed]) => (!collapsed || !previewCollapsed)) - ); + this.authorizationService.isAuthenticated(FeatureType.AdministratorOf).pipe(take(1)).subscribe((authorized) => { + this.createMenu(authorized); + super.ngOnInit(); + this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth'); + this.authService.isAuthenticated() + .subscribe((loggedIn: boolean) => { + if (loggedIn) { + this.menuService.showMenu(this.menuID); + } + }); + this.menuCollapsed.pipe(first()) + .subscribe((collapsed: boolean) => { + this.sidebarOpen = !collapsed; + this.sidebarClosed = collapsed; + }); + this.sidebarExpanded = combineLatestObservable(this.menuCollapsed, this.menuPreviewCollapsed) + .pipe( + map(([collapsed, previewCollapsed]) => (!collapsed || !previewCollapsed)) + ); + }); } /** * Initialize all menu sections and items for this menu */ - createMenu() { + createMenu(isAdministratorOfSite: boolean) { const menuList = [ /* News */ { @@ -309,7 +314,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { { id: 'access_control', active: false, - visible: true, + visible: isAdministratorOfSite, model: { type: MenuItemType.TEXT, text: 'menu.section.access_control' @@ -321,7 +326,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { id: 'access_control_people', parentID: 'access_control', active: false, - visible: true, + visible: isAdministratorOfSite, model: { type: MenuItemType.LINK, text: 'menu.section.access_control_people', @@ -332,7 +337,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { id: 'access_control_groups', parentID: 'access_control', active: false, - visible: true, + visible: isAdministratorOfSite, model: { type: MenuItemType.LINK, text: 'menu.section.access_control_groups', @@ -343,7 +348,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { id: 'access_control_authorizations', parentID: 'access_control', active: false, - visible: true, + visible: isAdministratorOfSite, model: { type: MenuItemType.LINK, text: 'menu.section.access_control_authorizations', @@ -354,7 +359,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { { id: 'admin_search', active: false, - visible: true, + visible: isAdministratorOfSite, model: { type: MenuItemType.LINK, text: 'menu.section.admin_search', @@ -367,7 +372,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { { id: 'registries', active: false, - visible: true, + visible: isAdministratorOfSite, model: { type: MenuItemType.TEXT, text: 'menu.section.registries' @@ -379,7 +384,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { id: 'registries_metadata', parentID: 'registries', active: false, - visible: true, + visible: isAdministratorOfSite, model: { type: MenuItemType.LINK, text: 'menu.section.registries_metadata', @@ -390,7 +395,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { id: 'registries_format', parentID: 'registries', active: false, - visible: true, + visible: isAdministratorOfSite, model: { type: MenuItemType.LINK, text: 'menu.section.registries_format', @@ -443,7 +448,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { { id: 'workflow', active: false, - visible: true, + visible: isAdministratorOfSite, model: { type: MenuItemType.LINK, text: 'menu.section.workflow', diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 0ba0851e4e..0acec5e728 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -10,6 +10,7 @@ import { Collection } from './core/shared/collection.model'; import { Item } from './core/shared/item.model'; import { getItemPageRoute } from './+item-page/item-page-routing.module'; import { getCollectionPageRoute } from './+collection-page/collection-page-routing.module'; +import { SiteAdministratorGuard } from './core/data/feature-authorization/site-administrator.guard'; const ITEM_MODULE_PATH = 'items'; @@ -82,7 +83,7 @@ export function getDSOPath(dso: DSpaceObject): string { }, { path: 'search', loadChildren: './+search-page/search-page-routing.module#SearchPageRoutingModule' }, { path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'}, - { path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] }, + { path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [SiteAdministratorGuard] }, { path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' }, { path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' }, { path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' }, diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index af394ea71a..eaa384d8a5 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -149,6 +149,7 @@ import { Feature } from './shared/feature.model'; import { Authorization } from './shared/authorization.model'; import { FeatureDataService } from './data/feature-authorization/feature-data.service'; import { AuthorizationDataService } from './data/feature-authorization/authorization-data.service'; +import { SiteAdministratorGuard } from './data/feature-authorization/site-administrator.guard'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -270,6 +271,7 @@ const PROVIDERS = [ WorkflowActionDataService, FeatureDataService, AuthorizationDataService, + SiteAdministratorGuard, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/data/feature-authorization/authorization-data.service.ts b/src/app/core/data/feature-authorization/authorization-data.service.ts index 686ff94b19..94e49a149f 100644 --- a/src/app/core/data/feature-authorization/authorization-data.service.ts +++ b/src/app/core/data/feature-authorization/authorization-data.service.ts @@ -59,8 +59,8 @@ export class AuthorizationDataService extends DataService { * If not provided, the UUID of the currently authenticated {@link EPerson} will be used. * @param featureId ID of the {@link Feature} to check {@link Authorization} for */ - isAuthenticated(objectUrl?: string, ePersonUuid?: string, featureId?: FeatureType): Observable { - return this.searchByObject(objectUrl, ePersonUuid, featureId).pipe( + isAuthenticated(featureId?: FeatureType, objectUrl?: string, ePersonUuid?: string): Observable { + return this.searchByObject(featureId, objectUrl, ePersonUuid).pipe( map((authorizationRD) => (authorizationRD.statusCode !== 401 && hasValue(authorizationRD.payload) && isNotEmpty(authorizationRD.payload.page))) ); } @@ -76,7 +76,7 @@ export class AuthorizationDataService extends DataService { * @param options {@link FindListOptions} to provide pagination and/or additional arguments * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - searchByObject(objectUrl?: string, ePersonUuid?: string, featureId?: FeatureType, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + searchByObject(featureId?: FeatureType, objectUrl?: string, ePersonUuid?: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { return observableOf(new AuthorizationSearchParams(objectUrl, ePersonUuid, featureId)).pipe( addSiteObjectUrlIfEmpty(this.siteService), addAuthenticatedUserUuidIfEmpty(this.authService), diff --git a/src/app/core/data/feature-authorization/authorization-utils.ts b/src/app/core/data/feature-authorization/authorization-utils.ts index 6cc5a3d2ef..352d9f06cb 100644 --- a/src/app/core/data/feature-authorization/authorization-utils.ts +++ b/src/app/core/data/feature-authorization/authorization-utils.ts @@ -42,7 +42,7 @@ export const addAuthenticatedUserUuidIfEmpty = (authService: AuthService) => map((ePerson) => Object.assign({}, params, { ePersonUuid: ePerson.uuid })) ); } else { - observableOf(params) + return observableOf(params) } }) ); diff --git a/src/app/core/data/feature-authorization/feature-type.ts b/src/app/core/data/feature-authorization/feature-type.ts index a4cbc23d15..eaeaeca94f 100644 --- a/src/app/core/data/feature-authorization/feature-type.ts +++ b/src/app/core/data/feature-authorization/feature-type.ts @@ -2,5 +2,6 @@ * Enum object for all possible {@link Feature} types */ export enum FeatureType { - LoginOnBehalfOf = 'loginOnBehalfOf' + LoginOnBehalfOf = 'loginOnBehalfOf', + AdministratorOf = 'administratorOf' } diff --git a/src/app/core/data/feature-authorization/site-administrator.guard.ts b/src/app/core/data/feature-authorization/site-administrator.guard.ts new file mode 100644 index 0000000000..43208fba20 --- /dev/null +++ b/src/app/core/data/feature-authorization/site-administrator.guard.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivate, CanLoad, Route, RouterStateSnapshot, UrlSegment } from '@angular/router'; +import { Observable } from 'rxjs'; +import { AuthorizationDataService } from './authorization-data.service'; +import { FeatureType } from './feature-type'; + +/** + * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have administrator + * rights to the {@link Site} + */ +@Injectable({ + providedIn: 'root' +}) +export class SiteAdministratorGuard implements CanActivate, CanLoad { + constructor(private authorizationService: AuthorizationDataService) { + } + + /** + * True when user has administrator rights to the {@link Site} + */ + canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.authorizationService.isAuthenticated(FeatureType.AdministratorOf); + } + + /** + * True when user has administrator rights to the {@link Site} + */ + canLoad(route: Route, segments: UrlSegment[]): Observable { + return this.authorizationService.isAuthenticated(FeatureType.AdministratorOf); + } +}