@digitalbazaar/ezcap-express 4.2.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @digitalbazaar/ezcap-express Changelog
2
2
 
3
+ ## 4.5.0 - 2021-12-17
4
+
5
+ ### Fixed
6
+ - Add `_createGetRevocationRootController` wrapper around
7
+ `_getRevocationRootController` and pass `getRootController` to it.
8
+
9
+ ### Added
10
+ - Add tests for `authorizeZcapRevocation`.
11
+
12
+ ## 4.4.0 - 2021-12-15
13
+
14
+ ### Added
15
+ - Add additional tests.
16
+
17
+ ## 4.3.1 - 2021-12-13
18
+
19
+ ### Fixed
20
+ - Fix `expectedAction` to be `write` for `DELETE` method.
21
+ - Throw error when no `expectedAction` is given for a given HTTP method and
22
+ provide defaults for all common HTTP methods.
23
+
24
+ ## 4.3.0 - 2021-12-10
25
+
26
+ ### Added
27
+ - Allow any controller in a delegated zcap's chain to revoke it. This authority
28
+ is inherent in delegation and is now reflected in code. This feature gives
29
+ delegators more fine-grained control to revoke zcaps that they did not
30
+ delegate directly but one of their delegates did, allowing them to stop
31
+ specific zcap usage without having to revoke more of the chain. It also
32
+ gives zcap controllers the ability to revoke their own zcaps (if desired)
33
+ and adds a sanity check to prevent the revocation of root zcaps that use
34
+ the `urn:zcap:root:` ID scheme.
35
+
3
36
  ## 4.2.0 - 2021-08-26
4
37
 
5
38
  ### Added
package/lib/helpers.js CHANGED
@@ -10,6 +10,19 @@ import {verifyHeaderValue} from '@digitalbazaar/http-digest-header';
10
10
 
11
11
  const {ZCAP_CONTEXT_URL} = zCapConstants;
12
12
 
13
+ const DEFAULT_ACTION_FOR_METHOD = new Map([
14
+ ['GET', 'read'],
15
+ ['HEAD', 'read'],
16
+ ['OPTIONS', 'read'],
17
+ ['POST', 'write'],
18
+ ['PUT', 'write'],
19
+ ['PATCH', 'write'],
20
+ ['DELETE', 'write'],
21
+ ['CONNECT', 'write'],
22
+ ['TRACE', 'write'],
23
+ ['PATCH', 'write']
24
+ ]);
25
+
13
26
  export const ZCAP_ROOT_PREFIX = 'urn:zcap:root:';
14
27
 
15
28
  // middleware used to collect expected values for zcap authorization
