1 /* eslint-disable no-unused-vars */
4 const common
= require('../common');
5 const DatabaseErrors
= require('./errors');
6 const svh
= require('./schema-version-helper');
7 const uuid
= require('uuid');
9 const _fileScope
= common
.fileScope(__filename
);
12 constructor(logger
, options
) {
14 this.options
= options
;
19 * Perform tasks needed to prepare database for use. Ensure this is called
20 * after construction, and before any other database activity.
21 * At the minimum, this will validate a compatible schema is present and usable.
22 * Some engines will also perform other initializations or async actions which
23 * are easier handled outside the constructor.
26 const _scope
= _fileScope('initialize');
28 const currentSchema
= await
this._currentSchema();
29 const current
= svh
.schemaVersionObjectToNumber(currentSchema
);
30 const min
= svh
.schemaVersionObjectToNumber(this.schemaVersionsSupported
.min
);
31 const max
= svh
.schemaVersionObjectToNumber(this.schemaVersionsSupported
.max
);
32 if (current
>= min
&& current
<= max
) {
33 this.logger
.debug(_scope
, 'schema supported', { currentSchema
, schemaVersionsSupported: this.schemaVersionsSupported
});
35 this.logger
.error(_scope
, 'schema not supported', { currentSchema
, schemaVersionsSupported: this.schemaVersionsSupported
});
36 throw new DatabaseErrors
.MigrationNeeded();
42 * @typedef {Object} SchemaVersionObject
43 * @property {Number} major
44 * @property {Number} minor
45 * @property {Number} patch
48 * Query the current schema version.
49 * This is a standalone query function, as it is called before statements are loaded.
50 * @returns {Promise<SchemaVersionObject>}
52 async
_currentSchema() {
53 this._notImplemented('_currentSchema', arguments
);
58 * Perform db connection health-check, if applicable.
59 * Throw something if a database situation should pull us out of a load-balancer.
62 this._notImplemented('healthCheck', arguments
);
67 * Wrap a function call in a database context.
68 * @param {Function} fn fn(ctx)
71 this._notImplemented('context', arguments
);
75 * Wrap a function call in a transaction context.
77 * @param {Function} fn fn(txCtx)
79 async
transaction(dbCtx
, fn
) {
80 this._notImplemented('transaction', arguments
);
85 * Basic type checking of object properties.
87 * Types may be any of the built-in types:
89 * - bigint (also allowed with 'number')
91 * - number (this will also allow 'bigint')
97 * Types may also be any of the following:
104 * @param {Object} object
105 * @param {String[]} properties
106 * @param {String[]} types
108 _ensureTypes(object
, properties
, types
) {
109 const _scope
= _fileScope('_ensureTypes');
111 if (!(object
&& properties
&& types
)) {
112 this.logger
.error(_scope
, 'undefined argument', { object
, properties
, types
});
113 throw new DatabaseErrors
.DataValidation();
116 const supportedTypes
= [
132 types
.forEach((t
) => {
133 if (!supportedTypes
.includes(t
)) {
134 this.logger
.error(_scope
, 'unsupported type', { object
, properties
, types
, unsupportedType: t
});
135 throw new DatabaseErrors
.DataValidation();
139 properties
.forEach((p
) => {
140 // eslint-disable-next-line security/detect-object-injection
141 const pObj
= object
[p
];
142 const pType
= typeof pObj
;
143 if (!types
.includes(pType
)
144 && !(types
.includes('array') && Array
.isArray(pObj
))
145 && !(types
.includes('buffer') && pObj
instanceof Buffer
)
146 && !(types
.includes('date') && pObj
instanceof Date
)
147 && !(types
.includes('infinites') && Math
.abs(pObj
) === Infinity
)
148 && !(types
.includes('null') && pObj
=== null)
149 && !(types
.includes('number') && pType
=== 'bigint')
150 && !(types
.includes('uuid') && uuid
.validate(pObj
))) {
151 const reason
= `'${p}' is '${pType}', but must be ${types.length > 1 ? 'one of ' : ''}'${types}'`;
152 this.logger
.error(_scope
, reason
, {});
153 throw new DatabaseErrors
.DataValidation(reason
);
160 * @typedef {Object} Authentication
161 * @property {String} identifier
162 * @property {String=} credential
163 * @property {Date} created
164 * @property {Date=} lastAuthentication
167 * @param {Authentication} authentication
169 _validateAuthentication(authentication
) {
171 [['identifier'], ['string']],
172 [['credential'], ['string', 'null']],
173 [['created'], ['date']],
174 [['lastAuthentication'], ['date', 'infinites']],
175 ].forEach(([properties
, types
]) => this._ensureTypes(authentication
, properties
, types
));
180 * @typedef {Object} Resource
181 * @property {String} resourceId - uuid
182 * @property {String} secret
183 * @property {String} description
184 * @property {Date} created
187 * @param {Resource} resource
189 _validateResource(resource
) {
191 [['resourceId', 'secret', 'description'], ['string']],
192 [['resourceId'], ['uuid']],
193 [['created'], ['date']],
194 ].forEach(([properties
, types
]) => this._ensureTypes(resource
, properties
, types
));
199 * @typedef {Object} Token
200 * @property {String} codeId - uuid
201 * @property {String} profile
202 * @property {Date} created
203 * @property {Date=} expires
204 * @property {Date=} refreshExpires
205 * @property {Date=} refreshed
206 * @property {*=} duration
207 * @property {*=} refreshDuration
208 * @property {Number|BigInt=} refresh_count
209 * @property {Boolean} is_revoked
210 * @property {Boolean} is_token
211 * @property {String} client_id
212 * @property {String[]} scopes
213 * @property {Object=} profileData
216 * @param {Token} token
218 _validateToken(token
) {
220 [['codeId', 'profile', 'clientId'], ['string']],
221 [['codeId'], ['uuid']],
222 [['created'], ['date']],
223 [['expires', 'refreshExpires', 'refreshed'], ['date', 'null']],
224 [['isToken', 'isRevoked'], ['boolean']],
225 [['scopes'], ['array']],
226 [['profileData'], ['object', 'null']],
227 ].forEach(([properties
, types
]) => this._ensureTypes(token
, properties
, types
));
228 this._ensureTypes(token
.scopes
, Object
.keys(token
.scopes
), ['string']);
233 * Interface methods need implementations. Ensure the db-interaction
234 * methods on the base class call this, so they may be overridden by
235 * implementation classes.
236 * @param {String} method
237 * @param {arguments} args
239 _notImplemented(method
, args
) {
240 this.logger
.error(_fileScope(method
), 'abstract method called', Array
.from(args
));
241 throw new DatabaseErrors
.NotImplemented(method
);
246 * Get all the almanac entries.
249 async
almanacGetAll(dbCtx
) {
250 this._notImplemented('almanacGetAll', arguments
);
255 * Insert or update an almanac entry.
257 * @param {String} event
258 * @param {Date=} date
260 async
almanacUpsert(dbCtx
, event
, date
) {
261 this._notImplemented('almanacUpsert', arguments
);
266 * Fetch the authentication record for an identifier.
268 * @param {String} identifier
269 * @returns {Promise<Authentication>}
271 async
authenticationGet(dbCtx
, identifier
) {
272 this._notImplemented('authenticationGet', arguments
);
277 * Update the authentication record for the identifier that
278 * correct credentials have been supplied.
280 * @param {String} identifier
281 * @returns {Promise<void>}
283 async
authenticationSuccess(dbCtx
, identifier
) {
284 this._notImplemented('authenticationSuccess', arguments
);
289 * Insert or update the credential for an identifier.
291 * @param {String} identifier
292 * @param {String} credential
293 * @param {String=} otpKey
294 * @returns {Promise<void>}
296 async
authenticationUpsert(dbCtx
, identifier
, credential
, otpKey
) {
297 this._notImplemented('authenticationUpsert', arguments
);
302 * Update the otpKey for an identifier.
304 * @param {String} identifier
305 * @param {String=} otpKey
306 * @returns {Promise<void>}
308 async
authenticationUpdateOTPKey(dbCtx
, identifier
, otpKey
) {
309 this._notImplemented('authenticationUpdateOTPKey', arguments
);
314 * Update the credential for an identifier.
316 * @param {String} identifier
317 * @param {String} credential
318 * @returns {Promise<void>}
320 async
authenticationUpdateCredential(dbCtx
, identifier
, credential
) {
321 this._notImplemented('authenticationUpdateCredentials', arguments
);
326 * Determine if profile url is known to this service.
328 * @param {String} profile
329 * @returns {Promise<Boolean>}
331 async
profileIsValid(dbCtx
, profile
) {
332 this._notImplemented('profileGet', arguments
);
337 * Insert a new relationship between a profile endpoint and
338 * an authenticated identifier.
340 * @param {String} profile
341 * @param {String} identifier
342 * @returns {Promise<void>}
344 async
profileIdentifierInsert(dbCtx
, profile
, identifier
) {
345 this._notImplemented('profileIdentifierInsert', arguments
);
350 * Adds a scope to be available for a profile to include on any authorization request.
352 * @param {String} profile
353 * @param {String} scope
354 * @returns {Promise<void>}
356 async
profileScopeInsert(dbCtx
, profile
, scope
) {
357 this._notImplemented('profileScopeInsert', arguments
);
362 * @typedef {Object} ScopeDetails
363 * @property {String} description
364 * @property {String[]=} profiles
367 * @typedef {Object.<String, Object>} ProfileScopes
368 * @property {Object.<String, Object>} profile
369 * @property {Object.<String, ScopeDetails>} profile.scope
372 * @typedef {Object.<String, Object>} ScopeIndex
373 * @property {ScopeDetails} scope
376 * @typedef {Object} ProfilesScopesReturn
377 * @property {ProfileScopes} profileScopes
378 * @property {ScopeIndex} scopeIndex
379 * @property {String[]} profiles
382 * Returns an object containing:
383 * - an object with profiles as keys to objects with scopes as keys to scope objects,
384 * which each contain a description of the scope and a list of profiles offering it
385 * - an object with scopes as keys to the same scope objects
386 * - a list of profiles
388 * @param {String} identifier
389 * @returns {Promise<ProfileScopesReturn>}
391 async
profilesScopesByIdentifier(dbCtx
, identifier
) {
392 this._notImplemented('profilesScopesByIdentifier', arguments
);
397 * @typedef ProfileScopesRow
400 * @property description
401 * @property application
402 * @property isPermanent
403 * @property isManuallyAdded
406 * Convert db row data into associative structures.
407 * Same behavior is shared by multiple engines.
408 * @param {ProfileScopesRow[]} profileScopesRows
409 * @returns {ProfileScopesReturn}
411 static _profilesScopesBuilder(profileScopesRows
) {
412 const scopeIndex
= {};
413 const profileScopes
= {};
414 const profileSet
= new Set();
416 (profileScopesRows
|| []).forEach(({ profile
, scope
, description
, application
, isPermanent
, isManuallyAdded
}) => {
417 if (scope
&& !(scope
in scopeIndex
)) {
418 scopeIndex
[scope
] = { // eslint-disable-line security/detect-object-injection
427 profileSet
.add(profile
);
428 if (!(profile
in profileScopes
)) {
429 profileScopes
[profile
] = {}; // eslint-disable-line security/detect-object-injection
432 if (profile
&& scope
) {
433 scopeIndex
[scope
].profiles
.push(profile
); // eslint-disable-line security/detect-object-injection
434 profileScopes
[profile
][scope
] = scopeIndex
[scope
]; // eslint-disable-line security/detect-object-injection
439 profiles: [...profileSet
],
447 * Sets list of additional scopes available to profile.
449 * @param {String} profile
450 * @param {String[]} scopes
451 * @returns {Promise<void>}
453 async
profileScopesSetAll(dbCtx
, profile
, scopes
) {
454 this._notImplemented('profileScopesSetAll', arguments
);
459 * Create (or revoke a duplicate) code as a token entry.
461 * @param {Object} data
462 * @param {String} data.codeId
463 * @param {Date} data.created
464 * @param {Boolean} data.isToken
465 * @param {String} data.clientId
466 * @param {String} data.profile - profile uri
467 * @param {String} data.identifier
468 * @param {String[]} data.scopes
469 * @param {Number|Null} data.lifespanSeconds - null sets expiration to Infinity
470 * @param {Number|Null} data.refreshLifespanSeconds - null sets refresh to none
471 * @param {String|Null} data.resource
472 * @param {Object|Null} data.profileData - profile data from profile uri
473 * @returns {Promise<Boolean>} whether redemption was successful
475 async
redeemCode(dbCtx
, { codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshLifespanSeconds
, profileData
} = {}) {
476 this._notImplemented('redeemCode', arguments
);
481 * @typedef {Object} RefreshedToken
482 * @property {Date} expires
483 * @property {Date} refreshExpires
484 * @property {String[]=} scopes if scopes were reduced
487 * Redeem a refresh token to renew token codeId.
489 * @param {String} codeId
490 * @param {Date} refreshed
491 * @param {String[]} removeScopes
492 * @returns {Promise<RefreshedToken>}
494 async
refreshCode(dbCtx
, codeId
, refreshed
, removeScopes
) {
495 this._notImplemented('refreshCode', arguments
);
500 * Fetch a resource server record.
502 * @param {String} identifier uuid
503 * @returns {Promise<Resource>}
505 async
resourceGet(dbCtx
, resourceId
) {
506 this._notImplemented('resourceGet', arguments
);
511 * Create, or update description of, a resourceId.
513 * @param {String=} resourceId uuid
514 * @param {String=} secret
515 * @param {String=} description
516 * @returns {Promise<void>}
518 async
resourceUpsert(dbCtx
, resourceId
, secret
, description
) {
519 this._notImplemented('resourceUpsert', arguments
);
524 * Register a scope and its description.
526 * @param {String} scope
527 * @param {String} application
528 * @param {String} description
529 * @returns {Promise<void>}
531 async
scopeUpsert(dbCtx
, scope
, application
, description
, manuallyAdded
= false) {
532 this._notImplemented('scopeUpsert', arguments
);
537 * Remove a non-permanent scope if it is not currently in use.
539 * @param {String} scope
540 * @returns {Promise<Boolean>}
542 async
scopeDelete(dbCtx
, scope
) {
543 this._notImplemented('scopeDelete', arguments
);
548 * @typedef {Number|BigInt} CleanupResult
551 * @typedef {Object} CleanupResult
554 * Remove any non-permanent and non-manually-created scopes not currently in use.
556 * @param {Number} atLeastMsSinceLast skip cleanup if already executed this recently
557 * @returns {Promise<CleanupResult>}
559 async
scopeCleanup(dbCtx
, atLeastMsSinceLast
) {
560 this._notImplemented('scopeClean', arguments
);
565 * Forget tokens after they have expired, and redeemed codes after they have expired.
567 * @param {Number} codeLifespanSeconds
568 * @param {Number} atLeastMsSinceLast skip cleanup if already executed this recently
569 * @returns {Promise<CleanupResult>}
571 async
tokenCleanup(dbCtx
, codeLifespanSeconds
, atLeastMsSinceLast
) {
572 this._notImplemented('tokenCleanup', arguments
);
577 * Look up a redeemed token by code_id.
579 * @param {String} codeId
580 * @returns {Promise<Token>}
582 async
tokenGetByCodeId(dbCtx
, codeId
) {
583 this._notImplemented('tokenGetByCodeId', arguments
);
588 * Sets a redeemed token as revoked.
590 * @param {String} codeId - uuid
591 * @returns {Promise<void>}
593 async
tokenRevokeByCodeId(dbCtx
, codeId
) {
594 this._notImplemented('tokenRevokeByCodeId', arguments
);
599 * Revoke the refreshability of a codeId.
601 * @param {String} codeId - uuid
602 * @returns {Promise<void>}
604 async
tokenRefreshRevokeByCodeId(dbCtx
, codeId
) {
605 this._notImplemented('tokenRefreshRevokeByCodeId', arguments
);
610 * Get all tokens assigned to identifier.
612 * @param {String} identifier
613 * @returns {Promise<Tokens[]>}
615 async
tokensGetByIdentifier(dbCtx
, identifier
) {
616 this._notImplemented('tokensGetByIdentifier', arguments
);
620 /** @typedef {Object} RedeemedTicketData
621 * @property {String} subject
622 * @property {String} resource
623 * @property {String=} iss
624 * @property {String} ticket
625 * @property {String} token
628 * Persist details of a redeemed ticket.
630 * @param {RedeemedTicketData} redeemedData
631 * @returns {Promise<void>}
633 async
ticketRedeemed(dbCtx
, redeemedData
) {
634 this._notImplemented('ticketRedeemed', arguments
);
639 * Update details of a redeemed ticket that it has been published.
641 * @param {RedeemedTicketData} redeemedData
642 * @returns {Promise<void>}
644 async
ticketTokenPublished(dbCtx
, redeemedData
) {
645 this._notImplemented('ticketTokenPublished', arguments
);
649 * Retrieve redeemed tokens which have not yet been published to queue.
650 * @param {Number} limit
651 * @returns {Promise<RedeemedData[]>}
653 async
ticketTokenGetUnpublished(dbCtx
, limit
) {
654 this._notImplemented('ticketTokenGetUnpublished', arguments
);
659 module
.exports
= Database
;