Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: #418 #410

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/capabilities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
"access": [
"dist/src/access"
],
"provider": [
"dist/src/provider"
],
"utils": [
"dist/src/utils"
],
Expand Down
66 changes: 66 additions & 0 deletions packages/capabilities/src/consumer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Provider Capabilities
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it could be helpful to others grokking this to link to the spec it's trying to implement?

*
* These can be imported directly with:
* ```js
* import * as Consumer from '@web3-storage/capabilities/consumer'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* ```
*
* @module
*/
import { capability, DID, URI, Link } from '@ucanto/validator'
// @ts-ignore
// eslint-disable-next-line no-unused-vars
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hopefully not needed now that this is in main

import * as Types from '@ucanto/interface'
import { equalWith, fail, equal, equalCID } from './utils.js'
import { top } from './top.js'

export { top }

/**
* Capability can only be delegated (but not invoked) allowing audience to
* derived any `consumer/` prefixed capability for the agent identified
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* derived any `consumer/` prefixed capability for the agent identified
* derive any `consumer/` prefixed capability for the agent identified

* by did:key in the `with` field.
*/
export const consumer = top.derive({
to: capability({
can: 'consumer/*',
with: DID,
derives: equalWith,
}),
derives: equalWith,
})

const base = top.or(consumer)

/**
* Capability can be invoked by an agent to request a `consumer/add` for an account.
*/
export const add = base.derive({
to: capability({
can: 'consumer/add',
/**
* Must be an provider DID
*/
with: DID,
nb: {
/**
* CID for the provider/get invocation
*/
request: Link,
/**
* Support specific space DIDs or undefined to request a provider for multiple spaces
*/
consumer: URI.match({ protocol: 'did:' }).optional(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
consumer: URI.match({ protocol: 'did:' }).optional(),
consumer: DID.match({ method: 'key' }).optional(),

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and maybe add an .or(...) for the literal did:* (which is not a DID due to *, but afaict is a valid URI)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but basically the point is - we can use the schemas to never allow ambiguous values like did:baz*

},
derives: (child, parent) => {
return (
fail(equalWith(child, parent)) ||
fail(equalCID(child.nb.request, parent.nb.request, 'request')) ||
fail(equal(child.nb.consumer, parent.nb.consumer, 'consumer')) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like to accomodate this from spec, this check might need to adjust to authorize deriving from nb.consumer: did:* (or consumer undefined, which spec says is equivalent) to nb.consumer: did:key:xyz

true
)
},
}),
derives: equalWith,
})
83 changes: 83 additions & 0 deletions packages/capabilities/src/provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Provider Capabilities
*
* These can be imported directly with:
* ```js
* import * as Provider from '@web3-storage/capabilities/provider'
* ```
*
* @module
*/
import { capability, DID, URI } from '@ucanto/validator'
// @ts-ignore
// eslint-disable-next-line no-unused-vars
import * as Types from '@ucanto/interface'
import { equalWith, fail, equal } from './utils.js'
import { top } from './top.js'

export { top }

/**
* Capability can only be delegated (but not invoked) allowing audience to
* derived any `provider/` prefixed capability for the agent identified
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* derived any `provider/` prefixed capability for the agent identified
* derive any `provider/` prefixed capability for the agent identified

* by did:key in the `with` field.
*/
export const provider = top.derive({
to: capability({
can: 'provider/*',
with: DID,
derives: equalWith,
}),
derives: equalWith,
})

const base = top.or(provider)

/**
* Capability can be invoked by an agent to request a `consumer/add` for a space.
*/
export const get = base.derive({
to: capability({
can: 'provider/get',
with: DID,
nb: {
provider: DID,
/**
* Support specific space DIDs or undefined to request a provider for multiple spaces
*/
consumer: URI.match({ protocol: 'did:' }).optional(),
},
derives: (child, parent) => {
return (
fail(equalWith(child, parent)) ||
fail(equal(child.nb.provider, parent.nb.provider, 'provider')) ||
fail(equal(child.nb.consumer, parent.nb.consumer, 'consumer')) ||
true
)
},
}),
derives: equalWith,
})

/**
* Capability can be invoked by an agent to add a provider to a space.
*/
export const add = base.derive({
to: capability({
can: 'provider/add',
with: DID,
nb: {
provider: DID,
consumer: URI.match({ protocol: 'did:' }),
},
derives: (child, parent) => {
return (
fail(equalWith(child, parent)) ||
fail(equal(child.nb.provider, parent.nb.provider, 'provider')) ||
fail(equal(child.nb.consumer, parent.nb.consumer, 'consumer')) ||
true
)
},
}),
derives: equalWith,
})
18 changes: 18 additions & 0 deletions packages/capabilities/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,24 @@ export function equal(child, parent, constraint) {
}
}

