bedrock-zcap-storage
Version:
Bedrock Authorization Capability Storage
200 lines (181 loc) • 6.49 kB
JavaScript
/*!
* Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved.
*/
import * as bedrock from 'bedrock';
import * as database from 'bedrock-mongodb';
import assert from 'assert-plus';
const {util: {BedrockError}} = bedrock;
/**
* An object containing information on the query plan.
*
* @typedef {object} ExplainObject
*/
/**
* Inserts an authorization into storage.
*
* @param {object} options - The options to use.
* @param {string} options.controller - The ID of the entity storing the
* authorization and that controls its potential removal from storage.
* @param {object} options.capability - The capability to insert.
*
* @returns {Promise<object>} Resolves to the database record.
*/
export async function insert({controller, capability} = {}) {
assert.string(controller, 'controller');
assert.object(capability, 'capability');
assert.string(capability.id, 'capability.id');
let {invocationTarget} = capability;
if(invocationTarget && typeof invocationTarget === 'object' &&
typeof invocationTarget.id === 'string') {
invocationTarget = invocationTarget.id;
}
if(typeof invocationTarget !== 'string') {
throw new TypeError(
'"capability.invocationTarget" must be a string or an object with ' +
'where "capability.invocationTarget.id" is a string.');
}
if(!invocationTarget.includes(':')) {
throw new Error(
'The ID of the capability\'s invocation target must be an absolute URI.');
}
// insert the capability and get the updated record
const now = Date.now();
const meta = {created: now, updated: now};
const record = {
id: database.hash(capability.id),
invocationTarget: database.hash(invocationTarget),
controller: database.hash(controller),
meta,
authorization: {controller, capability}
};
try {
const collection = database.collections['zcap-storage-authorization'];
const result = await collection.insertOne(
record, database.writeOptions);
return result.ops[0];
} catch(e) {
if(!database.isDuplicateError(e)) {
throw e;
}
throw new BedrockError(
'Duplicate authorization capability.',
'DuplicateError', {
public: true,
httpStatusCode: 409
}, e);
}
}
/**
* Gets a authorization from storage. In addition to the `id` of the
* capability, either `controller` or `invocationTarget` must be given.
*
* @param {object} options - The options to use.
* @param {string} options.id - The ID of the capability.
* @param {string} [options.controller] - The ID of the entity that stored the
* authorization.
* @param {string|Array} [options.invocationTarget] - The `invocationTarget`
* for the capability.
* @param {boolean} [options.explain=false] - An optional explain boolean.
*
* @returns {Promise<object | ExplainObject>} Resolves to `{capability, meta}`
* or an ExplainObject if `explain=true`.
*/
export async function get({
id, controller, invocationTarget, explain = false
} = {}) {
assert.string(id, 'id');
assert.optionalString(controller, 'controller');
if(invocationTarget !== undefined) {
if(Array.isArray(invocationTarget)) {
assert.optionalArrayOfString(invocationTarget, 'invocationTarget');
} else {
assert.optionalString(invocationTarget, 'invocationTarget');
}
}
if(!(controller || invocationTarget)) {
throw new TypeError(
'Either "controller" or "invocationTarget" must be given.');
}
const collection = database.collections['zcap-storage-authorization'];
const query = {
id: database.hash(id)
};
if(controller) {
query.controller = database.hash(controller);
}
if(invocationTarget) {
if(Array.isArray(invocationTarget)) {
query.invocationTarget = {
$in: invocationTarget.map(database.hash)
};
} else {
query.invocationTarget = database.hash(invocationTarget);
}
}
const projection = {_id: 0, authorization: 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',
{id, invocationTarget, controller, httpStatusCode: 404, public: true});
}
return record;
}
/**
* Retrieves all authorizations 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['zcap-storage-authorization'];
if(explain) {
const cursor = await collection.find(query, options);
return cursor.explain('executionStats');
}
return collection.find(query, options).toArray();
}
/**
* Removes an authorization from storage.
*
* @param {object} options - The options to use.
* @param {string} options.controller - The ID of the entity that stored the
* authorization.
* @param {string} options.id - The 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, explain = false} = {}) {
assert.string(controller, 'controller');
assert.string(id, 'id');
const collection = database.collections['zcap-storage-authorization'];
const query = {
controller: database.hash(controller),
id: database.hash(id)
};
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');
}
const result = await collection.deleteMany(query);
return result.result.n !== 0;
}