mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge remote-tracking branch 'remotes/origin/main' into authorities_and_controlled_vocabularies
# Conflicts: # src/app/core/core.module.ts
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
<button class="btn btn-light" [disabled]="!(canDelete$ | async)">
|
<button class="btn btn-light" [disabled]="!(canDelete$ | async)">
|
||||||
<i class="fa fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
|
<i class="fa fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="!isImpersonated" class="btn btn-light" [disabled]="!(canImpersonate$ | async)" (click)="impersonate()">
|
<button *ngIf="!isImpersonated" class="btn btn-light" [ngClass]="{'d-none' : !(canImpersonate$ | async)}" (click)="impersonate()">
|
||||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
||||||
</button>
|
</button>
|
||||||
<button *ngIf="isImpersonated" class="btn btn-light" (click)="stopImpersonating()">
|
<button *ngIf="isImpersonated" class="btn btn-light" (click)="stopImpersonating()">
|
||||||
|
@@ -33,6 +33,9 @@ import { NotificationsServiceStub } from '../../../../shared/testing/notificatio
|
|||||||
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
||||||
import { AuthService } from '../../../../core/auth/auth.service';
|
import { AuthService } from '../../../../core/auth/auth.service';
|
||||||
import { AuthServiceStub } from '../../../../shared/testing/auth-service.stub';
|
import { AuthServiceStub } from '../../../../shared/testing/auth-service.stub';
|
||||||
|
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||||
|
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||||
|
|
||||||
describe('EPersonFormComponent', () => {
|
describe('EPersonFormComponent', () => {
|
||||||
let component: EPersonFormComponent;
|
let component: EPersonFormComponent;
|
||||||
@@ -43,6 +46,8 @@ describe('EPersonFormComponent', () => {
|
|||||||
let mockEPeople;
|
let mockEPeople;
|
||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
let authService: AuthServiceStub;
|
let authService: AuthServiceStub;
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let groupsDataService: GroupDataService;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
mockEPeople = [EPersonMock, EPersonMock2];
|
mockEPeople = [EPersonMock, EPersonMock2];
|
||||||
@@ -108,6 +113,13 @@ describe('EPersonFormComponent', () => {
|
|||||||
builderService = getMockFormBuilderService();
|
builderService = getMockFormBuilderService();
|
||||||
translateService = getMockTranslateService();
|
translateService = getMockTranslateService();
|
||||||
authService = new AuthServiceStub();
|
authService = new AuthServiceStub();
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
|
groupsDataService = jasmine.createSpyObj('groupsDataService', {
|
||||||
|
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
|
getGroupRegistryRouterLink: ''
|
||||||
|
});
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
@@ -130,6 +142,8 @@ describe('EPersonFormComponent', () => {
|
|||||||
{ provide: RemoteDataBuildService, useValue: {} },
|
{ provide: RemoteDataBuildService, useValue: {} },
|
||||||
{ provide: HALEndpointService, useValue: {} },
|
{ provide: HALEndpointService, useValue: {} },
|
||||||
{ provide: AuthService, useValue: authService },
|
{ provide: AuthService, useValue: authService },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: GroupDataService, useValue: groupsDataService },
|
||||||
EPeopleRegistryComponent
|
EPeopleRegistryComponent
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
@@ -9,7 +9,7 @@ import {
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Subscription, combineLatest, of } from 'rxjs';
|
import { Subscription, combineLatest, of } from 'rxjs';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { take } from 'rxjs/operators';
|
import { switchMap, take } from 'rxjs/operators';
|
||||||
import { RestResponse } from '../../../../core/cache/response.models';
|
import { RestResponse } from '../../../../core/cache/response.models';
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
@@ -23,6 +23,8 @@ import { FormBuilderService } from '../../../../shared/form/builder/form-builder
|
|||||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||||
import { AuthService } from '../../../../core/auth/auth.service';
|
import { AuthService } from '../../../../core/auth/auth.service';
|
||||||
|
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-eperson-form',
|
selector: 'ds-eperson-form',
|
||||||
@@ -120,9 +122,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable whether or not the admin is allowed to impersonate the EPerson
|
* Observable whether or not the admin is allowed to impersonate the EPerson
|
||||||
* TODO: Initialize the observable once the REST API supports this (currently hardcoded to return true)
|
|
||||||
*/
|
*/
|
||||||
canImpersonate$: Observable<boolean> = of(true);
|
canImpersonate$: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of subscriptions
|
* List of subscriptions
|
||||||
@@ -158,7 +159,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
private formBuilderService: FormBuilderService,
|
private formBuilderService: FormBuilderService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private authService: AuthService) {
|
private authService: AuthService,
|
||||||
|
private authorizationService: AuthorizationDataService) {
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||||
this.epersonInitial = eperson;
|
this.epersonInitial = eperson;
|
||||||
if (hasValue(eperson)) {
|
if (hasValue(eperson)) {
|
||||||
@@ -242,6 +244,9 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
requireCertificate: eperson != null ? eperson.requireCertificate : false
|
requireCertificate: eperson != null ? eperson.requireCertificate : false
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
this.canImpersonate$ = this.epersonService.getActiveEPerson().pipe(
|
||||||
|
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, hasValue(eperson) ? eperson.self : undefined))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,19 +9,23 @@ import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service
|
|||||||
import { CSSVariableServiceStub } from '../../shared/testing/css-variable-service.stub';
|
import { CSSVariableServiceStub } from '../../shared/testing/css-variable-service.stub';
|
||||||
import { AuthServiceStub } from '../../shared/testing/auth-service.stub';
|
import { AuthServiceStub } from '../../shared/testing/auth-service.stub';
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
|
||||||
describe('AdminSidebarComponent', () => {
|
describe('AdminSidebarComponent', () => {
|
||||||
let comp: AdminSidebarComponent;
|
let comp: AdminSidebarComponent;
|
||||||
let fixture: ComponentFixture<AdminSidebarComponent>;
|
let fixture: ComponentFixture<AdminSidebarComponent>;
|
||||||
const menuService = new MenuServiceStub();
|
const menuService = new MenuServiceStub();
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule],
|
imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule],
|
||||||
declarations: [AdminSidebarComponent],
|
declarations: [AdminSidebarComponent],
|
||||||
@@ -31,6 +35,7 @@ describe('AdminSidebarComponent', () => {
|
|||||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||||
{ provide: AuthService, useClass: AuthServiceStub },
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
{ provide: ActivatedRoute, useValue: {} },
|
{ provide: ActivatedRoute, useValue: {} },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
{
|
{
|
||||||
provide: NgbModal, useValue: {
|
provide: NgbModal, useValue: {
|
||||||
open: () => {/*comment*/}
|
open: () => {/*comment*/}
|
||||||
|
@@ -18,6 +18,8 @@ import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model
|
|||||||
import { MenuComponent } from '../../shared/menu/menu.component';
|
import { MenuComponent } from '../../shared/menu/menu.component';
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||||
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component representing the admin sidebar
|
* Component representing the admin sidebar
|
||||||
@@ -61,7 +63,8 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
protected injector: Injector,
|
protected injector: Injector,
|
||||||
private variableService: CSSVariableService,
|
private variableService: CSSVariableService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private modalService: NgbModal
|
private modalService: NgbModal,
|
||||||
|
private authorizationService: AuthorizationDataService
|
||||||
) {
|
) {
|
||||||
super(menuService, injector);
|
super(menuService, injector);
|
||||||
}
|
}
|
||||||
@@ -71,6 +74,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.createMenu();
|
this.createMenu();
|
||||||
|
this.createSiteAdministratorMenuSections();
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
|
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
|
||||||
this.authService.isAuthenticated()
|
this.authService.isAuthenticated()
|
||||||
@@ -311,113 +315,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
} as LinkMenuItemModel,
|
} as LinkMenuItemModel,
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Access Control */
|
|
||||||
{
|
|
||||||
id: 'access_control',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.access_control'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
icon: 'key',
|
|
||||||
index: 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'access_control_people',
|
|
||||||
parentID: 'access_control',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.access_control_people',
|
|
||||||
link: '/admin/access-control/epeople'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'access_control_groups',
|
|
||||||
parentID: 'access_control',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.access_control_groups',
|
|
||||||
link: '/admin/access-control/groups'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'access_control_authorizations',
|
|
||||||
parentID: 'access_control',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.access_control_authorizations',
|
|
||||||
link: ''
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
/* Admin Search */
|
|
||||||
{
|
|
||||||
id: 'admin_search',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.admin_search',
|
|
||||||
link: '/admin/search'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'search',
|
|
||||||
index: 5
|
|
||||||
},
|
|
||||||
/* Registries */
|
|
||||||
{
|
|
||||||
id: 'registries',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.registries'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
icon: 'list',
|
|
||||||
index: 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'registries_metadata',
|
|
||||||
parentID: 'registries',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.registries_metadata',
|
|
||||||
link: 'admin/registries/metadata'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'registries_format',
|
|
||||||
parentID: 'registries',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.registries_format',
|
|
||||||
link: 'admin/registries/bitstream-formats'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Curation tasks */
|
|
||||||
{
|
|
||||||
id: 'curation_tasks',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.curation_task',
|
|
||||||
link: ''
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'filter',
|
|
||||||
index: 7
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Statistics */
|
/* Statistics */
|
||||||
{
|
{
|
||||||
id: 'statistics_task',
|
id: 'statistics_task',
|
||||||
@@ -445,6 +342,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
icon: 'cogs',
|
icon: 'cogs',
|
||||||
index: 9
|
index: 9
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Processes */
|
/* Processes */
|
||||||
{
|
{
|
||||||
id: 'processes',
|
id: 'processes',
|
||||||
@@ -458,24 +356,144 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
icon: 'terminal',
|
icon: 'terminal',
|
||||||
index: 10
|
index: 10
|
||||||
},
|
},
|
||||||
/* Workflow */
|
|
||||||
{
|
|
||||||
id: 'workflow',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.workflow',
|
|
||||||
link: '/admin/workflow'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'user-check',
|
|
||||||
index: 10
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||||
shouldPersistOnRouteChange: true
|
shouldPersistOnRouteChange: true
|
||||||
})));
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create menu sections dependent on whether or not the current user is a site administrator
|
||||||
|
*/
|
||||||
|
createSiteAdministratorMenuSections() {
|
||||||
|
this.authorizationService.isAuthorized(FeatureID.AdministratorOf).subscribe((authorized) => {
|
||||||
|
const menuList = [
|
||||||
|
/* Access Control */
|
||||||
|
{
|
||||||
|
id: 'access_control',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.access_control'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'key',
|
||||||
|
index: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'access_control_people',
|
||||||
|
parentID: 'access_control',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.access_control_people',
|
||||||
|
link: '/admin/access-control/epeople'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'access_control_groups',
|
||||||
|
parentID: 'access_control',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.access_control_groups',
|
||||||
|
link: '/admin/access-control/groups'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'access_control_authorizations',
|
||||||
|
parentID: 'access_control',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.access_control_authorizations',
|
||||||
|
link: ''
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
/* Admin Search */
|
||||||
|
{
|
||||||
|
id: 'admin_search',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.admin_search',
|
||||||
|
link: '/admin/search'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
icon: 'search',
|
||||||
|
index: 5
|
||||||
|
},
|
||||||
|
/* Registries */
|
||||||
|
{
|
||||||
|
id: 'registries',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.TEXT,
|
||||||
|
text: 'menu.section.registries'
|
||||||
|
} as TextMenuItemModel,
|
||||||
|
icon: 'list',
|
||||||
|
index: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'registries_metadata',
|
||||||
|
parentID: 'registries',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.registries_metadata',
|
||||||
|
link: 'admin/registries/metadata'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'registries_format',
|
||||||
|
parentID: 'registries',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.registries_format',
|
||||||
|
link: 'admin/registries/bitstream-formats'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Curation tasks */
|
||||||
|
{
|
||||||
|
id: 'curation_tasks',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.curation_task',
|
||||||
|
link: ''
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
icon: 'filter',
|
||||||
|
index: 7
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Workflow */
|
||||||
|
{
|
||||||
|
id: 'workflow',
|
||||||
|
active: false,
|
||||||
|
visible: authorized,
|
||||||
|
model: {
|
||||||
|
type: MenuItemType.LINK,
|
||||||
|
text: 'menu.section.workflow',
|
||||||
|
link: '/admin/workflow'
|
||||||
|
} as LinkMenuItemModel,
|
||||||
|
icon: 'user-check',
|
||||||
|
index: 11
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
||||||
|
shouldPersistOnRouteChange: true
|
||||||
|
})));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -10,6 +10,8 @@ import { Collection } from './core/shared/collection.model';
|
|||||||
import { Item } from './core/shared/item.model';
|
import { Item } from './core/shared/item.model';
|
||||||
import { getItemPageRoute } from './+item-page/item-page-routing.module';
|
import { getItemPageRoute } from './+item-page/item-page-routing.module';
|
||||||
import { getCollectionPageRoute } from './+collection-page/collection-page-routing.module';
|
import { getCollectionPageRoute } from './+collection-page/collection-page-routing.module';
|
||||||
|
import { SiteAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||||
|
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
|
||||||
|
|
||||||
const ITEM_MODULE_PATH = 'items';
|
const ITEM_MODULE_PATH = 'items';
|
||||||
|
|
||||||
@@ -76,6 +78,12 @@ export function getDSOPath(dso: DSpaceObject): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UNAUTHORIZED_PATH = 'unauthorized';
|
||||||
|
|
||||||
|
export function getUnauthorizedPath() {
|
||||||
|
return `/${UNAUTHORIZED_PATH}`;
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot([
|
RouterModule.forRoot([
|
||||||
@@ -98,7 +106,7 @@ export function getDSOPath(dso: DSpaceObject): string {
|
|||||||
},
|
},
|
||||||
{ path: 'search', loadChildren: './+search-page/search-page-routing.module#SearchPageRoutingModule' },
|
{ path: 'search', loadChildren: './+search-page/search-page-routing.module#SearchPageRoutingModule' },
|
||||||
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'},
|
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'},
|
||||||
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] },
|
{ path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [SiteAdministratorGuard] },
|
||||||
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
|
||||||
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
|
||||||
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
|
||||||
@@ -115,6 +123,7 @@ export function getDSOPath(dso: DSpaceObject): string {
|
|||||||
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
|
loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard]
|
||||||
},
|
},
|
||||||
{ path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard] },
|
{ path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard] },
|
||||||
|
{ path: UNAUTHORIZED_PATH, component: UnauthorizedComponent },
|
||||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
@@ -40,6 +40,7 @@ import { SharedModule } from './shared/shared.module';
|
|||||||
import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
|
import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
|
||||||
|
|
||||||
export function getBase() {
|
export function getBase() {
|
||||||
return environment.ui.nameSpace;
|
return environment.ui.nameSpace;
|
||||||
@@ -123,6 +124,7 @@ const EXPORTS = [
|
|||||||
declarations: [
|
declarations: [
|
||||||
...DECLARATIONS,
|
...DECLARATIONS,
|
||||||
BreadcrumbsComponent,
|
BreadcrumbsComponent,
|
||||||
|
UnauthorizedComponent,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
...EXPORTS
|
...EXPORTS
|
||||||
|
@@ -145,6 +145,11 @@ import { WorkflowAction } from './tasks/models/workflow-action-object.model';
|
|||||||
import { LocaleInterceptor } from './locale/locale.interceptor';
|
import { LocaleInterceptor } from './locale/locale.interceptor';
|
||||||
import { ItemTemplateDataService } from './data/item-template-data.service';
|
import { ItemTemplateDataService } from './data/item-template-data.service';
|
||||||
import { TemplateItem } from './shared/template-item.model';
|
import { TemplateItem } from './shared/template-item.model';
|
||||||
|
import { Feature } from './shared/feature.model';
|
||||||
|
import { Authorization } from './shared/authorization.model';
|
||||||
|
import { FeatureDataService } from './data/feature-authorization/feature-data.service';
|
||||||
|
import { AuthorizationDataService } from './data/feature-authorization/authorization-data.service';
|
||||||
|
import { SiteAdministratorGuard } from './data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||||
import { Registration } from './shared/registration.model';
|
import { Registration } from './shared/registration.model';
|
||||||
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
import { MetadataSchemaDataService } from './data/metadata-schema-data.service';
|
||||||
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
import { MetadataFieldDataService } from './data/metadata-field-data.service';
|
||||||
@@ -277,6 +282,9 @@ const PROVIDERS = [
|
|||||||
ProcessDataService,
|
ProcessDataService,
|
||||||
ScriptDataService,
|
ScriptDataService,
|
||||||
ProcessFilesResponseParsingService,
|
ProcessFilesResponseParsingService,
|
||||||
|
FeatureDataService,
|
||||||
|
AuthorizationDataService,
|
||||||
|
SiteAdministratorGuard,
|
||||||
MetadataSchemaDataService,
|
MetadataSchemaDataService,
|
||||||
MetadataFieldDataService,
|
MetadataFieldDataService,
|
||||||
TokenResponseParsingService,
|
TokenResponseParsingService,
|
||||||
@@ -344,6 +352,8 @@ export const models =
|
|||||||
VersionHistory,
|
VersionHistory,
|
||||||
WorkflowAction,
|
WorkflowAction,
|
||||||
TemplateItem,
|
TemplateItem,
|
||||||
|
Feature,
|
||||||
|
Authorization,
|
||||||
Registration,
|
Registration,
|
||||||
Vocabulary,
|
Vocabulary,
|
||||||
VocabularyEntry,
|
VocabularyEntry,
|
||||||
|
@@ -0,0 +1,198 @@
|
|||||||
|
import { AuthorizationDataService } from './authorization-data.service';
|
||||||
|
import { SiteDataService } from '../site-data.service';
|
||||||
|
import { AuthService } from '../../auth/auth.service';
|
||||||
|
import { Site } from '../../shared/site.model';
|
||||||
|
import { EPerson } from '../../eperson/models/eperson.model';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { FindListOptions } from '../request.models';
|
||||||
|
import { FeatureID } from './feature-id';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { RequestParam } from '../../cache/models/request-param.model';
|
||||||
|
import { Authorization } from '../../shared/authorization.model';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
|
import { Feature } from '../../shared/feature.model';
|
||||||
|
|
||||||
|
describe('AuthorizationDataService', () => {
|
||||||
|
let service: AuthorizationDataService;
|
||||||
|
let siteService: SiteDataService;
|
||||||
|
let authService: AuthService;
|
||||||
|
|
||||||
|
let site: Site;
|
||||||
|
let ePerson: EPerson;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
site = Object.assign(new Site(), {
|
||||||
|
id: 'test-site',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'test-site-href' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ePerson = Object.assign(new EPerson(), {
|
||||||
|
id: 'test-eperson',
|
||||||
|
uuid: 'test-eperson'
|
||||||
|
});
|
||||||
|
siteService = jasmine.createSpyObj('siteService', {
|
||||||
|
find: observableOf(site)
|
||||||
|
});
|
||||||
|
authService = {
|
||||||
|
isAuthenticated: () => observableOf(true),
|
||||||
|
getAuthenticatedUserFromStore: () => observableOf(ePerson)
|
||||||
|
} as AuthService;
|
||||||
|
service = new AuthorizationDataService(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, authService, siteService);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
|
spyOn(service, 'searchBy').and.returnValue(observableOf(undefined));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('searchByObject', () => {
|
||||||
|
const objectUrl = 'fake-object-url';
|
||||||
|
const ePersonUuid = 'fake-eperson-uuid';
|
||||||
|
|
||||||
|
function createExpected(providedObjectUrl: string, providedEPersonUuid?: string, providedFeatureId?: FeatureID): FindListOptions {
|
||||||
|
const searchParams = [new RequestParam('uri', providedObjectUrl)];
|
||||||
|
if (hasValue(providedFeatureId)) {
|
||||||
|
searchParams.push(new RequestParam('feature', providedFeatureId));
|
||||||
|
}
|
||||||
|
if (hasValue(providedEPersonUuid)) {
|
||||||
|
searchParams.push(new RequestParam('eperson', providedEPersonUuid));
|
||||||
|
}
|
||||||
|
return Object.assign(new FindListOptions(), { searchParams });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('when no arguments are provided and a user is authenticated', () => {
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when no arguments except for a feature are provided and a user is authenticated', () => {
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when a feature and object url are provided, but no user uuid and a user is authenticated', () => {
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when all arguments are provided', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.searchByObject(FeatureID.LoginOnBehalfOf, objectUrl, ePersonUuid).subscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call searchBy with the object\'s url, user\'s uuid and the feature', () => {
|
||||||
|
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', () => {
|
||||||
|
const featureID = FeatureID.AdministratorOf;
|
||||||
|
const validPayload = [
|
||||||
|
Object.assign(new Authorization(), {
|
||||||
|
feature: createSuccessfulRemoteDataObject$(Object.assign(new Feature(), {
|
||||||
|
id: 'invalid-feature'
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
Object.assign(new Authorization(), {
|
||||||
|
feature: createSuccessfulRemoteDataObject$(Object.assign(new Feature(), {
|
||||||
|
id: featureID
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
];
|
||||||
|
const invalidPayload = [
|
||||||
|
Object.assign(new Authorization(), {
|
||||||
|
feature: createSuccessfulRemoteDataObject$(Object.assign(new Feature(), {
|
||||||
|
id: 'invalid-feature'
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
Object.assign(new Authorization(), {
|
||||||
|
feature: createSuccessfulRemoteDataObject$(Object.assign(new Feature(), {
|
||||||
|
id: 'another-invalid-feature'
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
];
|
||||||
|
const emptyPayload = [];
|
||||||
|
|
||||||
|
describe('when searchByObject returns a 401', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'searchByObject').and.returnValue(observableOf(new RemoteData(false, false, true, undefined, undefined, 401)));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false', (done) => {
|
||||||
|
service.isAuthorized(featureID).subscribe((result) => {
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when searchByObject returns an empty list', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'searchByObject').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(emptyPayload)));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false', (done) => {
|
||||||
|
service.isAuthorized(featureID).subscribe((result) => {
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when searchByObject returns an invalid list', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'searchByObject').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(invalidPayload)));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true', (done) => {
|
||||||
|
service.isAuthorized(featureID).subscribe((result) => {
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when searchByObject returns a valid list', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(service, 'searchByObject').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(validPayload)));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true', (done) => {
|
||||||
|
service.isAuthorized(featureID).subscribe((result) => {
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,150 @@
|
|||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { AUTHORIZATION } from '../../shared/authorization.resource-type';
|
||||||
|
import { dataService } from '../../cache/builders/build-decorators';
|
||||||
|
import { DataService } from '../data.service';
|
||||||
|
import { Authorization } from '../../shared/authorization.model';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../../core.reducers';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { DSOChangeAnalyzer } from '../dso-change-analyzer.service';
|
||||||
|
import { AuthService } from '../../auth/auth.service';
|
||||||
|
import { SiteDataService } from '../site-data.service';
|
||||||
|
import { FindListOptions, FindListRequest } from '../request.models';
|
||||||
|
import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { PaginatedList } from '../paginated-list';
|
||||||
|
import { find, map, switchMap, tap } from 'rxjs/operators';
|
||||||
|
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';
|
||||||
|
import { FeatureID } from './feature-id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service to retrieve {@link Authorization}s from the REST API
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
@dataService(AUTHORIZATION)
|
||||||
|
export class AuthorizationDataService extends DataService<Authorization> {
|
||||||
|
protected linkPath = 'authorizations';
|
||||||
|
protected searchByObjectPath = 'object';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DSOChangeAnalyzer<Authorization>,
|
||||||
|
protected authService: AuthService,
|
||||||
|
protected siteService: SiteDataService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an {@link EPerson} (or anonymous) has access to a specific object within a {@link Feature}
|
||||||
|
* @param objectUrl URL to the object to search {@link Authorization}s for.
|
||||||
|
* If not provided, the repository's {@link Site} will be used.
|
||||||
|
* @param ePersonUuid UUID of the {@link EPerson} to search {@link Authorization}s for.
|
||||||
|
* If not provided, the UUID of the currently authenticated {@link EPerson} will be used.
|
||||||
|
* @param featureId ID of the {@link Feature} to check {@link Authorization} for
|
||||||
|
*/
|
||||||
|
isAuthorized(featureId?: FeatureID, objectUrl?: string, ePersonUuid?: string): Observable<boolean> {
|
||||||
|
return this.searchByObject(featureId, objectUrl, ePersonUuid, {}, followLink('feature')).pipe(
|
||||||
|
map((authorizationRD) => {
|
||||||
|
if (authorizationRD.statusCode !== 401 && hasValue(authorizationRD.payload) && isNotEmpty(authorizationRD.payload.page)) {
|
||||||
|
return authorizationRD.payload.page;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
oneAuthorizationMatchesFeature(featureId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for a list of {@link Authorization}s using the "object" search endpoint and providing optional object url,
|
||||||
|
* {@link EPerson} uuid and/or {@link Feature} id
|
||||||
|
* @param objectUrl URL to the object to search {@link Authorization}s for.
|
||||||
|
* If not provided, the repository's {@link Site} will be used.
|
||||||
|
* @param ePersonUuid UUID of the {@link EPerson} to search {@link Authorization}s for.
|
||||||
|
* If not provided, the UUID of the currently authenticated {@link EPerson} will be used.
|
||||||
|
* @param featureId ID of the {@link Feature} to search {@link Authorization}s for
|
||||||
|
* @param options {@link FindListOptions} to provide pagination and/or additional arguments
|
||||||
|
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a new FindListRequest with given search method
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @param linksToFollow The array of [[FollowLinkConfig]]
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<Authorization>>}
|
||||||
|
* Return an observable that emits response from the server
|
||||||
|
*/
|
||||||
|
searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<Authorization>>): Observable<RemoteData<PaginatedList<Authorization>>> {
|
||||||
|
const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||||
|
|
||||||
|
return hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
tap((href: string) => {
|
||||||
|
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
|
||||||
|
|
||||||
|
this.requestService.configure(request);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
switchMap((href) => this.requestService.getByHref(href)),
|
||||||
|
switchMap((href) =>
|
||||||
|
this.rdbService.buildList<Authorization>(hrefObs, ...linksToFollow) as Observable<RemoteData<PaginatedList<Authorization>>>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create {@link FindListOptions} with {@link RequestParam}s containing a "uri", "feature" and/or "eperson" parameter
|
||||||
|
* @param objectUrl Required parameter value to add to {@link RequestParam} "uri"
|
||||||
|
* @param options Optional initial {@link FindListOptions} to add parameters to
|
||||||
|
* @param ePersonUuid Optional parameter value to add to {@link RequestParam} "eperson"
|
||||||
|
* @param featureId Optional parameter value to add to {@link RequestParam} "feature"
|
||||||
|
*/
|
||||||
|
private createSearchOptions(objectUrl: string, options: FindListOptions = {}, ePersonUuid?: string, featureId?: FeatureID): FindListOptions {
|
||||||
|
let params = [];
|
||||||
|
if (isNotEmpty(options.searchParams)) {
|
||||||
|
params = [...options.searchParams];
|
||||||
|
}
|
||||||
|
params.push(new RequestParam('uri', objectUrl))
|
||||||
|
if (hasValue(featureId)) {
|
||||||
|
params.push(new RequestParam('feature', featureId));
|
||||||
|
}
|
||||||
|
if (hasValue(ePersonUuid)) {
|
||||||
|
params.push(new RequestParam('eperson', ePersonUuid));
|
||||||
|
}
|
||||||
|
return Object.assign(new FindListOptions(), options, {
|
||||||
|
searchParams: [...params]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { FeatureID } from './feature-id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search parameters for retrieving authorizations from the REST API
|
||||||
|
*/
|
||||||
|
export class AuthorizationSearchParams {
|
||||||
|
objectUrl: string;
|
||||||
|
ePersonUuid: string;
|
||||||
|
featureId: FeatureID;
|
||||||
|
|
||||||
|
constructor(objectUrl?: string, ePersonUuid?: string, featureId?: FeatureID) {
|
||||||
|
this.objectUrl = objectUrl;
|
||||||
|
this.ePersonUuid = ePersonUuid;
|
||||||
|
this.featureId = featureId;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,84 @@
|
|||||||
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { AuthorizationSearchParams } from './authorization-search-params';
|
||||||
|
import { SiteDataService } from '../site-data.service';
|
||||||
|
import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
|
import { of as observableOf, combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
|
import { AuthService } from '../../auth/auth.service';
|
||||||
|
import { Authorization } from '../../shared/authorization.model';
|
||||||
|
import { Feature } from '../../shared/feature.model';
|
||||||
|
import { FeatureID } from './feature-id';
|
||||||
|
import { getFirstSucceededRemoteDataPayload } from '../../shared/operators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator accepting {@link AuthorizationSearchParams} and adding the current {@link Site}'s selflink to the parameter's
|
||||||
|
* objectUrl property, if this property is empty
|
||||||
|
* @param siteService The {@link SiteDataService} used for retrieving the repository's {@link Site}
|
||||||
|
*/
|
||||||
|
export const addSiteObjectUrlIfEmpty = (siteService: SiteDataService) =>
|
||||||
|
(source: Observable<AuthorizationSearchParams>): Observable<AuthorizationSearchParams> =>
|
||||||
|
source.pipe(
|
||||||
|
switchMap((params: AuthorizationSearchParams) => {
|
||||||
|
if (hasNoValue(params.objectUrl)) {
|
||||||
|
return siteService.find().pipe(
|
||||||
|
map((site) => Object.assign({}, params, { objectUrl: site.self }))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return observableOf(params);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator accepting {@link AuthorizationSearchParams} and adding the authenticated user's uuid to the parameter's
|
||||||
|
* ePersonUuid property, if this property is empty and an {@link EPerson} is currently authenticated
|
||||||
|
* @param authService The {@link AuthService} used for retrieving the currently authenticated {@link EPerson}
|
||||||
|
*/
|
||||||
|
export const addAuthenticatedUserUuidIfEmpty = (authService: AuthService) =>
|
||||||
|
(source: Observable<AuthorizationSearchParams>): Observable<AuthorizationSearchParams> =>
|
||||||
|
source.pipe(
|
||||||
|
switchMap((params: AuthorizationSearchParams) => {
|
||||||
|
if (hasNoValue(params.ePersonUuid)) {
|
||||||
|
return authService.isAuthenticated().pipe(
|
||||||
|
switchMap((authenticated) => {
|
||||||
|
if (authenticated) {
|
||||||
|
return authService.getAuthenticatedUserFromStore().pipe(
|
||||||
|
map((ePerson) => Object.assign({}, params, { ePersonUuid: ePerson.uuid }))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return observableOf(params)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return observableOf(params);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator checking if at least one of the provided {@link Authorization}s contains a {@link Feature} that matches the
|
||||||
|
* provided {@link FeatureID}
|
||||||
|
* Note: This expects the {@link Authorization}s to contain a resolved link to their {@link Feature}. If they don't,
|
||||||
|
* this observable will always emit false.
|
||||||
|
* @param featureID
|
||||||
|
* @returns true if at least one {@link Feature} matches, false if none do
|
||||||
|
*/
|
||||||
|
export const oneAuthorizationMatchesFeature = (featureID: FeatureID) =>
|
||||||
|
(source: Observable<Authorization[]>): Observable<boolean> =>
|
||||||
|
source.pipe(
|
||||||
|
switchMap((authorizations: Authorization[]) => {
|
||||||
|
if (isNotEmpty(authorizations)) {
|
||||||
|
return observableCombineLatest(
|
||||||
|
...authorizations
|
||||||
|
.filter((authorization: Authorization) => hasValue(authorization.feature))
|
||||||
|
.map((authorization: Authorization) => authorization.feature.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return observableOf([]);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
map((features: Feature[]) => features.filter((feature: Feature) => feature.id === featureID).length > 0)
|
||||||
|
);
|
@@ -0,0 +1,66 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test implementation of abstract class FeatureAuthorizationGuard
|
||||||
|
* Provide the return values of the overwritten getters as constructor arguments
|
||||||
|
*/
|
||||||
|
class FeatureAuthorizationGuardImpl extends FeatureAuthorizationGuard {
|
||||||
|
constructor(protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router,
|
||||||
|
protected featureId: FeatureID,
|
||||||
|
protected objectUrl: string,
|
||||||
|
protected ePersonUuid: string) {
|
||||||
|
super(authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFeatureID(): FeatureID {
|
||||||
|
return this.featureId;
|
||||||
|
}
|
||||||
|
|
||||||
|
getObjectUrl(): string {
|
||||||
|
return this.objectUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEPersonUuid(): string {
|
||||||
|
return this.ePersonUuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('FeatureAuthorizationGuard', () => {
|
||||||
|
let guard: FeatureAuthorizationGuard;
|
||||||
|
let authorizationService: AuthorizationDataService;
|
||||||
|
let router: Router;
|
||||||
|
|
||||||
|
let featureId: FeatureID;
|
||||||
|
let objectUrl: string;
|
||||||
|
let ePersonUuid: string;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
featureId = FeatureID.LoginOnBehalfOf;
|
||||||
|
objectUrl = 'fake-object-url';
|
||||||
|
ePersonUuid = 'fake-eperson-uuid';
|
||||||
|
|
||||||
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true)
|
||||||
|
});
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
parseUrl: {}
|
||||||
|
});
|
||||||
|
guard = new FeatureAuthorizationGuardImpl(authorizationService, router, featureId, objectUrl, ePersonUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canActivate', () => {
|
||||||
|
it('should call authorizationService.isAuthenticated with the appropriate arguments', () => {
|
||||||
|
guard.canActivate(undefined, undefined).subscribe();
|
||||||
|
expect(authorizationService.isAuthorized).toHaveBeenCalledWith(featureId, objectUrl, ePersonUuid);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
CanActivate,
|
||||||
|
Router,
|
||||||
|
RouterStateSnapshot,
|
||||||
|
UrlTree
|
||||||
|
} from '@angular/router';
|
||||||
|
import { AuthorizationDataService } from '../authorization-data.service';
|
||||||
|
import { FeatureID } from '../feature-id';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { returnUnauthorizedUrlTreeOnFalse } from '../../../shared/operators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 FeatureAuthorizationGuard implements CanActivate {
|
||||||
|
constructor(protected authorizationService: AuthorizationDataService,
|
||||||
|
protected router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of feature to check authorization for
|
||||||
|
* Override this method to define a feature
|
||||||
|
*/
|
||||||
|
abstract getFeatureID(): 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have administrator
|
||||||
|
* rights to the {@link Site}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class SiteAdministratorGuard extends FeatureAuthorizationGuard {
|
||||||
|
constructor(protected authorizationService: AuthorizationDataService, protected router: Router) {
|
||||||
|
super(authorizationService, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check administrator authorization rights
|
||||||
|
*/
|
||||||
|
getFeatureID(): FeatureID {
|
||||||
|
return FeatureID.AdministratorOf;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,72 @@
|
|||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { FEATURE } from '../../shared/feature.resource-type';
|
||||||
|
import { dataService } from '../../cache/builders/build-decorators';
|
||||||
|
import { DataService } from '../data.service';
|
||||||
|
import { Feature } from '../../shared/feature.model';
|
||||||
|
import { RequestService } from '../request.service';
|
||||||
|
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../../core.reducers';
|
||||||
|
import { ObjectCacheService } from '../../cache/object-cache.service';
|
||||||
|
import { HALEndpointService } from '../../shared/hal-endpoint.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { DSOChangeAnalyzer } from '../dso-change-analyzer.service';
|
||||||
|
import { FindListOptions, FindListRequest } from '../request.models';
|
||||||
|
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { RemoteData } from '../remote-data';
|
||||||
|
import { PaginatedList } from '../paginated-list';
|
||||||
|
import { find, skipWhile, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A service to retrieve {@link Feature}s from the REST API
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
@dataService(FEATURE)
|
||||||
|
export class FeatureDataService extends DataService<Feature> {
|
||||||
|
protected linkPath = 'features';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DSOChangeAnalyzer<Feature>
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a new FindListRequest with given search method
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @param linksToFollow The array of [[FollowLinkConfig]]
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<Feature>>}
|
||||||
|
* Return an observable that emits response from the server
|
||||||
|
*/
|
||||||
|
searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<Feature>>): Observable<RemoteData<PaginatedList<Feature>>> {
|
||||||
|
const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||||
|
|
||||||
|
return hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
tap((href: string) => {
|
||||||
|
this.requestService.removeByHrefSubstring(href);
|
||||||
|
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
|
||||||
|
|
||||||
|
this.requestService.configure(request);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
switchMap((href) => this.requestService.getByHref(href)),
|
||||||
|
skipWhile((requestEntry) => hasValue(requestEntry) && requestEntry.completed),
|
||||||
|
switchMap((href) =>
|
||||||
|
this.rdbService.buildList<Feature>(hrefObs, ...linksToFollow) as Observable<RemoteData<PaginatedList<Feature>>>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
7
src/app/core/data/feature-authorization/feature-id.ts
Normal file
7
src/app/core/data/feature-authorization/feature-id.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Enum object for all possible {@link Feature} IDs
|
||||||
|
*/
|
||||||
|
export enum FeatureID {
|
||||||
|
LoginOnBehalfOf = 'loginOnBehalfOf',
|
||||||
|
AdministratorOf = 'administratorOf'
|
||||||
|
}
|
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { createSelector, select, Store } from '@ngrx/store';
|
import { createSelector, select, Store } from '@ngrx/store';
|
||||||
import { Operation } from 'fast-json-patch/lib/core';
|
import { Operation } from 'fast-json-patch/lib/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, find, map, take } from 'rxjs/operators';
|
import { filter, find, map, skipWhile, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
EPeopleRegistryCancelEPersonAction,
|
EPeopleRegistryCancelEPersonAction,
|
||||||
EPeopleRegistryEditEPersonAction
|
EPeopleRegistryEditEPersonAction
|
||||||
@@ -300,4 +300,33 @@ export class EPersonDataService extends DataService<EPerson> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a new FindListRequest with given search method
|
||||||
|
*
|
||||||
|
* @param searchMethod The search method for the object
|
||||||
|
* @param options The [[FindListOptions]] object
|
||||||
|
* @param linksToFollow The array of [[FollowLinkConfig]]
|
||||||
|
* @return {Observable<RemoteData<PaginatedList<EPerson>>}
|
||||||
|
* Return an observable that emits response from the server
|
||||||
|
*/
|
||||||
|
searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<EPerson>>): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
|
const hrefObs = this.getSearchByHref(searchMethod, options, ...linksToFollow);
|
||||||
|
|
||||||
|
return hrefObs.pipe(
|
||||||
|
find((href: string) => hasValue(href)),
|
||||||
|
tap((href: string) => {
|
||||||
|
this.requestService.removeByHrefSubstring(href);
|
||||||
|
const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
|
||||||
|
|
||||||
|
this.requestService.configure(request);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
switchMap((href) => this.requestService.getByHref(href)),
|
||||||
|
skipWhile((requestEntry) => hasValue(requestEntry) && requestEntry.completed),
|
||||||
|
switchMap((href) =>
|
||||||
|
this.rdbService.buildList<EPerson>(hrefObs, ...linksToFollow) as Observable<RemoteData<PaginatedList<EPerson>>>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -168,7 +168,8 @@ describe('MetadataService', () => {
|
|||||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||||
Meta,
|
Meta,
|
||||||
Title,
|
Title,
|
||||||
ItemDataService,
|
// tslint:disable-next-line:no-empty
|
||||||
|
{ provide: ItemDataService, useValue: { findById: () => {} } },
|
||||||
BrowseService,
|
BrowseService,
|
||||||
MetadataService
|
MetadataService
|
||||||
],
|
],
|
||||||
|
@@ -20,6 +20,10 @@ export class ServerResponseService {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setUnauthorized(message = 'Unauthorized'): this {
|
||||||
|
return this.setStatus(401, message)
|
||||||
|
}
|
||||||
|
|
||||||
setNotFound(message = 'Not found'): this {
|
setNotFound(message = 'Not found'): this {
|
||||||
return this.setStatus(404, message)
|
return this.setStatus(404, message)
|
||||||
}
|
}
|
||||||
|
54
src/app/core/shared/authorization.model.ts
Normal file
54
src/app/core/shared/authorization.model.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { link, typedObject } from '../cache/builders/build-decorators';
|
||||||
|
import { AUTHORIZATION } from './authorization.resource-type';
|
||||||
|
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
|
||||||
|
import { HALLink } from './hal-link.model';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { EPerson } from '../eperson/models/eperson.model';
|
||||||
|
import { EPERSON } from '../eperson/models/eperson.resource-type';
|
||||||
|
import { FEATURE } from './feature.resource-type';
|
||||||
|
import { DSpaceObject } from './dspace-object.model';
|
||||||
|
import { Feature } from './feature.model';
|
||||||
|
import { ITEM } from './item.resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a DSpace Authorization
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
@inheritSerialization(DSpaceObject)
|
||||||
|
export class Authorization extends DSpaceObject {
|
||||||
|
static type = AUTHORIZATION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique identifier for this authorization
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink;
|
||||||
|
eperson: HALLink;
|
||||||
|
feature: HALLink;
|
||||||
|
object: HALLink;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The EPerson this Authorization belongs to
|
||||||
|
* Null if the authorization grants access to anonymous users
|
||||||
|
*/
|
||||||
|
@link(EPERSON)
|
||||||
|
eperson?: Observable<RemoteData<EPerson>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Feature enabled by this Authorization
|
||||||
|
*/
|
||||||
|
@link(FEATURE)
|
||||||
|
feature?: Observable<RemoteData<Feature>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Object this authorization applies to
|
||||||
|
*/
|
||||||
|
@link(ITEM)
|
||||||
|
object?: Observable<RemoteData<DSpaceObject>>;
|
||||||
|
}
|
9
src/app/core/shared/authorization.resource-type.ts
Normal file
9
src/app/core/shared/authorization.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for Authorization
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const AUTHORIZATION = new ResourceType('authorization');
|
37
src/app/core/shared/feature.model.ts
Normal file
37
src/app/core/shared/feature.model.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { typedObject } from '../cache/builders/build-decorators';
|
||||||
|
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
|
||||||
|
import { FEATURE } from './feature.resource-type';
|
||||||
|
import { DSpaceObject } from './dspace-object.model';
|
||||||
|
import { HALLink } from './hal-link.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a DSpace Feature
|
||||||
|
*/
|
||||||
|
@typedObject
|
||||||
|
@inheritSerialization(DSpaceObject)
|
||||||
|
export class Feature extends DSpaceObject {
|
||||||
|
static type = FEATURE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique identifier for this feature
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A human readable description of the feature's purpose
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of resource types this feature applies to
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
resourcetypes: string[];
|
||||||
|
|
||||||
|
@deserialize
|
||||||
|
_links: {
|
||||||
|
self: HALLink;
|
||||||
|
};
|
||||||
|
}
|
9
src/app/core/shared/feature.resource-type.ts
Normal file
9
src/app/core/shared/feature.resource-type.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ResourceType } from './resource-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource type for Feature
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular
|
||||||
|
* dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const FEATURE = new ResourceType('feature');
|
@@ -1,4 +1,4 @@
|
|||||||
import { Router } from '@angular/router';
|
import { Router, UrlTree } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, find, flatMap, map, take, tap } from 'rxjs/operators';
|
import { filter, find, flatMap, map, take, tap } from 'rxjs/operators';
|
||||||
import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
|
||||||
@@ -11,6 +11,7 @@ import { RequestEntry } from '../data/request.reducer';
|
|||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { BrowseDefinition } from './browse-definition.model';
|
import { BrowseDefinition } from './browse-definition.model';
|
||||||
import { DSpaceObject } from './dspace-object.model';
|
import { DSpaceObject } from './dspace-object.model';
|
||||||
|
import { getUnauthorizedPath } from '../../app-routing.module';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file contains custom RxJS operators that can be used in multiple places
|
* This file contains custom RxJS operators that can be used in multiple places
|
||||||
@@ -180,6 +181,17 @@ export const redirectToPageNotFoundOn404 = (router: Router) =>
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operator that returns a UrlTree to the unauthorized page when the boolean received is false
|
||||||
|
* @param router
|
||||||
|
*/
|
||||||
|
export const returnUnauthorizedUrlTreeOnFalse = (router: Router) =>
|
||||||
|
(source: Observable<boolean>): Observable<boolean | UrlTree> =>
|
||||||
|
source.pipe(
|
||||||
|
map((authorized: boolean) => {
|
||||||
|
return authorized ? authorized : router.parseUrl(getUnauthorizedPath())
|
||||||
|
}));
|
||||||
|
|
||||||
export const getFinishedRemoteData = () =>
|
export const getFinishedRemoteData = () =>
|
||||||
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||||
source.pipe(find((rd: RemoteData<T>) => !rd.isLoading));
|
source.pipe(find((rd: RemoteData<T>) => !rd.isLoading));
|
||||||
|
10
src/app/unauthorized/unauthorized.component.html
Normal file
10
src/app/unauthorized/unauthorized.component.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="unauthorized container">
|
||||||
|
<h1>401</h1>
|
||||||
|
<h2><small>{{"401.unauthorized" | translate}}</small></h2>
|
||||||
|
<br/>
|
||||||
|
<p>{{"401.help" | translate}}</p>
|
||||||
|
<br/>
|
||||||
|
<p class="text-center">
|
||||||
|
<a routerLink="/home" class="btn btn-primary">{{"401.link.home-page" | translate}}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
0
src/app/unauthorized/unauthorized.component.scss
Normal file
0
src/app/unauthorized/unauthorized.component.scss
Normal file
32
src/app/unauthorized/unauthorized.component.ts
Normal file
32
src/app/unauthorized/unauthorized.component.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
import { ServerResponseService } from '../core/services/server-response.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component representing the `Unauthorized` DSpace page.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-unauthorized',
|
||||||
|
templateUrl: './unauthorized.component.html',
|
||||||
|
styleUrls: ['./unauthorized.component.scss']
|
||||||
|
})
|
||||||
|
export class UnauthorizedComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize instance variables
|
||||||
|
*
|
||||||
|
* @param {AuthService} authservice
|
||||||
|
* @param {ServerResponseService} responseService
|
||||||
|
*/
|
||||||
|
constructor(private authservice: AuthService, private responseService: ServerResponseService) {
|
||||||
|
this.responseService.setUnauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove redirect url from the state
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.authservice.clearRedirectUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,5 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
|
||||||
|
"401.help": "You're not authorized to access this page. You can use the button below to get back to the home page.",
|
||||||
|
|
||||||
|
"401.link.home-page": "Take me to the home page",
|
||||||
|
|
||||||
|
"401.unauthorized": "unauthorized",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ",
|
"404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ",
|
||||||
|
|
||||||
"404.link.home-page": "Take me to the home page",
|
"404.link.home-page": "Take me to the home page",
|
||||||
|
Reference in New Issue
Block a user