Skip to content

Commit

Permalink
feat: define access/claim in @web3-storage/capabilities (#409)
Browse files Browse the repository at this point in the history
Motivation:
* #408

Unblocks:
* people importing a canonical CapabilityParser for `access/claim`
* adding advanced tests e.g. by [reverting this and finishing the
TODOs](fd0b56f)
* these involve `can=./update` abilities, so may make most sense to add
after #392 which defines
tools for them
  • Loading branch information
gobengo authored Feb 1, 2023
1 parent 9c8ca0b commit 4d72ba3
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 2 deletions.
3 changes: 2 additions & 1 deletion packages/capabilities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@
"rules": {
"unicorn/prefer-number-properties": "off",
"unicorn/prefer-export-from": "off",
"unicorn/no-array-reduce": "off"
"unicorn/no-array-reduce": "off",
"jsdoc/no-undefined-types": "error"
},
"env": {
"mocha": true
Expand Down
10 changes: 9 additions & 1 deletion packages/capabilities/src/access.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export const access = top.derive({
to: capability({
can: 'access/*',
with: URI.match({ protocol: 'did:' }),
derives: equalWith,
}),
derives: equalWith,
})
Expand Down Expand Up @@ -100,3 +99,12 @@ export const session = capability({
key: DID.match({ method: 'key' }),
},
})

export const claim = base.derive({
to: capability({
can: 'access/claim',
with: DID.match({ method: 'key' }).or(DID.match({ method: 'mailto' })),
derives: equalWith,
}),
derives: equalWith,
})
148 changes: 148 additions & 0 deletions packages/capabilities/test/capabilities/access.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { access } from '@ucanto/validator'
import { Verifier } from '@ucanto/principal/ed25519'
import * as Access from '../../src/access.js'
import { alice, bob, service, mallory } from '../helpers/fixtures.js'
import * as Ucanto from '@ucanto/interface'
import { delegate, invoke } from '@ucanto/core'

describe('access capabilities', function () {
it('should self issue', async function () {
Expand Down Expand Up @@ -222,4 +224,150 @@ describe('access capabilities', function () {
})
}, /Expected a did:mailto: but got "did:NOT_MAILTO:web3.storage:test" instead/)
})

describe('access/claim', () => {
// ensure we can use the capability to produce the invocations from the spec at https://github.com/web3-storage/specs/blob/576b988fb7cfa60049611963179277c420605842/w3-access.md
it('can create/access delegations from spec', async () => {
const audience = service.withDID('did:web:web3.storage')
/**
* @type {Array<(arg: { issuer: Ucanto.Signer<Ucanto.DID<'key'>>}) => Ucanto.IssuedInvocation<Ucanto.InferInvokedCapability<typeof Access.claim>>>}
*/
const examples = [
// https://github.com/web3-storage/specs/blob/576b988fb7cfa60049611963179277c420605842/w3-access.md#accessclaim
({ issuer }) => {
return Access.claim.invoke({
issuer,
audience,
with: issuer.did(),
})
},
]
for (const example of examples) {
const invocation = await example({ issuer: bob }).delegate()
const result = await access(invocation, {
capability: Access.claim,
principal: Verifier,
authority: audience,
})
assert.ok(
result.error !== true,
'result of access(invocation) is not an error'
)
assert.deepEqual(
result.audience.did(),
audience.did(),
'result audience did is expected value'
)
assert.equal(
result.capability.can,
'access/claim',
'result capability.can is access/claim'
)
assert.deepEqual(result.capability.nb, {}, 'result has empty nb')
}
})
it('can be derived', async () => {
/** @type {Array<Ucanto.Ability>} */
const cansThatShouldDeriveAccessClaim = ['*', 'access/*']
for (const can of cansThatShouldDeriveAccessClaim) {
const invocation = await invoke({
issuer: alice,
audience: service,
capability: {
can: 'access/claim',
with: bob.did(),
},
proofs: [
await delegate({
issuer: bob,
audience: alice,
capabilities: [
{
can,
with: bob.did(),
},
],
}),
],
}).delegate()
const result = await access(invocation, {
capability: Access.claim,
principal: Verifier,
authority: service,
})
assert.ok(
result.error !== true,
'result of access(invocation) is not an error'
)
}
})
it('cannot invoke when .with uses unexpected did method', async () => {
const issuer = bob.withDID('did:foo:bar')
assert.throws(
() =>
Access.claim.invoke({
issuer,
audience: service,
// @ts-ignore - expected complaint from compiler. We want to make sure there is an equivalent error at runtime
with: issuer.did(),
}),
`Invalid 'with'`
)
})
it('does not authorize invocations whose .with uses unexpected did methods', async () => {
const issuer = bob
const audience = service
const invocation = await delegate({
issuer,
audience,
capabilities: [
{
can: 'access/claim',
with: issuer.withDID('did:foo:bar').did(),
},
],
})
const result = await access(
// @ts-ignore - expected complaint from compiler. We want to make sure there is an equivalent error at runtime
invocation,
{
capability: Access.claim,
principal: Verifier,
authority: audience,
}
)
assert.ok(result.error, 'result of access(invocation) is an error')
assert.deepEqual(result.name, 'Unauthorized')
assert.ok(
result.delegationErrors.find((e) =>
e.message.includes('but got "did:foo:bar" instead')
),
'a result.delegationErrors message mentions invalid with value'
)
})
it('does not authorize invocations whose .with is not an issuer in proofs', async () => {
const issuer = bob
const audience = service
const invocation = await Access.claim
.invoke({
issuer,
audience,
// note: this did is not same as issuer.did() so issuer has no proof that they can use this resource
with: alice.did(),
})
.delegate()
const result = await access(invocation, {
capability: Access.claim,
principal: Verifier,
authority: audience,
})
assert.ok(result.error, 'result of access(invocation) is an error')
assert.deepEqual(result.name, 'Unauthorized')
assert.ok(
result.failedProofs.find((e) => {
return /Capability (.+) is not authorized/.test(e.message)
})
)
})
})
})

0 comments on commit 4d72ba3

Please sign in to comment.