diff --git a/packages/interface/src/capability.ts b/packages/interface/src/capability.ts index 833e85cd..d9a655ad 100644 --- a/packages/interface/src/capability.ts +++ b/packages/interface/src/capability.ts @@ -298,17 +298,7 @@ export interface PrincipalOptions { principal: PrincipalParser } -export interface IssuingOptions { - /** - * You can provide default set of capabilities per did, which is used to - * validate whether claim is satisfied by `{ with: my:*, can: "*" }`. If - * not provided resolves to `[]`. - */ - - my?: (issuer: DID) => Capability[] -} - -export interface ProofResolver extends PrincipalOptions, IssuingOptions { +export interface ProofResolver extends PrincipalOptions { /** * You can provide a proof resolver that validator will call when UCAN * links to external proof. If resolver is not provided validator may not @@ -318,8 +308,7 @@ export interface ProofResolver extends PrincipalOptions, IssuingOptions { } export interface ValidationOptions - extends CanIssue, - IssuingOptions, + extends Partial, PrincipalOptions, ProofResolver { capability: CapabilityParser> diff --git a/packages/validator/src/lib.js b/packages/validator/src/lib.js index ba06a6eb..3d08c936 100644 --- a/packages/validator/src/lib.js +++ b/packages/validator/src/lib.js @@ -18,8 +18,6 @@ export { capability } from './capability.js' export * from './schema.js' export * as Schema from './schema.js' -const empty = () => [] - /** * @param {UCAN.Link} proof */ @@ -115,7 +113,7 @@ const resolveSources = async ({ delegation }, config) => { } else { // otherwise create source objects for it's capabilities, so we could // track which proof in which capability the are from. - for (const capability of iterateCapabilities(proof, config)) { + for (const capability of proof.capabilities) { sources.push({ capability, delegation: proof, @@ -130,6 +128,12 @@ const resolveSources = async ({ delegation }, config) => { return { sources, errors } } +/** + * @param {API.ParsedCapability} capability + * @param {API.DID} issuer + */ +const isSelfIssued = (capability, issuer) => capability.with === issuer + /** * @template {API.Ability} A * @template {API.URI} R @@ -141,9 +145,9 @@ const resolveSources = async ({ delegation }, config) => { */ export const access = async ( invocation, - { canIssue, principal, my = empty, resolve = unavailable, capability } + { canIssue = isSelfIssued, principal, resolve = unavailable, capability } ) => { - const config = { canIssue, my, resolve, principal, capability } + const config = { canIssue, resolve, principal, capability } const claim = capability.match({ capability: invocation.capabilities[0], @@ -334,59 +338,6 @@ class Unauthorized extends Failure { return { error, name, message, cause, stack } } } -const ALL = '*' - -/** - * @param {API.Delegation} delegation - * @param {Required} options - */ -function* iterateCapabilities({ issuer, capabilities }, { my }) { - const did = issuer.did() - for (const capability of capabilities) { - const uri = parseMyURI(capability.with, did) || parseAsURI(capability.with) - const { can } = capability - - if (uri) { - for (const capability of my(uri.did)) { - if ( - capability.with.startsWith(uri.protocol) && - (can === ALL || capability.can === can) - ) { - yield capability - } - } - } else { - yield capability - } - } -} - -const AS_PATTERN = /as:(.*):(.*)/ -const MY = /my:(.*)/ - -/** - * @param {string} uri - * @returns {{did:API.DID, protocol:string}|null} - */ -const parseAsURI = uri => { - const [, did, kind] = AS_PATTERN.exec(uri) || [] - return did != null && kind != null - ? { - did: /** @type {API.DID} */ (did), - protocol: kind === ALL ? '' : `${kind}:`, - } - : null -} - -/** - * @param {string} uri - * @param {API.DID} did - */ - -const parseMyURI = (uri, did) => { - const [, kind] = MY.exec(uri) || [] - return kind != null ? { did, protocol: kind === ALL ? '' : `${kind}:` } : null -} /** * @template {API.Delegation} T diff --git a/packages/validator/test/lib.spec.js b/packages/validator/test/lib.spec.js index 0ea54ae0..87f72676 100644 --- a/packages/validator/test/lib.spec.js +++ b/packages/validator/test/lib.spec.js @@ -8,13 +8,12 @@ import * as Client from '@ucanto/client' import { alice, bob, mallory, service as w3 } from './fixtures.js' import { UCAN, DID as Principal } from '@ucanto/core' import { UnavailableProof } from '../src/error.js' -import * as API from './types.js' const storeAdd = capability({ can: 'store/add', with: URI.match({ protocol: 'did:' }), nb: { - link: Link.match().optional(), + link: Link.optional(), }, derives: (claimed, delegated) => { if (claimed.with !== delegated.with) { @@ -35,7 +34,7 @@ const storeAdd = capability({ } }, }) -test('self-issued invocation', async () => { +test('authorize self-issued invocation', async () => { const invocation = await Client.delegate({ issuer: alice, audience: bob, @@ -50,9 +49,6 @@ test('self-issued invocation', async () => { const result = await access(invocation, { capability: storeAdd, principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, }) assert.containSubset(result, { @@ -67,7 +63,7 @@ test('self-issued invocation', async () => { }) }) -test('expired invocation', async () => { +test('unauthorized / expired invocation', async () => { const expiration = UCAN.now() - 5 const invocation = await Client.delegate({ issuer: alice, @@ -84,9 +80,6 @@ test('expired invocation', async () => { const result = await access(invocation, { capability: storeAdd, principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, }) assert.containSubset(result, { @@ -118,7 +111,7 @@ test('expired invocation', async () => { ) }) -test('not vaid before invocation', async () => { +test('unauthorized / not vaid before invocation', async () => { const notBefore = UCAN.now() + 500 const invocation = await Client.delegate({ issuer: alice, @@ -135,9 +128,6 @@ test('not vaid before invocation', async () => { const result = await access(invocation, { capability: storeAdd, principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, }) assert.containSubset(result, { @@ -151,7 +141,7 @@ test('not vaid before invocation', async () => { }) }) -test('invalid signature', async () => { +test('unauthorized / invalid signature', async () => { const invocation = await Client.delegate({ issuer: alice, audience: w3, @@ -168,9 +158,6 @@ test('invalid signature', async () => { const result = await access(invocation, { capability: storeAdd, principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, }) assert.containSubset(result, { @@ -185,7 +172,7 @@ test('invalid signature', async () => { }) }) -test('unknown capability', async () => { +test('unauthorized / unknown capability', async () => { const invocation = await Client.delegate({ issuer: alice, audience: w3, @@ -202,9 +189,6 @@ test('unknown capability', async () => { // @ts-ignore capability: storeAdd, principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, }) assert.containSubset(result, { @@ -217,7 +201,7 @@ test('unknown capability', async () => { }) }) -test('delegated invocation', async () => { +test('authorize / delegated invocation', async () => { const delegation = await Client.delegate({ issuer: alice, audience: bob, @@ -244,9 +228,6 @@ test('delegated invocation', async () => { const result = await access(invocation, { capability: storeAdd, principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, }) assert.containSubset(result, { @@ -272,6 +253,71 @@ test('delegated invocation', async () => { }) }) +test('authorize / delegation chain', async () => { + const aliceToBob = await Client.delegate({ + issuer: alice, + audience: bob, + capabilities: [ + { + can: 'store/add', + with: alice.did(), + }, + ], + }) + + const bobToMallory = await Client.delegate({ + issuer: bob, + audience: mallory, + capabilities: aliceToBob.capabilities, + proofs: [aliceToBob], + }) + + const invocation = await Client.delegate({ + issuer: mallory, + audience: w3, + capabilities: aliceToBob.capabilities, + proofs: [bobToMallory], + }) + + const result = await access(invocation, { + capability: storeAdd, + principal: ed25519.Verifier, + }) + + assert.containSubset(result, { + capability: { + can: 'store/add', + with: alice.did(), + nb: {}, + }, + issuer: Principal.parse(mallory.did()), + audience: Principal.parse(w3.did()), + proofs: [ + { + capability: { + can: 'store/add', + with: alice.did(), + nb: {}, + }, + issuer: Principal.parse(bob.did()), + audience: Principal.parse(mallory.did()), + proofs: [ + { + capability: { + can: 'store/add', + with: alice.did(), + nb: {}, + }, + issuer: Principal.parse(alice.did()), + audience: Principal.parse(bob.did()), + proofs: [], + }, + ], + }, + ], + }) +}) + test('invalid claim / no proofs', async () => { const invocation = await Client.delegate({ issuer: alice, @@ -286,9 +332,6 @@ test('invalid claim / no proofs', async () => { const result = await access(invocation, { principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, capability: storeAdd, }) @@ -331,9 +374,6 @@ test('invalid claim / expired', async () => { const result = await access(invocation, { principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, capability: storeAdd, }) @@ -378,9 +418,6 @@ test('invalid claim / not valid before', async () => { const result = await access(invocation, { principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, capability: storeAdd, }) @@ -425,9 +462,6 @@ test('invalid claim / invalid signature', async () => { const result = await access(invocation, { principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, capability: storeAdd, }) @@ -475,9 +509,6 @@ test('invalid claim / unknown capability', async () => { const result = await access(invocation, { principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, capability: storeAdd, }) @@ -521,9 +552,6 @@ test('invalid claim / malformed capability', async () => { const result = await access(invocation, { principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, capability: storeAdd, }) @@ -566,9 +594,6 @@ test('invalid claim / unavailable proof', async () => { const result = await access(invocation, { principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, capability: storeAdd, }) @@ -610,9 +635,6 @@ test('invalid claim / failed to resolve', async () => { const result = await access(invocation, { principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, resolve() { throw new Error('Boom!') }, @@ -658,9 +680,6 @@ test('invalid claim / invalid audience', async () => { const result = await access(invocation, { principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, capability: storeAdd, }) @@ -702,9 +721,6 @@ test('invalid claim / invalid claim', async () => { const result = await access(invocation, { principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, capability: storeAdd, }) @@ -758,9 +774,6 @@ test('invalid claim / invalid sub delegation', async () => { const result = await access(invocation, { principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, capability: storeAdd, }) @@ -779,314 +792,7 @@ test('invalid claim / invalid sub delegation', async () => { }) }) -test('delegate with my:*', async () => { - const delegation = await Client.delegate({ - issuer: alice, - audience: bob, - capabilities: [ - { - can: '*', - with: 'my:*', - }, - ], - }) - - const invocation = await Client.delegate({ - issuer: bob, - audience: w3, - capabilities: [ - { - can: 'store/add', - with: alice.did(), - }, - ], - proofs: [delegation], - }) - - const result = await access(invocation, { - principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, - my: issuer => { - return [ - { - can: 'store/add', - with: issuer, - }, - ] - }, - capability: storeAdd, - }) - - assert.containSubset(result, { - capability: { - can: 'store/add', - with: alice.did(), - }, - issuer: Principal.parse(bob.did()), - audience: Principal.parse(w3.did()), - proofs: [ - { - delegation, - issuer: Principal.parse(alice.did()), - audience: Principal.parse(bob.did()), - capability: { - can: 'store/add', - with: alice.did(), - }, - proofs: [], - }, - ], - }) - - assert.containSubset( - await access(invocation, { - principal: ed25519.Verifier, - canIssue: (claim, issuer) => claim.with === issuer, - capability: storeAdd, - }), - { - error: true, - } - ) -}) - -test('delegate with my:did', async () => { - const delegation = await Client.delegate({ - issuer: alice, - audience: bob, - capabilities: [ - { - can: '*', - with: 'my:did', - }, - ], - }) - - const invocation = await Client.delegate({ - issuer: bob, - audience: w3, - capabilities: [ - { - can: 'store/add', - with: alice.did(), - }, - ], - proofs: [delegation], - }) - - const result = await access(invocation, { - principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, - my: issuer => { - return [ - { - can: 'store/add', - with: issuer, - }, - ] - }, - capability: storeAdd, - }) - - assert.containSubset(result, { - capability: { - can: 'store/add', - with: alice.did(), - }, - issuer: Principal.parse(bob.did()), - audience: Principal.parse(w3.did()), - proofs: [ - { - delegation, - issuer: Principal.parse(alice.did()), - audience: Principal.parse(bob.did()), - capability: { - can: 'store/add', - with: alice.did(), - }, - proofs: [], - }, - ], - }) -}) - -test('delegate with as:*', async () => { - const my = await Client.delegate({ - issuer: alice, - audience: bob, - capabilities: [ - { - can: '*', - with: 'my:*', - }, - ], - }) - - const as = await Client.delegate({ - issuer: bob, - audience: mallory, - capabilities: [ - { - can: '*', - with: /** @type {API.UCAN.Resource} */ (`as:${alice.did()}:*`), - }, - ], - proofs: [my], - }) - - const invocation = await Client.delegate({ - issuer: mallory, - audience: w3, - capabilities: [ - { - can: 'store/add', - with: alice.did(), - }, - ], - proofs: [as], - }) - - const result = await access(invocation, { - principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, - my: issuer => { - return [ - { - can: 'store/add', - with: issuer, - }, - ] - }, - capability: storeAdd, - }) - - assert.containSubset(result, { - capability: { - can: 'store/add', - with: alice.did(), - }, - proofs: [ - { - delegation: as, - issuer: Principal.parse(bob.did()), - audience: Principal.parse(mallory.did()), - capability: { - can: 'store/add', - with: alice.did(), - }, - proofs: [ - { - delegation: my, - issuer: Principal.parse(alice.did()), - audience: Principal.parse(bob.did()), - capability: { - can: 'store/add', - with: alice.did(), - }, - }, - ], - }, - ], - }) -}) - -test('delegate with as:did', async () => { - const mailto = capability({ - can: 'msg/send', - with: URI.match({ protocol: 'mailto:' }), - }) - - const my = await Client.delegate({ - issuer: alice, - audience: bob, - capabilities: [ - { - can: '*', - with: 'my:*', - }, - ], - }) - - const as = await Client.delegate({ - issuer: bob, - audience: mallory, - capabilities: [ - { - can: 'msg/send', - with: /** @type {API.UCAN.Resource} */ (`as:${alice.did()}:mailto`), - }, - ], - proofs: [my], - }) - - const invocation = await Client.delegate({ - issuer: mallory, - audience: w3, - capabilities: [ - { - can: 'msg/send', - with: 'mailto:alice@web.mail', - }, - ], - proofs: [as], - }) - - const result = await access(invocation, { - principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return ( - claim.with === issuer || - (issuer === alice.did() && claim.can === 'msg/send') - ) - }, - my: issuer => { - return [ - { - can: 'msg/send', - with: 'mailto:alice@web.mail', - }, - ] - }, - capability: mailto, - }) - - assert.containSubset(result, { - capability: { - can: 'msg/send', - with: 'mailto:alice@web.mail', - }, - proofs: [ - { - delegation: as, - issuer: Principal.parse(bob.did()), - audience: Principal.parse(mallory.did()), - capability: { - can: 'msg/send', - with: 'mailto:alice@web.mail', - }, - proofs: [ - { - delegation: my, - issuer: Principal.parse(alice.did()), - audience: Principal.parse(bob.did()), - capability: { - can: 'msg/send', - with: 'mailto:alice@web.mail', - }, - }, - ], - }, - ], - }) -}) - -test('resolve proof', async () => { +test('authorize / resolve external proof', async () => { const delegation = await Client.delegate({ issuer: alice, audience: bob, @@ -1112,9 +818,6 @@ test('resolve proof', async () => { const result = await access(invocation, { principal: ed25519.Verifier, - canIssue: (claim, issuer) => { - return claim.with === issuer - }, resolve: async link => { if (link.toString() === delegation.cid.toString()) { return delegation