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: allow api inferring capabilities #259

Merged
merged 5 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions packages/core/src/delegation.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,71 @@ export const isLink =
*/
export const isDelegation = proof => !Link.isLink(proof)

/**
* Takes on or more delegations and returns all delegated capabilities in
Gozala marked this conversation as resolved.
Show resolved Hide resolved
* UCAN 0.10 format, expanding all the special forms like `with: ucan:*` and
* `can: *` to an explicit forms.
Gozala marked this conversation as resolved.
Show resolved Hide resolved
*
* @template {[API.Delegation, ...API.Delegation[]]} T
* @param {T} delegations
* @returns {API.InferAllowedFromDelegations<T>}
*/
export const allows = (...delegations) => {
/** @type {API.Allows} */
let allow = {}
for (const delegation of delegations) {
for (const capability of delegation.capabilities) {
// If uri is `ucan:*` then we include own capabilities along with
// delegated
const capabilities =
capability.with === 'ucan:*'
? iterateCapabilities(delegation)
: [capability]

for (const { with: uri, can, nb } of capabilities) {
const resource = allow[uri] || (allow[uri] = {})
const abilities = resource[can] || (resource[can] = [])
abilities.push({ ...Object(nb) })
Gozala marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

return /** @type {API.InferAllowedFromDelegations<T>} */ (allow)
}

/**
* Function takes a delegation and iterates over all the capabilities expanding
* all the special forms like `with: ucan:*` and `can: *`.
*
* @param {API.Delegation} delegation
* @returns {Iterable<API.Capability>}
*/
const iterateCapabilities = function* ({ issuer, capabilities, proofs }) {
for (const { with: uri, can, nb } of capabilities) {
if (uri === 'ucan:*') {
yield {
with: issuer.did(),
can,
Gozala marked this conversation as resolved.
Show resolved Hide resolved
nb,
}

for (const proof of proofs) {
if (isDelegation(proof)) {
for (const capability of iterateCapabilities(proof)) {
yield {
with: capability.with,
can: can === '*' ? capability.can : can,
nb: { ...capability.nb, ...Object(nb) },
Gozala marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
} else {
yield { with: uri, can, nb }
}
}
}

/**
* Represents UCAN chain view over the set of DAG UCAN nodes. You can think of
* this as UCAN interface of the CAR.
Expand Down
193 changes: 193 additions & 0 deletions packages/core/test/delegation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,196 @@ test('.delegate() return same value', async () => {

assert.equal(ucan.delegate(), ucan)
})

test('derive allows', async () => {
const echo = await delegate({
issuer: alice,
audience: w3,
capabilities: [
{
can: 'test/echo',
with: alice.did(),
},
],
})

assert.deepEqual(Delegation.allows(echo), {
[alice.did()]: {
'test/echo': [{}],
},
})
})

test('infer capabilities with ucan:*', async () => {
const ucan = await Delegation.delegate({
issuer: alice,
audience: bob,
capabilities: [
{
with: 'ucan:*',
can: 'test/echo',
},
],
proofs: [
await Delegation.delegate({
issuer: mallory,
audience: alice,
capabilities: [
{
with: mallory.did(),
can: '*',
},
],
}),
],
})

assert.deepEqual(Object(Delegation.allows(ucan)), {
[alice.did()]: {
'test/echo': [{}],
},
[mallory.did()]: {
'test/echo': [{}],
},
})
})

test('derive allow { with: "ucan:*", can: "*" }', async () => {
const ucan = await Delegation.delegate({
issuer: alice,
audience: bob,
capabilities: [
{
with: 'ucan:*',
can: '*',
},
],
proofs: [
await Delegation.delegate({
issuer: mallory,
audience: alice,
capabilities: [
{
with: mallory.did(),
can: 'debug/echo',
nb: {
message: 'hello',
},
},
{
with: mallory.did(),
can: 'test/echo',
},
],
}),
],
})

assert.deepEqual(Object(Delegation.allows(ucan)), {
[alice.did()]: {
'*': [{}],
},
[mallory.did()]: {
'debug/echo': [{ message: 'hello' }],
'test/echo': [{}],
},
})
})

test('allow * imposes caveats', async () => {
const ucan = await Delegation.delegate({
issuer: alice,
audience: bob,
capabilities: [
{
with: 'ucan:*',
can: '*',
nb: {
limit: 3,
},
},
],
proofs: [
await Delegation.delegate({
issuer: mallory,
audience: alice,
capabilities: [
{
with: mallory.did(),
can: 'debug/echo',
nb: {
message: 'hello',
},
},
{
with: mallory.did(),
can: 'test/echo',
},
],
}),
],
})

assert.deepEqual(Object(Delegation.allows(ucan)), {
[alice.did()]: {
'*': [
{
limit: 3,
},
],
},
[mallory.did()]: {
'debug/echo': [{ message: 'hello', limit: 3 }],
'test/echo': [{ limit: 3 }],
},
})
})

test('derive allow from multiple', async () => {
const a = await Delegation.delegate({
issuer: alice,
audience: bob,
capabilities: [
{
with: alice.did(),
can: 'store/*',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to test that store/* allows store/add

nb: {
size: 100,
},
},
],
})

const b = await Delegation.delegate({
issuer: mallory,
audience: bob,
capabilities: [
{
with: 'ucan:*',
can: '*',
},
],
proofs: [
await Delegation.delegate({
issuer: alice,
audience: mallory,
capabilities: [
{
with: alice.did(),
can: 'upload/add',
},
],
}),
],
})

assert.deepEqual(Object(Delegation.allows(a, b)), {
[mallory.did()]: {
'*': [{}],
},
[alice.did()]: {
'store/*': [{ size: 100 }],
'upload/add': [{}],
},
})
})
99 changes: 99 additions & 0 deletions packages/interface/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,105 @@ export interface Delegation<C extends Capabilities = Capabilities> {
delegate(): Await<Delegation<C>>
}

/**
* Type representing a UCAN capability set in UCAN 0.10 format.
* @see https://github.com/ucan-wg/spec/blob/0.10/README.md#241-examples
Gozala marked this conversation as resolved.
Show resolved Hide resolved
*/
export type Allows<
URI extends Resource = Resource,
Abilities extends ResourceAbilities = ResourceAbilities
> = {
[K in URI]: Abilities
}

/**
* Type representing a set of abilities for a specific resource. It is a map of
* abilities to a list of caveats. This type is used in representation of the
* UCAN capability set in UCAN 0.10 format.
*/
export type ResourceAbilities<
Can extends Ability = any,
Caveats extends Record<string, unknown> = Record<string, unknown>
> = {
[K in Can]: Caveats[]
}

/**
* Utility type that infers union of two {@link ResourceAbilities}. Type is used
* to infer capabilities of the {@link Delegation}.
*/
export type JoinAbilities<
T extends ResourceAbilities,
U extends ResourceAbilities
> = {
[K in keyof T | keyof U]: [
...(K extends keyof T ? T[K] : []),
...(K extends keyof U ? U[K] : [])
]
}

/**
* Utility type that infers union of two {@link Allows}. Type is used to infer
* capabilities of the {@link Delegation}.
*/
export type JoinAllows<T extends Allows, U extends Allows> = {
[K in keyof T | keyof U]: JoinAbilities<
K extends keyof T ? (T[K] extends ResourceAbilities ? T[K] : {}) : {},
K extends keyof U ? (U[K] extends ResourceAbilities ? U[K] : {}) : {}
>
}

/**
* Utility type that infers set of capabilities delegated by one or more
* {@link Delegation}s in UCAN 0.10 format.
*/
export type InferAllowedFromDelegations<T extends [unknown, ...unknown[]]> =
T extends [infer A]
? InferAllowedFromDelegation<A>
: T extends [infer A, infer B]
? JoinAllows<InferAllowedFromDelegation<A>, InferAllowedFromDelegation<B>>
: T extends [infer A, infer B, ...infer Rest]
? JoinAllows<
InferAllowedFromDelegation<A>,
InferAllowedFromDelegations<[B, ...Rest]>
>
: never

/**
* Utility type that infers set of capabilities delegated by a single
* {@link Delegation}
*/
export type InferAllowedFromDelegation<T> = T extends Delegation<
infer Capabilities
>
? InferAllowedFromCapabilities<Capabilities>
: never

/**
* Utility type that infers set of capabilities in UCAN 0.10 format from a
* {@link Capability} tuple.
*/
export type InferAllowedFromCapabilities<T> = T extends [infer A]
? InferAllowedFromCapability<A>
: T extends [infer A, ...infer Rest]
? JoinAllows<
InferAllowedFromCapability<A>,
InferAllowedFromCapabilities<Rest>
>
: never

/**
* Utility type that infers set of capabilities in UCAN 0.10 format from a
* single {@link Capability}.
*/
export type InferAllowedFromCapability<T> = T extends Capability<
infer Can,
infer URI,
infer Caveats
>
? { [K in URI]: { [K in Can]: (Caveats & {})[] } }
: never

export type DelegationJSON<T extends Delegation = Delegation> = ToJSON<
T,
{
Expand Down