Skip to content

Commit 0c2dbc6

Browse files
authored
feat: add revocation checker hook (#320)
* feat: add revocation checker hook * fix: tests * fix: failing tests
1 parent c8999a5 commit 0c2dbc6

13 files changed

+385
-92
lines changed

packages/interface/src/capability.ts

+28-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
IssuedInvocationView,
1313
UCANOptions,
1414
Verifier,
15+
Unit,
1516
} from './lib.js'
1617

1718
export interface Source {
@@ -350,6 +351,12 @@ export interface ProofResolver extends PrincipalOptions {
350351
resolve?: (proof: Link) => Await<Result<Delegation, UnavailableProof>>
351352
}
352353

354+
export interface RevocationChecker {
355+
validateAuthorization: (
356+
authorization: Authorization
357+
) => Await<Result<Unit, Revoked>>
358+
}
359+
353360
export interface Validator {
354361
/**
355362
* Validator must be provided a `Verifier` corresponding to local authority.
@@ -368,7 +375,8 @@ export interface ValidationOptions<
368375
Validator,
369376
PrincipalOptions,
370377
PrincipalResolver,
371-
ProofResolver {
378+
ProofResolver,
379+
RevocationChecker {
372380
capability: CapabilityParser<Match<C, any>>
373381
}
374382

@@ -377,7 +385,8 @@ export interface ClaimOptions
377385
Validator,
378386
PrincipalOptions,
379387
PrincipalResolver,
380-
ProofResolver {}
388+
ProofResolver,
389+
RevocationChecker {}
381390

382391
export interface DelegationError extends Failure {
383392
name: 'InvalidClaim'
@@ -425,6 +434,11 @@ export interface Expired extends Failure {
425434
readonly expiredAt: number
426435
}
427436

437+
export interface Revoked extends Failure {
438+
readonly name: 'Revoked'
439+
readonly delegation: Delegation
440+
}
441+
428442
export interface NotValidBefore extends Failure {
429443
readonly name: 'NotValidBefore'
430444
readonly delegation: Delegation
@@ -449,6 +463,7 @@ export interface SessionEscalation extends Failure {
449463
*/
450464
export type InvalidProof =
451465
| Expired
466+
| Revoked
452467
| NotValidBefore
453468
| InvalidSignature
454469
| InvalidAudience
@@ -465,6 +480,17 @@ export interface Unauthorized extends Failure {
465480
failedProofs: InvalidClaim[]
466481
}
467482

483+
export interface Authorization<
484+
Capability extends ParsedCapability = ParsedCapability
485+
> {
486+
delegation: Delegation
487+
capability: Capability
488+
489+
proofs: Authorization[]
490+
issuer: UCAN.Principal
491+
audience: UCAN.Principal
492+
}
493+
468494
export interface InvalidClaim extends Failure {
469495
issuer: UCAN.Principal
470496
name: 'InvalidClaim'

packages/interface/src/lib.ts

+3
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ import {
4242
DIDKeyResolutionError,
4343
ParsedCapability,
4444
CapabilityParser,
45+
Revoked,
4546
InferCapability,
47+
Authorization,
4648
} from './capability.js'
4749
import type * as Transport from './transport.js'
4850
import type { Tuple, Block } from './transport.js'
@@ -974,6 +976,7 @@ export interface ValidatorOptions {
974976

975977
readonly canIssue?: CanIssue['canIssue']
976978
readonly resolve?: InvocationContext['resolve']
979+
validateAuthorization: (proofs: Authorization) => Await<Result<Unit, Revoked>>
977980
}
978981

979982
export interface ServerOptions extends ValidatorOptions {

packages/server/src/handler.js

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const provideAdvanced =
6363
authority: options.id,
6464
capability,
6565
})
66+
6667
if (authorization.error) {
6768
return authorization
6869
} else {

packages/server/src/server.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ export { fail }
1717
* @param {API.Server<Service>} options
1818
* @returns {API.ServerView<Service>}
1919
*/
20-
export const create = options => new Server(options)
20+
export const create = options => {
21+
const server = new Server(options)
22+
return server
23+
}
2124

2225
/**
2326
* @template {Record<string, any>} S
@@ -33,6 +36,9 @@ class Server {
3336
this.service = service
3437
this.codec = codec
3538
this.catch = fail || (() => {})
39+
this.validateAuthorization = this.context.validateAuthorization.bind(
40+
this.context
41+
)
3642
}
3743
get id() {
3844
return this.context.id

packages/server/test/handler.spec.js

+44-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import { alice, bob, mallory, service } from './fixtures.js'
77
import { test, assert } from './test.js'
88
import * as Access from './service/access.js'
99
import { Verifier } from '@ucanto/principal/ed25519'
10-
import { Schema, UnavailableProof } from '@ucanto/validator'
10+
import {
11+
Schema,
12+
UnavailableProof,
13+
Unauthorized,
14+
Revoked,
15+
} from '@ucanto/validator'
1116
import { Absentee } from '@ucanto/principal'
1217
import { capability } from '../src/server.js'
1318
import { isLink, parseLink, fail } from '../src/lib.js'
@@ -31,6 +36,7 @@ const context = {
3136
resolve: link => ({
3237
error: new UnavailableProof(link),
3338
}),
39+
validateAuthorization: () => ({ ok: {} }),
3440
}
3541

3642
test('invocation', async () => {
@@ -113,6 +119,7 @@ test('checks service id', async () => {
113119
id: w3,
114120
service: { identity: Access },
115121
codec: CAR.inbound,
122+
validateAuthorization: () => ({ ok: {} }),
116123
})
117124

118125
const client = Client.connect({
@@ -186,6 +193,7 @@ test('checks for single capability invocation', async () => {
186193
id: w3,
187194
service: { identity: Access },
188195
codec: CAR.inbound,
196+
validateAuthorization: () => ({ ok: {} }),
189197
})
190198

191199
const client = Client.connect({
@@ -237,6 +245,7 @@ test('test access/claim provider', async () => {
237245
id: w3,
238246
service: { access: Access },
239247
codec: CAR.inbound,
248+
validateAuthorization: () => ({ ok: {} }),
240249
})
241250

242251
/**
@@ -305,6 +314,7 @@ test('handle did:mailto audiences', async () => {
305314
const result = await handler(request, {
306315
id: w3,
307316
principal: Verifier,
317+
validateAuthorization: () => ({ ok: {} }),
308318
})
309319

310320
assert.equal(result.error, undefined)
@@ -328,6 +338,7 @@ test('handle did:mailto audiences', async () => {
328338
const badAudience = await handler(badRequest, {
329339
id: w3,
330340
principal: Verifier,
341+
validateAuthorization: () => ({ ok: {} }),
331342
})
332343

333344
assert.containSubset(badAudience, {
@@ -661,6 +672,37 @@ test('fx.ok API', () => {
661672
)
662673
})
663674

675+
test('invocation fails if proof is revoked', async () => {
676+
const proof = await Client.delegate({
677+
issuer: w3,
678+
audience: alice,
679+
capabilities: [
680+
{
681+
can: 'identity/register',
682+
with: 'mailto:[email protected]',
683+
},
684+
],
685+
})
686+
687+
const invocation = await Client.delegate({
688+
issuer: alice,
689+
audience: w3,
690+
capabilities: proof.capabilities,
691+
proofs: [proof],
692+
})
693+
694+
const result = await Access.register(invocation, {
695+
...context,
696+
validateAuthorization: auth => {
697+
assert.deepEqual(auth.delegation.cid, invocation.cid)
698+
assert.deepEqual(auth.delegation.proofs, [proof])
699+
return { error: new Revoked(proof) }
700+
},
701+
})
702+
703+
assert.match(String(result.error), /Proof bafy.* has been revoked/)
704+
})
705+
664706
/**
665707
* @template {Record<string, any>} Service
666708
* @param {Service} service
@@ -670,6 +712,7 @@ const setup = service => {
670712
id: w3,
671713
service,
672714
codec: CAR.inbound,
715+
validateAuthorization: () => ({ ok: {} }),
673716
})
674717

675718
const consumer = Client.connect({

packages/server/test/server.spec.js

+7
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ test('encode delegated invocation', async () => {
7171
service: Service.create(),
7272
codec: CAR.inbound,
7373
id: w3,
74+
validateAuthorization: () => ({ ok: {} }),
7475
})
7576

7677
const connection = Client.connect({
@@ -184,6 +185,7 @@ test('unknown handler', async () => {
184185
id: w3,
185186
service: Service.create(),
186187
codec: CAR.inbound,
188+
validateAuthorization: () => ({ ok: {} }),
187189
})
188190

189191
const connection = Client.connect({
@@ -258,6 +260,7 @@ test('execution error', async () => {
258260
},
259261
codec: CAR.inbound,
260262
id: w3,
263+
validateAuthorization: () => ({ ok: {} }),
261264
})
262265

263266
const connection = Client.connect({
@@ -306,6 +309,7 @@ test('did:web server', async () => {
306309
service: Service.create(),
307310
codec: CAR.inbound,
308311
id: w3.withDID('did:web:web3.storage'),
312+
validateAuthorization: () => ({ ok: {} }),
309313
})
310314

311315
const connection = Client.connect({
@@ -346,6 +350,7 @@ test('unsupported content-type', async () => {
346350
'application/car': CAR.response,
347351
},
348352
}),
353+
validateAuthorization: () => ({ ok: {} }),
349354
})
350355

351356
const connection = Client.connect({
@@ -399,6 +404,7 @@ test('falsy errors are turned into {}', async () => {
399404
},
400405
id: w3.withDID('did:web:web3.storage'),
401406
codec: CAR.inbound,
407+
validateAuthorization: () => ({ ok: {} }),
402408
})
403409

404410
const connection = Client.connect({
@@ -426,6 +432,7 @@ test('run invocation without encode / decode', async () => {
426432
service: Service.create(),
427433
codec: CAR.inbound,
428434
id: w3,
435+
validateAuthorization: () => ({ ok: {} }),
429436
})
430437

431438
const identify = invoke({
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as API from '@ucanto/interface'
2+
3+
/**
4+
* @template {API.ParsedCapability} C
5+
* @implements {API.Authorization<C>}
6+
*/
7+
class Authorization {
8+
/**
9+
* @param {API.Match<C>} match
10+
* @param {API.Authorization<API.ParsedCapability>[]} proofs
11+
*/
12+
constructor(match, proofs) {
13+
this.match = match
14+
this.proofs = proofs
15+
}
16+
get capability() {
17+
return this.match.value
18+
}
19+
get delegation() {
20+
return this.match.source[0].delegation
21+
}
22+
get issuer() {
23+
return this.delegation.issuer
24+
}
25+
get audience() {
26+
return this.delegation.audience
27+
}
28+
}
29+
30+
/**
31+
* @template {API.ParsedCapability} C
32+
* @param {API.Match<C>} match
33+
* @param {API.Authorization<API.ParsedCapability>[]} proofs
34+
* @returns {API.Authorization<C>}
35+
*/
36+
export const create = (match, proofs = []) => new Authorization(match, proofs)
37+
38+
/**
39+
*
40+
* @param {API.Authorization} authorization
41+
* @returns {Iterable<API.UCANLink>}
42+
*/
43+
export const iterate = function* ({ delegation, proofs }) {
44+
yield delegation.cid
45+
for (const proof of proofs) {
46+
yield* iterate(proof)
47+
}
48+
}

0 commit comments

Comments
 (0)