Skip to content

Commit

Permalink
feat: implement admin/upload/inspect and admin/store/inspect capa…
Browse files Browse the repository at this point in the history
…bilities (#918)

1. implementations of new capabilities proposed in
storacha/specs#77
2. added a new `getCID` method to each of the `{Upload|Store}Table`
interfaces, and updated the in-memory implementations of each.
3. implementations of `admin/upload/inspect` and `admin/store/inspect`
invocation handlers
4. tests for all of the above
  • Loading branch information
travis authored Sep 13, 2023
1 parent 8d1b666 commit 5616a12
Show file tree
Hide file tree
Showing 20 changed files with 797 additions and 22 deletions.
48 changes: 48 additions & 0 deletions packages/capabilities/src/admin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { capability, struct, ok, Link } from '@ucanto/validator'
import { equalWith, and, equal, ProviderDID } from './utils.js'

export const admin = capability({
can: 'admin/*',
with: ProviderDID,
derives: equalWith,
})

export const upload = {
/**
* Capability can be invoked by a provider to get information about a content CID.
*/
inspect: capability({
can: 'admin/upload/inspect',
with: ProviderDID,
nb: struct({
root: Link,
}),
derives: (child, parent) => {
return (
and(equalWith(child, parent)) ||
and(equal(child.nb.root, parent.nb.root, 'root')) ||
ok({})
)
},
}),
}

export const store = {
/**
* Capability can be invoked by a provider to get information an upload shard CID.
*/
inspect: capability({
can: 'admin/store/inspect',
with: ProviderDID,
nb: struct({
link: Link,
}),
derives: (child, parent) => {
return (
and(equalWith(child, parent)) ||
and(equal(child.nb.link, parent.nb.link, 'link')) ||
ok({})
)
},
}),
}
5 changes: 5 additions & 0 deletions packages/capabilities/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as Consumer from './consumer.js'
import * as Customer from './customer.js'
import * as Console from './console.js'
import * as RateLimit from './rate-limit.js'
import * as Admin from './admin.js'
import * as Subscription from './subscription.js'
import * as Filecoin from './filecoin.js'

Expand All @@ -26,6 +27,7 @@ export {
RateLimit,
Subscription,
Filecoin,
Admin,
}

/** @type {import('./types.js').AbilitiesArray} */
Expand Down Expand Up @@ -59,4 +61,7 @@ export const abilitiesAsStrings = [
Filecoin.dealQueue.can,
Filecoin.dealAdd.can,
Filecoin.chainTrackerInfo.can,
Admin.admin.can,
Admin.upload.inspect.can,
Admin.store.inspect.can,
]
26 changes: 25 additions & 1 deletion packages/capabilities/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as ConsumerCaps from './consumer.js'
import * as SubscriptionCaps from './subscription.js'
import * as RateLimitCaps from './rate-limit.js'
import * as FilecoinCaps from './filecoin.js'
import * as AdminCaps from './admin.js'

export type { Unit }

Expand Down Expand Up @@ -211,6 +212,26 @@ export type Store = InferInvokedCapability<typeof store>
export type StoreAdd = InferInvokedCapability<typeof add>
export type StoreRemove = InferInvokedCapability<typeof remove>
export type StoreList = InferInvokedCapability<typeof list>
// Admin
export type Admin = InferInvokedCapability<typeof AdminCaps.admin>
export type AdminUploadInspect = InferInvokedCapability<
typeof AdminCaps.upload.inspect
>
export type AdminStoreInspect = InferInvokedCapability<
typeof AdminCaps.store.inspect
>
export interface SpaceAdmin {
did: DID
insertedAt: string
}
export interface AdminUploadInspectSuccess {
spaces: SpaceAdmin[]
}
export type AdminUploadInspectFailure = Ucanto.Failure
export interface AdminStoreInspectSuccess {
spaces: SpaceAdmin[]
}
export type AdminStoreInspectFailure = Ucanto.Failure
// Filecoin
export type FilecoinQueue = InferInvokedCapability<
typeof FilecoinCaps.filecoinQueue
Expand Down Expand Up @@ -263,5 +284,8 @@ export type AbilitiesArray = [
AggregateAdd['can'],
DealQueue['can'],
DealAdd['can'],
ChainTrackerInfo['can']
ChainTrackerInfo['can'],
Admin['can'],
AdminUploadInspect['can'],
AdminStoreInspect['can']
]
5 changes: 4 additions & 1 deletion packages/capabilities/src/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { fail, ok } from '@ucanto/validator'
import { DID, fail, ok } from '@ucanto/validator'
// eslint-disable-next-line no-unused-vars
import * as Types from '@ucanto/interface'

// e.g. did:web:web3.storage or did:web:staging.web3.storage
export const ProviderDID = DID.match({ method: 'web' })

/**
* Check URI can be delegated
*
Expand Down
194 changes: 194 additions & 0 deletions packages/capabilities/test/capabilities/admin.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import assert from 'assert'
import { access } from '@ucanto/validator'
import { delegate } from '@ucanto/core'
import { Verifier } from '@ucanto/principal/ed25519'
import * as Admin from '../../src/admin.js'
import { service, alice, readmeCID } from '../helpers/fixtures.js'

describe('admin/upload/inspect', async function () {
const agent = alice
it('can be invoked by the service on the service', async function () {
const invocation = Admin.upload.inspect.invoke({
issuer: service,
audience: service,
with: service.did(),
nb: {
root: readmeCID,
},
})
const result = await access(await invocation.delegate(), {
capability: Admin.upload.inspect,
principal: Verifier,
authority: service,
})
if (result.error) {
assert.fail('error in self issue')
} else {
assert.deepEqual(result.ok.audience.did(), service.did())
assert.equal(result.ok.capability.can, 'admin/upload/inspect')
assert.deepEqual(result.ok.capability.nb, {
root: readmeCID,
})
}
})

it('can be invoked by an agent delegated permissions by the service', async function () {
const auth = Admin.upload.inspect.invoke({
issuer: agent,
audience: service,
with: service.did(),
nb: {
root: readmeCID,
},
proofs: [
await delegate({
issuer: service,
audience: agent,
capabilities: [{ with: service.did(), can: 'admin/upload/inspect' }],
}),
],
})
const result = await access(await auth.delegate(), {
capability: Admin.upload.inspect,
principal: Verifier,
authority: service,
})
if (result.error) {
assert.fail(
`error in self issue: ${JSON.stringify(result.error.message)}`
)
} else {
assert.deepEqual(result.ok.audience.did(), service.did())
assert.equal(result.ok.capability.can, 'admin/upload/inspect')
assert.deepEqual(result.ok.capability.nb, {
root: readmeCID,
})
}
})

it('fails without a delegation from the service delegation', async function () {
const agent = alice
const auth = Admin.upload.inspect.invoke({
issuer: agent,
audience: service,
with: service.did(),
nb: {
root: readmeCID,
},
})

const result = await access(await auth.delegate(), {
capability: Admin.upload.inspect,
principal: Verifier,
authority: service,
})

assert.equal(result.error?.message.includes('not authorized'), true)
})

it('requires nb.root', async function () {
assert.throws(() => {
Admin.upload.inspect.invoke({
issuer: alice,
audience: service,
with: service.did(),
// @ts-ignore
nb: {},
})
}, /Error: Invalid 'nb' - Object contains invalid field "root"/)
})
})

describe('admin/store/inspect', function () {
const agent = alice
it('can be invoked by the service on the service', async function () {
const invocation = Admin.store.inspect.invoke({
issuer: service,
audience: service,
with: service.did(),
nb: {
link: readmeCID,
},
})
const result = await access(await invocation.delegate(), {
capability: Admin.store.inspect,
principal: Verifier,
authority: service,
})
if (result.error) {
assert.fail('error in self issue')
} else {
assert.deepEqual(result.ok.audience.did(), service.did())
assert.equal(result.ok.capability.can, 'admin/store/inspect')
assert.deepEqual(result.ok.capability.nb, {
link: readmeCID,
})
}
})

it('can be invoked by an agent delegated permissions by the service', async function () {
const auth = Admin.store.inspect.invoke({
issuer: agent,
audience: service,
with: service.did(),
nb: {
link: readmeCID,
},
proofs: [
await delegate({
issuer: service,
audience: agent,
capabilities: [{ with: service.did(), can: 'admin/store/inspect' }],
}),
],
})
const result = await access(await auth.delegate(), {
capability: Admin.store.inspect,
principal: Verifier,
authority: service,
})
if (result.error) {
assert.fail(
`error in self issue: ${JSON.stringify(result.error.message)}`
)
} else {
assert.deepEqual(result.ok.audience.did(), service.did())
assert.equal(result.ok.capability.can, 'admin/store/inspect')
assert.deepEqual(result.ok.capability.nb, {
link: readmeCID,
})
}
})

it('fails without a delegation from the service delegation', async function () {
const agent = alice
const auth = Admin.store.inspect.invoke({
issuer: agent,
audience: service,
with: service.did(),
nb: {
link: readmeCID,
},
})

const result = await access(await auth.delegate(), {
capability: Admin.store.inspect,
principal: Verifier,
authority: service,
})

assert.equal(result.error?.message.includes('not authorized'), true)
})

it('requires nb.shard', async function () {
assert.throws(() => {
Admin.store.inspect.invoke({
issuer: alice,
audience: service,
with: service.did(),
// @ts-ignore
nb: {},
})
}, /Error: Invalid 'nb' - Object contains invalid field "link"/)
})
})
5 changes: 5 additions & 0 deletions packages/capabilities/test/helpers/fixtures.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { parseLink } from '@ucanto/core'
import { Absentee } from '@ucanto/principal'
import { Signer } from '@ucanto/principal/ed25519'

Expand Down Expand Up @@ -31,3 +32,7 @@ export const malloryAccount = Absentee.from({
export const service = Signer.parse(
'MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8='
).withDID('did:web:test.web3.storage')

export const readmeCID = parseLink(
'bafybeihqfdg2ereoijjoyrqzr2x2wsasqm2udurforw7pa3tvbnxhojao4'
)
16 changes: 16 additions & 0 deletions packages/upload-api/src/admin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as Types from './types.js'
import * as StoreInspect from './admin/store/inspect.js'
import * as UploadInspect from './admin/upload/inspect.js'

/**
* @param {Types.AdminServiceContext} context
*/
export const createService = (context) => ({
store: {
inspect: StoreInspect.provide(context),
},

upload: {
inspect: UploadInspect.provide(context),
},
})
Loading

0 comments on commit 5616a12

Please sign in to comment.