From 5bfe8423565853eec6a61819abdca5dff944001a Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Mon, 20 May 2024 16:26:33 -0400 Subject: [PATCH 1/3] Try to fix codecov upload. --- .github/workflows/main.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4c11a89..2b15df5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -70,3 +70,9 @@ jobs: run: | cd test npm run coverage-ci + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./test/coverage/lcov.info + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} From 8ef75e6a7570b1bb0b9c914167c1b4990eb57458 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Mon, 20 May 2024 16:32:27 -0400 Subject: [PATCH 2/3] Enable clients to provide local workflow IDs. --- CHANGELOG.md | 3 +++ lib/index.js | 55 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb173b3..1deba00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ credential request (i.e., including the `credential` param and any other optional params) as an optional alternative to returning only the value of the `credential` param for issuance. +- Allow clients to provide local workflow IDs as long as they meet + the local ID validation requirements. This is to enable clients to + ensure that they do not create duplicate workflows. ## 4.3.0 - 2023-12-11 diff --git a/lib/index.js b/lib/index.js index 9ec2b10..36bef7f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2022-2023 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2022-2024 Digital Bazaar, Inc. All rights reserved. */ import * as bedrock from '@bedrock/core'; import * as exchangerSchemas from '../schemas/bedrock-vc-exchanger.js'; @@ -7,11 +7,13 @@ import {createService, schemas} from '@bedrock/service-core'; import {addRoutes} from './http.js'; import {initializeServiceAgent} from '@bedrock/service-agent'; import {klona} from 'klona'; +import {parseLocalId} from './helpers.js'; import '@bedrock/express'; // load config defaults import './config.js'; +const routePrefix = '/exchangers'; const serviceType = 'vc-exchanger'; const {util: {BedrockError}} = bedrock; @@ -31,10 +33,14 @@ bedrock.events.on('bedrock.init', async () => { // schema.required.push('credentialTemplates'); } + // allow `id` property in `createConfigBody`, to be more rigorously validated + // below in `validateConfigFn` + createConfigBody.properties.id = updateConfigBody.properties.id; + // create `vc-exchanger` service const service = await createService({ serviceType, - routePrefix: '/exchangers', + routePrefix, storageCost: { config: 1, revocation: 1 @@ -84,8 +90,25 @@ async function usageAggregator({meter, signal, service} = {}) { return service.configStorage.getUsage({meterId, signal}); } -async function validateConfigFn({config} = {}) { +async function validateConfigFn({config, op} = {}) { try { + // validate any `id` in a new config + if(op === 'create' && config.id !== undefined) { + try { + _validateId({id: config.id}); + } catch(e) { + throw new BedrockError( + `Invalid client-provided configuration ID: ${e.message}.`, { + name: 'DataError', + details: { + httpStatusCode: 400, + public: true + }, + cause: e + }); + } + } + // if credential templates are specified, then `zcaps` MUST include at // least `issue` const {credentialTemplates = [], zcaps = {}} = config; @@ -118,3 +141,29 @@ async function validateConfigFn({config} = {}) { } return {valid: true}; } + +function _validateId({id} = {}) { + // format: / + + // ensure `id` starts with appropriate base URL + const {baseUri} = bedrock.config.server; + const base = `${baseUri}${routePrefix}/`; + if(id.startsWith(base)) { + // ensure `id` ends with appropriate local ID + const expectedLastSlashIndex = base.length - 1; + const idx = id.lastIndexOf('/'); + if(idx === expectedLastSlashIndex) { + return parseLocalId({id}); + } + } + + throw new BedrockError( + `Configuration "id" must start with "${base}" and end in a multibase, ` + + 'base58-encoded local identifier.', { + name: 'DataError', + details: { + httpStatusCode: 400, + public: true + } + }); +} From 09ad064d6cd0343070d58a0559b5854f590f4a9e Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Mon, 20 May 2024 16:45:57 -0400 Subject: [PATCH 3/3] Add tests for client-provided config IDs. --- test/mocha/10-provision.js | 73 +++++++++++++++++++++++++++++++++++++- test/mocha/helpers.js | 26 +++++++------- 2 files changed, 85 insertions(+), 14 deletions(-) diff --git a/test/mocha/10-provision.js b/test/mocha/10-provision.js index b4659ec..4a0a523 100644 --- a/test/mocha/10-provision.js +++ b/test/mocha/10-provision.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2019-2024 Digital Bazaar, Inc. All rights reserved. */ import * as bedrock from '@bedrock/core'; import * as helpers from './helpers.js'; @@ -54,6 +54,28 @@ describe('provision', () => { const {id: capabilityAgentId} = capabilityAgent; result.controller.should.equal(capabilityAgentId); }); + it('creates a config with a client-chosen ID', async () => { + let err; + let result; + try { + const localId = await helpers.generateRandom(); + result = await helpers.createExchangerConfig({ + capabilityAgent, configOptions: { + id: `${mockData.baseUrl}/exchangers/${localId}` + } + }); + } catch(e) { + err = e; + } + assertNoError(err); + should.exist(result); + result.should.have.keys([ + 'controller', 'id', 'sequence', 'meterId' + ]); + result.sequence.should.equal(0); + const {id: capabilityAgentId} = capabilityAgent; + result.controller.should.equal(capabilityAgentId); + }); it('creates a config with only an issue zcap', async () => { let err; let result; @@ -147,6 +169,55 @@ describe('provision', () => { 'A capability to issue credentials is required when credential ' + 'templates are provided.'); }); + it('throws if duplicate client-chosen ID is used', async () => { + let err; + let result; + try { + // create config (should pass) + result = await helpers.createExchangerConfig({ + capabilityAgent, configOptions: { + id: `${mockData.baseUrl}/exchangers/z1A183gxYRXYFUnHUXsS7KVmA` + } + }); + should.exist(result); + // try to create duplicate (should throw) + result = undefined; + result = await helpers.createExchangerConfig({ + capabilityAgent, configOptions: { + id: `${mockData.baseUrl}/exchangers/z1A183gxYRXYFUnHUXsS7KVmA` + } + }); + } catch(e) { + err = e; + } + should.exist(err); + should.not.exist(result); + should.exist(err.data); + err.status.should.equal(409); + err.data.name.should.equal('DuplicateError'); + err.data.message.should.contain('Duplicate configuration'); + }); + it('throws if invalid client-chosen ID is used', async () => { + let err; + let result; + try { + result = await helpers.createExchangerConfig({ + capabilityAgent, configOptions: { + id: `${mockData.baseUrl}/exchangers/foo` + } + }); + } catch(e) { + err = e; + } + should.exist(err); + should.not.exist(result); + should.exist(err.data); + err.status.should.equal(400); + err.data.name.should.equal('DataError'); + err.data.message.should.contain('Configuration validation failed'); + err.data.details.cause.message.should.contain( + 'Invalid client-provided configuration ID'); + }); it('creates a config including proper ipAllowList', async () => { const ipAllowList = ['127.0.0.1/32', '::1/128']; diff --git a/test/mocha/helpers.js b/test/mocha/helpers.js index a4f5c1e..f8d74c3 100644 --- a/test/mocha/helpers.js +++ b/test/mocha/helpers.js @@ -102,7 +102,7 @@ export async function createCredentialOffer({ }; if(preAuthorized) { - exchange.openId.preAuthorizedCode = await _generateRandom(); + exchange.openId.preAuthorizedCode = await generateRandom(); const grant = {'pre-authorized_code': exchange.openId.preAuthorizedCode}; offer.grants['urn:ietf:params:oauth:grant-type:pre-authorized_code'] = grant; @@ -172,10 +172,10 @@ export async function createConfig({ export async function createExchangerConfig({ capabilityAgent, ipAllowList, meterId, zcaps, credentialTemplates, - steps, initialStep, oauth2 = false + steps, initialStep, oauth2 = false, + configOptions = {credentialTemplates, steps, initialStep} } = {}) { const url = `${mockData.baseUrl}/exchangers`; - const configOptions = {credentialTemplates, steps, initialStep}; return createConfig({ serviceType: 'vc-exchanger', url, capabilityAgent, ipAllowList, meterId, zcaps, configOptions, oauth2 @@ -462,6 +462,16 @@ export async function delegate({ }); } +export function generateRandom() { + // 128-bit random number, base58 multibase + multihash encoded + return generateId({ + bitLength: 128, + encoding: 'base58', + multibase: true, + multihash: true + }); +} + export async function getCredentialStatus({verifiableCredential}) { // get SLC for the VC const {credentialStatus} = verifiableCredential; @@ -712,13 +722,3 @@ async function keyResolver({id}) { const {data} = await httpClient.get(id, {agent: httpsAgent}); return data; } - -function _generateRandom() { - // 128-bit random number, base58 multibase + multihash encoded - return generateId({ - bitLength: 128, - encoding: 'base58', - multibase: true, - multihash: true - }); -}