Skip to content

Commit

Permalink
access agent proofs method picks sessionProofs based on the invocatio…
Browse files Browse the repository at this point in the history
…n audience
  • Loading branch information
gobengo committed Oct 31, 2023
1 parent 38fa0d3 commit c9547b6
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 44 deletions.
37 changes: 28 additions & 9 deletions packages/access-client/src/agent-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,18 @@ export const isSessionProof = (delegation) =>
* Get a map from CIDs to the session proofs that reference them
*
* @param {AgentData} data
* @param {Ucanto.DID} [issuer] - if provided, will only return session proofs issued by this id
* @returns {Record<string, Ucanto.Delegation>}
*/
export function getSessionProofs(data) {
export function getSessionProofs(data, issuer) {
/** @type {Record<string, Ucanto.Delegation>} */
const proofs = {}
for (const { delegation } of data.delegations.values()) {
if (issuer !== undefined && issuer !== delegation.issuer.did()) {
// delegation doesn't match issuer
// eslint-disable-next-line no-continue
continue
}
if (isSessionProof(delegation)) {
const cap = delegation.capabilities[0]
if (cap && !isExpired(delegation)) {
Expand All @@ -201,14 +207,27 @@ export function getSessionProofs(data) {
* @returns {boolean} whether the ucan matches the options
*/
export function matchSessionProof(ucan, options) {
if ( ! isSessionProof(ucan)) { return false; }
if (!isSessionProof(ucan)) {
return false
}
const cap = ucan.capabilities[0]
const matchesRequiredIssuer = (options.issuer === undefined) || options.issuer === ucan.issuer.did()
const isExpiredButNotAllowed = ( ! options.allowExpired) && isExpired(ucan)
const matchesRequiredProof = ( ! options.attestedProof) || (options.attestedProof.toString() === cap.nb.proof.toString())
if ( ! isSessionProof(ucan)) { return false; }
if (isExpiredButNotAllowed) { return false; }
if ( ! matchesRequiredIssuer) { return false; }
if ( ! matchesRequiredProof) { return false; }
const matchesRequiredIssuer =
options.issuer === undefined || options.issuer === ucan.issuer.did()
const isExpiredButNotAllowed = !options.allowExpired && isExpired(ucan)
const matchesRequiredProof =
!options.attestedProof ||
options.attestedProof.toString() === cap.nb.proof.toString()
if (!isSessionProof(ucan)) {
return false
}
if (isExpiredButNotAllowed) {
return false
}
if (!matchesRequiredIssuer) {
return false
}
if (!matchesRequiredProof) {
return false
}
return true
}
44 changes: 15 additions & 29 deletions packages/access-client/src/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
validate,
canDelegateCapability,
} from './delegations.js'
import { AgentData, getSessionProofs, matchSessionProof } from './agent-data.js'
import { AgentData, getSessionProofs } from './agent-data.js'
import { addProviderAndDelegateToAccount } from './agent-use-cases.js'
import { UCAN } from '@web3-storage/capabilities'

Expand Down Expand Up @@ -169,44 +169,30 @@ export class Agent {
* Query the delegations store for all the delegations matching the capabilities provided.
*
* @param {import('@ucanto/interface').Capability[]} [caps]
* @param {Ucanto.DID} [invocationAudience] - audience of invocation these proofs will be bundled with.
*/
#delegations(caps, invocationAudience) {
#delegations(caps) {
const _caps = new Set(caps)
/** @type {Array<{ delegation: Ucanto.Delegation, meta: import('./types.js').DelegationMeta }>} */
const values = []
for (const [, value] of this.#data.delegations) {
// check expiration
if (isExpired(value.delegation)) { continue; }
if (isTooEarly(value.delegation)) { continue; }
if (isExpired(value.delegation)) {
continue
}
if (isTooEarly(value.delegation)) {
continue
}

if (Array.isArray(caps) && caps.length > 0) {
// caps param is provided. Ensure that the delegations we're looping over can delegate to these caps
for (const cap of _caps) {
const capProbablyRequiresSessionProof = value.delegation.signature.code === ucanto.Signature.NON_STANDARD
const findSessionProofs = () => [...this.#data.delegations.values()].filter(m => matchSessionProof(m.delegation, {
attestedProof: value.delegation.cid,
issuer: invocationAudience,
}))
if (canDelegateCapability(value.delegation, cap)) {
const noSessionRequired = ! capProbablyRequiresSessionProof
const sessionProofs = capProbablyRequiresSessionProof ? findSessionProofs() : []
const hasRequiredSessionProof = capProbablyRequiresSessionProof && findSessionProofs().length > 0
const proofs =
// eslint-disable-next-line no-nested-ternary
(noSessionRequired)
? [value]
: hasRequiredSessionProof
? [value, ...sessionProofs]
: []

values.push(...proofs)
if (proofs.length) {
_caps.delete(cap)
}
values.push(value)
_caps.delete(cap)
}
}
} else { // no caps param is provided. Caller must want all delegations.
} else {
// no caps param is provided. Caller must want all delegations.
values.push(value)
}
}
Expand Down Expand Up @@ -268,17 +254,17 @@ export class Agent {
* proofs matching the passed capabilities require it.
*
* @param {import('@ucanto/interface').Capability[]} [caps] - Capabilities to filter by. Empty or undefined caps with return all the proofs.
* @param {Ucanto.DID} [invocationAudience] - audience of invocation these proofs will be bundled with.
* @param {Ucanto.DID} [invocationAudience] - audience of invocation these proofs will be bundled with.
*/
proofs(caps, invocationAudience) {
const arr = []
for (const { delegation } of this.#delegations(caps, invocationAudience)) {
for (const { delegation } of this.#delegations(caps)) {
if (delegation.audience.did() === this.issuer.did()) {
arr.push(delegation)
}
}

const sessions = getSessionProofs(this.#data)
const sessions = getSessionProofs(this.#data, invocationAudience)
for (const proof of arr) {
const session = sessions[proof.asCID.toString()]
if (session) {
Expand Down
16 changes: 10 additions & 6 deletions packages/access-client/test/agent.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,9 @@ describe('Agent', function () {
*/
it('should include session proof based on connection', async () => {
// const space = await ed25519.Signer.generate()
const account = DidMailto.fromEmail(`test-${Math.random().toString().slice(2)}@dag.house`)
const account = DidMailto.fromEmail(
`test-${Math.random().toString().slice(2)}@dag.house`
)
const serviceA = await ed25519.Signer.generate()
const serviceAWeb = serviceA.withDID('did:web:a.up.web3.storage')
const serviceB = await ed25519.Signer.generate()
Expand All @@ -369,8 +371,8 @@ describe('Agent', function () {
{
can: 'provider/add',
with: 'ucan:*',
}
]
},
],
})
const session = await Access.session.delegate({
issuer: service,
Expand All @@ -388,12 +390,14 @@ describe('Agent', function () {
[
{
can: 'provider/add',
with: account
}
with: account,
},
],
desiredInvocationAudience.did(),
desiredInvocationAudience.did()
)
assert.ok(proofsA)
assert.equal(proofsA[1].issuer.did(), desiredInvocationAudience.did())

// TODO(bengo): Show that the proofs vary based on agent.connection.id (like w3cli does)
})
})

0 comments on commit c9547b6

Please sign in to comment.