Merge pull request #822 from atmire/Features-support-part-2

Features support part 2
This commit is contained in:
Tim Donohue
2020-09-23 14:37:30 -05:00
committed by GitHub
26 changed files with 414 additions and 70 deletions

View File

@@ -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);
}
}

View File

@@ -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 {

View File

@@ -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);
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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();
}));

View File

@@ -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;
}
}

View 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);
}
}

View File

@@ -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
]
})

View File

@@ -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] },

View File

@@ -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,

View File

@@ -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', () => {

View File

@@ -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);
})

View File

@@ -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();
});
});
});
});

View File

@@ -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)
);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -3,5 +3,8 @@
*/
export enum FeatureID {
LoginOnBehalfOf = 'loginOnBehalfOf',
AdministratorOf = 'administratorOf'
AdministratorOf = 'administratorOf',
WithdrawItem = 'withdrawItem',
ReinstateItem = 'reinstateItem',
EPersonRegistration = 'epersonRegistration',
}

View File

@@ -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>

View File

@@ -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
],

View File

@@ -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() {