-
Notifications
You must be signed in to change notification settings - Fork 22
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: Generate Space proofs on the fly, on access/claim
#1555
Changes from 9 commits
ed9771d
4e48393
31ee9fb
dba4db6
7f16b60
9c94daa
fb13d85
6c7a438
8bf7036
5abf1ac
584d350
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,164 @@ | ||
import * as Server from '@ucanto/server' | ||
import * as Access from '@web3-storage/capabilities/access' | ||
import * as UCAN from '@ipld/dag-ucan' | ||
import * as API from '../types.js' | ||
import * as delegationsResponse from '../utils/delegations-response.js' | ||
import { createSessionProofs } from './confirm.js' | ||
|
||
/** | ||
* @param {API.AccessClaimContext} ctx | ||
*/ | ||
export const provide = (ctx) => | ||
Server.provide(Access.claim, (input) => claim(input, ctx)) | ||
|
||
/** | ||
* Checks if the given Principal is an Account. | ||
* @param {API.Principal} principal | ||
* @returns {principal is API.Principal<API.DID<'mailto'>>} | ||
*/ | ||
const isAccount = (principal) => principal.did().startsWith('did:mailto:') | ||
|
||
/** | ||
* Returns true when the delegation has a `ucan:*` capability. | ||
* @param {API.Delegation} delegation | ||
* @returns boolean | ||
*/ | ||
const isUcanStar = (delegation) => | ||
Peeja marked this conversation as resolved.
Show resolved
Hide resolved
|
||
delegation.capabilities.some((capability) => capability.with === 'ucan:*') | ||
|
||
/** | ||
* Returns true when the capability is a `ucan/attest` capability for the given | ||
* signer. | ||
* | ||
* @param {API.Capability} capability | ||
* @returns {capability is API.UCANAttest} | ||
*/ | ||
const isUCANAttest = (capability) => capability.can === 'ucan/attest' | ||
|
||
/** | ||
* @param {API.Input<Access.claim>} input | ||
* @param {API.AccessClaimContext} ctx | ||
* @returns {Promise<API.Result<API.AccessClaimSuccess, API.AccessClaimFailure>>} | ||
*/ | ||
export const claim = async ( | ||
{ invocation }, | ||
{ delegationsStorage: delegations } | ||
) => { | ||
export const claim = async ({ invocation }, { delegationsStorage, signer }) => { | ||
const claimedAudience = invocation.capabilities[0].with | ||
const claimedResult = await delegations.find({ audience: claimedAudience }) | ||
if (claimedResult.error) { | ||
const storedDelegationsResult = await delegationsStorage.find({ | ||
audience: claimedAudience, | ||
}) | ||
|
||
if (storedDelegationsResult.error) { | ||
return { | ||
error: { | ||
name: 'AccessClaimFailure', | ||
message: 'error finding delegations', | ||
cause: claimedResult.error, | ||
cause: storedDelegationsResult.error, | ||
}, | ||
} | ||
} | ||
|
||
const delegationsToReturnByCid = Object.fromEntries( | ||
storedDelegationsResult.ok.map((delegation) => [delegation.cid, delegation]) | ||
) | ||
|
||
// Find any attested ucan:* delegations and replace them with fresh ones. | ||
for (const delegation of storedDelegationsResult.ok) { | ||
// Ignore delegations that aren't attestations, and ours. | ||
const attestCap = delegation.capabilities.find(isUCANAttest) | ||
if (!(attestCap && attestCap.with === signer.did())) continue | ||
|
||
// Ignore invalid attestations. | ||
const valid = | ||
(await UCAN.verifySignature(delegation.data, signer)) && | ||
!UCAN.isTooEarly(delegation.data) && | ||
!UCAN.isExpired(delegation.data) | ||
if (!valid) continue | ||
|
||
// Ignore attestations of delegations we don't have. | ||
const attestedCid = attestCap.nb.proof | ||
const attestedDelegation = delegationsToReturnByCid[attestedCid.toString()] | ||
if (!(attestedDelegation && isUcanStar(attestedDelegation))) continue | ||
|
||
// Create new session proofs for the attested delegation. | ||
const sessionProofsResult = await createSessionProofsForLogin( | ||
attestedDelegation, | ||
delegationsStorage, | ||
signer | ||
) | ||
|
||
// If something went wrong, bail on the entire invocation with the error. | ||
// NB: This breaks out of the loop, because if this fails at all, we don't | ||
// need to keep looking. | ||
if (sessionProofsResult.error) { | ||
return { | ||
error: { | ||
name: 'AccessClaimFailure', | ||
message: 'error creating session proofs', | ||
cause: sessionProofsResult.error, | ||
}, | ||
} | ||
} | ||
|
||
// Delete the ones we're replacing... | ||
delete delegationsToReturnByCid[delegation.cid.toString()] | ||
delete delegationsToReturnByCid[attestedCid.toString()] | ||
|
||
// ...and add the new ones. | ||
for (const proof of sessionProofsResult.ok) { | ||
delegationsToReturnByCid[proof.cid.toString()] = proof | ||
} | ||
} | ||
|
||
return { | ||
ok: { | ||
delegations: delegationsResponse.encode(claimedResult.ok), | ||
delegations: delegationsResponse.encode( | ||
Object.values(delegationsToReturnByCid) | ||
), | ||
}, | ||
} | ||
} | ||
|
||
/** | ||
* @param {API.Delegation} loginDelegation | ||
* @param {API.DelegationsStorage} delegationsStorage | ||
* @param {API.Signer} signer | ||
* @returns {Promise<API.Result<API.Delegation[], API.AccessClaimFailure>>} | ||
*/ | ||
async function createSessionProofsForLogin( | ||
loginDelegation, | ||
delegationsStorage, | ||
signer | ||
) { | ||
// These should always be Accounts (did:mailto:), but if one's not, skip it. | ||
if (!isAccount(loginDelegation.issuer)) return { ok: [] } | ||
|
||
const accountDelegationsResult = await delegationsStorage.find({ | ||
audience: loginDelegation.issuer.did(), | ||
}) | ||
|
||
if (accountDelegationsResult.error) { | ||
return { | ||
error: { | ||
name: 'AccessClaimFailure', | ||
message: 'error finding delegations', | ||
cause: accountDelegationsResult.error, | ||
}, | ||
} | ||
} | ||
|
||
return { | ||
ok: await createSessionProofs({ | ||
service: signer, | ||
account: loginDelegation.issuer, | ||
agent: loginDelegation.audience, | ||
facts: loginDelegation.facts, | ||
capabilities: loginDelegation.capabilities, | ||
// We include all the delegations to the account so that the agent will | ||
// have delegation chains to all the delegated resources. | ||
// We should actually filter out only delegations that support delegated | ||
// capabilities, but for now we just include all of them since we only | ||
// implement sudo access anyway. | ||
delegationProofs: accountDelegationsResult.ok, | ||
expiration: Infinity, | ||
}), | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -403,6 +403,7 @@ export type UploadServiceContext = ConsumerServiceContext & | |
} | ||
|
||
export interface AccessClaimContext { | ||
signer: EdSigner.Signer | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can just be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. They were all this, but I've changed them to just |
||
delegationsStorage: Delegations | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