1
1
import {
2
2
AccessMap ,
3
3
getLoggerFor ,
4
- InternalServerError , joinUrl ,
4
+ IdentifierStrategy ,
5
+ InternalServerError ,
6
+ isContainerIdentifier ,
7
+ joinUrl ,
5
8
KeyValueStorage ,
6
9
NotFoundHttpError ,
7
10
ResourceIdentifier ,
@@ -67,7 +70,10 @@ const algMap = {
67
70
}
68
71
69
72
/**
70
- * Client interface for the UMA AS
73
+ * Client interface for the UMA AS.
74
+ *
75
+ * This class uses an EventEmitter and an in-memory map to keep track of registration progress,
76
+ * so does not work with worker threads.
71
77
*/
72
78
export class UmaClient implements SingleThreaded {
73
79
protected readonly logger = getLoggerFor ( this ) ;
@@ -80,12 +86,14 @@ export class UmaClient implements SingleThreaded {
80
86
/**
81
87
* @param umaIdStore - Key/value store containing the resource path -> UMA ID bindings.
82
88
* @param fetcher - Used to perform requests targeting the AS.
89
+ * @param identifierStrategy - Utility functions based on the path configuration of the server.
83
90
* @param resourceSet - Will be used to verify existence of resources.
84
91
* @param options - JWT verification options.
85
92
*/
86
93
constructor (
87
94
protected readonly umaIdStore : KeyValueStorage < string , string > ,
88
95
protected readonly fetcher : Fetcher ,
96
+ protected readonly identifierStrategy : IdentifierStrategy ,
89
97
protected readonly resourceSet : ResourceSet ,
90
98
protected readonly options : UmaVerificationOptions = { } ,
91
99
) {
@@ -128,7 +136,7 @@ export class UmaClient implements SingleThreaded {
128
136
// This can be a consequence of adding resources in the wrong way (e.g., copying files),
129
137
// or other special resources, such as derived resources.
130
138
if ( await this . resourceSet . hasResource ( target ) ) {
131
- await this . createResource ( target , issuer ) ;
139
+ await this . registerResource ( target , issuer ) ;
132
140
umaId = await this . umaIdStore . get ( target . path ) ;
133
141
} else {
134
142
throw new NotFoundHttpError ( ) ;
@@ -280,9 +288,31 @@ export class UmaClient implements SingleThreaded {
280
288
return configuration ;
281
289
}
282
290
283
- public async createResource ( resource : ResourceIdentifier , issuer : string ) : Promise < void > {
291
+ /**
292
+ * Updates the UMA registration for the given resource on the given issuer.
293
+ * This either registers a new UMA identifier or updates an existing one,
294
+ * depending on if it already exists.
295
+ * For containers, the resource_defaults will be registered,
296
+ * for all resources, the resource_relations with the parent container will be registered.
297
+ * For the latter, it is possible that the parent container is not registered yet,
298
+ * for example, in the case of seeding multiple resources simultaneously.
299
+ * In that case the registration will be done immediately,
300
+ * and updated with the relations once the parent registration is finished.
301
+ */
302
+ public async registerResource ( resource : ResourceIdentifier , issuer : string ) : Promise < void > {
303
+ if ( this . inProgressResources . has ( resource . path ) ) {
304
+ // It is possible a resource is still being registered when an updated registration is already requested.
305
+ // To prevent duplicate registrations of the same resource,
306
+ // the next call will only happen when the first one is finished.
307
+ await once ( this . registerEmitter , resource . path ) ;
308
+ return this . registerResource ( resource , issuer ) ;
309
+ }
284
310
this . inProgressResources . add ( resource . path ) ;
285
- const { resource_registration_endpoint : endpoint } = await this . fetchUmaConfig ( issuer ) ;
311
+ let { resource_registration_endpoint : endpoint } = await this . fetchUmaConfig ( issuer ) ;
312
+ const knownUmaId = await this . umaIdStore . get ( resource . path ) ;
313
+ if ( knownUmaId ) {
314
+ endpoint = joinUrl ( endpoint , knownUmaId ) ;
315
+ }
286
316
287
317
const description : ResourceDescription = {
288
318
name : resource . path ,
@@ -292,38 +322,77 @@ export class UmaClient implements SingleThreaded {
292
322
'urn:example:css:modes:create' ,
293
323
'urn:example:css:modes:delete' ,
294
324
'urn:example:css:modes:write' ,
295
- ]
325
+ ] ,
296
326
} ;
297
327
298
- this . logger . info ( `Creating resource registration for <${ resource . path } > at <${ endpoint } >` ) ;
328
+ if ( isContainerIdentifier ( resource ) ) {
329
+ description . resource_defaults = { 'http://www.w3.org/ns/ldp#contains' : description . resource_scopes } ;
330
+ }
299
331
300
- const request = {
301
- url : endpoint ,
302
- method : 'POST' ,
332
+ // This function can potentially cause multiple asynchronous calls to be required.
333
+ // These will be stored in this array so they can be executed simultaneously.
334
+ const promises : Promise < void > [ ] = [ ] ;
335
+ if ( ! this . identifierStrategy . isRootContainer ( resource ) ) {
336
+ const parentIdentifier = this . identifierStrategy . getParentContainer ( resource ) ;
337
+ const parentId = await this . umaIdStore . get ( parentIdentifier . path ) ;
338
+ if ( parentId ) {
339
+ description . resource_relations = { '@reverse' : { 'http://www.w3.org/ns/ldp#contains' : [ parentId ] } } ;
340
+ } else {
341
+ this . logger . warn ( `Unable to register parent relationship of ${
342
+ resource . path } due to missing parent ID. Waiting for parent registration.`) ;
343
+
344
+ promises . push (
345
+ once ( this . registerEmitter , parentIdentifier . path )
346
+ . then ( ( ) => this . registerResource ( resource , issuer ) ) ,
347
+ ) ;
348
+ // It is possible the parent is not yet being registered.
349
+ // We need to force a registration in such a case, otherwise the above event will never be fired.
350
+ if ( ! this . inProgressResources . has ( parentIdentifier . path ) ) {
351
+ promises . push ( this . registerResource ( parentIdentifier , issuer ) ) ;
352
+ }
353
+ }
354
+ }
355
+
356
+ this . logger . info (
357
+ `${ knownUmaId ? 'Updating' : 'Creating' } resource registration for <${ resource . path } > at <${ endpoint } >` ,
358
+ ) ;
359
+
360
+ const request : RequestInit = {
361
+ method : knownUmaId ? 'PUT' : 'POST' ,
303
362
headers : {
304
363
'Content-Type' : 'application/json' ,
305
364
'Accept' : 'application/json' ,
306
365
} ,
307
366
body : JSON . stringify ( description ) ,
308
367
} ;
309
368
310
- return this . fetcher . fetch ( endpoint , request ) . then ( async resp => {
311
- if ( resp . status !== 201 ) {
312
- throw new Error ( `Resource registration request failed. ${ await resp . text ( ) } ` ) ;
313
- }
369
+ const fetchPromise = this . fetcher . fetch ( endpoint , request ) . then ( async resp => {
370
+ if ( knownUmaId ) {
371
+ if ( resp . status !== 200 ) {
372
+ throw new InternalServerError ( `Resource update request failed. ${ await resp . text ( ) } ` ) ;
373
+ }
374
+ } else {
375
+ if ( resp . status !== 201 ) {
376
+ throw new InternalServerError ( `Resource registration request failed. ${ await resp . text ( ) } ` ) ;
377
+ }
314
378
315
- const { _id : umaId } = await resp . json ( ) ;
379
+ const { _id : umaId } = await resp . json ( ) ;
316
380
317
- if ( ! umaId || typeof umaId !== 'string' ) {
318
- throw new Error ( 'Unexpected response from UMA server; no UMA id received.' ) ;
319
- }
381
+ if ( ! isString ( umaId ) ) {
382
+ throw new InternalServerError ( 'Unexpected response from UMA server; no UMA id received.' ) ;
383
+ }
320
384
321
- await this . umaIdStore . set ( resource . path , umaId ) ;
322
- this . logger . info ( `Registered resource ${ resource . path } with UMA ID ${ umaId } ` ) ;
385
+ await this . umaIdStore . set ( resource . path , umaId ) ;
386
+ this . logger . info ( `Registered resource ${ resource . path } with UMA ID ${ umaId } ` ) ;
387
+ }
323
388
// Indicate this resource finished registration
324
389
this . inProgressResources . delete ( resource . path ) ;
325
390
this . registerEmitter . emit ( resource . path ) ;
326
391
} ) ;
392
+
393
+ // Execute all the required promises.
394
+ promises . push ( fetchPromise ) ;
395
+ await Promise . all ( promises ) ;
327
396
}
328
397
329
398
/**
0 commit comments