diff --git a/src/app/+collection-page/collection-page-administrator.guard.ts b/src/app/+collection-page/collection-page-administrator.guard.ts index 748cca81cb..c7866515b2 100644 --- a/src/app/+collection-page/collection-page-administrator.guard.ts +++ b/src/app/+collection-page/collection-page-administrator.guard.ts @@ -4,7 +4,7 @@ import { Collection } from '../core/shared/collection.model'; import { CollectionPageResolver } from './collection-page.resolver'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { Observable, of as observableOf } from 'rxjs'; -import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard'; +import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { AuthService } from '../core/auth/auth.service'; @@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service'; /** * Guard for preventing unauthorized access to certain {@link Collection} pages requiring administrator rights */ -export class CollectionPageAdministratorGuard extends DsoPageFeatureGuard { +export class CollectionPageAdministratorGuard extends DsoPageSingleFeatureGuard { constructor(protected resolver: CollectionPageResolver, protected authorizationService: AuthorizationDataService, protected router: Router, diff --git a/src/app/+community-page/community-page-administrator.guard.ts b/src/app/+community-page/community-page-administrator.guard.ts index fad4a78f07..fd7ce5f7bf 100644 --- a/src/app/+community-page/community-page-administrator.guard.ts +++ b/src/app/+community-page/community-page-administrator.guard.ts @@ -4,7 +4,7 @@ import { Community } from '../core/shared/community.model'; import { CommunityPageResolver } from './community-page.resolver'; import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; import { Observable, of as observableOf } from 'rxjs'; -import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard'; +import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { AuthService } from '../core/auth/auth.service'; @@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service'; /** * Guard for preventing unauthorized access to certain {@link Community} pages requiring administrator rights */ -export class CommunityPageAdministratorGuard extends DsoPageFeatureGuard { +export class CommunityPageAdministratorGuard extends DsoPageSingleFeatureGuard { constructor(protected resolver: CommunityPageResolver, protected authorizationService: AuthorizationDataService, protected router: Router, diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index b7d650d8c3..2535e42216 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -31,8 +31,13 @@ import { } from './edit-item-page.routing-paths'; import { ItemPageReinstateGuard } from './item-page-reinstate.guard'; import { ItemPageWithdrawGuard } from './item-page-withdraw.guard'; -import { ItemPageEditMetadataGuard } from '../item-page-edit-metadata.guard'; +import { ItemPageMetadataGuard } from './item-page-metadata.guard'; import { ItemPageAdministratorGuard } from '../item-page-administrator.guard'; +import { ItemPageStatusGuard } from './item-page-status.guard'; +import { ItemPageBitstreamsGuard } from './item-page-bitstreams.guard'; +import { ItemPageRelationshipsGuard } from './item-page-relationships.guard'; +import { ItemPageVersionHistoryGuard } from './item-page-version-history.guard'; +import { ItemPageCollectionMapperGuard } from './item-page-collection-mapper.guard'; /** * Routing module that handles the routing for the Edit Item page administrator functionality @@ -60,25 +65,25 @@ import { ItemPageAdministratorGuard } from '../item-page-administrator.guard'; path: 'status', component: ItemStatusComponent, data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true }, - canActivate: [ItemPageAdministratorGuard] + canActivate: [ItemPageStatusGuard] }, { path: 'bitstreams', component: ItemBitstreamsComponent, data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true }, - canActivate: [ItemPageAdministratorGuard] + canActivate: [ItemPageBitstreamsGuard] }, { path: 'metadata', component: ItemMetadataComponent, data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true }, - canActivate: [ItemPageEditMetadataGuard] + canActivate: [ItemPageMetadataGuard] }, { path: 'relationships', component: ItemRelationshipsComponent, data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true }, - canActivate: [ItemPageEditMetadataGuard] + canActivate: [ItemPageRelationshipsGuard] }, /* TODO - uncomment & fix when view page exists { @@ -96,13 +101,13 @@ import { ItemPageAdministratorGuard } from '../item-page-administrator.guard'; path: 'versionhistory', component: ItemVersionHistoryComponent, data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true }, - canActivate: [ItemPageAdministratorGuard] + canActivate: [ItemPageVersionHistoryGuard] }, { path: 'mapper', component: ItemCollectionMapperComponent, data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true }, - canActivate: [ItemPageAdministratorGuard] + canActivate: [ItemPageCollectionMapperGuard] } ] }, @@ -175,7 +180,12 @@ import { ItemPageAdministratorGuard } from '../item-page-administrator.guard'; ItemPageReinstateGuard, ItemPageWithdrawGuard, ItemPageAdministratorGuard, - ItemPageEditMetadataGuard, + ItemPageMetadataGuard, + ItemPageStatusGuard, + ItemPageBitstreamsGuard, + ItemPageRelationshipsGuard, + ItemPageVersionHistoryGuard, + ItemPageCollectionMapperGuard, ] }) export class EditItemPageRoutingModule { diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html index e3989f02f3..ecbc19aea8 100644 --- a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.html @@ -3,13 +3,15 @@ {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}} - -
- - {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}} +
+ + + + +
diff --git a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts index 00e7f8452a..7570119b3a 100644 --- a/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-operation/item-operation.component.spec.ts @@ -28,19 +28,19 @@ describe('ItemOperationComponent', () => { }); it('should render operation row', () => { - const span = fixture.debugElement.query(By.css('span')).nativeElement; + const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement; expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label'); - const link = fixture.debugElement.query(By.css('a')).nativeElement; - expect(link.href).toContain('url1'); - expect(link.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); + const button = fixture.debugElement.query(By.css('button')).nativeElement; + expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); }); it('should render disabled operation row', () => { itemOperation.setDisabled(true); fixture.detectChanges(); - const span = fixture.debugElement.query(By.css('span')).nativeElement; + const span = fixture.debugElement.query(By.css('.action-label span')).nativeElement; expect(span.textContent).toContain('item.edit.tabs.status.buttons.key1.label'); - const span2 = fixture.debugElement.query(By.css('span.btn-danger')).nativeElement; - expect(span2.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); + const button = fixture.debugElement.query(By.css('button')).nativeElement; + expect(button.disabled).toBeTrue(); + expect(button.textContent).toContain('item.edit.tabs.status.buttons.key1.button'); }); }); diff --git a/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts b/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts index 105889d42d..33302dcba6 100644 --- a/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts +++ b/src/app/+item-page/edit-item-page/item-operation/itemOperation.model.ts @@ -1,3 +1,5 @@ +import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; + /** * Represents an item operation used on the edit item page with a key, an operation URL to which will be navigated * when performing the action and an option to disable the operation. @@ -7,11 +9,15 @@ export class ItemOperation { operationKey: string; operationUrl: string; disabled: boolean; + authorized: boolean; + featureID: FeatureID; - constructor(operationKey: string, operationUrl: string) { + constructor(operationKey: string, operationUrl: string, featureID?: FeatureID, disabled = false, authorized = true) { this.operationKey = operationKey; this.operationUrl = operationUrl; - this.setDisabled(false); + this.featureID = featureID; + this.authorized = authorized; + this.setDisabled(disabled); } /** diff --git a/src/app/+item-page/edit-item-page/item-page-bitstreams.guard.ts b/src/app/+item-page/edit-item-page/item-page-bitstreams.guard.ts new file mode 100644 index 0000000000..764c6ac7c8 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-page-bitstreams.guard.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-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, of as observableOf } from 'rxjs'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthService } from '../../core/auth/auth.service'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Item} pages requiring manage bitstreams rights + */ +export class ItemPageBitstreamsGuard extends DsoPageSingleFeatureGuard { + constructor(protected resolver: ItemPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService) { + super(resolver, authorizationService, router, authService); + } + + /** + * Check manage bitstreams authorization rights + */ + getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.CanManageBitstreamBundles); + } +} diff --git a/src/app/+item-page/edit-item-page/item-page-collection-mapper.guard.ts b/src/app/+item-page/edit-item-page/item-page-collection-mapper.guard.ts new file mode 100644 index 0000000000..2380377aea --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-page-collection-mapper.guard.ts @@ -0,0 +1,31 @@ +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 { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; +import { Observable, of as observableOf } from 'rxjs'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthService } from '../../core/auth/auth.service'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Item} pages requiring manage mappings rights + */ +export class ItemPageCollectionMapperGuard extends DsoPageSingleFeatureGuard { + constructor(protected resolver: ItemPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService) { + super(resolver, authorizationService, router, authService); + } + + /** + * Check manage mappings authorization rights + */ + getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.CanManageMappings); + } +} diff --git a/src/app/+item-page/item-page-edit-metadata.guard.ts b/src/app/+item-page/edit-item-page/item-page-metadata.guard.ts similarity index 59% rename from src/app/+item-page/item-page-edit-metadata.guard.ts rename to src/app/+item-page/edit-item-page/item-page-metadata.guard.ts index a9b870b1cd..a6846bec4e 100644 --- a/src/app/+item-page/item-page-edit-metadata.guard.ts +++ b/src/app/+item-page/edit-item-page/item-page-metadata.guard.ts @@ -1,12 +1,12 @@ 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 { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { ItemPageResolver } from '../item-page.resolver'; +import { Item } from '../../core/shared/item.model'; +import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { Observable, of as observableOf } from 'rxjs'; -import { FeatureID } from '../core/data/feature-authorization/feature-id'; -import { AuthService } from '../core/auth/auth.service'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthService } from '../../core/auth/auth.service'; @Injectable({ providedIn: 'root' @@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service'; /** * Guard for preventing unauthorized access to certain {@link Item} pages requiring edit metadata rights */ -export class ItemPageEditMetadataGuard extends DsoPageFeatureGuard { +export class ItemPageMetadataGuard extends DsoPageSingleFeatureGuard { constructor(protected resolver: ItemPageResolver, protected authorizationService: AuthorizationDataService, protected router: Router, diff --git a/src/app/+item-page/edit-item-page/item-page-reinstate.guard.ts b/src/app/+item-page/edit-item-page/item-page-reinstate.guard.ts index 0288e30b0a..88c9c20b12 100644 --- a/src/app/+item-page/edit-item-page/item-page-reinstate.guard.ts +++ b/src/app/+item-page/edit-item-page/item-page-reinstate.guard.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { DsoPageFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard'; +import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-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'; @@ -14,7 +14,7 @@ import { AuthService } from '../../core/auth/auth.service'; /** * Guard for preventing unauthorized access to certain {@link Item} pages requiring reinstate rights */ -export class ItemPageReinstateGuard extends DsoPageFeatureGuard { +export class ItemPageReinstateGuard extends DsoPageSingleFeatureGuard { constructor(protected resolver: ItemPageResolver, protected authorizationService: AuthorizationDataService, protected router: Router, diff --git a/src/app/+item-page/edit-item-page/item-page-relationships.guard.ts b/src/app/+item-page/edit-item-page/item-page-relationships.guard.ts new file mode 100644 index 0000000000..77da92ae02 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-page-relationships.guard.ts @@ -0,0 +1,31 @@ +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 { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; +import { Observable, of as observableOf } from 'rxjs'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthService } from '../../core/auth/auth.service'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Item} pages requiring manage relationships rights + */ +export class ItemPageRelationshipsGuard extends DsoPageSingleFeatureGuard { + constructor(protected resolver: ItemPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService) { + super(resolver, authorizationService, router, authService); + } + + /** + * Check manage relationships authorization rights + */ + getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.CanManageRelationships); + } +} diff --git a/src/app/+item-page/edit-item-page/item-page-status.guard.ts b/src/app/+item-page/edit-item-page/item-page-status.guard.ts new file mode 100644 index 0000000000..98f963a4be --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-page-status.guard.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +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, of as observableOf } from 'rxjs'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthService } from '../../core/auth/auth.service'; +import { DsoPageSomeFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Item} pages requiring any of the rights required for + * the status page + */ +export class ItemPageStatusGuard extends DsoPageSomeFeatureGuard { + constructor(protected resolver: ItemPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService) { + super(resolver, authorizationService, router, authService); + } + + /** + * Check authorization rights + */ + getFeatureIDs(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf([FeatureID.CanManageMappings, FeatureID.WithdrawItem, FeatureID.ReinstateItem, FeatureID.CanManagePolicies, FeatureID.CanMakePrivate, FeatureID.CanDelete, FeatureID.CanMove]); + } +} diff --git a/src/app/+item-page/edit-item-page/item-page-version-history.guard.ts b/src/app/+item-page/edit-item-page/item-page-version-history.guard.ts new file mode 100644 index 0000000000..dccdd9e641 --- /dev/null +++ b/src/app/+item-page/edit-item-page/item-page-version-history.guard.ts @@ -0,0 +1,31 @@ +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 { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; +import { Observable, of as observableOf } from 'rxjs'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { AuthService } from '../../core/auth/auth.service'; + +@Injectable({ + providedIn: 'root' +}) +/** + * Guard for preventing unauthorized access to certain {@link Item} pages requiring manage versions rights + */ +export class ItemPageVersionHistoryGuard extends DsoPageSingleFeatureGuard { + constructor(protected resolver: ItemPageResolver, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService) { + super(resolver, authorizationService, router, authService); + } + + /** + * Check manage versions authorization rights + */ + getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(FeatureID.CanManageVersions); + } +} diff --git a/src/app/+item-page/edit-item-page/item-page-withdraw.guard.ts b/src/app/+item-page/edit-item-page/item-page-withdraw.guard.ts index 243f751974..de9b7d0147 100644 --- a/src/app/+item-page/edit-item-page/item-page-withdraw.guard.ts +++ b/src/app/+item-page/edit-item-page/item-page-withdraw.guard.ts @@ -1,4 +1,4 @@ -import { DsoPageFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard'; +import { DsoPageSingleFeatureGuard } from '../../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { Item } from '../../core/shared/item.model'; import { Injectable } from '@angular/core'; import { ItemPageResolver } from '../item-page.resolver'; @@ -14,7 +14,7 @@ import { AuthService } from '../../core/auth/auth.service'; /** * Guard for preventing unauthorized access to certain {@link Item} pages requiring withdraw rights */ -export class ItemPageWithdrawGuard extends DsoPageFeatureGuard { +export class ItemPageWithdrawGuard extends DsoPageSingleFeatureGuard { constructor(protected resolver: ItemPageResolver, protected authorizationService: AuthorizationDataService, protected router: Router, diff --git a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts index 2745fc8df7..f01f5c1f7a 100644 --- a/src/app/+item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/+item-page/edit-item-page/item-status/item-status.component.ts @@ -3,8 +3,8 @@ 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 { distinctUntilChanged, first, map } from 'rxjs/operators'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { distinctUntilChanged, first, map, mergeMap, toArray } from 'rxjs/operators'; +import { BehaviorSubject, Observable, from as observableFrom } 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'; @@ -78,42 +78,36 @@ export class ItemStatusComponent implements OnInit { The value is supposed to be a href for the button */ 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) { - operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private')); + operations.push(new ItemOperation('authorizations', this.getCurrentUrl(item) + '/authorizations', FeatureID.CanManagePolicies, true)); + operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper', FeatureID.CanManageMappings, true)); + if (item.isWithdrawn) { + operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate', FeatureID.ReinstateItem, true)); } else { - operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public')); + operations.push(new ItemOperation('withdraw', this.getCurrentUrl(item) + '/withdraw', FeatureID.WithdrawItem, true)); } - operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete')); - operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move')); + if (item.isDiscoverable) { + operations.push(new ItemOperation('private', this.getCurrentUrl(item) + '/private', FeatureID.CanMakePrivate, true)); + } else { + operations.push(new ItemOperation('public', this.getCurrentUrl(item) + '/public', FeatureID.CanMakePrivate, true)); + } + operations.push(new ItemOperation('delete', this.getCurrentUrl(item) + '/delete', FeatureID.CanDelete, true)); + operations.push(new ItemOperation('move', this.getCurrentUrl(item) + '/move', FeatureID.CanMove, true)); 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'); + observableFrom(operations).pipe( + mergeMap((operation) => { + if (hasValue(operation.featureID)) { + return this.authorizationService.isAuthorized(operation.featureID, item.self).pipe( + distinctUntilChanged(), + map((authorized) => new ItemOperation(operation.operationKey, operation.operationUrl, operation.featureID, !authorized, authorized)) + ); } else { - newOperations[indexOfWithdrawReinstate] = undefined; + return [operation]; } - 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); - }); - } + }), + toArray() + ).subscribe((ops) => this.operations$.next(ops)); }); this.itemPageRoute$ = this.itemRD$.pipe( getAllSucceededRemoteDataPayload(), diff --git a/src/app/+item-page/item-page-administrator.guard.ts b/src/app/+item-page/item-page-administrator.guard.ts index c90502472e..5d3464aa75 100644 --- a/src/app/+item-page/item-page-administrator.guard.ts +++ b/src/app/+item-page/item-page-administrator.guard.ts @@ -3,7 +3,7 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/ro 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 { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { Observable, of as observableOf } from 'rxjs'; import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { AuthService } from '../core/auth/auth.service'; @@ -14,7 +14,7 @@ import { AuthService } from '../core/auth/auth.service'; /** * Guard for preventing unauthorized access to certain {@link Item} pages requiring administrator rights */ -export class ItemPageAdministratorGuard extends DsoPageFeatureGuard { +export class ItemPageAdministratorGuard extends DsoPageSingleFeatureGuard { constructor(protected resolver: ItemPageResolver, protected authorizationService: AuthorizationDataService, protected router: Router, diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts index bc39397ed9..b41a322cb6 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { FeatureAuthorizationGuard } from './feature-authorization.guard'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; import { AuthorizationDataService } from '../authorization-data.service'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; import { AuthService } from '../../../auth/auth.service'; @@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id'; @Injectable({ providedIn: 'root' }) -export class CollectionAdministratorGuard extends FeatureAuthorizationGuard { +export class CollectionAdministratorGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts index afb1fea63d..2ab77a00cc 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { FeatureAuthorizationGuard } from './feature-authorization.guard'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; import { AuthorizationDataService } from '../authorization-data.service'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; import { AuthService } from '../../../auth/auth.service'; @@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id'; @Injectable({ providedIn: 'root' }) -export class CommunityAdministratorGuard extends FeatureAuthorizationGuard { +export class CommunityAdministratorGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts similarity index 85% rename from src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.spec.ts rename to src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts index f98e3f1837..6c1f330c69 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts @@ -4,14 +4,14 @@ import { RemoteData } from '../../remote-data'; import { Observable, 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 { DsoPageSingleFeatureGuard } from './dso-page-single-feature.guard'; import { FeatureID } from '../feature-id'; import { AuthService } from '../../../auth/auth.service'; /** - * Test implementation of abstract class DsoPageAdministratorGuard + * Test implementation of abstract class DsoPageSingleFeatureGuard */ -class DsoPageFeatureGuardImpl extends DsoPageFeatureGuard { +class DsoPageSingleFeatureGuardImpl extends DsoPageSingleFeatureGuard { constructor(protected resolver: Resolve>, protected authorizationService: AuthorizationDataService, protected router: Router, @@ -25,8 +25,8 @@ class DsoPageFeatureGuardImpl extends DsoPageFeatureGuard { } } -describe('DsoPageAdministratorGuard', () => { - let guard: DsoPageFeatureGuard; +describe('DsoPageSingleFeatureGuard', () => { + let guard: DsoPageSingleFeatureGuard; let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; @@ -62,7 +62,7 @@ describe('DsoPageAdministratorGuard', () => { }, parent: parentRoute }; - guard = new DsoPageFeatureGuardImpl(resolver, authorizationService, router, authService, undefined); + guard = new DsoPageSingleFeatureGuardImpl(resolver, authorizationService, router, authService, undefined); } beforeEach(() => { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts new file mode 100644 index 0000000000..3fc90f9069 --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts @@ -0,0 +1,27 @@ +import { DSpaceObject } from '../../../shared/dspace-object.model'; +import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; +import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { FeatureID } from '../feature-id'; +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; + +/** + * 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 DsoPageSingleFeatureGuard extends DsoPageSomeFeatureGuard { + /** + * The features to check authorization for + */ + getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.getFeatureID(route, state).pipe( + map((featureID) => [featureID]), + ); + } + + /** + * The type of feature to check authorization for + * Override this method to define a feature + */ + abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable; +} diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts new file mode 100644 index 0000000000..071b1b0731 --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts @@ -0,0 +1,87 @@ +import { AuthorizationDataService } from '../authorization-data.service'; +import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; +import { RemoteData } from '../../remote-data'; +import { Observable, of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { DSpaceObject } from '../../../shared/dspace-object.model'; +import { FeatureID } from '../feature-id'; +import { AuthService } from '../../../auth/auth.service'; +import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; + +/** + * Test implementation of abstract class DsoPageSomeFeatureGuard + */ +class DsoPageSomeFeatureGuardImpl extends DsoPageSomeFeatureGuard { + constructor(protected resolver: Resolve>, + protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService, + protected featureIDs: FeatureID[]) { + super(resolver, authorizationService, router, authService); + } + + getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.featureIDs); + } +} + +describe('DsoPageSomeFeatureGuard', () => { + let guard: DsoPageSomeFeatureGuard; + let authorizationService: AuthorizationDataService; + let router: Router; + let authService: AuthService; + let resolver: Resolve>; + let object: DSpaceObject; + let route; + let parentRoute; + + 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) + }); + authService = jasmine.createSpyObj('authService', { + isAuthenticated: observableOf(true) + }); + parentRoute = { + params: { + id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0' + } + }; + route = { + params: { + }, + parent: parentRoute + }; + guard = new DsoPageSomeFeatureGuardImpl(resolver, authorizationService, router, authService, []); + } + + beforeEach(() => { + init(); + }); + + describe('getObjectUrl', () => { + it('should return the resolved object\'s selflink', (done) => { + guard.getObjectUrl(route, undefined).subscribe((selflink) => { + expect(selflink).toEqual(object.self); + done(); + }); + }); + }); + + describe('getRouteWithDSOId', () => { + it('should return the route that has the UUID of the DSO', () => { + const foundRoute = (guard as any).getRouteWithDSOId(route); + expect(foundRoute).toBe(parentRoute); + }); + }); +}); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts similarity index 87% rename from src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.ts rename to src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts index c50dd7f95d..8683709345 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts @@ -5,15 +5,15 @@ import { Observable } from 'rxjs'; import { getAllSucceededRemoteDataPayload } from '../../../shared/operators'; import { map } from 'rxjs/operators'; import { DSpaceObject } from '../../../shared/dspace-object.model'; -import { FeatureAuthorizationGuard } from './feature-authorization.guard'; import { AuthService } from '../../../auth/auth.service'; import { hasNoValue, hasValue } from '../../../../shared/empty.util'; +import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; /** - * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature + * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for any specific feature in a list * This guard utilizes a resolver to retrieve the relevant object to check authorizations for */ -export abstract class DsoPageFeatureGuard extends FeatureAuthorizationGuard { +export abstract class DsoPageSomeFeatureGuard extends SomeFeatureAuthorizationGuard { constructor(protected resolver: Resolve>, protected authorizationService: AuthorizationDataService, protected router: Router, diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts index 3fee767fdc..5afd572326 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { FeatureAuthorizationGuard } from './feature-authorization.guard'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; import { AuthorizationDataService } from '../authorization-data.service'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; import { AuthService } from '../../../auth/auth.service'; @@ -13,7 +13,7 @@ import { FeatureID } from '../feature-id'; @Injectable({ providedIn: 'root' }) -export class GroupAdministratorGuard extends FeatureAuthorizationGuard { +export class GroupAdministratorGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts similarity index 82% rename from src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.spec.ts rename to src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts index 2c6f4b0717..635aa3530b 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts @@ -1,4 +1,4 @@ -import { FeatureAuthorizationGuard } from './feature-authorization.guard'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; import { Observable, of as observableOf } from 'rxjs'; @@ -6,10 +6,10 @@ import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/ro import { AuthService } from '../../../auth/auth.service'; /** - * Test implementation of abstract class FeatureAuthorizationGuard + * Test implementation of abstract class SingleFeatureAuthorizationGuard * Provide the return values of the overwritten getters as constructor arguments */ -class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard { +class SingleFeatureAuthorizationGuardImpl extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService, @@ -32,8 +32,8 @@ class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard { } } -describe('FeatureAuthorizationGuard', () => { - let guard: FeatureAuthorizationGuard; +describe('SingleFeatureAuthorizationGuard', () => { + let guard: SingleFeatureAuthorizationGuard; let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; @@ -56,7 +56,7 @@ describe('FeatureAuthorizationGuard', () => { authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true) }); - guard = new FeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid); + guard = new SingleFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid); } beforeEach(() => { diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts new file mode 100644 index 0000000000..cb71d2f418 --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts @@ -0,0 +1,27 @@ +import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { FeatureID } from '../feature-id'; +import { Observable } from 'rxjs'; +import { map} from 'rxjs/operators'; +import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; + +/** + * Abstract Guard for preventing unauthorized activating and loading of routes when a user + * doesn't have authorized rights on a specific feature and/or object. + * Override the desired getters in the parent class for checking specific authorization on a feature and/or object. + */ +export abstract class SingleFeatureAuthorizationGuard extends SomeFeatureAuthorizationGuard { + /** + * The features to check authorization for + */ + getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return this.getFeatureID(route, state).pipe( + map((featureID) => [featureID]), + ); + } + + /** + * The type of feature to check authorization for + * Override this method to define a feature + */ + abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable; +} diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts index bb678ebf33..cc6f50c161 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { FeatureAuthorizationGuard } from './feature-authorization.guard'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; import { FeatureID } from '../feature-id'; import { AuthorizationDataService } from '../authorization-data.service'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; @@ -13,7 +13,7 @@ import { AuthService } from '../../../auth/auth.service'; @Injectable({ providedIn: 'root' }) -export class SiteAdministratorGuard extends FeatureAuthorizationGuard { +export class SiteAdministratorGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts index 709d9ff266..bdbb8250e2 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts @@ -1,4 +1,4 @@ -import { FeatureAuthorizationGuard } from './feature-authorization.guard'; +import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; import { Injectable } from '@angular/core'; import { AuthorizationDataService } from '../authorization-data.service'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; @@ -13,7 +13,7 @@ import { AuthService } from '../../../auth/auth.service'; @Injectable({ providedIn: 'root' }) -export class SiteRegisterGuard extends FeatureAuthorizationGuard { +export class SiteRegisterGuard extends SingleFeatureAuthorizationGuard { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { super(authorizationService, router, authService); } diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts new file mode 100644 index 0000000000..90153d2d14 --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts @@ -0,0 +1,110 @@ +import { AuthorizationDataService } from '../authorization-data.service'; +import { FeatureID } from '../feature-id'; +import { Observable, of as observableOf } from 'rxjs'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { AuthService } from '../../../auth/auth.service'; +import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; + +/** + * Test implementation of abstract class SomeFeatureAuthorizationGuard + * Provide the return values of the overwritten getters as constructor arguments + */ +class SomeFeatureAuthorizationGuardImpl extends SomeFeatureAuthorizationGuard { + constructor(protected authorizationService: AuthorizationDataService, + protected router: Router, + protected authService: AuthService, + protected featureIds: FeatureID[], + protected objectUrl: string, + protected ePersonUuid: string) { + super(authorizationService, router, authService); + } + + getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.featureIds); + } + + getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.objectUrl); + } + + getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + return observableOf(this.ePersonUuid); + } +} + +describe('SomeFeatureAuthorizationGuard', () => { + let guard: SomeFeatureAuthorizationGuard; + let authorizationService: AuthorizationDataService; + let router: Router; + let authService: AuthService; + + let featureIds: FeatureID[]; + let authorizedFeatureIds: FeatureID[]; + let objectUrl: string; + let ePersonUuid: string; + + function init() { + featureIds = [FeatureID.LoginOnBehalfOf, FeatureID.CanDelete]; + authorizedFeatureIds = []; + objectUrl = 'fake-object-url'; + ePersonUuid = 'fake-eperson-uuid'; + + authorizationService = Object.assign({ + isAuthorized(featureId?: FeatureID): Observable { + return observableOf(authorizedFeatureIds.indexOf(featureId) > -1); + } + }); + router = jasmine.createSpyObj('router', { + parseUrl: {} + }); + authService = jasmine.createSpyObj('authService', { + isAuthenticated: observableOf(true) + }); + guard = new SomeFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureIds, objectUrl, ePersonUuid); + } + + beforeEach(() => { + init(); + }); + + describe('canActivate', () => { + describe('when the user isn\'t authorized', () => { + beforeEach(() => { + authorizedFeatureIds = []; + }); + + it('should not return true', (done) => { + guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + expect(result).not.toEqual(true); + done(); + }); + }); + }); + + describe('when the user is authorized for at least one of the guard\'s features', () => { + beforeEach(() => { + authorizedFeatureIds = [featureIds[0]]; + }); + + it('should return true', (done) => { + guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + expect(result).toEqual(true); + done(); + }); + }); + }); + + describe('when the user is authorized for all of the guard\'s features', () => { + beforeEach(() => { + authorizedFeatureIds = featureIds; + }); + + it('should return true', (done) => { + guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + expect(result).toEqual(true); + done(); + }); + }); + }); + }); +}); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts similarity index 64% rename from src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.ts rename to src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts index 86b75b637e..3a6cf745c9 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/feature-authorization.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts @@ -2,16 +2,16 @@ import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTr import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; -import { returnForbiddenUrlTreeOrLoginOnFalse } from '../../../shared/operators'; +import { returnForbiddenUrlTreeOrLoginOnAllFalse } from '../../../shared/operators'; import { switchMap } from 'rxjs/operators'; import { AuthService } from '../../../auth/auth.service'; /** * Abstract Guard for preventing unauthorized activating and loading of routes when a user - * doesn't have authorized rights on a specific feature and/or object. - * Override the desired getters in the parent class for checking specific authorization on a feature and/or object. + * doesn't have authorized rights on any of the specified features and/or object. + * Override the desired getters in the parent class for checking specific authorization on a list of features and/or object. */ -export abstract class FeatureAuthorizationGuard implements CanActivate { +export abstract class SomeFeatureAuthorizationGuard implements CanActivate { constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { @@ -22,17 +22,19 @@ export abstract class FeatureAuthorizationGuard implements CanActivate { * Redirect the user to the unauthorized page when he/she's not authorized for the given feature */ canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - 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)), - returnForbiddenUrlTreeOrLoginOnFalse(this.router, this.authService, state.url) + return observableCombineLatest(this.getFeatureIDs(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe( + switchMap(([featureIDs, objectUrl, ePersonUuid]) => + observableCombineLatest(...featureIDs.map((featureID) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid))) + ), + returnForbiddenUrlTreeOrLoginOnAllFalse(this.router, this.authService, state.url) ); } /** - * The type of feature to check authorization for - * Override this method to define a feature + * The features to check authorization for + * Override this method to define a list of features */ - abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable; + abstract getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable; /** * The URL of the object to check if the user has authorized rights for diff --git a/src/app/core/data/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index dbdd794665..6d070fcd4c 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -13,4 +13,11 @@ export enum FeatureID { IsCollectionAdmin = 'isCollectionAdmin', IsCommunityAdmin = 'isCommunityAdmin', CanDownload = 'canDownload', + CanManageVersions = 'canManageVersions', + CanManageBitstreamBundles = 'canManageBitstreamBundles', + CanManageRelationships = 'canManageRelationships', + CanManageMappings = 'canManageMappings', + CanManagePolicies = 'canManagePolicies', + CanMakePrivate = 'canMakePrivate', + CanMove = 'canMove', } diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index ee6520d848..20aaf23ba8 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -207,10 +207,23 @@ export const redirectOn4xx = (router: Router, authService: AuthService) => */ export const returnForbiddenUrlTreeOrLoginOnFalse = (router: Router, authService: AuthService, redirectUrl: string) => (source: Observable): Observable => + source.pipe( + map((authorized) => [authorized]), + returnForbiddenUrlTreeOrLoginOnAllFalse(router, authService, redirectUrl), + ); + +/** + * Operator that returns a UrlTree to a forbidden page or the login page when the booleans received are all false + * @param router The router used to navigate to a forbidden page + * @param authService The AuthService used to determine whether or not the user is logged in + * @param redirectUrl The URL to redirect back to after logging in + */ +export const returnForbiddenUrlTreeOrLoginOnAllFalse = (router: Router, authService: AuthService, redirectUrl: string) => + (source: Observable): Observable => observableCombineLatest(source, authService.isAuthenticated()).pipe( - map(([authorized, authenticated]: [boolean, boolean]) => { - if (authorized) { - return authorized; + map(([authorizedList, authenticated]: [boolean[], boolean]) => { + if (authorizedList.some((b: boolean) => b === true)) { + return true; } else { if (authenticated) { return router.parseUrl(getForbiddenRoute()); diff --git a/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts b/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts index 278a392e18..8d621ad4be 100644 --- a/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts +++ b/src/app/shared/mocks/dspace-rest/endpoint-mocking-rest.service.ts @@ -97,8 +97,14 @@ export class EndpointMockingRestService extends DspaceRestService { * the mock response if there is one, undefined otherwise */ private getMockData(urlStr: string): any { - const url = new URL(urlStr); - const key = url.pathname.slice(environment.rest.nameSpace.length); + let key; + if (this.mockResponseMap.has(urlStr)) { + key = urlStr; + } else { + // didn't find an exact match for the url, try to match only the endpoint without namespace and parameters + const url = new URL(urlStr); + key = url.pathname.slice(environment.rest.nameSpace.length); + } if (this.mockResponseMap.has(key)) { // parse and stringify to clone the object to ensure that any changes made // to it afterwards don't affect future calls diff --git a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-bitstreams-response.json b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-bitstreams-response.json new file mode 100644 index 0000000000..1642691672 --- /dev/null +++ b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-bitstreams-response.json @@ -0,0 +1,51 @@ +{ + "_embedded": { + "authorizations": [ + { + "id": "cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963", + "type": "authorization", + "_links": { + "eperson": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963/eperson" + }, + "feature": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963/feature" + }, + "object": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963/object" + }, + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageBitstreams_core.item_96715576-3748-4761-ad45-001646632963" + } + }, + "_embedded": { + "feature": { + "id": "canManageBitstreams", + "description": "It can be used to verify if the bitstreams of the specified objects can be managed", + "type": "feature", + "resourcetypes": [ + "core.item", + "core.bundle" + ], + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/features/canManageBitstreams" + } + } + } + } + } + ] + }, + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canManageBitstreams" + } + }, + "page": { + "size": 20, + "totalElements": 1, + "totalPages": 1, + "number": 0 + } +} diff --git a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-mappings-response.json b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-mappings-response.json new file mode 100644 index 0000000000..c186ef8cc4 --- /dev/null +++ b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-mappings-response.json @@ -0,0 +1,50 @@ +{ + "_embedded": { + "authorizations": [ + { + "id": "cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067", + "type": "authorization", + "_links": { + "eperson": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/eperson" + }, + "feature": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/feature" + }, + "object": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/object" + }, + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageMappings_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067" + } + }, + "_embedded": { + "feature": { + "id": "canManageMappings", + "description": "It can be used to verify if the mappings of the specified objects can be managed", + "type": "feature", + "resourcetypes": [ + "core.item" + ], + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/features/canManageMappings" + } + } + } + } + } + ] + }, + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/e98b0f27-5c19-49a0-960d-eb6ad5287067&feature=canManageMappings" + } + }, + "page": { + "size": 20, + "totalElements": 1, + "totalPages": 1, + "number": 0 + } +} diff --git a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-relationships-response.json b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-relationships-response.json new file mode 100644 index 0000000000..b6de452dd2 --- /dev/null +++ b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-relationships-response.json @@ -0,0 +1,50 @@ +{ + "_embedded": { + "authorizations": [ + { + "id": "cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e", + "type": "authorization", + "_links": { + "eperson": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e/eperson" + }, + "feature": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e/feature" + }, + "object": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e/object" + }, + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageRelationships_core.item_047556d1-3d01-4c53-bc68-0cee7ad7ed4e" + } + }, + "_embedded": { + "feature": { + "id": "canManageRelationships", + "description": "It can be used to verify if the relationships of the specified objects can be managed", + "type": "feature", + "resourcetypes": [ + "core.item" + ], + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/features/canManageRelationships" + } + } + } + } + } + ] + }, + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/047556d1-3d01-4c53-bc68-0cee7ad7ed4e&feature=canManageRelationships" + } + }, + "page": { + "size": 20, + "totalElements": 1, + "totalPages": 1, + "number": 0 + } +} diff --git a/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-versions-response.json b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-versions-response.json new file mode 100644 index 0000000000..55eb69a7bf --- /dev/null +++ b/src/app/shared/mocks/dspace-rest/mocks/mock-feature-item-can-manage-versions-response.json @@ -0,0 +1,50 @@ +{ + "_embedded": { + "authorizations": [ + { + "id": "cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067", + "type": "authorization", + "_links": { + "eperson": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/eperson" + }, + "feature": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/feature" + }, + "object": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067/object" + }, + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/cd824a61-95be-4e16-bccd-51fea26707d0_canManageVersions_core.item_e98b0f27-5c19-49a0-960d-eb6ad5287067" + } + }, + "_embedded": { + "feature": { + "id": "canManageVersions", + "description": "It can be used to verify if the versions of the specified objects can be managed", + "type": "feature", + "resourcetypes": [ + "core.item" + ], + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/features/canManageVersions" + } + } + } + } + } + ] + }, + "_links": { + "self": { + "href": "https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/e98b0f27-5c19-49a0-960d-eb6ad5287067&feature=canManageVersions" + } + }, + "page": { + "size": 20, + "totalElements": 1, + "totalPages": 1, + "number": 0 + } +} diff --git a/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts b/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts index 92f04a7bc5..a746416e77 100644 --- a/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts +++ b/src/app/shared/mocks/dspace-rest/mocks/response-map.mock.ts @@ -2,6 +2,10 @@ import { InjectionToken } from '@angular/core'; // import mockSubmissionResponse from './mock-submission-response.json'; // import mockPublicationResponse from './mock-publication-response.json'; // import mockUntypedItemResponse from './mock-untyped-item-response.json'; +import mockFeatureItemCanManageBitstreamsResponse from './mock-feature-item-can-manage-bitstreams-response.json'; +import mockFeatureItemCanManageRelationshipsResponse from './mock-feature-item-can-manage-relationships-response.json'; +import mockFeatureItemCanManageVersionsResponse from './mock-feature-item-can-manage-versions-response.json'; +import mockFeatureItemCanManageMappingsResponse from './mock-feature-item-can-manage-mappings-response.json'; export class ResponseMapMock extends Map {} @@ -16,4 +20,8 @@ export const mockResponseMap: ResponseMapMock = new Map([ // [ '/config/submissionforms/traditionalpageone', mockSubmissionResponse ] // [ '/api/pid/find', mockPublicationResponse ], // [ '/api/pid/find', mockUntypedItemResponse ], + [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/96715576-3748-4761-ad45-001646632963&feature=canManageBitstreams&embed=feature', mockFeatureItemCanManageBitstreamsResponse ], + [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/047556d1-3d01-4c53-bc68-0cee7ad7ed4e&feature=canManageRelationships&embed=feature', mockFeatureItemCanManageRelationshipsResponse ], + [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/e98b0f27-5c19-49a0-960d-eb6ad5287067&feature=canManageVersions&embed=feature', mockFeatureItemCanManageVersionsResponse ], + [ 'https://api7.dspace.org/server/api/authz/authorizations/search/object?uri=https://api7.dspace.org/server/api/core/items/e98b0f27-5c19-49a0-960d-eb6ad5287067&feature=canManageMappings&embed=feature', mockFeatureItemCanManageMappingsResponse ], ]); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index e514e71716..bdec3bfb23 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1520,7 +1520,7 @@ "item.edit.breadcrumbs": "Edit Item", - "item.edit.tabs.disabled.tooltip": "You don't have permission to access this tab", + "item.edit.tabs.disabled.tooltip": "You're not authorized to access this tab", "item.edit.tabs.mapper.head": "Collection Mapper", @@ -1765,6 +1765,8 @@ "item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository", + "item.edit.tabs.status.buttons.unauthorized": "You're not authorized to perform this action", + "item.edit.tabs.status.buttons.withdraw.button": "Withdraw...", "item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository",