Skip to content

Commit

Permalink
feat: use new ucanto allows api for canDelegateCapability and imple…
Browse files Browse the repository at this point in the history
…ment space registration (#539)

* use the new `allows` function in `ucanto` to reimplemeent
`canDelegateCapability`
* move space registration from `newCreateSpace` to `registerSpace` where
it belongs
* use agent's current account in registerSpace rather than requiring an
email

---------

Co-authored-by: Benjamin Goering <[email protected]>
  • Loading branch information
travis and gobengo authored Mar 15, 2023
1 parent 5c2bc71 commit 2156f23
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 119 deletions.
2 changes: 1 addition & 1 deletion packages/access-client/src/agent-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class AgentData {
}

/**
* @param {import('@ucanto/interface').DID} did
* @param {import('@ucanto/interface').DID<'key'>} did
*/
async setCurrentSpace(did) {
this.currentSpace = did
Expand Down
156 changes: 48 additions & 108 deletions packages/access-client/src/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import * as ucanto from '@ucanto/core'
import { URI } from '@ucanto/validator'
import { Peer } from './awake/peer.js'
import * as Space from '@web3-storage/capabilities/space'
import * as Voucher from '@web3-storage/capabilities/voucher'
import * as Access from '@web3-storage/capabilities/access'
import * as Provider from '@web3-storage/capabilities/provider'

Expand Down Expand Up @@ -43,43 +42,49 @@ function emailToSessionPrincipal(email) {
}

/**
* @param {Ucanto.Signer<Ucanto.DID<'key'>>} space
* @param {Ucanto.Signer<Ucanto.DID<'key'>>} issuer
* @param {Ucanto.DID} space
* @param {Ucanto.Principal<Ucanto.DID<'mailto'>>} account
* @returns
*/
async function createSpaceSaysAccountCanAdminSpace(space, account) {
async function createIssuerSaysAccountCanAdminSpace(issuer, space, account) {
return ucanto.delegate({
issuer: space,
issuer,
audience: account,
capabilities: [
{
can: 'space/*',
with: space.did(),
with: space,
},
{
can: 'store/*',
with: space.did(),
with: space,
},
{
can: 'upload/*',
with: space.did(),
with: space,
},
],
})
}

/**
* @param {Ucanto.Signer<Ucanto.DID<'key'>>} space
* @param {Ucanto.Signer<Ucanto.DID<'key'>>} issuer
* @param {Ucanto.DID} space
* @param {Ucanto.Principal<Ucanto.DID<'key'>>} device
*/
async function createSpaceSaysDeviceCanAccessDelegateWithSpace(space, device) {
async function createIssuerSaysDeviceCanAccessDelegateWithSpace(
issuer,
space,
device
) {
return ucanto.delegate({
issuer: space,
issuer,
audience: device,
capabilities: [
{
can: 'access/delegate',
with: space.did(),
with: space,
},
],
})
Expand Down Expand Up @@ -319,42 +324,6 @@ export class Agent {
return arr
}

/**
* Creates a space signer and a delegation to the agent
*
* @param {string} [name]
*/
async newCreateSpace(name) {
const signer = await Signer.generate()
const proof = await Space.top.delegate({
issuer: signer,
audience: this.issuer,
with: signer.did(),
expiration: Infinity,
})

await this.addProvider(signer)
await this.delegateSpaceAccessToAccount(signer)

/** @type {import('./types').SpaceMeta} */
const meta = { isRegistered: true }
// eslint-disable-next-line eqeqeq
if (name != undefined) {
if (typeof name !== 'string') {
throw new TypeError('invalid name')
}
meta.name = name
}

await this.#data.addSpace(signer.did(), meta, proof)

return {
did: signer.did(),
meta,
proof,
}
}

/**
* Creates a space signer and a delegation to the agent
*
Expand Down Expand Up @@ -466,7 +435,7 @@ export class Agent {
*
* Other methods will default to use the current space if no resource is defined
*
* @param {Ucanto.DID} space
* @param {Ucanto.DID<'key'>} space
*/
async setCurrentSpace(space) {
const proofs = this.proofs([
Expand Down Expand Up @@ -561,6 +530,10 @@ export class Agent {

await this.addProof(sessionDelegation)
this.#data.setSessionPrincipal(sessionPrincipal)

// claim delegations here because we will need an ucan/attest from the service to
// pair with the session delegation we just claimed to make it work
await this.claimDelegations()
}

async claimDelegations() {
Expand Down Expand Up @@ -603,8 +576,7 @@ export class Agent {
}

/**
*
* @param {Signer.EdSigner} space - TODO is this type correct?
* @param {Ucanto.DID<'key'>} space
*/
async addProvider(space) {
const sessionPrincipal = this.#data.sessionPrincipal
Expand All @@ -613,7 +585,7 @@ export class Agent {
throw new Error('cannot add provider, please authorize first')
}

return await this.invokeAndExecute(Provider.add, {
return this.invokeAndExecute(Provider.add, {
audience: this.connection.id,
with: sessionPrincipal.did(),
proofs: this.proofs([
Expand All @@ -625,14 +597,14 @@ export class Agent {
nb: {
// TODO probably need to make it possible to pass other providers in
provider: 'did:web:staging.web3.storage',
consumer: space.did(),
consumer: space,
},
})
}

/**
*
* @param {Signer.EdSigner} space - TODO is this type correct?
* @param {Ucanto.DID<'key'>} space
*/
async delegateSpaceAccessToAccount(space) {
const sessionPrincipal = this.#data.sessionPrincipal
Expand All @@ -644,10 +616,14 @@ export class Agent {
}

const spaceSaysAccountCanAdminSpace =
await createSpaceSaysAccountCanAdminSpace(space, sessionPrincipal)
return await this.invokeAndExecute(Access.delegate, {
await createIssuerSaysAccountCanAdminSpace(
this.issuer,
space,
sessionPrincipal
)
return this.invokeAndExecute(Access.delegate, {
audience: this.connection.id,
with: space.did(),
with: space,
expiration: Infinity,
nb: {
delegations: {
Expand All @@ -656,7 +632,8 @@ export class Agent {
},
},
proofs: [
await createSpaceSaysDeviceCanAccessDelegateWithSpace(
await createIssuerSaysDeviceCanAccessDelegateWithSpace(
this.issuer,
space,
this.issuer
),
Expand All @@ -671,13 +648,11 @@ export class Agent {
*
* It also adds a full space delegation to the service in the voucher/claim invocation to allow for recovery
*
* @param {string} email
* @param {object} [opts]
* @param {AbortSignal} [opts.signal]
*/
async registerSpace(email, opts) {
async registerSpace(opts) {
const space = this.currentSpace()
const service = this.connection.id
const spaceMeta = space ? this.#data.spaces.get(space) : undefined

if (!space || !spaceMeta) {
Expand All @@ -687,58 +662,23 @@ export class Agent {
if (spaceMeta && spaceMeta.isRegistered) {
throw new Error('Space already registered with web3.storage.')
}

const inv = await this.invokeAndExecute(Voucher.claim, {
nb: {
identity: URI.from(`mailto:${email}`),
product: 'product:free',
service: service.did(),
},
})

if (inv && inv.error) {
throw new Error('Voucher claim failed', { cause: inv })
const providerResult = await this.addProvider(
/** @type {Ucanto.DID<'key'>} */ (space)
)
if (providerResult.error) {
throw new Error(providerResult.message, { cause: providerResult })
}

const voucherRedeem =
/** @type {Ucanto.Delegation<[import('./types').VoucherRedeem]>} */ (
await this.#waitForDelegation(opts)
)
await this.addProof(voucherRedeem)
const delegationToService = await this.delegate({
abilities: ['*'],
audience: service,
expiration: Infinity,
audienceMeta: {
name: 'w3access',
type: 'service',
},
})

const accInv = await this.invokeAndExecute(Voucher.redeem, {
with: URI.from(service.did()),
nb: {
space,
identity: voucherRedeem.capabilities[0].nb.identity,
product: voucherRedeem.capabilities[0].nb.product,
},
proofs: [delegationToService],
facts: [
{
space: spaceMeta,
agent: this.meta,
},
],
})

if (accInv && accInv.error) {
throw new Error('Space registration failed', { cause: accInv })
const delegateSpaceAccessResult = await this.delegateSpaceAccessToAccount(
space
)
if (delegateSpaceAccessResult.error) {
// @ts-ignore it's very weird that this is throwing an error but line 692 above does not - ignore for now
throw new Error(delegateSpaceAccessResult.message, {
cause: delegateSpaceAccessResult,
})
}

spaceMeta.isRegistered = true

this.#data.addSpace(space, spaceMeta)
this.#data.removeDelegation(voucherRedeem.cid)
}

/**
Expand Down
21 changes: 13 additions & 8 deletions packages/access-client/src/delegations.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @ts-ignore
// eslint-disable-next-line no-unused-vars
import * as Ucanto from '@ucanto/interface'
import * as ucanto from '@ucanto/core'
import { canDelegateAbility } from '@web3-storage/capabilities/utils'

/**
Expand Down Expand Up @@ -44,7 +45,9 @@ export function validate(delegation, opts) {
} = opts ?? {}

if (checkAudience && delegation.audience.did() !== checkAudience.did()) {
throw new Error(`Delegation audience does not match required DID.`)
throw new Error(
`Delegation audience ${delegation.audience.did()} does not match required DID ${checkAudience.did()}`
)
}

if (checkIsExpired && isExpired(delegation)) {
Expand All @@ -62,13 +65,15 @@ export function validate(delegation, opts) {
* @param {import('@ucanto/interface').Capability} child
*/
export function canDelegateCapability(delegation, child) {
for (const parent of delegation.capabilities) {
// TODO is this right?
if (
(parent.with === child.with || parent.with === 'ucan:*') &&
canDelegateAbility(parent.can, child.can)
) {
return true
const allowsCapabilities = ucanto.Delegation.allows(delegation)
if (allowsCapabilities[child.with]) {
const cans = /** @type {import('@ucanto/interface').Ability[]} */ (
Object.keys(allowsCapabilities[child.with])
)
for (const can of cans) {
if (canDelegateAbility(can, child.can)) {
return true
}
}
}
return false
Expand Down
2 changes: 1 addition & 1 deletion packages/access-client/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export interface AgentDataModel {
meta: AgentMeta
principal: Signer<DID<'key'>>
sessionPrincipal?: Principal<Ucanto.DID<'mailto'>>
currentSpace?: DID
currentSpace?: DID<'key'>
spaces: Map<DID, SpaceMeta>
delegations: Map<CIDString, { meta: DelegationMeta; delegation: Delegation }>
}
Expand Down
2 changes: 1 addition & 1 deletion packages/access-client/test/agent-data.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ describe('AgentData', () => {
}
const store = new Store()
const data = await AgentData.create(undefined, { store })
await assert.doesNotReject(data.setCurrentSpace('did:x:y'))
await assert.doesNotReject(data.setCurrentSpace('did:key:y'))
})
})

0 comments on commit 2156f23

Please sign in to comment.