Skip to content

Commit 2b24511

Browse files
committed
Fix zcap policy schema to allow maxDelegationTtl.
1 parent 1457dd7 commit 2b24511

File tree

5 files changed

+150
-13
lines changed

5 files changed

+150
-13
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# bedrock-profile-http ChangeLog
22

3+
## 26.2.1 - 2025-11-dd
4+
5+
### Fixed
6+
- Fix zcap policy schema to allow `maxDelegationTtl`.
7+
38
## 26.2.0 - 2025-11-18
49

510
### Added

lib/refreshedZcapCache.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@ export async function getRefreshedZcap({profileId, capability}) {
2323
return REFRESHED_ZCAP_CACHE.memoize({key, fn});
2424
}
2525

26-
export async function getRefreshZcapPolicy({profileId, delegateId}) {
26+
function _createCacheKey({profileId, capability}) {
27+
const json = JSON.stringify({
28+
profileId, canonicalZcap: canonicalize(capability)
29+
});
30+
const hash = createHash('sha256').update(json, 'utf8').digest('base64url');
31+
return hash;
32+
}
33+
34+
async function _getZcapPolicy({profileId, delegateId}) {
2735
try {
2836
return brZcapStorage.policies.get({
2937
controller: profileId, delegate: delegateId
@@ -42,18 +50,10 @@ export async function getRefreshZcapPolicy({profileId, delegateId}) {
4250
}
4351
}
4452

45-
function _createCacheKey({profileId, capability}) {
46-
const json = JSON.stringify({
47-
profileId, canonicalZcap: canonicalize(capability)
48-
});
49-
const hash = createHash('sha256').update(json, 'utf8').digest('base64url');
50-
return hash;
51-
}
52-
5353
async function _getUncached({profileId, capability}) {
5454
// get the policy for the profile + controller (delegate)
5555
const {controller: delegateId} = capability;
56-
const policy = await getRefreshZcapPolicy({profileId, delegateId});
56+
const {policy} = await _getZcapPolicy({profileId, delegateId});
5757

5858
// check policy constraints
5959
const {authorizeZcapInvocationOptions} = bedrock.config['profile-http'];

lib/zcaps.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import * as bedrock from '@bedrock/core';
55
import * as brZcapStorage from '@bedrock/zcap-storage';
66
import * as middleware from './middleware.js';
77
import * as schemas from '../schemas/bedrock-profile-http.js';
8-
import {getRefreshedZcap, getRefreshZcapPolicy} from './refreshedZcapCache.js';
98
import {asyncHandler} from '@bedrock/express';
109
import cors from 'cors';
10+
import {getRefreshedZcap} from './refreshedZcapCache.js';
1111
import {createValidateMiddleware as validate} from '@bedrock/validation';
1212

1313
const {util: {BedrockError}} = bedrock;
@@ -124,7 +124,9 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
124124
middleware.authorizeProfileZcapRequest(),
125125
asyncHandler(async (req, res) => {
126126
const {profileId, delegateId} = req.params;
127-
const {policy} = await getRefreshZcapPolicy({profileId, delegateId});
127+
const {policy} = await brZcapStorage.policies.get({
128+
controller: profileId, delegate: delegateId
129+
});
128130
res.json({policy});
129131
}));
130132

@@ -161,7 +163,9 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
161163
middleware.authorizeProfileZcapRequest(),
162164
asyncHandler(async (req, res) => {
163165
const {profileId, delegateId} = req.params;
164-
const {policy} = await getRefreshZcapPolicy({profileId, delegateId});
166+
const {policy} = await brZcapStorage.policies.get({
167+
controller: profileId, delegate: delegateId
168+
});
165169
// return only `refresh=false` or `refresh.constraints` to client
166170
const viewablePolicy = {};
167171
const {refresh} = policy;

schemas/bedrock-profile-http.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ const zcapPolicy = {
322322
type: 'object',
323323
additionalProperties: false,
324324
properties: {
325+
maxDelegationTtl: {
326+
type: 'number'
327+
},
325328
maxTtlBeforeRefresh: {
326329
type: 'number'
327330
}

test/mocha/50-zcap-refresh.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,131 @@ describe('zcap refresh', () => {
527527
should.not.exist(result.results[0].error);
528528
should.not.exist(result.results[0].error);
529529

530+
// refresh zcap expiry should be more than 360 days from now
531+
const days360 = Date.now() + 1000 * 60 * 60 * 24 * 360;
532+
result.results.forEach(r => {
533+
const expires = (new Date(r.capability.expires)).getTime();
534+
expires.should.be.gte(days360);
535+
});
536+
537+
// set expected after
538+
expectedAfter = result.refresh.after;
539+
540+
// update record
541+
await mockData.refreshingService.configStorage.update({
542+
config: {...result.config, sequence: result.config.sequence + 1},
543+
refresh: {
544+
enabled: result.refresh.enabled,
545+
after: result.refresh.after
546+
}
547+
});
548+
resolve(mockData.refreshingService.configStorage.get({id: configId}));
549+
} catch(e) {
550+
reject(e);
551+
}
552+
}));
553+
554+
let err;
555+
let result;
556+
let zcaps;
557+
try {
558+
const {id: meterId} = await helpers.createMeter({
559+
controller: profileId, serviceType: 'refreshing'
560+
});
561+
zcaps = await _createZcaps({
562+
profileId, zcapClient, serviceAgent
563+
});
564+
result = await helpers.createConfig({
565+
profileId, zcapClient, meterId, servicePath: '/refreshables',
566+
options: {
567+
id: configId,
568+
zcaps
569+
}
570+
});
571+
} catch(e) {
572+
err = e;
573+
}
574+
assertNoError(err);
575+
should.exist(result);
576+
result.should.have.keys([
577+
'controller', 'id', 'sequence', 'meterId', 'zcaps'
578+
]);
579+
result.sequence.should.equal(0);
580+
result.controller.should.equal(profileId);
581+
582+
// wait for refresh promise to resolve
583+
const record = await configRefreshPromise;
584+
record.config.id.should.equal(configId);
585+
record.config.sequence.should.equal(1);
586+
record.meta.refresh.enabled.should.equal(true);
587+
record.meta.refresh.after.should.equal(expectedAfter);
588+
589+
// ensure zcaps changed
590+
for(const [key, value] of Object.entries(zcaps)) {
591+
record.config.zcaps[key].should.not.deep.equal(value);
592+
}
593+
});
594+
it('should refresh zcaps w/1 day max delegation TTL', async () => {
595+
// remove any existing policy
596+
await zcapClient.request({
597+
url: urls.policy,
598+
capability: rootZcap,
599+
method: 'delete',
600+
action: 'write'
601+
});
602+
603+
// add constrained policy
604+
await zcapClient.write({
605+
url: urls.policies,
606+
capability: rootZcap,
607+
json: {
608+
policy: {
609+
sequence: 0,
610+
controller: profileId,
611+
delegate: serviceAgent.id,
612+
refresh: {
613+
constraints: {
614+
maxDelegationTtl: 1000 * 60 * 60 * 24
615+
}
616+
}
617+
}
618+
}
619+
});
620+
621+
// function to be called when refreshing the created config
622+
let expectedAfter;
623+
const configId = `${mockData.baseUrl}/refreshables/${crypto.randomUUID()}`;
624+
const configRefreshPromise = new Promise((resolve, reject) =>
625+
mockData.refreshHandlerListeners.set(configId, async ({
626+
record, signal
627+
}) => {
628+
try {
629+
const now = Date.now();
630+
const later = now + 1000 * 60 * 5;
631+
const result = await refreshZcaps({
632+
serviceType: 'refreshing', config: record.config, signal
633+
});
634+
result.refresh.enabled.should.equal(true);
635+
should.exist(result.config);
636+
result.refresh.after.should.be.gte(later);
637+
should.exist(result.results);
638+
result.results.length.should.equal(4);
639+
result.results[0].refreshed.should.equal(true);
640+
result.results[1].refreshed.should.equal(true);
641+
result.results[2].refreshed.should.equal(true);
642+
result.results[3].refreshed.should.equal(true);
643+
should.not.exist(result.results[0].error);
644+
should.not.exist(result.results[0].error);
645+
should.not.exist(result.results[0].error);
646+
should.not.exist(result.results[0].error);
647+
648+
// refresh zcap expiry should be less than 2 days from now
649+
const twoDaysFromNow = Date.now() + 1000 * 60 * 60 * 24 * 2;
650+
result.results.forEach(r => {
651+
const expires = (new Date(r.capability.expires)).getTime();
652+
expires.should.be.lte(twoDaysFromNow);
653+
});
654+
530655
// set expected after
531656
expectedAfter = result.refresh.after;
532657

0 commit comments

Comments
 (0)