diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 465fb69dd2..50a285bdf9 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -1,12 +1,19 @@ import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; -import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; import { GlobalConfig } from '../../../config/global-config.interface'; import { isNotEmpty } from '../../shared/empty.util'; -import { AuthGetRequest, AuthPostRequest, GetRequest, PostRequest, RestRequest } from '../data/request.models'; -import { AuthStatusResponse, ErrorResponse } from '../cache/response.models'; +import { + AuthGetRequest, + AuthPostRequest, + GetRequest, + PostRequest, + RestRequest, + TokenPostRequest +} from '../data/request.models'; +import { AuthStatusResponse, ErrorResponse, RestResponse, TokenResponse } from '../cache/response.models'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { getResponseFromEntry } from '../shared/operators'; import { HttpClient } from '@angular/common/http'; @@ -15,6 +22,7 @@ import { HttpClient } from '@angular/common/http'; export class AuthRequestService { protected linkName = 'authn'; protected browseEndpoint = ''; + protected shortlivedtokensEndpoint = 'shortlivedtokens'; constructor(protected halService: HALEndpointService, protected requestService: RequestService, @@ -67,4 +75,19 @@ export class AuthRequestService { mergeMap((request: GetRequest) => this.fetchRequest(request)), distinctUntilChanged()); } + + /** + * Send a POST request to retrieve a short-lived token which provides download access of restricted files + */ + public getShortlivedToken(): Observable { + return this.halService.getEndpoint(`${this.linkName}/${this.shortlivedtokensEndpoint}`).pipe( + filter((href: string) => isNotEmpty(href)), + distinctUntilChanged(), + map((endpointURL: string) => new TokenPostRequest(this.requestService.generateRequestId(), endpointURL)), + tap((request: PostRequest) => this.requestService.configure(request)), + switchMap((request: PostRequest) => this.requestService.getByUUID(request.uuid)), + getResponseFromEntry(), + map((response: TokenResponse) => response.token) + ); + } } diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 588d9e2675..dc1ad4ddd7 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -546,4 +546,14 @@ export class AuthService { return this.getImpersonateID() === epersonId; } + /** + * Get a short-lived token for appending to download urls of restricted files + * Returns null if the user isn't authenticated + */ + getShortlivedToken(): Observable { + return this.isAuthenticated().pipe( + switchMap((authenticated) => authenticated ? this.authRequestService.getShortlivedToken() : observableOf(null)) + ); + } + } diff --git a/src/app/core/auth/token-response-parsing.service.ts b/src/app/core/auth/token-response-parsing.service.ts new file mode 100644 index 0000000000..a1b1e23aa4 --- /dev/null +++ b/src/app/core/auth/token-response-parsing.service.ts @@ -0,0 +1,23 @@ +import { ResponseParsingService } from '../data/parsing.service'; +import { RestRequest } from '../data/request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { RestResponse, TokenResponse } from '../cache/response.models'; +import { isNotEmpty } from '../../shared/empty.util'; +import { Injectable } from '@angular/core'; + +@Injectable() +/** + * A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a token string + * wrapped in a TokenResponse + */ +export class TokenResponseParsingService implements ResponseParsingService { + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + if (isNotEmpty(data.payload) && isNotEmpty(data.payload.token) && (data.statusCode === 200)) { + return new TokenResponse(data.payload.token, true, data.statusCode, data.statusText); + } else { + return new TokenResponse(null, false, data.statusCode, data.statusText) + } + } + +} diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts index 3f46ecf647..7439b05355 100644 --- a/src/app/core/cache/response.models.ts +++ b/src/app/core/cache/response.models.ts @@ -211,6 +211,20 @@ export class AuthStatusResponse extends RestResponse { } } +/** + * A REST Response containing a token + */ +export class TokenResponse extends RestResponse { + constructor( + public token: string, + public isSuccessful: boolean, + public statusCode: number, + public statusText: string + ) { + super(isSuccessful, statusCode, statusText); + } +} + export class IntegrationSuccessResponse extends RestResponse { constructor( public dataDefinition: PaginatedList, diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 715f7a5cc0..7426cffda3 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -145,6 +145,7 @@ 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 { TokenResponseParsingService } from './auth/token-response-parsing.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -264,6 +265,7 @@ const PROVIDERS = [ LicenseDataService, ItemTypeDataService, WorkflowActionDataService, + TokenResponseParsingService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 5866cce797..8f05114b32 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -20,6 +20,7 @@ import { URLCombiner } from '../url-combiner/url-combiner'; import { TaskResponseParsingService } from '../tasks/task-response-parsing.service'; import { ContentSourceResponseParsingService } from './content-source-response-parsing.service'; import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service'; +import { TokenResponseParsingService } from '../auth/token-response-parsing.service'; /* tslint:disable:max-classes-per-file */ @@ -241,6 +242,15 @@ export class AuthGetRequest extends GetRequest { } } +/** + * A POST request for retrieving a token + */ +export class TokenPostRequest extends PostRequest { + getResponseParser(): GenericConstructor { + return TokenResponseParsingService; + } +} + export class IntegrationRequest extends GetRequest { constructor(uuid: string, href: string) { super(uuid, href); diff --git a/src/app/core/shared/file.service.ts b/src/app/core/shared/file.service.ts index 7e89a4e5dd..841cb60869 100644 --- a/src/app/core/shared/file.service.ts +++ b/src/app/core/shared/file.service.ts @@ -1,10 +1,10 @@ -import { Injectable } from '@angular/core'; -import { HttpHeaders } from '@angular/common/http'; - -import { DSpaceRESTv2Service, HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { RestRequestMethod } from '../data/rest-request-method'; -import { saveAs } from 'file-saver'; +import { Inject, Injectable } from '@angular/core'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { AuthService } from '../auth/auth.service'; +import { take } from 'rxjs/operators'; +import { NativeWindowRef, NativeWindowService } from '../services/window.service'; +import { URLCombiner } from '../url-combiner/url-combiner'; +import { hasValue } from '../../shared/empty.util'; /** * Provides utility methods to save files on the client-side. @@ -12,22 +12,20 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response. @Injectable() export class FileService { constructor( - private restService: DSpaceRESTv2Service + @Inject(NativeWindowService) protected _window: NativeWindowRef, + private authService: AuthService ) { } /** - * Makes a HTTP Get request to download a file + * Combines an URL with a short-lived token and sets the current URL to the newly created one * * @param url * file url */ downloadFile(url: string) { - const headers = new HttpHeaders(); - const options: HttpOptions = Object.create({headers, responseType: 'blob'}); - return this.restService.request(RestRequestMethod.GET, url, null, options) - .subscribe((data) => { - saveAs(data.payload as Blob, this.getFileNameFromResponseContentDisposition(data)); - }); + this.authService.getShortlivedToken().pipe(take(1)).subscribe((token) => { + this._window.nativeWindow.location.href = hasValue(token) ? new URLCombiner(url, `?token=${token}`).toString() : url; + }); } /**