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

Design & Implement Validation Helpers #21

Closed
expede opened this issue Nov 30, 2021 · 14 comments
Closed

Design & Implement Validation Helpers #21

expede opened this issue Nov 30, 2021 · 14 comments
Assignees

Comments

@expede
Copy link
Member

expede commented Nov 30, 2021

Manual validation is a bad experience. We should provide a top level

// PSEUDOCODE

enum ValidationStatus = {
  Valid,
  Escalation,
  Unrecognized
}

// Passed to higher order functions
function compareAll(targetCap: MyCapability, proofCaps: Capability[]): ValidationStatus
function compareAny(targetCap: MyCapability, proofCap: Capability): ValidationStatus
function compare(targetCap: MyCapability, proofCap: MyCapability): ValidationStatus

interface Authorization<caps> {
  from: Did, // top-level issuer,
  to: Did, // top-level audience
  allowed: Capability<caps>[],
  unrecognized: object[]
}

interface FailureReason = "EXPIRED" | "TOO_EARLY" | {escelated: object} // ...

function validate(ucan: Ucan): Result<FailureReason, Authorization>

Here, compare can be expressed in terms of compareAll.

Compare All

Compares the focused capability against the entire list of proof capabilities. This is especially useful for cases of rights amplification, where more than one proof is needed.

All of the compare functions should get translated into compareAll under the hood

Compare Any

Especially useful when there's a version update or some backwards compatible change. Compares the focused capability one-at-a-time with the proof's capabilities, but checks against all of them.

Compare (Exact)

The usual case. Compare only if the capabilities are in the same namespace/semantics.

@expede
Copy link
Member Author

expede commented Nov 30, 2021

@matheus23 thoughts? (Other than the fact that I guarantee that the above code will fail to typecheck)

@matheus23
Copy link
Member

So I've been noodling a bit on this (as well as trying to implement e.g. the wnfs capability as defined in wnfs2 in some ways).

There's two difficult things to deal with when it comes to custom capabilities:

  1. I think we'd like validate in the success case to return enough information to be able to e.g. check private filesystem changes. For that we need to have all inumbers from the whole chain accumulated to be returned. The outermost capability doesn't contain all information what the user is actually capable of anymore, since it only contains one inumber.
    We could also just call this a special case and make validate work fine for most use cases and let it just return what capabilities the user has in the end.
  2. I think we want to have the implementation of custom capability checking versioned by ucan version. What I mean by that is: (1) we want the ts-ucans library to be able to handle multiple ucan versions and (2) if that's the case, then e.g. if we end up forcing another format for capabilities in ucan version 0.8.0, but there's a custom capability already in use for version 0.7.0, then that needs to also be able to parse the old version.
    What I'm thinking is that custom capability parsers should probably be indexed by ucan version as well?

@expede
Copy link
Member Author

expede commented Dec 2, 2021

cc @bgins

@expede
Copy link
Member Author

expede commented Dec 2, 2021

We could also just call this a special case and make validate work fine for most use cases and let it just return what capabilities the user has in the end.

Yeah, good point. This is a whole "parse don't validate". We should make the type return a Result or similar, which lets anyone plug into that functionality. Most people won't need that, but if we need it, others will, too. Perhaps it's one more layer in that hierarchy? But we can explore that in more detail when we get to the exact API. Since it's entirely possible to do, the question becomes ow to best expose that

@expede
Copy link
Member Author

expede commented Dec 2, 2021

What I'm thinking is that custom capability parsers should probably be indexed by ucan version as well?

Great idea — we can pass that into the checking interface by threading it for the validation functions. 👍

...alternately, we could automatically upgrade the format while validating. Feels like there may be more edge cases there, but it's less manual for the end developer

@matheus23
Copy link
Member

...alternately, we could automatically upgrade the format while validating. Feels like there may be more edge cases there, but it's less manual for the end developer

Yeah my thinking on this was that we can't update the format inside att, since we don't know much about its structure.
But I'll leave that as an issue for later #26

For now, I've tackled a reduced version of this. I.e. a "compare any" version. I can see ways of abstracting this further, but I'll wait for a good concrete use case before I'll abstract it in some possibly weird way 😛

  • ValidationStatus basically ended up as CapabilityResult<A> now
  • Something like Authorization<caps> is CapabilityWithInfo<A> now. Just per-capability (because from needs to be per-capability). And with some additional info.

I'll close this until we're ready to expand the API with some additional building blocks for custom capabilities ✌️

@expede expede reopened this Jan 27, 2022
@expede
Copy link
Member Author

expede commented Jan 27, 2022

Reopening, because it's still a live discussion with @Gozala

@Gozala
Copy link

Gozala commented Jan 28, 2022

Especially useful when there's a version update or some backwards compatible change. Compares the focused capability one-at-a-time with the proof's capabilities, but checks against all of them.

