@digitalbazaar/ezcap-express 4.2.0 → 4.3.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,17 @@
1
1
  # @digitalbazaar/ezcap-express Changelog
2
2
 
3
+ ## 4.3.0 - 2021-12-10
4
+
5
+ ### Added
6
+ - Allow any controller in a delegated zcap's chain to revoke it. This authority
7
+ is inherent in delegation and is now reflected in code. This feature gives
8
+ delegators more fine-grained control to revoke zcaps that they did not
9
+ delegate directly but one of their delegates did, allowing them to stop
10
+ specific zcap usage without having to revoke more of the chain. It also
11
+ gives zcap controllers the ability to revoke their own zcaps (if desired)
12
+ and adds a sanity check to prevent the revocation of root zcaps that use
13
+ the `urn:zcap:root:` ID scheme.
14
+
3
15
  ## 4.2.0 - 2021-08-26
4
16
 
5
17
  ### Added
package/lib/revoke.js CHANGED
@@ -48,32 +48,18 @@ 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
56
  documentLoader, getRootController: _getRevocationRootController,
57
57
  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
- }
58
+ })
73
59
  ];
74
60
  }
75
61
 
76
- function getDelegator({
62
+ function verifyCapabilityDelegation({
77
63
  documentLoader, getRootController, inspectCapabilityChain, suiteFactory,
78
64
  onError
79
65
  }) {
@@ -82,9 +68,18 @@ function getDelegator({
82
68
  expectedAction, expectedHost, expectedTarget, expectedRootCapability
83
69
  } = req.ezcap;
84
70
 
85
- // verify CapabilityDelegation
86
71
  const {body: capability} = req;
72
+
73
+ // early-disallow revocation of root zcaps that follow ID convention
74
+ if(capability.id.startsWith(helpers.ZCAP_ROOT_PREFIX)) {
75
+ const error = new Error('A root capability cannot be revoked.');
76
+ error.name = 'NotAllowedError';
77
+ return helpers.handleError({error, onError});
78
+ }
79
+
80
+ // verify CapabilityDelegation
87
81
  let delegator;
82
+ const chainControllers = [];
88
83
  try {
89
84
  const results = await _verifyDelegation({
90
85
  capability,
@@ -93,7 +88,10 @@ function getDelegator({
93
88
  getRootController
94
89
  }),
95
90
  expectedRootCapability,
96
- inspectCapabilityChain,
91
+ inspectCapabilityChain: _captureChainControllers({
92
+ inspectCapabilityChain,
93
+ chainControllers
94
+ }),
97
95
  suiteFactory
98
96
  });
99
97
  ({delegator} = results[0].purposeResult);
@@ -105,7 +103,7 @@ function getDelegator({
105
103
  return helpers.handleError({error, onError});
106
104
  }
107
105
 
108
- req.zcapRevocation = {delegator};
106
+ req.zcapRevocation = {delegator, chainControllers};
109
107
 
110
108
  // proceed to next middleware on next tick to prevent subsequent
111
109
  // middleware from potentially throwing here
@@ -144,39 +142,48 @@ async function _getRevocationRootController({
144
142
  }
145
143
 
146
144
  /* 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.
145
+ we use all zcap controllers from the submitted zcap's chain as the root
146
+ controller value for the target.
148
147
 
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.
148
+ This approach allows any party that has delegated a zcap or received one
149
+ to be able to send it for revocation. Subsequent code (in the revocation
150
+ route handler) will confirm that the delegation is proper and the zcap from
151
+ which it was delegated has not itself been revoked.
153
152
 
154
153
  To be clear, if the delegation chain is:
155
154
 
156
155
  root -> A -> B
157
156
 
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. */
162
-
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;
178
- }
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
+
162
+ As long no other zcap in the chain of `B` (e.g., `A`) has already been
163
+ revoked, then `B` will be revoked and stored as a revocation until `B`
164
+ expires. */
165
+
166
+ // use all `chainControllers`
167
+ // presumes `verifyCapabilityDelegation` middleware already called
168
+ return req.zcapRevocation.chainControllers;
169
+ }
179
170
 
180
- // presumes `getDelegator` middleware already called
181
- return req.zcapRevocation.delegator;
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
+ }
181
+
182
+ function _getCapabilityControllers({capability}) {
183
+ const {controller, id} = capability;
184
+ const result = controller || id;
185
+ if(!result) {
186
+ return [];
187
+ }
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.3.0",
4
4
  "main": "lib",
5
5
  "module": "main.js",
6
6
  "repository": {