-
Notifications
You must be signed in to change notification settings - Fork 5
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
Egress is authorized with UCAN (stubbed) #126
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
52faac5
`withAuthorizedSpace()` authorizes
Peeja d52d27f
Handle multiple indexer hits
Peeja 900aefc
Serves legacy content
Peeja 271cf89
Throw errors encountered by `delegationsStorage`
Peeja ddf51d8
Add comments and documentation
Peeja 08b06a2
Use mock properly 🤦♀️
Peeja fd16fae
`withAuthorizedSpace` scopes the `locator`
Peeja 721c2e1
Add query param stubs for delegations
Peeja 8e5ccb0
Move import to top
Peeja 03d89fe
Fix syntax (rebase error)
Peeja 4e285b4
Use `atob()` instead of `Buffer`
Peeja 4af9309
Use wildcard capability
Peeja File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
declare module '@web3-storage/w3cli/lib.js' { | ||
import { Client } from '@web3-storage/w3up-client' | ||
export declare function getClient(): Promise<Client> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import sade from 'sade' | ||
import { getClient } from '@web3-storage/w3cli/lib.js' | ||
import * as ed25519 from '@ucanto/principal/ed25519' | ||
import * as serve from '../src/capabilities/serve.js' | ||
|
||
const cli = sade('delegate-serve.js <space> [token]') | ||
|
||
cli | ||
.describe( | ||
`Delegates ${serve.star.can} to the Gateway for <space>, with an optional token. Outputs a base64url string suitable for the stub_delegation query parameter. Pipe the output to pbcopy or similar for the quickest workflow.` | ||
) | ||
.action(async (space, token) => { | ||
const client = await getClient() | ||
|
||
const gatewayIdentity = (await ed25519.Signer.generate()).withDID( | ||
'did:web:w3s.link' | ||
) | ||
|
||
const delegation = await serve.star.delegate({ | ||
issuer: client.agent.issuer, | ||
audience: gatewayIdentity, | ||
with: space, | ||
nb: { token: token ?? null }, | ||
expiration: Infinity, | ||
proofs: client.proofs([ | ||
{ | ||
can: serve.star.can, | ||
with: space | ||
} | ||
]) | ||
}) | ||
|
||
const carResult = await delegation.archive() | ||
if (carResult.error) throw carResult.error | ||
process.stdout.write(Buffer.from(carResult.ok).toString('base64url')) | ||
}) | ||
|
||
cli.parse(process.argv) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { capability, Schema, DID, nullable, string } from '@ucanto/validator' | ||
|
||
/** | ||
* "Manage the serving of content owned by the subject Space." | ||
* | ||
* A Principal who may `space/content/serve/*` is permitted to perform all | ||
* operations related to serving content owned by the Space, including actually | ||
* serving it and recording egress charges. | ||
*/ | ||
export const star = capability({ | ||
can: 'space/content/serve/*', | ||
/** | ||
* The Space which contains the content. This Space will be charged egress | ||
* fees if content is actually retrieved by way of this invocation. | ||
*/ | ||
with: DID, | ||
nb: Schema.struct({ | ||
/** The authorization token, if any, used for this request. */ | ||
token: nullable(string()) | ||
}) | ||
}) | ||
|
||
/** | ||
* "Serve content owned by the subject Space over HTTP." | ||
* | ||
* A Principal who may `space/content/serve/transport/http` is permitted to | ||
* serve any content owned by the Space, in the manner of an [IPFS Gateway]. The | ||
* content may be a Blob stored by a Storage Node, or indexed content stored | ||
* within such Blobs (ie, Shards). | ||
* | ||
* Note that the args do not currently specify *what* content should be served. | ||
* Invoking this command does not currently *serve* the content in any way, but | ||
* merely validates the authority to do so. Currently, the entirety of a Space | ||
* must use the same authorization, thus the content does not need to be | ||
* identified. In the future, this command may refer directly to a piece of | ||
* content by CID. | ||
* | ||
* [IPFS Gateway]: https://specs.ipfs.tech/http-gateways/path-gateway/ | ||
*/ | ||
export const transportHttp = capability({ | ||
can: 'space/content/serve/transport/http', | ||
/** | ||
* The Space which contains the content. This Space will be charged egress | ||
* fees if content is actually retrieved by way of this invocation. | ||
*/ | ||
with: DID, | ||
nb: Schema.struct({ | ||
/** The authorization token, if any, used for this request. */ | ||
token: nullable(string()) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import { Verifier } from '@ucanto/principal' | ||
import { ok, access, Unauthorized } from '@ucanto/validator' | ||
import { HttpError } from '@web3-storage/gateway-lib/util' | ||
import * as serve from '../capabilities/serve.js' | ||
|
||
/** | ||
* @import * as Ucanto from '@ucanto/interface' | ||
* @import { Locator } from '@web3-storage/blob-fetcher' | ||
* @import { IpfsUrlContext, Middleware } from '@web3-storage/gateway-lib' | ||
* @import { LocatorContext } from './withLocator.types.js' | ||
* @import { AuthTokenContext } from './withAuthToken.types.js' | ||
* @import { SpaceContext, DelegationsStorageContext } from './withAuthorizedSpace.types.js' | ||
*/ | ||
|
||
/** | ||
* Attempts to locate the {@link IpfsUrlContext.dataCid}. If it's able to, | ||
* attempts to authorize the request to access the data. | ||
* | ||
* @throws {HttpError} (404) If the locator tells us the data is not found, or | ||
* if no located space is one the request is authorized to access. | ||
* @throws {Error} If the locator fails in any other way. | ||
* @type {( | ||
* Middleware< | ||
* LocatorContext & IpfsUrlContext & AuthTokenContext & DelegationsStorageContext & SpaceContext, | ||
* LocatorContext & IpfsUrlContext & AuthTokenContext & DelegationsStorageContext, | ||
* {} | ||
* > | ||
* )} | ||
*/ | ||
export function withAuthorizedSpace (handler) { | ||
return async (request, env, ctx) => { | ||
const { locator, dataCid } = ctx | ||
const locRes = await locator.locate(dataCid.multihash) | ||
if (locRes.error) { | ||
if (locRes.error.name === 'NotFound') { | ||
throw new HttpError('Not Found', { status: 404, cause: locRes.error }) | ||
} | ||
throw new Error(`failed to locate: ${dataCid}`, { cause: locRes.error }) | ||
} | ||
|
||
// Legacy behavior: Site results which have no Space attached are from | ||
// before we started authorizing serving content explicitly. For these, we | ||
// always serve the content, but only if the request has no authorization | ||
// token. | ||
const shouldServeLegacy = | ||
locRes.ok.site.some((site) => site.space === undefined) && | ||
ctx.authToken === null | ||
|
||
if (shouldServeLegacy) { | ||
return handler(request, env, { ...ctx, space: null }) | ||
} | ||
|
||
// These Spaces all have the content we're to serve, if we're allowed to. | ||
const spaces = locRes.ok.site | ||
.map((site) => site.space) | ||
.filter((s) => s !== undefined) | ||
|
||
try { | ||
// First space to successfully authorize is the one we'll use. | ||
const space = await Promise.any( | ||
spaces.map(async (space) => { | ||
const result = await authorize(space, ctx) | ||
if (result.error) throw result.error | ||
return space | ||
}) | ||
) | ||
return handler(request, env, { | ||
...ctx, | ||
space, | ||
locator: spaceScopedLocator(locator, space) | ||
}) | ||
} catch (error) { | ||
// If all Spaces failed to authorize, throw the first error. | ||
if ( | ||
error instanceof AggregateError && | ||
error.errors.every((e) => e instanceof Unauthorized) | ||
) { | ||
throw new HttpError('Not Found', { status: 404, cause: error }) | ||
} else { | ||
throw error | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Authorizes the request to serve content from the given Space. Looks for | ||
* authorizing delegations in the | ||
* {@link DelegationsStorageContext.delegationsStorage}. | ||
* | ||
* @param {Ucanto.DID} space | ||
* @param {AuthTokenContext & DelegationsStorageContext} ctx | ||
* @returns {Promise<Ucanto.Result<{}, Ucanto.Failure>>} | ||
*/ | ||
const authorize = async (space, ctx) => { | ||
// Look up delegations that might authorize us to serve the content. | ||
const relevantDelegationsResult = await ctx.delegationsStorage.find({ | ||
audience: ctx.gatewayIdentity.did(), | ||
can: serve.transportHttp.can, | ||
with: space | ||
}) | ||
|
||
if (relevantDelegationsResult.error) return relevantDelegationsResult | ||
|
||
// Create an invocation of the serve capability. | ||
const invocation = await serve.transportHttp | ||
.invoke({ | ||
issuer: ctx.gatewayIdentity, | ||
audience: ctx.gatewayIdentity, | ||
with: space, | ||
nb: { | ||
token: ctx.authToken | ||
}, | ||
proofs: relevantDelegationsResult.ok | ||
}) | ||
.delegate() | ||
|
||
// Validate the invocation. | ||
const accessResult = await access(invocation, { | ||
capability: serve.transportHttp, | ||
authority: ctx.gatewayIdentity, | ||
principal: Verifier, | ||
validateAuthorization: () => ok({}) | ||
}) | ||
|
||
if (accessResult.error) { | ||
return accessResult | ||
} | ||
|
||
return { ok: {} } | ||
} | ||
|
||
/** | ||
* Wraps a {@link Locator} and locates content only from a specific Space. | ||
* | ||
* @param {Locator} locator | ||
* @param {Ucanto.DID} space | ||
* @returns {Locator} | ||
*/ | ||
const spaceScopedLocator = (locator, space) => ({ | ||
locate: async (digest) => { | ||
const locateResult = await locator.locate(digest) | ||
if (locateResult.error) { | ||
return locateResult | ||
} else { | ||
return { | ||
ok: { | ||
...locateResult.ok, | ||
site: locateResult.ok.site.filter((site) => site.space === space) | ||
} | ||
} | ||
} | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import * as Ucanto from '@ucanto/interface' | ||
import { Context as MiddlewareContext } from '@web3-storage/gateway-lib' | ||
|
||
export interface DelegationsStorageContext extends MiddlewareContext { | ||
/** The identity of the gateway itself */ | ||
gatewayIdentity: Ucanto.Signer | ||
delegationsStorage: DelegationsStorage | ||
} | ||
|
||
export interface SpaceContext extends MiddlewareContext { | ||
space: Ucanto.DID | null | ||
} | ||
|
||
// TEMP: https://github.com/storacha/blob-fetcher/pull/13/files | ||
declare module '@web3-storage/blob-fetcher' { | ||
interface Site { | ||
space?: Ucanto.DID | ||
} | ||
} | ||
|
||
// TEMP | ||
|
||
export interface Query { | ||
audience?: Ucanto.DID | ||
can: string | ||
with: Ucanto.Resource | ||
} | ||
|
||
export interface DelegationsStorage { | ||
/** | ||
* find all items that match the query | ||
*/ | ||
find: ( | ||
query: Query | ||
) => Promise<Ucanto.Result<Ucanto.Delegation[], Ucanto.Failure>> | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
suggestion: I've released the new version of the
w3up
capabilities package in case you want to use the newspace/content/serve/*
from that lib.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.
Ooh, excellent, thanks!