I'm bit confused about this given the following function signature:

function compareAny(targetCap: MyCapability, proofCap: Capability): ValidationStatus

Is it meant to be following instead ?

function compareAny(targetCap: MyCapability, proofCaps: Capability[]): ValidationStatus

@Gozala
Copy link

Gozala commented Jan 28, 2022

I'll focus on compareAll as all the other ones can be expressed with it. More specifically I think it would be a great idea to make ValidationStatus proper type union so that Valid case is able to provide information about which available capabilities (a.k.a proofCaps) met claimed capability (a.k.a targetCap).

Specifically I would imagine that in some cases claim may be met by multiple sub-sets of available capabilities, so that validator can check (each set) in the next link in the proof chain.

Additionally I like how @matheus23's design introduced capability parser which can filter out Unrecognized capabilities before even comparing things. That way campareAll can either say claim is valid or it escalates.

@Gozala
Copy link

Gozala commented Jan 28, 2022

I end up writing what I think would be a best API for this also including inline below

/**
 * Function checks if the claimed capability is met by given set of capaibilites. Note that function takes capability
 * views as opposed to raw capaibilites (This implies that raw JSON capabilites were succesfully parsed into known
 * capability with specific semantics).
 * 
 * Function returns either succesfull sets of proofs that support claimed capability or a set of escalation errors.
 * If claimed capability is unfunded, meaning give capabilities are not comparable result is still an error of empty
 * escalation set (Maybe we should consider union with 3 variants instead to make this more explicit)
 * 
 * Please note that it is possible that claim may be met by some capaibilites while at the same time it may
 * escalate constraint of others. In this case function still returns succesfull set of proofs discarding
 * violations (maybe it should not ?).
 * 
 * Note: succesfull result provides iterator of proofs. That allows constraint solver to be lazy as in
 * return succesfully as soon as first proof is found and defer finding other proofs until constraint
 * solver deciedes to explore other paths.
 */
declare function claim<C extends CapabilityView> (capability:C, given:C[]): Result<EscalationError<C>[], IterableIterator<Proof<C>>>


/**
 * Represents succesfully parsed capability. Idea is that user could provide capability parser that UCAN
 * library will use to filter out capaibilites that can be compared from the ones it is unable to
 * recognize.
 */
interface CapabilityView<C extends Capability = Capability> {
  capability: C
}

/**
 * Basically any JSON value, but could be refined further if desired
 */
interface Capability { [key:string]: string|number|boolean|null|Capability|Capability[] }



type Result<X, T> =
   | { ok: true, value: T }
   | { ok: false, value: X }

/**
 * Proof of a claimed capability, contains claimed capability (view) and subset of available
 * capaibilites that satisfy it.
 */
interface Proof<C> {
    claimed: C
    capaibilites: C[]
}

/**
 * Represents capability escalation and contains non empty set of
 * contstraint violations.
 */

export interface EscalationError<C> extends RangeError {
  readonly name: "EscalationError"

  /**
   * claimed capability
   */
  readonly claimed: C

  /**
   * escalated capability
   */
  readonly escalated: C

  /**
   * non empty set of constraint violations
   */
  readonly violations: ConstraintViolationError<C>[]
}

/**
 * Represents specific constraint violation by the claimed capability.
 */
export interface ConstraintViolationError<C> extends RangeError {
  readonly name: "ConstraintViolationError"
  /**
   * Constraint that was violated.
   */
  readonly claimed: Constraint<C>
  /**
   * Claim that exceeds imposed constraint.
   */
  readonly violated: Constraint<C>
}

/**
 * Represents violated constraint in the claim. Carrynig information about
 * which specific constraint.
 */
export interface Constraint<C> {
  readonly capability: C
  readonly name: string
  readonly value: unknown
}



// -----------------

/**
 * Function attempt to access capability from given UCAN view (which in nutshell is UCAN with attached capability parser
 * so it can map known capabilites to richer views and surface unknown capabilities). It returns is either successful result
 * with capability view and authorization proof chain or an error describing reason why requested capability is invaid.
 * 
 * Access internally utilized `claim` function and walks up the proof chain until it is able to proove that claim is unfounded.
 */

declare function access <C extends CapabilityView>(capability:Capability, ucan:UCANView<C>):Result<InvalidClaim<C>, Access<C>>


interface InvalidClaim<C> {
    readonly name: 'ExpriedClaim'
    readonly claim: C
    readonly by: DID
    // I know to is broken english but "from" is too ambigius as it can be "claim from gozala" or  "gozala claimed car from robox"
    readonly to: DID

    reason: UnfundedClaim<C> | ExpriedClaim<C> | InactiveClaim<C> | ViolatingClaim<C> | InvalidClaim<C>
}


interface ExpriedClaim<C> {
    readonly name: 'ExpriedClaim'
    readonly by: DID
    readonly to: DID

    readonly claim: C

