UNPKG

@bedrock/zcap-storage

Version:
259 lines (230 loc) 8.53 kB
/*! * Copyright (c) 2019-2025 Digital Bazaar, Inc. All rights reserved. */ import * as bedrock from '@bedrock/core'; import * as database from '@bedrock/mongodb'; import assert from 'assert-plus'; import {LruCache} from '@digitalbazaar/lru-memoize'; const {util: {BedrockError}} = bedrock; let ZCAP_CACHE; const COLLECTION_NAME = 'zcap-storage-zcap'; bedrock.events.on('bedrock.init', async () => { const cfg = bedrock.config['zcap-storage']; let cacheConfig = cfg.caches.zcap; // coerce `maxSize` w/o `sizeCalculation` to `max` if(cacheConfig.maxSize !== undefined && cacheConfig.sizeCalculation === undefined) { cacheConfig = {...cacheConfig, max: cacheConfig.maxSize}; delete cacheConfig.maxSize; } // coerce `maxAge` to `ttl` in `cacheConfig` if(cacheConfig.maxAge !== undefined) { cacheConfig = {...cacheConfig, ttl: cacheConfig.maxAge}; delete cacheConfig.maxAge; } ZCAP_CACHE = new LruCache(cacheConfig); }); bedrock.events.on('bedrock-mongodb.ready', async () => { /* Note: The `zcap-storage-zcap` collection is used to store capabilities for its controller to later invoke. */ await database.openCollections([COLLECTION_NAME]); await database.createIndexes([{ // enable queries by controller and reference ID (reference IDs are // scoped to controllers) collection: COLLECTION_NAME, fields: {controller: 1, referenceId: 1}, options: {unique: true} }, { // enable queries by controller and id; as this is a unique index and IDs // are controlled by the zcap creator, we scope the index to the controller // to prevent entities from squatting on IDs collection: COLLECTION_NAME, fields: {controller: 1, id: 1}, options: {unique: true} }, { // enable queries by controller and invoker collection: COLLECTION_NAME, fields: {controller: 1, invoker: 1}, options: {unique: false} }]); }); /** * Inserts a received capability into storage for later invocation. * * @param {object} options - The options to use. * @param {string} options.controller - The ID of the entity storing the zcap. * @param {string} options.referenceId - An application specific ID for the * zcap, scoped to the controller. * @param {object} options.capability - The zcap to insert. * * @returns {Promise<object>} Resolves to the database record. */ export async function insert({controller, referenceId, capability} = {}) { assert.string(controller, 'controller'); assert.string(referenceId, 'referenceId'); assert.object(capability, 'capability'); assert.string(capability.id, 'capability.id'); assert.string(capability.controller, 'capability.controller'); // insert the capability and get the updated record const now = Date.now(); const meta = {created: now, updated: now, controller, referenceId}; const record = { id: database.hash(capability.id), controller: database.hash(controller), invoker: database.hash(capability.controller), referenceId: database.hash(referenceId), meta, capability }; try { const collection = database.collections[COLLECTION_NAME]; await collection.insertOne(record); return record; } catch(e) { if(!database.isDuplicateError(e)) { throw e; } throw new BedrockError( 'Duplicate authorization capability.', 'DuplicateError', { public: true, httpStatusCode: 409 }, e); } } /** * Gets a received capability from storage. In addition to the `controller` of * the capability, either `id` or `referenceId` must be given. * * @param {object} options - The options to use. * @param {string} options.controller - The ID of the entity that stored the * zcap. * @param {string} [options.id] - The ID of the zcap. * @param {string} [options.referenceId] - The application specific ID of the * zcap. * @param {boolean} [options.explain=false] - An optional explain boolean. * @param {boolean} [options.useCache=true] - `true` to allow use of a cache, * `false` not to. * * @returns {Promise<object | ExplainObject>} Resolves to `{capability, meta}` * or an ExplainObject if `explain=true`. */ export async function get({ controller, id, referenceId, explain = false, useCache = true } = {}) { assert.string(controller, 'controller'); assert.optionalString(id, 'id'); assert.optionalString(referenceId, 'referenceId'); if(!(id || referenceId)) { throw new TypeError('Either "id" or "referenceId" must be given.'); } if(explain || !useCache) { return _getUncachedZcapRecord({controller, id, referenceId, explain}); } // use cache // note: if the zcap has expired it will still be returned from the cache // until its cache max age is hit; zcaps should always be refreshed sooner // than the cache max age to ensure proper continued use const key = id ? JSON.stringify({controller, id}) : JSON.stringify({controller, referenceId}); const fn = () => _getUncachedZcapRecord({controller, id, referenceId}); return ZCAP_CACHE.memoize({key, fn}); } /** * Retrieves all capabilities from storage that match the given query. * * @param {object} options - The options to use. * @param {object} [options.query = {}] - The optional query to use. * @param {object} [options.options = {}] - Options (eg: 'sort', 'limit', * 'projection'). * @param {boolean} [options.explain=false] - An optional explain boolean. * * @returns {Promise<Array | ExplainObject>} Resolves to the records that * matched the query or an ExplainObject if `explain=true`. */ export async function find({ query = {}, options = {}, explain = false } = {}) { const collection = database.collections[COLLECTION_NAME]; const cursor = await collection.find(query, options); if(explain) { return cursor.explain('executionStats'); } return cursor.toArray(); } /** * Removes a capability from storage. * * @param {object} options - The options to use. * @param {string} options.controller - The ID of the entity that stored the * zcap. * @param {string} [options.id] - The ID of the zcap to remove. * @param {string} [options.referenceId] - The application specific ID of the * zcap to remove. * @param {boolean} [options.explain=false] - An optional explain boolean. * * @returns {Promise<boolean | ExplainObject>} Resolves to `true` if a zcap was * removed and `false` if not or an ExplainObject if `explain=true`. */ export async function remove({ controller, id, referenceId, explain = false } = {}) { assert.string(controller, 'controller'); assert.optionalString(id, 'id'); assert.optionalString(referenceId, 'referenceId'); if(!(id || referenceId)) { throw new TypeError('Either "id" or "referenceId" must be given.'); } const collection = database.collections[COLLECTION_NAME]; const query = {controller: database.hash(controller)}; if(id) { query.id = database.hash(id); } if(referenceId) { query.referenceId = database.hash(referenceId); } if(explain) { // 'find()' is used here because 'deleteMany()' doesn't return a // cursor which allows the use of the explain function. const cursor = await collection.find(query); return cursor.explain('executionStats'); } // clear cache values ZCAP_CACHE.delete(JSON.stringify({controller, id})); ZCAP_CACHE.delete(JSON.stringify({controller, referenceId})); const result = await collection.deleteMany(query); return result.deletedCount !== 0; } async function _getUncachedZcapRecord({ controller, id, referenceId, explain = false } = {}) { const collection = database.collections[COLLECTION_NAME]; const query = {controller: database.hash(controller)}; if(id) { query.id = database.hash(id); } if(referenceId) { query.referenceId = database.hash(referenceId); } const projection = {_id: 0, capability: 1, meta: 1}; if(explain) { // 'find().limit(1)' is used here because 'findOne()' doesn't return a // cursor which allows the use of the explain function. const cursor = await collection.find(query, {projection}).limit(1); return cursor.explain('executionStats'); } const record = await collection.findOne(query, {projection}); if(!record) { throw new BedrockError( 'Authorization capability not found.', 'NotFoundError', {controller, id, referenceId, httpStatusCode: 404, public: true}); } return record; } /** * An object containing information on the query plan. * * @typedef {object} ExplainObject */