/**
* @param {Types.Link<unknown, number, number, 0 | 1> | undefined} child
* @param {Types.Link<unknown, number, number, 0 | 1> | undefined} parent
* @param {string} constraint
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be helpful to explain this parameter. It's used to build the error message?

*/

export function equalCID(child, parent, constraint) {
if (parent === undefined) {
return true
} else if (child && !child.equals(parent)) {
return new Failure(
`Constrain violation: ${child} violates imposed ${constraint} constraint ${parent}`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`Constrain violation: ${child} violates imposed ${constraint} constraint ${parent}`
`Constraint violation: ${child} violates imposed ${constraint} constraint ${parent}`

)
} else {
return true
}
}

/**
* @template {Types.ParsedCapability<"store/add"|"store/remove", Types.URI<'did:'>, {link?: Types.Link<unknown, number, number, 0|1>}>} T
* @param {T} claimed
Expand Down
165 changes: 165 additions & 0 deletions packages/capabilities/test/capabilities/consumer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import assert from 'assert'
import { access } from '@ucanto/validator'
import { Verifier } from '@ucanto/principal/ed25519'
import * as Consumer from '../../src/consumer.js'
import { alice, bob, service, mallory } from '../helpers/fixtures.js'
import { parseLink } from '@ucanto/core'
import { createCborCid } from '../helpers/utils.js'

describe('consumer capabilities', function () {
describe('consumer/add', function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could use a test ensuring that this example would be authorized, whose proof includes consumer: did:* but the invocation att has consumer: did:key:zSpace

it('should not self issue', async function () {
const space = bob
const provider = mallory.withDID(
'did:web:ucan.web3.storage:providers:free'
)
const invocation = Consumer.add.invoke({
issuer: alice,
audience: service,
with: provider.did(),
nb: {
request: parseLink('bafkqaaa'),
consumer: space.did(),
},
})

const result = await access(await invocation.delegate(), {
capability: Consumer.add,
principal: Verifier,
authority: service,
})
if (result.error) {
assert.ok(
result.message.includes(`Capability can not be (self) issued`)
)
} else {
assert.fail('should return error')
}
})

it('should fail different nb.request', async function () {
const space = bob
const provider = alice
const consume = Consumer.add.invoke({
issuer: mallory,
audience: service,
with: provider.did(),
nb: {
request: parseLink('bafkqaaa'),
consumer: space.did(),
},
proofs: [
await Consumer.add.delegate({
issuer: provider,
audience: mallory,
with: provider.did(),
nb: {
request: await createCborCid('root'),
consumer: space.did(),
},
}),
],
})

const result = await access(await consume.delegate(), {
capability: Consumer.add,
principal: Verifier,
authority: service,
})
if (result.error) {
assert.ok(
result.message.includes(
`Constrain violation: ${parseLink(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`Constrain violation: ${parseLink(
`Constraint violation: ${parseLink(

'bafkqaaa'
)} violates imposed request constraint ${await createCborCid(
'root'
)}`
)
)
} else {
assert.fail('should return error')
}
})

it('should fail different nb.consumer', async function () {
const space = bob
const provider = alice
const consume = Consumer.add.invoke({
issuer: mallory,
audience: service,
with: provider.did(),
nb: {
request: parseLink('bafkqaaa'),
consumer: space.did(),
},
proofs: [
await Consumer.add.delegate({
issuer: provider,
audience: mallory,
with: provider.did(),
nb: {
request: parseLink('bafkqaaa'),
consumer: mallory.did(),
},
}),
],
})

const result = await access(await consume.delegate(), {
capability: Consumer.add,
principal: Verifier,
authority: service,
})
if (result.error) {
assert.ok(
result.message.includes(
`${space.did()} violates imposed consumer constraint ${mallory.did()}`
)
)
} else {
assert.fail('should return error')
}
})

it('should invoke with good proof', async function () {
const space = bob
const provider = alice
const consume = Consumer.add.invoke({
issuer: mallory,
audience: service,
with: provider.did(),
nb: {
request: parseLink('bafkqaaa'),
consumer: space.did(),
},
proofs: [
await Consumer.add.delegate({
issuer: provider,
audience: mallory,
with: provider.did(),
nb: {
request: parseLink('bafkqaaa'),
consumer: space.did(),
},
}),
],
})

const result = await access(await consume.delegate(), {
capability: Consumer.add,
principal: Verifier,
authority: service,
})
if (result.error) {
assert.fail('should return error')
} else {
assert.deepEqual(result.audience.did(), service.did())
assert.equal(result.capability.can, 'consumer/add')
assert.deepEqual(result.capability.nb, {
request: parseLink('bafkqaaa'),
consumer: space.did(),
})
}
})
})
})
Loading