mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge pull request #822 from atmire/Features-support-part-2
Features support part 2
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Collection } from '../core/shared/collection.model';
|
||||||
|
import { CollectionPageResolver } from './collection-page.resolver';
|
||||||
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Collection} pages requiring administrator rights
|
||||||
|
*/
|
||||||
|
export class CollectionPageAdministratorGuard extends DsoPageFeatureGuard<Collection> {
|
||||||
|
constructor(protected resolver: CollectionPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(resolver, authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check administrator authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.AdministratorOf);
|
||||||
|
}
|
||||||
|
}
|
@@ -19,6 +19,7 @@ import {
|
|||||||
COLLECTION_EDIT_PATH,
|
COLLECTION_EDIT_PATH,
|
||||||
COLLECTION_CREATE_PATH
|
COLLECTION_CREATE_PATH
|
||||||
} from './collection-page-routing-paths';
|
} from './collection-page-routing-paths';
|
||||||
|
import { CollectionPageAdministratorGuard } from './collection-page-administrator.guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -39,7 +40,7 @@ import {
|
|||||||
{
|
{
|
||||||
path: COLLECTION_EDIT_PATH,
|
path: COLLECTION_EDIT_PATH,
|
||||||
loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule',
|
loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule',
|
||||||
canActivate: [AuthenticatedGuard]
|
canActivate: [CollectionPageAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'delete',
|
path: 'delete',
|
||||||
@@ -78,7 +79,8 @@ import {
|
|||||||
CollectionBreadcrumbResolver,
|
CollectionBreadcrumbResolver,
|
||||||
DSOBreadcrumbsService,
|
DSOBreadcrumbsService,
|
||||||
LinkService,
|
LinkService,
|
||||||
CreateCollectionPageGuard
|
CreateCollectionPageGuard,
|
||||||
|
CollectionPageAdministratorGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CollectionPageRoutingModule {
|
export class CollectionPageRoutingModule {
|
||||||
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Community } from '../core/shared/community.model';
|
||||||
|
import { CommunityPageResolver } from './community-page.resolver';
|
||||||
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Community} pages requiring administrator rights
|
||||||
|
*/
|
||||||
|
export class CommunityPageAdministratorGuard extends DsoPageFeatureGuard<Community> {
|
||||||
|
constructor(protected resolver: CommunityPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(resolver, authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check administrator authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.AdministratorOf);
|
||||||
|
}
|
||||||
|
}
|
@@ -11,6 +11,7 @@ import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-bread
|
|||||||
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service';
|
||||||
import { LinkService } from '../core/cache/builders/link.service';
|
import { LinkService } from '../core/cache/builders/link.service';
|
||||||
import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-routing-paths';
|
import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-routing-paths';
|
||||||
|
import { CommunityPageAdministratorGuard } from './community-page-administrator.guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -31,7 +32,7 @@ import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-rou
|
|||||||
{
|
{
|
||||||
path: COMMUNITY_EDIT_PATH,
|
path: COMMUNITY_EDIT_PATH,
|
||||||
loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule',
|
loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule',
|
||||||
canActivate: [AuthenticatedGuard]
|
canActivate: [CommunityPageAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'delete',
|
path: 'delete',
|
||||||
@@ -53,7 +54,8 @@ import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-rou
|
|||||||
CommunityBreadcrumbResolver,
|
CommunityBreadcrumbResolver,
|
||||||
DSOBreadcrumbsService,
|
DSOBreadcrumbsService,
|
||||||
LinkService,
|
LinkService,
|
||||||
CreateCommunityPageGuard
|
CreateCommunityPageGuard,
|
||||||
|
CommunityPageAdministratorGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CommunityPageRoutingModule {
|
export class CommunityPageRoutingModule {
|
||||||
|
@@ -29,6 +29,8 @@ import {
|
|||||||
ITEM_EDIT_REINSTATE_PATH,
|
ITEM_EDIT_REINSTATE_PATH,
|
||||||
ITEM_EDIT_WITHDRAW_PATH
|
ITEM_EDIT_WITHDRAW_PATH
|
||||||
} from './edit-item-page.routing-paths';
|
} from './edit-item-page.routing-paths';
|
||||||
|
import { ItemPageReinstateGuard } from './item-page-reinstate.guard';
|
||||||
|
import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routing module that handles the routing for the Edit Item page administrator functionality
|
* Routing module that handles the routing for the Edit Item page administrator functionality
|
||||||
@@ -98,10 +100,12 @@ import {
|
|||||||
{
|
{
|
||||||
path: ITEM_EDIT_WITHDRAW_PATH,
|
path: ITEM_EDIT_WITHDRAW_PATH,
|
||||||
component: ItemWithdrawComponent,
|
component: ItemWithdrawComponent,
|
||||||
|
canActivate: [ItemPageWithdrawGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ITEM_EDIT_REINSTATE_PATH,
|
path: ITEM_EDIT_REINSTATE_PATH,
|
||||||
component: ItemReinstateComponent,
|
component: ItemReinstateComponent,
|
||||||
|
canActivate: [ItemPageReinstateGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ITEM_EDIT_PRIVATE_PATH,
|
path: ITEM_EDIT_PRIVATE_PATH,
|
||||||
@@ -154,7 +158,9 @@ import {
|
|||||||
I18nBreadcrumbResolver,
|
I18nBreadcrumbResolver,
|
||||||
I18nBreadcrumbsService,
|
I18nBreadcrumbsService,
|
||||||
ResourcePolicyResolver,
|
ResourcePolicyResolver,
|
||||||
ResourcePolicyTargetResolver
|
ResourcePolicyTargetResolver,
|
||||||
|
ItemPageReinstateGuard,
|
||||||
|
ItemPageWithdrawGuard
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class EditItemPageRoutingModule {
|
export class EditItemPageRoutingModule {
|
||||||
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { DsoPageFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring reinstate rights
|
||||||
|
*/
|
||||||
|
export class ItemPageReinstateGuard extends DsoPageFeatureGuard<Item> {
|
||||||
|
constructor(protected resolver: ItemPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(resolver, authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check reinstate authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.ReinstateItem);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { DsoPageFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ItemPageResolver } from '../item-page.resolver';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring withdraw rights
|
||||||
|
*/
|
||||||
|
export class ItemPageWithdrawGuard extends DsoPageFeatureGuard<Item> {
|
||||||
|
constructor(protected resolver: ItemPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(resolver, authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check withdraw authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.WithdrawItem);
|
||||||
|
}
|
||||||
|
}
|
@@ -15,7 +15,7 @@
|
|||||||
<a [routerLink]="getItemPage((itemRD$ | async)?.payload)">{{getItemPage((itemRD$ | async)?.payload)}}</a>
|
<a [routerLink]="getItemPage((itemRD$ | async)?.payload)">{{getItemPage((itemRD$ | async)?.payload)}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngFor="let operation of operations" class="w-100 pt-3">
|
<div *ngFor="let operation of (operations$ | async)" class="w-100" [ngClass]="{'pt-3': operation}">
|
||||||
<ds-item-operation [operation]="operation"></ds-item-operation>
|
<ds-item-operation *ngIf="operation" [operation]="operation"></ds-item-operation>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -12,6 +12,7 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
|
||||||
describe('ItemStatusComponent', () => {
|
describe('ItemStatusComponent', () => {
|
||||||
let comp: ItemStatusComponent;
|
let comp: ItemStatusComponent;
|
||||||
@@ -20,7 +21,10 @@ describe('ItemStatusComponent', () => {
|
|||||||
const mockItem = Object.assign(new Item(), {
|
const mockItem = Object.assign(new Item(), {
|
||||||
id: 'fake-id',
|
id: 'fake-id',
|
||||||
handle: 'fake/handle',
|
handle: 'fake/handle',
|
||||||
lastModified: '2018'
|
lastModified: '2018',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'test-item-selflink' }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemPageUrl = `items/${mockItem.id}`;
|
const itemPageUrl = `items/${mockItem.id}`;
|
||||||
@@ -31,13 +35,20 @@ describe('ItemStatusComponent', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||||
declarations: [ItemStatusComponent],
|
declarations: [ItemStatusComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: routeStub },
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
@@ -3,15 +3,19 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
|
|||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { ItemOperation } from '../item-operation/itemOperation.model';
|
import { ItemOperation } from '../item-operation/itemOperation.model';
|
||||||
import { first, map } from 'rxjs/operators';
|
import { distinctUntilChanged, first, map } from 'rxjs/operators';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
import { getItemEditRoute, getItemPageRoute } from '../../item-page-routing-paths';
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-status',
|
selector: 'ds-item-status',
|
||||||
templateUrl: './item-status.component.html',
|
templateUrl: './item-status.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.Default,
|
||||||
animations: [
|
animations: [
|
||||||
fadeIn,
|
fadeIn,
|
||||||
fadeInOut
|
fadeInOut
|
||||||
@@ -40,14 +44,15 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
* The possible actions that can be performed on the item
|
* The possible actions that can be performed on the item
|
||||||
* key: id value: url to action's component
|
* key: id value: url to action's component
|
||||||
*/
|
*/
|
||||||
operations: ItemOperation[];
|
operations$: BehaviorSubject<ItemOperation[]> = new BehaviorSubject<ItemOperation[]>([]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The keys of the actions (to loop over)
|
* The keys of the actions (to loop over)
|
||||||
*/
|
*/
|
||||||
actionsKeys;
|
actionsKeys;
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute) {
|
constructor(private route: ActivatedRoute,
|
||||||
|
private authorizationService: AuthorizationDataService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -67,21 +72,43 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
|
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
|
||||||
The value is supposed to be a href for the button
|
The value is supposed to be a href for the button
|
||||||
*/
|
*/
|
||||||
this.operations = [];
|
const operations = [];
|
||||||
this.operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations'));
|
operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations'));
|
||||||
this.operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper'));
|
operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper'));
|
||||||
if (item.isWithdrawn) {
|
operations.push(undefined);
|
||||||
this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate'));
|
// Store the index of the "withdraw" or "reinstate" operation, because it's added asynchronously
|
||||||
} else {
|
const indexOfWithdrawReinstate = operations.length - 1;
|
||||||
this.operations.push(new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw'));
|
|
||||||
}
|
|
||||||
if (item.isDiscoverable) {
|
if (item.isDiscoverable) {
|
||||||
this.operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private'));
|
operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private'));
|
||||||
} else {
|
} else {
|
||||||
this.operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public'));
|
operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public'));
|
||||||
|
}
|
||||||
|
operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete'));
|
||||||
|
operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move'));
|
||||||
|
|
||||||
|
this.operations$.next(operations);
|
||||||
|
|
||||||
|
if (item.isWithdrawn) {
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.ReinstateItem, item.self).pipe(distinctUntilChanged()).subscribe((authorized) => {
|
||||||
|
const newOperations = [...this.operations$.value];
|
||||||
|
if (authorized) {
|
||||||
|
newOperations[indexOfWithdrawReinstate] = new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate');
|
||||||
|
} else {
|
||||||
|
newOperations[indexOfWithdrawReinstate] = undefined;
|
||||||
|
}
|
||||||
|
this.operations$.next(newOperations);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.WithdrawItem, item.self).pipe(distinctUntilChanged()).subscribe((authorized) => {
|
||||||
|
const newOperations = [...this.operations$.value];
|
||||||
|
if (authorized) {
|
||||||
|
newOperations[indexOfWithdrawReinstate] = new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw');
|
||||||
|
} else {
|
||||||
|
newOperations[indexOfWithdrawReinstate] = undefined;
|
||||||
|
}
|
||||||
|
this.operations$.next(newOperations);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete'));
|
|
||||||
this.operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -102,4 +129,8 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
return getItemEditRoute(item.id);
|
return getItemEditRoute(item.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trackOperation(index: number, operation: ItemOperation) {
|
||||||
|
return hasValue(operation) ? operation.operationKey : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
30
src/app/+item-page/item-page-administrator.guard.ts
Normal file
30
src/app/+item-page/item-page-administrator.guard.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ItemPageResolver } from './item-page.resolver';
|
||||||
|
import { Item } from '../core/shared/item.model';
|
||||||
|
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights
|
||||||
|
*/
|
||||||
|
export class ItemPageAdministratorGuard extends DsoPageFeatureGuard<Item> {
|
||||||
|
constructor(protected resolver: ItemPageResolver,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(resolver, authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check administrator authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.AdministratorOf);
|
||||||
|
}
|
||||||
|
}
|
@@ -10,6 +10,7 @@ import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.servi
|
|||||||
import { LinkService } from '../core/cache/builders/link.service';
|
import { LinkService } from '../core/cache/builders/link.service';
|
||||||
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
||||||
import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths';
|
import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths';
|
||||||
|
import { ItemPageAdministratorGuard } from './item-page-administrator.guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -34,7 +35,7 @@ import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths
|
|||||||
{
|
{
|
||||||
path: ITEM_EDIT_PATH,
|
path: ITEM_EDIT_PATH,
|
||||||
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
||||||
canActivate: [AuthenticatedGuard]
|
canActivate: [ItemPageAdministratorGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: UPLOAD_BITSTREAM_PATH,
|
path: UPLOAD_BITSTREAM_PATH,
|
||||||
@@ -49,7 +50,8 @@ import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths
|
|||||||
ItemPageResolver,
|
ItemPageResolver,
|
||||||
ItemBreadcrumbResolver,
|
ItemBreadcrumbResolver,
|
||||||
DSOBreadcrumbsService,
|
DSOBreadcrumbsService,
|
||||||
LinkService
|
LinkService,
|
||||||
|
ItemPageAdministratorGuard
|
||||||
]
|
]
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@@ -21,6 +21,7 @@ import { COMMUNITY_MODULE_PATH } from './+community-page/community-page-routing-
|
|||||||
import { ITEM_MODULE_PATH } from './+item-page/item-page-routing-paths';
|
import { ITEM_MODULE_PATH } from './+item-page/item-page-routing-paths';
|
||||||
import { ReloadGuard } from './core/reload/reload.guard';
|
import { ReloadGuard } from './core/reload/reload.guard';
|
||||||
import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard';
|
import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-user-agreement-current-user.guard';
|
||||||
|
import { SiteRegisterGuard } from './core/data/feature-authorization/feature-authorization-guard/site-register.guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -33,7 +34,7 @@ import { EndUserAgreementCurrentUserGuard } from './core/end-user-agreement/end-
|
|||||||
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
{ path: 'community-list', loadChildren: './community-list-page/community-list-page.module#CommunityListPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
{ path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
{ path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
{ path: REGISTER_PATH, loadChildren: './register-page/register-page.module#RegisterPageModule' },
|
{ path: REGISTER_PATH, loadChildren: './register-page/register-page.module#RegisterPageModule', canActivate: [SiteRegisterGuard] },
|
||||||
{ path: FORGOT_PASSWORD_PATH, loadChildren: './forgot-password/forgot-password.module#ForgotPasswordModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
{ path: FORGOT_PASSWORD_PATH, loadChildren: './forgot-password/forgot-password.module#ForgotPasswordModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
{ path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
{ path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule', canActivate: [EndUserAgreementCurrentUserGuard] },
|
||||||
|
@@ -170,6 +170,7 @@ import { ReloadGuard } from './reload/reload.guard';
|
|||||||
import { EndUserAgreementCurrentUserGuard } from './end-user-agreement/end-user-agreement-current-user.guard';
|
import { EndUserAgreementCurrentUserGuard } from './end-user-agreement/end-user-agreement-current-user.guard';
|
||||||
import { EndUserAgreementCookieGuard } from './end-user-agreement/end-user-agreement-cookie.guard';
|
import { EndUserAgreementCookieGuard } from './end-user-agreement/end-user-agreement-cookie.guard';
|
||||||
import { EndUserAgreementService } from './end-user-agreement/end-user-agreement.service';
|
import { EndUserAgreementService } from './end-user-agreement/end-user-agreement.service';
|
||||||
|
import { SiteRegisterGuard } from './data/feature-authorization/feature-authorization-guard/site-register.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
|
||||||
@@ -292,6 +293,7 @@ const PROVIDERS = [
|
|||||||
FeatureDataService,
|
FeatureDataService,
|
||||||
AuthorizationDataService,
|
AuthorizationDataService,
|
||||||
SiteAdministratorGuard,
|
SiteAdministratorGuard,
|
||||||
|
SiteRegisterGuard,
|
||||||
MetadataSchemaDataService,
|
MetadataSchemaDataService,
|
||||||
MetadataFieldDataService,
|
MetadataFieldDataService,
|
||||||
TokenResponseParsingService,
|
TokenResponseParsingService,
|
||||||
|
@@ -63,33 +63,33 @@ describe('AuthorizationDataService', () => {
|
|||||||
return Object.assign(new FindListOptions(), { searchParams });
|
return Object.assign(new FindListOptions(), { searchParams });
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('when no arguments are provided and a user is authenticated', () => {
|
describe('when no arguments are provided', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.searchByObject().subscribe();
|
service.searchByObject().subscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call searchBy with the site\'s url and authenticated user\'s uuid', () => {
|
it('should call searchBy with the site\'s url', () => {
|
||||||
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self, ePerson.uuid));
|
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when no arguments except for a feature are provided and a user is authenticated', () => {
|
describe('when no arguments except for a feature are provided', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.searchByObject(FeatureID.LoginOnBehalfOf).subscribe();
|
service.searchByObject(FeatureID.LoginOnBehalfOf).subscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call searchBy with the site\'s url, authenticated user\'s uuid and the feature', () => {
|
it('should call searchBy with the site\'s url and the feature', () => {
|
||||||
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self, ePerson.uuid, FeatureID.LoginOnBehalfOf));
|
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self, null, FeatureID.LoginOnBehalfOf));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when a feature and object url are provided, but no user uuid and a user is authenticated', () => {
|
describe('when a feature and object url are provided', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.searchByObject(FeatureID.LoginOnBehalfOf, objectUrl).subscribe();
|
service.searchByObject(FeatureID.LoginOnBehalfOf, objectUrl).subscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call searchBy with the object\'s url, authenticated user\'s uuid and the feature', () => {
|
it('should call searchBy with the object\'s url and the feature', () => {
|
||||||
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, ePerson.uuid, FeatureID.LoginOnBehalfOf));
|
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, null, FeatureID.LoginOnBehalfOf));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,17 +102,6 @@ describe('AuthorizationDataService', () => {
|
|||||||
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, ePersonUuid, FeatureID.LoginOnBehalfOf));
|
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, ePersonUuid, FeatureID.LoginOnBehalfOf));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when no arguments are provided and no user is authenticated', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(authService, 'isAuthenticated').and.returnValue(observableOf(false));
|
|
||||||
service.searchByObject().subscribe();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call searchBy with the site\'s url', () => {
|
|
||||||
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isAuthorized', () => {
|
describe('isAuthorized', () => {
|
||||||
|
@@ -25,7 +25,6 @@ import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
|||||||
import { RequestParam } from '../../cache/models/request-param.model';
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
import { AuthorizationSearchParams } from './authorization-search-params';
|
import { AuthorizationSearchParams } from './authorization-search-params';
|
||||||
import {
|
import {
|
||||||
addAuthenticatedUserUuidIfEmpty,
|
|
||||||
addSiteObjectUrlIfEmpty,
|
addSiteObjectUrlIfEmpty,
|
||||||
oneAuthorizationMatchesFeature
|
oneAuthorizationMatchesFeature
|
||||||
} from './authorization-utils';
|
} from './authorization-utils';
|
||||||
@@ -90,7 +89,6 @@ export class AuthorizationDataService extends DataService<Authorization> {
|
|||||||
searchByObject(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<Authorization>>): Observable<RemoteData<PaginatedList<Authorization>>> {
|
searchByObject(featureId?: FeatureID, 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),
|
|
||||||
switchMap((params: AuthorizationSearchParams) => {
|
switchMap((params: AuthorizationSearchParams) => {
|
||||||
return this.searchBy(this.searchByObjectPath, this.createSearchOptions(params.objectUrl, options, params.ePersonUuid, params.featureId), ...linksToFollow);
|
return this.searchBy(this.searchByObjectPath, this.createSearchOptions(params.objectUrl, options, params.ePersonUuid, params.featureId), ...linksToFollow);
|
||||||
})
|
})
|
||||||
|
@@ -0,0 +1,63 @@
|
|||||||
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } 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';
|
||||||
|
import { DsoPageFeatureGuard } from './dso-page-feature.guard';
|
||||||
|
import { FeatureID } from '../feature-id';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test implementation of abstract class DsoPageAdministratorGuard
|
||||||
|
*/
|
||||||
|
class DsoPageFeatureGuardImpl extends DsoPageFeatureGuard<any> {
|
||||||
|
constructor(protected resolver: Resolve<RemoteData<any>>,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected featureID: FeatureID) {
|
||||||
|
super(resolver, authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(this.featureID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DsoPageAdministratorGuard', () => {
|
||||||
|
let guard: DsoPageFeatureGuard<any>;
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let router: Router;
|
||||||
|
let resolver: Resolve<RemoteData<any>>;
|
||||||
|
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 DsoPageFeatureGuardImpl(resolver, authorizationService, router, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { RemoteData } from '../../remote-data';
|
||||||
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { getAllSucceededRemoteDataPayload } from '../../../shared/operators';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||||
|
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature
|
||||||
|
* This guard utilizes a resolver to retrieve the relevant object to check authorizations for
|
||||||
|
*/
|
||||||
|
export abstract class DsoPageFeatureGuard<T extends DSpaceObject> extends FeatureAuthorizationGuard {
|
||||||
|
constructor(protected resolver: Resolve<RemoteData<T>>,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router) {
|
||||||
|
super(authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check authorization rights for the object resolved using the provided resolver
|
||||||
|
*/
|
||||||
|
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||||
|
return (this.resolver.resolve(route, state) as Observable<RemoteData<T>>).pipe(
|
||||||
|
getAllSucceededRemoteDataPayload(),
|
||||||
|
map((dso) => dso.self)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,8 @@ import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
|||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { of as observableOf } from 'rxjs';
|
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
|
* Test implementation of abstract class FeatureAuthorizationGuard
|
||||||
@@ -17,16 +18,16 @@ class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard {
|
|||||||
super(authorizationService, router);
|
super(authorizationService, router);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeatureID(): FeatureID {
|
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
return this.featureId;
|
return observableOf(this.featureId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getObjectUrl(): string {
|
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||||
return this.objectUrl;
|
return observableOf(this.objectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
getEPersonUuid(): string {
|
getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||||
return this.ePersonUuid;
|
return observableOf(this.ePersonUuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,6 +9,8 @@ import { AuthorizationDataService } from '../authorization-data.service';
|
|||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { returnUnauthorizedUrlTreeOnFalse } from '../../../shared/operators';
|
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
|
* 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
|
* 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
|
* Redirect the user to the unauthorized page when he/she's not authorized for the given feature
|
||||||
*/
|
*/
|
||||||
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||||
return this.authorizationService.isAuthorized(this.getFeatureID(), this.getObjectUrl(), this.getEPersonUuid()).pipe(returnUnauthorizedUrlTreeOnFalse(this.router));
|
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
|
* The type of feature to check authorization for
|
||||||
* Override this method to define a feature
|
* Override this method to define a feature
|
||||||
*/
|
*/
|
||||||
abstract getFeatureID(): FeatureID;
|
abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URL of the object to check if the user has authorized rights for
|
* 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
|
* Override this method to define an object URL. If not provided, the {@link Site}'s URL will be used
|
||||||
*/
|
*/
|
||||||
getObjectUrl(): string {
|
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||||
return undefined;
|
return observableOf(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The UUID of the user to check authorization rights for
|
* 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.
|
* Override this method to define an {@link EPerson} UUID. If not provided, the authenticated user's UUID will be used.
|
||||||
*/
|
*/
|
||||||
getEPersonUuid(): string {
|
getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||||
return undefined;
|
return observableOf(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,9 @@ import { Injectable } from '@angular/core';
|
|||||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||||
import { FeatureID } from '../feature-id';
|
import { FeatureID } from '../feature-id';
|
||||||
import { AuthorizationDataService } from '../authorization-data.service';
|
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
|
* 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
|
* Check administrator authorization rights
|
||||||
*/
|
*/
|
||||||
getFeatureID(): FeatureID {
|
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
return FeatureID.AdministratorOf;
|
return observableOf(FeatureID.AdministratorOf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,27 @@
|
|||||||
|
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
|
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { FeatureID } from '../feature-id';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have registration
|
||||||
|
* rights to the {@link Site}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class SiteRegisterGuard extends FeatureAuthorizationGuard {
|
||||||
|
constructor(protected authorizationService: AuthorizationDataService, protected router: Router) {
|
||||||
|
super(authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check registration authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||||
|
return observableOf(FeatureID.EPersonRegistration);
|
||||||
|
}
|
||||||
|
}
|
@@ -3,5 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
export enum FeatureID {
|
export enum FeatureID {
|
||||||
LoginOnBehalfOf = 'loginOnBehalfOf',
|
LoginOnBehalfOf = 'loginOnBehalfOf',
|
||||||
AdministratorOf = 'administratorOf'
|
AdministratorOf = 'administratorOf',
|
||||||
|
WithdrawItem = 'withdrawItem',
|
||||||
|
ReinstateItem = 'reinstateItem',
|
||||||
|
EPersonRegistration = 'epersonRegistration',
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,6 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<a class="dropdown-item" [routerLink]="[getRegisterRoute()]">{{"login.form.new-user" | translate}}</a>
|
<a class="dropdown-item" *ngIf="canRegister$ | async" [routerLink]="[getRegisterRoute()]">{{"login.form.new-user" | translate}}</a>
|
||||||
<a class="dropdown-item" [routerLink]="[getForgotRoute()]">{{"login.form.forgot-password" | translate}}</a>
|
<a class="dropdown-item" [routerLink]="[getForgotRoute()]">{{"login.form.forgot-password" | translate}}</a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -19,6 +19,8 @@ import { provideMockStore } from '@ngrx/store/testing';
|
|||||||
import { createTestComponent } from '../testing/utils.test';
|
import { createTestComponent } from '../testing/utils.test';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
import { HardRedirectService } from '../../core/services/hard-redirect.service';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { of } from 'rxjs/internal/observable/of';
|
||||||
|
|
||||||
describe('LogInComponent', () => {
|
describe('LogInComponent', () => {
|
||||||
|
|
||||||
@@ -36,11 +38,17 @@ describe('LogInComponent', () => {
|
|||||||
};
|
};
|
||||||
let hardRedirectService: HardRedirectService;
|
let hardRedirectService: HardRedirectService;
|
||||||
|
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
|
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
|
||||||
redirect: {},
|
redirect: {},
|
||||||
getCurrentRoute: {}
|
getCurrentRoute: {}
|
||||||
});
|
});
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: of(true)
|
||||||
|
});
|
||||||
|
|
||||||
// refine the test module by declaring the test component
|
// refine the test module by declaring the test component
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -65,6 +73,7 @@ describe('LogInComponent', () => {
|
|||||||
// { provide: Router, useValue: new RouterStub() },
|
// { provide: Router, useValue: new RouterStub() },
|
||||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
provideMockStore({ initialState }),
|
provideMockStore({ initialState }),
|
||||||
LogInComponent
|
LogInComponent
|
||||||
],
|
],
|
||||||
|
@@ -12,6 +12,8 @@ import { CoreState } from '../../core/core.reducers';
|
|||||||
import { getForgotPasswordRoute, getRegisterRoute } from '../../app-routing-paths';
|
import { getForgotPasswordRoute, getRegisterRoute } from '../../app-routing-paths';
|
||||||
import { hasValue } from '../empty.util';
|
import { hasValue } from '../empty.util';
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* /users/sign-in
|
* /users/sign-in
|
||||||
@@ -48,8 +50,14 @@ export class LogInComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
public loading: Observable<boolean>;
|
public loading: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the current user (or anonymous) is authorized to register an account
|
||||||
|
*/
|
||||||
|
canRegister$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(private store: Store<CoreState>,
|
constructor(private store: Store<CoreState>,
|
||||||
private authService: AuthService) {
|
private authService: AuthService,
|
||||||
|
private authorizationService: AuthorizationDataService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -70,6 +78,8 @@ export class LogInComponent implements OnInit {
|
|||||||
this.authService.clearRedirectUrl();
|
this.authService.clearRedirectUrl();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.canRegister$ = this.authorizationService.isAuthorized(FeatureID.EPersonRegistration);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRegisterRoute() {
|
getRegisterRoute() {
|
||||||
|
Reference in New Issue
Block a user