Skip to content
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: implement admin/upload/inspect and admin/store/inspect capabilities #918

Merged
merged 12 commits into from
Sep 13, 2023
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
Loading