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_CREATE_PATH
|
||||
} from './collection-page-routing-paths';
|
||||
import { CollectionPageAdministratorGuard } from './collection-page-administrator.guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -39,7 +40,7 @@ import {
|
||||
{
|
||||
path: COLLECTION_EDIT_PATH,
|
||||
loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule',
|
||||
canActivate: [AuthenticatedGuard]
|
||||
canActivate: [CollectionPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: 'delete',
|
||||
@@ -78,7 +79,8 @@ import {
|
||||
CollectionBreadcrumbResolver,
|
||||
DSOBreadcrumbsService,
|
||||
LinkService,
|
||||
CreateCollectionPageGuard
|
||||
CreateCollectionPageGuard,
|
||||
CollectionPageAdministratorGuard
|
||||
]
|
||||
})
|
||||
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 { LinkService } from '../core/cache/builders/link.service';
|
||||
import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-routing-paths';
|
||||
import { CommunityPageAdministratorGuard } from './community-page-administrator.guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -31,7 +32,7 @@ import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-rou
|
||||
{
|
||||
path: COMMUNITY_EDIT_PATH,
|
||||
loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule',
|
||||
canActivate: [AuthenticatedGuard]
|
||||
canActivate: [CommunityPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: 'delete',
|
||||
@@ -53,7 +54,8 @@ import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-rou
|
||||
CommunityBreadcrumbResolver,
|
||||
DSOBreadcrumbsService,
|
||||
LinkService,
|
||||
CreateCommunityPageGuard
|
||||
CreateCommunityPageGuard,
|
||||
CommunityPageAdministratorGuard
|
||||
]
|
||||
})
|
||||
export class CommunityPageRoutingModule {
|
||||
|
@@ -29,6 +29,8 @@ import {
|
||||
ITEM_EDIT_REINSTATE_PATH,
|
||||
ITEM_EDIT_WITHDRAW_PATH
|
||||
} 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
|
||||
@@ -98,10 +100,12 @@ import {
|
||||
{
|
||||
path: ITEM_EDIT_WITHDRAW_PATH,
|
||||
component: ItemWithdrawComponent,
|
||||
canActivate: [ItemPageWithdrawGuard]
|
||||
},
|
||||
{
|
||||
path: ITEM_EDIT_REINSTATE_PATH,
|
||||
component: ItemReinstateComponent,
|
||||
canActivate: [ItemPageReinstateGuard]
|
||||
},
|
||||
{
|
||||
path: ITEM_EDIT_PRIVATE_PATH,
|
||||
@@ -154,7 +158,9 @@ import {
|
||||
I18nBreadcrumbResolver,
|
||||
I18nBreadcrumbsService,
|
||||
ResourcePolicyResolver,
|
||||
ResourcePolicyTargetResolver
|
||||
ResourcePolicyTargetResolver,
|
||||
ItemPageReinstateGuard,
|
||||
ItemPageWithdrawGuard
|
||||
]
|
||||
})
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div *ngFor="let operation of operations" class="w-100 pt-3">
|
||||
<ds-item-operation [operation]="operation"></ds-item-operation>
|
||||
<div *ngFor="let operation of (operations$ | async)" class="w-100" [ngClass]="{'pt-3': operation}">
|
||||
<ds-item-operation *ngIf="operation" [operation]="operation"></ds-item-operation>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -12,6 +12,7 @@ import { By } from '@angular/platform-browser';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
|
||||
describe('ItemStatusComponent', () => {
|
||||
let comp: ItemStatusComponent;
|
||||
@@ -20,7 +21,10 @@ describe('ItemStatusComponent', () => {
|
||||
const mockItem = Object.assign(new Item(), {
|
||||
id: 'fake-id',
|
||||
handle: 'fake/handle',
|
||||
lastModified: '2018'
|
||||
lastModified: '2018',
|
||||
_links: {
|
||||
self: { href: 'test-item-selflink' }
|
||||
}
|
||||
});
|
||||
|
||||
const itemPageUrl = `items/${mockItem.id}`;
|
||||
@@ -31,13 +35,20 @@ describe('ItemStatusComponent', () => {
|
||||
}
|
||||
};
|
||||
|
||||
let authorizationService: AuthorizationDataService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [ItemStatusComponent],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
], schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
@@ -3,15 +3,19 @@ import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
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 { RemoteData } from '../../../core/data/remote-data';
|
||||
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({
|
||||
selector: 'ds-item-status',
|
||||
templateUrl: './item-status.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
animations: [
|
||||
fadeIn,
|
||||
fadeInOut
|
||||
@@ -40,14 +44,15 @@ export class ItemStatusComponent implements OnInit {
|
||||
* The possible actions that can be performed on the item
|
||||
* key: id value: url to action's component
|
||||
*/
|
||||
operations: ItemOperation[];
|
||||
operations$: BehaviorSubject<ItemOperation[]> = new BehaviorSubject<ItemOperation[]>([]);
|
||||
|
||||
/**
|
||||
* The keys of the actions (to loop over)
|
||||
*/
|
||||
actionsKeys;
|
||||
|
||||
constructor(private route: ActivatedRoute) {
|
||||
constructor(private route: ActivatedRoute,
|
||||
private authorizationService: AuthorizationDataService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -67,21 +72,43 @@ export class ItemStatusComponent implements OnInit {
|
||||
i18n example: 'item.edit.tabs.status.buttons.<key>.label'
|
||||
The value is supposed to be a href for the button
|
||||
*/
|
||||
this.operations = [];
|
||||
this.operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations'));
|
||||
this.operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper'));
|
||||
if (item.isWithdrawn) {
|
||||
this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate'));
|
||||
} else {
|
||||
this.operations.push(new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw'));
|
||||
}
|
||||
const operations = [];
|
||||
operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations'));
|
||||
operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper'));
|
||||
operations.push(undefined);
|
||||
// Store the index of the "withdraw" or "reinstate" operation, because it's added asynchronously
|
||||
const indexOfWithdrawReinstate = operations.length - 1;
|
||||
if (item.isDiscoverable) {
|
||||
this.operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private'));
|
||||
operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private'));
|
||||
} 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);
|
||||
}
|
||||
|
||||
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 { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
|
||||
import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths';
|
||||
import { ItemPageAdministratorGuard } from './item-page-administrator.guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -34,7 +35,7 @@ import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths
|
||||
{
|
||||
path: ITEM_EDIT_PATH,
|
||||
loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule',
|
||||
canActivate: [AuthenticatedGuard]
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: UPLOAD_BITSTREAM_PATH,
|
||||
@@ -49,7 +50,8 @@ import { UPLOAD_BITSTREAM_PATH, ITEM_EDIT_PATH } from './item-page-routing-paths
|
||||
ItemPageResolver,
|
||||
ItemBreadcrumbResolver,
|
||||
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 { ReloadGuard } from './core/reload/reload.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({
|
||||
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: '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: 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: 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] },
|
||||
|
@@ -170,6 +170,7 @@ import { ReloadGuard } from './reload/reload.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 { 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
|
||||
@@ -292,6 +293,7 @@ const PROVIDERS = [
|
||||
FeatureDataService,
|
||||
AuthorizationDataService,
|
||||
SiteAdministratorGuard,
|
||||
SiteRegisterGuard,
|
||||
MetadataSchemaDataService,
|
||||
MetadataFieldDataService,
|
||||
TokenResponseParsingService,
|
||||
|
@@ -63,33 +63,33 @@ describe('AuthorizationDataService', () => {
|
||||
return Object.assign(new FindListOptions(), { searchParams });
|
||||
}
|
||||
|
||||
describe('when no arguments are provided and a user is authenticated', () => {
|
||||
describe('when no arguments are provided', () => {
|
||||
beforeEach(() => {
|
||||
service.searchByObject().subscribe();
|
||||
});
|
||||
|
||||
it('should call searchBy with the site\'s url and authenticated user\'s uuid', () => {
|
||||
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self, ePerson.uuid));
|
||||
it('should call searchBy with the site\'s url', () => {
|
||||
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(() => {
|
||||
service.searchByObject(FeatureID.LoginOnBehalfOf).subscribe();
|
||||
});
|
||||
|
||||
it('should call searchBy with the site\'s url, authenticated user\'s uuid and the feature', () => {
|
||||
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(site.self, ePerson.uuid, FeatureID.LoginOnBehalfOf));
|
||||
it('should call searchBy with the site\'s url and the feature', () => {
|
||||
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(() => {
|
||||
service.searchByObject(FeatureID.LoginOnBehalfOf, objectUrl).subscribe();
|
||||
});
|
||||
|
||||
it('should call searchBy with the object\'s url, authenticated user\'s uuid and the feature', () => {
|
||||
expect(service.searchBy).toHaveBeenCalledWith('object', createExpected(objectUrl, ePerson.uuid, FeatureID.LoginOnBehalfOf));
|
||||
it('should call searchBy with the object\'s url and the feature', () => {
|
||||
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));
|
||||
});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
@@ -25,7 +25,6 @@ import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { RequestParam } from '../../cache/models/request-param.model';
|
||||
import { AuthorizationSearchParams } from './authorization-search-params';
|
||||
import {
|
||||
addAuthenticatedUserUuidIfEmpty,
|
||||
addSiteObjectUrlIfEmpty,
|
||||
oneAuthorizationMatchesFeature
|
||||
} 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>>> {
|
||||
return observableOf(new AuthorizationSearchParams(objectUrl, ePersonUuid, featureId)).pipe(
|
||||
addSiteObjectUrlIfEmpty(this.siteService),
|
||||
addAuthenticatedUserUuidIfEmpty(this.authService),
|
||||
switchMap((params: AuthorizationSearchParams) => {
|
||||
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 { 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<FeatureID> {
|
||||
return observableOf(this.featureId);
|
||||
}
|
||||
|
||||
getObjectUrl(): string {
|
||||
return this.objectUrl;
|
||||
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||
return observableOf(this.objectUrl);
|
||||
}
|
||||
|
||||
getEPersonUuid(): string {
|
||||
return this.ePersonUuid;
|
||||
getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||
return observableOf(this.ePersonUuid);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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<boolean | UrlTree> {
|
||||
return this.authorizationService.isAuthorized(this.getFeatureID(), this.getObjectUrl(), this.getEPersonUuid()).pipe(returnUnauthorizedUrlTreeOnFalse(this.router));
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
|
||||
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<FeatureID>;
|
||||
|
||||
/**
|
||||
* 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<string> {
|
||||
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<string> {
|
||||
return observableOf(undefined);
|
||||
}
|
||||
}
|
||||
|
@@ -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<FeatureID> {
|
||||
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 {
|
||||
LoginOnBehalfOf = 'loginOnBehalfOf',
|
||||
AdministratorOf = 'administratorOf'
|
||||
AdministratorOf = 'administratorOf',
|
||||
WithdrawItem = 'withdrawItem',
|
||||
ReinstateItem = 'reinstateItem',
|
||||
EPersonRegistration = 'epersonRegistration',
|
||||
}
|
||||
|
@@ -8,6 +8,6 @@
|
||||
</ng-container>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
@@ -19,6 +19,8 @@ import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { createTestComponent } from '../testing/utils.test';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
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', () => {
|
||||
|
||||
@@ -36,11 +38,17 @@ describe('LogInComponent', () => {
|
||||
};
|
||||
let hardRedirectService: HardRedirectService;
|
||||
|
||||
let authorizationService: AuthorizationDataService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
hardRedirectService = jasmine.createSpyObj('hardRedirectService', {
|
||||
redirect: {},
|
||||
getCurrentRoute: {}
|
||||
});
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: of(true)
|
||||
});
|
||||
|
||||
// refine the test module by declaring the test component
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -65,6 +73,7 @@ describe('LogInComponent', () => {
|
||||
// { provide: Router, useValue: new RouterStub() },
|
||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
provideMockStore({ initialState }),
|
||||
LogInComponent
|
||||
],
|
||||
|
@@ -12,6 +12,8 @@ import { CoreState } from '../../core/core.reducers';
|
||||
import { getForgotPasswordRoute, getRegisterRoute } from '../../app-routing-paths';
|
||||
import { hasValue } from '../empty.util';
|
||||
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
|
||||
@@ -48,8 +50,14 @@ export class LogInComponent implements OnInit {
|
||||
*/
|
||||
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>,
|
||||
private authService: AuthService) {
|
||||
private authService: AuthService,
|
||||
private authorizationService: AuthorizationDataService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -70,6 +78,8 @@ export class LogInComponent implements OnInit {
|
||||
this.authService.clearRedirectUrl();
|
||||
}
|
||||
});
|
||||
|
||||
this.canRegister$ = this.authorizationService.isAuthorized(FeatureID.EPersonRegistration);
|
||||
}
|
||||
|
||||
getRegisterRoute() {
|
||||
|
Reference in New Issue
Block a user