-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #33 from ucan-wg/matheus23/custom-capabilities
Custom capabilities
- Loading branch information
Showing
18 changed files
with
1,238 additions
and
246 deletions.
There are no files selected for viewing
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,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 } : {}), | ||
} | ||
} |
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,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) | ||
} |
Oops, something went wrong.