    expiredAt: Time
}


interface InactiveClaim<C> {
    readonly name: 'InactiveClaim'
    readonly from: DID
    readonly to: DID

    readonly claim: C

    activeAt: Time
}

interface UnfundedClaim<C> {
    readonly name: 'UnfundedClaim'
    readonly claim: C

    readonly by: DID
    readonly to: DID
}

interface ViolatingClaim<C> {
    readonly name: 'ViolatingClaim'
    readonly from: DID
    readonly to: DID

    readonly claim: C
    readonly escalates: EscalationError<C>[]
}

interface Access<C> {
  ok: true
  capability: C
  to: DID
  proof: Authorization<C>
}

interface Authorization<C> {
    by: DID
    granted: C[]

    proof: Authorization<C> | null
}

interface DID {}
interface UCANView<C extends CapabilityView> {
  ucan: UCAN
  capabilityParser: CapabilityParser<C>

  // capabilities that parser was unable to parse
  unkownCapabilities: IterableIterator<Capability>
}

interface UCAN {}

interface CapabilityParser<C> {
    /**
     * Returns either succesfully parsed capability or unknown capability back
     */
    parse(capability:Capability): Result<Capability, C>
}


type Time = number

With such a claim in place compareAll is just a claim function that throws away proofs. And propose access is comparable with validate but in contract it provides complete proof chains.

Neither deal with unknown capabilities, I think it's better done in layer above which in my sketch is UCANView that can utilize parser idea from @matheus23 before checking any claims and if desired reference to unknown capabilities could be exposed from InvalidClaim<C> and Access<C> types as well.

@Gozala
Copy link

Gozala commented Jan 28, 2022

One other thing that I would like to try is to define CapabilityViews such that they will bind implementation to exercise them. So that instead of doing e.g.

if (validate(request.ucan).ok && canWriteTo(request.path, request.ucan)) {
   fs.write(request.path, request.content)
}

it could turn into

const access = access({ with: request.path, can: 'write' }, ucan)
if (access.ok) {
  access.capability.write(request.path, request.content)
}

@expede
Copy link
Member Author

expede commented Jan 28, 2022

I'm bit confused about this given the following function signature:

function compareAny(targetCap: MyCapability, proofCap: Capability): ValidationStatus

Is it meant to be following instead ?

function compareAny(targetCap: MyCapability, proofCaps: Capability[]): Validatio

Yeah, the naming could likely be better. This is very much a high level sketch.

From the initial description:

Here, compare can be expressed in terms of compareAll.
Compare All

Compares the focused capability against the entire list of proof capabilities. This is especially useful for cases of rights amplification, where more than one proof is needed.

All of the compare functions should get translated into compareAll under the hood
Compare Any

Especially useful when there's a version update or some backwards compatible change. Compares the focused capability one-at-a-time with the proof's capabilities, but checks against all of them.

Compare (Exact)

The usual case. Compare only if the capabilities are in the same namespace/semantics.

Or to put it another way:

  • compareAll is a one-to-many
  • compareAny (maybe compareOne is better?) is a one-to-one relationship
  • compare (compare exact) is compareOne, but filters out proof capabilities that have a different resource type (e.g. focused only on email capabilities, so that you don't have to write the ignoring matcher)

@matheus23
Copy link
Member

Hey @Gozala. First of all: Thank you for the work you're putting into this, I really appreciate it! It's good to have more eyes & brains on this problem.

I'm currently working on upgrading our haskell code to UCAN version 0.7/0.8, and I'm collecting thoughts from my experience there & from talking with you into another round of iteration on ts-ucan.

Some ideas I'm sure I'll straight up steal from you (:stuck_out_tongue_winking_eye:):

  1. Returning a concise, verifiable proof (or Authorization?) for each valid capability a UCAN has is a great idea. This should also enable checking revocation in the future, and I'm thinking if this could also fit the role of what's cached on a service per-UCAN.
  2. Actually using a recursive JSON type to represent capabilities.

Looking at your proposal, it looks like these are the things you really want to have in ts-ucan:

  1. Much more precise error reporting (which by the way - will also be helpful for our https://ucan.xyz/validator app)
  2. Rights amplification
  3. Better naming
  4. Parsing UCANs into domain-specific types earlier (UCANView, CapabilityView)

I'll be looking at these things, especially in the bigger view of where ts-ucan is right now and what we have. We could add the capability parsing to ts-ucan's Chained type, which is horribly named at the moment.
Although I'll probably postpone (2) for now.
Also, let me know if I'm missing something that you're trying to get at!

Is there anything in particular that's blocking you from proceeding @Gozala? The current API should be sufficient for implementing a service that accepts UCANs, although I might be missing something. Should we maybe get on a call to discuss this in real-time?

@jeffgca
Copy link
Contributor

jeffgca commented Jun 3, 2022

This is being implemented in PR #79

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants