@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 +33 -0
- package/lib/helpers.js +25 -5
- package/lib/revoke.js +68 -61
- package/package.json +16 -3
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,
|
|
94
|
-
that the server's router ensures that only the "POST" handler will
|
|
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
|
|
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
|
-
|
|
51
|
+
verifyCapabilityDelegation({
|
|
52
52
|
documentLoader, getRootController, suiteFactory, inspectCapabilityChain,
|
|
53
53
|
onError
|
|
54
54
|
}),
|
|
55
55
|
authorizeZcapInvocationAfterParse({
|
|
56
|
-
documentLoader,
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
revocationsSubPath = '/revocations/'
|
|
136
|
+
function _createGetRevocationRootController({
|
|
137
|
+
getRootController, revocationsSubPath = '/revocations/'
|
|
139
138
|
}) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
+
// use all `chainControllers`
|
|
166
|
+
// presumes `verifyCapabilityDelegation` middleware already called
|
|
167
|
+
return req.zcapRevocation.chainControllers;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
157
170
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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.
|
|
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"
|