Skip to content

Commit

Permalink
Merge pull request #33 from ucan-wg/matheus23/custom-capabilities
Browse files Browse the repository at this point in the history
Custom capabilities
  • Loading branch information
matheus23 authored Dec 17, 2021
2 parents 95d5991 + d31b2d1 commit 0324e88
Show file tree
Hide file tree
Showing 18 changed files with 1,238 additions and 246 deletions.
137 changes: 137 additions & 0 deletions src/attenuation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// https://whitepaper.fission.codes/access-control/ucan/jwt-authentication#attenuation
import { Capability, Ucan } from "./types"
import { Chained } from "./chained"
import * as util from "./util"


export interface CapabilitySemantics<A> {
/**
* Try to parse a capability into a representation used for
* delegation & returning in the `capabilities` call.
*
* If the capability doesn't seem to match the format expected
* for the capabilities with the semantics currently defined,
* return `null`.
*/
tryParsing(cap: Capability): A | null
/**
* This figures out whether a given `childCap` can be delegated from `parentCap`.
* There are three possible results with three return types respectively:
* - `A`: The delegation is possible and results in the rights returned.
* - `null`: The capabilities from `parentCap` and `childCap` are unrelated and can't be compared nor delegated.
* - `CapabilityEscalation<A>`: It's clear that `childCap` is meant to be delegated from `parentCap`, but there's a rights escalation.
*/
tryDelegating(parentCap: A, childCap: A): A | null | CapabilityEscalation<A>
// TODO builders
}


export interface CapabilityInfo {
originator: string // DID
expiresAt: number
notBefore?: number
}


export interface CapabilityWithInfo<A> {
info: CapabilityInfo
capability: A
}


export type CapabilityResult<A>
= CapabilityWithInfo<A>
| CapabilityEscalation<A>


export interface CapabilityEscalation<A> {
escalation: string // reason
capability: A // the capability that escalated rights
}

function isCapabilityEscalation<A>(obj: unknown): obj is CapabilityEscalation<A> {
return util.isRecord(obj)
&& util.hasProp(obj, "escalation") && typeof obj.escalation === "string"
&& util.hasProp(obj, "capability")
}


export function capabilities<A>(
ucan: Chained,
capability: CapabilitySemantics<A>,
): Iterable<CapabilityResult<A>> {

function* findParsingCaps(ucan: Ucan<never>): Iterable<CapabilityWithInfo<A>> {
const capInfo = parseCapabilityInfo(ucan)
for (const cap of ucan.payload.att) {
const parsedCap = capability.tryParsing(cap)
if (parsedCap != null) yield { info: capInfo, capability: parsedCap }
}
}

const delegate = (ucan: Ucan<never>, capabilitiesInProofs: () => Iterable<() => Iterable<CapabilityResult<A>>>) => {
return function* () {
for (const parsedChildCap of findParsingCaps(ucan)) {
let isCoveredByProof = false
for (const capabilitiesInProof of capabilitiesInProofs()) {
for (const parsedParentCap of capabilitiesInProof()) {
// pass through capability escalations from parents
if (isCapabilityEscalation(parsedParentCap)) {
yield parsedParentCap
} else {
// try figuring out whether we can delegate the capabilities from this to the parent
const delegated = capability.tryDelegating(parsedParentCap.capability, parsedChildCap.capability)
// if the capabilities *are* related, then this will be non-null
// otherwise we just continue looking
if (delegated != null) {
// we infer that the capability was meant to be delegated
isCoveredByProof = true
// it's still possible that that delegation was invalid, i.e. an escalation, though
if (isCapabilityEscalation(delegated)) {
yield delegated // which is an escalation
} else {
yield {
info: delegateCapabilityInfo(parsedChildCap.info, parsedParentCap.info),
capability: delegated
}
}
}
}
}
}
// If a capability can't be considered to be delegated by any of its proofs
// (or if there are no proofs),
// then we root its origin in the UCAN we're looking at.
if (!isCoveredByProof) {
yield parsedChildCap
}
}
}
}

return ucan.reduce(delegate)()
}

function delegateCapabilityInfo(childInfo: CapabilityInfo, parentInfo: CapabilityInfo): CapabilityInfo {
let notBefore = {}
if (childInfo.notBefore != null && parentInfo.notBefore != null) {
notBefore = { notBefore: Math.max(childInfo.notBefore, parentInfo.notBefore) }
} else if (parentInfo.notBefore != null) {
notBefore = { notBefore: parentInfo.notBefore }
} else {
notBefore = { notBefore: childInfo.notBefore }
}
return {
originator: parentInfo.originator,
expiresAt: Math.min(childInfo.expiresAt, parentInfo.expiresAt),
...notBefore,
}
}

function parseCapabilityInfo(ucan: Ucan<never>): CapabilityInfo {
return {
originator: ucan.payload.iss,
expiresAt: ucan.payload.exp,
...(ucan.payload.nbf != null ? { notBefore: ucan.payload.nbf } : {}),
}
}
162 changes: 162 additions & 0 deletions src/capability/wnfs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Capability } from "../types"
import { capabilities, CapabilityEscalation, CapabilitySemantics } from "../attenuation"
import { Chained } from "../chained"


export const wnfsCapLevels = {
"SUPER_USER": 0,
"OVERWRITE": -1,
"SOFT_DELETE": -2,
"REVISE": -3,
"CREATE": -4,
}

export type WnfsCap = keyof typeof wnfsCapLevels

export function isWnfsCap(obj: unknown): obj is WnfsCap {
return typeof obj === "string" && Object.keys(wnfsCapLevels).includes(obj)
}



/////////////////////////////
// Public WNFS Capabilities
/////////////////////////////


export interface WnfsPublicCapability {
user: string // e.g. matheus23.fission.name
publicPath: string[]
cap: WnfsCap
}

export const wnfsPublicSemantics: CapabilitySemantics<WnfsPublicCapability> = {

/**
* Example valid public wnfs capability:
* ```js
* {
* wnfs: "boris.fission.name/public/path/to/dir/or/file",
* cap: "OVERWRITE"
* }
* ```
*/
tryParsing(cap: Capability): WnfsPublicCapability | null {
if (typeof cap.wnfs !== "string" || !isWnfsCap(cap.cap)) return null

// remove trailing slash
const trimmed = cap.wnfs.endsWith("/") ? cap.wnfs.slice(0, -1) : cap.wnfs
const split = trimmed.split("/")
const user = split[0]
const publicPath = split.slice(2) // drop first two: matheus23.fission.name/public/keep/this
if (user == null || split[1] !== "public") return null
return {
user,
publicPath,
cap: cap.cap,
}
},

tryDelegating(parentCap: WnfsPublicCapability, childCap: WnfsPublicCapability): WnfsPublicCapability | null | CapabilityEscalation<WnfsPublicCapability> {
// need to delegate the same user's file system
if (childCap.user !== parentCap.user) return null

// must not escalate capability level
if (wnfsCapLevels[childCap.cap] > wnfsCapLevels[parentCap.cap]) {
return {
escalation: "Capability level escalation",
capability: childCap,
}
}

// parentCap path must be a prefix of childCap path
if (childCap.publicPath.length < parentCap.publicPath.length) {
return {
escalation: "WNFS Public path access escalation",
capability: childCap,
}
}

for (let i = 0; i < parentCap.publicPath.length; i++) {
if (childCap.publicPath[i] !== parentCap.publicPath[i]) {
return {
escalation: "WNFS Public path access escalation",
capability: childCap,
}
}
}

return childCap
},

}

export function wnfsPublicCapabilities(ucan: Chained) {
return capabilities(ucan, wnfsPublicSemantics)
}



/////////////////////////////
// Private WNFS Capabilities
/////////////////////////////


export interface WnfsPrivateCapability {
user: string
requiredINumbers: Set<string>
cap: WnfsCap
}

const wnfsPrivateSemantics: CapabilitySemantics<WnfsPrivateCapability> = {

/**
* Example valid private wnfs capability:
*
* ```js
* {
* wnfs: "boris.fission.name/private/fccXmZ8HYmpwxkvPSjwW9A",
* cap: "OVERWRITE"
* }
* ```
*/
tryParsing(cap: Capability): WnfsPrivateCapability | null {
if (typeof cap.wnfs !== "string" || !isWnfsCap(cap.cap)) return null

// split up "boris.fission.name/private/fccXmZ8HYmpwxkvPSjwW9A" into "<user>/private/<inumberBase64url>"
const split = cap.wnfs.split("/")
const user = split[0]
const inumberBase64url = split[2]

if (user == null || split[1] !== "private" || inumberBase64url == null) return null

return {
user,
requiredINumbers: new Set([inumberBase64url]),
cap: cap.cap,
}
},

tryDelegating<T extends WnfsPrivateCapability>(parentCap: T, childCap: T): T | null | CapabilityEscalation<WnfsPrivateCapability> {
// If the users don't match, these capabilities are unrelated.
if (childCap.user !== parentCap.user) return null

// This escalation *could* be wrong, but we shouldn't assume they're unrelated either.
if (wnfsCapLevels[childCap.cap] > wnfsCapLevels[parentCap.cap]) {
return {
escalation: "Capability level escalation",
capability: childCap,
}
}

return {
...childCap,
requiredINumbers: new Set([...childCap.requiredINumbers.values(), ...parentCap.requiredINumbers.values()]),
}
},

}

export function wnfsPrivateCapabilities(ucan: Chained) {
return capabilities(ucan, wnfsPrivateSemantics)
}
Loading

0 comments on commit 0324e88

Please sign in to comment.