@@ -87,16 +100,23 @@ export function createExpectationMiddleware({
87
100
 
88
101
  /* Note: This is safe as long as the server's request handling
89
102
  infrastructure differentiates based on HTTP method (as is typical practice
90
- with express/connect routing. So, while the client specifies the HTTP
103
+ with express/connect routing). So, while the client specifies the HTTP
91
104
  method, the server specifies the handler for that HTTP method. For example,
92
105
  this middleware will ensure that if a client specifies "POST" then it
93
- must be invoking a zcap that grants "write" action authority. Then, provided
94
- that the server's router ensures that only the "POST" handler will be
95
- executed (typical routing practice), all is well. If the handler code is
106
+ must be invoking a zcap that grants "write" action authority. Then,
107
+ provided that the server's router ensures that only the "POST" handler will
108
+ be executed (typical routing practice), all is well. If the handler code is
96
109
  chosen via some other means, e.g., via the request body, then the caller
97
110
  MUST provide the expected action and not rely on default behavior. */
98
111
  if(req.ezcap.expectedAction === undefined) {
99
- req.ezcap.expectedAction = req.method === 'POST' ? 'write' : 'read';
112
+ req.ezcap.expectedAction = DEFAULT_ACTION_FOR_METHOD.get(req.method);
113
+ if(req.ezcap.expectedAction === undefined) {
114
+ const error = new Error(
115
+ `The HTTP method ${req.method} has no expected capability action.`);
116
+ error.name = 'NotSupportedError';
117
+ error.httpStatusCode = 400;
118
+ return helpers.handleError({error, onError});
119
+ }
100
120
  }
101
121
 
102
122
  try {
package/lib/revoke.js CHANGED
@@ -48,32 +48,20 @@ export function authorizeZcapRevocation({
48
48
  expectedHost, expectedAction, getExpectedTarget,
49
49
  getExpectedRootCapabilityId, onError
50
50
  }),
51
- getDelegator({
51
+ verifyCapabilityDelegation({
52
52
  documentLoader, getRootController, suiteFactory, inspectCapabilityChain,
53
53
  onError
54
54
  }),
55
55
  authorizeZcapInvocationAfterParse({
56
- documentLoader, getRootController: _getRevocationRootController,
56
+ documentLoader,
57
+ getRootController: _createGetRevocationRootController(
58
+ {getRootController}),
57
59
  suiteFactory, allowTargetAttenuation, inspectCapabilityChain, onError
58
- }),
59
- function(req, res, next) {
60
- // ensure that the invoker of the write capability is the delegator
61
- // of the capability to be revoked
62
- const {zcapRevocation: {delegator}} = req;
63
- const invoker = req.zcap.controller || req.zcap.invoker;
64
- if(delegator !== invoker) {
65
- const error = new Error(
66
- 'Permission denied; the zcap invoker must be the delegator of the ' +
67
- 'zcap that is to be revoked.');
68
- error.name = 'NotAllowedError';
69
- return next(error);
70
- }
71
- next();
72
- }
60
+ })
73
61
  ];
74
62
  }
75
63
 
76
- function getDelegator({
64
+ function verifyCapabilityDelegation({
77
65
  documentLoader, getRootController, inspectCapabilityChain, suiteFactory,
78
66
  onError
79
67
  }) {
@@ -82,9 +70,18 @@ function getDelegator({
82
70
  expectedAction, expectedHost, expectedTarget, expectedRootCapability
83
71
  } = req.ezcap;
84
72
 
85
- // verify CapabilityDelegation
86
73
  const {body: capability} = req;
74
+
75
+ // early-disallow revocation of root zcaps that follow ID convention
76
+ if(capability.id.startsWith(helpers.ZCAP_ROOT_PREFIX)) {
77
+ const error = new Error('A root capability cannot be revoked.');
78
+ error.name = 'NotAllowedError';
79
+ return helpers.handleError({error, onError});
80
+ }
81
+
82
+ // verify CapabilityDelegation
87
83
  let delegator;
84
+ const chainControllers = [];
88
85
  try {
89
86
  const results = await _verifyDelegation({
90
87
  capability,
@@ -93,7 +90,10 @@ function getDelegator({
93
90
  getRootController
94
91
  }),
95
92
  expectedRootCapability,
96
- inspectCapabilityChain,
93
+ inspectCapabilityChain: _captureChainControllers({
94
+ inspectCapabilityChain,
95
+ chainControllers
96
+ }),
97
97
  suiteFactory
98
98
  });
99
99
  ({delegator} = results[0].purposeResult);
@@ -105,7 +105,7 @@ function getDelegator({
105
105
  return helpers.handleError({error, onError});
106
106
  }
107
107
 
108
- req.zcapRevocation = {delegator};
108
+ req.zcapRevocation = {delegator, chainControllers};
109
109
 
110
110
  // proceed to next middleware on next tick to prevent subsequent
111
111
  // middleware from potentially throwing here
@@ -133,50 +133,57 @@ async function _verifyDelegation({
133
133
  return results;
134
134
  }
135
135
 
136
- async function _getRevocationRootController({
137
- req, rootCapabilityId, rootInvocationTarget, getRootController,
138
- revocationsSubPath = '/revocations/'
136
+ function _createGetRevocationRootController({
137
+ getRootController, revocationsSubPath = '/revocations/'
139
138
  }) {
140
- // if `revocations` is not in the root invocation target, then defer to
141
- // `getRootController` to try and provide the root controller
142
- if(!rootInvocationTarget.includes(revocationsSubPath)) {
143
- return getRootController({req, rootCapabilityId, rootInvocationTarget});
144
- }
145
-
146
- /* Note: If the invocation target is a zcap-specific revocation endpoint,
147
- we use the delegator of the zcap as the root controller for the target.
148
-
149
- This approach allows any party that has delegated a zcap to be able to send
150
- it for revocation. Subsequent code (in the revocation route handler) will
151
- confirm that the delegation is proper and the zcap from which it was
152
- delegated has not itself been revoked.
139
+ return async function _getRevocationRootController({
140
+ req, rootCapabilityId, rootInvocationTarget
141
+ }) {
142
+ // if `revocations` is not in the root invocation target, then defer to
143
+ // `getRootController` to try and provide the root controller
144
+ if(!rootInvocationTarget.includes(revocationsSubPath)) {
145
+ return getRootController({req, rootCapabilityId, rootInvocationTarget});
146
+ }
153
147
 
154
- To be clear, if the delegation chain is:
148
+ /* Note: If the invocation target is a zcap-specific revocation endpoint,
149
+ we use all zcap controllers from the submitted zcap's chain as the root
150
+ controller value for the target.
151
+ This approach allows any party that has delegated a zcap or received one
152
+ to be able to send it for revocation. Subsequent code (in the revocation
153
+ route handler) will confirm that the delegation is proper and the zcap
154
+ from which it was delegated has not itself been revoked.
155
+ To be clear, if the delegation chain is:
156
+ root -> A -> B
157
+ Any zcap controller in the chain of B may invoke a root zcap with a
158
+ `target` of `<baseUrl>/revocations/<ID of B>` (and an ID of
159
+ `urn:zcap:root:encodeURIComponent(<baseUrl>/revocations/<ID of B>)`). This
160
+ means that `root`, `A`, or `B` may revoke `B`.
161
+ As long no other zcap in the chain of `B` (e.g., `A`) has already been
162
+ revoked, then `B` will be revoked and stored as a revocation until `B`
163
+ expires. */
155
164
 
156
- root -> A -> B
165
+ // use all `chainControllers`
166
+ // presumes `verifyCapabilityDelegation` middleware already called
167
+ return req.zcapRevocation.chainControllers;
168
+ };
169
+ }
157
170
 
158
- Then the delegator of B may invoke a root zcap: `urn:zcap:root:<ID of B>`
159
- with a target of `<baseUrl>/revocations/<ID of B>`. As long as `A` has not
160
- also been revoked, then `B` will be revoked and stored as a revocation until
161
- `B` expires. */
171
+ function _captureChainControllers({inspectCapabilityChain, chainControllers}) {
172
+ return async function _inspectCapabilityChain(chainDetails) {
173
+ // collect every controller in the chain
174
+ const {capabilityChain} = chainDetails;
175
+ for(const capability of capabilityChain.values()) {
176
+ chainControllers.push(..._getCapabilityControllers({capability}));
177
+ }
178
+ return inspectCapabilityChain(chainDetails);
179
+ };
180
+ }
162
181
 
163
- // ensure the `rootInvocationTarget` matches the request URL (i.e., that
164
- // the caller POSTed a zcap with an ID that matches up with the URL to
165
- // which they POSTed); this is not a security issue if this check is not
166
- // performed, however, it can help clients debug errors on their end
167
- const requestUrl = `${req.protocol}://${req.get('host')}${req.url}`;
168
- if(rootInvocationTarget !== requestUrl) {
169
- const error = new Error(
170
- 'The request URL does not match the root invocation target. Ensure ' +
171
- 'that the capability is sent to a URL that includes its ID.');
172
- error.name = 'URLMismatchError';
173
- error.details = {
174
- rootInvocationTarget,
175
- requestUrl
176
- };
177
- throw error;
182
+ function _getCapabilityControllers({capability}) {
183
+ const {controller, id} = capability;
184
+ const result = controller || id;
185
+ if(!result) {
186
+ return [];
178
187
  }
179
-
180
- // presumes `getDelegator` middleware already called
181
- return req.zcapRevocation.delegator;
188
+ return Array.isArray(result) ? result : [result];
182
189
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalbazaar/ezcap-express",
3
- "version": "4.2.0",
3
+ "version": "4.5.0",
4
4
  "main": "lib",
5
5
  "module": "main.js",
6
6
  "repository": {
@@ -16,7 +16,10 @@
16
16
  "generate-readme": "jsdoc2md -t readme-template.hbs lib/*.js > README.md",
17
17
  "lint": "eslint .",
18
18
  "test": "npm run test-node",
19
- "test-node": "cross-env NODE_ENV=test mocha -r esm --preserve-symlinks -t 30000 -A -R ${REPORTER:-spec} --require tests/test-mocha.js tests/*.spec.js"
19
+ "test-node": "cross-env NODE_ENV=test mocha -r esm --preserve-symlinks -t 30000 -A -R ${REPORTER:-spec} --require tests/test-mocha.js tests/*.spec.js",
20
+ "coverage": "cross-env NODE_ENV=test ESM_OPTIONS='{cache:false}' nyc --reporter=lcov --reporter=text-summary npm test",
21
+ "coverage-ci": "cross-env NODE_ENV=test ESM_OPTIONS='{cache:false}' nyc --reporter=lcovonly npm test",
22
+ "coverage-report": "nyc report"
20
23
  },
21
24
  "dependencies": {
22
25
  "@digitalbazaar/ed25519-signature-2020": "^3.0.0",
@@ -30,13 +33,23 @@
30
33
  "jsonld-signatures": "^9.3.0"
31
34
  },
32
35
  "devDependencies": {
36
+ "@digitalbazaar/did-method-key": "^2.0.0",
37
+ "@digitalbazaar/ezcap": "^1.0.0",
38
+ "@digitalbazaar/http-client": "^2.0.1",
39
+ "@digitalbazaar/security-document-loader": "^1.1.1",
40
+ "bnid": "^2.1.0",
33
41
  "chai": "^4.2.0",
42
+ "chai-http": "^4.3.0",
34
43
  "cross-env": "^7.0.2",
35
44
  "eslint": "^7.30.0",
36
45
  "eslint-config-digitalbazaar": "^2.6.1",
37
46
  "eslint-plugin-jsdoc": "^32.2.0",
47
+ "express": "^4.17.1",
48
+ "http-signature-zcap-invoke": "^3.1.0",
38
49
  "jsdoc-to-markdown": "^6.0.1",
39
- "mocha": "^8.1.3"
50
+ "mocha": "^8.1.3",
51
+ "nyc": "^15.1.0",
52
+ "zcap-context": "^1.2.1"
40
53
  },
41
54
  "engines": {
42
55
  "node": ">=14.0.0"