mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
71429: SiteAdministratorGuard on admin routes + authorization check on visibility of admin sidebar sections
This commit is contained in:
@@ -245,7 +245,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
this.canImpersonate$ = this.epersonService.getActiveEPerson().pipe(
|
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))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@ import { Component, Injector, OnInit } from '@angular/core';
|
|||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { combineLatest as combineLatestObservable } from 'rxjs';
|
import { combineLatest as combineLatestObservable } from 'rxjs';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
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 { AuthService } from '../../core/auth/auth.service';
|
||||||
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
||||||
import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
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 { MenuComponent } from '../../shared/menu/menu.component';
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.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
|
* Component representing the admin sidebar
|
||||||
@@ -61,7 +63,8 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
protected injector: Injector,
|
protected injector: Injector,
|
||||||
private variableService: CSSVariableService,
|
private variableService: CSSVariableService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private modalService: NgbModal
|
private modalService: NgbModal,
|
||||||
|
private authorizationService: AuthorizationDataService
|
||||||
) {
|
) {
|
||||||
super(menuService, injector);
|
super(menuService, injector);
|
||||||
}
|
}
|
||||||
@@ -70,7 +73,8 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
* Set and calculate all initial values of the instance variables
|
* Set and calculate all initial values of the instance variables
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.createMenu();
|
this.authorizationService.isAuthenticated(FeatureType.AdministratorOf).pipe(take(1)).subscribe((authorized) => {
|
||||||
|
this.createMenu(authorized);
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
|
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
|
||||||
this.authService.isAuthenticated()
|
this.authService.isAuthenticated()
|
||||||
@@ -88,12 +92,13 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
.pipe(
|
.pipe(
|
||||||
map(([collapsed, previewCollapsed]) => (!collapsed || !previewCollapsed))
|
map(([collapsed, previewCollapsed]) => (!collapsed || !previewCollapsed))
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize all menu sections and items for this menu
|
* Initialize all menu sections and items for this menu
|
||||||
*/
|
*/
|
||||||
createMenu() {
|
createMenu(isAdministratorOfSite: boolean) {
|
||||||
const menuList = [
|
const menuList = [
|
||||||
/* News */
|
/* News */
|
||||||
{
|
{
|
||||||
@@ -309,7 +314,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
{
|
{
|
||||||
id: 'access_control',
|
id: 'access_control',
|
||||||
active: false,
|
active: false,
|
||||||
visible: true,
|
visible: isAdministratorOfSite,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.TEXT,
|
type: MenuItemType.TEXT,
|
||||||
text: 'menu.section.access_control'
|
text: 'menu.section.access_control'
|
||||||
@@ -321,7 +326,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
id: 'access_control_people',
|
id: 'access_control_people',
|
||||||
parentID: 'access_control',
|
parentID: 'access_control',
|
||||||
active: false,
|
active: false,
|
||||||
visible: true,
|
visible: isAdministratorOfSite,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.access_control_people',
|
text: 'menu.section.access_control_people',
|
||||||
@@ -332,7 +337,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
id: 'access_control_groups',
|
id: 'access_control_groups',
|
||||||
parentID: 'access_control',
|
parentID: 'access_control',
|
||||||
active: false,
|
active: false,
|
||||||
visible: true,
|
visible: isAdministratorOfSite,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.access_control_groups',
|
text: 'menu.section.access_control_groups',
|
||||||
@@ -343,7 +348,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
id: 'access_control_authorizations',
|
id: 'access_control_authorizations',
|
||||||
parentID: 'access_control',
|
parentID: 'access_control',
|
||||||
active: false,
|
active: false,
|
||||||
visible: true,
|
visible: isAdministratorOfSite,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.access_control_authorizations',
|
text: 'menu.section.access_control_authorizations',
|
||||||
@@ -354,7 +359,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
{
|
{
|
||||||
id: 'admin_search',
|
id: 'admin_search',
|
||||||
active: false,
|
active: false,
|
||||||
visible: true,
|
visible: isAdministratorOfSite,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.admin_search',
|
text: 'menu.section.admin_search',
|
||||||
@@ -367,7 +372,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
{
|
{
|
||||||
id: 'registries',
|
id: 'registries',
|
||||||
active: false,
|
active: false,
|
||||||
visible: true,
|
visible: isAdministratorOfSite,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.TEXT,
|
type: MenuItemType.TEXT,
|
||||||
text: 'menu.section.registries'
|
text: 'menu.section.registries'
|
||||||
@@ -379,7 +384,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
id: 'registries_metadata',
|
id: 'registries_metadata',
|
||||||
parentID: 'registries',
|
parentID: 'registries',
|
||||||
active: false,
|
active: false,
|
||||||
visible: true,
|
visible: isAdministratorOfSite,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.registries_metadata',
|
text: 'menu.section.registries_metadata',
|
||||||
@@ -390,7 +395,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
id: 'registries_format',
|
id: 'registries_format',
|
||||||
parentID: 'registries',
|
parentID: 'registries',
|
||||||
active: false,
|
active: false,
|
||||||
visible: true,
|
visible: isAdministratorOfSite,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.registries_format',
|
text: 'menu.section.registries_format',
|
||||||
@@ -443,7 +448,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
{
|
{
|
||||||
id: 'workflow',
|
id: 'workflow',
|
||||||
active: false,
|
active: false,
|
||||||
visible: true,
|
visible: isAdministratorOfSite,
|
||||||
model: {
|
model: {
|
||||||
type: MenuItemType.LINK,
|
type: MenuItemType.LINK,
|
||||||
text: 'menu.section.workflow',
|
text: 'menu.section.workflow',
|
||||||
|
@@ -10,6 +10,7 @@ import { Collection } from './core/shared/collection.model';
|
|||||||
import { Item } from './core/shared/item.model';
|
import { Item } from './core/shared/item.model';
|
||||||
import { getItemPageRoute } from './+item-page/item-page-routing.module';
|
import { getItemPageRoute } from './+item-page/item-page-routing.module';
|
||||||
import { getCollectionPageRoute } from './+collection-page/collection-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';
|
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: 'search', loadChildren: './+search-page/search-page-routing.module#SearchPageRoutingModule' },
|
||||||
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'},
|
{ 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: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
||||||
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
||||||
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
||||||
|
@@ -149,6 +149,7 @@ import { Feature } from './shared/feature.model';
|
|||||||
import { Authorization } from './shared/authorization.model';
|
import { Authorization } from './shared/authorization.model';
|
||||||
import { FeatureDataService } from './data/feature-authorization/feature-data.service';
|
import { FeatureDataService } from './data/feature-authorization/feature-data.service';
|
||||||
import { AuthorizationDataService } from './data/feature-authorization/authorization-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
|
* When not in production, endpoint responses can be mocked for testing purposes
|
||||||
@@ -270,6 +271,7 @@ const PROVIDERS = [
|
|||||||
WorkflowActionDataService,
|
WorkflowActionDataService,
|
||||||
FeatureDataService,
|
FeatureDataService,
|
||||||
AuthorizationDataService,
|
AuthorizationDataService,
|
||||||
|
SiteAdministratorGuard,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
{
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
@@ -59,8 +59,8 @@ export class AuthorizationDataService extends DataService<Authorization> {
|
|||||||
* If not provided, the UUID of the currently authenticated {@link EPerson} will be used.
|
* 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
|
* @param featureId ID of the {@link Feature} to check {@link Authorization} for
|
||||||
*/
|
*/
|
||||||
isAuthenticated(objectUrl?: string, ePersonUuid?: string, featureId?: FeatureType): Observable<boolean> {
|
isAuthenticated(featureId?: FeatureType, objectUrl?: string, ePersonUuid?: string): Observable<boolean> {
|
||||||
return this.searchByObject(objectUrl, ePersonUuid, featureId).pipe(
|
return this.searchByObject(featureId, objectUrl, ePersonUuid).pipe(
|
||||||
map((authorizationRD) => (authorizationRD.statusCode !== 401 && hasValue(authorizationRD.payload) && isNotEmpty(authorizationRD.payload.page)))
|
map((authorizationRD) => (authorizationRD.statusCode !== 401 && hasValue(authorizationRD.payload) && isNotEmpty(authorizationRD.payload.page)))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ export class AuthorizationDataService extends DataService<Authorization> {
|
|||||||
* @param options {@link FindListOptions} to provide pagination and/or additional arguments
|
* @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
|
* @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<FollowLinkConfig<Authorization>>): Observable<RemoteData<PaginatedList<Authorization>>> {
|
searchByObject(featureId?: FeatureType, objectUrl?: string, ePersonUuid?: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<Authorization>>): Observable<RemoteData<PaginatedList<Authorization>>> {
|
||||||
return observableOf(new AuthorizationSearchParams(objectUrl, ePersonUuid, featureId)).pipe(
|
return observableOf(new AuthorizationSearchParams(objectUrl, ePersonUuid, featureId)).pipe(
|
||||||
addSiteObjectUrlIfEmpty(this.siteService),
|
addSiteObjectUrlIfEmpty(this.siteService),
|
||||||
addAuthenticatedUserUuidIfEmpty(this.authService),
|
addAuthenticatedUserUuidIfEmpty(this.authService),
|
||||||
|
@@ -42,7 +42,7 @@ export const addAuthenticatedUserUuidIfEmpty = (authService: AuthService) =>
|
|||||||
map((ePerson) => Object.assign({}, params, { ePersonUuid: ePerson.uuid }))
|
map((ePerson) => Object.assign({}, params, { ePersonUuid: ePerson.uuid }))
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
observableOf(params)
|
return observableOf(params)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@@ -2,5 +2,6 @@
|
|||||||
* Enum object for all possible {@link Feature} types
|
* Enum object for all possible {@link Feature} types
|
||||||
*/
|
*/
|
||||||
export enum FeatureType {
|
export enum FeatureType {
|
||||||
LoginOnBehalfOf = 'loginOnBehalfOf'
|
LoginOnBehalfOf = 'loginOnBehalfOf',
|
||||||
|
AdministratorOf = 'administratorOf'
|
||||||
}
|
}
|
||||||
|
@@ -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<boolean> {
|
||||||
|
return this.authorizationService.isAuthenticated(FeatureType.AdministratorOf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when user has administrator rights to the {@link Site}
|
||||||
|
*/
|
||||||
|
canLoad(route: Route, segments: UrlSegment[]): Observable<boolean> {
|
||||||
|
return this.authorizationService.isAuthenticated(FeatureType.AdministratorOf);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user