26867e45f35ca8b50b0c15729e7421f9c5ad1648
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 * Query the current schema version.
43 * This is a standalone query function, as it is called before statements are loaded.
44 * @returns {Object} version
45 * @returns {Number} version.major
46 * @returns {Number} version.minor
47 * @returns {Number} version.patch
49 async
_currentSchema() {
50 this._notImplemented('_currentSchema', arguments
);
55 * Perform db connection health-check, if applicable.
56 * Throw something if a database situation should pull us out of a load-balancer.
59 this._notImplemented('healthCheck', arguments
);
64 * Wrap a function call in a database context.
65 * @param {Function} fn fn(ctx)
68 this._notImplemented('context', arguments
);
72 * Wrap a function call in a transaction context.
74 * @param {Function} fn fn(txCtx)
76 async
transaction(dbCtx
, fn
) {
77 this._notImplemented('transaction', arguments
);
99 static _isInfinites(x
) {
100 return typeof(x
) === 'number'
101 && Math
.abs(x
) === Infinity
;
105 * Basic type checking of object properties.
106 * @param {Object} object
107 * @param {String[]} properties
108 * @param {String[]} types
110 _ensureTypes(object
, properties
, types
) {
111 const _scope
= _fileScope('_ensureTypes');
113 if (!(object
&& properties
&& types
)) {
114 this.logger
.error(_scope
, 'undefined argument', { object
, properties
, types
});
115 throw new DatabaseErrors
.DataValidation();
117 properties
.forEach((p
) => {
118 // eslint-disable-next-line security/detect-object-injection
119 const pObj
= object
[p
];
120 const pType
= typeof pObj
;
121 if (!types
.includes(pType
)
122 && !(types
.includes('array') && Array
.isArray(pObj
))
123 && !(types
.includes('buffer') && pObj
instanceof Buffer
)
124 && !(types
.includes('date') && pObj
instanceof Date
)
125 && !(types
.includes('infinites'))
126 && !(types
.includes('null') && pObj
=== null)
127 && !(types
.includes('number') && pType
=== 'bigint')
128 && !(types
.includes('uuid') && Database
._isUUID(pObj
))) {
129 const reason
= `'${p}' is '${pType}', but must be ${types.length > 1 ? 'one of ' : ''}'${types}'`;
130 this.logger
.error(_scope
, reason
, {});
131 throw new DatabaseErrors
.DataValidation(reason
);
138 * @typedef {Object} Authentication
139 * @property {String} identifier
140 * @property {String=} credential
141 * @property {Date} created
142 * @property {Date=} lastAuthenticated
145 * @param {Authentication} authentication
147 _validateAuthentication(authentication
) {
149 [['identifier'], ['string']],
150 [['credential'], ['string', 'null']],
151 [['created'], ['date']],
152 [['lastAuthenticated'], ['date', 'infinites']],
153 ].forEach(([properties
, types
]) => this._ensureTypes(authentication
, properties
, types
));
158 * @typedef {Object} Resource
159 * @property {String} resourceId - uuid
160 * @property {String} secret
161 * @property {String} description
162 * @property {Date} created
165 * @param {Resource} resource
167 _validateResource(resource
) {
169 [['resourceId', 'secret', 'description'], ['string']],
170 [['resourceId'], ['uuid']],
171 [['created'], ['date']],
172 ].forEach(([properties
, types
]) => this._ensureTypes(resource
, properties
, types
));
177 * @typedef {Object} Token
178 * @property {String} codeId - uuid
179 * @property {String} profile
180 * @property {Date} created
181 * @property {Date=} expires
182 * @property {Date=} refreshExpires
183 * @property {Date=} refreshed
184 * @property {*=} duration
185 * @property {*=} refreshDuration
186 * @property {Number|BigInt=} refresh_count
187 * @property {Boolean} is_revoked
188 * @property {Boolean} is_token
189 * @property {String} client_id
190 * @property {String[]} scopes
191 * @property {Object=} profileData
194 * @param {Token} token
196 _validateToken(token
) {
198 [['codeId', 'profile', 'clientId'], ['string']],
199 [['codeId'], ['uuid']],
200 [['created'], ['date']],
201 [['expires', 'refreshExpires', 'refreshed'], ['date', 'null']],
202 [['isToken', 'isRevoked'], ['boolean']],
203 [['scopes'], ['array']],
204 [['profileData'], ['object', 'null']],
205 ].forEach(([properties
, types
]) => this._ensureTypes(token
, properties
, types
));
206 this._ensureTypes(token
.scopes
, Object
.keys(token
.scopes
), ['string']);
211 * Interface methods need implementations. Ensure the db-interaction
212 * methods on the base class call this, so they may be overridden by
213 * implementation classes.
214 * @param {String} method
215 * @param {arguments} args
217 _notImplemented(method
, args
) {
218 this.logger
.error(_fileScope(method
), 'abstract method called', Array
.from(args
));
219 throw new DatabaseErrors
.NotImplemented(method
);
224 * Get all the almanac entries.
227 async
almanacGetAll(dbCtx
) {
228 this._notImplemented('almanacGetAll', arguments
);
233 * Fetch the authentication record for an identifier.
235 * @param {String} identifier
236 * @returns {Promise<Authentication>}
238 async
authenticationGet(dbCtx
, identifier
) {
239 this._notImplemented('authenticationGet', arguments
);
244 * Update the authentication record for the identifier that
245 * correct credentials have been supplied.
247 * @param {String} identifier
248 * @returns {Promise<void>}
250 async
authenticationSuccess(dbCtx
, identifier
) {
251 this._notImplemented('authenticationSuccess', arguments
);
256 * Insert or update the credential for an identifier.
258 * @param {String} identifier
259 * @param {String} credential
260 * @returns {Promise<void>}
262 async
authenticationUpsert(dbCtx
, identifier
, credential
) {
263 this._notImplemented('authenticationUpsert', arguments
);
268 * Determine if profile url is known to this service.
270 * @param {String} profile
271 * @returns {Promise<Boolean>}
273 async
profileIsValid(dbCtx
, profile
) {
274 this._notImplemented('profileGet', arguments
);
279 * Insert a new relationship between a profile endpoint and
280 * an authenticated identifier.
282 * @param {String} profile
283 * @param {String} identifier
284 * @returns {Promise<void>}
286 async
profileIdentifierInsert(dbCtx
, profile
, identifier
) {
287 this._notImplemented('profileIdentifierInsert', arguments
);
292 * Adds a scope to be available for a profile to include on any authorization request.
294 * @param {String} profile
295 * @param {String} scope
296 * @returns {Promise<void>}
298 async
profileScopeInsert(dbCtx
, profile
, scope
) {
299 this._notImplemented('profileScopeInsert', arguments
);
304 * @typedef {Object} ScopeDetails
305 * @property {String} description
306 * @property {String[]=} profiles
309 * @typedef {Object.<String, Object>} ProfileScopes
310 * @property {Object.<String, Object>} profile
311 * @property {Object.<String, ScopeDetails>} profile.scope
314 * @typedef {Object.<String, Object>} ScopeIndex
315 * @property {ScopeDetails} scope
318 * @typedef {Object} ProfilesScopesReturn
319 * @property {ProfileScopes} profileScopes
320 * @property {ScopeIndex} scopeIndex
321 * @property {String[]} profiles
324 * Returns an object containing:
325 * - an object with profiles as keys to objects with scopes as keys to scope objects,
326 * which each contain a description of the scope and a list of profiles offering it
327 * - an object with scopes as keys to the same scope objects
328 * - a list of profiles
330 * @param {String} identifier
331 * @returns {Promise<ProfileScopesReturn>}
333 async
profilesScopesByIdentifier(dbCtx
, identifier
) {
334 this._notImplemented('profilesScopesByIdentifier', arguments
);
339 * @typedef ProfileScopesRow
342 * @property description
343 * @property application
344 * @property isPermanent
345 * @property isManuallyAdded
348 * Convert db row data into associative structures.
349 * Same behavior is shared by multiple engines.
350 * @param {ProfileScopesRow[]} profileScopesRows
351 * @returns {ProfileScopesReturn}
353 static _profilesScopesBuilder(profileScopesRows
) {
354 const scopeIndex
= {};
355 const profileScopes
= {};
356 const profileSet
= new Set();
358 (profileScopesRows
|| []).forEach(({ profile
, scope
, description
, application
, isPermanent
, isManuallyAdded
}) => {
359 if (scope
&& !(scope
in scopeIndex
)) {
360 scopeIndex
[scope
] = { // eslint-disable-line security/detect-object-injection
369 profileSet
.add(profile
);
370 if (!(profile
in profileScopes
)) {
371 profileScopes
[profile
] = {}; // eslint-disable-line security/detect-object-injection
374 if (profile
&& scope
) {
375 scopeIndex
[scope
].profiles
.push(profile
); // eslint-disable-line security/detect-object-injection
376 profileScopes
[profile
][scope
] = scopeIndex
[scope
]; // eslint-disable-line security/detect-object-injection
381 profiles: [...profileSet
],
389 * Sets list of additional scopes available to profile.
391 * @param {String} profile
392 * @param {String[]} scopes
393 * @returns {Promise<void>}
395 async
profileScopesSetAll(dbCtx
, profile
, scopes
) {
396 this._notImplemented('profileScopesSetAll', arguments
);
401 * Create (or revoke a duplicate) code as a token entry.
403 * @param {Object} data
404 * @param {String} data.codeId
405 * @param {Date} data.created
406 * @param {Boolean} data.isToken
407 * @param {String} data.clientId
408 * @param {String} data.profile - profile uri
409 * @param {String} data.identifier
410 * @param {String[]} data.scopes
411 * @param {Number|Null} data.lifespanSeconds - null sets expiration to Infinity
412 * @param {Number|Null} data.refreshLifespanSeconds - null sets refresh to none
413 * @param {String|Null} data.resource
414 * @param {Object|Null} data.profileData - profile data from profile uri
415 * @returns {Promise<Boolean>} whether redemption was successful
417 async
redeemCode(dbCtx
, { codeId
, created
, isToken
, clientId
, profile
, identifier
, scopes
, lifespanSeconds
, refreshLifespanSeconds
, profileData
} = {}) {
418 this._notImplemented('redeemCode', arguments
);
423 * @typedef {Object} RefreshedToken
424 * @property {Date} expires
425 * @property {Date} refreshExpires
426 * @property {String[]=} scopes if scopes were reduced
429 * Redeem a refresh token to renew token codeId.
431 * @param {String} codeId
432 * @param {Date} refreshed
433 * @param {String[]} removeScopes
434 * @returns {Promise<RefreshedToken>}
436 async
refreshCode(dbCtx
, codeId
, refreshed
, removeScopes
) {
437 this._notImplemented('refreshCode', arguments
);
442 * Fetch a resource server record.
444 * @param {String} identifier uuid
445 * @returns {Promise<Resource>}
447 async
resourceGet(dbCtx
, resourceId
) {
448 this._notImplemented('resourceGet', arguments
);
453 * Create, or update description of, a resourceId.
455 * @param {String=} resourceId uuid
456 * @param {String=} secret
457 * @param {String=} description
458 * @returns {Promise<void>}
460 async
resourceUpsert(dbCtx
, resourceId
, secret
, description
) {
461 this._notImplemented('resourceUpsert', arguments
);
466 * Register a scope and its description.
468 * @param {String} scope
469 * @param {String} application
470 * @param {String} description
471 * @returns {Promise<void>}
473 async
scopeUpsert(dbCtx
, scope
, application
, description
, manuallyAdded
= false) {
474 this._notImplemented('scopeUpsert', arguments
);
479 * Remove a non-permanent scope if it is not currently in use.
481 * @param {String} scope
482 * @returns {Promise<Boolean>}
484 async
scopeDelete(dbCtx
, scope
) {
485 this._notImplemented('scopeDelete', arguments
);
490 * @typedef {Number|BigInt} CleanupResult
493 * @typedef {Object} CleanupResult
496 * Remove any non-permanent and non-manually-created scopes not currently in use.
498 * @param {Number} atLeastMsSinceLast skip cleanup if already executed this recently
499 * @returns {Promise<CleanupResult>}
501 async
scopeCleanup(dbCtx
, atLeastMsSinceLast
) {
502 this._notImplemented('scopeClean', arguments
);
507 * Forget tokens after they have expired, and redeemed codes after they have expired.
509 * @param {Number} codeLifespanSeconds
510 * @param {Number} atLeastMsSinceLast skip cleanup if already executed this recently
511 * @returns {Promise<CleanupResult>}
513 async
tokenCleanup(dbCtx
, codeLifespanSeconds
, atLeastMsSinceLast
) {
514 this._notImplemented('tokenCleanup', arguments
);
519 * Look up a redeemed token by code_id.
521 * @param {String} codeId
522 * @returns {Promise<Token>}
524 async
tokenGetByCodeId(dbCtx
, codeId
) {
525 this._notImplemented('tokenGetByCodeId', arguments
);
530 * Sets a redeemed token as revoked.
532 * @param {String} codeId - uuid
533 * @returns {Promise<void>}
535 async
tokenRevokeByCodeId(dbCtx
, codeId
) {
536 this._notImplemented('tokenRevokeByCodeId', arguments
);
541 * Revoke the refreshability of a codeId.
543 * @param {String} codeId - uuid
544 * @returns {Promise<void>}
546 async
tokenRefreshRevokeByCodeId(dbCtx
, codeId
) {
547 this._notImplemented('tokenRefreshRevokeByCodeId', arguments
);
552 * Get all tokens assigned to identifier.
554 * @param {String} identifier
555 * @returns {Promise<Tokens[]>}
557 async
tokensGetByIdentifier(dbCtx
, identifier
) {
558 this._notImplemented('tokensGetByIdentifier', arguments
);
563 module
.exports
= Database
;