diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 715f7a5cc0..af394ea71a 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -145,6 +145,10 @@ import { Version } from './shared/version.model'; import { VersionHistory } from './shared/version-history.model'; import { WorkflowActionDataService } from './data/workflow-action-data.service'; import { WorkflowAction } from './tasks/models/workflow-action-object.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'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -264,6 +268,8 @@ const PROVIDERS = [ LicenseDataService, ItemTypeDataService, WorkflowActionDataService, + FeatureDataService, + AuthorizationDataService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, @@ -314,7 +320,9 @@ export const models = ExternalSourceEntry, Version, VersionHistory, - WorkflowAction + WorkflowAction, + Feature, + Authorization ]; @NgModule({ diff --git a/src/app/core/data/feature-authorization/authorization-data.service.ts b/src/app/core/data/feature-authorization/authorization-data.service.ts new file mode 100644 index 0000000000..57a02435dc --- /dev/null +++ b/src/app/core/data/feature-authorization/authorization-data.service.ts @@ -0,0 +1,126 @@ +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 { 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, skipWhile, 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 } from './authorization-utils'; + +/** + * A service to retrieve {@link Authorization}s from the REST API + */ +@Injectable() +@dataService(AUTHORIZATION) +export class AuthorizationDataService extends DataService { + protected linkPath = 'authorizations'; + protected searchByObjectPath = 'object'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer, + protected authService: AuthService, + protected siteService: SiteDataService + ) { + super(); + } + + /** + * 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(objectUrl?: string, ePersonUuid?: string, featureId?: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + 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>} + * Return an observable that emits response from the server + */ + searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + 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(hrefObs, ...linksToFollow) as Observable>> + ) + ); + } + + /** + * 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?: string): 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] + }); + } +} diff --git a/src/app/core/data/feature-authorization/authorization-search-params.ts b/src/app/core/data/feature-authorization/authorization-search-params.ts new file mode 100644 index 0000000000..8d545039ba --- /dev/null +++ b/src/app/core/data/feature-authorization/authorization-search-params.ts @@ -0,0 +1,11 @@ +export class AuthorizationSearchParams { + objectUrl: string; + ePersonUuid: string; + featureId: string; + + constructor(objectUrl?: string, ePersonUuid?: string, featureId?: string) { + this.objectUrl = objectUrl; + this.ePersonUuid = ePersonUuid; + this.featureId = featureId; + } +} diff --git a/src/app/core/data/feature-authorization/authorization-utils.ts b/src/app/core/data/feature-authorization/authorization-utils.ts new file mode 100644 index 0000000000..6cc5a3d2ef --- /dev/null +++ b/src/app/core/data/feature-authorization/authorization-utils.ts @@ -0,0 +1,53 @@ +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 } from '../../../shared/empty.util'; +import { of as observableOf } from 'rxjs'; +import { AuthService } from '../../auth/auth.service'; + +/** + * 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): Observable => + 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): Observable => + 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 { + observableOf(params) + } + }) + ); + } else { + return observableOf(params); + } + }) + ); diff --git a/src/app/core/data/feature-authorization/feature-data.service.ts b/src/app/core/data/feature-authorization/feature-data.service.ts new file mode 100644 index 0000000000..8003b6c31d --- /dev/null +++ b/src/app/core/data/feature-authorization/feature-data.service.ts @@ -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 { + protected linkPath = 'features'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer + ) { + 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>} + * Return an observable that emits response from the server + */ + searchBy(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + 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(hrefObs, ...linksToFollow) as Observable>> + ) + ); + } +} diff --git a/src/app/core/shared/authorization.model.ts b/src/app/core/shared/authorization.model.ts new file mode 100644 index 0000000000..0dfb7fd631 --- /dev/null +++ b/src/app/core/shared/authorization.model.ts @@ -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>; + + /** + * The Feature enabled by this Authorization + */ + @link(FEATURE) + feature?: Observable>; + + /** + * The Object this authorization applies to + */ + @link(ITEM) + object?: Observable>; +} diff --git a/src/app/core/shared/authorization.resource-type.ts b/src/app/core/shared/authorization.resource-type.ts new file mode 100644 index 0000000000..e5547314b3 --- /dev/null +++ b/src/app/core/shared/authorization.resource-type.ts @@ -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'); diff --git a/src/app/core/shared/feature.model.ts b/src/app/core/shared/feature.model.ts new file mode 100644 index 0000000000..cc0fd2db87 --- /dev/null +++ b/src/app/core/shared/feature.model.ts @@ -0,0 +1,31 @@ +import { typedObject } from '../cache/builders/build-decorators'; +import { autoserialize, inheritSerialization } from 'cerialize'; +import { FEATURE } from './feature.resource-type'; +import { DSpaceObject } from './dspace-object.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[]; +} diff --git a/src/app/core/shared/feature.resource-type.ts b/src/app/core/shared/feature.resource-type.ts new file mode 100644 index 0000000000..98ee526ce3 --- /dev/null +++ b/src/app/core/shared/feature.resource-type.ts @@ -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');