From 77f3c7c82ed2058040e73921d50f384e0cb525d9 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 15 Aug 2024 11:28:14 +0200 Subject: [PATCH 01/37] refactor: move some things around --- server/handler.go | 14 +----- validator/authorization.go | 90 +------------------------------------- validator/capability.go | 12 +++++ validator/lib.go | 85 +++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 101 deletions(-) diff --git a/server/handler.go b/server/handler.go index 6ebbef0..2f9937a 100644 --- a/server/handler.go +++ b/server/handler.go @@ -6,7 +6,6 @@ import ( "github.com/storacha-network/go-ucanto/core/invocation" "github.com/storacha-network/go-ucanto/core/ipld" "github.com/storacha-network/go-ucanto/core/result" - "github.com/storacha-network/go-ucanto/did" "github.com/storacha-network/go-ucanto/server/transaction" "github.com/storacha-network/go-ucanto/ucan" "github.com/storacha-network/go-ucanto/validator" @@ -19,18 +18,7 @@ type HandlerFunc[C any, O, X ipld.Builder] func(capability ucan.Capability[C], i // when validation succeeds. func Provide[C any, O, X ipld.Builder](capability validator.CapabilityParser[C], handler HandlerFunc[C, O, X]) ServiceMethod[O, X] { return func(invocation invocation.Invocation, context InvocationContext) (transaction.Transaction[O, X], error) { - canIssue := func(capability ucan.Capability[C], issuer did.DID) bool { - anycap := ucan.NewCapability(capability.Can(), capability.With(), any(capability.Nb())) - return context.CanIssue(anycap, issuer) - } - - validateAuthorization := func(auth validator.Authorization[C]) result.Failure { - anycap := ucan.NewCapability(auth.Capability().Can(), auth.Capability().With(), any(auth.Capability().Nb())) - anyauth := validator.NewAuthorization(anycap) - return context.ValidateAuthorization(anyauth) - } - - vctx := validator.NewValidationContext(capability, canIssue, validateAuthorization) + vctx := validator.NewValidationContext(capability, context.CanIssue, context.ValidateAuthorization) authorization, err := validator.Access(invocation, vctx) if err != nil { diff --git a/validator/authorization.go b/validator/authorization.go index 3f4f0f4..9f99f1e 100644 --- a/validator/authorization.go +++ b/validator/authorization.go @@ -1,46 +1,9 @@ package validator import ( - "github.com/storacha-network/go-ucanto/core/delegation" - "github.com/storacha-network/go-ucanto/core/invocation" - "github.com/storacha-network/go-ucanto/core/result" - "github.com/storacha-network/go-ucanto/did" - "github.com/storacha-network/go-ucanto/principal" "github.com/storacha-network/go-ucanto/ucan" ) -// PrincipalParser provides verifier instances that can validate UCANs issued -// by a given principal. -type PrincipalParser interface { - Parse(str string) (principal.Verifier, error) -} - -type CanIssuer[Caveats any] interface { - // CanIssue informs validator whether given capability can be issued by a - // given DID or whether it needs to be delegated to the issuer. - CanIssue(capability ucan.Capability[Caveats], issuer did.DID) bool -} - -// CanIssue informs validator whether given capability can be issued by a -// given DID or whether it needs to be delegated to the issuer. -type CanIssueFunc[Caveats any] func(capability ucan.Capability[Caveats], issuer did.DID) bool - -type RevocationChecker[Caveats any] interface { - // ValidateAuthorization validates that the passed authorization has not been - // revoked. - ValidateAuthorization(auth Authorization[Caveats]) result.Failure -} - -// RevocationCheckerFunc validates the passed authorization and returns -// a result indicating validity. -type RevocationCheckerFunc[Caveats any] func(auth Authorization[Caveats]) result.Failure - -type ValidationContext[Caveats any] interface { - RevocationChecker[Caveats] - CanIssuer[Caveats] - Capability() CapabilityParser[Caveats] -} - type Authorization[Caveats any] interface { Capability() ucan.Capability[Caveats] } @@ -49,59 +12,10 @@ type authorization[Caveats any] struct { capability ucan.Capability[Caveats] } -func (a *authorization[Caveats]) Capability() ucan.Capability[Caveats] { +func (a authorization[Caveats]) Capability() ucan.Capability[Caveats] { return a.capability } func NewAuthorization[Caveats any](capability ucan.Capability[Caveats]) Authorization[Caveats] { - return &authorization[Caveats]{capability: capability} -} - -type source struct { - capability ucan.Capability[any] -} - -func (s *source) Capability() ucan.Capability[any] { - return s.capability -} - -func (s *source) Delegation() delegation.Delegation { - return nil -} - -type validationContext[Caveats any] struct { - capability CapabilityParser[Caveats] - canIssue CanIssueFunc[Caveats] - validateAuthorization RevocationCheckerFunc[Caveats] -} - -func (vc *validationContext[Caveats]) CanIssue(capability ucan.Capability[Caveats], issuer did.DID) bool { - return vc.canIssue(capability, issuer) -} - -func (vc *validationContext[Caveats]) ValidateAuthorization(auth Authorization[Caveats]) result.Failure { - return vc.validateAuthorization(auth) -} - -func (vc *validationContext[Caveats]) Capability() CapabilityParser[Caveats] { - return vc.capability -} - -var _ ValidationContext[any] = (*validationContext[any])(nil) - -func NewValidationContext[Caveats any](capability CapabilityParser[Caveats], canIssue CanIssueFunc[Caveats], validateAuthorization RevocationCheckerFunc[Caveats]) ValidationContext[Caveats] { - vc := validationContext[Caveats]{capability, canIssue, validateAuthorization} - return &vc -} - -func Access[Caveats any](invocation invocation.Invocation, context ValidationContext[Caveats]) (result.Result[Authorization[Caveats], result.Failure], error) { - cap := invocation.Capabilities()[0] - src := source{capability: cap} - - // TODO: parser.Select() - match := context.Capability().Match(&src) - - return result.MapOk(match, func(o ucan.Capability[Caveats]) Authorization[Caveats] { - return &authorization[Caveats]{capability: o} - }), nil + return authorization[Caveats]{capability: capability} } diff --git a/validator/capability.go b/validator/capability.go index 54d2819..0550ce8 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -12,6 +12,18 @@ type Source interface { Delegation() delegation.Delegation } +type source struct { + capability ucan.Capability[any] +} + +func (s source) Capability() ucan.Capability[any] { + return s.capability +} + +func (s source) Delegation() delegation.Delegation { + return nil +} + type CapabilityParser[Caveats any] interface { Can() ucan.Ability // New creates a new capability from the passed options. diff --git a/validator/lib.go b/validator/lib.go index 19ce12e..300efd1 100644 --- a/validator/lib.go +++ b/validator/lib.go @@ -1,10 +1,95 @@ package validator import ( + "github.com/storacha-network/go-ucanto/core/invocation" + "github.com/storacha-network/go-ucanto/core/result" "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/principal" "github.com/storacha-network/go-ucanto/ucan" ) func IsSelfIssued[Caveats any](capability ucan.Capability[Caveats], issuer did.DID) bool { return capability.With() == issuer.DID().String() } + +// PrincipalParser provides verifier instances that can validate UCANs issued +// by a given principal. +type PrincipalParser interface { + Parse(str string) (principal.Verifier, error) +} + +type CanIssuer[Caveats any] interface { + // CanIssue informs validator whether given capability can be issued by a + // given DID or whether it needs to be delegated to the issuer. + CanIssue(capability ucan.Capability[Caveats], issuer did.DID) bool +} + +// CanIssue informs validator whether given capability can be issued by a +// given DID or whether it needs to be delegated to the issuer. +type CanIssueFunc[Caveats any] func(capability ucan.Capability[Caveats], issuer did.DID) bool + +type RevocationChecker[Caveats any] interface { + // ValidateAuthorization validates that the passed authorization has not been + // revoked. + ValidateAuthorization(auth Authorization[Caveats]) result.Failure +} + +// RevocationCheckerFunc validates the passed authorization and returns +// a result indicating validity. +type RevocationCheckerFunc[Caveats any] func(auth Authorization[Caveats]) result.Failure + +type ClaimContext interface { + RevocationChecker[any] + CanIssuer[any] +} + +type ValidationContext[Caveats any] interface { + RevocationChecker[any] + CanIssuer[any] + Capability() CapabilityParser[Caveats] +} + +type validationContext[Caveats any] struct { + capability CapabilityParser[Caveats] + canIssue CanIssueFunc[any] + validateAuthorization RevocationCheckerFunc[any] +} + +func (vc validationContext[Caveats]) CanIssue(capability ucan.Capability[any], issuer did.DID) bool { + return vc.canIssue(capability, issuer) +} + +func (vc validationContext[Caveats]) ValidateAuthorization(auth Authorization[any]) result.Failure { + return vc.validateAuthorization(auth) +} + +func (vc validationContext[Caveats]) Capability() CapabilityParser[Caveats] { + return vc.capability +} + +func NewValidationContext[Caveats any](capability CapabilityParser[Caveats], canIssue CanIssueFunc[any], validateAuthorization RevocationCheckerFunc[any]) ValidationContext[Caveats] { + return validationContext[Caveats]{capability, canIssue, validateAuthorization} +} + +// Access finds a valid path in a proof chain of the given `invocation` by +// exploring every possible option. On success an `Authorization` object is +// returned that illustrates the valid path. If no valid path is found +// `Unauthorized` error is returned detailing all explored paths and where they +// proved to fail. +func Access[Caveats any](invocation invocation.Invocation, context ValidationContext[Caveats]) (result.Result[Authorization[Caveats], result.Failure], error) { + cap := invocation.Capabilities()[0] + src := source{capability: cap} + + // TODO: parser.Select() + match := context.Capability().Match(src) + + return result.MapOk(match, func(o ucan.Capability[Caveats]) Authorization[Caveats] { + return authorization[Caveats]{capability: o} + }), nil +} + +// Claim attempts to find a valid proof chain for the claimed `capability` given +// set of `proofs`. On success an `Authorization` object with detailed proof +// chain is returned and on failure `Unauthorized` error is returned with +// details on paths explored and why they have failed. +// func Claim() From fc485545b4bfb8c1a0c4bfe72940de3c66a2dbe0 Mon Sep 17 00:00:00 2001 From: hannahhoward Date: Mon, 29 Jul 2024 18:56:51 -0700 Subject: [PATCH 02/37] feat(validator): add error models add all errors from JS Ucanto --- ucan/capability.go | 18 ++ ucan/ucan.go | 14 +- validator/datamodel/errors.go | 63 +++++ validator/datamodel/errors.ipldsch | 25 ++ validator/error.go | 391 +++++++++++++++++++++++++++++ validator/util.go | 38 +++ 6 files changed, 546 insertions(+), 3 deletions(-) create mode 100644 validator/datamodel/errors.go create mode 100644 validator/datamodel/errors.ipldsch create mode 100644 validator/error.go create mode 100644 validator/util.go diff --git a/ucan/capability.go b/ucan/capability.go index a07e036..ab3ca17 100644 --- a/ucan/capability.go +++ b/ucan/capability.go @@ -1,5 +1,15 @@ package ucan +import ( + "encoding/json" +) + +type jsonModel struct { + With Resource `json:"with"` + Can Ability `json:"can"` + Nb interface{} `json:"nb,omitempty"` +} + type capability[T any] struct { can Ability nb T @@ -20,6 +30,14 @@ func (c *capability[T]) With() Resource { return c.with } +func (c *capability[T]) MarshalJSON() ([]byte, error) { + return json.Marshal(jsonModel{ + With: c.with, + Can: c.can, + Nb: c.nb, + }) +} + func NewCapability[Caveats any](can Ability, with Resource, nb Caveats) Capability[Caveats] { return &capability[Caveats]{ can: can, diff --git a/ucan/ucan.go b/ucan/ucan.go index 5ef1de4..2870718 100644 --- a/ucan/ucan.go +++ b/ucan/ucan.go @@ -1,6 +1,8 @@ package ucan import ( + "encoding/json" + "github.com/ipld/go-ipld-prime" "github.com/storacha-network/go-ucanto/did" "github.com/storacha-network/go-ucanto/ucan/crypto" @@ -15,11 +17,17 @@ type Resource = string // It MUST have format `${string}/${string}` | "*" type Ability = string +// UnknownCapability is a capability whose Nb type is unknown +type UnknownCapability interface { + json.Marshaler + Can() Ability + With() Resource +} + // Capability represents an ability that a UCAN holder can perform with some // resource. type Capability[Caveats any] interface { - Can() Ability - With() Resource + UnknownCapability Nb() Caveats } @@ -35,7 +43,7 @@ type Link = ipld.Link // It MUST have format `${number}.${number}.${number}` type Version = string -// UTCUnixTimestamp is a timestamp in seconds since the Unix epoch. +// UTCUnixTimestamp is a timestamp in milliseconds since the Unix epoch. type UTCUnixTimestamp = uint64 // https://github.com/ucan-wg/spec/#324-nonce diff --git a/validator/datamodel/errors.go b/validator/datamodel/errors.go new file mode 100644 index 0000000..995a8ca --- /dev/null +++ b/validator/datamodel/errors.go @@ -0,0 +1,63 @@ +package datamodel + +import ( + // for go:embed + _ "embed" + "fmt" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/schema" +) + +//go:embed errors.ipldsch +var errorsch []byte + +var ( + errorTypeSystem *schema.TypeSystem +) + +func init() { + ts, err := ipld.LoadSchemaBytes(errorsch) + if err != nil { + panic(fmt.Errorf("failed to load IPLD schema: %s", err)) + } + errorTypeSystem = ts +} + +func InvalidAudienceType() schema.Type { + return errorTypeSystem.TypeByName("InvalidAudience") +} + +type Delegation struct { + Audience string +} + +type InvalidAudienceModel struct { + Name *string + Audience string + Delegation Delegation + Message string + Stack *string +} + +type ExpiredModel struct { + Name *string + Message string + ExpiredAt int64 + Stack *string +} + +func ExpiredType() schema.Type { + return errorTypeSystem.TypeByName("Expired") +} + +type NotValidBeforeModel struct { + Name *string + Message string + ValidAt int64 + Stack *string +} + +func NotValidBeforeType() schema.Type { + return errorTypeSystem.TypeByName("NotValidBefore") +} diff --git a/validator/datamodel/errors.ipldsch b/validator/datamodel/errors.ipldsch new file mode 100644 index 0000000..fdd20fc --- /dev/null +++ b/validator/datamodel/errors.ipldsch @@ -0,0 +1,25 @@ +type Delegation struct { + Audience String +} + +type InvalidAudience struct { + Name optional String + Audience String + Delegation Delegation + Message String + Stack optional Sstring +} + +type Expired struct { + Name optional String + Message String + ExpiredAt Integer + Stack optional String +} + +type NotValidBefore struct { + Name optional String + Message String + ValidAt Integer + Stack optional String +} diff --git a/validator/error.go b/validator/error.go new file mode 100644 index 0000000..50e8047 --- /dev/null +++ b/validator/error.go @@ -0,0 +1,391 @@ +package validator + +import ( + "encoding/json" + "errors" + "fmt" + "math" + "strings" + "time" + + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/bindnode" + "github.com/web3-storage/go-ucanto/core/delegation" + "github.com/web3-storage/go-ucanto/core/result" + "github.com/web3-storage/go-ucanto/did" + "github.com/web3-storage/go-ucanto/ucan" + vdm "github.com/web3-storage/go-ucanto/validator/datamodel" +) + +// go hack for union type -- unexported method cannot be implemented outside module limiting satisfying types +type DelegationSubError interface { + error + isDelegationSubError() +} + +type InvalidProofError interface { + error + isInvalidProofError() +} + +type EscalatedCapabilityError[Caveats any] struct { + result.NamedWithStackTrace + claimed ucan.Capability[Caveats] + delegated interface{} + cause error +} + +func NewEscalatedCapabilityError[Caveats any](claimed ucan.Capability[Caveats], delegated interface{}, cause error) error { + return EscalatedCapabilityError[Caveats]{result.NamedWithCurrentStackTrace("EscalatedCapability"), claimed, delegated, cause} +} + +func (ece EscalatedCapabilityError[Caveats]) Unwrap() error { + return ece.cause +} + +func (ece EscalatedCapabilityError[Caveats]) Error() string { + return fmt.Sprintf("Constraint violation: %s", ece.cause.Error()) +} + +func (ece EscalatedCapabilityError[Caveats]) isDelegationSubError() { +} + +/** + * @implements {API.DelegationError} + */ +type DelegationError struct { + result.NamedWithStackTrace + causes []DelegationSubError + context interface{} +} + +func NewDelegationError(causes []DelegationSubError, context interface{}) error { + return DelegationError{result.NamedWithCurrentStackTrace("InvalidClaim"), causes, context} +} + +func (de DelegationError) Error() string { + return fmt.Sprintf("Cannot derive %s from delegated capabilities: %s", de.context, errors.Join(de.Unwrap()...).Error()) +} + +func (de DelegationError) Unwrap() []error { + errs := make([]error, 0, len(de.causes)) + for _, cause := range de.causes { + errs = append(errs, cause) + } + return errs +} + +func (de DelegationError) isDelegationSubError() {} + +type SessionEscalationError struct { + result.NamedWithStackTrace + delegation delegation.Delegation + cause error +} + +func NewSessionEscalationError(delegation delegation.Delegation, cause error) error { + return SessionEscalationError{result.NamedWithCurrentStackTrace("SessionEscalation"), delegation, cause} +} + +func (see SessionEscalationError) Error() string { + issuer := see.delegation.Issuer().DID() + return strings.Join([]string{ + fmt.Sprintf("Delegation %s issued by %s has an invalid session", see.delegation.Link(), issuer), + li(see.cause.Error()), + }, "\n") +} + +func (see SessionEscalationError) isInvalidProofError() {} + +type InvalidSignatureError struct { + result.NamedWithStackTrace + delegation delegation.Delegation + verifier ucan.Verifier +} + +func NewInvalidSignatureError(delegation delegation.Delegation, verifier ucan.Verifier) error { + return InvalidSignatureError{result.NamedWithCurrentStackTrace("InvalidSignature"), delegation, verifier} +} + +func (ise InvalidSignatureError) Issuer() ucan.Principal { + return ise.delegation.Issuer() +} +func (ise InvalidSignatureError) Audience() ucan.Principal { + return ise.delegation.Audience() +} + +func (ise InvalidSignatureError) Error() string { + issuer := ise.Issuer().DID() + key := ise.verifier.DID() + if !strings.HasPrefix(issuer.String(), "did:key") { + return fmt.Sprintf(`Proof %s does not has a valid signature from %s`, ise.delegation.Link(), key) + } + return strings.Join([]string{ + fmt.Sprintf("Proof %s issued by %s does not has a valid signature from %s", ise.delegation.Link(), issuer, key), + " ℹ️ Probably issuer signed with a different key, which got rotated, invalidating delegations that were issued with prior keys", + }, "\n") +} + +func (ise InvalidSignatureError) isInvalidProofError() {} + +type UnavailableProofError struct { + result.NamedWithStackTrace + link ucan.Link + cause error +} + +func NewUnavailableProofError(link ucan.Link, cause error) error { + return UnavailableProofError{result.NamedWithCurrentStackTrace("UnavailableProof"), link, cause} +} + +func (upe UnavailableProofError) Unwrap() error { + return upe.cause +} + +func (upe UnavailableProofError) Error() string { + messages := []string{ + fmt.Sprintf("Linked proof '%s' is not included and could not be resolved", upe.link), + } + if upe.cause != nil { + messages = append(messages, li(fmt.Sprintf("Proof resolution failed with: %s", upe.cause.Error()))) + } + return strings.Join(messages, "\n") +} + +func (upe UnavailableProofError) isInvalidProofError() {} + +type DIDKeyResolutionError struct { + result.NamedWithStackTrace + did did.DID + cause error +} + +func NewDIDKeyResolutionError(did did.DID, cause error) error { + return DIDKeyResolutionError{result.NamedWithCurrentStackTrace("DIDKeyResolutionError"), did, cause} +} + +func (dkre DIDKeyResolutionError) Unwrap() error { + return dkre.cause +} + +func (dkre DIDKeyResolutionError) Error() string { + return fmt.Sprintf("Unable to resolve '%s' key", dkre.did) +} + +func (dkre DIDKeyResolutionError) isInvalidProofError() {} + +type PrincipalAlignmentError struct { + result.NamedWithStackTrace + audience ucan.Principal + delegation delegation.Delegation +} + +func NewPrincipalAlignmentError(audience ucan.Principal, delegation delegation.Delegation) error { + return PrincipalAlignmentError{result.NamedWithCurrentStackTrace("InvalidAudience"), audience, delegation} +} + +func (pae PrincipalAlignmentError) Error() string { + return fmt.Sprintf("Delegation audience is '%s' instead of '%s'", pae.delegation.Audience().DID(), pae.audience.DID()) +} + +func (pae PrincipalAlignmentError) ToIPLD() datamodel.Node { + name := pae.Name() + stack := pae.Stack() + invalidAudienceModel := vdm.InvalidAudienceModel{ + Name: &name, + Audience: pae.audience.DID().String(), + Delegation: vdm.Delegation{Audience: pae.delegation.Audience().DID().String()}, + Message: pae.Error(), + Stack: &stack, + } + return bindnode.Wrap(&invalidAudienceModel, vdm.InvalidAudienceType()) +} + +func (pae PrincipalAlignmentError) isInvalidProofError() {} + +type MalformedCapabilityError[Caveats any] struct { + result.NamedWithStackTrace + capability ucan.Capability[Caveats] + cause error +} + +func NewMalformedCapabilityError[Caveats any](capability ucan.Capability[Caveats], cause error) error { + return MalformedCapabilityError[Caveats]{result.NamedWithCurrentStackTrace("MalformedCapability"), capability, cause} +} + +func (mce MalformedCapabilityError[Caveats]) Error() string { + capabilityJSON, _ := json.Marshal(mce.capability) + return strings.Join([]string{ + fmt.Sprintf("Encountered malformed '%s' capability: %s", mce.capability.Can(), string(capabilityJSON)), + li(mce.cause.Error()), + }, "\n") +} + +func (mce MalformedCapabilityError[Caveats]) isDelegationSubError() {} + +type UnknownCapabilityError[Caveats any] struct { + result.NamedWithStackTrace + capability ucan.Capability[Caveats] +} + +func NewUnknownCapabilityError[Caveats any](capability ucan.Capability[Caveats]) error { + return UnknownCapabilityError[Caveats]{result.NamedWithCurrentStackTrace("UnknownCapability"), capability} +} + +func (uce UnknownCapabilityError[Caveats]) Error() string { + capabilityJSON, _ := json.Marshal(uce.capability) + return fmt.Sprintf("Encountered unknown capability: %s", string(capabilityJSON)) +} + +func (uce UnknownCapabilityError[Caveats]) isDelegationSubError() {} + +type ExpiredError struct { + result.NamedWithStackTrace + delegation delegation.Delegation +} + +func NewExpiredError(delegation delegation.Delegation) error { + return ExpiredError{result.NamedWithCurrentStackTrace("Expired"), delegation} +} + +func (ee ExpiredError) Error() string { + return fmt.Sprintf("Proof %s has expired on %s", ee.delegation.Link(), + time.UnixMilli(int64(ee.delegation.Expiration())).Format(time.RFC3339)) +} + +func (ee ExpiredError) ToIPLD() datamodel.Node { + name := ee.Name() + stack := ee.Stack() + expiredModel := vdm.ExpiredModel{ + Name: &name, + Message: ee.Error(), + ExpiredAt: int64(ee.delegation.Expiration()), + Stack: &stack, + } + return bindnode.Wrap(expiredModel, vdm.ExpiredType()) +} + +func (ee ExpiredError) isInvalidProofError() {} + +type RevokedError struct { + result.NamedWithStackTrace + delegation delegation.Delegation +} + +func NewRevokedError(delegation delegation.Delegation) error { + return RevokedError{result.NamedWithCurrentStackTrace("Revoked"), delegation} +} + +func (re RevokedError) Error() string { + return fmt.Sprintf("Proof %s has been revoked", re.delegation.Link()) +} + +func (re RevokedError) isInvalidProofError() {} + +type NotValidBeforeError struct { + result.NamedWithStackTrace + delegation delegation.Delegation +} + +func NewNotValidBeforeERror(delegation delegation.Delegation) error { + return NotValidBeforeError{result.NamedWithCurrentStackTrace("NotValidBefore"), delegation} +} + +func (nvbe NotValidBeforeError) Error() string { + return fmt.Sprintf("Proof %s is not valid before %s", nvbe.delegation.Link(), + time.UnixMilli(int64(nvbe.delegation.NotBefore())).Format(time.RFC3339)) +} + +func (nvbe NotValidBeforeError) ToIPLD() datamodel.Node { + name := nvbe.Name() + stack := nvbe.Stack() + notValidBeforeModel := vdm.NotValidBeforeModel{ + Name: &name, + Message: nvbe.Error(), + ValidAt: int64(nvbe.delegation.NotBefore()), + Stack: &stack, + } + return bindnode.Wrap(notValidBeforeModel, vdm.NotValidBeforeType()) +} + +func (nvbe NotValidBeforeError) isInvalidProofError() {} + +// TODO: this may just be the concrete type from the implementation once +// the rest of the validator is done +type InvalidClaim interface { + result.NamedWithStackTrace + error + Issuer() ucan.Principal + Delegation() delegation.Delegation +} + +type UnauthorizedError[Caveats any] struct { + result.NamedWithStackTrace + capability ucan.Capability[Caveats] + delegationErrors []DelegationError + // this is a hack... it will allow you to make an array of capabilities of different types + unknownCapabilities []ucan.UnknownCapability + invalidProofs []InvalidProofError + failedProofs []InvalidClaim +} + +func NewUnauthorizedError[Caveats any]( + capability ucan.Capability[Caveats], + delegationErrors []DelegationError, + unknownCapabilities []ucan.UnknownCapability, + invalidProofs []InvalidProofError, + failedProofs []InvalidClaim, +) error { + return UnauthorizedError[Caveats]{ + result.NamedWithCurrentStackTrace("Unauthorized"), + capability, + delegationErrors, + unknownCapabilities, + invalidProofs, + failedProofs, + } +} + +func (ue UnauthorizedError[Caveats]) Error() string { + errorStrings := make([]string, 0, len(ue.failedProofs)+len(ue.delegationErrors)+len(ue.invalidProofs)) + + for _, failedProof := range ue.failedProofs { + errorStrings = append(errorStrings, li(failedProof.Error())) + } + + for _, delegationError := range ue.delegationErrors { + errorStrings = append(errorStrings, li(delegationError.Error())) + } + + for _, invalidProof := range ue.invalidProofs { + errorStrings = append(errorStrings, li(invalidProof.Error())) + } + + unknowns := make([]string, 0, len(ue.unknownCapabilities)) + for _, unknownCapability := range ue.unknownCapabilities { + out, _ := unknownCapability.MarshalJSON() + unknowns = append(unknowns, li(string(out))) + } + + finalList := make([]string, 0, 2+int(math.Min(1, float64(len(errorStrings))))) + finalList = append(finalList, fmt.Sprintf("Claim %+v is not authorized", ue.capability)) + if len(errorStrings) > 0 { + finalList = append(finalList, errorStrings...) + } else { + finalList = append(finalList, li("No matching delegated capability found")) + } + if len(unknowns) > 0 { + finalList = append(finalList, li(fmt.Sprintf("Encountered unknown capabilities\n%s", strings.Join(unknowns, "\n")))) + } + + return strings.Join(finalList, "\n") +} + +func indent(message string) string { + indent := " " + return indent + strings.Join(strings.Split(message, "\n"), "\n$"+indent) +} + +func li(message string) string { + return indent("- " + message) +} diff --git a/validator/util.go b/validator/util.go new file mode 100644 index 0000000..f5e2969 --- /dev/null +++ b/validator/util.go @@ -0,0 +1,38 @@ +package validator + +func combine[T any](dataset [][]T) [][]T { + first, rest := dataset[0], dataset[1:] + results := make([][]T, 0, len(first)) + for _, value := range first { + results = append(results, []T{value}) + } + for _, values := range rest { + tuples := results + results = make([][]T, 0, len(tuples)) + for _, value := range values { + for _, tuple := range tuples { + newTuple := make([]T, len(tuple), len(tuple)+1) + _ = copy(newTuple, tuple) + results = append(results, append(newTuple, value)) + } + } + } + return results +} + +func intersection[T comparable](left []T, right []T) []T { + set := make([]T, 0) + hash := make(map[T]struct{}) + + for _, v := range left { + hash[v] = struct{}{} + } + + for _, v := range right { + if _, ok := hash[v]; ok { + set = append(set, v) + } + } + + return set +} From 37e2c10373134a56f64e7af22ba23f4b517264e6 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Fri, 16 Aug 2024 15:50:26 +0200 Subject: [PATCH 03/37] feat: progress --- core/delegation/delegation.go | 29 +------ server/handler.go | 13 ++- server/options.go | 31 ++++++++ server/server.go | 55 +++++++++---- ucan/lib.go | 8 +- ucan/view.go | 20 +++-- validator/error.go | 32 ++++---- validator/lib.go | 146 +++++++++++++++++++++++++++++----- 8 files changed, 244 insertions(+), 90 deletions(-) diff --git a/core/delegation/delegation.go b/core/delegation/delegation.go index d6349c3..48f33cc 100644 --- a/core/delegation/delegation.go +++ b/core/delegation/delegation.go @@ -23,51 +23,30 @@ import ( // into a UCAN token and used as proof for an invocation or further delegations. type Delegation interface { ipld.View + ucan.UCAN // Link returns the IPLD link of the root block of the delegation. Link() ucan.Link // Archive writes the delegation to a Content Addressed aRchive (CAR). Archive() io.Reader - // Issuer is the signer of the UCAN. - Issuer() ucan.Principal - // Audience is the principal delegated to. - Audience() ucan.Principal - // Version is the spec version the UCAN conforms to. - Version() ucan.Version - // Capabilities are claimed abilities that can be performed on a resource. - Capabilities() []ucan.Capability[any] - // Expiration is the time in seconds since the Unix epoch that the UCAN - // becomes invalid. - Expiration() ucan.UTCUnixTimestamp - // NotBefore is the time in seconds since the Unix epoch that the UCAN - // becomes valid. - NotBefore() ucan.UTCUnixTimestamp - // Nonce is a randomly generated string to provide a unique UCAN. - Nonce() ucan.Nonce - // Facts are arbitrary facts and proofs of knowledge. - Facts() []ucan.Fact - // Proofs of delegation. - Proofs() []ucan.Link - // Signature of the UCAN issuer. - Signature() signature.SignatureView } type delegation struct { rt ipld.Block blks blockstore.BlockReader - ucan ucan.UCANView + ucan ucan.View once sync.Once } var _ Delegation = (*delegation)(nil) -func (d *delegation) data() ucan.UCANView { +func (d *delegation) data() ucan.View { d.once.Do(func() { data := udm.UCANModel{} err := block.Decode(d.rt, &data, udm.Type(), cbor.Codec, sha256.Hasher) if err != nil { fmt.Printf("Error: decoding UCAN: %s\n", err) } - d.ucan, err = ucan.NewUCANView(&data) + d.ucan, err = ucan.NewUCAN(&data) if err != nil { fmt.Printf("Error: constructing UCAN view: %s\n", err) } diff --git a/server/handler.go b/server/handler.go index 2f9937a..3ad9705 100644 --- a/server/handler.go +++ b/server/handler.go @@ -18,7 +18,14 @@ type HandlerFunc[C any, O, X ipld.Builder] func(capability ucan.Capability[C], i // when validation succeeds. func Provide[C any, O, X ipld.Builder](capability validator.CapabilityParser[C], handler HandlerFunc[C, O, X]) ServiceMethod[O, X] { return func(invocation invocation.Invocation, context InvocationContext) (transaction.Transaction[O, X], error) { - vctx := validator.NewValidationContext(capability, context.CanIssue, context.ValidateAuthorization) + vctx := validator.NewValidationContext( + capability, + context.CanIssue, + context.ValidateAuthorization, + context.ResolveProof, + context.ParsePrincipal, + context.ResolveDIDKey, + ) authorization, err := validator.Access(invocation, vctx) if err != nil { @@ -27,8 +34,8 @@ func Provide[C any, O, X ipld.Builder](capability validator.CapabilityParser[C], return result.MatchResultR2(authorization, func(ok validator.Authorization[C]) (transaction.Transaction[O, X], error) { return handler(ok.Capability(), invocation, context) - }, func(err result.Failure) (transaction.Transaction[O, X], error) { - if failure, ok := err.(X); ok { + }, func(err validator.UnauthorizedError[C]) (transaction.Transaction[O, X], error) { + if failure, ok := any(err).(X); ok { return transaction.NewTransaction(result.Error[O](failure)), nil } return nil, fmt.Errorf("error was not an IPLD builder") diff --git a/server/options.go b/server/options.go index 59b85a0..72bc8a9 100644 --- a/server/options.go +++ b/server/options.go @@ -17,6 +17,9 @@ type srvConfig struct { service map[string]ServiceMethod[ipld.Builder, ipld.Builder] validateAuthorization validator.RevocationCheckerFunc[any] canIssue validator.CanIssueFunc[any] + resolveProof validator.ProofResolverFunc + parsePrincipal validator.PrincipalParserFunc + resolveDIDKey validator.PrincipalResolverFunc catch ErrorHandlerFunc } @@ -78,3 +81,31 @@ func WithCanIssue(fn validator.CanIssueFunc[any]) Option { return nil } } + +// WithProofResolver configures a function that finds delegations corresponding +// to a given link. If a resolver is not provided the validator may not be able +// to explore corresponding path within a proof chain. +func WithProofResolver(fn validator.ProofResolverFunc) Option { + return func(cfg *srvConfig) error { + cfg.resolveProof = fn + return nil + } +} + +// WithPrincipalParser configures a function that provides verifier instances +// that can validate UCANs issued by a given principal. +func WithPrincipalParser(fn validator.PrincipalParserFunc) Option { + return func(cfg *srvConfig) error { + cfg.parsePrincipal = fn + return nil + } +} + +// WithPrincipalResolver configures a function that resolves the key of a +// principal that is identified by DID different from did:key method. +func WithPrincipalResolver(fn validator.PrincipalResolverFunc) Option { + return func(cfg *srvConfig) error { + cfg.resolveDIDKey = fn + return nil + } +} diff --git a/server/server.go b/server/server.go index f982f33..7c41e11 100644 --- a/server/server.go +++ b/server/server.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/storacha-network/go-ucanto/core/dag/blockstore" + "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/invocation" "github.com/storacha-network/go-ucanto/core/invocation/ran" "github.com/storacha-network/go-ucanto/core/ipld" @@ -29,9 +30,11 @@ import ( type InvocationContext interface { validator.RevocationChecker[any] validator.CanIssuer[any] + validator.ProofResolver + validator.PrincipalParser + validator.PrincipalResolver // ID is the DID of the service the invocation was sent to. ID() principal.Signer - Principal() validator.PrincipalParser } // ServiceMethod is an invocation handler. @@ -100,43 +103,63 @@ func NewServer(id principal.Signer, options ...Option) (ServerView, error) { } } - ctx := &context{id: id, canIssue: canIssue, principal: &principalParser{}} - svr := &server{id: id, service: cfg.service, context: ctx, codec: codec, catch: catch} + resolveProof := cfg.resolveProof + if resolveProof == nil { + resolveProof = validator.ProofUnavailable + } + + parsePrincipal := cfg.parsePrincipal + if parsePrincipal == nil { + parsePrincipal = ParsePrincipal + } + + resolveDIDKey := cfg.resolveDIDKey + if resolveDIDKey == nil { + resolveDIDKey = validator.FailDIDKeyResolution + } + + ctx := context{id, canIssue, validateAuthorization, resolveProof, parsePrincipal, resolveDIDKey} + svr := &server{id, cfg.service, ctx, codec, catch} return svr, nil } -type principalParser struct{} - -func (p *principalParser) Parse(str string) (principal.Verifier, error) { +func ParsePrincipal(str string) (principal.Verifier, error) { + // TODO: Ed or RSA return verifier.Parse(str) } -var _ validator.PrincipalParser = (*principalParser)(nil) - type context struct { id principal.Signer canIssue validator.CanIssueFunc[any] - principal validator.PrincipalParser validateAuthorization validator.RevocationCheckerFunc[any] + resolveProof validator.ProofResolverFunc + parsePrincipal validator.PrincipalParserFunc + resolveDIDKey validator.PrincipalResolverFunc } -func (ctx *context) ID() principal.Signer { +func (ctx context) ID() principal.Signer { return ctx.id } -func (ctx *context) CanIssue(capability ucan.Capability[any], issuer did.DID) bool { +func (ctx context) CanIssue(capability ucan.Capability[any], issuer did.DID) bool { return ctx.canIssue(capability, issuer) } -func (ctx *context) Principal() validator.PrincipalParser { - return ctx.principal +func (ctx context) ValidateAuthorization(auth validator.Authorization[any]) result.Failure { + return ctx.validateAuthorization(auth) } -func (ctx *context) ValidateAuthorization(auth validator.Authorization[any]) result.Failure { - return ctx.validateAuthorization(auth) +func (ctx context) ResolveProof(proof ucan.Link) result.Result[delegation.Delegation, validator.UnavailableProofError] { + return ctx.resolveProof(proof) } -var _ InvocationContext = (*context)(nil) +func (ctx context) ParsePrincipal(str string) (principal.Verifier, error) { + return ctx.parsePrincipal(str) +} + +func (ctx context) ResolveDIDKey(did did.DID) result.Result[did.DID, validator.DIDKeyResolutionError] { + return ctx.resolveDIDKey(did) +} type server struct { id principal.Signer diff --git a/ucan/lib.go b/ucan/lib.go index 37e5be0..ac3b7ac 100644 --- a/ucan/lib.go +++ b/ucan/lib.go @@ -76,7 +76,7 @@ type FactBuilder = MapBuilder // Issue creates a new signed token with a given issuer. If expiration is // not set it defaults to 30 seconds from now. -func Issue(issuer Signer, audience Principal, capabilities []Capability[ipld.Builder], options ...Option) (UCANView, error) { +func Issue(issuer Signer, audience Principal, capabilities []Capability[ipld.Builder], options ...Option) (View, error) { cfg := ucanConfig{} for _, opt := range options { if err := opt(&cfg); err != nil { @@ -163,7 +163,7 @@ func Issue(issuer Signer, audience Principal, capabilities []Capability[ipld.Bui if cfg.nbf != 0 { model.Nbf = &cfg.nbf } - return NewUCANView(&model) + return NewUCAN(&model) } func encodeSignaturePayload(header *hdm.HeaderModel, payload *pdm.PayloadModel) ([]byte, error) { @@ -175,12 +175,12 @@ func encodeSignaturePayload(header *hdm.HeaderModel, payload *pdm.PayloadModel) } // IsExpired checks if a UCAN is expired. -func IsExpired(ucan UCANView) bool { +func IsExpired(ucan UCAN) bool { return ucan.Expiration() <= Now() } // IsTooEarly checks if a UCAN is not active yet. -func IsTooEarly(ucan UCANView) bool { +func IsTooEarly(ucan UCAN) bool { nbf := ucan.NotBefore() return nbf != 0 && Now() <= nbf } diff --git a/ucan/view.go b/ucan/view.go index e8868fd..fdd6e57 100644 --- a/ucan/view.go +++ b/ucan/view.go @@ -8,11 +8,7 @@ import ( udm "github.com/storacha-network/go-ucanto/ucan/datamodel/ucan" ) -// UCANView represents a decoded "view" of a UCAN that can be used in your -// domain logic, etc. -type UCANView interface { - // Model references the underlying IPLD datamodel instance. - Model() *udm.UCANModel +type UCAN interface { // Issuer is the signer of the UCAN. Issuer() Principal // Audience is the principal delegated to. @@ -37,11 +33,19 @@ type UCANView interface { Signature() signature.SignatureView } +// View represents a decoded "view" of a UCAN that can be used in your +// domain logic, etc. +type View interface { + UCAN + // Model references the underlying IPLD datamodel instance. + Model() *udm.UCANModel +} + type ucanView struct { model *udm.UCANModel } -var _ UCANView = (*ucanView)(nil) +var _ View = (*ucanView)(nil) func (v *ucanView) Audience() Principal { did, err := did.Decode(v.model.Aud) @@ -114,7 +118,7 @@ func (v *ucanView) Version() string { return v.model.V } -// NewUCANView creates a UCAN view from the underlying data model. Please note +// NewUCAN creates a UCAN view from the underlying data model. Please note // that this function does no verification of the model and it is callers // responsibility to ensure that: // @@ -124,6 +128,6 @@ func (v *ucanView) Version() string { // // In other words you should never use this function unless you've parsed or // decoded a valid UCAN and want to wrap it into a view. -func NewUCANView(model *udm.UCANModel) (UCANView, error) { +func NewUCAN(model *udm.UCANModel) (View, error) { return &ucanView{model}, nil } diff --git a/validator/error.go b/validator/error.go index 50e8047..ba7d6b4 100644 --- a/validator/error.go +++ b/validator/error.go @@ -9,12 +9,12 @@ import ( "time" "github.com/ipld/go-ipld-prime/datamodel" - "github.com/ipld/go-ipld-prime/node/bindnode" - "github.com/web3-storage/go-ucanto/core/delegation" - "github.com/web3-storage/go-ucanto/core/result" - "github.com/web3-storage/go-ucanto/did" - "github.com/web3-storage/go-ucanto/ucan" - vdm "github.com/web3-storage/go-ucanto/validator/datamodel" + "github.com/storacha-network/go-ucanto/core/delegation" + "github.com/storacha-network/go-ucanto/core/ipld" + "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/ucan" + vdm "github.com/storacha-network/go-ucanto/validator/datamodel" ) // go hack for union type -- unexported method cannot be implemented outside module limiting satisfying types @@ -134,7 +134,7 @@ type UnavailableProofError struct { cause error } -func NewUnavailableProofError(link ucan.Link, cause error) error { +func NewUnavailableProofError(link ucan.Link, cause error) UnavailableProofError { return UnavailableProofError{result.NamedWithCurrentStackTrace("UnavailableProof"), link, cause} } @@ -160,7 +160,7 @@ type DIDKeyResolutionError struct { cause error } -func NewDIDKeyResolutionError(did did.DID, cause error) error { +func NewDIDKeyResolutionError(did did.DID, cause error) DIDKeyResolutionError { return DIDKeyResolutionError{result.NamedWithCurrentStackTrace("DIDKeyResolutionError"), did, cause} } @@ -180,7 +180,7 @@ type PrincipalAlignmentError struct { delegation delegation.Delegation } -func NewPrincipalAlignmentError(audience ucan.Principal, delegation delegation.Delegation) error { +func NewPrincipalAlignmentError(audience ucan.Principal, delegation delegation.Delegation) PrincipalAlignmentError { return PrincipalAlignmentError{result.NamedWithCurrentStackTrace("InvalidAudience"), audience, delegation} } @@ -188,7 +188,7 @@ func (pae PrincipalAlignmentError) Error() string { return fmt.Sprintf("Delegation audience is '%s' instead of '%s'", pae.delegation.Audience().DID(), pae.audience.DID()) } -func (pae PrincipalAlignmentError) ToIPLD() datamodel.Node { +func (pae PrincipalAlignmentError) Build() (datamodel.Node, error) { name := pae.Name() stack := pae.Stack() invalidAudienceModel := vdm.InvalidAudienceModel{ @@ -198,7 +198,7 @@ func (pae PrincipalAlignmentError) ToIPLD() datamodel.Node { Message: pae.Error(), Stack: &stack, } - return bindnode.Wrap(&invalidAudienceModel, vdm.InvalidAudienceType()) + return ipld.WrapWithRecovery(&invalidAudienceModel, vdm.InvalidAudienceType()) } func (pae PrincipalAlignmentError) isInvalidProofError() {} @@ -209,7 +209,7 @@ type MalformedCapabilityError[Caveats any] struct { cause error } -func NewMalformedCapabilityError[Caveats any](capability ucan.Capability[Caveats], cause error) error { +func NewMalformedCapabilityError[Caveats any](capability ucan.Capability[Caveats], cause error) MalformedCapabilityError[Caveats] { return MalformedCapabilityError[Caveats]{result.NamedWithCurrentStackTrace("MalformedCapability"), capability, cause} } @@ -253,7 +253,7 @@ func (ee ExpiredError) Error() string { time.UnixMilli(int64(ee.delegation.Expiration())).Format(time.RFC3339)) } -func (ee ExpiredError) ToIPLD() datamodel.Node { +func (ee ExpiredError) Build() (datamodel.Node, error) { name := ee.Name() stack := ee.Stack() expiredModel := vdm.ExpiredModel{ @@ -262,7 +262,7 @@ func (ee ExpiredError) ToIPLD() datamodel.Node { ExpiredAt: int64(ee.delegation.Expiration()), Stack: &stack, } - return bindnode.Wrap(expiredModel, vdm.ExpiredType()) + return ipld.WrapWithRecovery(expiredModel, vdm.ExpiredType()) } func (ee ExpiredError) isInvalidProofError() {} @@ -296,7 +296,7 @@ func (nvbe NotValidBeforeError) Error() string { time.UnixMilli(int64(nvbe.delegation.NotBefore())).Format(time.RFC3339)) } -func (nvbe NotValidBeforeError) ToIPLD() datamodel.Node { +func (nvbe NotValidBeforeError) Build() (datamodel.Node, error) { name := nvbe.Name() stack := nvbe.Stack() notValidBeforeModel := vdm.NotValidBeforeModel{ @@ -305,7 +305,7 @@ func (nvbe NotValidBeforeError) ToIPLD() datamodel.Node { ValidAt: int64(nvbe.delegation.NotBefore()), Stack: &stack, } - return bindnode.Wrap(notValidBeforeModel, vdm.NotValidBeforeType()) + return ipld.WrapWithRecovery(notValidBeforeModel, vdm.NotValidBeforeType()) } func (nvbe NotValidBeforeError) isInvalidProofError() {} diff --git a/validator/lib.go b/validator/lib.go index 300efd1..7ff407d 100644 --- a/validator/lib.go +++ b/validator/lib.go @@ -1,6 +1,9 @@ package validator import ( + "fmt" + + "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/invocation" "github.com/storacha-network/go-ucanto/core/result" "github.com/storacha-network/go-ucanto/did" @@ -12,12 +15,44 @@ func IsSelfIssued[Caveats any](capability ucan.Capability[Caveats], issuer did.D return capability.With() == issuer.DID().String() } +func ProofUnavailable(p ucan.Link) result.Result[delegation.Delegation, UnavailableProofError] { + return result.Error[delegation.Delegation](NewUnavailableProofError(p, fmt.Errorf("no proof resolver configured"))) +} + +func FailDIDKeyResolution(d did.DID) result.Result[did.DID, DIDKeyResolutionError] { + return result.Error[did.DID](NewDIDKeyResolutionError(d, fmt.Errorf("no DID resolver configured"))) +} + // PrincipalParser provides verifier instances that can validate UCANs issued // by a given principal. type PrincipalParser interface { - Parse(str string) (principal.Verifier, error) + ParsePrincipal(str string) (principal.Verifier, error) } +type PrincipalParserFunc = func(str string) (principal.Verifier, error) + +// PrincipalResolver is used to resolve a key of the principal that is +// identified by DID different from did:key method. It can be passed into a +// UCAN validator in order to augmented it with additional DID methods support. +type PrincipalResolver interface { + ResolveDIDKey(did did.DID) result.Result[did.DID, DIDKeyResolutionError] +} + +// PrincipalResolverFunc resolves the key of a principal that is identified by +// DID different from did:key method. +type PrincipalResolverFunc = func(did did.DID) result.Result[did.DID, DIDKeyResolutionError] + +// ProofResolver finds a delegations when external proof links are present in +// UCANs. If a resolver is not provided the validator may not be able to explore +// corresponding path within a proof chain. +type ProofResolver interface { + // Resolve finds a delegation corresponding to an external proof link. + ResolveProof(proof ucan.Link) result.Result[delegation.Delegation, UnavailableProofError] +} + +// Resolve finds a delegation corresponding to an external proof link. +type ProofResolverFunc = func(proof ucan.Link) result.Result[delegation.Delegation, UnavailableProofError] + type CanIssuer[Caveats any] interface { // CanIssue informs validator whether given capability can be issued by a // given DID or whether it needs to be delegated to the issuer. @@ -41,11 +76,13 @@ type RevocationCheckerFunc[Caveats any] func(auth Authorization[Caveats]) result type ClaimContext interface { RevocationChecker[any] CanIssuer[any] + ProofResolver + PrincipalParser + PrincipalResolver } type ValidationContext[Caveats any] interface { - RevocationChecker[any] - CanIssuer[any] + ClaimContext Capability() CapabilityParser[Caveats] } @@ -53,6 +90,13 @@ type validationContext[Caveats any] struct { capability CapabilityParser[Caveats] canIssue CanIssueFunc[any] validateAuthorization RevocationCheckerFunc[any] + resolveProof ProofResolverFunc + parsePrincipal PrincipalParserFunc + resolveDIDKey PrincipalResolverFunc +} + +func (vc validationContext[Caveats]) Capability() CapabilityParser[Caveats] { + return vc.capability } func (vc validationContext[Caveats]) CanIssue(capability ucan.Capability[any], issuer did.DID) bool { @@ -63,12 +107,34 @@ func (vc validationContext[Caveats]) ValidateAuthorization(auth Authorization[an return vc.validateAuthorization(auth) } -func (vc validationContext[Caveats]) Capability() CapabilityParser[Caveats] { - return vc.capability +func (vc validationContext[Caveats]) ResolveProof(proof ucan.Link) result.Result[delegation.Delegation, UnavailableProofError] { + return vc.resolveProof(proof) +} + +func (vc validationContext[Caveats]) ParsePrincipal(str string) (principal.Verifier, error) { + return vc.parsePrincipal(str) +} + +func (vc validationContext[Caveats]) ResolveDIDKey(did did.DID) result.Result[did.DID, DIDKeyResolutionError] { + return vc.resolveDIDKey(did) } -func NewValidationContext[Caveats any](capability CapabilityParser[Caveats], canIssue CanIssueFunc[any], validateAuthorization RevocationCheckerFunc[any]) ValidationContext[Caveats] { - return validationContext[Caveats]{capability, canIssue, validateAuthorization} +func NewValidationContext[Caveats any]( + capability CapabilityParser[Caveats], + canIssue CanIssueFunc[any], + validateAuthorization RevocationCheckerFunc[any], + resolveProof ProofResolverFunc, + parsePrincipal PrincipalParserFunc, + resolveDIDKey PrincipalResolverFunc, +) ValidationContext[Caveats] { + return validationContext[Caveats]{ + capability, + canIssue, + validateAuthorization, + resolveProof, + parsePrincipal, + resolveDIDKey, + } } // Access finds a valid path in a proof chain of the given `invocation` by @@ -76,20 +142,64 @@ func NewValidationContext[Caveats any](capability CapabilityParser[Caveats], can // returned that illustrates the valid path. If no valid path is found // `Unauthorized` error is returned detailing all explored paths and where they // proved to fail. -func Access[Caveats any](invocation invocation.Invocation, context ValidationContext[Caveats]) (result.Result[Authorization[Caveats], result.Failure], error) { - cap := invocation.Capabilities()[0] - src := source{capability: cap} - - // TODO: parser.Select() - match := context.Capability().Match(src) - - return result.MapOk(match, func(o ucan.Capability[Caveats]) Authorization[Caveats] { - return authorization[Caveats]{capability: o} - }), nil +func Access[Caveats any](invocation invocation.Invocation, context ValidationContext[Caveats]) (result.Result[Authorization[Caveats], UnauthorizedError[Caveats]], error) { + prf := []delegation.Proof{delegation.FromDelegation(invocation)} + return Claim(context.Capability(), prf, context) } // Claim attempts to find a valid proof chain for the claimed `capability` given // set of `proofs`. On success an `Authorization` object with detailed proof // chain is returned and on failure `Unauthorized` error is returned with // details on paths explored and why they have failed. -// func Claim() +func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegation.Proof, context ClaimContext) (result.Result[Authorization[Caveats], UnauthorizedError[Caveats]], error) { + delegations, errors := resolveProofs(proofs, context) + + for _, d := range delegations { + // Validate each proof if valid add each capability to the list of sources. + // otherwise collect the error. + result.MatchResultR0( + validate(d, delegations, context), + ) + } + + // cap := invocation.Capabilities()[0] + + // var sources []Source + + // src := source{capability: cap} + + // // TODO: parser.Select() + // match := context.Capability().Match(src) + + // return result.MapOk(match, func(o ucan.Capability[Caveats]) Authorization[Caveats] { + // return authorization[Caveats]{capability: o} + // }), nil +} + +// resolveProofs takes `proofs` from the delegation which may contain +// a `Delegation` or a link to one and attempts to resolve links by side loading +// them. It returns a set of resolved `Delegation`s and errors for the proofs +// that could not be resolved. +func resolveProofs(proofs []delegation.Proof, resolver ProofResolver) (dels []delegation.Delegation, errs []UnavailableProofError) { + for _, p := range proofs { + d, ok := p.Delegation() + if ok { + dels = append(dels, d) + } else { + result.MatchResultR0( + resolver.ResolveProof(p.Link()), + func(d delegation.Delegation) { dels = append(dels, d) }, + func(err UnavailableProofError) { errs = append(errs, err) }, + ) + } + } + return +} + +// Validate a delegation to check it is within the time bound and that it is +// authorized by the issuer. +func validate(delegation delegation.Delegation, proofs []delegation.Delegation, context ClaimContext) { + if ucan.IsExpired(delegation) { + + } +} From 4770f7c4dcf015075a9af02106442f7d3712ef47 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 19 Aug 2024 18:33:13 +0200 Subject: [PATCH 04/37] feat: policy for capability validation --- core/delegation/delegation.go | 24 +++-- core/result/datamodel/failure.go | 37 ------- core/result/datamodel/failure.ipldsch | 5 - core/result/result.go | 74 ++------------ core/schema/did.go | 11 +- core/schema/did_test.go | 7 +- core/schema/schema.go | 20 ++-- core/schema/struct.go | 23 ++++- core/schema/struct_test.go | 9 +- principal/lib.go | 5 + server/error.go | 65 +++++------- server/handler.go | 1 + server/server.go | 9 +- transport/http.go | 4 +- ucan/crypto/signature/signature.go | 60 +++++++++++ ucan/formatter/formatter.go | 13 ++- ucan/lib.go | 49 +++++++-- validator/capability.go | 11 +- validator/error.go | 87 +++++++++------- validator/lib.go | 142 +++++++++++++++++++++++--- 20 files changed, 394 insertions(+), 262 deletions(-) delete mode 100644 core/result/datamodel/failure.go delete mode 100644 core/result/datamodel/failure.ipldsch diff --git a/core/delegation/delegation.go b/core/delegation/delegation.go index 48f33cc..7429c57 100644 --- a/core/delegation/delegation.go +++ b/core/delegation/delegation.go @@ -24,6 +24,8 @@ import ( type Delegation interface { ipld.View ucan.UCAN + // Data returns the UCAN view of the delegation. + Data() ucan.View // Link returns the IPLD link of the root block of the delegation. Link() ucan.Link // Archive writes the delegation to a Content Addressed aRchive (CAR). @@ -39,7 +41,7 @@ type delegation struct { var _ Delegation = (*delegation)(nil) -func (d *delegation) data() ucan.View { +func (d *delegation) Data() ucan.View { d.once.Do(func() { data := udm.UCANModel{} err := block.Decode(d.rt, &data, udm.Type(), cbor.Codec, sha256.Hasher) @@ -71,43 +73,43 @@ func (d *delegation) Archive() io.Reader { } func (d *delegation) Issuer() ucan.Principal { - return d.data().Issuer() + return d.Data().Issuer() } func (d *delegation) Audience() ucan.Principal { - return d.data().Audience() + return d.Data().Audience() } func (d *delegation) Version() ucan.Version { - return d.data().Version() + return d.Data().Version() } func (d *delegation) Capabilities() []ucan.Capability[any] { - return d.data().Capabilities() + return d.Data().Capabilities() } func (d *delegation) Expiration() ucan.UTCUnixTimestamp { - return d.data().Expiration() + return d.Data().Expiration() } func (d *delegation) NotBefore() ucan.UTCUnixTimestamp { - return d.data().NotBefore() + return d.Data().NotBefore() } func (d *delegation) Nonce() ucan.Nonce { - return d.data().Nonce() + return d.Data().Nonce() } func (d *delegation) Facts() []ucan.Fact { - return d.data().Facts() + return d.Data().Facts() } func (d *delegation) Proofs() []ucan.Link { - return d.data().Proofs() + return d.Data().Proofs() } func (d *delegation) Signature() signature.SignatureView { - return d.data().Signature() + return d.Data().Signature() } func NewDelegation(root ipld.Block, bs blockstore.BlockReader) Delegation { diff --git a/core/result/datamodel/failure.go b/core/result/datamodel/failure.go deleted file mode 100644 index 694ae32..0000000 --- a/core/result/datamodel/failure.go +++ /dev/null @@ -1,37 +0,0 @@ -package datamodel - -import ( - // to use go:embed - _ "embed" - "fmt" - - "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/schema" - ucanipld "github.com/storacha-network/go-ucanto/core/ipld" -) - -//go:embed failure.ipldsch -var failureSchema []byte - -// Failure is a generic failure -type Failure struct { - Name *string - Message string - Stack *string -} - -func (f *Failure) Build() (ipld.Node, error) { - return ucanipld.WrapWithRecovery(f, typ) -} - -var ( - typ schema.Type -) - -func init() { - ts, err := ipld.LoadSchemaBytes(failureSchema) - if err != nil { - panic(fmt.Errorf("loading failure schema: %w", err)) - } - typ = ts.TypeByName("Failure") -} diff --git a/core/result/datamodel/failure.ipldsch b/core/result/datamodel/failure.ipldsch deleted file mode 100644 index 243f336..0000000 --- a/core/result/datamodel/failure.ipldsch +++ /dev/null @@ -1,5 +0,0 @@ -type Failure struct { - name optional String - message String - stack optional String -} \ No newline at end of file diff --git a/core/result/result.go b/core/result/result.go index c990ee4..a2d816f 100644 --- a/core/result/result.go +++ b/core/result/result.go @@ -1,12 +1,9 @@ package result import ( - "fmt" - "runtime" - - "github.com/pkg/errors" "github.com/storacha-network/go-ucanto/core/ipld" - "github.com/storacha-network/go-ucanto/core/result/datamodel" + "github.com/storacha-network/go-ucanto/core/result/failure" + "github.com/storacha-network/go-ucanto/core/result/failure/datamodel" ) // Result is a golang compatible generic result type @@ -176,72 +173,19 @@ func Wrap[O any, X comparable](inner func() (O, X)) Result[O, X] { return Ok[O, X](o) } -// Named is an error that you can read a name from -type Named interface { - Name() string -} - -// WithStackTrace is an error that you can read a stack trace from -type WithStackTrace interface { - Stack() string -} - -// IPLDConvertableError is an error with a custom method to convert to an IPLD Node -type IPLDConvertableError interface { - error - ipld.Builder -} - -type Failure interface { - error - Named -} - -type NamedWithStackTrace interface { - Named - WithStackTrace -} - -type namedWithStackTrace struct { - name string - stack errors.StackTrace -} - -func (n namedWithStackTrace) Name() string { - return n.name -} - -func (n namedWithStackTrace) Stack() string { - return fmt.Sprintf("%+v", n.stack) -} - -func NamedWithCurrentStackTrace(name string) NamedWithStackTrace { - const depth = 32 - - var pcs [depth]uintptr - n := runtime.Callers(3, pcs[:]) - - f := make(errors.StackTrace, n) - for i := 0; i < n; i++ { - f[i] = errors.Frame(pcs[i]) - } - - return namedWithStackTrace{name, f} -} - func NewFailure(err error) Result[ipld.Builder, ipld.Builder] { - if ipldConvertableError, ok := err.(IPLDConvertableError); ok { + if ipldConvertableError, ok := err.(failure.IPLDConvertableError); ok { return Error[ipld.Builder, ipld.Builder](ipldConvertableError) } - failure := datamodel.Failure{Message: err.Error()} - if named, ok := err.(Named); ok { + model := datamodel.Failure{Message: err.Error()} + if named, ok := err.(failure.Named); ok { name := named.Name() - failure.Name = &name + model.Name = &name } - if withStackTrace, ok := err.(WithStackTrace); ok { + if withStackTrace, ok := err.(failure.WithStackTrace); ok { stack := withStackTrace.Stack() - failure.Stack = &stack + model.Stack = &stack } - return Error[ipld.Builder, ipld.Builder](&failure) + return Error[ipld.Builder, ipld.Builder](&model) } diff --git a/core/schema/did.go b/core/schema/did.go index 4f971c6..6883d67 100644 --- a/core/schema/did.go +++ b/core/schema/did.go @@ -2,30 +2,31 @@ package schema import ( "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/did" ) var didreader = reader[string, did.DID]{ - readFunc: func(input string) result.Result[did.DID, result.Failure] { + readFunc: func(input string) result.Result[did.DID, failure.Failure] { d, err := did.Parse(input) if err != nil { return result.Error[did.DID](NewSchemaError(err.Error())) } - return result.Ok[did.DID, result.Failure](d) + return result.Ok[did.DID, failure.Failure](d) }, } func DID() Reader[string, did.DID] { - return &didreader + return didreader } // DIDString read a string that is in DID format. func DIDString() Reader[string, string] { - return &didstrreader + return didstrreader } var didstrreader = reader[string, string]{ - readFunc: func(input string) result.Result[string, result.Failure] { + readFunc: func(input string) result.Result[string, failure.Failure] { return result.MapOk(DID().Read(input), func(id did.DID) string { return id.String() }) diff --git a/core/schema/did_test.go b/core/schema/did_test.go index d16d199..eeecf67 100644 --- a/core/schema/did_test.go +++ b/core/schema/did_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/did" "github.com/stretchr/testify/require" ) @@ -12,14 +13,14 @@ func TestReadDID(t *testing.T) { res := DID().Read("notadid") result.MatchResultR0(res, func(ok did.DID) { t.Fatalf("unexpectedly parsed a non-DID as a DID: %s", ok.String()) - }, func(err result.Failure) { + }, func(err failure.Failure) { require.Equal(t, err.Name(), "SchemaError") }) res = DID().Read("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") result.MatchResultR0(res, func(ok did.DID) { require.Equal(t, ok.DID().String(), "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") - }, func(err result.Failure) { + }, func(err failure.Failure) { t.Fatalf("unexpected error reading DID: %s", err) }) } @@ -28,7 +29,7 @@ func TestReadDIDString(t *testing.T) { res := DIDString().Read("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") result.MatchResultR0(res, func(ok string) { require.Equal(t, ok, "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") - }, func(err result.Failure) { + }, func(err failure.Failure) { t.Fatalf("unexpected error reading DID: %s", err) }) } diff --git a/core/schema/schema.go b/core/schema/schema.go index 8790f7e..5289645 100644 --- a/core/schema/schema.go +++ b/core/schema/schema.go @@ -4,17 +4,18 @@ import ( "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/storacha-network/go-ucanto/core/ipld" "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" ) type Reader[I, O any] interface { - Read(input I) result.Result[O, result.Failure] + Read(input I) result.Result[O, failure.Failure] } type reader[I, O any] struct { - readFunc func(input I) result.Result[O, result.Failure] + readFunc func(input I) result.Result[O, failure.Failure] } -func (r *reader[I, O]) Read(input I) result.Result[O, result.Failure] { +func (r reader[I, O]) Read(input I) result.Result[O, failure.Failure] { return r.readFunc(input) } @@ -22,15 +23,15 @@ type schemaerr struct { message string } -func (se *schemaerr) Name() string { +func (se schemaerr) Name() string { return "SchemaError" } -func (se *schemaerr) Error() string { +func (se schemaerr) Error() string { return se.message } -func (se *schemaerr) Build() (ipld.Node, error) { +func (se schemaerr) Build() (ipld.Node, error) { np := basicnode.Prototype.Any nb := np.NewBuilder() ma, err := nb.BeginMap(2) @@ -45,9 +46,6 @@ func (se *schemaerr) Build() (ipld.Node, error) { return nb.Build(), nil } -var _ result.Failure = (*schemaerr)(nil) -var _ ipld.Builder = (*schemaerr)(nil) - -func NewSchemaError(message string) result.Failure { - return &schemaerr{message} +func NewSchemaError(message string) failure.Failure { + return schemaerr{message} } diff --git a/core/schema/struct.go b/core/schema/struct.go index 48541b0..242b891 100644 --- a/core/schema/struct.go +++ b/core/schema/struct.go @@ -3,27 +3,40 @@ package schema import ( "github.com/ipld/go-ipld-prime/schema" "github.com/storacha-network/go-ucanto/core/ipld" + "github.com/storacha-network/go-ucanto/core/policy" "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" ) type strukt[T any] struct { - typ schema.Type + typ schema.Type + policy policy.Policy } -func (s *strukt[T]) Read(input any) result.Result[T, result.Failure] { +func (s strukt[T]) Read(input any) result.Result[T, failure.Failure] { node, ok := input.(ipld.Node) if !ok { return result.Error[T](NewSchemaError("unexpected input: not an IPLD node")) } + if s.policy != nil { + ok, err := policy.Match(s.policy, node) + if err != nil { + return result.Error[T](NewSchemaError(err.Error())) + } + if !ok { + return result.Error[T](NewSchemaError("input did not match policy")) + } + } + bind, err := ipld.Rebind[T](node, s.typ) if err != nil { return result.Error[T](NewSchemaError(err.Error())) } - return result.Ok[T, result.Failure](bind) + return result.Ok[T, failure.Failure](bind) } -func Struct[T any](typ schema.Type) Reader[any, T] { - return &strukt[T]{typ: typ} +func Struct[T any](typ schema.Type, policy policy.Policy) Reader[any, T] { + return strukt[T]{typ, policy} } diff --git a/core/schema/struct_test.go b/core/schema/struct_test.go index d6a21ae..f6ff21f 100644 --- a/core/schema/struct_test.go +++ b/core/schema/struct_test.go @@ -7,6 +7,7 @@ import ( "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/testing/helpers" "github.com/stretchr/testify/require" ) @@ -31,11 +32,11 @@ func TestReadStruct(t *testing.T) { ma.Finish() nd := nb.Build() - res := Struct[TestStruct](ts.TypeByName("TestStruct")).Read(nd) + res := Struct[TestStruct](ts.TypeByName("TestStruct"), nil).Read(nd) result.MatchResultR0(res, func(ok TestStruct) { fmt.Printf("%+v\n", ok) require.Equal(t, ok.Name, "foo") - }, func(err result.Failure) { + }, func(err failure.Failure) { t.Fatalf("unexpected error reading struct: %s", err) }) }) @@ -49,10 +50,10 @@ func TestReadStruct(t *testing.T) { ma.Finish() nd := nb.Build() - res := Struct[TestStruct](ts.TypeByName("TestStruct")).Read(nd) + res := Struct[TestStruct](ts.TypeByName("TestStruct"), nil).Read(nd) result.MatchResultR0(res, func(ok TestStruct) { t.Fatalf("unexpectedly read incompatible struct: %+v", ok) - }, func(err result.Failure) { + }, func(err failure.Failure) { fmt.Printf("%+v\n", err) require.Equal(t, err.Name(), "SchemaError") }) diff --git a/principal/lib.go b/principal/lib.go index ce13c9f..0b0d19e 100644 --- a/principal/lib.go +++ b/principal/lib.go @@ -4,6 +4,9 @@ import ( "github.com/storacha-network/go-ucanto/ucan" ) +// Signer is the principal that can issue UCANs (and sign payloads). While it's +// primary role is to sign payloads it also provides a `Verifier` interface so +// it can be used for verifying signed payloads as well. type Signer interface { ucan.Signer Code() uint64 @@ -11,6 +14,8 @@ type Signer interface { Encode() []byte } +// Verifier is the principal that issued a UCAN. In usually represents remote +// principal and is used to verify that certain payloads were signed by it. type Verifier interface { ucan.Verifier Code() uint64 diff --git a/server/error.go b/server/error.go index 8925bba..1f01c49 100644 --- a/server/error.go +++ b/server/error.go @@ -4,13 +4,13 @@ import ( "fmt" "github.com/storacha-network/go-ucanto/core/ipld" - "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" sdm "github.com/storacha-network/go-ucanto/server/datamodel" "github.com/storacha-network/go-ucanto/ucan" ) type HandlerNotFoundError[Caveats any] interface { - result.Failure + failure.Failure Capability() ucan.Capability[Caveats] } @@ -18,19 +18,19 @@ type handlerNotFoundError[Caveats any] struct { capability ucan.Capability[Caveats] } -func (h *handlerNotFoundError[C]) Capability() ucan.Capability[C] { +func (h handlerNotFoundError[C]) Capability() ucan.Capability[C] { return h.capability } -func (h *handlerNotFoundError[C]) Error() string { +func (h handlerNotFoundError[C]) Error() string { return fmt.Sprintf("service does not implement {can: \"%s\"} handler", h.capability.Can()) } -func (h *handlerNotFoundError[C]) Name() string { +func (h handlerNotFoundError[C]) Name() string { return "HandlerNotFoundError" } -func (h *handlerNotFoundError[C]) Build() (ipld.Node, error) { +func (h handlerNotFoundError[C]) Build() (ipld.Node, error) { name := h.Name() mdl := sdm.HandlerNotFoundErrorModel{ @@ -45,16 +45,13 @@ func (h *handlerNotFoundError[C]) Build() (ipld.Node, error) { return ipld.WrapWithRecovery(&mdl, sdm.HandlerNotFoundErrorType()) } -var _ HandlerNotFoundError[any] = (*handlerNotFoundError[any])(nil) -var _ ipld.Builder = (*handlerNotFoundError[any])(nil) - -func NewHandlerNotFoundError[Caveats any](capability ucan.Capability[Caveats]) *handlerNotFoundError[Caveats] { - return &handlerNotFoundError[Caveats]{capability} +func NewHandlerNotFoundError[Caveats any](capability ucan.Capability[Caveats]) HandlerNotFoundError[Caveats] { + return handlerNotFoundError[Caveats]{capability} } type HandlerExecutionError[Caveats any] interface { - result.Failure - result.WithStackTrace + failure.Failure + failure.WithStackTrace Cause() error Capability() ucan.Capability[Caveats] } @@ -64,41 +61,41 @@ type handlerExecutionError[Caveats any] struct { capability ucan.Capability[Caveats] } -func (h *handlerExecutionError[C]) Capability() ucan.Capability[C] { +func (h handlerExecutionError[C]) Capability() ucan.Capability[C] { return h.capability } -func (h *handlerExecutionError[C]) Cause() error { +func (h handlerExecutionError[C]) Cause() error { return h.cause } -func (h *handlerExecutionError[C]) Error() string { +func (h handlerExecutionError[C]) Error() string { return fmt.Sprintf("service handler {can: \"%s\"} error: %s", h.capability.Can(), h.cause.Error()) } -func (h *handlerExecutionError[C]) Name() string { +func (h handlerExecutionError[C]) Name() string { return "HandlerExecutionError" } -func (h *handlerExecutionError[C]) Stack() string { +func (h handlerExecutionError[C]) Stack() string { var stack string - if serr, ok := h.cause.(result.WithStackTrace); ok { + if serr, ok := h.cause.(failure.WithStackTrace); ok { stack = serr.Stack() } return stack } -func (h *handlerExecutionError[C]) Build() (ipld.Node, error) { +func (h handlerExecutionError[C]) Build() (ipld.Node, error) { name := h.Name() stack := h.Stack() var cname string - if ncause, ok := h.cause.(result.Named); ok { + if ncause, ok := h.cause.(failure.Named); ok { cname = ncause.Name() } var cstack string - if scause, ok := h.cause.(result.WithStackTrace); ok { + if scause, ok := h.cause.(failure.WithStackTrace); ok { cstack = scause.Stack() } @@ -120,15 +117,12 @@ func (h *handlerExecutionError[C]) Build() (ipld.Node, error) { return ipld.WrapWithRecovery(&mdl, sdm.HandlerExecutionErrorType()) } -var _ HandlerExecutionError[any] = (*handlerExecutionError[any])(nil) -var _ ipld.Builder = (*handlerExecutionError[any])(nil) - -func NewHandlerExecutionError[Caveats any](cause error, capability ucan.Capability[Caveats]) *handlerExecutionError[Caveats] { - return &handlerExecutionError[Caveats]{cause, capability} +func NewHandlerExecutionError[Caveats any](cause error, capability ucan.Capability[Caveats]) HandlerExecutionError[Caveats] { + return handlerExecutionError[Caveats]{cause, capability} } type InvocationCapabilityError interface { - result.Failure + failure.Failure Capabilities() []ucan.Capability[any] } @@ -136,19 +130,19 @@ type invocationCapabilityError struct { capabilities []ucan.Capability[any] } -func (i *invocationCapabilityError) Capabilities() []ucan.Capability[any] { +func (i invocationCapabilityError) Capabilities() []ucan.Capability[any] { return i.capabilities } -func (i *invocationCapabilityError) Error() string { +func (i invocationCapabilityError) Error() string { return "Invocation is required to have a single capability." } -func (i *invocationCapabilityError) Name() string { +func (i invocationCapabilityError) Name() string { return "InvocationCapabilityError" } -func (i *invocationCapabilityError) Build() (ipld.Node, error) { +func (i invocationCapabilityError) Build() (ipld.Node, error) { name := i.Name() var capmdls []sdm.CapabilityModel for _, cap := range i.Capabilities() { @@ -167,9 +161,6 @@ func (i *invocationCapabilityError) Build() (ipld.Node, error) { return ipld.WrapWithRecovery(&mdl, sdm.InvocationCapabilityErrorType()) } -var _ InvocationCapabilityError = (*invocationCapabilityError)(nil) -var _ ipld.Builder = (*invocationCapabilityError)(nil) - -func NewInvocationCapabilityError(capabilities []ucan.Capability[any]) *invocationCapabilityError { - return &invocationCapabilityError{capabilities} +func NewInvocationCapabilityError(capabilities []ucan.Capability[any]) InvocationCapabilityError { + return invocationCapabilityError{capabilities} } diff --git a/server/handler.go b/server/handler.go index 3ad9705..36948db 100644 --- a/server/handler.go +++ b/server/handler.go @@ -19,6 +19,7 @@ type HandlerFunc[C any, O, X ipld.Builder] func(capability ucan.Capability[C], i func Provide[C any, O, X ipld.Builder](capability validator.CapabilityParser[C], handler HandlerFunc[C, O, X]) ServiceMethod[O, X] { return func(invocation invocation.Invocation, context InvocationContext) (transaction.Transaction[O, X], error) { vctx := validator.NewValidationContext( + context.ID().Verifier(), capability, context.CanIssue, context.ValidateAuthorization, diff --git a/server/server.go b/server/server.go index 7c41e11..8965e7a 100644 --- a/server/server.go +++ b/server/server.go @@ -15,6 +15,7 @@ import ( "github.com/storacha-network/go-ucanto/core/message" "github.com/storacha-network/go-ucanto/core/receipt" "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/did" "github.com/storacha-network/go-ucanto/principal" "github.com/storacha-network/go-ucanto/principal/ed25519/verifier" @@ -98,7 +99,7 @@ func NewServer(id principal.Signer, options ...Option) (ServerView, error) { validateAuthorization := cfg.validateAuthorization if validateAuthorization == nil { - validateAuthorization = func(auth validator.Authorization[any]) result.Failure { + validateAuthorization = func(auth validator.Authorization[any]) failure.Failure { return nil } } @@ -145,11 +146,11 @@ func (ctx context) CanIssue(capability ucan.Capability[any], issuer did.DID) boo return ctx.canIssue(capability, issuer) } -func (ctx context) ValidateAuthorization(auth validator.Authorization[any]) result.Failure { +func (ctx context) ValidateAuthorization(auth validator.Authorization[any]) failure.Failure { return ctx.validateAuthorization(auth) } -func (ctx context) ResolveProof(proof ucan.Link) result.Result[delegation.Delegation, validator.UnavailableProofError] { +func (ctx context) ResolveProof(proof ucan.Link) result.Result[delegation.Delegation, validator.UnavailableProof] { return ctx.resolveProof(proof) } @@ -157,7 +158,7 @@ func (ctx context) ParsePrincipal(str string) (principal.Verifier, error) { return ctx.parsePrincipal(str) } -func (ctx context) ResolveDIDKey(did did.DID) result.Result[did.DID, validator.DIDKeyResolutionError] { +func (ctx context) ResolveDIDKey(did did.DID) result.Result[did.DID, validator.UnresolvedDID] { return ctx.resolveDIDKey(did) } diff --git a/transport/http.go b/transport/http.go index 7addfcf..1cb4634 100644 --- a/transport/http.go +++ b/transport/http.go @@ -4,7 +4,7 @@ import ( "io" "net/http" - "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" ) type HTTPRequest interface { @@ -19,7 +19,7 @@ type HTTPResponse interface { } type HTTPError interface { - result.Failure + failure.Failure Status() int Headers() http.Header } diff --git a/ucan/crypto/signature/signature.go b/ucan/crypto/signature/signature.go index b1d0909..d6fa708 100644 --- a/ucan/crypto/signature/signature.go +++ b/ucan/crypto/signature/signature.go @@ -2,11 +2,71 @@ package signature import ( "bytes" + "fmt" "github.com/multiformats/go-varint" ) +const NON_STANDARD = 0xd000 +const ES256K = 0xd0e7 +const BLS12381G1 = 0xd0ea +const BLS12381G2 = 0xd0eb const EdDSA = 0xd0ed +const ES256 = 0xd01200 +const ES384 = 0xd01201 +const ES512 = 0xd01202 +const RS256 = 0xd01205 +const EIP191 = 0xd191 + +func CodeName(code uint64) (string, error) { + switch code { + case ES256K: + return "ES256K", nil + case BLS12381G1: + return "BLS12381G1", nil + case BLS12381G2: + return "BLS12381G2", nil + case EdDSA: + return "EdDSA", nil + case ES256: + return "ES256", nil + case ES384: + return "ES384", nil + case ES512: + return "ES512", nil + case RS256: + return "RS256", nil + case EIP191: + return "EIP191", nil + default: + return "", fmt.Errorf("unknown signature algorithm code 0x%x", code) + } +} + +func NameCode(name string) (uint64, error) { + switch name { + case "ES256K": + return ES256K, nil + case "BLS12381G1": + return BLS12381G1, nil + case "BLS12381G2": + return BLS12381G2, nil + case "EdDSA": + return EdDSA, nil + case "ES256": + return ES256, nil + case "ES384": + return ES384, nil + case "ES512": + return ES512, nil + case "RS256": + return RS256, nil + case "EIP191": + return EIP191, nil + default: + return NON_STANDARD, nil + } +} type Signature interface { Code() uint64 diff --git a/ucan/formatter/formatter.go b/ucan/formatter/formatter.go index fc5a499..15a440a 100644 --- a/ucan/formatter/formatter.go +++ b/ucan/formatter/formatter.go @@ -11,8 +11,8 @@ import ( pdm "github.com/storacha-network/go-ucanto/ucan/datamodel/payload" ) -func FormatSignPayload(header *hdm.HeaderModel, payload *pdm.PayloadModel) (string, error) { - hdr, err := FormatHeader(header) +func FormatSignPayload(payload pdm.PayloadModel, version string, algorithm string) (string, error) { + hdr, err := FormatHeader(version, algorithm) if err != nil { return "", fmt.Errorf("formatting header: %s", hdr) } @@ -23,7 +23,12 @@ func FormatSignPayload(header *hdm.HeaderModel, payload *pdm.PayloadModel) (stri return fmt.Sprintf("%s.%s", hdr, pld), nil } -func FormatHeader(header *hdm.HeaderModel) (string, error) { +func FormatHeader(version string, algorithm string) (string, error) { + header := hdm.HeaderModel{ + Alg: algorithm, + Ucv: version, + Typ: "JWT", + } bytes, err := ipld.Marshal(dagjson.Encode, header, hdm.Type()) if err != nil { return "", fmt.Errorf("dag-json encoding header: %s", err) @@ -31,7 +36,7 @@ func FormatHeader(header *hdm.HeaderModel) (string, error) { return base64.RawURLEncoding.EncodeToString(bytes), nil } -func FormatPayload(payload *pdm.PayloadModel) (string, error) { +func FormatPayload(payload pdm.PayloadModel) (string, error) { bytes, err := ipld.Marshal(dagjson.Encode, payload, pdm.Type()) if err != nil { return "", fmt.Errorf("dag-json encoding payload: %s", err) diff --git a/ucan/lib.go b/ucan/lib.go index ac3b7ac..1c9e44c 100644 --- a/ucan/lib.go +++ b/ucan/lib.go @@ -5,8 +5,7 @@ import ( "time" "github.com/ipld/go-ipld-prime/datamodel" - "github.com/storacha-network/go-ucanto/core/ipld" - hdm "github.com/storacha-network/go-ucanto/ucan/datamodel/header" + "github.com/storacha-network/go-ucanto/ucan/crypto/signature" pdm "github.com/storacha-network/go-ucanto/ucan/datamodel/payload" udm "github.com/storacha-network/go-ucanto/ucan/datamodel/ucan" "github.com/storacha-network/go-ucanto/ucan/formatter" @@ -74,9 +73,14 @@ type MapBuilder interface { type FactBuilder = MapBuilder +// CaveatBuilder builds a datamodel.Node from the underlying data. +type CaveatBuilder interface { + Build() (datamodel.Node, error) +} + // Issue creates a new signed token with a given issuer. If expiration is // not set it defaults to 30 seconds from now. -func Issue(issuer Signer, audience Principal, capabilities []Capability[ipld.Builder], options ...Option) (View, error) { +func Issue(issuer Signer, audience Principal, capabilities []Capability[CaveatBuilder], options ...Option) (View, error) { cfg := ucanConfig{} for _, opt := range options { if err := opt(&cfg); err != nil { @@ -123,11 +127,6 @@ func Issue(issuer Signer, audience Principal, capabilities []Capability[ipld.Bui }) } - header := hdm.HeaderModel{ - Alg: issuer.SignatureAlgorithm(), - Ucv: version, - Typ: "JWT", - } payload := pdm.PayloadModel{ Iss: issuer.DID().String(), Aud: audience.DID().String(), @@ -142,7 +141,7 @@ func Issue(issuer Signer, audience Principal, capabilities []Capability[ipld.Bui if cfg.nbf != 0 { payload.Nbf = &cfg.nbf } - bytes, err := encodeSignaturePayload(&header, &payload) + bytes, err := encodeSignaturePayload(payload, version, issuer.SignatureAlgorithm()) if err != nil { return nil, fmt.Errorf("encoding signature payload: %s", err) } @@ -166,14 +165,42 @@ func Issue(issuer Signer, audience Principal, capabilities []Capability[ipld.Bui return NewUCAN(&model) } -func encodeSignaturePayload(header *hdm.HeaderModel, payload *pdm.PayloadModel) ([]byte, error) { - str, err := formatter.FormatSignPayload(header, payload) +func encodeSignaturePayload(payload pdm.PayloadModel, version string, algorithm string) ([]byte, error) { + str, err := formatter.FormatSignPayload(payload, version, algorithm) if err != nil { return nil, err } return []byte(str), nil } +func VerifySignature(ucan View, verifier Verifier) (bool, error) { + alg, err := signature.CodeName(ucan.Signature().Code()) + if err != nil { + return false, err + } + + var prfstrs []string + for _, link := range ucan.Proofs() { + prfstrs = append(prfstrs, link.String()) + } + + payload := pdm.PayloadModel{ + Iss: ucan.Issuer().DID().String(), + Aud: ucan.Audience().DID().String(), + Att: ucan.Model().Att, + Prf: prfstrs, + Exp: ucan.Expiration(), + Fct: ucan.Model().Fct, + } + + msg, err := encodeSignaturePayload(payload, ucan.Version(), alg) + if err != nil { + return false, err + } + + return ucan.Issuer().DID() == verifier.DID() && verifier.Verify(msg, ucan.Signature()), nil +} + // IsExpired checks if a UCAN is expired. func IsExpired(ucan UCAN) bool { return ucan.Expiration() <= Now() diff --git a/validator/capability.go b/validator/capability.go index 0550ce8..8b46650 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -3,6 +3,7 @@ package validator import ( "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/ucan" ) @@ -28,7 +29,7 @@ type CapabilityParser[Caveats any] interface { Can() ucan.Ability // New creates a new capability from the passed options. New(with ucan.Resource, nb Caveats) ucan.Capability[Caveats] - Match(source Source) result.Result[ucan.Capability[Caveats], result.Failure] + Match(source Source) result.Result[ucan.Capability[Caveats], failure.Failure] } type Descriptor[I, O any] interface { @@ -63,7 +64,7 @@ func (c *capability[C]) Can() ucan.Ability { return c.descriptor.Can() } -func (c *capability[Caveats]) Match(source Source) result.Result[ucan.Capability[Caveats], result.Failure] { +func (c *capability[Caveats]) Match(source Source) result.Result[ucan.Capability[Caveats], failure.Failure] { return parseCapability(c.descriptor, source) } @@ -76,14 +77,14 @@ func NewCapability[Caveats any](can ucan.Ability, with schema.Reader[string, uca return &capability[Caveats]{descriptor: &d} } -func parseCapability[O any](descriptor Descriptor[any, O], source Source) result.Result[ucan.Capability[O], result.Failure] { +func parseCapability[O any](descriptor Descriptor[any, O], source Source) result.Result[ucan.Capability[O], failure.Failure] { cap := source.Capability() - return result.MatchResultR1(descriptor.With().Read(cap.With()), func(with ucan.Resource) result.Result[ucan.Capability[O], result.Failure] { + return result.MatchResultR1(descriptor.With().Read(cap.With()), func(with ucan.Resource) result.Result[ucan.Capability[O], failure.Failure] { return result.MapOk(descriptor.Nb().Read(cap.Nb()), func(nb O) ucan.Capability[O] { pcap := ucan.NewCapability(cap.Can(), with, nb) return pcap }) - }, func(x result.Failure) result.Result[ucan.Capability[O], result.Failure] { + }, func(x failure.Failure) result.Result[ucan.Capability[O], failure.Failure] { return result.Error[ucan.Capability[O]](x) }) } diff --git a/validator/error.go b/validator/error.go index ba7d6b4..844a4cc 100644 --- a/validator/error.go +++ b/validator/error.go @@ -11,7 +11,7 @@ import ( "github.com/ipld/go-ipld-prime/datamodel" "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/ipld" - "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/did" "github.com/storacha-network/go-ucanto/ucan" vdm "github.com/storacha-network/go-ucanto/validator/datamodel" @@ -29,14 +29,14 @@ type InvalidProofError interface { } type EscalatedCapabilityError[Caveats any] struct { - result.NamedWithStackTrace + failure.NamedWithStackTrace claimed ucan.Capability[Caveats] delegated interface{} cause error } func NewEscalatedCapabilityError[Caveats any](claimed ucan.Capability[Caveats], delegated interface{}, cause error) error { - return EscalatedCapabilityError[Caveats]{result.NamedWithCurrentStackTrace("EscalatedCapability"), claimed, delegated, cause} + return EscalatedCapabilityError[Caveats]{failure.NamedWithCurrentStackTrace("EscalatedCapability"), claimed, delegated, cause} } func (ece EscalatedCapabilityError[Caveats]) Unwrap() error { @@ -50,17 +50,14 @@ func (ece EscalatedCapabilityError[Caveats]) Error() string { func (ece EscalatedCapabilityError[Caveats]) isDelegationSubError() { } -/** - * @implements {API.DelegationError} - */ type DelegationError struct { - result.NamedWithStackTrace + failure.NamedWithStackTrace causes []DelegationSubError context interface{} } func NewDelegationError(causes []DelegationSubError, context interface{}) error { - return DelegationError{result.NamedWithCurrentStackTrace("InvalidClaim"), causes, context} + return DelegationError{failure.NamedWithCurrentStackTrace("InvalidClaim"), causes, context} } func (de DelegationError) Error() string { @@ -78,13 +75,13 @@ func (de DelegationError) Unwrap() []error { func (de DelegationError) isDelegationSubError() {} type SessionEscalationError struct { - result.NamedWithStackTrace + failure.NamedWithStackTrace delegation delegation.Delegation cause error } func NewSessionEscalationError(delegation delegation.Delegation, cause error) error { - return SessionEscalationError{result.NamedWithCurrentStackTrace("SessionEscalation"), delegation, cause} + return SessionEscalationError{failure.NamedWithCurrentStackTrace("SessionEscalation"), delegation, cause} } func (see SessionEscalationError) Error() string { @@ -98,13 +95,13 @@ func (see SessionEscalationError) Error() string { func (see SessionEscalationError) isInvalidProofError() {} type InvalidSignatureError struct { - result.NamedWithStackTrace + failure.NamedWithStackTrace delegation delegation.Delegation verifier ucan.Verifier } func NewInvalidSignatureError(delegation delegation.Delegation, verifier ucan.Verifier) error { - return InvalidSignatureError{result.NamedWithCurrentStackTrace("InvalidSignature"), delegation, verifier} + return InvalidSignatureError{failure.NamedWithCurrentStackTrace("InvalidSignature"), delegation, verifier} } func (ise InvalidSignatureError) Issuer() ucan.Principal { @@ -128,20 +125,29 @@ func (ise InvalidSignatureError) Error() string { func (ise InvalidSignatureError) isInvalidProofError() {} +type UnavailableProof interface { + failure.Failure + Link() ucan.Link +} + type UnavailableProofError struct { - result.NamedWithStackTrace + failure.NamedWithStackTrace link ucan.Link cause error } -func NewUnavailableProofError(link ucan.Link, cause error) UnavailableProofError { - return UnavailableProofError{result.NamedWithCurrentStackTrace("UnavailableProof"), link, cause} +func NewUnavailableProofError(link ucan.Link, cause error) UnavailableProof { + return UnavailableProofError{failure.NamedWithCurrentStackTrace("UnavailableProof"), link, cause} } func (upe UnavailableProofError) Unwrap() error { return upe.cause } +func (upe UnavailableProofError) Link() ucan.Link { + return upe.link +} + func (upe UnavailableProofError) Error() string { messages := []string{ fmt.Sprintf("Linked proof '%s' is not included and could not be resolved", upe.link), @@ -154,20 +160,29 @@ func (upe UnavailableProofError) Error() string { func (upe UnavailableProofError) isInvalidProofError() {} +type UnresolvedDID interface { + failure.Failure + DID() did.DID +} + type DIDKeyResolutionError struct { - result.NamedWithStackTrace + failure.NamedWithStackTrace did did.DID cause error } -func NewDIDKeyResolutionError(did did.DID, cause error) DIDKeyResolutionError { - return DIDKeyResolutionError{result.NamedWithCurrentStackTrace("DIDKeyResolutionError"), did, cause} +func NewDIDKeyResolutionError(did did.DID, cause error) UnresolvedDID { + return DIDKeyResolutionError{failure.NamedWithCurrentStackTrace("DIDKeyResolutionError"), did, cause} } func (dkre DIDKeyResolutionError) Unwrap() error { return dkre.cause } +func (dkre DIDKeyResolutionError) DID() did.DID { + return dkre.did +} + func (dkre DIDKeyResolutionError) Error() string { return fmt.Sprintf("Unable to resolve '%s' key", dkre.did) } @@ -175,13 +190,13 @@ func (dkre DIDKeyResolutionError) Error() string { func (dkre DIDKeyResolutionError) isInvalidProofError() {} type PrincipalAlignmentError struct { - result.NamedWithStackTrace + failure.NamedWithStackTrace audience ucan.Principal delegation delegation.Delegation } -func NewPrincipalAlignmentError(audience ucan.Principal, delegation delegation.Delegation) PrincipalAlignmentError { - return PrincipalAlignmentError{result.NamedWithCurrentStackTrace("InvalidAudience"), audience, delegation} +func NewPrincipalAlignmentError(audience ucan.Principal, delegation delegation.Delegation) error { + return PrincipalAlignmentError{failure.NamedWithCurrentStackTrace("InvalidAudience"), audience, delegation} } func (pae PrincipalAlignmentError) Error() string { @@ -204,13 +219,13 @@ func (pae PrincipalAlignmentError) Build() (datamodel.Node, error) { func (pae PrincipalAlignmentError) isInvalidProofError() {} type MalformedCapabilityError[Caveats any] struct { - result.NamedWithStackTrace + failure.NamedWithStackTrace capability ucan.Capability[Caveats] cause error } -func NewMalformedCapabilityError[Caveats any](capability ucan.Capability[Caveats], cause error) MalformedCapabilityError[Caveats] { - return MalformedCapabilityError[Caveats]{result.NamedWithCurrentStackTrace("MalformedCapability"), capability, cause} +func NewMalformedCapabilityError[Caveats any](capability ucan.Capability[Caveats], cause error) error { + return MalformedCapabilityError[Caveats]{failure.NamedWithCurrentStackTrace("MalformedCapability"), capability, cause} } func (mce MalformedCapabilityError[Caveats]) Error() string { @@ -224,12 +239,12 @@ func (mce MalformedCapabilityError[Caveats]) Error() string { func (mce MalformedCapabilityError[Caveats]) isDelegationSubError() {} type UnknownCapabilityError[Caveats any] struct { - result.NamedWithStackTrace + failure.NamedWithStackTrace capability ucan.Capability[Caveats] } func NewUnknownCapabilityError[Caveats any](capability ucan.Capability[Caveats]) error { - return UnknownCapabilityError[Caveats]{result.NamedWithCurrentStackTrace("UnknownCapability"), capability} + return UnknownCapabilityError[Caveats]{failure.NamedWithCurrentStackTrace("UnknownCapability"), capability} } func (uce UnknownCapabilityError[Caveats]) Error() string { @@ -240,12 +255,12 @@ func (uce UnknownCapabilityError[Caveats]) Error() string { func (uce UnknownCapabilityError[Caveats]) isDelegationSubError() {} type ExpiredError struct { - result.NamedWithStackTrace + failure.NamedWithStackTrace delegation delegation.Delegation } func NewExpiredError(delegation delegation.Delegation) error { - return ExpiredError{result.NamedWithCurrentStackTrace("Expired"), delegation} + return ExpiredError{failure.NamedWithCurrentStackTrace("Expired"), delegation} } func (ee ExpiredError) Error() string { @@ -268,12 +283,12 @@ func (ee ExpiredError) Build() (datamodel.Node, error) { func (ee ExpiredError) isInvalidProofError() {} type RevokedError struct { - result.NamedWithStackTrace + failure.NamedWithStackTrace delegation delegation.Delegation } func NewRevokedError(delegation delegation.Delegation) error { - return RevokedError{result.NamedWithCurrentStackTrace("Revoked"), delegation} + return RevokedError{failure.NamedWithCurrentStackTrace("Revoked"), delegation} } func (re RevokedError) Error() string { @@ -283,12 +298,12 @@ func (re RevokedError) Error() string { func (re RevokedError) isInvalidProofError() {} type NotValidBeforeError struct { - result.NamedWithStackTrace + failure.NamedWithStackTrace delegation delegation.Delegation } -func NewNotValidBeforeERror(delegation delegation.Delegation) error { - return NotValidBeforeError{result.NamedWithCurrentStackTrace("NotValidBefore"), delegation} +func NewNotValidBeforeError(delegation delegation.Delegation) error { + return NotValidBeforeError{failure.NamedWithCurrentStackTrace("NotValidBefore"), delegation} } func (nvbe NotValidBeforeError) Error() string { @@ -313,14 +328,14 @@ func (nvbe NotValidBeforeError) isInvalidProofError() {} // TODO: this may just be the concrete type from the implementation once // the rest of the validator is done type InvalidClaim interface { - result.NamedWithStackTrace + failure.NamedWithStackTrace error Issuer() ucan.Principal Delegation() delegation.Delegation } type UnauthorizedError[Caveats any] struct { - result.NamedWithStackTrace + failure.NamedWithStackTrace capability ucan.Capability[Caveats] delegationErrors []DelegationError // this is a hack... it will allow you to make an array of capabilities of different types @@ -337,7 +352,7 @@ func NewUnauthorizedError[Caveats any]( failedProofs []InvalidClaim, ) error { return UnauthorizedError[Caveats]{ - result.NamedWithCurrentStackTrace("Unauthorized"), + failure.NamedWithCurrentStackTrace("Unauthorized"), capability, delegationErrors, unknownCapabilities, diff --git a/validator/lib.go b/validator/lib.go index 7ff407d..3bf493b 100644 --- a/validator/lib.go +++ b/validator/lib.go @@ -2,24 +2,31 @@ package validator import ( "fmt" + "strings" "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/invocation" + "github.com/storacha-network/go-ucanto/core/policy" + "github.com/storacha-network/go-ucanto/core/policy/literal" + "github.com/storacha-network/go-ucanto/core/policy/selector" "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" + "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/did" "github.com/storacha-network/go-ucanto/principal" "github.com/storacha-network/go-ucanto/ucan" + vdm "github.com/storacha-network/go-ucanto/validator/datamodel" ) func IsSelfIssued[Caveats any](capability ucan.Capability[Caveats], issuer did.DID) bool { return capability.With() == issuer.DID().String() } -func ProofUnavailable(p ucan.Link) result.Result[delegation.Delegation, UnavailableProofError] { +func ProofUnavailable(p ucan.Link) result.Result[delegation.Delegation, UnavailableProof] { return result.Error[delegation.Delegation](NewUnavailableProofError(p, fmt.Errorf("no proof resolver configured"))) } -func FailDIDKeyResolution(d did.DID) result.Result[did.DID, DIDKeyResolutionError] { +func FailDIDKeyResolution(d did.DID) result.Result[did.DID, UnresolvedDID] { return result.Error[did.DID](NewDIDKeyResolutionError(d, fmt.Errorf("no DID resolver configured"))) } @@ -35,23 +42,23 @@ type PrincipalParserFunc = func(str string) (principal.Verifier, error) // identified by DID different from did:key method. It can be passed into a // UCAN validator in order to augmented it with additional DID methods support. type PrincipalResolver interface { - ResolveDIDKey(did did.DID) result.Result[did.DID, DIDKeyResolutionError] + ResolveDIDKey(did did.DID) result.Result[did.DID, UnresolvedDID] } // PrincipalResolverFunc resolves the key of a principal that is identified by // DID different from did:key method. -type PrincipalResolverFunc = func(did did.DID) result.Result[did.DID, DIDKeyResolutionError] +type PrincipalResolverFunc = func(did did.DID) result.Result[did.DID, UnresolvedDID] // ProofResolver finds a delegations when external proof links are present in // UCANs. If a resolver is not provided the validator may not be able to explore // corresponding path within a proof chain. type ProofResolver interface { // Resolve finds a delegation corresponding to an external proof link. - ResolveProof(proof ucan.Link) result.Result[delegation.Delegation, UnavailableProofError] + ResolveProof(proof ucan.Link) result.Result[delegation.Delegation, UnavailableProof] } // Resolve finds a delegation corresponding to an external proof link. -type ProofResolverFunc = func(proof ucan.Link) result.Result[delegation.Delegation, UnavailableProofError] +type ProofResolverFunc = func(proof ucan.Link) result.Result[delegation.Delegation, UnavailableProof] type CanIssuer[Caveats any] interface { // CanIssue informs validator whether given capability can be issued by a @@ -66,14 +73,26 @@ type CanIssueFunc[Caveats any] func(capability ucan.Capability[Caveats], issuer type RevocationChecker[Caveats any] interface { // ValidateAuthorization validates that the passed authorization has not been // revoked. - ValidateAuthorization(auth Authorization[Caveats]) result.Failure + ValidateAuthorization(auth Authorization[Caveats]) failure.Failure } // RevocationCheckerFunc validates the passed authorization and returns // a result indicating validity. -type RevocationCheckerFunc[Caveats any] func(auth Authorization[Caveats]) result.Failure +type RevocationCheckerFunc[Caveats any] func(auth Authorization[Caveats]) failure.Failure + +// Validator must provide a `Verifier` corresponding to local authority. +// +// A capability provider service will use one corresponding to own DID or it's +// supervisor's DID if it acts under it's authority. +// +// This allows a service identified by non did:key e.g. did:web or did:dns to +// pass resolved key so it does not need to be resolved at runtime. +type Validator interface { + Authority() principal.Verifier +} type ClaimContext interface { + Validator RevocationChecker[any] CanIssuer[any] ProofResolver @@ -87,6 +106,7 @@ type ValidationContext[Caveats any] interface { } type validationContext[Caveats any] struct { + authority principal.Verifier capability CapabilityParser[Caveats] canIssue CanIssueFunc[any] validateAuthorization RevocationCheckerFunc[any] @@ -95,6 +115,10 @@ type validationContext[Caveats any] struct { resolveDIDKey PrincipalResolverFunc } +func (vc validationContext[Caveats]) Authority() principal.Verifier { + return vc.authority +} + func (vc validationContext[Caveats]) Capability() CapabilityParser[Caveats] { return vc.capability } @@ -103,11 +127,11 @@ func (vc validationContext[Caveats]) CanIssue(capability ucan.Capability[any], i return vc.canIssue(capability, issuer) } -func (vc validationContext[Caveats]) ValidateAuthorization(auth Authorization[any]) result.Failure { +func (vc validationContext[Caveats]) ValidateAuthorization(auth Authorization[any]) failure.Failure { return vc.validateAuthorization(auth) } -func (vc validationContext[Caveats]) ResolveProof(proof ucan.Link) result.Result[delegation.Delegation, UnavailableProofError] { +func (vc validationContext[Caveats]) ResolveProof(proof ucan.Link) result.Result[delegation.Delegation, UnavailableProof] { return vc.resolveProof(proof) } @@ -115,11 +139,12 @@ func (vc validationContext[Caveats]) ParsePrincipal(str string) (principal.Verif return vc.parsePrincipal(str) } -func (vc validationContext[Caveats]) ResolveDIDKey(did did.DID) result.Result[did.DID, DIDKeyResolutionError] { +func (vc validationContext[Caveats]) ResolveDIDKey(did did.DID) result.Result[did.DID, UnresolvedDID] { return vc.resolveDIDKey(did) } func NewValidationContext[Caveats any]( + authority principal.Verifier, capability CapabilityParser[Caveats], canIssue CanIssueFunc[any], validateAuthorization RevocationCheckerFunc[any], @@ -128,6 +153,7 @@ func NewValidationContext[Caveats any]( resolveDIDKey PrincipalResolverFunc, ) ValidationContext[Caveats] { return validationContext[Caveats]{ + authority, capability, canIssue, validateAuthorization, @@ -142,7 +168,7 @@ func NewValidationContext[Caveats any]( // returned that illustrates the valid path. If no valid path is found // `Unauthorized` error is returned detailing all explored paths and where they // proved to fail. -func Access[Caveats any](invocation invocation.Invocation, context ValidationContext[Caveats]) (result.Result[Authorization[Caveats], UnauthorizedError[Caveats]], error) { +func Access[Caveats any](invocation invocation.Invocation, context ValidationContext[Caveats]) result.Result[Authorization[Caveats], UnauthorizedError[Caveats]] { prf := []delegation.Proof{delegation.FromDelegation(invocation)} return Claim(context.Capability(), prf, context) } @@ -151,7 +177,7 @@ func Access[Caveats any](invocation invocation.Invocation, context ValidationCon // set of `proofs`. On success an `Authorization` object with detailed proof // chain is returned and on failure `Unauthorized` error is returned with // details on paths explored and why they have failed. -func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegation.Proof, context ClaimContext) (result.Result[Authorization[Caveats], UnauthorizedError[Caveats]], error) { +func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegation.Proof, context ClaimContext) result.Result[Authorization[Caveats], UnauthorizedError[Caveats]] { delegations, errors := resolveProofs(proofs, context) for _, d := range delegations { @@ -180,7 +206,7 @@ func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegatio // a `Delegation` or a link to one and attempts to resolve links by side loading // them. It returns a set of resolved `Delegation`s and errors for the proofs // that could not be resolved. -func resolveProofs(proofs []delegation.Proof, resolver ProofResolver) (dels []delegation.Delegation, errs []UnavailableProofError) { +func resolveProofs(proofs []delegation.Proof, resolver ProofResolver) (dels []delegation.Delegation, errs []UnavailableProof) { for _, p := range proofs { d, ok := p.Delegation() if ok { @@ -189,7 +215,7 @@ func resolveProofs(proofs []delegation.Proof, resolver ProofResolver) (dels []de result.MatchResultR0( resolver.ResolveProof(p.Link()), func(d delegation.Delegation) { dels = append(dels, d) }, - func(err UnavailableProofError) { errs = append(errs, err) }, + func(err UnavailableProof) { errs = append(errs, err) }, ) } } @@ -198,8 +224,90 @@ func resolveProofs(proofs []delegation.Proof, resolver ProofResolver) (dels []de // Validate a delegation to check it is within the time bound and that it is // authorized by the issuer. -func validate(delegation delegation.Delegation, proofs []delegation.Delegation, context ClaimContext) { - if ucan.IsExpired(delegation) { +func validate(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) result.Result[delegation.Delegation, failure.Failure] { + if ucan.IsExpired(dlg) { + return result.Error[delegation.Delegation](failure.FromError(NewExpiredError(dlg))) + } + if ucan.IsTooEarly(dlg) { + return result.Error[delegation.Delegation](failure.FromError(NewNotValidBeforeError(dlg))) + } + return verifyAuthorization(dlg, prfs, ctx) +} +// verifyAuthorization verifies that delegation has been authorized by the +// issuer. If issued by the did:key principal checks that the signature is +// valid. If issued by the root authority checks that the signature is valid. If +// issued by the principal identified by other DID method attempts to resolve a +// valid `ucan/attest` attestation from the authority, if attestation is not +// found falls back to resolving did:key for the issuer and verifying its +// signature. +func verifyAuthorization(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) result.Result[delegation.Delegation, failure.Failure] { + issuer := dlg.Issuer().DID() + // If the issuer is a did:key we just verify a signature + if strings.HasPrefix(issuer.String(), "did:key:") { + vfr, err := ctx.ParsePrincipal(issuer.String()) + if err != nil { + return result.Error[delegation.Delegation](failure.FromError(err)) + } + return verifySignature(dlg, vfr) } + + return result.MapResultR0( + // Attempt to resolve embedded authorization session from the authority + verifySession(dlg, prfs, ctx), + func(_ Authorization[vdm.AttestationModel]) delegation.Delegation { + return dlg + }, + func(err UnauthorizedError[vdm.AttestationModel]) failure.Failure { + if len(err.failedProofs) > 0 { + return NewSessionEscalationError(dlg, err) + } + // Otherwise we try to resolve did:key from the DID instead + // and use that to verify the signature + return result.MapResultR0( + ctx.ResolveDIDKey(issuer) + ) + }, + ) +} + +func verifySignature(dlg delegation.Delegation, vfr principal.Verifier) result.Result[delegation.Delegation, failure.Failure] { + ok, err := ucan.VerifySignature(dlg.Data(), vfr) + if err != nil { + return result.Error[delegation.Delegation](failure.FromError(err)) + } + if !ok { + return result.Error[delegation.Delegation](failure.FromError(NewInvalidSignatureError(dlg, vfr))) + } + return result.Ok[delegation.Delegation, failure.Failure](dlg) +} + +// verifySession attempts to find an authorization session - an `ucan/attest` +// capability delegation where `with` matches `config.authority` and `nb.proof` +// matches given delegation. +// +// https://github.com/storacha-network/specs/blob/main/w3-session.md#authorization-session +func verifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) result.Result[Authorization[vdm.AttestationModel], UnauthorizedError[vdm.AttestationModel]] { + // Create a schema that will match an authorization for this exact delegation + attestation := NewCapability( + "ucan/attest", + schema.Literal(ctx.Authority().DID().String()), + schema.Struct[vdm.AttestationModel]( + vdm.AttestationType(), + policy.Policy{ + policy.Equal(selector.MustParse(".proof"), literal.Link(dlg.Link())), + }, + ), + ) + + // We only consider attestations otherwise we will end up doing an + // exponential scan if there are other proofs that require attestations. + var aprfs []delegation.Proof + for _, p := range prfs { + if p.Capabilities()[0].Can() == "ucan/attest" { + aprfs = append(aprfs, delegation.FromDelegation(p)) + } + } + + return Claim(attestation, aprfs, ctx) } From b51775cbf865fe9ba8196b114051d569b4849f13 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 19 Aug 2024 23:16:36 +0200 Subject: [PATCH 05/37] feat: add policy implementation --- core/policy/literal/literal.go | 124 ++++++++ core/policy/match.go | 242 +++++++++++++++ core/policy/match_test.go | 283 ++++++++++++++++++ core/policy/selector/selector.go | 110 +++++++ core/policy/selector/selector_test.go | 134 +++++++++ core/policy/statement.go | 204 +++++++++++++ core/result/failure/datamodel/failure.go | 37 +++ core/result/failure/datamodel/failure.ipldsch | 5 + core/result/failure/faillure.go | 100 +++++++ core/schema/literal.go | 19 ++ validator/datamodel/attestation.go | 29 ++ validator/datamodel/attestration.ipldsch | 3 + 12 files changed, 1290 insertions(+) create mode 100644 core/policy/literal/literal.go create mode 100644 core/policy/match.go create mode 100644 core/policy/match_test.go create mode 100644 core/policy/selector/selector.go create mode 100644 core/policy/selector/selector_test.go create mode 100644 core/policy/statement.go create mode 100644 core/result/failure/datamodel/failure.go create mode 100644 core/result/failure/datamodel/failure.ipldsch create mode 100644 core/result/failure/faillure.go create mode 100644 core/schema/literal.go create mode 100644 validator/datamodel/attestation.go create mode 100644 validator/datamodel/attestration.ipldsch diff --git a/core/policy/literal/literal.go b/core/policy/literal/literal.go new file mode 100644 index 0000000..61d949e --- /dev/null +++ b/core/policy/literal/literal.go @@ -0,0 +1,124 @@ +package literal + +import ( + "fmt" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/basicnode" +) + +var ErrType = fmt.Errorf("literal is not this type") + +const ( + Kind_IPLD = "ipld" + Kind_Int = "int" + Kind_Float = "float" + Kind_String = "string" +) + +type Literal interface { + Kind() string // ipld | integer | float | string + AsNode() (ipld.Node, error) + AsInt() (int64, error) + AsFloat() (float64, error) + AsString() (string, error) +} + +type literal struct{} + +func (l literal) AsFloat() (float64, error) { + return 0, ErrType +} + +func (l literal) AsInt() (int64, error) { + return 0, ErrType +} + +func (l literal) AsNode() (datamodel.Node, error) { + return nil, ErrType +} + +func (l literal) AsString() (string, error) { + return "", ErrType +} + +type node struct { + literal + value ipld.Node +} + +func (l node) AsNode() (datamodel.Node, error) { + return l.value, nil +} + +func (l node) Kind() string { + return Kind_IPLD +} + +func Node(n ipld.Node) Literal { + return node{value: n} +} + +func Link(cid ipld.Link) Literal { + nb := basicnode.Prototype.Link.NewBuilder() + nb.AssignLink(cid) + return node{value: nb.Build()} +} + +func Bool(val bool) Literal { + nb := basicnode.Prototype.Bool.NewBuilder() + nb.AssignBool(val) + return node{value: nb.Build()} +} + +type nint struct { + literal + value int64 +} + +func (l nint) AsInt() (int64, error) { + return l.value, nil +} + +func (l nint) Kind() string { + return Kind_Int +} + +func Int(num int64) Literal { + return nint{value: num} +} + +type nfloat struct { + literal + value float64 +} + +func (l nfloat) AsFloat() (float64, error) { + return l.value, nil +} + +func (l nfloat) Kind() string { + return Kind_Float +} + +func Float(num float64) Literal { + return nfloat{value: num} +} + +type str struct { + literal + value string +} + +func (l str) AsString() (string, error) { + return l.value, nil +} + +func (l str) Kind() string { + return Kind_String +} + +func String(s string) Literal { + return str{value: s} +} diff --git a/core/policy/match.go b/core/policy/match.go new file mode 100644 index 0000000..63266a2 --- /dev/null +++ b/core/policy/match.go @@ -0,0 +1,242 @@ +package policy + +import ( + "cmp" + "fmt" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/storacha-network/go-ucanto/core/policy/literal" + "github.com/storacha-network/go-ucanto/core/policy/selector" +) + +// Match determines if the IPLD node matches the policy document. +func Match(policy Policy, node ipld.Node) (bool, error) { + for _, stmt := range policy { + ok, err := matchStatement(stmt, node) + if err != nil || !ok { + return ok, err + } + } + return true, nil +} + +func matchStatement(statement Statement, node ipld.Node) (bool, error) { + switch statement.Kind() { + case Kind_Equal: + if s, ok := statement.(EqualityStatement); ok { + n, err := selectNode(s.Selector(), node) + if err != nil { + if _, ok := err.(datamodel.ErrNotExists); ok { + return false, nil + } + return false, fmt.Errorf("selecting node: %w", err) + } + return isDeepEqual(s.Value(), n) + } + case Kind_GreaterThan: + if s, ok := statement.(InequalityStatement); ok { + n, err := selectNode(s.Selector(), node) + if err != nil { + if _, ok := err.(datamodel.ErrNotExists); ok { + return false, nil + } + return false, fmt.Errorf("selecting node: %w", err) + } + return isOrdered(s, n, gt) + } + case Kind_GreaterThanOrEqual: + if s, ok := statement.(InequalityStatement); ok { + n, err := selectNode(s.Selector(), node) + if err != nil { + if _, ok := err.(datamodel.ErrNotExists); ok { + return false, nil + } + return false, fmt.Errorf("selecting node: %w", err) + } + return isOrdered(s, n, gte) + } + case Kind_LessThan: + if s, ok := statement.(InequalityStatement); ok { + n, err := selectNode(s.Selector(), node) + if err != nil { + if _, ok := err.(datamodel.ErrNotExists); ok { + return false, nil + } + return false, fmt.Errorf("selecting node: %w", err) + } + return isOrdered(s, n, lt) + } + case Kind_LessThanOrEqual: + if s, ok := statement.(InequalityStatement); ok { + n, err := selectNode(s.Selector(), node) + if err != nil { + if _, ok := err.(datamodel.ErrNotExists); ok { + return false, nil + } + return false, fmt.Errorf("selecting node: %w", err) + } + return isOrdered(s, n, lte) + } + case Kind_Negation: + if s, ok := statement.(NegationStatement); ok { + r, err := matchStatement(s.Value(), node) + if err != nil { + return false, err + } + return !r, err + } + case Kind_Conjunction: + if s, ok := statement.(ConjunctionStatement); ok { + for _, cs := range s.Value() { + r, err := matchStatement(cs, node) + if err != nil { + return false, err + } + if !r { + return false, nil + } + } + return true, nil + } + case Kind_Disjunction: + if s, ok := statement.(DisjunctionStatement); ok { + for _, cs := range s.Value() { + r, err := matchStatement(cs, node) + if err != nil { + return false, err + } + if r { + return true, nil + } + } + return false, nil + } + case Kind_Wildcard: + case Kind_Universal: + case Kind_Existential: + } + return false, fmt.Errorf("statement kind not implemented: %s", statement.Kind()) +} + +func selectNode(sel selector.Selector, node ipld.Node) (child ipld.Node, err error) { + if sel.Identity() { + child = node + } else if sel.Field() != "" { + child, err = node.LookupByString(sel.Field()) + } else { + child, err = node.LookupByIndex(int64(sel.Index())) + } + return +} + +func isOrdered(stmt InequalityStatement, node ipld.Node, satisfies func(order int) bool) (bool, error) { + if stmt.Value().Kind() == literal.Kind_Int && node.Kind() == ipld.Kind_Int { + a, err := node.AsInt() + if err != nil { + return false, fmt.Errorf("extracting node int: %w", err) + } + b, err := stmt.Value().AsInt() + if err != nil { + return false, fmt.Errorf("extracting selector int: %w", err) + } + return satisfies(cmp.Compare(a, b)), nil + } + + if stmt.Value().Kind() == literal.Kind_Float && node.Kind() == ipld.Kind_Float { + a, err := node.AsFloat() + if err != nil { + return false, fmt.Errorf("extracting node float: %w", err) + } + b, err := stmt.Value().AsFloat() + if err != nil { + return false, fmt.Errorf("extracting selector float: %w", err) + } + return satisfies(cmp.Compare(a, b)), nil + } + + return false, fmt.Errorf("selector type %s is not compatible with node type %s: kind mismatch: need int or float", stmt.Value().Kind(), node.Kind()) +} + +func isDeepEqual(value literal.Literal, node ipld.Node) (bool, error) { + switch value.Kind() { + case literal.Kind_String: + if node.Kind() != ipld.Kind_String { + return false, nil + } + a, err := node.AsString() + if err != nil { + return false, fmt.Errorf("extracting node string: %w", err) + } + b, err := value.AsString() + if err != nil { + return false, fmt.Errorf("extracting selector string: %w", err) + } + return a == b, nil + case literal.Kind_Int: + if node.Kind() != ipld.Kind_Int { + return false, nil + } + a, err := node.AsInt() + if err != nil { + return false, fmt.Errorf("extracting node int: %w", err) + } + b, err := value.AsInt() + if err != nil { + return false, fmt.Errorf("extracting selector int: %w", err) + } + return a == b, nil + case literal.Kind_Float: + if node.Kind() != ipld.Kind_Float { + return false, nil + } + a, err := node.AsFloat() + if err != nil { + return false, fmt.Errorf("extracting node float: %w", err) + } + b, err := value.AsFloat() + if err != nil { + return false, fmt.Errorf("extracting selector float: %w", err) + } + return a == b, nil + case literal.Kind_IPLD: + v, err := value.AsNode() + if err != nil { + return false, fmt.Errorf("extracting selector node: %w", err) + } + if v.Kind() != node.Kind() { + return false, nil + } + // TODO: should be easy enough to do the basic types, map, struct and list + // might be harder. + switch v.Kind() { + case ipld.Kind_Bool: + a, err := node.AsBool() + if err != nil { + return false, fmt.Errorf("extracting node boolean: %w", err) + } + b, err := v.AsBool() + if err != nil { + return false, fmt.Errorf("extracting selector node boolean: %w", err) + } + return a == b, nil + case ipld.Kind_Link: + a, err := node.AsLink() + if err != nil { + return false, fmt.Errorf("extracting node link: %w", err) + } + b, err := v.AsLink() + if err != nil { + return false, fmt.Errorf("extracting selector node link: %w", err) + } + return a.Binary() == b.Binary(), nil + } + return false, fmt.Errorf("unsupported IPLD kind: %s", v.Kind()) + } + return false, fmt.Errorf("unknown literal kind: %s", value.Kind()) +} + +func gt(order int) bool { return order == 1 } +func gte(order int) bool { return order == 0 || order == 1 } +func lt(order int) bool { return order == -1 } +func lte(order int) bool { return order == 0 || order == -1 } diff --git a/core/policy/match_test.go b/core/policy/match_test.go new file mode 100644 index 0000000..5d9f1f7 --- /dev/null +++ b/core/policy/match_test.go @@ -0,0 +1,283 @@ +package policy + +import ( + "testing" + + "github.com/ipfs/go-cid" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/storacha-network/go-ucanto/core/policy/literal" + "github.com/storacha-network/go-ucanto/core/policy/selector" + "github.com/stretchr/testify/require" +) + +func TestMatch(t *testing.T) { + t.Run("equality string", func(t *testing.T) { + np := basicnode.Prototype.String + nb := np.NewBuilder() + nb.AssignString("test") + nd := nb.Build() + + pol := Policy{Equal(selector.MustParse("."), literal.String("test"))} + ok, err := Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.String("test2"))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.False(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.Int(138))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.False(t, ok) + }) + + t.Run("equality int", func(t *testing.T) { + np := basicnode.Prototype.Int + nb := np.NewBuilder() + nb.AssignInt(138) + nd := nb.Build() + + pol := Policy{Equal(selector.MustParse("."), literal.Int(138))} + ok, err := Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.Int(1138))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.False(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.String("138"))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.False(t, ok) + }) + + t.Run("equality float", func(t *testing.T) { + np := basicnode.Prototype.Float + nb := np.NewBuilder() + nb.AssignFloat(1.138) + nd := nb.Build() + + pol := Policy{Equal(selector.MustParse("."), literal.Float(1.138))} + ok, err := Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.Float(11.38))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.False(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.String("138"))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.False(t, ok) + }) + + t.Run("equality IPLD Link", func(t *testing.T) { + l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")} + l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")} + + np := basicnode.Prototype.Link + nb := np.NewBuilder() + nb.AssignLink(l0) + nd := nb.Build() + + pol := Policy{Equal(selector.MustParse("."), literal.Link(l0))} + ok, err := Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.Link(l1))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.False(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq"))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.False(t, ok) + }) + + t.Run("equality string in map", func(t *testing.T) { + np := basicnode.Prototype.Map + nb := np.NewBuilder() + ma, _ := nb.BeginMap(1) + ma.AssembleKey().AssignString("foo") + ma.AssembleValue().AssignString("bar") + ma.Finish() + nd := nb.Build() + + pol := Policy{Equal(selector.MustParse(".foo"), literal.String("bar"))} + ok, err := Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + + pol = Policy{Equal(selector.MustParse(".[\"foo\"]"), literal.String("bar"))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + + pol = Policy{Equal(selector.MustParse(".foo"), literal.String("baz"))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.False(t, ok) + + pol = Policy{Equal(selector.MustParse(".foobar"), literal.String("bar"))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.False(t, ok) + }) + + t.Run("equality string in list", func(t *testing.T) { + np := basicnode.Prototype.List + nb := np.NewBuilder() + la, _ := nb.BeginList(1) + la.AssembleValue().AssignString("foo") + la.Finish() + nd := nb.Build() + + pol := Policy{Equal(selector.MustParse(".[0]"), literal.String("foo"))} + ok, err := Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + + pol = Policy{Equal(selector.MustParse(".[1]"), literal.String("foo"))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.False(t, ok) + }) + + t.Run("inequality gt int", func(t *testing.T) { + np := basicnode.Prototype.Int + nb := np.NewBuilder() + nb.AssignInt(138) + nd := nb.Build() + + pol := Policy{GreaterThan(selector.MustParse("."), literal.Int(1))} + ok, err := Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + }) + + t.Run("inequality gte int", func(t *testing.T) { + np := basicnode.Prototype.Int + nb := np.NewBuilder() + nb.AssignInt(138) + nd := nb.Build() + + pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(1))} + ok, err := Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + + pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(138))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + }) + + t.Run("inequality gt float", func(t *testing.T) { + np := basicnode.Prototype.Float + nb := np.NewBuilder() + nb.AssignFloat(1.38) + nd := nb.Build() + + pol := Policy{GreaterThan(selector.MustParse("."), literal.Float(1))} + ok, err := Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + }) + + t.Run("inequality gte float", func(t *testing.T) { + np := basicnode.Prototype.Float + nb := np.NewBuilder() + nb.AssignFloat(1.38) + nd := nb.Build() + + pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1))} + ok, err := Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + + pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1.38))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + }) + + t.Run("negation", func(t *testing.T) { + np := basicnode.Prototype.Bool + nb := np.NewBuilder() + nb.AssignBool(false) + nd := nb.Build() + + pol := Policy{Not(Equal(selector.MustParse("."), literal.Bool(true)))} + ok, err := Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + + pol = Policy{Not(Equal(selector.MustParse("."), literal.Bool(false)))} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.False(t, ok) + }) + + t.Run("conjunction", func(t *testing.T) { + np := basicnode.Prototype.Int + nb := np.NewBuilder() + nb.AssignInt(138) + nd := nb.Build() + + pol := Policy{ + And( + GreaterThan(selector.MustParse("."), literal.Int(1)), + LessThan(selector.MustParse("."), literal.Int(1138)), + ), + } + ok, err := Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + + pol = Policy{ + And( + GreaterThan(selector.MustParse("."), literal.Int(1)), + Equal(selector.MustParse("."), literal.Int(1138)), + ), + } + ok, err = Match(pol, nd) + require.NoError(t, err) + require.False(t, ok) + }) + + t.Run("disjunction", func(t *testing.T) { + np := basicnode.Prototype.Int + nb := np.NewBuilder() + nb.AssignInt(138) + nd := nb.Build() + + pol := Policy{ + Or( + GreaterThan(selector.MustParse("."), literal.Int(138)), + LessThan(selector.MustParse("."), literal.Int(1138)), + ), + } + ok, err := Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) + + pol = Policy{ + Or( + GreaterThan(selector.MustParse("."), literal.Int(138)), + Equal(selector.MustParse("."), literal.Int(1138)), + ), + } + ok, err = Match(pol, nd) + require.NoError(t, err) + require.False(t, ok) + }) +} diff --git a/core/policy/selector/selector.go b/core/policy/selector/selector.go new file mode 100644 index 0000000..c00f840 --- /dev/null +++ b/core/policy/selector/selector.go @@ -0,0 +1,110 @@ +package selector + +import ( + "fmt" + "strconv" + "strings" +) + +// Selector describes a UCAN policy selector, as specified here: +// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#selectors +type Selector interface { + // Identity flags that this selector is the identity selector. + Identity() bool + // Optional flags that this selector is optional. + Optional() bool + // Field is the name of a field in a struct/map. + Field() string + // Index is an index of a slice. + Index() int + // String returns the selector's string representation. + String() string +} + +type selector struct { + str string + identity bool + optional bool + field string + index int +} + +func (s selector) Field() string { + return s.field +} + +func (s selector) Identity() bool { + return s.identity +} + +func (s selector) Index() int { + return s.index +} + +func (s selector) Optional() bool { + return s.optional +} + +func (s selector) String() string { + return s.str +} + +// TODO: probably regex or better parser +func Parse(sel string) (Selector, error) { + s := sel + if s == "." { + return selector{sel, true, false, "", 0}, nil + } + + optional := strings.HasSuffix(s, "?") + if optional { + s = s[0 : len(s)-1] + } + + dotted := strings.HasPrefix(s, ".") + if dotted { + s = s[1:] + } + + // collection values + if s == "[]" { + return nil, fmt.Errorf("unsupported selector: %s", sel) + } + + if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") { + s = s[1 : len(s)-1] + + // explicit field selector + if strings.HasPrefix(s, "\"") && strings.HasSuffix(s, "\"") { + return selector{sel, false, optional, s[1 : len(s)-1], 0}, nil + } + + // collection range + if strings.Contains(s, ":") { + return nil, fmt.Errorf("unsupported selector: %s", sel) + } + + // index selector + idx, err := strconv.Atoi(s) + if err != nil { + return nil, fmt.Errorf("parsing index selector value: %s", err) + } + + return selector{sel, false, optional, "", idx}, nil + } + + if !dotted { + return nil, fmt.Errorf("invalid selector: %s", sel) + } + + // dotted field selector + return selector{sel, false, optional, s, 0}, nil +} + +func MustParse(sel string) Selector { + s, err := Parse(sel) + if err != nil { + panic(err) + } + return s +} diff --git a/core/policy/selector/selector_test.go b/core/policy/selector/selector_test.go new file mode 100644 index 0000000..d8fab0b --- /dev/null +++ b/core/policy/selector/selector_test.go @@ -0,0 +1,134 @@ +package selector + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParse(t *testing.T) { + t.Run("identity", func(t *testing.T) { + sel, err := Parse(".") + require.NoError(t, err) + require.True(t, sel.Identity()) + require.False(t, sel.Optional()) + require.Empty(t, sel.Field()) + require.Empty(t, sel.Index()) + }) + + t.Run("dotted field", func(t *testing.T) { + sel, err := Parse(".foo") + require.NoError(t, err) + require.False(t, sel.Identity()) + require.False(t, sel.Optional()) + require.Equal(t, sel.Field(), "foo") + require.Empty(t, sel.Index()) + }) + + t.Run("dotted explicit field", func(t *testing.T) { + sel, err := Parse(".[\"foo\"]") + require.NoError(t, err) + require.False(t, sel.Identity()) + require.False(t, sel.Optional()) + require.Equal(t, sel.Field(), "foo") + require.Empty(t, sel.Index()) + }) + + t.Run("dotted index", func(t *testing.T) { + sel, err := Parse(".[138]") + require.NoError(t, err) + require.False(t, sel.Identity()) + require.False(t, sel.Optional()) + require.Empty(t, sel.Field()) + require.Equal(t, sel.Index(), 138) + }) + + t.Run("explicit field", func(t *testing.T) { + sel, err := Parse("[\"foo\"]") + require.NoError(t, err) + require.False(t, sel.Identity()) + require.False(t, sel.Optional()) + require.Equal(t, sel.Field(), "foo") + require.Empty(t, sel.Index()) + }) + + t.Run("index", func(t *testing.T) { + sel, err := Parse("[138]") + require.NoError(t, err) + require.False(t, sel.Identity()) + require.False(t, sel.Optional()) + require.Empty(t, sel.Field()) + require.Equal(t, sel.Index(), 138) + }) + + t.Run("negative index", func(t *testing.T) { + sel, err := Parse("[-138]") + require.NoError(t, err) + require.False(t, sel.Identity()) + require.False(t, sel.Optional()) + require.Empty(t, sel.Field()) + require.Equal(t, sel.Index(), -138) + }) + + t.Run("optional dotted field", func(t *testing.T) { + sel, err := Parse(".foo?") + require.NoError(t, err) + require.False(t, sel.Identity()) + require.True(t, sel.Optional()) + require.Equal(t, sel.Field(), "foo") + require.Empty(t, sel.Index()) + }) + + t.Run("optional dotted explicit field", func(t *testing.T) { + sel, err := Parse(".[\"foo\"]?") + require.NoError(t, err) + require.False(t, sel.Identity()) + require.True(t, sel.Optional()) + require.Equal(t, sel.Field(), "foo") + require.Empty(t, sel.Index()) + }) + + t.Run("optional dotted index", func(t *testing.T) { + sel, err := Parse(".[138]?") + require.NoError(t, err) + require.False(t, sel.Identity()) + require.True(t, sel.Optional()) + require.Empty(t, sel.Field()) + require.Equal(t, sel.Index(), 138) + }) + + t.Run("optional explicit field", func(t *testing.T) { + sel, err := Parse("[\"foo\"]?") + require.NoError(t, err) + require.False(t, sel.Identity()) + require.True(t, sel.Optional()) + require.Equal(t, sel.Field(), "foo") + require.Empty(t, sel.Index()) + }) + + t.Run("optional index", func(t *testing.T) { + sel, err := Parse("[138]?") + require.NoError(t, err) + require.False(t, sel.Identity()) + require.True(t, sel.Optional()) + require.Empty(t, sel.Field()) + require.Equal(t, sel.Index(), 138) + }) + + t.Run("non dotted", func(t *testing.T) { + _, err := Parse("foo") + if err == nil { + t.Fatalf("expected error parsing selector") + } + fmt.Println(err) + }) + + t.Run("non quoted", func(t *testing.T) { + _, err := Parse(".[foo]") + if err == nil { + t.Fatalf("expected error parsing selector") + } + fmt.Println(err) + }) +} diff --git a/core/policy/statement.go b/core/policy/statement.go new file mode 100644 index 0000000..cbedc9f --- /dev/null +++ b/core/policy/statement.go @@ -0,0 +1,204 @@ +package policy + +// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy + +import ( + "github.com/storacha-network/go-ucanto/core/policy/literal" + "github.com/storacha-network/go-ucanto/core/policy/selector" +) + +const ( + Kind_Equal = "==" + Kind_GreaterThan = ">" + Kind_GreaterThanOrEqual = ">=" + Kind_LessThan = "<" + Kind_LessThanOrEqual = "<=" + Kind_Negation = "not" + Kind_Conjunction = "and" + Kind_Disjunction = "or" + Kind_Wildcard = "like" + Kind_Universal = "all" + Kind_Existential = "any" +) + +type Policy = []Statement + +type Statement interface { + Kind() string +} + +type EqualityStatement interface { + Statement + Selector() selector.Selector + Value() literal.Literal +} + +type InequalityStatement interface { + Statement + Selector() selector.Selector + Value() literal.Literal +} + +type WildcardStatement interface { + Statement + Selector() selector.Selector + Value() string +} + +type ConnectiveStatement interface { + Statement +} + +type NegationStatement interface { + ConnectiveStatement + Value() Statement +} + +type ConjunctionStatement interface { + ConnectiveStatement + Value() []Statement +} + +type DisjunctionStatement interface { + ConnectiveStatement + Value() []Statement +} + +type QuantifierStatement interface { + Statement + Selector() selector.Selector + Value() Policy +} + +type equality struct { + kind string + selector selector.Selector + value literal.Literal +} + +func (e equality) Kind() string { + return e.kind +} + +func (e equality) Value() literal.Literal { + return e.value +} + +func (e equality) Selector() selector.Selector { + return e.selector +} + +func Equal(selector selector.Selector, value literal.Literal) EqualityStatement { + return equality{Kind_Equal, selector, value} +} + +func GreaterThan(selector selector.Selector, value literal.Literal) InequalityStatement { + return equality{Kind_GreaterThan, selector, value} +} + +func GreaterThanOrEqual(selector selector.Selector, value literal.Literal) InequalityStatement { + return equality{Kind_GreaterThanOrEqual, selector, value} +} + +func LessThan(selector selector.Selector, value literal.Literal) InequalityStatement { + return equality{Kind_LessThan, selector, value} +} + +func LessThanOrEqual(selector selector.Selector, value literal.Literal) InequalityStatement { + return equality{Kind_LessThanOrEqual, selector, value} +} + +type negation struct { + statement Statement +} + +func (n negation) Kind() string { + return Kind_Negation +} + +func (n negation) Value() Statement { + return n.statement +} + +func Not(stmt Statement) NegationStatement { + return negation{stmt} +} + +type conjunction struct { + statements []Statement +} + +func (n conjunction) Kind() string { + return Kind_Conjunction +} + +func (n conjunction) Value() []Statement { + return n.statements +} + +func And(stmts ...Statement) ConjunctionStatement { + return conjunction{stmts} +} + +type disjunction struct { + statements []Statement +} + +func (n disjunction) Kind() string { + return Kind_Disjunction +} + +func (n disjunction) Value() []Statement { + return n.statements +} + +func Or(stmts ...Statement) DisjunctionStatement { + return disjunction{stmts} +} + +type wildcard struct { + selector selector.Selector + pattern string +} + +func (n wildcard) Kind() string { + return Kind_Wildcard +} + +func (n wildcard) Selector() selector.Selector { + return n.selector +} + +func (n wildcard) Value() string { + return n.pattern +} + +func Like(selector selector.Selector, pattern string) WildcardStatement { + return wildcard{selector, pattern} +} + +type quantifier struct { + kind string + selector selector.Selector + policy Policy +} + +func (n quantifier) Kind() string { + return n.kind +} + +func (n quantifier) Selector() selector.Selector { + return n.selector +} + +func (n quantifier) Value() Policy { + return n.policy +} + +func All(selector selector.Selector, policy Policy) QuantifierStatement { + return quantifier{Kind_Universal, selector, policy} +} + +func Any(selector selector.Selector, policy Policy) QuantifierStatement { + return quantifier{Kind_Existential, selector, policy} +} diff --git a/core/result/failure/datamodel/failure.go b/core/result/failure/datamodel/failure.go new file mode 100644 index 0000000..694ae32 --- /dev/null +++ b/core/result/failure/datamodel/failure.go @@ -0,0 +1,37 @@ +package datamodel + +import ( + // to use go:embed + _ "embed" + "fmt" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/schema" + ucanipld "github.com/storacha-network/go-ucanto/core/ipld" +) + +//go:embed failure.ipldsch +var failureSchema []byte + +// Failure is a generic failure +type Failure struct { + Name *string + Message string + Stack *string +} + +func (f *Failure) Build() (ipld.Node, error) { + return ucanipld.WrapWithRecovery(f, typ) +} + +var ( + typ schema.Type +) + +func init() { + ts, err := ipld.LoadSchemaBytes(failureSchema) + if err != nil { + panic(fmt.Errorf("loading failure schema: %w", err)) + } + typ = ts.TypeByName("Failure") +} diff --git a/core/result/failure/datamodel/failure.ipldsch b/core/result/failure/datamodel/failure.ipldsch new file mode 100644 index 0000000..243f336 --- /dev/null +++ b/core/result/failure/datamodel/failure.ipldsch @@ -0,0 +1,5 @@ +type Failure struct { + name optional String + message String + stack optional String +} \ No newline at end of file diff --git a/core/result/failure/faillure.go b/core/result/failure/faillure.go new file mode 100644 index 0000000..1caa0cb --- /dev/null +++ b/core/result/failure/faillure.go @@ -0,0 +1,100 @@ +package failure + +import ( + "fmt" + "runtime" + + "github.com/pkg/errors" + "github.com/storacha-network/go-ucanto/core/ipld" + "github.com/storacha-network/go-ucanto/core/result/failure/datamodel" +) + +// Named is an error that you can read a name from +type Named interface { + Name() string +} + +// WithStackTrace is an error that you can read a stack trace from +type WithStackTrace interface { + Stack() string +} + +// IPLDConvertableError is an error with a custom method to convert to an IPLD Node +type IPLDConvertableError interface { + error + ipld.Builder +} + +type Failure interface { + error + Named +} + +type NamedWithStackTrace interface { + Named + WithStackTrace +} + +type namedWithStackTrace struct { + name string + stack errors.StackTrace +} + +func (n namedWithStackTrace) Name() string { + return n.name +} + +func (n namedWithStackTrace) Stack() string { + return fmt.Sprintf("%+v", n.stack) +} + +func NamedWithCurrentStackTrace(name string) NamedWithStackTrace { + const depth = 32 + + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + + f := make(errors.StackTrace, n) + for i := 0; i < n; i++ { + f[i] = errors.Frame(pcs[i]) + } + + return namedWithStackTrace{name, f} +} + +type failure struct { + model datamodel.Failure +} + +func (f failure) Name() string { + return *f.model.Name +} + +func (f failure) Message() string { + return f.model.Message +} + +func (f failure) Error() string { + return f.model.Message +} + +func (f failure) Stack() string { + return *f.model.Stack +} + +func (f failure) Build() (ipld.Node, error) { + return f.model.Build() +} + +func FromError(err error) Failure { + model := datamodel.Failure{Message: err.Error()} + if named, ok := err.(Named); ok { + name := named.Name() + model.Name = &name + } + if withStackTrace, ok := err.(WithStackTrace); ok { + stack := withStackTrace.Stack() + model.Stack = &stack + } + return failure{model} +} diff --git a/core/schema/literal.go b/core/schema/literal.go new file mode 100644 index 0000000..3d33f98 --- /dev/null +++ b/core/schema/literal.go @@ -0,0 +1,19 @@ +package schema + +import ( + "fmt" + + "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" +) + +func Literal(expected string) Reader[string, string] { + return reader[string, string]{ + readFunc: func(input string) result.Result[string, failure.Failure] { + if input != expected { + return result.Error[string](NewSchemaError(fmt.Sprintf("expected literal %s instead got %s", expected, input))) + } + return result.Ok[string, failure.Failure](input) + }, + } +} diff --git a/validator/datamodel/attestation.go b/validator/datamodel/attestation.go new file mode 100644 index 0000000..bce2084 --- /dev/null +++ b/validator/datamodel/attestation.go @@ -0,0 +1,29 @@ +package datamodel + +import ( + _ "embed" + "fmt" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/schema" +) + +//go:embed errors.ipldsch +var attestationsch []byte +var attestationTypeSystem *schema.TypeSystem + +func init() { + ts, err := ipld.LoadSchemaBytes(attestationsch) + if err != nil { + panic(fmt.Errorf("failed to load IPLD schema: %s", err)) + } + attestationTypeSystem = ts +} + +func AttestationType() schema.Type { + return attestationTypeSystem.TypeByName("Attestation") +} + +type AttestationModel struct { + Proof ipld.Link +} diff --git a/validator/datamodel/attestration.ipldsch b/validator/datamodel/attestration.ipldsch new file mode 100644 index 0000000..e67d2ac --- /dev/null +++ b/validator/datamodel/attestration.ipldsch @@ -0,0 +1,3 @@ +type Attestation struct { + proof Link +} From 5bba918796d3eb6df2877f6e7d0246d73a69b919 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 20 Aug 2024 15:55:04 +0200 Subject: [PATCH 06/37] feat: better selector --- core/policy/literal/literal.go | 124 ++------- core/policy/match.go | 170 +++++------- core/policy/match_test.go | 10 + core/policy/selector/selector.go | 386 +++++++++++++++++++++++--- core/policy/selector/selector_test.go | 255 ++++++++++++----- core/policy/statement.go | 20 +- core/result/failure/faillure.go | 10 +- 7 files changed, 642 insertions(+), 333 deletions(-) diff --git a/core/policy/literal/literal.go b/core/policy/literal/literal.go index 61d949e..d5fe54e 100644 --- a/core/policy/literal/literal.go +++ b/core/policy/literal/literal.go @@ -1,124 +1,52 @@ package literal import ( - "fmt" - "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/node/basicnode" ) -var ErrType = fmt.Errorf("literal is not this type") - -const ( - Kind_IPLD = "ipld" - Kind_Int = "int" - Kind_Float = "float" - Kind_String = "string" -) - -type Literal interface { - Kind() string // ipld | integer | float | string - AsNode() (ipld.Node, error) - AsInt() (int64, error) - AsFloat() (float64, error) - AsString() (string, error) -} - -type literal struct{} - -func (l literal) AsFloat() (float64, error) { - return 0, ErrType -} - -func (l literal) AsInt() (int64, error) { - return 0, ErrType +func Node(n ipld.Node) ipld.Node { + return n } -func (l literal) AsNode() (datamodel.Node, error) { - return nil, ErrType -} - -func (l literal) AsString() (string, error) { - return "", ErrType -} - -type node struct { - literal - value ipld.Node -} - -func (l node) AsNode() (datamodel.Node, error) { - return l.value, nil -} - -func (l node) Kind() string { - return Kind_IPLD -} - -func Node(n ipld.Node) Literal { - return node{value: n} -} - -func Link(cid ipld.Link) Literal { +func Link(cid ipld.Link) ipld.Node { nb := basicnode.Prototype.Link.NewBuilder() nb.AssignLink(cid) - return node{value: nb.Build()} + return nb.Build() } -func Bool(val bool) Literal { +func Bool(val bool) ipld.Node { nb := basicnode.Prototype.Bool.NewBuilder() nb.AssignBool(val) - return node{value: nb.Build()} -} - -type nint struct { - literal - value int64 -} - -func (l nint) AsInt() (int64, error) { - return l.value, nil -} - -func (l nint) Kind() string { - return Kind_Int -} - -func Int(num int64) Literal { - return nint{value: num} -} - -type nfloat struct { - literal - value float64 -} - -func (l nfloat) AsFloat() (float64, error) { - return l.value, nil -} - -func (l nfloat) Kind() string { - return Kind_Float + return nb.Build() } -func Float(num float64) Literal { - return nfloat{value: num} +func Int(val int64) ipld.Node { + nb := basicnode.Prototype.Int.NewBuilder() + nb.AssignInt(val) + return nb.Build() } -type str struct { - literal - value string +func Float(val float64) ipld.Node { + nb := basicnode.Prototype.Float.NewBuilder() + nb.AssignFloat(val) + return nb.Build() } -func (l str) AsString() (string, error) { - return l.value, nil +func String(val string) ipld.Node { + nb := basicnode.Prototype.String.NewBuilder() + nb.AssignString(val) + return nb.Build() } -func (l str) Kind() string { - return Kind_String +func Bytes(val []byte) ipld.Node { + nb := basicnode.Prototype.Bytes.NewBuilder() + nb.AssignBytes(val) + return nb.Build() } -func String(s string) Literal { - return str{value: s} +func Null() ipld.Node { + nb := basicnode.Prototype.Any.NewBuilder() + nb.AssignNull() + return nb.Build() } diff --git a/core/policy/match.go b/core/policy/match.go index 63266a2..95203ec 100644 --- a/core/policy/match.go +++ b/core/policy/match.go @@ -5,8 +5,6 @@ import ( "fmt" "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/datamodel" - "github.com/storacha-network/go-ucanto/core/policy/literal" "github.com/storacha-network/go-ucanto/core/policy/selector" ) @@ -25,58 +23,43 @@ func matchStatement(statement Statement, node ipld.Node) (bool, error) { switch statement.Kind() { case Kind_Equal: if s, ok := statement.(EqualityStatement); ok { - n, err := selectNode(s.Selector(), node) - if err != nil { - if _, ok := err.(datamodel.ErrNotExists); ok { - return false, nil - } - return false, fmt.Errorf("selecting node: %w", err) + one, _, err := selector.Select(s.Selector(), node) + if err != nil || one == nil { + return false, nil } - return isDeepEqual(s.Value(), n) + return isDeepEqual(s.Value(), one) } case Kind_GreaterThan: if s, ok := statement.(InequalityStatement); ok { - n, err := selectNode(s.Selector(), node) - if err != nil { - if _, ok := err.(datamodel.ErrNotExists); ok { - return false, nil - } - return false, fmt.Errorf("selecting node: %w", err) + one, _, err := selector.Select(s.Selector(), node) + if err != nil || one == nil { + return false, nil } - return isOrdered(s, n, gt) + return isOrdered(s.Value(), one, gt) } case Kind_GreaterThanOrEqual: if s, ok := statement.(InequalityStatement); ok { - n, err := selectNode(s.Selector(), node) - if err != nil { - if _, ok := err.(datamodel.ErrNotExists); ok { - return false, nil - } - return false, fmt.Errorf("selecting node: %w", err) + one, _, err := selector.Select(s.Selector(), node) + if err != nil || one == nil { + return false, nil } - return isOrdered(s, n, gte) + return isOrdered(s.Value(), one, gte) } case Kind_LessThan: if s, ok := statement.(InequalityStatement); ok { - n, err := selectNode(s.Selector(), node) - if err != nil { - if _, ok := err.(datamodel.ErrNotExists); ok { - return false, nil - } - return false, fmt.Errorf("selecting node: %w", err) + one, _, err := selector.Select(s.Selector(), node) + if err != nil || one == nil { + return false, nil } - return isOrdered(s, n, lt) + return isOrdered(s.Value(), one, lt) } case Kind_LessThanOrEqual: if s, ok := statement.(InequalityStatement); ok { - n, err := selectNode(s.Selector(), node) - if err != nil { - if _, ok := err.(datamodel.ErrNotExists); ok { - return false, nil - } - return false, fmt.Errorf("selecting node: %w", err) + one, _, err := selector.Select(s.Selector(), node) + if err != nil || one == nil { + return false, nil } - return isOrdered(s, n, lte) + return isOrdered(s.Value(), one, lte) } case Kind_Negation: if s, ok := statement.(NegationStatement); ok { @@ -101,6 +84,9 @@ func matchStatement(statement Statement, node ipld.Node) (bool, error) { } case Kind_Disjunction: if s, ok := statement.(DisjunctionStatement); ok { + if len(s.Value()) == 0 { + return true, nil + } for _, cs := range s.Value() { r, err := matchStatement(cs, node) if err != nil { @@ -116,124 +102,102 @@ func matchStatement(statement Statement, node ipld.Node) (bool, error) { case Kind_Universal: case Kind_Existential: } - return false, fmt.Errorf("statement kind not implemented: %s", statement.Kind()) -} - -func selectNode(sel selector.Selector, node ipld.Node) (child ipld.Node, err error) { - if sel.Identity() { - child = node - } else if sel.Field() != "" { - child, err = node.LookupByString(sel.Field()) - } else { - child, err = node.LookupByIndex(int64(sel.Index())) - } - return + return false, fmt.Errorf("unimplemented statement kind: %s", statement.Kind()) } -func isOrdered(stmt InequalityStatement, node ipld.Node, satisfies func(order int) bool) (bool, error) { - if stmt.Value().Kind() == literal.Kind_Int && node.Kind() == ipld.Kind_Int { - a, err := node.AsInt() +func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) (bool, error) { + if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int { + a, err := actual.AsInt() if err != nil { return false, fmt.Errorf("extracting node int: %w", err) } - b, err := stmt.Value().AsInt() + b, err := expected.AsInt() if err != nil { return false, fmt.Errorf("extracting selector int: %w", err) } return satisfies(cmp.Compare(a, b)), nil } - if stmt.Value().Kind() == literal.Kind_Float && node.Kind() == ipld.Kind_Float { - a, err := node.AsFloat() + if expected.Kind() == ipld.Kind_Float && actual.Kind() == ipld.Kind_Float { + a, err := actual.AsFloat() if err != nil { return false, fmt.Errorf("extracting node float: %w", err) } - b, err := stmt.Value().AsFloat() + b, err := expected.AsFloat() if err != nil { return false, fmt.Errorf("extracting selector float: %w", err) } return satisfies(cmp.Compare(a, b)), nil } - return false, fmt.Errorf("selector type %s is not compatible with node type %s: kind mismatch: need int or float", stmt.Value().Kind(), node.Kind()) + return false, fmt.Errorf("unsupported IPLD kinds in ordered comparison: %s %s", expected.Kind(), actual.Kind()) } -func isDeepEqual(value literal.Literal, node ipld.Node) (bool, error) { - switch value.Kind() { - case literal.Kind_String: - if node.Kind() != ipld.Kind_String { - return false, nil - } - a, err := node.AsString() +func isDeepEqual(expected ipld.Node, actual ipld.Node) (bool, error) { + if expected.Kind() != actual.Kind() { + return false, nil + } + // TODO: should be easy enough to do the basic types, map, struct and list + // might be harder. + switch expected.Kind() { + case ipld.Kind_String: + a, err := actual.AsString() if err != nil { return false, fmt.Errorf("extracting node string: %w", err) } - b, err := value.AsString() + b, err := expected.AsString() if err != nil { return false, fmt.Errorf("extracting selector string: %w", err) } return a == b, nil - case literal.Kind_Int: - if node.Kind() != ipld.Kind_Int { + case ipld.Kind_Int: + if actual.Kind() != ipld.Kind_Int { return false, nil } - a, err := node.AsInt() + a, err := actual.AsInt() if err != nil { return false, fmt.Errorf("extracting node int: %w", err) } - b, err := value.AsInt() + b, err := expected.AsInt() if err != nil { return false, fmt.Errorf("extracting selector int: %w", err) } return a == b, nil - case literal.Kind_Float: - if node.Kind() != ipld.Kind_Float { + case ipld.Kind_Float: + if actual.Kind() != ipld.Kind_Float { return false, nil } - a, err := node.AsFloat() + a, err := actual.AsFloat() if err != nil { return false, fmt.Errorf("extracting node float: %w", err) } - b, err := value.AsFloat() + b, err := expected.AsFloat() if err != nil { return false, fmt.Errorf("extracting selector float: %w", err) } return a == b, nil - case literal.Kind_IPLD: - v, err := value.AsNode() + case ipld.Kind_Bool: + a, err := actual.AsBool() if err != nil { - return false, fmt.Errorf("extracting selector node: %w", err) + return false, fmt.Errorf("extracting node boolean: %w", err) } - if v.Kind() != node.Kind() { - return false, nil + b, err := expected.AsBool() + if err != nil { + return false, fmt.Errorf("extracting selector node boolean: %w", err) } - // TODO: should be easy enough to do the basic types, map, struct and list - // might be harder. - switch v.Kind() { - case ipld.Kind_Bool: - a, err := node.AsBool() - if err != nil { - return false, fmt.Errorf("extracting node boolean: %w", err) - } - b, err := v.AsBool() - if err != nil { - return false, fmt.Errorf("extracting selector node boolean: %w", err) - } - return a == b, nil - case ipld.Kind_Link: - a, err := node.AsLink() - if err != nil { - return false, fmt.Errorf("extracting node link: %w", err) - } - b, err := v.AsLink() - if err != nil { - return false, fmt.Errorf("extracting selector node link: %w", err) - } - return a.Binary() == b.Binary(), nil + return a == b, nil + case ipld.Kind_Link: + a, err := actual.AsLink() + if err != nil { + return false, fmt.Errorf("extracting node link: %w", err) + } + b, err := expected.AsLink() + if err != nil { + return false, fmt.Errorf("extracting selector node link: %w", err) } - return false, fmt.Errorf("unsupported IPLD kind: %s", v.Kind()) + return a.Binary() == b.Binary(), nil } - return false, fmt.Errorf("unknown literal kind: %s", value.Kind()) + return false, fmt.Errorf("unsupported IPLD kind in equality comparison: %s", expected.Kind()) } func gt(order int) bool { return order == 1 } diff --git a/core/policy/match_test.go b/core/policy/match_test.go index 5d9f1f7..69e27dd 100644 --- a/core/policy/match_test.go +++ b/core/policy/match_test.go @@ -252,6 +252,11 @@ func TestMatch(t *testing.T) { ok, err = Match(pol, nd) require.NoError(t, err) require.False(t, ok) + + pol = Policy{And()} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) }) t.Run("disjunction", func(t *testing.T) { @@ -279,5 +284,10 @@ func TestMatch(t *testing.T) { ok, err = Match(pol, nd) require.NoError(t, err) require.False(t, ok) + + pol = Policy{Or()} + ok, err = Match(pol, nd) + require.NoError(t, err) + require.True(t, ok) }) } diff --git a/core/policy/selector/selector.go b/core/policy/selector/selector.go index c00f840..373a5b8 100644 --- a/core/policy/selector/selector.go +++ b/core/policy/selector/selector.go @@ -2,103 +2,232 @@ package selector import ( "fmt" + "regexp" "strconv" "strings" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" ) // Selector describes a UCAN policy selector, as specified here: // https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#selectors -type Selector interface { +type Selector []Segment + +func (s Selector) String() string { + var str string + for _, seg := range s { + str += seg.String() + } + return str +} + +type Segment interface { // Identity flags that this selector is the identity selector. Identity() bool // Optional flags that this selector is optional. Optional() bool + // Iterator flags that this selector is an iterator segment. + Iterator() bool + // Slice flags that this segemnt targets a range of a slice. + Slice() []int // Field is the name of a field in a struct/map. Field() string // Index is an index of a slice. Index() int - // String returns the selector's string representation. + // String returns the segment's string representation. String() string } -type selector struct { +var Identity = Segment(segment{".", true, false, false, nil, "", 0}) + +type segment struct { str string identity bool optional bool + iterator bool + slice []int field string index int } -func (s selector) Field() string { - return s.field +func (s segment) String() string { + return s.str } -func (s selector) Identity() bool { +func (s segment) Identity() bool { return s.identity } -func (s selector) Index() int { - return s.index +func (s segment) Optional() bool { + return s.optional } -func (s selector) Optional() bool { - return s.optional +func (s segment) Iterator() bool { + return s.iterator } -func (s selector) String() string { - return s.str +func (s segment) Slice() []int { + return s.slice } -// TODO: probably regex or better parser -func Parse(sel string) (Selector, error) { - s := sel - if s == "." { - return selector{sel, true, false, "", 0}, nil - } +func (s segment) Field() string { + return s.field +} - optional := strings.HasSuffix(s, "?") - if optional { - s = s[0 : len(s)-1] - } +func (s segment) Index() int { + return s.index +} - dotted := strings.HasPrefix(s, ".") - if dotted { - s = s[1:] +func Parse(str string) (Selector, error) { + if string(str[0]) != "." { + return nil, NewParseError("selector must start with identity segment '.'", str, 0, string(str[0])) } - // collection values - if s == "[]" { - return nil, fmt.Errorf("unsupported selector: %s", sel) + col := 0 + var sel Selector + for _, tok := range tokenize(str) { + seg := tok + opt := strings.HasSuffix(tok, "?") + if opt { + seg = tok[0 : len(tok)-1] + } + switch seg { + case ".": + if len(sel) > 0 && sel[len(sel)-1].Identity() { + return nil, NewParseError("selector contains unsupported recursive descent segment: '..'", str, col, tok) + } + sel = append(sel, Identity) + case "[]": + sel = append(sel, segment{tok, false, opt, true, nil, "", 0}) + default: + if strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]") { + lookup := seg[1 : len(seg)-1] + + if regexp.MustCompile(`^-?\d+$`).MatchString(lookup) { // index + idx, err := strconv.Atoi(lookup) + if err != nil { + return nil, NewParseError("invalid index", str, col, tok) + } + sel = append(sel, segment{str: tok, optional: opt, index: idx}) + } else if strings.HasPrefix(lookup, "\"") && strings.HasSuffix(lookup, "\"") { // explicit field + sel = append(sel, segment{str: tok, optional: opt, field: lookup[1 : len(lookup)-1]}) + } else if regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`).MatchString(lookup) { // slice [3:5] or [:5] or [3:] + var rng []int + splt := strings.Split(lookup, ":") + if splt[0] == "" { + rng = append(rng, 0) + } else { + i, err := strconv.Atoi(splt[0]) + if err != nil { + return nil, NewParseError("invalid slice index", str, col, tok) + } + rng = append(rng, i) + } + if splt[1] != "" { + i, err := strconv.Atoi(splt[1]) + if err != nil { + return nil, NewParseError("invalid slice index", str, col, tok) + } + rng = append(rng, i) + } + sel = append(sel, segment{str: tok, optional: opt, slice: rng}) + } else { + return nil, NewParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok) + } + } else if regexp.MustCompile(`^\.[a-zA-Z_]*?$`).MatchString(seg) { + sel = append(sel, segment{str: tok, optional: opt, field: seg[1:]}) + } else { + return nil, NewParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok) + } + } + col += len(tok) } + return sel, nil +} - if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") { - s = s[1 : len(s)-1] +func tokenize(str string) []string { + var toks []string + col := 0 + ofs := 0 + ctx := "" - // explicit field selector - if strings.HasPrefix(s, "\"") && strings.HasSuffix(s, "\"") { - return selector{sel, false, optional, s[1 : len(s)-1], 0}, nil - } + for col < len(str) { + char := string(str[col]) - // collection range - if strings.Contains(s, ":") { - return nil, fmt.Errorf("unsupported selector: %s", sel) + if char == "\"" && string(str[col-1]) != "\\" { + col++ + if ctx == "\"" { + ctx = "" + } else { + ctx = "\"" + } + continue } - // index selector - idx, err := strconv.Atoi(s) - if err != nil { - return nil, fmt.Errorf("parsing index selector value: %s", err) + if ctx == "\"" { + col++ + continue } - return selector{sel, false, optional, "", idx}, nil + if char == "." || char == "[" { + if ofs < col { + toks = append(toks, str[ofs:col]) + } + ofs = col + } + col++ } - if !dotted { - return nil, fmt.Errorf("invalid selector: %s", sel) + if ofs < col && ctx != "\"" { + toks = append(toks, str[ofs:col]) } - // dotted field selector - return selector{sel, false, optional, s, 0}, nil + return toks +} + +type ParseError interface { + error + Name() string + Message() string + Source() string + Column() int + Token() string +} + +type parseerr struct { + msg string + src string + col int + tok string +} + +func (p parseerr) Name() string { + return "ParseError" +} + +func (p parseerr) Message() string { + return p.msg +} + +func (p parseerr) Column() int { + return p.col +} + +func (p parseerr) Error() string { + return p.msg +} + +func (p parseerr) Source() string { + return p.src +} + +func (p parseerr) Token() string { + return p.tok +} + +func NewParseError(message string, source string, column int, token string) error { + return parseerr{message, source, column, token} } func MustParse(sel string) Selector { @@ -108,3 +237,168 @@ func MustParse(sel string) Selector { } return s } + +func Select(sel Selector, subject ipld.Node) (ipld.Node, []ipld.Node, error) { + return resolve(sel, subject, nil) +} + +func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.Node, error) { + cur := subject + for i, seg := range sel { + if seg.Identity() { + continue + } else if seg.Iterator() { + if cur != nil && cur.Kind() == datamodel.Kind_List { + var many []ipld.Node + it := cur.ListIterator() + for { + if it.Done() { + break + } + + i, v, err := it.Next() + if err != nil { + return nil, nil, err + } + + key := fmt.Sprintf("%d", i) + o, m, err := resolve(sel[i+1:], v, append(at[:], key)) + if err != nil { + return nil, nil, err + } + + if m != nil { + many = append(many, m...) + } else { + many = append(many, o) + } + } + return nil, many, nil + } else if cur != nil && cur.Kind() == datamodel.Kind_Map { + var many []ipld.Node + it := cur.MapIterator() + for { + if it.Done() { + break + } + + k, v, err := it.Next() + if err != nil { + return nil, nil, err + } + + key, _ := k.AsString() + o, m, err := resolve(sel[i+1:], v, append(at[:], key)) + if err != nil { + return nil, nil, err + } + + if m != nil { + many = append(many, m...) + } else { + many = append(many, o) + } + } + return nil, many, nil + } else if seg.Optional() { + cur = nil + } else { + return nil, nil, NewResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at) + } + + } else if seg.Field() != "" { + at = append(at, seg.Field()) + if cur != nil && cur.Kind() == datamodel.Kind_Map { + n, err := cur.LookupByString(seg.Field()) + if err != nil { + if _, ok := err.(datamodel.ErrNotExists); ok { + if seg.Optional() { + cur = nil + } else { + return nil, nil, NewResolutionError(fmt.Sprintf("object has no field named: %s", seg.Field()), at) + } + } else { + return nil, nil, err + } + } + cur = n + } else if seg.Optional() { + cur = nil + } else { + return nil, nil, NewResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at) + } + } else if seg.Slice() != nil { + if cur != nil && cur.Kind() == datamodel.Kind_List { + return nil, nil, NewResolutionError("list slice selection not yet implemented", at) + } else if cur != nil && cur.Kind() == datamodel.Kind_Bytes { + return nil, nil, NewResolutionError("bytes slice selection not yet implemented", at) + } else if seg.Optional() { + cur = nil + } else { + return nil, nil, NewResolutionError(fmt.Sprintf("can not index: %s on kind: %s", seg.Field(), kindString(cur)), at) + } + } else { + at = append(at, fmt.Sprintf("%d", seg.Index())) + if cur != nil && cur.Kind() == datamodel.Kind_List { + n, err := cur.LookupByIndex(int64(seg.Index())) + if err != nil { + if _, ok := err.(datamodel.ErrNotExists); ok { + if seg.Optional() { + cur = nil + } else { + return nil, nil, NewResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at) + } + } else { + return nil, nil, err + } + } + cur = n + } else if seg.Optional() { + cur = nil + } else { + return nil, nil, NewResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at) + } + } + } + + return cur, nil, nil +} + +func kindString(n datamodel.Node) string { + if n == nil { + return "null" + } + return n.Kind().String() +} + +type ResolutionError interface { + error + Name() string + Message() string + At() []string +} + +type resolutionerr struct { + msg string + at []string +} + +func (r resolutionerr) Name() string { + return "ResolutionError" +} + +func (r resolutionerr) Message() string { + return fmt.Sprintf("can not resolve path: .%s", strings.Join(r.at, ".")) +} + +func (r resolutionerr) At() []string { + return r.at +} + +func (r resolutionerr) Error() string { + return r.Message() +} + +func NewResolutionError(message string, at []string) error { + return resolutionerr{message, at} +} diff --git a/core/policy/selector/selector_test.go b/core/policy/selector/selector_test.go index d8fab0b..3173cd1 100644 --- a/core/policy/selector/selector_test.go +++ b/core/policy/selector/selector_test.go @@ -11,124 +11,229 @@ func TestParse(t *testing.T) { t.Run("identity", func(t *testing.T) { sel, err := Parse(".") require.NoError(t, err) - require.True(t, sel.Identity()) - require.False(t, sel.Optional()) - require.Empty(t, sel.Field()) - require.Empty(t, sel.Index()) + require.Equal(t, 1, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) }) - t.Run("dotted field", func(t *testing.T) { + t.Run("field", func(t *testing.T) { sel, err := Parse(".foo") require.NoError(t, err) - require.False(t, sel.Identity()) - require.False(t, sel.Optional()) - require.Equal(t, sel.Field(), "foo") - require.Empty(t, sel.Index()) + require.Equal(t, 1, len(sel)) + require.False(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Equal(t, sel[0].Field(), "foo") + require.Empty(t, sel[0].Index()) }) - t.Run("dotted explicit field", func(t *testing.T) { - sel, err := Parse(".[\"foo\"]") + t.Run("explicit field", func(t *testing.T) { + sel, err := Parse(`.["foo"]`) require.NoError(t, err) - require.False(t, sel.Identity()) - require.False(t, sel.Optional()) - require.Equal(t, sel.Field(), "foo") - require.Empty(t, sel.Index()) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.False(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Equal(t, sel[1].Field(), "foo") + require.Empty(t, sel[1].Index()) }) - t.Run("dotted index", func(t *testing.T) { + t.Run("index", func(t *testing.T) { sel, err := Parse(".[138]") require.NoError(t, err) - require.False(t, sel.Identity()) - require.False(t, sel.Optional()) - require.Empty(t, sel.Field()) - require.Equal(t, sel.Index(), 138) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.False(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Empty(t, sel[1].Field()) + require.Equal(t, sel[1].Index(), 138) }) - t.Run("explicit field", func(t *testing.T) { - sel, err := Parse("[\"foo\"]") + t.Run("negative index", func(t *testing.T) { + sel, err := Parse(".[-138]") require.NoError(t, err) - require.False(t, sel.Identity()) - require.False(t, sel.Optional()) - require.Equal(t, sel.Field(), "foo") - require.Empty(t, sel.Index()) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.False(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Empty(t, sel[1].Field()) + require.Equal(t, sel[1].Index(), -138) }) - t.Run("index", func(t *testing.T) { - sel, err := Parse("[138]") + t.Run("iterator", func(t *testing.T) { + sel, err := Parse(".[]") require.NoError(t, err) - require.False(t, sel.Identity()) - require.False(t, sel.Optional()) - require.Empty(t, sel.Field()) - require.Equal(t, sel.Index(), 138) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.False(t, sel[1].Optional()) + require.True(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Empty(t, sel[1].Field()) + require.Empty(t, sel[1].Index()) }) - t.Run("negative index", func(t *testing.T) { - sel, err := Parse("[-138]") - require.NoError(t, err) - require.False(t, sel.Identity()) - require.False(t, sel.Optional()) - require.Empty(t, sel.Field()) - require.Equal(t, sel.Index(), -138) - }) - - t.Run("optional dotted field", func(t *testing.T) { + t.Run("optional field", func(t *testing.T) { sel, err := Parse(".foo?") require.NoError(t, err) - require.False(t, sel.Identity()) - require.True(t, sel.Optional()) - require.Equal(t, sel.Field(), "foo") - require.Empty(t, sel.Index()) + require.Equal(t, 1, len(sel)) + require.False(t, sel[0].Identity()) + require.True(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Equal(t, sel[0].Field(), "foo") + require.Empty(t, sel[0].Index()) }) - t.Run("optional dotted explicit field", func(t *testing.T) { - sel, err := Parse(".[\"foo\"]?") + t.Run("optional explicit field", func(t *testing.T) { + sel, err := Parse(`.["foo"]?`) require.NoError(t, err) - require.False(t, sel.Identity()) - require.True(t, sel.Optional()) - require.Equal(t, sel.Field(), "foo") - require.Empty(t, sel.Index()) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.True(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Equal(t, sel[1].Field(), "foo") + require.Empty(t, sel[1].Index()) }) - t.Run("optional dotted index", func(t *testing.T) { + t.Run("optional index", func(t *testing.T) { sel, err := Parse(".[138]?") require.NoError(t, err) - require.False(t, sel.Identity()) - require.True(t, sel.Optional()) - require.Empty(t, sel.Field()) - require.Equal(t, sel.Index(), 138) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.True(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Empty(t, sel[1].Field()) + require.Equal(t, sel[1].Index(), 138) }) - t.Run("optional explicit field", func(t *testing.T) { - sel, err := Parse("[\"foo\"]?") + t.Run("optional iterator", func(t *testing.T) { + sel, err := Parse(".[]?") require.NoError(t, err) - require.False(t, sel.Identity()) - require.True(t, sel.Optional()) - require.Equal(t, sel.Field(), "foo") - require.Empty(t, sel.Index()) + require.Equal(t, 2, len(sel)) + require.True(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Empty(t, sel[0].Field()) + require.Empty(t, sel[0].Index()) + require.False(t, sel[1].Identity()) + require.True(t, sel[1].Optional()) + require.True(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Empty(t, sel[1].Field()) + require.Empty(t, sel[1].Index()) }) - t.Run("optional index", func(t *testing.T) { - sel, err := Parse("[138]?") + t.Run("nesting", func(t *testing.T) { + sel, err := Parse(`.foo.["bar"].[138]?.baz[1:]`) require.NoError(t, err) - require.False(t, sel.Identity()) - require.True(t, sel.Optional()) - require.Empty(t, sel.Field()) - require.Equal(t, sel.Index(), 138) + printSegments(sel) + require.Equal(t, 7, len(sel)) + require.False(t, sel[0].Identity()) + require.False(t, sel[0].Optional()) + require.False(t, sel[0].Iterator()) + require.Empty(t, sel[0].Slice()) + require.Equal(t, sel[0].Field(), "foo") + require.Empty(t, sel[0].Index()) + require.True(t, sel[1].Identity()) + require.False(t, sel[1].Optional()) + require.False(t, sel[1].Iterator()) + require.Empty(t, sel[1].Slice()) + require.Empty(t, sel[1].Field()) + require.Empty(t, sel[1].Index()) + require.False(t, sel[2].Identity()) + require.False(t, sel[2].Optional()) + require.False(t, sel[2].Iterator()) + require.Empty(t, sel[2].Slice()) + require.Equal(t, sel[2].Field(), "bar") + require.Empty(t, sel[2].Index()) + require.True(t, sel[3].Identity()) + require.False(t, sel[3].Optional()) + require.False(t, sel[3].Iterator()) + require.Empty(t, sel[3].Slice()) + require.Empty(t, sel[3].Field()) + require.Empty(t, sel[3].Index()) + require.False(t, sel[4].Identity()) + require.True(t, sel[4].Optional()) + require.False(t, sel[4].Iterator()) + require.Empty(t, sel[4].Slice()) + require.Empty(t, sel[4].Field()) + require.Equal(t, sel[4].Index(), 138) + require.False(t, sel[5].Identity()) + require.False(t, sel[5].Optional()) + require.False(t, sel[5].Iterator()) + require.Empty(t, sel[5].Slice()) + require.Equal(t, sel[5].Field(), "baz") + require.Empty(t, sel[5].Index()) + require.False(t, sel[6].Identity()) + require.False(t, sel[6].Optional()) + require.False(t, sel[6].Iterator()) + require.Equal(t, sel[6].Slice(), []int{1}) + require.Empty(t, sel[6].Field()) + require.Empty(t, sel[6].Index()) }) t.Run("non dotted", func(t *testing.T) { _, err := Parse("foo") - if err == nil { - t.Fatalf("expected error parsing selector") - } + require.NotNil(t, err) fmt.Println(err) }) t.Run("non quoted", func(t *testing.T) { _, err := Parse(".[foo]") - if err == nil { - t.Fatalf("expected error parsing selector") - } + require.NotNil(t, err) fmt.Println(err) }) } + +func printSegments(s Selector) { + for i, seg := range s { + fmt.Printf("%d: %s\n", i, seg.String()) + } +} diff --git a/core/policy/statement.go b/core/policy/statement.go index cbedc9f..26201fa 100644 --- a/core/policy/statement.go +++ b/core/policy/statement.go @@ -3,7 +3,7 @@ package policy // https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy import ( - "github.com/storacha-network/go-ucanto/core/policy/literal" + "github.com/ipld/go-ipld-prime" "github.com/storacha-network/go-ucanto/core/policy/selector" ) @@ -30,13 +30,13 @@ type Statement interface { type EqualityStatement interface { Statement Selector() selector.Selector - Value() literal.Literal + Value() ipld.Node } type InequalityStatement interface { Statement Selector() selector.Selector - Value() literal.Literal + Value() ipld.Node } type WildcardStatement interface { @@ -73,14 +73,14 @@ type QuantifierStatement interface { type equality struct { kind string selector selector.Selector - value literal.Literal + value ipld.Node } func (e equality) Kind() string { return e.kind } -func (e equality) Value() literal.Literal { +func (e equality) Value() ipld.Node { return e.value } @@ -88,23 +88,23 @@ func (e equality) Selector() selector.Selector { return e.selector } -func Equal(selector selector.Selector, value literal.Literal) EqualityStatement { +func Equal(selector selector.Selector, value ipld.Node) EqualityStatement { return equality{Kind_Equal, selector, value} } -func GreaterThan(selector selector.Selector, value literal.Literal) InequalityStatement { +func GreaterThan(selector selector.Selector, value ipld.Node) InequalityStatement { return equality{Kind_GreaterThan, selector, value} } -func GreaterThanOrEqual(selector selector.Selector, value literal.Literal) InequalityStatement { +func GreaterThanOrEqual(selector selector.Selector, value ipld.Node) InequalityStatement { return equality{Kind_GreaterThanOrEqual, selector, value} } -func LessThan(selector selector.Selector, value literal.Literal) InequalityStatement { +func LessThan(selector selector.Selector, value ipld.Node) InequalityStatement { return equality{Kind_LessThan, selector, value} } -func LessThanOrEqual(selector selector.Selector, value literal.Literal) InequalityStatement { +func LessThanOrEqual(selector selector.Selector, value ipld.Node) InequalityStatement { return equality{Kind_LessThanOrEqual, selector, value} } diff --git a/core/result/failure/faillure.go b/core/result/failure/faillure.go index 1caa0cb..4d4ca4a 100644 --- a/core/result/failure/faillure.go +++ b/core/result/failure/faillure.go @@ -64,6 +64,7 @@ func NamedWithCurrentStackTrace(name string) NamedWithStackTrace { type failure struct { model datamodel.Failure + build func() (ipld.Node, error) } func (f failure) Name() string { @@ -83,6 +84,9 @@ func (f failure) Stack() string { } func (f failure) Build() (ipld.Node, error) { + if f.build != nil { + return f.build() + } return f.model.Build() } @@ -96,5 +100,9 @@ func FromError(err error) Failure { stack := withStackTrace.Stack() model.Stack = &stack } - return failure{model} + fail := failure{model: model} + if builder, ok := err.(ipld.Builder); ok { + fail.build = builder.Build + } + return fail } From 0f7760f008b261738916ca4e9bf301a5fc992cb2 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 20 Aug 2024 22:27:56 +0200 Subject: [PATCH 07/37] feat: simplify --- core/policy/match.go | 143 ++++------------- core/policy/match_test.go | 122 +++++++-------- core/policy/{statement.go => policy.go} | 0 core/policy/selector/selector.go | 24 ++- core/policy/selector/selector_test.go | 194 +++++++++++++++++++++++- core/schema/struct.go | 5 +- 6 files changed, 303 insertions(+), 185 deletions(-) rename core/policy/{statement.go => policy.go} (100%) diff --git a/core/policy/match.go b/core/policy/match.go index 95203ec..950ef23 100644 --- a/core/policy/match.go +++ b/core/policy/match.go @@ -5,35 +5,37 @@ import ( "fmt" "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/must" "github.com/storacha-network/go-ucanto/core/policy/selector" ) // Match determines if the IPLD node matches the policy document. -func Match(policy Policy, node ipld.Node) (bool, error) { +func Match(policy Policy, node ipld.Node) bool { for _, stmt := range policy { - ok, err := matchStatement(stmt, node) - if err != nil || !ok { - return ok, err + ok := matchStatement(stmt, node) + if !ok { + return ok } } - return true, nil + return true } -func matchStatement(statement Statement, node ipld.Node) (bool, error) { +func matchStatement(statement Statement, node ipld.Node) bool { switch statement.Kind() { case Kind_Equal: if s, ok := statement.(EqualityStatement); ok { one, _, err := selector.Select(s.Selector(), node) if err != nil || one == nil { - return false, nil + return false } - return isDeepEqual(s.Value(), one) + return datamodel.DeepEqual(s.Value(), one) } case Kind_GreaterThan: if s, ok := statement.(InequalityStatement); ok { one, _, err := selector.Select(s.Selector(), node) if err != nil || one == nil { - return false, nil + return false } return isOrdered(s.Value(), one, gt) } @@ -41,7 +43,7 @@ func matchStatement(statement Statement, node ipld.Node) (bool, error) { if s, ok := statement.(InequalityStatement); ok { one, _, err := selector.Select(s.Selector(), node) if err != nil || one == nil { - return false, nil + return false } return isOrdered(s.Value(), one, gte) } @@ -49,7 +51,7 @@ func matchStatement(statement Statement, node ipld.Node) (bool, error) { if s, ok := statement.(InequalityStatement); ok { one, _, err := selector.Select(s.Selector(), node) if err != nil || one == nil { - return false, nil + return false } return isOrdered(s.Value(), one, lt) } @@ -57,147 +59,64 @@ func matchStatement(statement Statement, node ipld.Node) (bool, error) { if s, ok := statement.(InequalityStatement); ok { one, _, err := selector.Select(s.Selector(), node) if err != nil || one == nil { - return false, nil + return false } return isOrdered(s.Value(), one, lte) } case Kind_Negation: if s, ok := statement.(NegationStatement); ok { - r, err := matchStatement(s.Value(), node) - if err != nil { - return false, err - } - return !r, err + return !matchStatement(s.Value(), node) } case Kind_Conjunction: if s, ok := statement.(ConjunctionStatement); ok { for _, cs := range s.Value() { - r, err := matchStatement(cs, node) - if err != nil { - return false, err - } + r := matchStatement(cs, node) if !r { - return false, nil + return false } } - return true, nil + return true } case Kind_Disjunction: if s, ok := statement.(DisjunctionStatement); ok { if len(s.Value()) == 0 { - return true, nil + return true } for _, cs := range s.Value() { - r, err := matchStatement(cs, node) - if err != nil { - return false, err - } + r := matchStatement(cs, node) if r { - return true, nil + return true } } - return false, nil + return false } case Kind_Wildcard: case Kind_Universal: case Kind_Existential: } - return false, fmt.Errorf("unimplemented statement kind: %s", statement.Kind()) + panic(fmt.Errorf("unimplemented statement kind: %s", statement.Kind())) } -func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) (bool, error) { +func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool { if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int { - a, err := actual.AsInt() - if err != nil { - return false, fmt.Errorf("extracting node int: %w", err) - } - b, err := expected.AsInt() - if err != nil { - return false, fmt.Errorf("extracting selector int: %w", err) - } - return satisfies(cmp.Compare(a, b)), nil + a := must.Int(actual) + b := must.Int(expected) + return satisfies(cmp.Compare(a, b)) } if expected.Kind() == ipld.Kind_Float && actual.Kind() == ipld.Kind_Float { a, err := actual.AsFloat() if err != nil { - return false, fmt.Errorf("extracting node float: %w", err) + panic(fmt.Errorf("extracting node float: %w", err)) } b, err := expected.AsFloat() if err != nil { - return false, fmt.Errorf("extracting selector float: %w", err) + panic(fmt.Errorf("extracting selector float: %w", err)) } - return satisfies(cmp.Compare(a, b)), nil + return satisfies(cmp.Compare(a, b)) } - return false, fmt.Errorf("unsupported IPLD kinds in ordered comparison: %s %s", expected.Kind(), actual.Kind()) -} - -func isDeepEqual(expected ipld.Node, actual ipld.Node) (bool, error) { - if expected.Kind() != actual.Kind() { - return false, nil - } - // TODO: should be easy enough to do the basic types, map, struct and list - // might be harder. - switch expected.Kind() { - case ipld.Kind_String: - a, err := actual.AsString() - if err != nil { - return false, fmt.Errorf("extracting node string: %w", err) - } - b, err := expected.AsString() - if err != nil { - return false, fmt.Errorf("extracting selector string: %w", err) - } - return a == b, nil - case ipld.Kind_Int: - if actual.Kind() != ipld.Kind_Int { - return false, nil - } - a, err := actual.AsInt() - if err != nil { - return false, fmt.Errorf("extracting node int: %w", err) - } - b, err := expected.AsInt() - if err != nil { - return false, fmt.Errorf("extracting selector int: %w", err) - } - return a == b, nil - case ipld.Kind_Float: - if actual.Kind() != ipld.Kind_Float { - return false, nil - } - a, err := actual.AsFloat() - if err != nil { - return false, fmt.Errorf("extracting node float: %w", err) - } - b, err := expected.AsFloat() - if err != nil { - return false, fmt.Errorf("extracting selector float: %w", err) - } - return a == b, nil - case ipld.Kind_Bool: - a, err := actual.AsBool() - if err != nil { - return false, fmt.Errorf("extracting node boolean: %w", err) - } - b, err := expected.AsBool() - if err != nil { - return false, fmt.Errorf("extracting selector node boolean: %w", err) - } - return a == b, nil - case ipld.Kind_Link: - a, err := actual.AsLink() - if err != nil { - return false, fmt.Errorf("extracting node link: %w", err) - } - b, err := expected.AsLink() - if err != nil { - return false, fmt.Errorf("extracting selector node link: %w", err) - } - return a.Binary() == b.Binary(), nil - } - return false, fmt.Errorf("unsupported IPLD kind in equality comparison: %s", expected.Kind()) + return false } func gt(order int) bool { return order == 1 } diff --git a/core/policy/match_test.go b/core/policy/match_test.go index 69e27dd..183f1bf 100644 --- a/core/policy/match_test.go +++ b/core/policy/match_test.go @@ -19,18 +19,15 @@ func TestMatch(t *testing.T) { nd := nb.Build() pol := Policy{Equal(selector.MustParse("."), literal.String("test"))} - ok, err := Match(pol, nd) - require.NoError(t, err) + ok := Match(pol, nd) require.True(t, ok) pol = Policy{Equal(selector.MustParse("."), literal.String("test2"))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.False(t, ok) pol = Policy{Equal(selector.MustParse("."), literal.Int(138))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.False(t, ok) }) @@ -41,18 +38,15 @@ func TestMatch(t *testing.T) { nd := nb.Build() pol := Policy{Equal(selector.MustParse("."), literal.Int(138))} - ok, err := Match(pol, nd) - require.NoError(t, err) + ok := Match(pol, nd) require.True(t, ok) pol = Policy{Equal(selector.MustParse("."), literal.Int(1138))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.False(t, ok) pol = Policy{Equal(selector.MustParse("."), literal.String("138"))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.False(t, ok) }) @@ -63,18 +57,15 @@ func TestMatch(t *testing.T) { nd := nb.Build() pol := Policy{Equal(selector.MustParse("."), literal.Float(1.138))} - ok, err := Match(pol, nd) - require.NoError(t, err) + ok := Match(pol, nd) require.True(t, ok) pol = Policy{Equal(selector.MustParse("."), literal.Float(11.38))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.False(t, ok) pol = Policy{Equal(selector.MustParse("."), literal.String("138"))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.False(t, ok) }) @@ -88,18 +79,15 @@ func TestMatch(t *testing.T) { nd := nb.Build() pol := Policy{Equal(selector.MustParse("."), literal.Link(l0))} - ok, err := Match(pol, nd) - require.NoError(t, err) + ok := Match(pol, nd) require.True(t, ok) pol = Policy{Equal(selector.MustParse("."), literal.Link(l1))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.False(t, ok) pol = Policy{Equal(selector.MustParse("."), literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq"))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.False(t, ok) }) @@ -113,23 +101,19 @@ func TestMatch(t *testing.T) { nd := nb.Build() pol := Policy{Equal(selector.MustParse(".foo"), literal.String("bar"))} - ok, err := Match(pol, nd) - require.NoError(t, err) + ok := Match(pol, nd) require.True(t, ok) pol = Policy{Equal(selector.MustParse(".[\"foo\"]"), literal.String("bar"))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.True(t, ok) pol = Policy{Equal(selector.MustParse(".foo"), literal.String("baz"))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.False(t, ok) pol = Policy{Equal(selector.MustParse(".foobar"), literal.String("bar"))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.False(t, ok) }) @@ -142,13 +126,11 @@ func TestMatch(t *testing.T) { nd := nb.Build() pol := Policy{Equal(selector.MustParse(".[0]"), literal.String("foo"))} - ok, err := Match(pol, nd) - require.NoError(t, err) + ok := Match(pol, nd) require.True(t, ok) pol = Policy{Equal(selector.MustParse(".[1]"), literal.String("foo"))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.False(t, ok) }) @@ -159,8 +141,7 @@ func TestMatch(t *testing.T) { nd := nb.Build() pol := Policy{GreaterThan(selector.MustParse("."), literal.Int(1))} - ok, err := Match(pol, nd) - require.NoError(t, err) + ok := Match(pol, nd) require.True(t, ok) }) @@ -171,13 +152,11 @@ func TestMatch(t *testing.T) { nd := nb.Build() pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(1))} - ok, err := Match(pol, nd) - require.NoError(t, err) + ok := Match(pol, nd) require.True(t, ok) pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(138))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.True(t, ok) }) @@ -188,8 +167,7 @@ func TestMatch(t *testing.T) { nd := nb.Build() pol := Policy{GreaterThan(selector.MustParse("."), literal.Float(1))} - ok, err := Match(pol, nd) - require.NoError(t, err) + ok := Match(pol, nd) require.True(t, ok) }) @@ -200,13 +178,37 @@ func TestMatch(t *testing.T) { nd := nb.Build() pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1))} - ok, err := Match(pol, nd) - require.NoError(t, err) + ok := Match(pol, nd) require.True(t, ok) pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1.38))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) + require.True(t, ok) + }) + + t.Run("inequality lt int", func(t *testing.T) { + np := basicnode.Prototype.Int + nb := np.NewBuilder() + nb.AssignInt(138) + nd := nb.Build() + + pol := Policy{LessThan(selector.MustParse("."), literal.Int(1138))} + ok := Match(pol, nd) + require.True(t, ok) + }) + + t.Run("inequality lte int", func(t *testing.T) { + np := basicnode.Prototype.Int + nb := np.NewBuilder() + nb.AssignInt(138) + nd := nb.Build() + + pol := Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(1138))} + ok := Match(pol, nd) + require.True(t, ok) + + pol = Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(138))} + ok = Match(pol, nd) require.True(t, ok) }) @@ -217,13 +219,11 @@ func TestMatch(t *testing.T) { nd := nb.Build() pol := Policy{Not(Equal(selector.MustParse("."), literal.Bool(true)))} - ok, err := Match(pol, nd) - require.NoError(t, err) + ok := Match(pol, nd) require.True(t, ok) pol = Policy{Not(Equal(selector.MustParse("."), literal.Bool(false)))} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.False(t, ok) }) @@ -239,8 +239,7 @@ func TestMatch(t *testing.T) { LessThan(selector.MustParse("."), literal.Int(1138)), ), } - ok, err := Match(pol, nd) - require.NoError(t, err) + ok := Match(pol, nd) require.True(t, ok) pol = Policy{ @@ -249,13 +248,11 @@ func TestMatch(t *testing.T) { Equal(selector.MustParse("."), literal.Int(1138)), ), } - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.False(t, ok) pol = Policy{And()} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.True(t, ok) }) @@ -271,8 +268,7 @@ func TestMatch(t *testing.T) { LessThan(selector.MustParse("."), literal.Int(1138)), ), } - ok, err := Match(pol, nd) - require.NoError(t, err) + ok := Match(pol, nd) require.True(t, ok) pol = Policy{ @@ -281,13 +277,11 @@ func TestMatch(t *testing.T) { Equal(selector.MustParse("."), literal.Int(1138)), ), } - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.False(t, ok) pol = Policy{Or()} - ok, err = Match(pol, nd) - require.NoError(t, err) + ok = Match(pol, nd) require.True(t, ok) }) } diff --git a/core/policy/statement.go b/core/policy/policy.go similarity index 100% rename from core/policy/statement.go rename to core/policy/policy.go diff --git a/core/policy/selector/selector.go b/core/policy/selector/selector.go index 373a5b8..5f9d9a5 100644 --- a/core/policy/selector/selector.go +++ b/core/policy/selector/selector.go @@ -8,6 +8,7 @@ import ( "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/schema" ) // Selector describes a UCAN policy selector, as specified here: @@ -238,6 +239,8 @@ func MustParse(sel string) Selector { return s } +// Select uses a selector to extract an IPLD node or set of nodes from the +// passed subject node. func Select(sel Selector, subject ipld.Node) (ipld.Node, []ipld.Node, error) { return resolve(sel, subject, nil) } @@ -256,12 +259,12 @@ func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.No break } - i, v, err := it.Next() + k, v, err := it.Next() if err != nil { return nil, nil, err } - key := fmt.Sprintf("%d", i) + key := fmt.Sprintf("%d", k) o, m, err := resolve(sel[i+1:], v, append(at[:], key)) if err != nil { return nil, nil, err @@ -311,7 +314,7 @@ func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.No if cur != nil && cur.Kind() == datamodel.Kind_Map { n, err := cur.LookupByString(seg.Field()) if err != nil { - if _, ok := err.(datamodel.ErrNotExists); ok { + if isMissing(err) { if seg.Optional() { cur = nil } else { @@ -342,7 +345,7 @@ func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.No if cur != nil && cur.Kind() == datamodel.Kind_List { n, err := cur.LookupByIndex(int64(seg.Index())) if err != nil { - if _, ok := err.(datamodel.ErrNotExists); ok { + if isMissing(err) { if seg.Optional() { cur = nil } else { @@ -371,6 +374,19 @@ func kindString(n datamodel.Node) string { return n.Kind().String() } +func isMissing(err error) bool { + if _, ok := err.(datamodel.ErrNotExists); ok { + return true + } + if _, ok := err.(schema.ErrNoSuchField); ok { + return true + } + if _, ok := err.(schema.ErrInvalidKey); ok { + return true + } + return false +} + type ResolutionError interface { error Name() string diff --git a/core/policy/selector/selector_test.go b/core/policy/selector/selector_test.go index 3173cd1..b19282f 100644 --- a/core/policy/selector/selector_test.go +++ b/core/policy/selector/selector_test.go @@ -4,6 +4,10 @@ import ( "fmt" "testing" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/must" + "github.com/ipld/go-ipld-prime/node/bindnode" + "github.com/ipld/go-ipld-prime/printer" "github.com/stretchr/testify/require" ) @@ -171,9 +175,11 @@ func TestParse(t *testing.T) { }) t.Run("nesting", func(t *testing.T) { - sel, err := Parse(`.foo.["bar"].[138]?.baz[1:]`) + str := `.foo.["bar"].[138]?.baz[1:]` + sel, err := Parse(str) require.NoError(t, err) printSegments(sel) + require.Equal(t, str, sel.String()) require.Equal(t, 7, len(sel)) require.False(t, sel[0].Identity()) require.False(t, sel[0].Optional()) @@ -237,3 +243,189 @@ func printSegments(s Selector) { fmt.Printf("%d: %s\n", i, seg.String()) } } + +func TestSelect(t *testing.T) { + type name struct { + First string + Middle *string + Last string + } + type interest struct { + Name string + Outdoor bool + Experience int + } + type user struct { + Name name + Age int + Nationalities []string + Interests []interest + } + + ts, err := ipld.LoadSchemaBytes([]byte(` + type User struct { + name Name + age Int + nationalities [String] + interests [Interest] + } + type Name struct { + first String + middle optional String + last String + } + type Interest struct { + name String + outdoor Bool + experience Int + } + `)) + require.NoError(t, err) + typ := ts.TypeByName("User") + + am := "Joan" + alice := user{ + Name: name{First: "Alice", Middle: &am, Last: "Wonderland"}, + Age: 24, + Nationalities: []string{"British"}, + Interests: []interest{ + {Name: "Cycling", Outdoor: true, Experience: 4}, + {Name: "Chess", Outdoor: false, Experience: 2}, + }, + } + bob := user{ + Name: name{First: "Bob", Last: "Builder"}, + Age: 35, + Nationalities: []string{"Canadian", "South African"}, + Interests: []interest{ + {Name: "Snowboarding", Outdoor: true, Experience: 8}, + {Name: "Reading", Outdoor: false, Experience: 25}, + }, + } + + anode := bindnode.Wrap(&alice, typ) + bnode := bindnode.Wrap(&bob, typ) + + t.Run("identity", func(t *testing.T) { + sel, err := Parse(".") + require.NoError(t, err) + + one, many, err := Select(sel, anode) + require.NoError(t, err) + require.NotEmpty(t, one) + require.Empty(t, many) + + fmt.Println(printer.Sprint(one)) + + age := must.Int(must.Node(one.LookupByString("age"))) + require.Equal(t, int64(alice.Age), age) + }) + + t.Run("nested property", func(t *testing.T) { + sel, err := Parse(".name.first") + require.NoError(t, err) + + one, many, err := Select(sel, anode) + require.NoError(t, err) + require.NotEmpty(t, one) + require.Empty(t, many) + + fmt.Println(printer.Sprint(one)) + + name := must.String(one) + require.Equal(t, alice.Name.First, name) + + one, many, err = Select(sel, bnode) + require.NoError(t, err) + require.NotEmpty(t, one) + require.Empty(t, many) + + fmt.Println(printer.Sprint(one)) + + name = must.String(one) + require.Equal(t, bob.Name.First, name) + }) + + t.Run("optional nested property", func(t *testing.T) { + sel, err := Parse(".name.middle?") + require.NoError(t, err) + + one, many, err := Select(sel, anode) + require.NoError(t, err) + require.NotEmpty(t, one) + require.Empty(t, many) + + fmt.Println(printer.Sprint(one)) + + name := must.String(one) + require.Equal(t, *alice.Name.Middle, name) + + one, many, err = Select(sel, bnode) + require.NoError(t, err) + require.Empty(t, one) + require.Empty(t, many) + }) + + t.Run("not exists", func(t *testing.T) { + sel, err := Parse(".name.foo") + require.NoError(t, err) + + one, many, err := Select(sel, anode) + require.Error(t, err) + require.Empty(t, one) + require.Empty(t, many) + + fmt.Println(err) + + if _, ok := err.(ResolutionError); !ok { + t.Fatalf("error was not a resolution error") + } + }) + + t.Run("optional not exists", func(t *testing.T) { + sel, err := Parse(".name.foo?") + require.NoError(t, err) + + one, many, err := Select(sel, anode) + require.NoError(t, err) + require.Empty(t, one) + require.Empty(t, many) + }) + + t.Run("iterator", func(t *testing.T) { + sel, err := Parse(".interests[]") + require.NoError(t, err) + + one, many, err := Select(sel, anode) + require.NoError(t, err) + require.Empty(t, one) + require.NotEmpty(t, many) + + for _, n := range many { + fmt.Println(printer.Sprint(n)) + } + + iname := must.String(must.Node(many[0].LookupByString("name"))) + require.Equal(t, alice.Interests[0].Name, iname) + + iname = must.String(must.Node(many[1].LookupByString("name"))) + require.Equal(t, alice.Interests[1].Name, iname) + }) + + t.Run("map iterator", func(t *testing.T) { + sel, err := Parse(".interests[0][]") + require.NoError(t, err) + + one, many, err := Select(sel, anode) + require.NoError(t, err) + require.Empty(t, one) + require.NotEmpty(t, many) + + for _, n := range many { + fmt.Println(printer.Sprint(n)) + } + + require.Equal(t, alice.Interests[0].Name, must.String(many[0])) + require.Equal(t, alice.Interests[0].Experience, int(must.Int(many[2]))) + }) +} diff --git a/core/schema/struct.go b/core/schema/struct.go index 242b891..0ad3ca6 100644 --- a/core/schema/struct.go +++ b/core/schema/struct.go @@ -20,10 +20,7 @@ func (s strukt[T]) Read(input any) result.Result[T, failure.Failure] { } if s.policy != nil { - ok, err := policy.Match(s.policy, node) - if err != nil { - return result.Error[T](NewSchemaError(err.Error())) - } + ok := policy.Match(s.policy, node) if !ok { return result.Error[T](NewSchemaError("input did not match policy")) } From 9a910f1f2780ea0495b32ca0756ab923f0596e37 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 20 Aug 2024 22:34:25 +0200 Subject: [PATCH 08/37] chore: tidy regexps --- core/policy/selector/selector.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/core/policy/selector/selector.go b/core/policy/selector/selector.go index 5f9d9a5..40a432a 100644 --- a/core/policy/selector/selector.go +++ b/core/policy/selector/selector.go @@ -40,7 +40,13 @@ type Segment interface { String() string } -var Identity = Segment(segment{".", true, false, false, nil, "", 0}) +var Identity = segment{".", true, false, false, nil, "", 0} + +var ( + indexRegex = regexp.MustCompile(`^-?\d+$`) + sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`) + fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`) +) type segment struct { str string @@ -105,7 +111,7 @@ func Parse(str string) (Selector, error) { if strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]") { lookup := seg[1 : len(seg)-1] - if regexp.MustCompile(`^-?\d+$`).MatchString(lookup) { // index + if indexRegex.MatchString(lookup) { // index idx, err := strconv.Atoi(lookup) if err != nil { return nil, NewParseError("invalid index", str, col, tok) @@ -113,7 +119,7 @@ func Parse(str string) (Selector, error) { sel = append(sel, segment{str: tok, optional: opt, index: idx}) } else if strings.HasPrefix(lookup, "\"") && strings.HasSuffix(lookup, "\"") { // explicit field sel = append(sel, segment{str: tok, optional: opt, field: lookup[1 : len(lookup)-1]}) - } else if regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`).MatchString(lookup) { // slice [3:5] or [:5] or [3:] + } else if sliceRegex.MatchString(lookup) { // slice [3:5] or [:5] or [3:] var rng []int splt := strings.Split(lookup, ":") if splt[0] == "" { @@ -136,7 +142,7 @@ func Parse(str string) (Selector, error) { } else { return nil, NewParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok) } - } else if regexp.MustCompile(`^\.[a-zA-Z_]*?$`).MatchString(seg) { + } else if fieldRegex.MatchString(seg) { sel = append(sel, segment{str: tok, optional: opt, field: seg[1:]}) } else { return nil, NewParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok) From 0183581a5b970240467604c5b140f96ad5b2647d Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 21 Aug 2024 08:13:44 +0200 Subject: [PATCH 09/37] feat: wildcard --- core/policy/match.go | 23 ++++++++++++++----- core/policy/match_test.go | 47 +++++++++++++++++++++++++++++++++++++++ core/policy/policy.go | 37 +++++++++++++++--------------- go.mod | 1 + go.sum | 2 ++ 5 files changed, 86 insertions(+), 24 deletions(-) diff --git a/core/policy/match.go b/core/policy/match.go index 950ef23..afeb7f6 100644 --- a/core/policy/match.go +++ b/core/policy/match.go @@ -63,11 +63,11 @@ func matchStatement(statement Statement, node ipld.Node) bool { } return isOrdered(s.Value(), one, lte) } - case Kind_Negation: + case Kind_Not: if s, ok := statement.(NegationStatement); ok { return !matchStatement(s.Value(), node) } - case Kind_Conjunction: + case Kind_And: if s, ok := statement.(ConjunctionStatement); ok { for _, cs := range s.Value() { r := matchStatement(cs, node) @@ -77,7 +77,7 @@ func matchStatement(statement Statement, node ipld.Node) bool { } return true } - case Kind_Disjunction: + case Kind_Or: if s, ok := statement.(DisjunctionStatement); ok { if len(s.Value()) == 0 { return true @@ -90,9 +90,20 @@ func matchStatement(statement Statement, node ipld.Node) bool { } return false } - case Kind_Wildcard: - case Kind_Universal: - case Kind_Existential: + case Kind_Like: + if s, ok := statement.(WildcardStatement); ok { + one, _, err := selector.Select(s.Selector(), node) + if err != nil || one == nil { + return false + } + v, err := one.AsString() + if err != nil { + return false + } + return s.Value().Match(v) + } + case Kind_All: + case Kind_Any: } panic(fmt.Errorf("unimplemented statement kind: %s", statement.Kind())) } diff --git a/core/policy/match_test.go b/core/policy/match_test.go index 183f1bf..59d9141 100644 --- a/core/policy/match_test.go +++ b/core/policy/match_test.go @@ -1,8 +1,10 @@ package policy import ( + "fmt" "testing" + "github.com/gobwas/glob" "github.com/ipfs/go-cid" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" @@ -284,4 +286,49 @@ func TestMatch(t *testing.T) { ok = Match(pol, nd) require.True(t, ok) }) + + t.Run("wildcard", func(t *testing.T) { + glb, err := glob.Compile(`Alice\*, Bob*, Carol.`) + require.NoError(t, err) + + for _, s := range []string{ + "Alice*, Bob, Carol.", + "Alice*, Bob, Dan, Erin, Carol.", + "Alice*, Bob , Carol.", + "Alice*, Bob*, Carol.", + } { + func(s string) { + t.Run(fmt.Sprintf("pass %s", s), func(t *testing.T) { + np := basicnode.Prototype.String + nb := np.NewBuilder() + nb.AssignString(s) + nd := nb.Build() + + pol := Policy{Like(selector.MustParse("."), glb)} + ok := Match(pol, nd) + require.True(t, ok) + }) + }(s) + } + + for _, s := range []string{ + "Alice*, Bob, Carol", + "Alice*, Bob*, Carol!", + "Alice, Bob, Carol.", + " Alice*, Bob, Carol. ", + } { + func(s string) { + t.Run(fmt.Sprintf("fail %s", s), func(t *testing.T) { + np := basicnode.Prototype.String + nb := np.NewBuilder() + nb.AssignString(s) + nd := nb.Build() + + pol := Policy{Like(selector.MustParse("."), glb)} + ok := Match(pol, nd) + require.False(t, ok) + }) + }(s) + } + }) } diff --git a/core/policy/policy.go b/core/policy/policy.go index 26201fa..b03bf5a 100644 --- a/core/policy/policy.go +++ b/core/policy/policy.go @@ -3,6 +3,7 @@ package policy // https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy import ( + "github.com/gobwas/glob" "github.com/ipld/go-ipld-prime" "github.com/storacha-network/go-ucanto/core/policy/selector" ) @@ -13,12 +14,12 @@ const ( Kind_GreaterThanOrEqual = ">=" Kind_LessThan = "<" Kind_LessThanOrEqual = "<=" - Kind_Negation = "not" - Kind_Conjunction = "and" - Kind_Disjunction = "or" - Kind_Wildcard = "like" - Kind_Universal = "all" - Kind_Existential = "any" + Kind_Not = "not" + Kind_And = "and" + Kind_Or = "or" + Kind_Like = "like" + Kind_All = "all" + Kind_Any = "any" ) type Policy = []Statement @@ -42,7 +43,7 @@ type InequalityStatement interface { type WildcardStatement interface { Statement Selector() selector.Selector - Value() string + Value() glob.Glob } type ConnectiveStatement interface { @@ -113,7 +114,7 @@ type negation struct { } func (n negation) Kind() string { - return Kind_Negation + return Kind_Not } func (n negation) Value() Statement { @@ -129,7 +130,7 @@ type conjunction struct { } func (n conjunction) Kind() string { - return Kind_Conjunction + return Kind_And } func (n conjunction) Value() []Statement { @@ -145,7 +146,7 @@ type disjunction struct { } func (n disjunction) Kind() string { - return Kind_Disjunction + return Kind_Or } func (n disjunction) Value() []Statement { @@ -158,23 +159,23 @@ func Or(stmts ...Statement) DisjunctionStatement { type wildcard struct { selector selector.Selector - pattern string + glob glob.Glob } func (n wildcard) Kind() string { - return Kind_Wildcard + return Kind_Like } func (n wildcard) Selector() selector.Selector { return n.selector } -func (n wildcard) Value() string { - return n.pattern +func (n wildcard) Value() glob.Glob { + return n.glob } -func Like(selector selector.Selector, pattern string) WildcardStatement { - return wildcard{selector, pattern} +func Like(selector selector.Selector, glob glob.Glob) WildcardStatement { + return wildcard{selector, glob} } type quantifier struct { @@ -196,9 +197,9 @@ func (n quantifier) Value() Policy { } func All(selector selector.Selector, policy Policy) QuantifierStatement { - return quantifier{Kind_Universal, selector, policy} + return quantifier{Kind_All, selector, policy} } func Any(selector selector.Selector, policy Policy) QuantifierStatement { - return quantifier{Kind_Existential, selector, policy} + return quantifier{Kind_Any, selector, policy} } diff --git a/go.mod b/go.mod index d705408..41a5c73 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect diff --git a/go.sum b/go.sum index c412866..8b85739 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= From e0c9b6a100ac54086892adcfc7f3d9ea0f5d3e68 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 21 Aug 2024 08:44:17 +0200 Subject: [PATCH 10/37] feat: quantification --- core/policy/match.go | 28 ++++++++++++++- core/policy/match_test.go | 74 +++++++++++++++++++++++++++++++++++++++ core/policy/policy.go | 4 +-- 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/core/policy/match.go b/core/policy/match.go index afeb7f6..5cf07a5 100644 --- a/core/policy/match.go +++ b/core/policy/match.go @@ -15,7 +15,7 @@ func Match(policy Policy, node ipld.Node) bool { for _, stmt := range policy { ok := matchStatement(stmt, node) if !ok { - return ok + return false } } return true @@ -103,7 +103,33 @@ func matchStatement(statement Statement, node ipld.Node) bool { return s.Value().Match(v) } case Kind_All: + if s, ok := statement.(QuantifierStatement); ok { + _, many, err := selector.Select(s.Selector(), node) + if err != nil || many == nil { + return false + } + for _, n := range many { + ok := Match(s.Value(), n) + if !ok { + return false + } + } + return true + } case Kind_Any: + if s, ok := statement.(QuantifierStatement); ok { + _, many, err := selector.Select(s.Selector(), node) + if err != nil || many == nil { + return false + } + for _, n := range many { + ok := Match(s.Value(), n) + if ok { + return true + } + } + return false + } } panic(fmt.Errorf("unimplemented statement kind: %s", statement.Kind())) } diff --git a/core/policy/match_test.go b/core/policy/match_test.go index 59d9141..10049a9 100644 --- a/core/policy/match_test.go +++ b/core/policy/match_test.go @@ -6,6 +6,7 @@ import ( "github.com/gobwas/glob" "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/storacha-network/go-ucanto/core/policy/literal" @@ -331,4 +332,77 @@ func TestMatch(t *testing.T) { }(s) } }) + + buildValueNode := func(v int64) ipld.Node { + np := basicnode.Prototype.Map + nb := np.NewBuilder() + ma, _ := nb.BeginMap(1) + ma.AssembleKey().AssignString("value") + ma.AssembleValue().AssignInt(v) + ma.Finish() + return nb.Build() + } + + t.Run("quantification all", func(t *testing.T) { + np := basicnode.Prototype.List + nb := np.NewBuilder() + la, _ := nb.BeginList(5) + la.AssembleValue().AssignNode(buildValueNode(5)) + la.AssembleValue().AssignNode(buildValueNode(10)) + la.AssembleValue().AssignNode(buildValueNode(20)) + la.AssembleValue().AssignNode(buildValueNode(50)) + la.AssembleValue().AssignNode(buildValueNode(100)) + la.Finish() + nd := nb.Build() + + pol := Policy{ + All( + selector.MustParse(".[]"), + GreaterThan(selector.MustParse(".value"), literal.Int(2)), + ), + } + ok := Match(pol, nd) + require.True(t, ok) + + pol = Policy{ + All( + selector.MustParse(".[]"), + GreaterThan(selector.MustParse(".value"), literal.Int(20)), + ), + } + ok = Match(pol, nd) + require.False(t, ok) + }) + + t.Run("quantification any", func(t *testing.T) { + np := basicnode.Prototype.List + nb := np.NewBuilder() + la, _ := nb.BeginList(5) + la.AssembleValue().AssignNode(buildValueNode(5)) + la.AssembleValue().AssignNode(buildValueNode(10)) + la.AssembleValue().AssignNode(buildValueNode(20)) + la.AssembleValue().AssignNode(buildValueNode(50)) + la.AssembleValue().AssignNode(buildValueNode(100)) + la.Finish() + nd := nb.Build() + + pol := Policy{ + Any( + selector.MustParse(".[]"), + GreaterThan(selector.MustParse(".value"), literal.Int(10)), + LessThan(selector.MustParse(".value"), literal.Int(50)), + ), + } + ok := Match(pol, nd) + require.True(t, ok) + + pol = Policy{ + Any( + selector.MustParse(".[]"), + GreaterThan(selector.MustParse(".value"), literal.Int(100)), + ), + } + ok = Match(pol, nd) + require.False(t, ok) + }) } diff --git a/core/policy/policy.go b/core/policy/policy.go index b03bf5a..49f1f48 100644 --- a/core/policy/policy.go +++ b/core/policy/policy.go @@ -196,10 +196,10 @@ func (n quantifier) Value() Policy { return n.policy } -func All(selector selector.Selector, policy Policy) QuantifierStatement { +func All(selector selector.Selector, policy ...Statement) QuantifierStatement { return quantifier{Kind_All, selector, policy} } -func Any(selector selector.Selector, policy Policy) QuantifierStatement { +func Any(selector selector.Selector, policy ...Statement) QuantifierStatement { return quantifier{Kind_Any, selector, policy} } From 76107093bf43dfb285516b6f7b13da20979a3aa0 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 21 Aug 2024 08:48:39 +0200 Subject: [PATCH 11/37] refactor: reorg tests --- core/policy/match_test.go | 536 +++++++++++++++++++------------------- 1 file changed, 271 insertions(+), 265 deletions(-) diff --git a/core/policy/match_test.go b/core/policy/match_test.go index 10049a9..e7546a5 100644 --- a/core/policy/match_test.go +++ b/core/policy/match_test.go @@ -15,204 +15,208 @@ import ( ) func TestMatch(t *testing.T) { - t.Run("equality string", func(t *testing.T) { - np := basicnode.Prototype.String - nb := np.NewBuilder() - nb.AssignString("test") - nd := nb.Build() - - pol := Policy{Equal(selector.MustParse("."), literal.String("test"))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.String("test2"))} - ok = Match(pol, nd) - require.False(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.Int(138))} - ok = Match(pol, nd) - require.False(t, ok) - }) - - t.Run("equality int", func(t *testing.T) { - np := basicnode.Prototype.Int - nb := np.NewBuilder() - nb.AssignInt(138) - nd := nb.Build() - - pol := Policy{Equal(selector.MustParse("."), literal.Int(138))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.Int(1138))} - ok = Match(pol, nd) - require.False(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.String("138"))} - ok = Match(pol, nd) - require.False(t, ok) + t.Run("equality", func(t *testing.T) { + t.Run("string", func(t *testing.T) { + np := basicnode.Prototype.String + nb := np.NewBuilder() + nb.AssignString("test") + nd := nb.Build() + + pol := Policy{Equal(selector.MustParse("."), literal.String("test"))} + ok := Match(pol, nd) + require.True(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.String("test2"))} + ok = Match(pol, nd) + require.False(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.Int(138))} + ok = Match(pol, nd) + require.False(t, ok) + }) + + t.Run("int", func(t *testing.T) { + np := basicnode.Prototype.Int + nb := np.NewBuilder() + nb.AssignInt(138) + nd := nb.Build() + + pol := Policy{Equal(selector.MustParse("."), literal.Int(138))} + ok := Match(pol, nd) + require.True(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.Int(1138))} + ok = Match(pol, nd) + require.False(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.String("138"))} + ok = Match(pol, nd) + require.False(t, ok) + }) + + t.Run("float", func(t *testing.T) { + np := basicnode.Prototype.Float + nb := np.NewBuilder() + nb.AssignFloat(1.138) + nd := nb.Build() + + pol := Policy{Equal(selector.MustParse("."), literal.Float(1.138))} + ok := Match(pol, nd) + require.True(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.Float(11.38))} + ok = Match(pol, nd) + require.False(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.String("138"))} + ok = Match(pol, nd) + require.False(t, ok) + }) + + t.Run("IPLD Link", func(t *testing.T) { + l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")} + l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")} + + np := basicnode.Prototype.Link + nb := np.NewBuilder() + nb.AssignLink(l0) + nd := nb.Build() + + pol := Policy{Equal(selector.MustParse("."), literal.Link(l0))} + ok := Match(pol, nd) + require.True(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.Link(l1))} + ok = Match(pol, nd) + require.False(t, ok) + + pol = Policy{Equal(selector.MustParse("."), literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq"))} + ok = Match(pol, nd) + require.False(t, ok) + }) + + t.Run("string in map", func(t *testing.T) { + np := basicnode.Prototype.Map + nb := np.NewBuilder() + ma, _ := nb.BeginMap(1) + ma.AssembleKey().AssignString("foo") + ma.AssembleValue().AssignString("bar") + ma.Finish() + nd := nb.Build() + + pol := Policy{Equal(selector.MustParse(".foo"), literal.String("bar"))} + ok := Match(pol, nd) + require.True(t, ok) + + pol = Policy{Equal(selector.MustParse(".[\"foo\"]"), literal.String("bar"))} + ok = Match(pol, nd) + require.True(t, ok) + + pol = Policy{Equal(selector.MustParse(".foo"), literal.String("baz"))} + ok = Match(pol, nd) + require.False(t, ok) + + pol = Policy{Equal(selector.MustParse(".foobar"), literal.String("bar"))} + ok = Match(pol, nd) + require.False(t, ok) + }) + + t.Run("string in list", func(t *testing.T) { + np := basicnode.Prototype.List + nb := np.NewBuilder() + la, _ := nb.BeginList(1) + la.AssembleValue().AssignString("foo") + la.Finish() + nd := nb.Build() + + pol := Policy{Equal(selector.MustParse(".[0]"), literal.String("foo"))} + ok := Match(pol, nd) + require.True(t, ok) + + pol = Policy{Equal(selector.MustParse(".[1]"), literal.String("foo"))} + ok = Match(pol, nd) + require.False(t, ok) + }) }) - t.Run("equality float", func(t *testing.T) { - np := basicnode.Prototype.Float - nb := np.NewBuilder() - nb.AssignFloat(1.138) - nd := nb.Build() - - pol := Policy{Equal(selector.MustParse("."), literal.Float(1.138))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.Float(11.38))} - ok = Match(pol, nd) - require.False(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.String("138"))} - ok = Match(pol, nd) - require.False(t, ok) - }) - - t.Run("equality IPLD Link", func(t *testing.T) { - l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")} - l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")} - - np := basicnode.Prototype.Link - nb := np.NewBuilder() - nb.AssignLink(l0) - nd := nb.Build() - - pol := Policy{Equal(selector.MustParse("."), literal.Link(l0))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.Link(l1))} - ok = Match(pol, nd) - require.False(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq"))} - ok = Match(pol, nd) - require.False(t, ok) - }) - - t.Run("equality string in map", func(t *testing.T) { - np := basicnode.Prototype.Map - nb := np.NewBuilder() - ma, _ := nb.BeginMap(1) - ma.AssembleKey().AssignString("foo") - ma.AssembleValue().AssignString("bar") - ma.Finish() - nd := nb.Build() - - pol := Policy{Equal(selector.MustParse(".foo"), literal.String("bar"))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{Equal(selector.MustParse(".[\"foo\"]"), literal.String("bar"))} - ok = Match(pol, nd) - require.True(t, ok) - - pol = Policy{Equal(selector.MustParse(".foo"), literal.String("baz"))} - ok = Match(pol, nd) - require.False(t, ok) - - pol = Policy{Equal(selector.MustParse(".foobar"), literal.String("bar"))} - ok = Match(pol, nd) - require.False(t, ok) - }) - - t.Run("equality string in list", func(t *testing.T) { - np := basicnode.Prototype.List - nb := np.NewBuilder() - la, _ := nb.BeginList(1) - la.AssembleValue().AssignString("foo") - la.Finish() - nd := nb.Build() - - pol := Policy{Equal(selector.MustParse(".[0]"), literal.String("foo"))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{Equal(selector.MustParse(".[1]"), literal.String("foo"))} - ok = Match(pol, nd) - require.False(t, ok) - }) - - t.Run("inequality gt int", func(t *testing.T) { - np := basicnode.Prototype.Int - nb := np.NewBuilder() - nb.AssignInt(138) - nd := nb.Build() - - pol := Policy{GreaterThan(selector.MustParse("."), literal.Int(1))} - ok := Match(pol, nd) - require.True(t, ok) - }) - - t.Run("inequality gte int", func(t *testing.T) { - np := basicnode.Prototype.Int - nb := np.NewBuilder() - nb.AssignInt(138) - nd := nb.Build() - - pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(1))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(138))} - ok = Match(pol, nd) - require.True(t, ok) - }) - - t.Run("inequality gt float", func(t *testing.T) { - np := basicnode.Prototype.Float - nb := np.NewBuilder() - nb.AssignFloat(1.38) - nd := nb.Build() - - pol := Policy{GreaterThan(selector.MustParse("."), literal.Float(1))} - ok := Match(pol, nd) - require.True(t, ok) - }) - - t.Run("inequality gte float", func(t *testing.T) { - np := basicnode.Prototype.Float - nb := np.NewBuilder() - nb.AssignFloat(1.38) - nd := nb.Build() - - pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1.38))} - ok = Match(pol, nd) - require.True(t, ok) - }) - - t.Run("inequality lt int", func(t *testing.T) { - np := basicnode.Prototype.Int - nb := np.NewBuilder() - nb.AssignInt(138) - nd := nb.Build() - - pol := Policy{LessThan(selector.MustParse("."), literal.Int(1138))} - ok := Match(pol, nd) - require.True(t, ok) - }) - - t.Run("inequality lte int", func(t *testing.T) { - np := basicnode.Prototype.Int - nb := np.NewBuilder() - nb.AssignInt(138) - nd := nb.Build() - - pol := Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(1138))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(138))} - ok = Match(pol, nd) - require.True(t, ok) + t.Run("inequality", func(t *testing.T) { + t.Run("gt int", func(t *testing.T) { + np := basicnode.Prototype.Int + nb := np.NewBuilder() + nb.AssignInt(138) + nd := nb.Build() + + pol := Policy{GreaterThan(selector.MustParse("."), literal.Int(1))} + ok := Match(pol, nd) + require.True(t, ok) + }) + + t.Run("gte int", func(t *testing.T) { + np := basicnode.Prototype.Int + nb := np.NewBuilder() + nb.AssignInt(138) + nd := nb.Build() + + pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(1))} + ok := Match(pol, nd) + require.True(t, ok) + + pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(138))} + ok = Match(pol, nd) + require.True(t, ok) + }) + + t.Run("gt float", func(t *testing.T) { + np := basicnode.Prototype.Float + nb := np.NewBuilder() + nb.AssignFloat(1.38) + nd := nb.Build() + + pol := Policy{GreaterThan(selector.MustParse("."), literal.Float(1))} + ok := Match(pol, nd) + require.True(t, ok) + }) + + t.Run("gte float", func(t *testing.T) { + np := basicnode.Prototype.Float + nb := np.NewBuilder() + nb.AssignFloat(1.38) + nd := nb.Build() + + pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1))} + ok := Match(pol, nd) + require.True(t, ok) + + pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1.38))} + ok = Match(pol, nd) + require.True(t, ok) + }) + + t.Run("lt int", func(t *testing.T) { + np := basicnode.Prototype.Int + nb := np.NewBuilder() + nb.AssignInt(138) + nd := nb.Build() + + pol := Policy{LessThan(selector.MustParse("."), literal.Int(1138))} + ok := Match(pol, nd) + require.True(t, ok) + }) + + t.Run("lte int", func(t *testing.T) { + np := basicnode.Prototype.Int + nb := np.NewBuilder() + nb.AssignInt(138) + nd := nb.Build() + + pol := Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(1138))} + ok := Match(pol, nd) + require.True(t, ok) + + pol = Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(138))} + ok = Match(pol, nd) + require.True(t, ok) + }) }) t.Run("negation", func(t *testing.T) { @@ -333,76 +337,78 @@ func TestMatch(t *testing.T) { } }) - buildValueNode := func(v int64) ipld.Node { - np := basicnode.Prototype.Map - nb := np.NewBuilder() - ma, _ := nb.BeginMap(1) - ma.AssembleKey().AssignString("value") - ma.AssembleValue().AssignInt(v) - ma.Finish() - return nb.Build() - } - - t.Run("quantification all", func(t *testing.T) { - np := basicnode.Prototype.List - nb := np.NewBuilder() - la, _ := nb.BeginList(5) - la.AssembleValue().AssignNode(buildValueNode(5)) - la.AssembleValue().AssignNode(buildValueNode(10)) - la.AssembleValue().AssignNode(buildValueNode(20)) - la.AssembleValue().AssignNode(buildValueNode(50)) - la.AssembleValue().AssignNode(buildValueNode(100)) - la.Finish() - nd := nb.Build() - - pol := Policy{ - All( - selector.MustParse(".[]"), - GreaterThan(selector.MustParse(".value"), literal.Int(2)), - ), + t.Run("quantification", func(t *testing.T) { + buildValueNode := func(v int64) ipld.Node { + np := basicnode.Prototype.Map + nb := np.NewBuilder() + ma, _ := nb.BeginMap(1) + ma.AssembleKey().AssignString("value") + ma.AssembleValue().AssignInt(v) + ma.Finish() + return nb.Build() } - ok := Match(pol, nd) - require.True(t, ok) - pol = Policy{ - All( - selector.MustParse(".[]"), - GreaterThan(selector.MustParse(".value"), literal.Int(20)), - ), - } - ok = Match(pol, nd) - require.False(t, ok) - }) - - t.Run("quantification any", func(t *testing.T) { - np := basicnode.Prototype.List - nb := np.NewBuilder() - la, _ := nb.BeginList(5) - la.AssembleValue().AssignNode(buildValueNode(5)) - la.AssembleValue().AssignNode(buildValueNode(10)) - la.AssembleValue().AssignNode(buildValueNode(20)) - la.AssembleValue().AssignNode(buildValueNode(50)) - la.AssembleValue().AssignNode(buildValueNode(100)) - la.Finish() - nd := nb.Build() - - pol := Policy{ - Any( - selector.MustParse(".[]"), - GreaterThan(selector.MustParse(".value"), literal.Int(10)), - LessThan(selector.MustParse(".value"), literal.Int(50)), - ), - } - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{ - Any( - selector.MustParse(".[]"), - GreaterThan(selector.MustParse(".value"), literal.Int(100)), - ), - } - ok = Match(pol, nd) - require.False(t, ok) + t.Run("all", func(t *testing.T) { + np := basicnode.Prototype.List + nb := np.NewBuilder() + la, _ := nb.BeginList(5) + la.AssembleValue().AssignNode(buildValueNode(5)) + la.AssembleValue().AssignNode(buildValueNode(10)) + la.AssembleValue().AssignNode(buildValueNode(20)) + la.AssembleValue().AssignNode(buildValueNode(50)) + la.AssembleValue().AssignNode(buildValueNode(100)) + la.Finish() + nd := nb.Build() + + pol := Policy{ + All( + selector.MustParse(".[]"), + GreaterThan(selector.MustParse(".value"), literal.Int(2)), + ), + } + ok := Match(pol, nd) + require.True(t, ok) + + pol = Policy{ + All( + selector.MustParse(".[]"), + GreaterThan(selector.MustParse(".value"), literal.Int(20)), + ), + } + ok = Match(pol, nd) + require.False(t, ok) + }) + + t.Run("any", func(t *testing.T) { + np := basicnode.Prototype.List + nb := np.NewBuilder() + la, _ := nb.BeginList(5) + la.AssembleValue().AssignNode(buildValueNode(5)) + la.AssembleValue().AssignNode(buildValueNode(10)) + la.AssembleValue().AssignNode(buildValueNode(20)) + la.AssembleValue().AssignNode(buildValueNode(50)) + la.AssembleValue().AssignNode(buildValueNode(100)) + la.Finish() + nd := nb.Build() + + pol := Policy{ + Any( + selector.MustParse(".[]"), + GreaterThan(selector.MustParse(".value"), literal.Int(10)), + LessThan(selector.MustParse(".value"), literal.Int(50)), + ), + } + ok := Match(pol, nd) + require.True(t, ok) + + pol = Policy{ + Any( + selector.MustParse(".[]"), + GreaterThan(selector.MustParse(".value"), literal.Int(100)), + ), + } + ok = Match(pol, nd) + require.False(t, ok) + }) }) } From 20adcd2247ad8c206acd0bf8076f8baacc107738 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 21 Aug 2024 18:57:13 +0200 Subject: [PATCH 12/37] feat: more validation --- core/result/result.go | 3 + validator/authorization.go | 30 +++++- validator/capability.go | 125 ++++++++++++++++++++--- validator/error.go | 197 ++++++++++++++++++++++++++++--------- validator/lib.go | 170 ++++++++++++++++++++++++-------- 5 files changed, 419 insertions(+), 106 deletions(-) diff --git a/core/result/result.go b/core/result/result.go index a2d816f..f858287 100644 --- a/core/result/result.go +++ b/core/result/result.go @@ -189,3 +189,6 @@ func NewFailure(err error) Result[ipld.Builder, ipld.Builder] { } return Error[ipld.Builder, ipld.Builder](&model) } + +// https://en.wikipedia.org/wiki/Unit_type +type Unit interface{} diff --git a/validator/authorization.go b/validator/authorization.go index 9f99f1e..74aff31 100644 --- a/validator/authorization.go +++ b/validator/authorization.go @@ -1,21 +1,43 @@ package validator import ( + "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/ucan" ) type Authorization[Caveats any] interface { + Audience() ucan.Principal Capability() ucan.Capability[Caveats] + Delegation() delegation.Delegation + Issuer() ucan.Principal + Proofs() []Authorization[Caveats] } type authorization[Caveats any] struct { - capability ucan.Capability[Caveats] + match Match[Caveats] + proofs []Authorization[Caveats] +} + +func (a authorization[Caveats]) Audience() ucan.Principal { + return a.Delegation().Audience() } func (a authorization[Caveats]) Capability() ucan.Capability[Caveats] { - return a.capability + return a.match.Value() +} + +func (a authorization[Caveats]) Delegation() delegation.Delegation { + return a.match.Source()[0].Delegation() +} + +func (a authorization[Caveats]) Issuer() ucan.Principal { + return a.Delegation().Issuer() +} + +func (a authorization[Caveats]) Proofs() []Authorization[Caveats] { + return a.proofs } -func NewAuthorization[Caveats any](capability ucan.Capability[Caveats]) Authorization[Caveats] { - return authorization[Caveats]{capability: capability} +func NewAuthorization[Caveats any](match Match[Caveats], proofs []Authorization[Caveats]) Authorization[Caveats] { + return authorization[Caveats]{match, proofs} } diff --git a/validator/capability.go b/validator/capability.go index 8b46650..9d02c00 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -1,6 +1,8 @@ package validator import ( + "fmt" + "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/result" "github.com/storacha-network/go-ucanto/core/result/failure" @@ -15,6 +17,7 @@ type Source interface { type source struct { capability ucan.Capability[any] + delegation delegation.Delegation } func (s source) Capability() ucan.Capability[any] { @@ -22,14 +25,59 @@ func (s source) Capability() ucan.Capability[any] { } func (s source) Delegation() delegation.Delegation { + return s.delegation +} + +type Matcher[Caveats any] interface { + Match(source Source) result.Result[Match[Caveats], InvalidCapability] +} + +type Selector[Caveats any] interface { + Select(sources []Source) ([]Match[Caveats], []DelegationError, []ucan.Capability[any], error) +} + +type Match[Caveats any] interface { + Source() []Source + Value() ucan.Capability[Caveats] + Proofs() []delegation.Delegation + Prune(context CanIssuer[Caveats]) Match[Caveats] +} + +type match[Caveats any] struct { + sources []Source + value ucan.Capability[Caveats] + descriptor Descriptor[any, Caveats] +} + +func (m match[Caveats]) Proofs() []delegation.Delegation { + return []delegation.Delegation{m.sources[0].Delegation()} +} + +func (m match[Caveats]) Prune(context CanIssuer[Caveats]) Match[Caveats] { + if context.CanIssue(m.value, m.sources[0].Delegation().Issuer().DID()) { + return m + } return nil } +func (m match[Caveats]) Source() []Source { + return m.sources +} + +func (m match[Caveats]) Value() ucan.Capability[Caveats] { + return m.value +} + +func NewMatch[Caveats any](source Source, capability ucan.Capability[Caveats], descriptor Descriptor[any, Caveats]) Match[Caveats] { + return match[Caveats]{[]Source{source}, capability, descriptor} +} + type CapabilityParser[Caveats any] interface { + Matcher[Caveats] + Selector[Caveats] Can() ucan.Ability // New creates a new capability from the passed options. New(with ucan.Resource, nb Caveats) ucan.Capability[Caveats] - Match(source Source) result.Result[ucan.Capability[Caveats], failure.Failure] } type Descriptor[I, O any] interface { @@ -64,8 +112,21 @@ func (c *capability[C]) Can() ucan.Ability { return c.descriptor.Can() } -func (c *capability[Caveats]) Match(source Source) result.Result[ucan.Capability[Caveats], failure.Failure] { - return parseCapability(c.descriptor, source) +func (c *capability[Caveats]) Select(capabilities []Source) ([]Match[Caveats], []DelegationError, []ucan.Capability[any], error) { + return Select(c, capabilities) +} + +func (c *capability[Caveats]) Match(source Source) result.Result[Match[Caveats], InvalidCapability] { + return result.MapOk( + parseCapability(c.descriptor, source), + func(cap ucan.Capability[Caveats]) Match[Caveats] { + return NewMatch(source, cap, c.descriptor) + }, + ) +} + +func (c *capability[C]) String() string { + return fmt.Sprintf(`{can:"%s"}`, c.Can()) } func (c *capability[Caveats]) New(with ucan.Resource, nb Caveats) ucan.Capability[Caveats] { @@ -77,14 +138,54 @@ func NewCapability[Caveats any](can ucan.Ability, with schema.Reader[string, uca return &capability[Caveats]{descriptor: &d} } -func parseCapability[O any](descriptor Descriptor[any, O], source Source) result.Result[ucan.Capability[O], failure.Failure] { +func parseCapability[O any](descriptor Descriptor[any, O], source Source) result.Result[ucan.Capability[O], InvalidCapability] { cap := source.Capability() - return result.MatchResultR1(descriptor.With().Read(cap.With()), func(with ucan.Resource) result.Result[ucan.Capability[O], failure.Failure] { - return result.MapOk(descriptor.Nb().Read(cap.Nb()), func(nb O) ucan.Capability[O] { - pcap := ucan.NewCapability(cap.Can(), with, nb) - return pcap - }) - }, func(x failure.Failure) result.Result[ucan.Capability[O], failure.Failure] { - return result.Error[ucan.Capability[O]](x) - }) + + if descriptor.Can() != cap.Can() { + return result.Error[ucan.Capability[O], InvalidCapability](NewUnknownCapabilityError(cap)) + } + + return result.MatchResultR1( + descriptor.With().Read(cap.With()), + func(with ucan.Resource) result.Result[ucan.Capability[O], InvalidCapability] { + return result.MapResultR0( + descriptor.Nb().Read(cap.Nb()), + func(nb O) ucan.Capability[O] { + pcap := ucan.NewCapability(cap.Can(), with, nb) + return pcap + }, + func(x failure.Failure) InvalidCapability { + return NewMalformedCapabilityError(cap, x) + }, + ) + }, + func(x failure.Failure) result.Result[ucan.Capability[O], InvalidCapability] { + return result.Error[ucan.Capability[O], InvalidCapability](NewMalformedCapabilityError(cap, x)) + }, + ) +} + +func Select[Caveats any](matcher Matcher[Caveats], capabilities []Source) (matches []Match[Caveats], errors []DelegationError, unknowns []ucan.Capability[any], err error) { + for _, capability := range capabilities { + err = result.MatchResultR1( + matcher.Match(capability), + func(match Match[Caveats]) error { + matches = append(matches, match) + return nil + }, + func(err InvalidCapability) error { + if uerr, ok := err.(UnknownCapability); ok { + unknowns = append(unknowns, uerr.Capability()) + } + if serr, ok := err.(DelegationSubError); ok { + errors = append(errors, NewDelegationError([]DelegationSubError{serr}, capability.Capability())) + } + return fmt.Errorf("unexpected error type in match result") + }, + ) + if err != nil { + return + } + } + return } diff --git a/validator/error.go b/validator/error.go index 844a4cc..961efce 100644 --- a/validator/error.go +++ b/validator/error.go @@ -23,9 +23,9 @@ type DelegationSubError interface { isDelegationSubError() } -type InvalidProofError interface { - error - isInvalidProofError() +type InvalidProof interface { + failure.Failure + isInvalidProof() } type EscalatedCapabilityError[Caveats any] struct { @@ -35,7 +35,7 @@ type EscalatedCapabilityError[Caveats any] struct { cause error } -func NewEscalatedCapabilityError[Caveats any](claimed ucan.Capability[Caveats], delegated interface{}, cause error) error { +func NewEscalatedCapabilityError[Caveats any](claimed ucan.Capability[Caveats], delegated interface{}, cause error) failure.Failure { return EscalatedCapabilityError[Caveats]{failure.NamedWithCurrentStackTrace("EscalatedCapability"), claimed, delegated, cause} } @@ -50,21 +50,36 @@ func (ece EscalatedCapabilityError[Caveats]) Error() string { func (ece EscalatedCapabilityError[Caveats]) isDelegationSubError() { } -type DelegationError struct { +type DelegationError interface { + failure.Failure + Causes() []DelegationSubError + Context() any + isDelegationError() +} + +type delegationError struct { failure.NamedWithStackTrace causes []DelegationSubError context interface{} } -func NewDelegationError(causes []DelegationSubError, context interface{}) error { - return DelegationError{failure.NamedWithCurrentStackTrace("InvalidClaim"), causes, context} +func NewDelegationError(causes []DelegationSubError, context interface{}) DelegationError { + return delegationError{failure.NamedWithCurrentStackTrace("InvalidClaim"), causes, context} } -func (de DelegationError) Error() string { +func (de delegationError) Error() string { return fmt.Sprintf("Cannot derive %s from delegated capabilities: %s", de.context, errors.Join(de.Unwrap()...).Error()) } -func (de DelegationError) Unwrap() []error { +func (de delegationError) Causes() []DelegationSubError { + return de.causes +} + +func (de delegationError) Context() any { + return de.context +} + +func (de delegationError) Unwrap() []error { errs := make([]error, 0, len(de.causes)) for _, cause := range de.causes { errs = append(errs, cause) @@ -72,7 +87,8 @@ func (de DelegationError) Unwrap() []error { return errs } -func (de DelegationError) isDelegationSubError() {} +func (de delegationError) isDelegationError() {} +func (de delegationError) isDelegationSubError() {} type SessionEscalationError struct { failure.NamedWithStackTrace @@ -80,7 +96,7 @@ type SessionEscalationError struct { cause error } -func NewSessionEscalationError(delegation delegation.Delegation, cause error) error { +func NewSessionEscalationError(delegation delegation.Delegation, cause error) InvalidProof { return SessionEscalationError{failure.NamedWithCurrentStackTrace("SessionEscalation"), delegation, cause} } @@ -92,7 +108,15 @@ func (see SessionEscalationError) Error() string { }, "\n") } -func (see SessionEscalationError) isInvalidProofError() {} +func (see SessionEscalationError) isInvalidProof() {} + +type InvalidSignature interface { + InvalidProof + Issuer() ucan.Principal + Audience() ucan.Principal + Delegation() delegation.Delegation + isInvalidSignature() +} type InvalidSignatureError struct { failure.NamedWithStackTrace @@ -100,17 +124,22 @@ type InvalidSignatureError struct { verifier ucan.Verifier } -func NewInvalidSignatureError(delegation delegation.Delegation, verifier ucan.Verifier) error { +func NewInvalidSignatureError(delegation delegation.Delegation, verifier ucan.Verifier) InvalidSignature { return InvalidSignatureError{failure.NamedWithCurrentStackTrace("InvalidSignature"), delegation, verifier} } func (ise InvalidSignatureError) Issuer() ucan.Principal { return ise.delegation.Issuer() } + func (ise InvalidSignatureError) Audience() ucan.Principal { return ise.delegation.Audience() } +func (ise InvalidSignatureError) Delegation() delegation.Delegation { + return ise.delegation +} + func (ise InvalidSignatureError) Error() string { issuer := ise.Issuer().DID() key := ise.verifier.DID() @@ -123,11 +152,13 @@ func (ise InvalidSignatureError) Error() string { }, "\n") } -func (ise InvalidSignatureError) isInvalidProofError() {} +func (ise InvalidSignatureError) isInvalidSignature() {} +func (ise InvalidSignatureError) isInvalidProof() {} type UnavailableProof interface { - failure.Failure + InvalidProof Link() ucan.Link + isUnavailableProof() } type UnavailableProofError struct { @@ -158,11 +189,13 @@ func (upe UnavailableProofError) Error() string { return strings.Join(messages, "\n") } -func (upe UnavailableProofError) isInvalidProofError() {} +func (upe UnavailableProofError) isUnavailableProof() {} +func (upe UnavailableProofError) isInvalidProof() {} type UnresolvedDID interface { - failure.Failure + InvalidProof DID() did.DID + isUnresolvedDID() } type DIDKeyResolutionError struct { @@ -187,7 +220,8 @@ func (dkre DIDKeyResolutionError) Error() string { return fmt.Sprintf("Unable to resolve '%s' key", dkre.did) } -func (dkre DIDKeyResolutionError) isInvalidProofError() {} +func (dkre DIDKeyResolutionError) isUnresolvedDID() {} +func (dkre DIDKeyResolutionError) isInvalidProof() {} type PrincipalAlignmentError struct { failure.NamedWithStackTrace @@ -195,7 +229,7 @@ type PrincipalAlignmentError struct { delegation delegation.Delegation } -func NewPrincipalAlignmentError(audience ucan.Principal, delegation delegation.Delegation) error { +func NewPrincipalAlignmentError(audience ucan.Principal, delegation delegation.Delegation) failure.Failure { return PrincipalAlignmentError{failure.NamedWithCurrentStackTrace("InvalidAudience"), audience, delegation} } @@ -216,19 +250,31 @@ func (pae PrincipalAlignmentError) Build() (datamodel.Node, error) { return ipld.WrapWithRecovery(&invalidAudienceModel, vdm.InvalidAudienceType()) } -func (pae PrincipalAlignmentError) isInvalidProofError() {} +func (pae PrincipalAlignmentError) isInvalidProof() {} + +// InvalidCapability is an error produced when parsing capabilities. +type InvalidCapability interface { + failure.Failure + isInvalidCapability() +} + +type MalformedCapability interface { + InvalidCapability + Capability() ucan.Capability[any] + isMalformedCapability() +} -type MalformedCapabilityError[Caveats any] struct { +type MalformedCapabilityError struct { failure.NamedWithStackTrace - capability ucan.Capability[Caveats] + capability ucan.Capability[any] cause error } -func NewMalformedCapabilityError[Caveats any](capability ucan.Capability[Caveats], cause error) error { - return MalformedCapabilityError[Caveats]{failure.NamedWithCurrentStackTrace("MalformedCapability"), capability, cause} +func NewMalformedCapabilityError(capability ucan.Capability[any], cause error) MalformedCapability { + return MalformedCapabilityError{failure.NamedWithCurrentStackTrace("MalformedCapability"), capability, cause} } -func (mce MalformedCapabilityError[Caveats]) Error() string { +func (mce MalformedCapabilityError) Error() string { capabilityJSON, _ := json.Marshal(mce.capability) return strings.Join([]string{ fmt.Sprintf("Encountered malformed '%s' capability: %s", mce.capability.Can(), string(capabilityJSON)), @@ -236,30 +282,48 @@ func (mce MalformedCapabilityError[Caveats]) Error() string { }, "\n") } -func (mce MalformedCapabilityError[Caveats]) isDelegationSubError() {} +func (mce MalformedCapabilityError) Capability() ucan.Capability[any] { + return mce.capability +} + +func (mce MalformedCapabilityError) isMalformedCapability() {} +func (mce MalformedCapabilityError) isInvalidCapability() {} +func (mce MalformedCapabilityError) isDelegationSubError() {} -type UnknownCapabilityError[Caveats any] struct { +type UnknownCapability interface { + InvalidCapability + Capability() ucan.Capability[any] + isUnknownCapability() +} + +type UnknownCapabilityError struct { failure.NamedWithStackTrace - capability ucan.Capability[Caveats] + capability ucan.Capability[any] } -func NewUnknownCapabilityError[Caveats any](capability ucan.Capability[Caveats]) error { - return UnknownCapabilityError[Caveats]{failure.NamedWithCurrentStackTrace("UnknownCapability"), capability} +func NewUnknownCapabilityError(capability ucan.Capability[any]) UnknownCapability { + return UnknownCapabilityError{failure.NamedWithCurrentStackTrace("UnknownCapability"), capability} } -func (uce UnknownCapabilityError[Caveats]) Error() string { +func (uce UnknownCapabilityError) Error() string { capabilityJSON, _ := json.Marshal(uce.capability) return fmt.Sprintf("Encountered unknown capability: %s", string(capabilityJSON)) } -func (uce UnknownCapabilityError[Caveats]) isDelegationSubError() {} +func (uce UnknownCapabilityError) Capability() ucan.Capability[any] { + return uce.capability +} + +func (uce UnknownCapabilityError) isUnknownCapability() {} +func (uce UnknownCapabilityError) isInvalidCapability() {} +func (uce UnknownCapabilityError) isDelegationSubError() {} type ExpiredError struct { failure.NamedWithStackTrace delegation delegation.Delegation } -func NewExpiredError(delegation delegation.Delegation) error { +func NewExpiredError(delegation delegation.Delegation) InvalidProof { return ExpiredError{failure.NamedWithCurrentStackTrace("Expired"), delegation} } @@ -280,29 +344,40 @@ func (ee ExpiredError) Build() (datamodel.Node, error) { return ipld.WrapWithRecovery(expiredModel, vdm.ExpiredType()) } -func (ee ExpiredError) isInvalidProofError() {} +func (ee ExpiredError) isInvalidProof() {} + +type Revoked interface { + InvalidProof + Delegation() delegation.Delegation + isRevoked() +} type RevokedError struct { failure.NamedWithStackTrace delegation delegation.Delegation } -func NewRevokedError(delegation delegation.Delegation) error { +func NewRevokedError(delegation delegation.Delegation) Revoked { return RevokedError{failure.NamedWithCurrentStackTrace("Revoked"), delegation} } +func (re RevokedError) Delegation() delegation.Delegation { + return re.delegation +} + func (re RevokedError) Error() string { return fmt.Sprintf("Proof %s has been revoked", re.delegation.Link()) } -func (re RevokedError) isInvalidProofError() {} +func (re RevokedError) isInvalidProof() {} +func (re RevokedError) isRevoked() {} type NotValidBeforeError struct { failure.NamedWithStackTrace delegation delegation.Delegation } -func NewNotValidBeforeError(delegation delegation.Delegation) error { +func NewNotValidBeforeError(delegation delegation.Delegation) InvalidProof { return NotValidBeforeError{failure.NamedWithCurrentStackTrace("NotValidBefore"), delegation} } @@ -323,34 +398,42 @@ func (nvbe NotValidBeforeError) Build() (datamodel.Node, error) { return ipld.WrapWithRecovery(notValidBeforeModel, vdm.NotValidBeforeType()) } -func (nvbe NotValidBeforeError) isInvalidProofError() {} +func (nvbe NotValidBeforeError) isInvalidProof() {} // TODO: this may just be the concrete type from the implementation once // the rest of the validator is done type InvalidClaim interface { - failure.NamedWithStackTrace - error + failure.Failure Issuer() ucan.Principal Delegation() delegation.Delegation } +type Unauthorized interface { + failure.Failure + DelegationErrors() []DelegationError + UnknownCapabilities() []ucan.Capability[any] + InvalidProofs() []InvalidProof + FailedProofs() []InvalidClaim + isUnauthorized() +} + type UnauthorizedError[Caveats any] struct { failure.NamedWithStackTrace - capability ucan.Capability[Caveats] + capability CapabilityParser[Caveats] delegationErrors []DelegationError // this is a hack... it will allow you to make an array of capabilities of different types - unknownCapabilities []ucan.UnknownCapability - invalidProofs []InvalidProofError + unknownCapabilities []ucan.Capability[any] + invalidProofs []InvalidProof failedProofs []InvalidClaim } func NewUnauthorizedError[Caveats any]( - capability ucan.Capability[Caveats], + capability CapabilityParser[Caveats], delegationErrors []DelegationError, - unknownCapabilities []ucan.UnknownCapability, - invalidProofs []InvalidProofError, + unknownCapabilities []ucan.Capability[any], + invalidProofs []InvalidProof, failedProofs []InvalidClaim, -) error { +) Unauthorized { return UnauthorizedError[Caveats]{ failure.NamedWithCurrentStackTrace("Unauthorized"), capability, @@ -383,7 +466,7 @@ func (ue UnauthorizedError[Caveats]) Error() string { } finalList := make([]string, 0, 2+int(math.Min(1, float64(len(errorStrings))))) - finalList = append(finalList, fmt.Sprintf("Claim %+v is not authorized", ue.capability)) + finalList = append(finalList, fmt.Sprintf("Claim %s is not authorized", ue.capability)) if len(errorStrings) > 0 { finalList = append(finalList, errorStrings...) } else { @@ -396,6 +479,24 @@ func (ue UnauthorizedError[Caveats]) Error() string { return strings.Join(finalList, "\n") } +func (ue UnauthorizedError[Caveats]) DelegationErrors() []DelegationError { + return ue.delegationErrors +} + +func (ue UnauthorizedError[Caveats]) UnknownCapabilities() []ucan.Capability[any] { + return ue.unknownCapabilities +} + +func (ue UnauthorizedError[Caveats]) InvalidProofs() []InvalidProof { + return ue.invalidProofs +} + +func (ue UnauthorizedError[Caveats]) FailedProofs() []InvalidClaim { + return ue.failedProofs +} + +func (ue UnauthorizedError[Caveats]) isUnauthorized() {} + func indent(message string) string { indent := " " return indent + strings.Join(strings.Split(message, "\n"), "\n$"+indent) diff --git a/validator/lib.go b/validator/lib.go index 3bf493b..117a209 100644 --- a/validator/lib.go +++ b/validator/lib.go @@ -10,7 +10,6 @@ import ( "github.com/storacha-network/go-ucanto/core/policy/literal" "github.com/storacha-network/go-ucanto/core/policy/selector" "github.com/storacha-network/go-ucanto/core/result" - "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/did" "github.com/storacha-network/go-ucanto/principal" @@ -70,15 +69,24 @@ type CanIssuer[Caveats any] interface { // given DID or whether it needs to be delegated to the issuer. type CanIssueFunc[Caveats any] func(capability ucan.Capability[Caveats], issuer did.DID) bool +// canissuer converts an CanIssuer[any] to CanIssuer[Caveats] +type canissuer[Caveats any] struct { + canIssue CanIssueFunc[any] +} + +func (ci canissuer[Caveats]) CanIssue(c ucan.Capability[Caveats], d did.DID) bool { + return ci.canIssue(ucan.NewCapability[any](c.Can(), c.With(), c.Nb()), d) +} + type RevocationChecker[Caveats any] interface { // ValidateAuthorization validates that the passed authorization has not been // revoked. - ValidateAuthorization(auth Authorization[Caveats]) failure.Failure + ValidateAuthorization(auth Authorization[Caveats]) result.Result[result.Unit, Revoked] } // RevocationCheckerFunc validates the passed authorization and returns // a result indicating validity. -type RevocationCheckerFunc[Caveats any] func(auth Authorization[Caveats]) failure.Failure +type RevocationCheckerFunc[Caveats any] func(auth Authorization[Caveats]) result.Result[result.Unit, Revoked] // Validator must provide a `Verifier` corresponding to local authority. // @@ -127,7 +135,7 @@ func (vc validationContext[Caveats]) CanIssue(capability ucan.Capability[any], i return vc.canIssue(capability, issuer) } -func (vc validationContext[Caveats]) ValidateAuthorization(auth Authorization[any]) failure.Failure { +func (vc validationContext[Caveats]) ValidateAuthorization(auth Authorization[any]) result.Result[result.Unit, Revoked] { return vc.validateAuthorization(auth) } @@ -168,7 +176,7 @@ func NewValidationContext[Caveats any]( // returned that illustrates the valid path. If no valid path is found // `Unauthorized` error is returned detailing all explored paths and where they // proved to fail. -func Access[Caveats any](invocation invocation.Invocation, context ValidationContext[Caveats]) result.Result[Authorization[Caveats], UnauthorizedError[Caveats]] { +func Access[Caveats any](invocation invocation.Invocation, context ValidationContext[Caveats]) (result.Result[Authorization[Caveats], Unauthorized], error) { prf := []delegation.Proof{delegation.FromDelegation(invocation)} return Claim(context.Capability(), prf, context) } @@ -177,29 +185,64 @@ func Access[Caveats any](invocation invocation.Invocation, context ValidationCon // set of `proofs`. On success an `Authorization` object with detailed proof // chain is returned and on failure `Unauthorized` error is returned with // details on paths explored and why they have failed. -func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegation.Proof, context ClaimContext) result.Result[Authorization[Caveats], UnauthorizedError[Caveats]] { - delegations, errors := resolveProofs(proofs, context) +func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegation.Proof, context ClaimContext) (result.Result[Authorization[Caveats], Unauthorized], error) { + var sources []Source + var invalidprf []InvalidProof + + delegations, rerrs := resolveProofs(proofs, context) + for _, err := range rerrs { + invalidprf = append(invalidprf, err) + } for _, d := range delegations { + validation, err := validate(d, delegations, context) + if err != nil { + return nil, err + } + // Validate each proof if valid add each capability to the list of sources. // otherwise collect the error. result.MatchResultR0( - validate(d, delegations, context), + validation, + func(d delegation.Delegation) { + for _, c := range d.Capabilities() { + sources = append(sources, source{c, d}) + } + }, + func(x InvalidProof) { + invalidprf = append(invalidprf, x) + }, ) } - // cap := invocation.Capabilities()[0] - - // var sources []Source - - // src := source{capability: cap} + // look for the matching capability + matches, dlgerrs, unknowns, err := capability.Select(sources) + if err != nil { + return nil, err + } - // // TODO: parser.Select() - // match := context.Capability().Match(src) + var failedprf []InvalidClaim + for _, matched := range matches { + selector := matched.Prune(canissuer[Caveats]{canIssue: context.CanIssue}) + if selector == nil { + authorization := NewAuthorization(matched, nil) + revoked := result.MatchResultR1( + context.ValidateAuthorization(authorization), + func(o result.Unit) Revoked { return nil }, + func(x Revoked) Revoked { return x }, + ) + if revoked == nil { + return result.Ok[Authorization[Caveats], Unauthorized](authorization), nil + } + invalidprf = append(invalidprf, revoked) + } else { + authorize(matched, context) + } + } - // return result.MapOk(match, func(o ucan.Capability[Caveats]) Authorization[Caveats] { - // return authorization[Caveats]{capability: o} - // }), nil + return result.Error[Authorization[Caveats]]( + NewUnauthorizedError(capability, dlgerrs, unknowns, invalidprf, failedprf), + ), nil } // resolveProofs takes `proofs` from the delegation which may contain @@ -215,7 +258,7 @@ func resolveProofs(proofs []delegation.Proof, resolver ProofResolver) (dels []de result.MatchResultR0( resolver.ResolveProof(p.Link()), func(d delegation.Delegation) { dels = append(dels, d) }, - func(err UnavailableProof) { errs = append(errs, err) }, + func(x UnavailableProof) { errs = append(errs, x) }, ) } } @@ -224,12 +267,12 @@ func resolveProofs(proofs []delegation.Proof, resolver ProofResolver) (dels []de // Validate a delegation to check it is within the time bound and that it is // authorized by the issuer. -func validate(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) result.Result[delegation.Delegation, failure.Failure] { +func validate(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (result.Result[delegation.Delegation, InvalidProof], error) { if ucan.IsExpired(dlg) { - return result.Error[delegation.Delegation](failure.FromError(NewExpiredError(dlg))) + return result.Error[delegation.Delegation, InvalidProof](NewExpiredError(dlg)), nil } if ucan.IsTooEarly(dlg) { - return result.Error[delegation.Delegation](failure.FromError(NewNotValidBeforeError(dlg))) + return result.Error[delegation.Delegation, InvalidProof](NewNotValidBeforeError(dlg)), nil } return verifyAuthorization(dlg, prfs, ctx) } @@ -241,45 +284,83 @@ func validate(dlg delegation.Delegation, prfs []delegation.Delegation, ctx Claim // valid `ucan/attest` attestation from the authority, if attestation is not // found falls back to resolving did:key for the issuer and verifying its // signature. -func verifyAuthorization(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) result.Result[delegation.Delegation, failure.Failure] { +func verifyAuthorization(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (result.Result[delegation.Delegation, InvalidProof], error) { issuer := dlg.Issuer().DID() // If the issuer is a did:key we just verify a signature if strings.HasPrefix(issuer.String(), "did:key:") { vfr, err := ctx.ParsePrincipal(issuer.String()) if err != nil { - return result.Error[delegation.Delegation](failure.FromError(err)) + return nil, err + } + sig, err := verifySignature(dlg, vfr) + if err != nil { + return nil, err } - return verifySignature(dlg, vfr) + return result.MapError(sig, func(err InvalidSignature) InvalidProof { + return InvalidProof(err) + }), nil + } + + // Attempt to resolve embedded authorization session from the authority + sess, err := verifySession(dlg, prfs, ctx) + if err != nil { + return nil, err } - - return result.MapResultR0( - // Attempt to resolve embedded authorization session from the authority - verifySession(dlg, prfs, ctx), - func(_ Authorization[vdm.AttestationModel]) delegation.Delegation { - return dlg + + return result.MatchResultR2( + sess, + // If we have valid session we consider authorization valid + func(a Authorization[vdm.AttestationModel]) (result.Result[delegation.Delegation, InvalidProof], error) { + return result.Ok[delegation.Delegation, InvalidProof](dlg), nil }, - func(err UnauthorizedError[vdm.AttestationModel]) failure.Failure { - if len(err.failedProofs) > 0 { - return NewSessionEscalationError(dlg, err) + func(x Unauthorized) (result.Result[delegation.Delegation, InvalidProof], error) { + if len(x.FailedProofs()) > 0 { + return result.Error[delegation.Delegation, InvalidProof](NewSessionEscalationError(dlg, x)), nil } + // Otherwise we try to resolve did:key from the DID instead - // and use that to verify the signature - return result.MapResultR0( - ctx.ResolveDIDKey(issuer) + // and use that to verify the signature + vfr, err := result.MapResultR1( + ctx.ResolveDIDKey(issuer), + func(did did.DID) (principal.Verifier, error) { + return ctx.ParsePrincipal(did.String()) + }, + func(err UnresolvedDID) (UnresolvedDID, error) { + return err, nil + }, + ) + if err != nil { + return nil, err + } + + return result.MatchResultR2( + vfr, + func(v principal.Verifier) (result.Result[delegation.Delegation, InvalidProof], error) { + sig, err := verifySignature(dlg, v) + if err != nil { + return nil, err + } + return result.MapError(sig, func(err InvalidSignature) InvalidProof { + return InvalidProof(err) + }), nil + }, + func(x UnresolvedDID) (result.Result[delegation.Delegation, InvalidProof], error) { + return result.Error[delegation.Delegation, InvalidProof](x), nil + }, ) }, ) } -func verifySignature(dlg delegation.Delegation, vfr principal.Verifier) result.Result[delegation.Delegation, failure.Failure] { +func verifySignature(dlg delegation.Delegation, vfr principal.Verifier) (result.Result[delegation.Delegation, InvalidSignature], error) { ok, err := ucan.VerifySignature(dlg.Data(), vfr) if err != nil { - return result.Error[delegation.Delegation](failure.FromError(err)) + return nil, err } if !ok { - return result.Error[delegation.Delegation](failure.FromError(NewInvalidSignatureError(dlg, vfr))) + return result.Error[delegation.Delegation](NewInvalidSignatureError(dlg, vfr)), nil } - return result.Ok[delegation.Delegation, failure.Failure](dlg) + return result.Ok[delegation.Delegation, InvalidSignature](dlg), nil } // verifySession attempts to find an authorization session - an `ucan/attest` @@ -287,7 +368,7 @@ func verifySignature(dlg delegation.Delegation, vfr principal.Verifier) result.R // matches given delegation. // // https://github.com/storacha-network/specs/blob/main/w3-session.md#authorization-session -func verifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) result.Result[Authorization[vdm.AttestationModel], UnauthorizedError[vdm.AttestationModel]] { +func verifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (result.Result[Authorization[vdm.AttestationModel], Unauthorized], error) { // Create a schema that will match an authorization for this exact delegation attestation := NewCapability( "ucan/attest", @@ -311,3 +392,8 @@ func verifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx return Claim(attestation, aprfs, ctx) } + +// authorize verifies whether any of the delegated proofs grant give capability. +func authorize[Caveats any](match Match[Caveats], context ClaimContext) (result.Result[Authorization[Caveats], InvalidClaim], error) { + +} From 9baee6ca06e92e7628819358938dee9fa80e2ac6 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 21 Aug 2024 22:48:20 +0200 Subject: [PATCH 13/37] feat: authorization implementation --- server/handler.go | 2 +- server/server.go | 7 +++---- server/server_test.go | 4 ++-- validator/authorization.go | 34 ++++++++++++++++++++++++++++++++++ validator/capability.go | 4 ++-- validator/lib.go | 7 ++++--- 6 files changed, 46 insertions(+), 12 deletions(-) diff --git a/server/handler.go b/server/handler.go index 36948db..3d27667 100644 --- a/server/handler.go +++ b/server/handler.go @@ -35,7 +35,7 @@ func Provide[C any, O, X ipld.Builder](capability validator.CapabilityParser[C], return result.MatchResultR2(authorization, func(ok validator.Authorization[C]) (transaction.Transaction[O, X], error) { return handler(ok.Capability(), invocation, context) - }, func(err validator.UnauthorizedError[C]) (transaction.Transaction[O, X], error) { + }, func(err validator.Unauthorized) (transaction.Transaction[O, X], error) { if failure, ok := any(err).(X); ok { return transaction.NewTransaction(result.Error[O](failure)), nil } diff --git a/server/server.go b/server/server.go index 8965e7a..179a5f7 100644 --- a/server/server.go +++ b/server/server.go @@ -15,7 +15,6 @@ import ( "github.com/storacha-network/go-ucanto/core/message" "github.com/storacha-network/go-ucanto/core/receipt" "github.com/storacha-network/go-ucanto/core/result" - "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/did" "github.com/storacha-network/go-ucanto/principal" "github.com/storacha-network/go-ucanto/principal/ed25519/verifier" @@ -99,8 +98,8 @@ func NewServer(id principal.Signer, options ...Option) (ServerView, error) { validateAuthorization := cfg.validateAuthorization if validateAuthorization == nil { - validateAuthorization = func(auth validator.Authorization[any]) failure.Failure { - return nil + validateAuthorization = func(auth validator.Authorization[any]) result.Result[result.Unit, validator.Revoked] { + return result.Ok[result.Unit, validator.Revoked](nil) } } @@ -146,7 +145,7 @@ func (ctx context) CanIssue(capability ucan.Capability[any], issuer did.DID) boo return ctx.canIssue(capability, issuer) } -func (ctx context) ValidateAuthorization(auth validator.Authorization[any]) failure.Failure { +func (ctx context) ValidateAuthorization(auth validator.Authorization[any]) result.Result[result.Unit, validator.Revoked] { return ctx.validateAuthorization(auth) } diff --git a/server/server_test.go b/server/server_test.go index 1c74285..23fb56e 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -115,7 +115,7 @@ func TestSimpleHandler(t *testing.T) { uploadadd := validator.NewCapability( "upload/add", schema.DIDString(), - schema.Struct[uploadAddCaveats](uploadAddCaveatsType()), + schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), ) server := helpers.Must(NewServer( @@ -168,7 +168,7 @@ func TestHandlerExecutionError(t *testing.T) { uploadadd := validator.NewCapability( "upload/add", schema.DIDString(), - schema.Struct[uploadAddCaveats](uploadAddCaveatsType()), + schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), ) server := helpers.Must(NewServer( diff --git a/validator/authorization.go b/validator/authorization.go index 74aff31..e97b09a 100644 --- a/validator/authorization.go +++ b/validator/authorization.go @@ -41,3 +41,37 @@ func (a authorization[Caveats]) Proofs() []Authorization[Caveats] { func NewAuthorization[Caveats any](match Match[Caveats], proofs []Authorization[Caveats]) Authorization[Caveats] { return authorization[Caveats]{match, proofs} } + +type unknownauth[C any] struct { + auth Authorization[C] +} + +func (a unknownauth[C]) Audience() ucan.Principal { + return a.auth.Audience() +} + +func (a unknownauth[C]) Capability() ucan.Capability[any] { + cap := a.auth.Capability() + return ucan.NewCapability[any](cap.Can(), cap.With(), cap.Nb()) +} + +func (a unknownauth[C]) Delegation() delegation.Delegation { + return a.auth.Delegation() +} + +func (a unknownauth[C]) Issuer() ucan.Principal { + return a.Delegation().Issuer() +} + +func (a unknownauth[C]) Proofs() []Authorization[any] { + var prf []Authorization[any] + for _, p := range a.Proofs() { + prf = append(prf, ConvertUnknownAuthorization(p)) + } + return prf +} + +// ConvertUnknownAuthorization converts an Authorization[Caveats] to Authorization[any] +func ConvertUnknownAuthorization[Caveats any](auth Authorization[Caveats]) Authorization[any] { + return unknownauth[Caveats]{auth} +} diff --git a/validator/capability.go b/validator/capability.go index 9d02c00..7b71210 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -108,7 +108,7 @@ type capability[Caveats any] struct { descriptor Descriptor[any, Caveats] } -func (c *capability[C]) Can() ucan.Ability { +func (c *capability[Caveats]) Can() ucan.Ability { return c.descriptor.Can() } @@ -125,7 +125,7 @@ func (c *capability[Caveats]) Match(source Source) result.Result[Match[Caveats], ) } -func (c *capability[C]) String() string { +func (c *capability[Caveats]) String() string { return fmt.Sprintf(`{can:"%s"}`, c.Can()) } diff --git a/validator/lib.go b/validator/lib.go index 117a209..dac55e2 100644 --- a/validator/lib.go +++ b/validator/lib.go @@ -227,7 +227,7 @@ func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegatio if selector == nil { authorization := NewAuthorization(matched, nil) revoked := result.MatchResultR1( - context.ValidateAuthorization(authorization), + context.ValidateAuthorization(ConvertUnknownAuthorization(authorization)), func(o result.Unit) Revoked { return nil }, func(x Revoked) Revoked { return x }, ) @@ -393,7 +393,8 @@ func verifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx return Claim(attestation, aprfs, ctx) } -// authorize verifies whether any of the delegated proofs grant give capability. +// authorize verifies whether any of the delegated proofs grant capability. func authorize[Caveats any](match Match[Caveats], context ClaimContext) (result.Result[Authorization[Caveats], InvalidClaim], error) { - + // load proofs from all delegations + sources, errors, err := resolveMatch(match, config) } From 21e16a719f4aedccbf4a8e6c3c1f77fd0eb37d7a Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 22 Aug 2024 16:28:21 +0200 Subject: [PATCH 14/37] feat: progress towards completing Authorize function --- validator/capability.go | 118 ++++++++++++++++++++++++++++++----- validator/error.go | 22 +++++++ validator/lib.go | 132 +++++++++++++++++++++++++++++++++------- 3 files changed, 234 insertions(+), 38 deletions(-) diff --git a/validator/capability.go b/validator/capability.go index 7b71210..3184734 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -2,6 +2,7 @@ package validator import ( "fmt" + "strings" "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/result" @@ -28,6 +29,10 @@ func (s source) Delegation() delegation.Delegation { return s.delegation } +func NewSource(capability ucan.Capability[any], delegation delegation.Delegation) Source { + return source{capability, delegation} +} + type Matcher[Caveats any] interface { Match(source Source) result.Result[Match[Caveats], InvalidCapability] } @@ -37,6 +42,7 @@ type Selector[Caveats any] interface { } type Match[Caveats any] interface { + Selector[Caveats] Source() []Source Value() ucan.Capability[Caveats] Proofs() []delegation.Delegation @@ -68,6 +74,14 @@ func (m match[Caveats]) Value() ucan.Capability[Caveats] { return m.value } +func (m match[Caveats]) Select(sources []Source) (matches []Match[Caveats], errors []DelegationError, unknowns []ucan.Capability[any], err error) { + for _, cap := range sources { + result := ResolveCapability(m.descriptor, m.value, cap) + } + // TODODODO + return +} + func NewMatch[Caveats any](source Source, capability ucan.Capability[Caveats], descriptor Descriptor[any, Caveats]) Match[Caveats] { return match[Caveats]{[]Source{source}, capability, descriptor} } @@ -87,20 +101,21 @@ type Descriptor[I, O any] interface { } type descriptor[Caveats any] struct { - can ucan.Ability - with schema.Reader[string, ucan.Resource] - nb schema.Reader[any, Caveats] + can ucan.Ability + with schema.Reader[string, ucan.Resource] + nb schema.Reader[any, Caveats] + derives DerivesFunc[Caveats] } -func (d *descriptor[C]) Can() ucan.Ability { +func (d descriptor[C]) Can() ucan.Ability { return d.can } -func (d *descriptor[C]) With() schema.Reader[string, ucan.Resource] { +func (d descriptor[C]) With() schema.Reader[string, ucan.Resource] { return d.with } -func (d *descriptor[C]) Nb() schema.Reader[any, C] { +func (d descriptor[C]) Nb() schema.Reader[any, C] { return d.nb } @@ -108,37 +123,44 @@ type capability[Caveats any] struct { descriptor Descriptor[any, Caveats] } -func (c *capability[Caveats]) Can() ucan.Ability { +func (c capability[Caveats]) Can() ucan.Ability { return c.descriptor.Can() } -func (c *capability[Caveats]) Select(capabilities []Source) ([]Match[Caveats], []DelegationError, []ucan.Capability[any], error) { +func (c capability[Caveats]) Select(capabilities []Source) ([]Match[Caveats], []DelegationError, []ucan.Capability[any], error) { return Select(c, capabilities) } -func (c *capability[Caveats]) Match(source Source) result.Result[Match[Caveats], InvalidCapability] { +func (c capability[Caveats]) Match(source Source) result.Result[Match[Caveats], InvalidCapability] { return result.MapOk( - parseCapability(c.descriptor, source), + ParseCapability(c.descriptor, source), func(cap ucan.Capability[Caveats]) Match[Caveats] { return NewMatch(source, cap, c.descriptor) }, ) } -func (c *capability[Caveats]) String() string { +func (c capability[Caveats]) String() string { return fmt.Sprintf(`{can:"%s"}`, c.Can()) } -func (c *capability[Caveats]) New(with ucan.Resource, nb Caveats) ucan.Capability[Caveats] { +func (c capability[Caveats]) New(with ucan.Resource, nb Caveats) ucan.Capability[Caveats] { return ucan.NewCapability(c.descriptor.Can(), with, nb) } -func NewCapability[Caveats any](can ucan.Ability, with schema.Reader[string, ucan.Resource], nb schema.Reader[any, Caveats]) CapabilityParser[Caveats] { - d := descriptor[Caveats]{can: can, with: with, nb: nb} - return &capability[Caveats]{descriptor: &d} +type DerivesFunc[Caveats any] func(parent ucan.Capability[Caveats], child ucan.Capability[Caveats]) result.Result[result.Unit, failure.Failure] + +func NewCapability[Caveats any]( + can ucan.Ability, + with schema.Reader[string, ucan.Resource], + nb schema.Reader[any, Caveats], + derives DerivesFunc[Caveats], +) CapabilityParser[Caveats] { + d := descriptor[Caveats]{can, with, nb, derives} + return &capability[Caveats]{descriptor: d} } -func parseCapability[O any](descriptor Descriptor[any, O], source Source) result.Result[ucan.Capability[O], InvalidCapability] { +func ParseCapability[O any](descriptor Descriptor[any, O], source Source) result.Result[ucan.Capability[O], InvalidCapability] { cap := source.Capability() if descriptor.Can() != cap.Can() { @@ -189,3 +211,67 @@ func Select[Caveats any](matcher Matcher[Caveats], capabilities []Source) (match } return } + +// ResolveCapability resolves delegated capability `source` from the `claimed` +// capability using provided capability `parser`. It is similar to +// `parseCapability` except `source` here is treated as capability pattern which +// is matched against the `claimed` capability. This means we resolve `can` and +// `with` fields from the `claimed` capability and... +// TODO: inherit all missing `nb` fields from the claimed capability. +func ResolveCapability[Caveats any](descriptor Descriptor[any, Caveats], claimed ucan.Capability[Caveats], source Source) result.Result[ucan.Capability[Caveats], InvalidCapability] { + can := ResolveAbility(source.Capability().Can(), claimed.Can()) + if can == "" { + return result.Error[ucan.Capability[Caveats], InvalidCapability](NewUnknownCapabilityError(source.Capability())) + } + + resource := ResolveResource(source.Capability().With(), claimed.With()) + if resource == "" { + resource = source.Capability().With() + } + + return result.MapResultR0( + descriptor.With().Read(resource), + func(uri string) { + // TODODODODODOD + descriptor.Nb().Read() + }, + func(x failure.Failure) InvalidCapability { + return NewMalformedCapabilityError(source.Capability(), x) + }, + ) +} + +// ResolveAbility resolves ability `pattern` of the delegated capability from +// the ability of the claimed capability. If pattern matches returns claimed +// ability otherwise returns "". +// +// - pattern "*" can "store/add" → "store/add" +// - pattern "store/*" can "store/add" → "store/add" +// - pattern "*" can "store/add" → "store/add" +// - pattern "*" can "store/add" → "" +// - pattern "*" can "store/add" → "" +// - pattern "*" can "store/add" → "" +func ResolveAbility(pattern string, can ucan.Ability) ucan.Ability { + if pattern == can || pattern == "*" { + return can + } + if strings.HasSuffix(pattern, "/*") && strings.HasPrefix(can, pattern[0:len(pattern)-1]) { + return can + } + return "" +} + +// ResolveResource resolves `source` resource of the delegated capability from +// the resource `uri` of the claimed capability. If `source` is `"ucan:*""` or +// matches `uri` then it returns `uri` back otherwise it returns "". +// +// - source "ucan:*" resource "did:key:zAlice" → "did:key:zAlice" +// - source "ucan:*" resource "https://example.com" → "https://example.com" +// - source "did:*" resource "did:key:zAlice" → "" +// - source "did:key:zAlice" resource "did:key:zAlice" → "did:key:zAlice" +func ResolveResource(source string, uri ucan.Resource) ucan.Resource { + if source == uri || source == "ucan:*" { + return uri + } + return "" +} diff --git a/validator/error.go b/validator/error.go index 961efce..a6ee23f 100644 --- a/validator/error.go +++ b/validator/error.go @@ -505,3 +505,25 @@ func indent(message string) string { func li(message string) string { return indent("- " + message) } + +type ProofError struct { + failure.NamedWithStackTrace + proof ucan.Link + cause error +} + +func (pe ProofError) Error() string { + return fmt.Sprintf("Capability can not be derived from prf: %s because: %s\n", pe.proof, li(pe.cause.Error())) +} + +func (pe ProofError) Proof() ucan.Link { + return pe.proof +} + +func (pe ProofError) Unwrap() error { + return pe.cause +} + +func NewProofError(proof ucan.Link, cause error) ProofError { + return ProofError{failure.NamedWithCurrentStackTrace("ProofError"), proof, cause} +} diff --git a/validator/lib.go b/validator/lib.go index dac55e2..424617f 100644 --- a/validator/lib.go +++ b/validator/lib.go @@ -3,7 +3,9 @@ package validator import ( "fmt" "strings" + "sync" + "github.com/storacha-network/go-ucanto/core/dag/blockstore" "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/invocation" "github.com/storacha-network/go-ucanto/core/policy" @@ -35,7 +37,7 @@ type PrincipalParser interface { ParsePrincipal(str string) (principal.Verifier, error) } -type PrincipalParserFunc = func(str string) (principal.Verifier, error) +type PrincipalParserFunc func(str string) (principal.Verifier, error) // PrincipalResolver is used to resolve a key of the principal that is // identified by DID different from did:key method. It can be passed into a @@ -46,7 +48,7 @@ type PrincipalResolver interface { // PrincipalResolverFunc resolves the key of a principal that is identified by // DID different from did:key method. -type PrincipalResolverFunc = func(did did.DID) result.Result[did.DID, UnresolvedDID] +type PrincipalResolverFunc func(did did.DID) result.Result[did.DID, UnresolvedDID] // ProofResolver finds a delegations when external proof links are present in // UCANs. If a resolver is not provided the validator may not be able to explore @@ -57,7 +59,7 @@ type ProofResolver interface { } // Resolve finds a delegation corresponding to an external proof link. -type ProofResolverFunc = func(proof ucan.Link) result.Result[delegation.Delegation, UnavailableProof] +type ProofResolverFunc func(proof ucan.Link) result.Result[delegation.Delegation, UnavailableProof] type CanIssuer[Caveats any] interface { // CanIssue informs validator whether given capability can be issued by a @@ -189,13 +191,13 @@ func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegatio var sources []Source var invalidprf []InvalidProof - delegations, rerrs := resolveProofs(proofs, context) + delegations, rerrs := ResolveProofs(proofs, context) for _, err := range rerrs { invalidprf = append(invalidprf, err) } for _, d := range delegations { - validation, err := validate(d, delegations, context) + validation, err := Validate(d, delegations, context) if err != nil { return nil, err } @@ -206,7 +208,7 @@ func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegatio validation, func(d delegation.Delegation) { for _, c := range d.Capabilities() { - sources = append(sources, source{c, d}) + sources = append(sources, NewSource(c, d)) } }, func(x InvalidProof) { @@ -236,7 +238,7 @@ func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegatio } invalidprf = append(invalidprf, revoked) } else { - authorize(matched, context) + Authorize(matched, context) } } @@ -245,11 +247,11 @@ func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegatio ), nil } -// resolveProofs takes `proofs` from the delegation which may contain +// ResolveProofs takes `proofs` from the delegation which may contain // a `Delegation` or a link to one and attempts to resolve links by side loading // them. It returns a set of resolved `Delegation`s and errors for the proofs // that could not be resolved. -func resolveProofs(proofs []delegation.Proof, resolver ProofResolver) (dels []delegation.Delegation, errs []UnavailableProof) { +func ResolveProofs(proofs []delegation.Proof, resolver ProofResolver) (dels []delegation.Delegation, errs []UnavailableProof) { for _, p := range proofs { d, ok := p.Delegation() if ok { @@ -267,24 +269,24 @@ func resolveProofs(proofs []delegation.Proof, resolver ProofResolver) (dels []de // Validate a delegation to check it is within the time bound and that it is // authorized by the issuer. -func validate(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (result.Result[delegation.Delegation, InvalidProof], error) { +func Validate(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (result.Result[delegation.Delegation, InvalidProof], error) { if ucan.IsExpired(dlg) { return result.Error[delegation.Delegation, InvalidProof](NewExpiredError(dlg)), nil } if ucan.IsTooEarly(dlg) { return result.Error[delegation.Delegation, InvalidProof](NewNotValidBeforeError(dlg)), nil } - return verifyAuthorization(dlg, prfs, ctx) + return VerifyAuthorization(dlg, prfs, ctx) } -// verifyAuthorization verifies that delegation has been authorized by the +// VerifyAuthorization verifies that delegation has been authorized by the // issuer. If issued by the did:key principal checks that the signature is // valid. If issued by the root authority checks that the signature is valid. If // issued by the principal identified by other DID method attempts to resolve a // valid `ucan/attest` attestation from the authority, if attestation is not // found falls back to resolving did:key for the issuer and verifying its // signature. -func verifyAuthorization(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (result.Result[delegation.Delegation, InvalidProof], error) { +func VerifyAuthorization(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (result.Result[delegation.Delegation, InvalidProof], error) { issuer := dlg.Issuer().DID() // If the issuer is a did:key we just verify a signature if strings.HasPrefix(issuer.String(), "did:key:") { @@ -292,7 +294,7 @@ func verifyAuthorization(dlg delegation.Delegation, prfs []delegation.Delegation if err != nil { return nil, err } - sig, err := verifySignature(dlg, vfr) + sig, err := VerifySignature(dlg, vfr) if err != nil { return nil, err } @@ -302,7 +304,7 @@ func verifyAuthorization(dlg delegation.Delegation, prfs []delegation.Delegation } // Attempt to resolve embedded authorization session from the authority - sess, err := verifySession(dlg, prfs, ctx) + sess, err := VerifySession(dlg, prfs, ctx) if err != nil { return nil, err } @@ -336,7 +338,7 @@ func verifyAuthorization(dlg delegation.Delegation, prfs []delegation.Delegation return result.MatchResultR2( vfr, func(v principal.Verifier) (result.Result[delegation.Delegation, InvalidProof], error) { - sig, err := verifySignature(dlg, v) + sig, err := VerifySignature(dlg, v) if err != nil { return nil, err } @@ -352,7 +354,7 @@ func verifyAuthorization(dlg delegation.Delegation, prfs []delegation.Delegation ) } -func verifySignature(dlg delegation.Delegation, vfr principal.Verifier) (result.Result[delegation.Delegation, InvalidSignature], error) { +func VerifySignature(dlg delegation.Delegation, vfr principal.Verifier) (result.Result[delegation.Delegation, InvalidSignature], error) { ok, err := ucan.VerifySignature(dlg.Data(), vfr) if err != nil { return nil, err @@ -363,12 +365,12 @@ func verifySignature(dlg delegation.Delegation, vfr principal.Verifier) (result. return result.Ok[delegation.Delegation, InvalidSignature](dlg), nil } -// verifySession attempts to find an authorization session - an `ucan/attest` +// VerifySession attempts to find an authorization session - an `ucan/attest` // capability delegation where `with` matches `config.authority` and `nb.proof` // matches given delegation. // // https://github.com/storacha-network/specs/blob/main/w3-session.md#authorization-session -func verifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (result.Result[Authorization[vdm.AttestationModel], Unauthorized], error) { +func VerifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (result.Result[Authorization[vdm.AttestationModel], Unauthorized], error) { // Create a schema that will match an authorization for this exact delegation attestation := NewCapability( "ucan/attest", @@ -393,8 +395,94 @@ func verifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx return Claim(attestation, aprfs, ctx) } -// authorize verifies whether any of the delegated proofs grant capability. -func authorize[Caveats any](match Match[Caveats], context ClaimContext) (result.Result[Authorization[Caveats], InvalidClaim], error) { +// Authorize verifies whether any of the delegated proofs grant capability. +func Authorize[Caveats any](match Match[Caveats], context ClaimContext) (result.Result[Authorization[Caveats], InvalidClaim], error) { // load proofs from all delegations - sources, errors, err := resolveMatch(match, config) + sources, errors, err := ResolveMatch(match, context) + if err != nil { + return nil, err + } + + match.Select(sources) +} + +func ResolveMatch[Caveats any](match Match[Caveats], context ClaimContext) (sources []Source, errors []ProofError, err error) { + includes := map[string]struct{}{} + var wg sync.WaitGroup + var lock sync.RWMutex + for _, source := range match.Source() { + id := source.Delegation().Link().String() + if _, ok := includes[id]; !ok { + includes[id] = struct{}{} + wg.Add(1) + go func(s Source) { + srcs, errs, rerr := ResolveSources(s, context) + lock.Lock() + defer lock.Unlock() + defer wg.Done() + if rerr != nil { + err = rerr + return + } + sources = append(sources, srcs...) + errors = append(errors, errs...) + }(source) + } + } + wg.Wait() + return +} + +func ResolveSources(source Source, context ClaimContext) (sources []Source, errors []ProofError, err error) { + dlg := source.Delegation() + var prfs []delegation.Delegation + + br, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(dlg.Blocks())) + if err != nil { + return nil, nil, err + } + + dlgs, failedprf := ResolveProofs( + delegation.NewProofsView(dlg.Proofs(), br), + context, + ) + + // All the proofs that failed to resolve are saved as proof errors. + for _, err := range failedprf { + errors = append(errors, NewProofError(err.Link(), err)) + } + + // All the proofs that resolved are checked for principal alignment. Ones that + // do not align are saved as proof errors. + for _, prf := range dlgs { + // If proof does not delegate to a matching audience save an proof error. + if dlg.Issuer().DID() != prf.Audience().DID() { + errors = append(errors, NewProofError(prf.Link(), NewPrincipalAlignmentError(dlg.Issuer(), prf))) + } else { + prfs = append(prfs, prf) + } + } + // In the second pass we attempt to proofs that were resolved and are aligned. + for _, prf := range prfs { + validation, err := Validate(prf, prfs, context) + if err != nil { + return nil, nil, err + } + // If proof is not valid (expired, not active yet or has incorrect + // signature) save a corresponding proof error. + // otherwise create source objects for it's capabilities, so we could + // track which proof in which capability the are from. + result.MatchResultR0( + validation, + func(d delegation.Delegation) { + for _, cap := range prf.Capabilities() { + sources = append(sources, NewSource(cap, prf)) + } + }, + func(x InvalidProof) { + errors = append(errors, NewProofError(prf.Link(), x)) + }, + ) + } + return } From a1820a1fc4f259b27a9496391740cd6932e60e9a Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Fri, 23 Aug 2024 12:58:36 +0200 Subject: [PATCH 15/37] feat: compiles --- core/delegation/delegate.go | 3 +- core/invocation/invocation.go | 5 +- core/result/result.go | 4 +- server/server_test.go | 4 +- ucan/formatter/formatter.go | 4 +- ucan/lib.go | 2 +- validator/capability.go | 124 ++++++++++++++---- validator/datamodel/attestation.go | 2 +- ...testration.ipldsch => attestation.ipldsch} | 0 validator/datamodel/errors.ipldsch | 6 +- validator/error.go | 91 ++++++++++++- validator/lib.go | 83 +++++++++++- 12 files changed, 275 insertions(+), 53 deletions(-) rename validator/datamodel/{attestration.ipldsch => attestation.ipldsch} (100%) diff --git a/core/delegation/delegate.go b/core/delegation/delegate.go index 83a4375..a176513 100644 --- a/core/delegation/delegate.go +++ b/core/delegation/delegate.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/storacha-network/go-ucanto/core/dag/blockstore" - "github.com/storacha-network/go-ucanto/core/ipld" "github.com/storacha-network/go-ucanto/core/ipld/block" "github.com/storacha-network/go-ucanto/core/ipld/codec/cbor" "github.com/storacha-network/go-ucanto/core/ipld/hash/sha256" @@ -71,7 +70,7 @@ func WithProofs(prf Proofs) Option { // Delegate creates a new signed token with a given `options.issuer`. If // expiration is not set it defaults to 30 seconds from now. Returns UCAN in // primary IPLD representation. -func Delegate(issuer ucan.Signer, audience ucan.Principal, capabilities []ucan.Capability[ipld.Builder], options ...Option) (Delegation, error) { +func Delegate[C ucan.CaveatBuilder](issuer ucan.Signer, audience ucan.Principal, capabilities []ucan.Capability[C], options ...Option) (Delegation, error) { cfg := delegationConfig{} for _, opt := range options { if err := opt(&cfg); err != nil { diff --git a/core/invocation/invocation.go b/core/invocation/invocation.go index 7109ae4..5340709 100644 --- a/core/invocation/invocation.go +++ b/core/invocation/invocation.go @@ -32,7 +32,6 @@ type IssuedInvocation interface { Invocation } -func Invoke[C ipld.Builder](issuer ucan.Signer, audience ucan.Principal, capability ucan.Capability[C], options ...delegation.Option) (IssuedInvocation, error) { - bcap := ucan.NewCapability(capability.Can(), capability.With(), ipld.Builder(capability.Nb())) - return delegation.Delegate(issuer, audience, []ucan.Capability[ipld.Builder]{bcap}, options...) +func Invoke[C ucan.CaveatBuilder](issuer ucan.Signer, audience ucan.Principal, capability ucan.Capability[C], options ...delegation.Option) (IssuedInvocation, error) { + return delegation.Delegate(issuer, audience, []ucan.Capability[C]{capability}, options...) } diff --git a/core/result/result.go b/core/result/result.go index f858287..c24bb00 100644 --- a/core/result/result.go +++ b/core/result/result.go @@ -7,7 +7,6 @@ import ( ) // Result is a golang compatible generic result type -// https://github.com/ucan-wg/receipt/#6-result type Result[O any, X any] interface { isResult(ok O, err X) } @@ -19,8 +18,7 @@ type errResult[O any, X any] struct { value X } -func (o *okResult[O, X]) isResult(ok O, err X) {} - +func (o *okResult[O, X]) isResult(ok O, err X) {} func (e *errResult[O, X]) isResult(ok O, err X) {} // MatchResultR3 handles a result with functions returning 3 values diff --git a/server/server_test.go b/server/server_test.go index 23fb56e..c24cae2 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -81,7 +81,7 @@ func TestHandlerNotFound(t *testing.T) { capability := ucan.NewCapability( "upload/add", space.DID().String(), - ipld.Builder(&uploadAddCaveats{Root: rt}), + uploadAddCaveats{Root: rt}, ) invs := []invocation.Invocation{helpers.Must(invocation.Invoke(alice, service, capability))} @@ -116,6 +116,7 @@ func TestSimpleHandler(t *testing.T) { "upload/add", schema.DIDString(), schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), + nil, ) server := helpers.Must(NewServer( @@ -169,6 +170,7 @@ func TestHandlerExecutionError(t *testing.T) { "upload/add", schema.DIDString(), schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), + nil, ) server := helpers.Must(NewServer( diff --git a/ucan/formatter/formatter.go b/ucan/formatter/formatter.go index 15a440a..b90d722 100644 --- a/ucan/formatter/formatter.go +++ b/ucan/formatter/formatter.go @@ -29,7 +29,7 @@ func FormatHeader(version string, algorithm string) (string, error) { Ucv: version, Typ: "JWT", } - bytes, err := ipld.Marshal(dagjson.Encode, header, hdm.Type()) + bytes, err := ipld.Marshal(dagjson.Encode, &header, hdm.Type()) if err != nil { return "", fmt.Errorf("dag-json encoding header: %s", err) } @@ -37,7 +37,7 @@ func FormatHeader(version string, algorithm string) (string, error) { } func FormatPayload(payload pdm.PayloadModel) (string, error) { - bytes, err := ipld.Marshal(dagjson.Encode, payload, pdm.Type()) + bytes, err := ipld.Marshal(dagjson.Encode, &payload, pdm.Type()) if err != nil { return "", fmt.Errorf("dag-json encoding payload: %s", err) } diff --git a/ucan/lib.go b/ucan/lib.go index 1c9e44c..e8faa9e 100644 --- a/ucan/lib.go +++ b/ucan/lib.go @@ -80,7 +80,7 @@ type CaveatBuilder interface { // Issue creates a new signed token with a given issuer. If expiration is // not set it defaults to 30 seconds from now. -func Issue(issuer Signer, audience Principal, capabilities []Capability[CaveatBuilder], options ...Option) (View, error) { +func Issue[C CaveatBuilder](issuer Signer, audience Principal, capabilities []Capability[C], options ...Option) (View, error) { cfg := ucanConfig{} for _, opt := range options { if err := opt(&cfg); err != nil { diff --git a/validator/capability.go b/validator/capability.go index 3184734..5a355e1 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -52,7 +52,7 @@ type Match[Caveats any] interface { type match[Caveats any] struct { sources []Source value ucan.Capability[Caveats] - descriptor Descriptor[any, Caveats] + descriptor Descriptor[Caveats] } func (m match[Caveats]) Proofs() []delegation.Delegation { @@ -75,14 +75,44 @@ func (m match[Caveats]) Value() ucan.Capability[Caveats] { } func (m match[Caveats]) Select(sources []Source) (matches []Match[Caveats], errors []DelegationError, unknowns []ucan.Capability[any], err error) { - for _, cap := range sources { - result := ResolveCapability(m.descriptor, m.value, cap) + for _, source := range sources { + err = result.MatchResultR1( + ResolveCapability(m.descriptor, m.value, source), + func(cap ucan.Capability[Caveats]) error { + result.MatchResultR0( + m.descriptor.Derives(m.value, cap), + func(_ result.Unit) { + matches = append(matches, NewMatch(source, cap, m.descriptor)) + }, + func(x failure.Failure) { + errors = append(errors, NewDelegationError([]DelegationSubError{NewEscalatedCapabilityError(m.value, cap, x)}, m)) + }, + ) + return nil + }, + func(x InvalidCapability) error { + if uerr, ok := err.(UnknownCapability); ok { + unknowns = append(unknowns, uerr.Capability()) + } + if merr, ok := err.(MalformedCapability); ok { + errors = append(errors, NewDelegationError([]DelegationSubError{merr}, m)) + } + return fmt.Errorf("unexpected error type in resolved capability result") + }, + ) + if err != nil { + return + } } - // TODODODO return } -func NewMatch[Caveats any](source Source, capability ucan.Capability[Caveats], descriptor Descriptor[any, Caveats]) Match[Caveats] { +func (m match[Caveats]) String() string { + s, _ := m.value.MarshalJSON() + return string(s) +} + +func NewMatch[Caveats any](source Source, capability ucan.Capability[Caveats], descriptor Descriptor[Caveats]) Match[Caveats] { return match[Caveats]{[]Source{source}, capability, descriptor} } @@ -94,10 +124,18 @@ type CapabilityParser[Caveats any] interface { New(with ucan.Resource, nb Caveats) ucan.Capability[Caveats] } -type Descriptor[I, O any] interface { +type Derivable[Caveats any] interface { + // Derives determines if a capability is derivable from another. + Derives(claimed, delegated ucan.Capability[Caveats]) result.Result[result.Unit, failure.Failure] +} + +type DerivesFunc[Caveats any] func(claimed, delegated ucan.Capability[Caveats]) result.Result[result.Unit, failure.Failure] + +type Descriptor[Caveats any] interface { + Derivable[Caveats] Can() ucan.Ability With() schema.Reader[string, ucan.Resource] - Nb() schema.Reader[I, O] + Nb() schema.Reader[any, Caveats] } type descriptor[Caveats any] struct { @@ -119,8 +157,12 @@ func (d descriptor[C]) Nb() schema.Reader[any, C] { return d.nb } +func (d descriptor[C]) Derives(parent, child ucan.Capability[C]) result.Result[result.Unit, failure.Failure] { + return d.derives(parent, child) +} + type capability[Caveats any] struct { - descriptor Descriptor[any, Caveats] + descriptor Descriptor[Caveats] } func (c capability[Caveats]) Can() ucan.Ability { @@ -148,19 +190,20 @@ func (c capability[Caveats]) New(with ucan.Resource, nb Caveats) ucan.Capability return ucan.NewCapability(c.descriptor.Can(), with, nb) } -type DerivesFunc[Caveats any] func(parent ucan.Capability[Caveats], child ucan.Capability[Caveats]) result.Result[result.Unit, failure.Failure] - func NewCapability[Caveats any]( can ucan.Ability, with schema.Reader[string, ucan.Resource], nb schema.Reader[any, Caveats], derives DerivesFunc[Caveats], ) CapabilityParser[Caveats] { + if derives == nil { + derives = DefaultDerives + } d := descriptor[Caveats]{can, with, nb, derives} return &capability[Caveats]{descriptor: d} } -func ParseCapability[O any](descriptor Descriptor[any, O], source Source) result.Result[ucan.Capability[O], InvalidCapability] { +func ParseCapability[O any](descriptor Descriptor[O], source Source) result.Result[ucan.Capability[O], InvalidCapability] { cap := source.Capability() if descriptor.Can() != cap.Can() { @@ -218,7 +261,7 @@ func Select[Caveats any](matcher Matcher[Caveats], capabilities []Source) (match // is matched against the `claimed` capability. This means we resolve `can` and // `with` fields from the `claimed` capability and... // TODO: inherit all missing `nb` fields from the claimed capability. -func ResolveCapability[Caveats any](descriptor Descriptor[any, Caveats], claimed ucan.Capability[Caveats], source Source) result.Result[ucan.Capability[Caveats], InvalidCapability] { +func ResolveCapability[Caveats any](descriptor Descriptor[Caveats], claimed ucan.Capability[Caveats], source Source) result.Result[ucan.Capability[Caveats], InvalidCapability] { can := ResolveAbility(source.Capability().Can(), claimed.Can()) if can == "" { return result.Error[ucan.Capability[Caveats], InvalidCapability](NewUnknownCapabilityError(source.Capability())) @@ -229,14 +272,22 @@ func ResolveCapability[Caveats any](descriptor Descriptor[any, Caveats], claimed resource = source.Capability().With() } - return result.MapResultR0( + return result.MatchResultR1( descriptor.With().Read(resource), - func(uri string) { - // TODODODODODOD - descriptor.Nb().Read() + func(uri string) result.Result[ucan.Capability[Caveats], InvalidCapability] { + return result.MapResultR0( + // TODO: inherit missing fields + descriptor.Nb().Read(claimed), + func(nb Caveats) ucan.Capability[Caveats] { + return ucan.NewCapability(can, resource, nb) + }, + func(x failure.Failure) InvalidCapability { + return NewMalformedCapabilityError(source.Capability(), x) + }, + ) }, - func(x failure.Failure) InvalidCapability { - return NewMalformedCapabilityError(source.Capability(), x) + func(x failure.Failure) result.Result[ucan.Capability[Caveats], InvalidCapability] { + return result.Error[ucan.Capability[Caveats], InvalidCapability](NewMalformedCapabilityError(source.Capability(), x)) }, ) } @@ -245,12 +296,12 @@ func ResolveCapability[Caveats any](descriptor Descriptor[any, Caveats], claimed // the ability of the claimed capability. If pattern matches returns claimed // ability otherwise returns "". // -// - pattern "*" can "store/add" → "store/add" -// - pattern "store/*" can "store/add" → "store/add" -// - pattern "*" can "store/add" → "store/add" -// - pattern "*" can "store/add" → "" -// - pattern "*" can "store/add" → "" -// - pattern "*" can "store/add" → "" +// - pattern "*" can "store/add" → "store/add" +// - pattern "store/*" can "store/add" → "store/add" +// - pattern "*" can "store/add" → "store/add" +// - pattern "*" can "store/add" → "" +// - pattern "*" can "store/add" → "" +// - pattern "*" can "store/add" → "" func ResolveAbility(pattern string, can ucan.Ability) ucan.Ability { if pattern == can || pattern == "*" { return can @@ -265,13 +316,30 @@ func ResolveAbility(pattern string, can ucan.Ability) ucan.Ability { // the resource `uri` of the claimed capability. If `source` is `"ucan:*""` or // matches `uri` then it returns `uri` back otherwise it returns "". // -// - source "ucan:*" resource "did:key:zAlice" → "did:key:zAlice" -// - source "ucan:*" resource "https://example.com" → "https://example.com" -// - source "did:*" resource "did:key:zAlice" → "" -// - source "did:key:zAlice" resource "did:key:zAlice" → "did:key:zAlice" +// - source "ucan:*" uri "did:key:zAlice" → "did:key:zAlice" +// - source "ucan:*" uri "https://example.com" → "https://example.com" +// - source "did:*" uri "did:key:zAlice" → "" +// - source "did:key:zAlice" uri "did:key:zAlice" → "did:key:zAlice" func ResolveResource(source string, uri ucan.Resource) ucan.Resource { if source == uri || source == "ucan:*" { return uri } return "" } + +func DefaultDerives[Caveats any](claimed, delegated ucan.Capability[Caveats]) result.Result[result.Unit, failure.Failure] { + dres := delegated.With() + cres := claimed.With() + + if strings.HasSuffix(dres, "*") { + if !strings.HasPrefix(cres, dres[0:len(dres)-1]) { + return result.Error[result.Unit](schema.NewSchemaError(fmt.Sprintf("Resource %s does not match delegated %s", cres, dres))) + } + } else if dres != cres { + return result.Error[result.Unit](schema.NewSchemaError(fmt.Sprintf("Resource %s is not contained by %s", cres, dres))) + } + + // TODO: is it possible to ensure claimed caveats match delegated caveats? + + return result.Ok[result.Unit, failure.Failure](struct{}{}) +} diff --git a/validator/datamodel/attestation.go b/validator/datamodel/attestation.go index bce2084..d2c88f8 100644 --- a/validator/datamodel/attestation.go +++ b/validator/datamodel/attestation.go @@ -8,7 +8,7 @@ import ( "github.com/ipld/go-ipld-prime/schema" ) -//go:embed errors.ipldsch +//go:embed attestation.ipldsch var attestationsch []byte var attestationTypeSystem *schema.TypeSystem diff --git a/validator/datamodel/attestration.ipldsch b/validator/datamodel/attestation.ipldsch similarity index 100% rename from validator/datamodel/attestration.ipldsch rename to validator/datamodel/attestation.ipldsch diff --git a/validator/datamodel/errors.ipldsch b/validator/datamodel/errors.ipldsch index fdd20fc..f57c6a7 100644 --- a/validator/datamodel/errors.ipldsch +++ b/validator/datamodel/errors.ipldsch @@ -7,19 +7,19 @@ type InvalidAudience struct { Audience String Delegation Delegation Message String - Stack optional Sstring + Stack optional String } type Expired struct { Name optional String Message String - ExpiredAt Integer + ExpiredAt Int Stack optional String } type NotValidBefore struct { Name optional String Message String - ValidAt Integer + ValidAt Int Stack optional String } diff --git a/validator/error.go b/validator/error.go index a6ee23f..9267d4d 100644 --- a/validator/error.go +++ b/validator/error.go @@ -35,7 +35,7 @@ type EscalatedCapabilityError[Caveats any] struct { cause error } -func NewEscalatedCapabilityError[Caveats any](claimed ucan.Capability[Caveats], delegated interface{}, cause error) failure.Failure { +func NewEscalatedCapabilityError[Caveats any](claimed ucan.Capability[Caveats], delegated interface{}, cause error) EscalatedCapabilityError[Caveats] { return EscalatedCapabilityError[Caveats]{failure.NamedWithCurrentStackTrace("EscalatedCapability"), claimed, delegated, cause} } @@ -260,6 +260,7 @@ type InvalidCapability interface { type MalformedCapability interface { InvalidCapability + DelegationSubError Capability() ucan.Capability[any] isMalformedCapability() } @@ -400,14 +401,98 @@ func (nvbe NotValidBeforeError) Build() (datamodel.Node, error) { func (nvbe NotValidBeforeError) isInvalidProof() {} -// TODO: this may just be the concrete type from the implementation once -// the rest of the validator is done type InvalidClaim interface { failure.Failure Issuer() ucan.Principal Delegation() delegation.Delegation } +type InvalidClaimError[Caveats any] struct { + failure.NamedWithStackTrace + match Match[Caveats] + delegationErrors []DelegationError + unknownCapabilities []ucan.Capability[any] + invalidProofs []ProofError + failedProofs []InvalidClaim +} + +func NewInvalidClaimError[Caveats any]( + match Match[Caveats], + delegationErrors []DelegationError, + unknownCapabilities []ucan.Capability[any], + invalidProofs []ProofError, + failedProofs []InvalidClaim, +) InvalidClaim { + return InvalidClaimError[Caveats]{ + failure.NamedWithCurrentStackTrace("InvalidClaim"), + match, + delegationErrors, + unknownCapabilities, + invalidProofs, + failedProofs, + } +} + +func (ice InvalidClaimError[Caveats]) Error() string { + errorStrings := make([]string, 0, len(ice.failedProofs)+len(ice.delegationErrors)+len(ice.invalidProofs)) + + for _, failedProof := range ice.failedProofs { + errorStrings = append(errorStrings, li(failedProof.Error())) + } + + for _, delegationError := range ice.delegationErrors { + errorStrings = append(errorStrings, li(delegationError.Error())) + } + + for _, invalidProof := range ice.invalidProofs { + errorStrings = append(errorStrings, li(invalidProof.Error())) + } + + unknowns := make([]string, 0, len(ice.unknownCapabilities)) + for _, unknownCapability := range ice.unknownCapabilities { + out, _ := unknownCapability.MarshalJSON() + unknowns = append(unknowns, li(string(out))) + } + + var finalList []string + finalList = append(finalList, fmt.Sprintf("Capability %s is not authorized because:", ice.match)) + finalList = append(finalList, li(fmt.Sprintf("Capability can not be (self) issued by '%s'", ice.Issuer().DID()))) + if len(errorStrings) > 0 { + finalList = append(finalList, errorStrings...) + } else { + finalList = append(finalList, li("Delegated capability not found")) + } + if len(unknowns) > 0 { + finalList = append(finalList, li(fmt.Sprintf("Encountered unknown capabilities\n%s", strings.Join(unknowns, "\n")))) + } + + return strings.Join(finalList, "\n") +} + +func (ice InvalidClaimError[Caveats]) Issuer() ucan.Principal { + return ice.Delegation().Issuer() +} + +func (ice InvalidClaimError[Caveats]) Delegation() delegation.Delegation { + return ice.match.Source()[0].Delegation() +} + +func (ice InvalidClaimError[Caveats]) DelegationErrors() []DelegationError { + return ice.delegationErrors +} + +func (ice InvalidClaimError[Caveats]) UnknownCapabilities() []ucan.Capability[any] { + return ice.unknownCapabilities +} + +func (ice InvalidClaimError[Caveats]) InvalidProofs() []ProofError { + return ice.invalidProofs +} + +func (ice InvalidClaimError[Caveats]) FailedProofs() []InvalidClaim { + return ice.failedProofs +} + type Unauthorized interface { failure.Failure DelegationErrors() []DelegationError diff --git a/validator/lib.go b/validator/lib.go index 424617f..182b1ac 100644 --- a/validator/lib.go +++ b/validator/lib.go @@ -12,6 +12,7 @@ import ( "github.com/storacha-network/go-ucanto/core/policy/literal" "github.com/storacha-network/go-ucanto/core/policy/selector" "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/did" "github.com/storacha-network/go-ucanto/principal" @@ -227,18 +228,44 @@ func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegatio for _, matched := range matches { selector := matched.Prune(canissuer[Caveats]{canIssue: context.CanIssue}) if selector == nil { - authorization := NewAuthorization(matched, nil) + auth := NewAuthorization(matched, nil) revoked := result.MatchResultR1( - context.ValidateAuthorization(ConvertUnknownAuthorization(authorization)), + context.ValidateAuthorization(ConvertUnknownAuthorization(auth)), func(o result.Unit) Revoked { return nil }, func(x Revoked) Revoked { return x }, ) if revoked == nil { - return result.Ok[Authorization[Caveats], Unauthorized](authorization), nil + return result.Ok[Authorization[Caveats], Unauthorized](auth), nil } invalidprf = append(invalidprf, revoked) } else { - Authorize(matched, context) + ar, err := Authorize(matched, context) + if err != nil { + return nil, err + } + auth := result.MatchResultR1( + ar, + func(a Authorization[Caveats]) Authorization[Caveats] { + auth := NewAuthorization(matched, []Authorization[Caveats]{a}) + return result.MatchResultR1( + context.ValidateAuthorization(ConvertUnknownAuthorization(auth)), + func(o result.Unit) Authorization[Caveats] { + return auth + }, + func(x Revoked) Authorization[Caveats] { + invalidprf = append(invalidprf, x) + return nil + }, + ) + }, + func(x InvalidClaim) Authorization[Caveats] { + failedprf = append(failedprf, x) + return nil + }, + ) + if auth != nil { + return result.Ok[Authorization[Caveats], Unauthorized](auth), nil + } } } @@ -381,6 +408,17 @@ func VerifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx policy.Equal(selector.MustParse(".proof"), literal.Link(dlg.Link())), }, ), + func(claimed, delegated ucan.Capability[vdm.AttestationModel]) result.Result[result.Unit, failure.Failure] { + return result.AndThen( + DefaultDerives(claimed, delegated), + func(o result.Unit) result.Result[result.Unit, failure.Failure] { + if claimed.Nb().Proof != delegated.Nb().Proof { + return result.Error[result.Unit, failure.Failure](schema.NewSchemaError(fmt.Sprintf(`proof: %s violates %s`, claimed.Nb().Proof, delegated.Nb().Proof))) + } + return result.Ok[result.Unit, failure.Failure](o) + }, + ) + }, ) // We only consider attestations otherwise we will end up doing an @@ -398,12 +436,45 @@ func VerifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx // Authorize verifies whether any of the delegated proofs grant capability. func Authorize[Caveats any](match Match[Caveats], context ClaimContext) (result.Result[Authorization[Caveats], InvalidClaim], error) { // load proofs from all delegations - sources, errors, err := ResolveMatch(match, context) + sources, invalidprf, err := ResolveMatch(match, context) + if err != nil { + return nil, err + } + + matches, dlgerrs, unknowns, err := match.Select(sources) if err != nil { return nil, err } - match.Select(sources) + var failedprf []InvalidClaim + for _, matched := range matches { + selector := matched.Prune(canissuer[Caveats]{canIssue: context.CanIssue}) + if selector == nil { + return result.Ok[Authorization[Caveats], InvalidClaim](NewAuthorization(matched, nil)), nil + } else { + ar, err := Authorize(matched, context) + if err != nil { + return nil, err + } + auth := result.MatchResultR1( + ar, + func(a Authorization[Caveats]) Authorization[Caveats] { + return NewAuthorization(matched, []Authorization[Caveats]{a}) + }, + func(x InvalidClaim) Authorization[Caveats] { + failedprf = append(failedprf, x) + return nil + }, + ) + if auth != nil { + return result.Ok[Authorization[Caveats], InvalidClaim](auth), nil + } + } + } + + return result.Error[Authorization[Caveats], InvalidClaim]( + NewInvalidClaimError(match, dlgerrs, unknowns, invalidprf, failedprf), + ), nil } func ResolveMatch[Caveats any](match Match[Caveats], context ClaimContext) (sources []Source, errors []ProofError, err error) { From 545184804c23877c6ee8e706839c63ec98348e2f Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Fri, 23 Aug 2024 14:38:37 +0200 Subject: [PATCH 16/37] test: initial test --- validator/authorization.go | 2 +- validator/capability.go | 6 ++ validator/fixtures_test.go | 14 +++++ validator/lib_test.go | 114 +++++++++++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 validator/fixtures_test.go diff --git a/validator/authorization.go b/validator/authorization.go index e97b09a..d622bcc 100644 --- a/validator/authorization.go +++ b/validator/authorization.go @@ -65,7 +65,7 @@ func (a unknownauth[C]) Issuer() ucan.Principal { func (a unknownauth[C]) Proofs() []Authorization[any] { var prf []Authorization[any] - for _, p := range a.Proofs() { + for _, p := range a.auth.Proofs() { prf = append(prf, ConvertUnknownAuthorization(p)) } return prf diff --git a/validator/capability.go b/validator/capability.go index 5a355e1..6379f33 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -122,6 +122,8 @@ type CapabilityParser[Caveats any] interface { Can() ucan.Ability // New creates a new capability from the passed options. New(with ucan.Resource, nb Caveats) ucan.Capability[Caveats] + // Invoke creates an invocation of this capability. + // Invoke(with ucan.Resource, nb Caveats) (invocation.IssuedInvocation, error) } type Derivable[Caveats any] interface { @@ -190,6 +192,10 @@ func (c capability[Caveats]) New(with ucan.Resource, nb Caveats) ucan.Capability return ucan.NewCapability(c.descriptor.Can(), with, nb) } +// func (c capability[Caveats]) Invoke(issuer ucan.Signer, audience ucan.Principal, with ucan.Resource, nb Caveats, options ...delegation.Option) (invocation.IssuedInvocation, error) { +// return invocation.Invoke(issuer, audience, c.New(with, nb), options...) +// } + func NewCapability[Caveats any]( can ucan.Ability, with schema.Reader[string, ucan.Resource], diff --git a/validator/fixtures_test.go b/validator/fixtures_test.go new file mode 100644 index 0000000..8cdaa68 --- /dev/null +++ b/validator/fixtures_test.go @@ -0,0 +1,14 @@ +package validator + +import "github.com/storacha-network/go-ucanto/principal/ed25519/signer" + +// did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi +var alice, _ = signer.Parse("MgCZT5vOnYZoVAeyjnzuJIVY9J4LNtJ+f8Js0cTPuKUpFne0BVEDJjEu6quFIU8yp91/TY/+MYK8GvlKoTDnqOCovCVM=") + +// did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob +var bob, _ = signer.Parse("MgCYbj5AJfVvdrjkjNCxB3iAUwx7RQHVQ7H1sKyHy46Iose0BEevXgL1V73PD9snOCIoONgb+yQ9sycYchQC8kygR4qY=") + +// did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL +var mallory, _ = signer.Parse("'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=") + +var service, _ = signer.Parse("MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=") diff --git a/validator/lib_test.go b/validator/lib_test.go index c64b66e..c7b585b 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -1,12 +1,126 @@ package validator import ( + "fmt" "testing" + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/storacha-network/go-ucanto/core/invocation" + "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" + "github.com/storacha-network/go-ucanto/core/schema" + "github.com/storacha-network/go-ucanto/principal" "github.com/storacha-network/go-ucanto/principal/ed25519/signer" + "github.com/storacha-network/go-ucanto/principal/ed25519/verifier" "github.com/storacha-network/go-ucanto/ucan" + "github.com/stretchr/testify/require" ) +type storeAddCaveats struct { + Link ipld.Link + Origin ipld.Link +} + +func (c storeAddCaveats) Build() (ipld.Node, error) { + np := basicnode.Prototype.Any + nb := np.NewBuilder() + ma, _ := nb.BeginMap(2) + ma.AssembleKey().AssignString("link") + ma.AssembleValue().AssignLink(c.Link) + ma.AssembleKey().AssignString("origin") + ma.AssembleValue().AssignLink(c.Origin) + ma.Finish() + return nb.Build(), nil +} + +func newStoreAddCapability(t *testing.T) CapabilityParser[storeAddCaveats] { + t.Helper() + + typ, err := ipld.LoadSchemaBytes([]byte(` + type StoreAddCaveats struct { + link Link + origin optional Link + } + `)) + require.NoError(t, err) + + return NewCapability( + "store/add", + schema.DIDString(), + schema.Struct[storeAddCaveats](typ.TypeByName("StoreAddCaveats"), nil), + func(claimed, delegated ucan.Capability[storeAddCaveats]) result.Result[result.Unit, failure.Failure] { + if claimed.With() != delegated.With() { + err := fmt.Errorf("Expected 'with: \"%s\"' instead got '%s'", delegated.With(), claimed.With()) + return result.Error[result.Unit](failure.FromError(err)) + } + + if delegated.Nb().Link != nil && delegated.Nb().Link != claimed.Nb().Link { + var err error + if claimed.Nb().Link == nil { + err = fmt.Errorf("Link violates imposed %s constraint", delegated.Nb().Link) + } else { + err = fmt.Errorf("Link %s violates imposed %s constraint", claimed.Nb().Link, delegated.Nb().Link) + } + return result.Error[result.Unit](failure.FromError(err)) + } + + return result.Ok[result.Unit, failure.Failure](struct{}{}) + }, + ) +} + +func TestAccess(t *testing.T) { + storeAdd := newStoreAddCapability(t) + testLink := cidlink.Link{Cid: cid.MustParse("bafkqaaa")} + validateAuthOk := func(auth Authorization[any]) result.Result[result.Unit, Revoked] { + return result.Ok[result.Unit, Revoked](nil) + } + parseEdPrincipal := func(str string) (principal.Verifier, error) { + return verifier.Parse(str) + } + + t.Run("authorize self-issued invocation", func(t *testing.T) { + inv, err := invocation.Invoke( + alice, + bob, + storeAdd.New( + alice.DID().String(), + storeAddCaveats{Link: testLink}, + ), + ) + require.NoError(t, err) + + context := NewValidationContext( + service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + res, err := Access(inv, context) + require.NoError(t, err) + + result.MatchResultR0( + res, + func(a Authorization[storeAddCaveats]) { + require.Equal(t, storeAdd.Can(), a.Capability().Can()) + require.Equal(t, alice.DID(), a.Capability().With()) + require.Equal(t, alice.DID(), a.Issuer().DID()) + require.Equal(t, bob.DID(), a.Audience().DID()) + }, + func(x Unauthorized) { + t.Fatalf("unexpected unauthorized failure: %s", x) + }, + ) + }) +} + func TestIsSelfIssued(t *testing.T) { alice, err := signer.Generate() if err != nil { From f40f9327ac9648643b50c7ee61f516616ccf9feb Mon Sep 17 00:00:00 2001 From: Steve Moyer Date: Fri, 23 Aug 2024 14:32:29 -0400 Subject: [PATCH 17/37] test(selector): add tests for "Supported Forms" --- core/policy/selector/supported.json | 163 +++++++++++++++++++++ core/policy/selector/supported_test.go | 187 +++++++++++++++++++++++++ go.mod | 10 +- go.sum | 12 ++ 4 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 core/policy/selector/supported.json create mode 100644 core/policy/selector/supported_test.go diff --git a/core/policy/selector/supported.json b/core/policy/selector/supported.json new file mode 100644 index 0000000..e8c9781 --- /dev/null +++ b/core/policy/selector/supported.json @@ -0,0 +1,163 @@ +{ + "pass": [ + { + "name": "Identity", + "selector": ".", + "input": "{\"x\":1}", + "output": "{\"x\":1}" + }, + { + "name": "Iterator", + "selector": ".[]", + "input": "[1, 2]", + "output": "[1, 2]" + }, + { + "name": "Optional Null Iterator", + "selector": ".[]?", + "input": "null", + "output": "()" + }, + { + "name": "Optional Iterator", + "selector": ".[][]?", + "input": "[[1], 2, [3]]", + "output": "[1, 3]" + }, + { + "name": "Object Key", + "selector": ".x", + "input": "{\"x\": 1 }", + "output": "1" + }, + { + "name": "Quoted Key", + "selector": ".[\"x\"]", + "input": "{\"x\": 1}", + "output": "1" + }, + { + "name": "Index", + "selector": ".[0]", + "input": "[1, 2]", + "output": "1" + }, + { + "name": "Negative Index", + "selector": ".[-1]", + "input": "[1, 2]", + "output": "2" + }, + { + "name": "String Index", + "selector": ".[0]", + "input": "\"Hi\"", + "output": "\"H\"" + }, + { + "name": "Bytes Index", + "selector": ".[0]", + "input": "{\"/\":{\"bytes\":\"AAE\"}", + "output": "0" + }, + { + "name": "Array Slice", + "selector": ".[0:2]", + "input": "[0, 1, 2]", + "output": "[0, 1]" + }, + { + "name": "Array Slice", + "selector": ".[1:]", + "input": "[0, 1, 2]", + "output": "[1, 2]" + }, + { + "name": "Array Slice", + "selector": ".[:2]", + "input": "[0, 1, 2]", + "output": "[0, 1]" + }, + { + "name": "String Slice", + "selector": ".[0:2]", + "input": "\"hello\"", + "output": "\"he\"" + }, + { + "name": "Bytes Index", + "selector": ".[1:]", + "input": "{\"/\":{\"bytes\":\"AAEC\"}}", + "output": "{\"/\":{\"bytes\":\"AQI\"}}" + } + ], + "null": [ + { + "name": "Optional Missing Key", + "selector": ".x?", + "input": "{}" + }, + { + "name": "Optional Null Key", + "selector": ".x?", + "input": "null" + }, + { + "name": "Optional Array Key", + "selector": ".x?", + "input": "[]" + }, + { + "name": "Optional Quoted Key", + "selector": ".[\"x\"]?", + "input": "{}" + }, + { + "name": ".length?", + "selector": ".length?", + "input": "[1, 2]" + }, + { + "name": "Optional Index", + "selector": ".[4]?", + "input": "[0, 1]" + } + ], + "fail": [ + { + "name": "Null Iterator", + "selector": ".[]", + "input": "null" + }, + { + "name": "Nested Iterator", + "selector": ".[][]", + "input": "[[1], 2, [3]]" + }, + { + "name": "Missing Key", + "selector": ".x", + "input": "{}" + }, + { + "name": "Null Key", + "selector": ".x", + "input": "null" + }, + { + "name": "Array Key", + "selector": ".x", + "input": "[]" + }, + { + "name": ".length", + "selector": ".length", + "input": "[1, 2]" + }, + { + "name": "Out of bound Index", + "selector": ".[4]", + "input": "[0, 1]" + } + ] +} \ No newline at end of file diff --git a/core/policy/selector/supported_test.go b/core/policy/selector/supported_test.go new file mode 100644 index 0000000..8a29471 --- /dev/null +++ b/core/policy/selector/supported_test.go @@ -0,0 +1,187 @@ +package selector_test + +import ( + "bytes" + _ "embed" + "encoding/json" + "fmt" + "strings" + "testing" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagjson" + "github.com/ipld/go-ipld-prime/datamodel" + basicnode "github.com/ipld/go-ipld-prime/node/basic" + "github.com/storacha-network/go-ucanto/core/policy/selector" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/wI2L/jsondiff" +) + +//go:embed supported.json +var supported []byte + +type Testcase struct { + Name string `json:"name"` + Selector string `json:"selector"` + Input string `json:"input"` +} + +func (tc Testcase) Select(t *testing.T) (datamodel.Node, []datamodel.Node, error) { + t.Helper() + + sel, err := selector.Parse(tc.Selector) + require.NoError(t, err) + + return selector.Select(sel, node(t, tc.Input)) +} + +type SuccessTestcase struct { + Testcase + Output *string `json:"output"` +} + +func (tc SuccessTestcase) SelectAndCompare(t *testing.T) { + t.Helper() + + exp := node(t, *tc.Output) + + node, nodes, err := tc.Select(t) + require.NoError(t, err) + require.NotEqual(t, node != nil, len(nodes) > 0) // XOR (only one of node or nodes should be set) + + if node == nil { + nb := basicnode.Prototype.List.NewBuilder() + la, err := nb.BeginList(int64(len(nodes))) + require.NoError(t, err) + + for _, n := range nodes { + // TODO: This code is probably not needed if the Select operation properly prunes nil values - e.g.: Optional Iterator + if n == nil { + n = datamodel.Null + } + + require.NoError(t, la.AssembleValue().AssignNode(n)) + } + + require.NoError(t, la.Finish()) + + node = nb.Build() + } + + equalIPLD(t, exp, node) +} + +type Testcases struct { + SuccessTestcases []SuccessTestcase `json:"pass"` + NullTestcases []Testcase `json:"null"` + ErrorTestcases []Testcase `json:"fail"` +} + +// TestSupported Forms runs tests against the Selector according to the +// proposed "Supported Forms" presented in this GitHub issue: +// https://github.com/ucan-wg/delegation/issues/5#issue-2154766496 +func TestSupportedForms(t *testing.T) { + t.Parallel() + + var testcases Testcases + + require.NoError(t, json.Unmarshal(supported, &testcases)) + + t.Run("node(s)", func(t *testing.T) { + t.Parallel() + + for _, testcase := range testcases.SuccessTestcases { + testcase := testcase + + t.Run(testcase.Name, func(t *testing.T) { + t.Parallel() + + // TODO: This test case panics during Select, though Parse works - reports + // "index out of range [-1]" so a bit of subtraction and some bounds checking + // should fix this testcase. + if testcase.Name == "Negative Index" { + t.Skip() + } + + testcase.SelectAndCompare(t) + }) + } + }) + + t.Run("null", func(t *testing.T) { + t.Parallel() + + for _, testcase := range testcases.NullTestcases { + testcase := testcase + + t.Run(testcase.Name, func(t *testing.T) { + t.Parallel() + + node, nodes, err := testcase.Select(t) + require.NoError(t, err) + // TODO: should Select return a single node which is sometimes a list or null? + // require.Equal(t, datamodel.Null, node) + assert.Nil(t, node) + assert.Empty(t, nodes) + }) + } + }) + + t.Run("error", func(t *testing.T) { + t.Parallel() + + for _, testcase := range testcases.ErrorTestcases { + testcase := testcase + + t.Run(testcase.Name, func(t *testing.T) { + t.Parallel() + + node, nodes, err := testcase.Select(t) + require.Error(t, err) + assert.Nil(t, node) + assert.Empty(t, nodes) + }) + } + }) +} + +func equalIPLD(t *testing.T, expected datamodel.Node, actual datamodel.Node, msgAndArgs ...interface{}) bool { + t.Helper() + + if !assert.ObjectsAreEqual(expected, actual) { + exp, act := &bytes.Buffer{}, &bytes.Buffer{} + if err := dagjson.Encode(expected, exp); err != nil { + return assert.Fail(t, "Failed to encode json for expected IPLD node") + } + + if err := dagjson.Encode(actual, act); err != nil { + return assert.Fail(t, "Failed to encode JSON for actual IPLD node") + } + + diff, err := jsondiff.CompareJSON(act.Bytes(), exp.Bytes()) + if err != nil { + return assert.Fail(t, "Failed to create diff of expected and actual IPLD nodes") + } + + return assert.Fail(t, fmt.Sprintf("Not equal: \n"+ + "expected: %s\n"+ + "actual: %s\n"+ + "diff: %s", exp, act, diff), msgAndArgs) + } + + return true +} + +func node(t *testing.T, json string) ipld.Node { + t.Helper() + + np := basicnode.Prototype.Any + nb := np.NewBuilder() + require.NoError(t, dagjson.Decode(nb, strings.NewReader(json))) + + node := nb.Build() + require.NotNil(t, node) + + return node +} diff --git a/go.mod b/go.mod index 41a5c73..0cb5165 100644 --- a/go.mod +++ b/go.mod @@ -14,11 +14,18 @@ require ( github.com/stretchr/testify v1.8.4 ) +require ( + github.com/tidwall/gjson v1.17.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect +) + require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/gobwas/glob v0.2.3 // indirect + github.com/gobwas/glob v0.2.3 github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect @@ -49,6 +56,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/polydawn/refmt v0.89.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/wI2L/jsondiff v0.6.0 github.com/whyrusleeping/cbor-gen v0.0.0-20230818171029-f91ae536ca25 // indirect go.opentelemetry.io/otel v1.13.0 // indirect go.opentelemetry.io/otel/trace v1.13.0 // indirect diff --git a/go.sum b/go.sum index 8b85739..bdd4f8b 100644 --- a/go.sum +++ b/go.sum @@ -230,7 +230,19 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/wI2L/jsondiff v0.6.0 h1:zrsH3FbfVa3JO9llxrcDy/XLkYPLgoMX6Mz3T2PP2AI= +github.com/wI2L/jsondiff v0.6.0/go.mod h1:D6aQ5gKgPF9g17j+E9N7aasmU1O+XvfmWm1y8UMmNpw= github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s= github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= From 0e2a3b291e0c8b5607de3cb8dcf0eebde93f3efd Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 2 Sep 2024 14:20:43 +0200 Subject: [PATCH 18/37] fix: initial test --- validator/capability.go | 4 ++-- validator/error.go | 2 +- validator/lib_test.go | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/validator/capability.go b/validator/capability.go index 6379f33..262221c 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -61,9 +61,9 @@ func (m match[Caveats]) Proofs() []delegation.Delegation { func (m match[Caveats]) Prune(context CanIssuer[Caveats]) Match[Caveats] { if context.CanIssue(m.value, m.sources[0].Delegation().Issuer().DID()) { - return m + return nil } - return nil + return m } func (m match[Caveats]) Source() []Source { diff --git a/validator/error.go b/validator/error.go index 9267d4d..7c69cc0 100644 --- a/validator/error.go +++ b/validator/error.go @@ -584,7 +584,7 @@ func (ue UnauthorizedError[Caveats]) isUnauthorized() {} func indent(message string) string { indent := " " - return indent + strings.Join(strings.Split(message, "\n"), "\n$"+indent) + return indent + strings.Join(strings.Split(message, "\n"), "\n"+indent) } func li(message string) string { diff --git a/validator/lib_test.go b/validator/lib_test.go index c7b585b..6c6c958 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -30,8 +30,10 @@ func (c storeAddCaveats) Build() (ipld.Node, error) { ma, _ := nb.BeginMap(2) ma.AssembleKey().AssignString("link") ma.AssembleValue().AssignLink(c.Link) - ma.AssembleKey().AssignString("origin") - ma.AssembleValue().AssignLink(c.Origin) + if c.Origin != nil { + ma.AssembleKey().AssignString("origin") + ma.AssembleValue().AssignLink(c.Origin) + } ma.Finish() return nb.Build(), nil } @@ -110,7 +112,7 @@ func TestAccess(t *testing.T) { res, func(a Authorization[storeAddCaveats]) { require.Equal(t, storeAdd.Can(), a.Capability().Can()) - require.Equal(t, alice.DID(), a.Capability().With()) + require.Equal(t, alice.DID().String(), a.Capability().With()) require.Equal(t, alice.DID(), a.Issuer().DID()) require.Equal(t, bob.DID(), a.Audience().DID()) }, From 815d7c39946d2193a10acc0ed72b5ed3678a16f2 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 3 Sep 2024 17:04:44 +0200 Subject: [PATCH 19/37] test: more tests and fixes --- validator/error.go | 6 +- validator/lib_test.go | 199 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 166 insertions(+), 39 deletions(-) diff --git a/validator/error.go b/validator/error.go index 7c69cc0..e41d0b8 100644 --- a/validator/error.go +++ b/validator/error.go @@ -143,7 +143,7 @@ func (ise InvalidSignatureError) Delegation() delegation.Delegation { func (ise InvalidSignatureError) Error() string { issuer := ise.Issuer().DID() key := ise.verifier.DID() - if !strings.HasPrefix(issuer.String(), "did:key") { + if strings.HasPrefix(issuer.String(), "did:key") { return fmt.Sprintf(`Proof %s does not has a valid signature from %s`, ise.delegation.Link(), key) } return strings.Join([]string{ @@ -330,7 +330,7 @@ func NewExpiredError(delegation delegation.Delegation) InvalidProof { func (ee ExpiredError) Error() string { return fmt.Sprintf("Proof %s has expired on %s", ee.delegation.Link(), - time.UnixMilli(int64(ee.delegation.Expiration())).Format(time.RFC3339)) + time.Unix(int64(ee.delegation.Expiration()), 0).Format(time.RFC3339)) } func (ee ExpiredError) Build() (datamodel.Node, error) { @@ -384,7 +384,7 @@ func NewNotValidBeforeError(delegation delegation.Delegation) InvalidProof { func (nvbe NotValidBeforeError) Error() string { return fmt.Sprintf("Proof %s is not valid before %s", nvbe.delegation.Link(), - time.UnixMilli(int64(nvbe.delegation.NotBefore())).Format(time.RFC3339)) + time.Unix(int64(nvbe.delegation.NotBefore()), 0).Format(time.RFC3339)) } func (nvbe NotValidBeforeError) Build() (datamodel.Node, error) { diff --git a/validator/lib_test.go b/validator/lib_test.go index 6c6c958..6b01627 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -3,11 +3,13 @@ package validator import ( "fmt" "testing" + "time" "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/invocation" "github.com/storacha-network/go-ucanto/core/result" "github.com/storacha-network/go-ucanto/core/result/failure" @@ -84,42 +86,167 @@ func TestAccess(t *testing.T) { return verifier.Parse(str) } - t.Run("authorize self-issued invocation", func(t *testing.T) { - inv, err := invocation.Invoke( - alice, - bob, - storeAdd.New( - alice.DID().String(), - storeAddCaveats{Link: testLink}, - ), - ) - require.NoError(t, err) - - context := NewValidationContext( - service.Verifier(), - storeAdd, - IsSelfIssued, - validateAuthOk, - ProofUnavailable, - parseEdPrincipal, - FailDIDKeyResolution, - ) - - res, err := Access(inv, context) - require.NoError(t, err) - - result.MatchResultR0( - res, - func(a Authorization[storeAddCaveats]) { - require.Equal(t, storeAdd.Can(), a.Capability().Can()) - require.Equal(t, alice.DID().String(), a.Capability().With()) - require.Equal(t, alice.DID(), a.Issuer().DID()) - require.Equal(t, bob.DID(), a.Audience().DID()) - }, - func(x Unauthorized) { - t.Fatalf("unexpected unauthorized failure: %s", x) - }, - ) + t.Run("authorized", func(t *testing.T) { + t.Run("self-issued invocation", func(t *testing.T) { + inv, err := invocation.Invoke( + alice, + bob, + storeAdd.New( + alice.DID().String(), + storeAddCaveats{Link: testLink}, + ), + ) + require.NoError(t, err) + + context := NewValidationContext( + service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + res, err := Access(inv, context) + require.NoError(t, err) + + result.MatchResultR0( + res, + func(a Authorization[storeAddCaveats]) { + require.Equal(t, storeAdd.Can(), a.Capability().Can()) + require.Equal(t, alice.DID().String(), a.Capability().With()) + require.Equal(t, alice.DID(), a.Issuer().DID()) + require.Equal(t, bob.DID(), a.Audience().DID()) + }, + func(x Unauthorized) { + t.Fatalf("unexpected unauthorized failure: %s", x) + }, + ) + }) + }) + + t.Run("unauthorized", func(t *testing.T) { + t.Run("expired invocation", func(t *testing.T) { + exp := ucan.Now() - 5 + inv, err := invocation.Invoke( + alice, + service, + storeAdd.New( + alice.DID().String(), + storeAddCaveats{Link: testLink}, + ), + delegation.WithExpiration(exp), + ) + require.NoError(t, err) + + context := NewValidationContext( + service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + res, err := Access(inv, context) + require.NoError(t, err) + + result.MatchResultR0( + res, + func(a Authorization[storeAddCaveats]) { + t.Fatalf("unexpected authorization: %+v", a) + }, + func(x Unauthorized) { + require.Equal(t, x.Name(), "Unauthorized") + + isoexp := time.Unix(int64(exp), 0).Format(time.RFC3339) + msg := fmt.Sprintf("Claim %s is not authorized\n - Proof %s has expired on %s", storeAdd, inv.Link(), isoexp) + require.Equal(t, msg, x.Error()) + }, + ) + }) + + t.Run("not valid before", func(t *testing.T) { + nbf := ucan.Now() + 500 + inv, err := invocation.Invoke( + alice, + service, + storeAdd.New( + alice.DID().String(), + storeAddCaveats{Link: testLink}, + ), + delegation.WithNotBefore(nbf), + ) + require.NoError(t, err) + + context := NewValidationContext( + service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + res, err := Access(inv, context) + require.NoError(t, err) + + result.MatchResultR0( + res, + func(a Authorization[storeAddCaveats]) { + t.Fatalf("unexpected authorization: %+v", a) + }, + func(x Unauthorized) { + require.Equal(t, x.Name(), "Unauthorized") + + isoexp := time.Unix(int64(nbf), 0).Format(time.RFC3339) + msg := fmt.Sprintf("Claim %s is not authorized\n - Proof %s is not valid before %s", storeAdd, inv.Link(), isoexp) + require.Equal(t, msg, x.Error()) + }, + ) + }) + + t.Run("invalid signature", func(t *testing.T) { + inv, err := invocation.Invoke( + alice, + service, + storeAdd.New( + alice.DID().String(), + storeAddCaveats{Link: testLink}, + ), + ) + require.NoError(t, err) + + inv.Data().Model().S = bob.Sign(inv.Root().Bytes()).Bytes() + + context := NewValidationContext( + service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + res, err := Access(inv, context) + require.NoError(t, err) + + result.MatchResultR0( + res, + func(a Authorization[storeAddCaveats]) { + t.Fatalf("unexpected authorization: %+v", a) + }, + func(x Unauthorized) { + require.Equal(t, x.Name(), "Unauthorized") + msg := fmt.Sprintf("Claim %s is not authorized\n - Proof %s does not has a valid signature from %s", storeAdd, inv.Link(), alice.DID()) + require.Equal(t, msg, x.Error()) + }, + ) + }) }) } From fb370bd0079cd82f1f9cad65c15246164bf9119f Mon Sep 17 00:00:00 2001 From: Hannah Howard Date: Tue, 3 Sep 2024 08:07:29 -0700 Subject: [PATCH 20/37] feat: CAR offsets (#16) # Goals To feature complete blob index module in indexer-service, we need offset and length for each block when reading from a car, so that we can generate sharded dag index. # Implementation - Adds a small bit of CAR offfset math under the hood so that we can get block offsets and lengths - Adds a tests that verifies these can be used to read exact blocks out of a CAR file - Adds a small utility function to convert one Iterator to another (i.e. make an interator where each element is wrapped) --- core/car/car.go | 31 ++++++++++++++++++++++++++++++- core/car/car_test.go | 24 ++++++++++++++++++++++-- core/iterable/iterable.go | 11 +++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/core/car/car.go b/core/car/car.go index aec721e..7aadf7e 100644 --- a/core/car/car.go +++ b/core/car/car.go @@ -10,6 +10,7 @@ import ( ipldcar "github.com/ipld/go-car" "github.com/ipld/go-car/util" cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/multiformats/go-varint" "github.com/storacha-network/go-ucanto/core/ipld" "github.com/storacha-network/go-ucanto/core/ipld/block" "github.com/storacha-network/go-ucanto/core/iterable" @@ -57,6 +58,26 @@ func Encode(roots []ipld.Link, blocks iterable.Iterator[ipld.Block]) io.Reader { return reader } +type CarBlock interface { + ipld.Block + Offset() uint64 + Length() uint64 +} + +type carBlock struct { + ipld.Block + offset uint64 + length uint64 +} + +func (cb carBlock) Offset() uint64 { + return cb.offset +} + +func (cb carBlock) Length() uint64 { + return cb.length +} + func Decode(reader io.Reader) ([]ipld.Link, iterable.Iterator[ipld.Block], error) { br := bufio.NewReader(reader) @@ -69,6 +90,11 @@ func Decode(reader io.Reader) ([]ipld.Link, iterable.Iterator[ipld.Block], error return nil, nil, fmt.Errorf("invalid car version: %d", h.Version) } + offset, err := ipldcar.HeaderSize(h) + if err != nil { + return nil, nil, err + } + var roots []ipld.Link for _, r := range h.Roots { roots = append(roots, cidlink.Link{Cid: r}) @@ -92,6 +118,9 @@ func Decode(reader io.Reader) ([]ipld.Link, iterable.Iterator[ipld.Block], error return nil, fmt.Errorf("mismatch in content integrity, name: %s, data: %s", cid, hashed) } - return block.NewBlock(cidlink.Link{Cid: cid}, bytes), nil + ss := uint64(cid.ByteLen()) + uint64(len(bytes)) + offset += uint64(varint.UvarintSize(ss)) + ss + + return carBlock{block.NewBlock(cidlink.Link{Cid: cid}, bytes), offset - uint64(len(bytes)), uint64(len(bytes))}, nil }), nil } diff --git a/core/car/car_test.go b/core/car/car_test.go index d885198..f37cf70 100644 --- a/core/car/car_test.go +++ b/core/car/car_test.go @@ -47,7 +47,7 @@ func TestDecodeCAR(t *testing.T) { t.Fatalf("unexpected root: %s, expected: %s", roots[0], fixtures[0].root) } - var blks []ipld.Block + var blks []CarBlock for { b, err := blocks.Next() if err != nil { @@ -56,7 +56,11 @@ func TestDecodeCAR(t *testing.T) { } t.Fatalf("reading blocks: %s", err) } - blks = append(blks, b) + cb, ok := b.(CarBlock) + if !ok { + t.Fatalf("did not return a CarBlock") + } + blks = append(blks, cb) } if len(blks) != len(fixtures[0].blocks) { @@ -66,6 +70,22 @@ func TestDecodeCAR(t *testing.T) { if b.String() != blks[i].Link().String() { t.Fatalf("unexpected block: %s, expected: %s", b, blks[i].Link()) } + // verify offset and length can be used to directly read the block in the CAR file + file.Seek(int64(blks[i].Offset()), io.SeekStart) + data := make([]byte, blks[i].Length()) + _, err := file.Read(data) + if err != nil { + t.Fatalf("error reading block from raw file") + } + hashed, err := blks[i].Link().(cidlink.Link).Cid.Prefix().Sum(data) + if err != nil { + t.Fatalf("error hashing block from raw file") + } + + if hashed.String() != blks[i].Link().String() { + t.Fatalf("raw read from offset block: %s, expected: %s", hashed, blks[i].Link()) + } + } } diff --git a/core/iterable/iterable.go b/core/iterable/iterable.go index 669f2e9..b590ac1 100644 --- a/core/iterable/iterable.go +++ b/core/iterable/iterable.go @@ -50,6 +50,17 @@ func Collect[T any](it Iterator[T]) ([]T, error) { return items, nil } +func Map[T, U any](it Iterator[T], mapFn func(T) U) Iterator[U] { + return NewIterator(func() (U, error) { + t, err := it.Next() + if err != nil { + var undef U + return undef, err + } + return mapFn(t), nil + }) +} + func Concat[T any](iterators ...Iterator[T]) Iterator[T] { if len(iterators) == 0 { return From([]T{}) From 7b3a12cdf865151a1c09c775cd9c73433f9461b4 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 3 Sep 2024 18:19:38 +0100 Subject: [PATCH 21/37] test: more tests --- ucan/capability.go | 4 +-- validator/capability.go | 22 ++++++++------ validator/lib_test.go | 65 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 73 insertions(+), 18 deletions(-) diff --git a/ucan/capability.go b/ucan/capability.go index ab3ca17..2d1e736 100644 --- a/ucan/capability.go +++ b/ucan/capability.go @@ -5,8 +5,8 @@ import ( ) type jsonModel struct { - With Resource `json:"with"` Can Ability `json:"can"` + With Resource `json:"with"` Nb interface{} `json:"nb,omitempty"` } @@ -32,8 +32,8 @@ func (c *capability[T]) With() Resource { func (c *capability[T]) MarshalJSON() ([]byte, error) { return json.Marshal(jsonModel{ - With: c.with, Can: c.can, + With: c.with, Nb: c.nb, }) } diff --git a/validator/capability.go b/validator/capability.go index 262221c..36a1c0f 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -91,13 +91,15 @@ func (m match[Caveats]) Select(sources []Source) (matches []Match[Caveats], erro return nil }, func(x InvalidCapability) error { - if uerr, ok := err.(UnknownCapability); ok { + if uerr, ok := x.(UnknownCapability); ok { unknowns = append(unknowns, uerr.Capability()) + return nil } - if merr, ok := err.(MalformedCapability); ok { + if merr, ok := x.(MalformedCapability); ok { errors = append(errors, NewDelegationError([]DelegationSubError{merr}, m)) + return nil } - return fmt.Errorf("unexpected error type in resolved capability result") + return fmt.Errorf("unexpected error type in resolved capability result: %w", err) }, ) if err != nil { @@ -244,14 +246,16 @@ func Select[Caveats any](matcher Matcher[Caveats], capabilities []Source) (match matches = append(matches, match) return nil }, - func(err InvalidCapability) error { - if uerr, ok := err.(UnknownCapability); ok { - unknowns = append(unknowns, uerr.Capability()) + func(x InvalidCapability) error { + if ux, ok := x.(UnknownCapability); ok { + unknowns = append(unknowns, ux.Capability()) + return nil } - if serr, ok := err.(DelegationSubError); ok { - errors = append(errors, NewDelegationError([]DelegationSubError{serr}, capability.Capability())) + if sx, ok := x.(DelegationSubError); ok { + errors = append(errors, NewDelegationError([]DelegationSubError{sx}, capability.Capability())) + return nil } - return fmt.Errorf("unexpected error type in match result") + return fmt.Errorf("unexpected error type in match result: %w", x) }, ) if err != nil { diff --git a/validator/lib_test.go b/validator/lib_test.go index 6b01627..4722b1c 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -2,6 +2,7 @@ package validator import ( "fmt" + "strings" "testing" "time" @@ -160,9 +161,10 @@ func TestAccess(t *testing.T) { }, func(x Unauthorized) { require.Equal(t, x.Name(), "Unauthorized") - - isoexp := time.Unix(int64(exp), 0).Format(time.RFC3339) - msg := fmt.Sprintf("Claim %s is not authorized\n - Proof %s has expired on %s", storeAdd, inv.Link(), isoexp) + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(" - Proof %s has expired on %s", inv.Link(), time.Unix(int64(exp), 0).Format(time.RFC3339)), + }, "\n") require.Equal(t, msg, x.Error()) }, ) @@ -201,9 +203,10 @@ func TestAccess(t *testing.T) { }, func(x Unauthorized) { require.Equal(t, x.Name(), "Unauthorized") - - isoexp := time.Unix(int64(nbf), 0).Format(time.RFC3339) - msg := fmt.Sprintf("Claim %s is not authorized\n - Proof %s is not valid before %s", storeAdd, inv.Link(), isoexp) + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(" - Proof %s is not valid before %s", inv.Link(), time.Unix(int64(nbf), 0).Format(time.RFC3339)), + }, "\n") require.Equal(t, msg, x.Error()) }, ) @@ -242,7 +245,55 @@ func TestAccess(t *testing.T) { }, func(x Unauthorized) { require.Equal(t, x.Name(), "Unauthorized") - msg := fmt.Sprintf("Claim %s is not authorized\n - Proof %s does not has a valid signature from %s", storeAdd, inv.Link(), alice.DID()) + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(" - Proof %s does not has a valid signature from %s", inv.Link(), alice.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }, + ) + }) + + t.Run("unknown capability", func(t *testing.T) { + type storeWriteCaveats = storeAddCaveats + + inv, err := invocation.Invoke( + alice, + service, + ucan.NewCapability( + "store/write", + alice.DID().String(), + storeWriteCaveats{Link: testLink}, + ), + ) + require.NoError(t, err) + + context := NewValidationContext( + service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + res, err := Access(inv, context) + require.NoError(t, err) + + result.MatchResultR0( + res, + func(a Authorization[storeAddCaveats]) { + t.Fatalf("unexpected authorization: %+v", a) + }, + func(x Unauthorized) { + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + " - No matching delegated capability found", + " - Encountered unknown capabilities", + fmt.Sprintf(" - {\"can\":\"store/write\",\"with\":\"%s\",\"nb\":{}}", alice.DID()), + }, "\n") require.Equal(t, msg, x.Error()) }, ) From d85346719405077c2eecf9e9f38335dd8a3b6bae Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 4 Sep 2024 18:18:04 +0100 Subject: [PATCH 22/37] wip: switch to non-result internals --- core/schema/did.go | 17 +- core/schema/did_test.go | 30 +-- core/schema/literal.go | 7 +- core/schema/schema.go | 7 +- core/schema/struct.go | 12 +- core/schema/struct_test.go | 24 +- server/handler.go | 24 +- server/server.go | 10 +- server/transaction/transaction.go | 8 +- validator/capability.go | 192 +++++++--------- validator/error.go | 54 ++++- validator/lib.go | 360 ++++++++++++------------------ validator/lib_test.go | 144 ++++-------- 13 files changed, 380 insertions(+), 509 deletions(-) diff --git a/core/schema/did.go b/core/schema/did.go index 6883d67..b6ff250 100644 --- a/core/schema/did.go +++ b/core/schema/did.go @@ -1,18 +1,17 @@ package schema import ( - "github.com/storacha-network/go-ucanto/core/result" "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/did" ) var didreader = reader[string, did.DID]{ - readFunc: func(input string) result.Result[did.DID, failure.Failure] { + readFunc: func(input string) (did.DID, failure.Failure) { d, err := did.Parse(input) if err != nil { - return result.Error[did.DID](NewSchemaError(err.Error())) + return did.Undef, NewSchemaError(err.Error()) } - return result.Ok[did.DID, failure.Failure](d) + return d, nil }, } @@ -26,9 +25,11 @@ func DIDString() Reader[string, string] { } var didstrreader = reader[string, string]{ - readFunc: func(input string) result.Result[string, failure.Failure] { - return result.MapOk(DID().Read(input), func(id did.DID) string { - return id.String() - }) + readFunc: func(input string) (string, failure.Failure) { + d, err := DID().Read(input) + if err != nil { + return "", err + } + return d.String(), nil }, } diff --git a/core/schema/did_test.go b/core/schema/did_test.go index eeecf67..54cdae6 100644 --- a/core/schema/did_test.go +++ b/core/schema/did_test.go @@ -3,33 +3,23 @@ package schema import ( "testing" - "github.com/storacha-network/go-ucanto/core/result" - "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/did" "github.com/stretchr/testify/require" ) func TestReadDID(t *testing.T) { - res := DID().Read("notadid") - result.MatchResultR0(res, func(ok did.DID) { - t.Fatalf("unexpectedly parsed a non-DID as a DID: %s", ok.String()) - }, func(err failure.Failure) { - require.Equal(t, err.Name(), "SchemaError") - }) + res, err := DID().Read("notadid") + require.Error(t, err) + require.Equal(t, res, did.Undef) + require.Equal(t, err.Name(), "SchemaError") - res = DID().Read("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") - result.MatchResultR0(res, func(ok did.DID) { - require.Equal(t, ok.DID().String(), "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") - }, func(err failure.Failure) { - t.Fatalf("unexpected error reading DID: %s", err) - }) + res, err = DID().Read("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") + require.NoError(t, err) + require.Equal(t, res.String(), "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") } func TestReadDIDString(t *testing.T) { - res := DIDString().Read("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") - result.MatchResultR0(res, func(ok string) { - require.Equal(t, ok, "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") - }, func(err failure.Failure) { - t.Fatalf("unexpected error reading DID: %s", err) - }) + res, err := DIDString().Read("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") + require.NoError(t, err) + require.Equal(t, res, "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z") } diff --git a/core/schema/literal.go b/core/schema/literal.go index 3d33f98..a63fdf0 100644 --- a/core/schema/literal.go +++ b/core/schema/literal.go @@ -3,17 +3,16 @@ package schema import ( "fmt" - "github.com/storacha-network/go-ucanto/core/result" "github.com/storacha-network/go-ucanto/core/result/failure" ) func Literal(expected string) Reader[string, string] { return reader[string, string]{ - readFunc: func(input string) result.Result[string, failure.Failure] { + readFunc: func(input string) (string, failure.Failure) { if input != expected { - return result.Error[string](NewSchemaError(fmt.Sprintf("expected literal %s instead got %s", expected, input))) + return "", NewSchemaError(fmt.Sprintf("expected literal %s instead got %s", expected, input)) } - return result.Ok[string, failure.Failure](input) + return input, nil }, } } diff --git a/core/schema/schema.go b/core/schema/schema.go index 5289645..8eb5cbc 100644 --- a/core/schema/schema.go +++ b/core/schema/schema.go @@ -3,19 +3,18 @@ package schema import ( "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/storacha-network/go-ucanto/core/ipld" - "github.com/storacha-network/go-ucanto/core/result" "github.com/storacha-network/go-ucanto/core/result/failure" ) type Reader[I, O any] interface { - Read(input I) result.Result[O, failure.Failure] + Read(input I) (O, failure.Failure) } type reader[I, O any] struct { - readFunc func(input I) result.Result[O, failure.Failure] + readFunc func(input I) (O, failure.Failure) } -func (r reader[I, O]) Read(input I) result.Result[O, failure.Failure] { +func (r reader[I, O]) Read(input I) (O, failure.Failure) { return r.readFunc(input) } diff --git a/core/schema/struct.go b/core/schema/struct.go index 0ad3ca6..eccce1c 100644 --- a/core/schema/struct.go +++ b/core/schema/struct.go @@ -4,7 +4,6 @@ import ( "github.com/ipld/go-ipld-prime/schema" "github.com/storacha-network/go-ucanto/core/ipld" "github.com/storacha-network/go-ucanto/core/policy" - "github.com/storacha-network/go-ucanto/core/result" "github.com/storacha-network/go-ucanto/core/result/failure" ) @@ -13,25 +12,26 @@ type strukt[T any] struct { policy policy.Policy } -func (s strukt[T]) Read(input any) result.Result[T, failure.Failure] { +func (s strukt[T]) Read(input any) (T, failure.Failure) { + var bind T node, ok := input.(ipld.Node) if !ok { - return result.Error[T](NewSchemaError("unexpected input: not an IPLD node")) + return bind, NewSchemaError("unexpected input: not an IPLD node") } if s.policy != nil { ok := policy.Match(s.policy, node) if !ok { - return result.Error[T](NewSchemaError("input did not match policy")) + return bind, NewSchemaError("input did not match policy") } } bind, err := ipld.Rebind[T](node, s.typ) if err != nil { - return result.Error[T](NewSchemaError(err.Error())) + return bind, NewSchemaError(err.Error()) } - return result.Ok[T, failure.Failure](bind) + return bind, nil } func Struct[T any](typ schema.Type, policy policy.Policy) Reader[any, T] { diff --git a/core/schema/struct_test.go b/core/schema/struct_test.go index f6ff21f..614da3f 100644 --- a/core/schema/struct_test.go +++ b/core/schema/struct_test.go @@ -6,8 +6,6 @@ import ( "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/node/basicnode" - "github.com/storacha-network/go-ucanto/core/result" - "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/testing/helpers" "github.com/stretchr/testify/require" ) @@ -32,13 +30,10 @@ func TestReadStruct(t *testing.T) { ma.Finish() nd := nb.Build() - res := Struct[TestStruct](ts.TypeByName("TestStruct"), nil).Read(nd) - result.MatchResultR0(res, func(ok TestStruct) { - fmt.Printf("%+v\n", ok) - require.Equal(t, ok.Name, "foo") - }, func(err failure.Failure) { - t.Fatalf("unexpected error reading struct: %s", err) - }) + res, err := Struct[TestStruct](ts.TypeByName("TestStruct"), nil).Read(nd) + require.NoError(t, err) + fmt.Printf("%+v\n", res) + require.Equal(t, res.Name, "foo") }) t.Run("Failure", func(t *testing.T) { @@ -50,12 +45,9 @@ func TestReadStruct(t *testing.T) { ma.Finish() nd := nb.Build() - res := Struct[TestStruct](ts.TypeByName("TestStruct"), nil).Read(nd) - result.MatchResultR0(res, func(ok TestStruct) { - t.Fatalf("unexpectedly read incompatible struct: %+v", ok) - }, func(err failure.Failure) { - fmt.Printf("%+v\n", err) - require.Equal(t, err.Name(), "SchemaError") - }) + _, err := Struct[TestStruct](ts.TypeByName("TestStruct"), nil).Read(nd) + require.Error(t, err) + fmt.Printf("%+v\n", err) + require.Equal(t, err.Name(), "SchemaError") }) } diff --git a/server/handler.go b/server/handler.go index 3d27667..24a1cad 100644 --- a/server/handler.go +++ b/server/handler.go @@ -1,8 +1,6 @@ package server import ( - "fmt" - "github.com/storacha-network/go-ucanto/core/invocation" "github.com/storacha-network/go-ucanto/core/ipld" "github.com/storacha-network/go-ucanto/core/result" @@ -11,13 +9,13 @@ import ( "github.com/storacha-network/go-ucanto/validator" ) -type HandlerFunc[C any, O, X ipld.Builder] func(capability ucan.Capability[C], invocation invocation.Invocation, context InvocationContext) (transaction.Transaction[O, X], error) +type HandlerFunc[C any, O any] func(capability ucan.Capability[C], invocation invocation.Invocation, context InvocationContext) (out O, fork []ipld.Link, join ipld.Link, err error) // Provide is used to define given capability provider. It decorates the passed // handler and takes care of UCAN validation. It only calls the handler // when validation succeeds. -func Provide[C any, O, X ipld.Builder](capability validator.CapabilityParser[C], handler HandlerFunc[C, O, X]) ServiceMethod[O, X] { - return func(invocation invocation.Invocation, context InvocationContext) (transaction.Transaction[O, X], error) { +func Provide[C any, O any](capability validator.CapabilityParser[C], handler HandlerFunc[C, O]) ServiceMethod[O, any] { + return func(invocation invocation.Invocation, context InvocationContext) (transaction.Transaction[O, any], error) { vctx := validator.NewValidationContext( context.ID().Verifier(), capability, @@ -30,16 +28,14 @@ func Provide[C any, O, X ipld.Builder](capability validator.CapabilityParser[C], authorization, err := validator.Access(invocation, vctx) if err != nil { - return nil, err + return transaction.NewTransaction(result.Error[O](any(err))), err + } + + o, fk, jn, herr := handler(authorization.Capability(), invocation, context) + if herr != nil { + } - return result.MatchResultR2(authorization, func(ok validator.Authorization[C]) (transaction.Transaction[O, X], error) { - return handler(ok.Capability(), invocation, context) - }, func(err validator.Unauthorized) (transaction.Transaction[O, X], error) { - if failure, ok := any(err).(X); ok { - return transaction.NewTransaction(result.Error[O](failure)), nil - } - return nil, fmt.Errorf("error was not an IPLD builder") - }) + return transaction.NewTransaction(result.Ok[O, any](o)) } } diff --git a/server/server.go b/server/server.go index 179a5f7..5d3061e 100644 --- a/server/server.go +++ b/server/server.go @@ -98,8 +98,8 @@ func NewServer(id principal.Signer, options ...Option) (ServerView, error) { validateAuthorization := cfg.validateAuthorization if validateAuthorization == nil { - validateAuthorization = func(auth validator.Authorization[any]) result.Result[result.Unit, validator.Revoked] { - return result.Ok[result.Unit, validator.Revoked](nil) + validateAuthorization = func(auth validator.Authorization[any]) validator.Revoked { + return nil } } @@ -145,11 +145,11 @@ func (ctx context) CanIssue(capability ucan.Capability[any], issuer did.DID) boo return ctx.canIssue(capability, issuer) } -func (ctx context) ValidateAuthorization(auth validator.Authorization[any]) result.Result[result.Unit, validator.Revoked] { +func (ctx context) ValidateAuthorization(auth validator.Authorization[any]) validator.Revoked { return ctx.validateAuthorization(auth) } -func (ctx context) ResolveProof(proof ucan.Link) result.Result[delegation.Delegation, validator.UnavailableProof] { +func (ctx context) ResolveProof(proof ucan.Link) (delegation.Delegation, validator.UnavailableProof) { return ctx.resolveProof(proof) } @@ -157,7 +157,7 @@ func (ctx context) ParsePrincipal(str string) (principal.Verifier, error) { return ctx.parsePrincipal(str) } -func (ctx context) ResolveDIDKey(did did.DID) result.Result[did.DID, validator.UnresolvedDID] { +func (ctx context) ResolveDIDKey(did did.DID) (did.DID, validator.UnresolvedDID) { return ctx.resolveDIDKey(did) } diff --git a/server/transaction/transaction.go b/server/transaction/transaction.go index 9eda9f8..4d11b22 100644 --- a/server/transaction/transaction.go +++ b/server/transaction/transaction.go @@ -8,7 +8,7 @@ import ( // Transaction defines a result & effect pair, used by provider that wishes to // return results that have effects. -type Transaction[O, X any] interface { +type Transaction[O any, X any] interface { Out() result.Result[O, X] Fx() receipt.Effects } @@ -18,16 +18,14 @@ type transaction[O, X any] struct { fx receipt.Effects } -func (t *transaction[O, X]) Out() result.Result[O, X] { +func (t transaction[O, X]) Out() result.Result[O, X] { return t.out } -func (t *transaction[O, X]) Fx() receipt.Effects { +func (t transaction[O, X]) Fx() receipt.Effects { return t.fx } -var _ Transaction[any, any] = (*transaction[any, any])(nil) - type effects struct { fork []ipld.Link join ipld.Link diff --git a/validator/capability.go b/validator/capability.go index 36a1c0f..5909906 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -5,7 +5,6 @@ import ( "strings" "github.com/storacha-network/go-ucanto/core/delegation" - "github.com/storacha-network/go-ucanto/core/result" "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/ucan" @@ -34,11 +33,11 @@ func NewSource(capability ucan.Capability[any], delegation delegation.Delegation } type Matcher[Caveats any] interface { - Match(source Source) result.Result[Match[Caveats], InvalidCapability] + Match(source Source) (Match[Caveats], InvalidCapability) } type Selector[Caveats any] interface { - Select(sources []Source) ([]Match[Caveats], []DelegationError, []ucan.Capability[any], error) + Select(sources []Source) ([]Match[Caveats], []DelegationError, []ucan.Capability[any]) } type Match[Caveats any] interface { @@ -74,37 +73,27 @@ func (m match[Caveats]) Value() ucan.Capability[Caveats] { return m.value } -func (m match[Caveats]) Select(sources []Source) (matches []Match[Caveats], errors []DelegationError, unknowns []ucan.Capability[any], err error) { +func (m match[Caveats]) Select(sources []Source) (matches []Match[Caveats], errors []DelegationError, unknowns []ucan.Capability[any]) { for _, source := range sources { - err = result.MatchResultR1( - ResolveCapability(m.descriptor, m.value, source), - func(cap ucan.Capability[Caveats]) error { - result.MatchResultR0( - m.descriptor.Derives(m.value, cap), - func(_ result.Unit) { - matches = append(matches, NewMatch(source, cap, m.descriptor)) - }, - func(x failure.Failure) { - errors = append(errors, NewDelegationError([]DelegationSubError{NewEscalatedCapabilityError(m.value, cap, x)}, m)) - }, - ) - return nil - }, - func(x InvalidCapability) error { - if uerr, ok := x.(UnknownCapability); ok { - unknowns = append(unknowns, uerr.Capability()) - return nil - } - if merr, ok := x.(MalformedCapability); ok { - errors = append(errors, NewDelegationError([]DelegationSubError{merr}, m)) - return nil - } - return fmt.Errorf("unexpected error type in resolved capability result: %w", err) - }, - ) + cap, err := ResolveCapability(m.descriptor, m.value, source) if err != nil { - return + if uerr, ok := err.(UnknownCapability); ok { + unknowns = append(unknowns, uerr.Capability()) + } else if merr, ok := err.(MalformedCapability); ok { + errors = append(errors, NewDelegationError([]DelegationSubError{merr}, m)) + } else { + panic(fmt.Errorf("unexpected error type in resolved capability result: %w", err)) + } + continue } + + derr := m.descriptor.Derives(m.value, cap) + if derr != nil { + errors = append(errors, NewDelegationError([]DelegationSubError{NewEscalatedCapabilityError(m.value, cap, derr)}, m)) + continue + } + + matches = append(matches, NewMatch(source, cap, m.descriptor)) } return } @@ -129,11 +118,16 @@ type CapabilityParser[Caveats any] interface { } type Derivable[Caveats any] interface { - // Derives determines if a capability is derivable from another. - Derives(claimed, delegated ucan.Capability[Caveats]) result.Result[result.Unit, failure.Failure] + // Derives determines if a capability is derivable from another. Return `nil` + // to indicate the delegated capability can be derived from the claimed + // capability. + Derives(claimed, delegated ucan.Capability[Caveats]) failure.Failure } -type DerivesFunc[Caveats any] func(claimed, delegated ucan.Capability[Caveats]) result.Result[result.Unit, failure.Failure] +// DerivesFunc determines if a capability is derivable from another. Return +// `nil` to indicate the delegated capability can be derived from the claimed +// capability. +type DerivesFunc[Caveats any] func(claimed, delegated ucan.Capability[Caveats]) failure.Failure type Descriptor[Caveats any] interface { Derivable[Caveats] @@ -161,7 +155,7 @@ func (d descriptor[C]) Nb() schema.Reader[any, C] { return d.nb } -func (d descriptor[C]) Derives(parent, child ucan.Capability[C]) result.Result[result.Unit, failure.Failure] { +func (d descriptor[C]) Derives(parent, child ucan.Capability[C]) failure.Failure { return d.derives(parent, child) } @@ -173,17 +167,16 @@ func (c capability[Caveats]) Can() ucan.Ability { return c.descriptor.Can() } -func (c capability[Caveats]) Select(capabilities []Source) ([]Match[Caveats], []DelegationError, []ucan.Capability[any], error) { +func (c capability[Caveats]) Select(capabilities []Source) ([]Match[Caveats], []DelegationError, []ucan.Capability[any]) { return Select(c, capabilities) } -func (c capability[Caveats]) Match(source Source) result.Result[Match[Caveats], InvalidCapability] { - return result.MapOk( - ParseCapability(c.descriptor, source), - func(cap ucan.Capability[Caveats]) Match[Caveats] { - return NewMatch(source, cap, c.descriptor) - }, - ) +func (c capability[Caveats]) Match(source Source) (Match[Caveats], InvalidCapability) { + cap, err := ParseCapability(c.descriptor, source) + if err != nil { + return nil, err + } + return NewMatch(source, cap, c.descriptor), nil } func (c capability[Caveats]) String() string { @@ -211,70 +204,54 @@ func NewCapability[Caveats any]( return &capability[Caveats]{descriptor: d} } -func ParseCapability[O any](descriptor Descriptor[O], source Source) result.Result[ucan.Capability[O], InvalidCapability] { +func ParseCapability[O any](descriptor Descriptor[O], source Source) (ucan.Capability[O], InvalidCapability) { cap := source.Capability() if descriptor.Can() != cap.Can() { - return result.Error[ucan.Capability[O], InvalidCapability](NewUnknownCapabilityError(cap)) + return nil, NewUnknownCapabilityError(cap) + } + + uri, err := descriptor.With().Read(cap.With()) + if err != nil { + return nil, NewMalformedCapabilityError(cap, err) + } + + nb, err := descriptor.Nb().Read(cap.Nb()) + if err != nil { + return nil, NewMalformedCapabilityError(cap, err) } - return result.MatchResultR1( - descriptor.With().Read(cap.With()), - func(with ucan.Resource) result.Result[ucan.Capability[O], InvalidCapability] { - return result.MapResultR0( - descriptor.Nb().Read(cap.Nb()), - func(nb O) ucan.Capability[O] { - pcap := ucan.NewCapability(cap.Can(), with, nb) - return pcap - }, - func(x failure.Failure) InvalidCapability { - return NewMalformedCapabilityError(cap, x) - }, - ) - }, - func(x failure.Failure) result.Result[ucan.Capability[O], InvalidCapability] { - return result.Error[ucan.Capability[O], InvalidCapability](NewMalformedCapabilityError(cap, x)) - }, - ) -} - -func Select[Caveats any](matcher Matcher[Caveats], capabilities []Source) (matches []Match[Caveats], errors []DelegationError, unknowns []ucan.Capability[any], err error) { + return ucan.NewCapability(cap.Can(), uri, nb), nil +} + +func Select[Caveats any](matcher Matcher[Caveats], capabilities []Source) (matches []Match[Caveats], errors []DelegationError, unknowns []ucan.Capability[any]) { for _, capability := range capabilities { - err = result.MatchResultR1( - matcher.Match(capability), - func(match Match[Caveats]) error { - matches = append(matches, match) - return nil - }, - func(x InvalidCapability) error { - if ux, ok := x.(UnknownCapability); ok { - unknowns = append(unknowns, ux.Capability()) - return nil - } - if sx, ok := x.(DelegationSubError); ok { - errors = append(errors, NewDelegationError([]DelegationSubError{sx}, capability.Capability())) - return nil - } - return fmt.Errorf("unexpected error type in match result: %w", x) - }, - ) + match, err := matcher.Match(capability) if err != nil { - return + if ux, ok := err.(UnknownCapability); ok { + unknowns = append(unknowns, ux.Capability()) + } else if sx, ok := err.(DelegationSubError); ok { + errors = append(errors, NewDelegationError([]DelegationSubError{sx}, capability.Capability())) + } else { + panic(fmt.Errorf("unexpected error type in match result: %w", err)) + } + continue } + matches = append(matches, match) } return } // ResolveCapability resolves delegated capability `source` from the `claimed` // capability using provided capability `parser`. It is similar to -// `parseCapability` except `source` here is treated as capability pattern which +// [ParseCapability] except `source` here is treated as capability pattern which // is matched against the `claimed` capability. This means we resolve `can` and // `with` fields from the `claimed` capability and... // TODO: inherit all missing `nb` fields from the claimed capability. -func ResolveCapability[Caveats any](descriptor Descriptor[Caveats], claimed ucan.Capability[Caveats], source Source) result.Result[ucan.Capability[Caveats], InvalidCapability] { +func ResolveCapability[Caveats any](descriptor Descriptor[Caveats], claimed ucan.Capability[Caveats], source Source) (ucan.Capability[Caveats], InvalidCapability) { can := ResolveAbility(source.Capability().Can(), claimed.Can()) if can == "" { - return result.Error[ucan.Capability[Caveats], InvalidCapability](NewUnknownCapabilityError(source.Capability())) + return nil, NewUnknownCapabilityError(source.Capability()) } resource := ResolveResource(source.Capability().With(), claimed.With()) @@ -282,24 +259,18 @@ func ResolveCapability[Caveats any](descriptor Descriptor[Caveats], claimed ucan resource = source.Capability().With() } - return result.MatchResultR1( - descriptor.With().Read(resource), - func(uri string) result.Result[ucan.Capability[Caveats], InvalidCapability] { - return result.MapResultR0( - // TODO: inherit missing fields - descriptor.Nb().Read(claimed), - func(nb Caveats) ucan.Capability[Caveats] { - return ucan.NewCapability(can, resource, nb) - }, - func(x failure.Failure) InvalidCapability { - return NewMalformedCapabilityError(source.Capability(), x) - }, - ) - }, - func(x failure.Failure) result.Result[ucan.Capability[Caveats], InvalidCapability] { - return result.Error[ucan.Capability[Caveats], InvalidCapability](NewMalformedCapabilityError(source.Capability(), x)) - }, - ) + uri, err := descriptor.With().Read(resource) + if err != nil { + return nil, NewMalformedCapabilityError(source.Capability(), err) + } + + // TODO: inherit missing fields + nb, err := descriptor.Nb().Read(claimed) + if err != nil { + return nil, NewMalformedCapabilityError(source.Capability(), err) + } + + return ucan.NewCapability(can, uri, nb), nil } // ResolveAbility resolves ability `pattern` of the delegated capability from @@ -337,19 +308,18 @@ func ResolveResource(source string, uri ucan.Resource) ucan.Resource { return "" } -func DefaultDerives[Caveats any](claimed, delegated ucan.Capability[Caveats]) result.Result[result.Unit, failure.Failure] { +func DefaultDerives[Caveats any](claimed, delegated ucan.Capability[Caveats]) failure.Failure { dres := delegated.With() cres := claimed.With() if strings.HasSuffix(dres, "*") { if !strings.HasPrefix(cres, dres[0:len(dres)-1]) { - return result.Error[result.Unit](schema.NewSchemaError(fmt.Sprintf("Resource %s does not match delegated %s", cres, dres))) + return schema.NewSchemaError(fmt.Sprintf("Resource %s does not match delegated %s", cres, dres)) } } else if dres != cres { - return result.Error[result.Unit](schema.NewSchemaError(fmt.Sprintf("Resource %s is not contained by %s", cres, dres))) + return schema.NewSchemaError(fmt.Sprintf("Resource %s is not contained by %s", cres, dres)) } // TODO: is it possible to ensure claimed caveats match delegated caveats? - - return result.Ok[result.Unit, failure.Failure](struct{}{}) + return nil } diff --git a/validator/error.go b/validator/error.go index e41d0b8..424de4b 100644 --- a/validator/error.go +++ b/validator/error.go @@ -110,11 +110,62 @@ func (see SessionEscalationError) Error() string { func (see SessionEscalationError) isInvalidProof() {} -type InvalidSignature interface { +// BadSignature is a signature that could not be verified or has been verified +// invalid. i.e. it is an [UnverifiableSignature] or an [InvalidSignature]. +type BadSignature interface { InvalidProof Issuer() ucan.Principal Audience() ucan.Principal Delegation() delegation.Delegation + isBadSignature() +} + +// UnverifiableSignature is a signature that cannot be verified. i.e. some error +// occurred when attempting to verify the signature. +type UnverifiableSignature interface { + BadSignature + Unwrap() error + isUnverifiableSignature() +} + +type UnverifiableSignatureError struct { + failure.NamedWithStackTrace + delegation delegation.Delegation + cause error +} + +func NewUnverifiableSignatureError(delegation delegation.Delegation, cause error) UnverifiableSignature { + return UnverifiableSignatureError{failure.NamedWithCurrentStackTrace("UnverifiableSignature"), delegation, cause} +} + +func (use UnverifiableSignatureError) Issuer() ucan.Principal { + return use.delegation.Issuer() +} + +func (use UnverifiableSignatureError) Audience() ucan.Principal { + return use.delegation.Audience() +} + +func (use UnverifiableSignatureError) Delegation() delegation.Delegation { + return use.delegation +} + +func (use UnverifiableSignatureError) Error() string { + issuer := use.Issuer().DID() + return fmt.Sprintf("Proof %s issued by %s cannot be verified:\n%s", use.delegation.Link(), issuer, li(use.cause.Error())) +} + +func (use UnverifiableSignatureError) Unwrap() error { + return use.cause +} + +func (use UnverifiableSignatureError) isUnverifiableSignature() {} +func (use UnverifiableSignatureError) isBadSignature() {} +func (use UnverifiableSignatureError) isInvalidProof() {} + +// InvalidSignature is a signature that is verified to be invalid. +type InvalidSignature interface { + BadSignature isInvalidSignature() } @@ -153,6 +204,7 @@ func (ise InvalidSignatureError) Error() string { } func (ise InvalidSignatureError) isInvalidSignature() {} +func (ise InvalidSignatureError) isBadSignature() {} func (ise InvalidSignatureError) isInvalidProof() {} type UnavailableProof interface { diff --git a/validator/lib.go b/validator/lib.go index 182b1ac..96f1654 100644 --- a/validator/lib.go +++ b/validator/lib.go @@ -11,7 +11,6 @@ import ( "github.com/storacha-network/go-ucanto/core/policy" "github.com/storacha-network/go-ucanto/core/policy/literal" "github.com/storacha-network/go-ucanto/core/policy/selector" - "github.com/storacha-network/go-ucanto/core/result" "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/did" @@ -24,12 +23,12 @@ func IsSelfIssued[Caveats any](capability ucan.Capability[Caveats], issuer did.D return capability.With() == issuer.DID().String() } -func ProofUnavailable(p ucan.Link) result.Result[delegation.Delegation, UnavailableProof] { - return result.Error[delegation.Delegation](NewUnavailableProofError(p, fmt.Errorf("no proof resolver configured"))) +func ProofUnavailable(p ucan.Link) (delegation.Delegation, UnavailableProof) { + return nil, NewUnavailableProofError(p, fmt.Errorf("no proof resolver configured")) } -func FailDIDKeyResolution(d did.DID) result.Result[did.DID, UnresolvedDID] { - return result.Error[did.DID](NewDIDKeyResolutionError(d, fmt.Errorf("no DID resolver configured"))) +func FailDIDKeyResolution(d did.DID) (did.DID, UnresolvedDID) { + return did.Undef, NewDIDKeyResolutionError(d, fmt.Errorf("no DID resolver configured")) } // PrincipalParser provides verifier instances that can validate UCANs issued @@ -44,23 +43,23 @@ type PrincipalParserFunc func(str string) (principal.Verifier, error) // identified by DID different from did:key method. It can be passed into a // UCAN validator in order to augmented it with additional DID methods support. type PrincipalResolver interface { - ResolveDIDKey(did did.DID) result.Result[did.DID, UnresolvedDID] + ResolveDIDKey(did did.DID) (did.DID, UnresolvedDID) } // PrincipalResolverFunc resolves the key of a principal that is identified by // DID different from did:key method. -type PrincipalResolverFunc func(did did.DID) result.Result[did.DID, UnresolvedDID] +type PrincipalResolverFunc func(did did.DID) (did.DID, UnresolvedDID) // ProofResolver finds a delegations when external proof links are present in // UCANs. If a resolver is not provided the validator may not be able to explore // corresponding path within a proof chain. type ProofResolver interface { // Resolve finds a delegation corresponding to an external proof link. - ResolveProof(proof ucan.Link) result.Result[delegation.Delegation, UnavailableProof] + ResolveProof(proof ucan.Link) (delegation.Delegation, UnavailableProof) } // Resolve finds a delegation corresponding to an external proof link. -type ProofResolverFunc func(proof ucan.Link) result.Result[delegation.Delegation, UnavailableProof] +type ProofResolverFunc func(proof ucan.Link) (delegation.Delegation, UnavailableProof) type CanIssuer[Caveats any] interface { // CanIssue informs validator whether given capability can be issued by a @@ -83,15 +82,15 @@ func (ci canissuer[Caveats]) CanIssue(c ucan.Capability[Caveats], d did.DID) boo type RevocationChecker[Caveats any] interface { // ValidateAuthorization validates that the passed authorization has not been - // revoked. - ValidateAuthorization(auth Authorization[Caveats]) result.Result[result.Unit, Revoked] + // revoked. It returns `nil` if not revoked. + ValidateAuthorization(auth Authorization[Caveats]) Revoked } -// RevocationCheckerFunc validates the passed authorization and returns -// a result indicating validity. -type RevocationCheckerFunc[Caveats any] func(auth Authorization[Caveats]) result.Result[result.Unit, Revoked] +// RevocationCheckerFunc validates that the passed authorization has not been +// revoked. It returns `nil` if not revoked. +type RevocationCheckerFunc[Caveats any] func(auth Authorization[Caveats]) Revoked -// Validator must provide a `Verifier` corresponding to local authority. +// Validator must provide a [principal.Verifier] corresponding to local authority. // // A capability provider service will use one corresponding to own DID or it's // supervisor's DID if it acts under it's authority. @@ -138,11 +137,11 @@ func (vc validationContext[Caveats]) CanIssue(capability ucan.Capability[any], i return vc.canIssue(capability, issuer) } -func (vc validationContext[Caveats]) ValidateAuthorization(auth Authorization[any]) result.Result[result.Unit, Revoked] { +func (vc validationContext[Caveats]) ValidateAuthorization(auth Authorization[any]) Revoked { return vc.validateAuthorization(auth) } -func (vc validationContext[Caveats]) ResolveProof(proof ucan.Link) result.Result[delegation.Delegation, UnavailableProof] { +func (vc validationContext[Caveats]) ResolveProof(proof ucan.Link) (delegation.Delegation, UnavailableProof) { return vc.resolveProof(proof) } @@ -150,7 +149,7 @@ func (vc validationContext[Caveats]) ParsePrincipal(str string) (principal.Verif return vc.parsePrincipal(str) } -func (vc validationContext[Caveats]) ResolveDIDKey(did did.DID) result.Result[did.DID, UnresolvedDID] { +func (vc validationContext[Caveats]) ResolveDIDKey(did did.DID) (did.DID, UnresolvedDID) { return vc.resolveDIDKey(did) } @@ -174,21 +173,21 @@ func NewValidationContext[Caveats any]( } } -// Access finds a valid path in a proof chain of the given `invocation` by -// exploring every possible option. On success an `Authorization` object is -// returned that illustrates the valid path. If no valid path is found -// `Unauthorized` error is returned detailing all explored paths and where they -// proved to fail. -func Access[Caveats any](invocation invocation.Invocation, context ValidationContext[Caveats]) (result.Result[Authorization[Caveats], Unauthorized], error) { +// Access finds a valid path in a proof chain of the given +// [invocation.Invocation] by exploring every possible option. On success an +// [Authorization] object is returned that illustrates the valid path. If no +// valid path is found [Unauthorized] error is returned detailing all explored +// paths and where they proved to fail. +func Access[Caveats any](invocation invocation.Invocation, context ValidationContext[Caveats]) (Authorization[Caveats], Unauthorized) { prf := []delegation.Proof{delegation.FromDelegation(invocation)} return Claim(context.Capability(), prf, context) } -// Claim attempts to find a valid proof chain for the claimed `capability` given -// set of `proofs`. On success an `Authorization` object with detailed proof -// chain is returned and on failure `Unauthorized` error is returned with +// Claim attempts to find a valid proof chain for the claimed [CapabilityParser] +// given set of `proofs`. On success an [Authorization] object with detailed +// proof chain is returned and on failure [Unauthorized] error is returned with // details on paths explored and why they have failed. -func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegation.Proof, context ClaimContext) (result.Result[Authorization[Caveats], Unauthorized], error) { +func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegation.Proof, context ClaimContext) (Authorization[Caveats], Unauthorized) { var sources []Source var invalidprf []InvalidProof @@ -197,98 +196,71 @@ func Claim[Caveats any](capability CapabilityParser[Caveats], proofs []delegatio invalidprf = append(invalidprf, err) } - for _, d := range delegations { - validation, err := Validate(d, delegations, context) + for _, prf := range delegations { + // Validate each proof if valid add each capability to the list of sources + // or collect the error. + validation, err := Validate(prf, delegations, context) if err != nil { - return nil, err + invalidprf = append(invalidprf, err) + continue } - // Validate each proof if valid add each capability to the list of sources. - // otherwise collect the error. - result.MatchResultR0( - validation, - func(d delegation.Delegation) { - for _, c := range d.Capabilities() { - sources = append(sources, NewSource(c, d)) - } - }, - func(x InvalidProof) { - invalidprf = append(invalidprf, x) - }, - ) + for _, c := range validation.Capabilities() { + sources = append(sources, NewSource(c, prf)) + } } // look for the matching capability - matches, dlgerrs, unknowns, err := capability.Select(sources) - if err != nil { - return nil, err - } + matches, dlgerrs, unknowns := capability.Select(sources) var failedprf []InvalidClaim for _, matched := range matches { selector := matched.Prune(canissuer[Caveats]{canIssue: context.CanIssue}) if selector == nil { auth := NewAuthorization(matched, nil) - revoked := result.MatchResultR1( - context.ValidateAuthorization(ConvertUnknownAuthorization(auth)), - func(o result.Unit) Revoked { return nil }, - func(x Revoked) Revoked { return x }, - ) - if revoked == nil { - return result.Ok[Authorization[Caveats], Unauthorized](auth), nil + revoked := context.ValidateAuthorization(ConvertUnknownAuthorization(auth)) + if revoked != nil { + invalidprf = append(invalidprf, revoked) + continue } + return auth, nil + } + + a, err := Authorize(matched, context) + if err != nil { + failedprf = append(failedprf, err) + continue + } + + auth := NewAuthorization(matched, []Authorization[Caveats]{a}) + revoked := context.ValidateAuthorization(ConvertUnknownAuthorization(auth)) + if revoked != nil { invalidprf = append(invalidprf, revoked) - } else { - ar, err := Authorize(matched, context) - if err != nil { - return nil, err - } - auth := result.MatchResultR1( - ar, - func(a Authorization[Caveats]) Authorization[Caveats] { - auth := NewAuthorization(matched, []Authorization[Caveats]{a}) - return result.MatchResultR1( - context.ValidateAuthorization(ConvertUnknownAuthorization(auth)), - func(o result.Unit) Authorization[Caveats] { - return auth - }, - func(x Revoked) Authorization[Caveats] { - invalidprf = append(invalidprf, x) - return nil - }, - ) - }, - func(x InvalidClaim) Authorization[Caveats] { - failedprf = append(failedprf, x) - return nil - }, - ) - if auth != nil { - return result.Ok[Authorization[Caveats], Unauthorized](auth), nil - } + continue } + + return auth, nil } - return result.Error[Authorization[Caveats]]( - NewUnauthorizedError(capability, dlgerrs, unknowns, invalidprf, failedprf), - ), nil + return nil, NewUnauthorizedError(capability, dlgerrs, unknowns, invalidprf, failedprf) } // ResolveProofs takes `proofs` from the delegation which may contain -// a `Delegation` or a link to one and attempts to resolve links by side loading -// them. It returns a set of resolved `Delegation`s and errors for the proofs -// that could not be resolved. +// a [delegation.Delegation] or a link to one and attempts to resolve links by +// side loading them. It returns a set of resolved [delegation.Delegation]s and +// errors for the proofs that could not be resolved. func ResolveProofs(proofs []delegation.Proof, resolver ProofResolver) (dels []delegation.Delegation, errs []UnavailableProof) { for _, p := range proofs { d, ok := p.Delegation() if ok { dels = append(dels, d) } else { - result.MatchResultR0( - resolver.ResolveProof(p.Link()), - func(d delegation.Delegation) { dels = append(dels, d) }, - func(x UnavailableProof) { errs = append(errs, x) }, - ) + d, err := resolver.ResolveProof(p.Link()) + if err != nil { + errs = append(errs, err) + continue + } + dels = append(dels, d) } } return @@ -296,12 +268,12 @@ func ResolveProofs(proofs []delegation.Proof, resolver ProofResolver) (dels []de // Validate a delegation to check it is within the time bound and that it is // authorized by the issuer. -func Validate(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (result.Result[delegation.Delegation, InvalidProof], error) { +func Validate(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (delegation.Delegation, InvalidProof) { if ucan.IsExpired(dlg) { - return result.Error[delegation.Delegation, InvalidProof](NewExpiredError(dlg)), nil + return nil, NewExpiredError(dlg) } if ucan.IsTooEarly(dlg) { - return result.Error[delegation.Delegation, InvalidProof](NewNotValidBeforeError(dlg)), nil + return nil, NewNotValidBeforeError(dlg) } return VerifyAuthorization(dlg, prfs, ctx) } @@ -313,91 +285,67 @@ func Validate(dlg delegation.Delegation, prfs []delegation.Delegation, ctx Claim // valid `ucan/attest` attestation from the authority, if attestation is not // found falls back to resolving did:key for the issuer and verifying its // signature. -func VerifyAuthorization(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (result.Result[delegation.Delegation, InvalidProof], error) { +func VerifyAuthorization(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (delegation.Delegation, InvalidProof) { issuer := dlg.Issuer().DID() // If the issuer is a did:key we just verify a signature if strings.HasPrefix(issuer.String(), "did:key:") { vfr, err := ctx.ParsePrincipal(issuer.String()) if err != nil { - return nil, err + return nil, NewUnverifiableSignatureError(dlg, err) } - sig, err := VerifySignature(dlg, vfr) - if err != nil { - return nil, err + dlg, serr := VerifySignature(dlg, vfr) + if serr != nil { + return nil, serr } - return result.MapError(sig, func(err InvalidSignature) InvalidProof { - return InvalidProof(err) - }), nil + return dlg, nil } // Attempt to resolve embedded authorization session from the authority - sess, err := VerifySession(dlg, prfs, ctx) + _, err := VerifySession(dlg, prfs, ctx) if err != nil { - return nil, err - } + if len(err.FailedProofs()) > 0 { + return nil, NewSessionEscalationError(dlg, err) + } - return result.MatchResultR2( - sess, - // If we have valid session we consider authorization valid - func(a Authorization[vdm.AttestationModel]) (result.Result[delegation.Delegation, InvalidProof], error) { - return result.Ok[delegation.Delegation, InvalidProof](dlg), nil - }, - func(x Unauthorized) (result.Result[delegation.Delegation, InvalidProof], error) { - if len(x.FailedProofs()) > 0 { - return result.Error[delegation.Delegation, InvalidProof](NewSessionEscalationError(dlg, x)), nil - } + // Otherwise we try to resolve did:key from the DID instead + // and use that to verify the signature + did, err := ctx.ResolveDIDKey(issuer) + if err != nil { + return nil, err + } - // Otherwise we try to resolve did:key from the DID instead - // and use that to verify the signature - vfr, err := result.MapResultR1( - ctx.ResolveDIDKey(issuer), - func(did did.DID) (principal.Verifier, error) { - return ctx.ParsePrincipal(did.String()) - }, - func(err UnresolvedDID) (UnresolvedDID, error) { - return err, nil - }, - ) - if err != nil { - return nil, err - } + vfr, perr := ctx.ParsePrincipal(did.String()) + if perr != nil { + return nil, NewUnverifiableSignatureError(dlg, perr) + } - return result.MatchResultR2( - vfr, - func(v principal.Verifier) (result.Result[delegation.Delegation, InvalidProof], error) { - sig, err := VerifySignature(dlg, v) - if err != nil { - return nil, err - } - return result.MapError(sig, func(err InvalidSignature) InvalidProof { - return InvalidProof(err) - }), nil - }, - func(x UnresolvedDID) (result.Result[delegation.Delegation, InvalidProof], error) { - return result.Error[delegation.Delegation, InvalidProof](x), nil - }, - ) - }, - ) + _, verr := VerifySignature(dlg, vfr) + if verr != nil { + return nil, verr + } + } + + return dlg, nil } -func VerifySignature(dlg delegation.Delegation, vfr principal.Verifier) (result.Result[delegation.Delegation, InvalidSignature], error) { +// VerifySignature verifies the delegation was signed by the passed verifier. +func VerifySignature(dlg delegation.Delegation, vfr principal.Verifier) (delegation.Delegation, BadSignature) { ok, err := ucan.VerifySignature(dlg.Data(), vfr) if err != nil { - return nil, err + return nil, NewUnverifiableSignatureError(dlg, err) } if !ok { - return result.Error[delegation.Delegation](NewInvalidSignatureError(dlg, vfr)), nil + return nil, NewInvalidSignatureError(dlg, vfr) } - return result.Ok[delegation.Delegation, InvalidSignature](dlg), nil + return dlg, nil } // VerifySession attempts to find an authorization session - an `ucan/attest` -// capability delegation where `with` matches `config.authority` and `nb.proof` +// capability delegation where `with` matches `ctx.Authority()` and `nb.proof` // matches given delegation. // // https://github.com/storacha-network/specs/blob/main/w3-session.md#authorization-session -func VerifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (result.Result[Authorization[vdm.AttestationModel], Unauthorized], error) { +func VerifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx ClaimContext) (Authorization[vdm.AttestationModel], Unauthorized) { // Create a schema that will match an authorization for this exact delegation attestation := NewCapability( "ucan/attest", @@ -408,16 +356,15 @@ func VerifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx policy.Equal(selector.MustParse(".proof"), literal.Link(dlg.Link())), }, ), - func(claimed, delegated ucan.Capability[vdm.AttestationModel]) result.Result[result.Unit, failure.Failure] { - return result.AndThen( - DefaultDerives(claimed, delegated), - func(o result.Unit) result.Result[result.Unit, failure.Failure] { - if claimed.Nb().Proof != delegated.Nb().Proof { - return result.Error[result.Unit, failure.Failure](schema.NewSchemaError(fmt.Sprintf(`proof: %s violates %s`, claimed.Nb().Proof, delegated.Nb().Proof))) - } - return result.Ok[result.Unit, failure.Failure](o) - }, - ) + func(claimed, delegated ucan.Capability[vdm.AttestationModel]) failure.Failure { + err := DefaultDerives(claimed, delegated) + if err != nil { + return err + } + if claimed.Nb().Proof != delegated.Nb().Proof { + return schema.NewSchemaError(fmt.Sprintf(`proof: %s violates %s`, claimed.Nb().Proof, delegated.Nb().Proof)) + } + return nil }, ) @@ -434,50 +381,31 @@ func VerifySession(dlg delegation.Delegation, prfs []delegation.Delegation, ctx } // Authorize verifies whether any of the delegated proofs grant capability. -func Authorize[Caveats any](match Match[Caveats], context ClaimContext) (result.Result[Authorization[Caveats], InvalidClaim], error) { +func Authorize[Caveats any](match Match[Caveats], context ClaimContext) (Authorization[Caveats], InvalidClaim) { // load proofs from all delegations - sources, invalidprf, err := ResolveMatch(match, context) - if err != nil { - return nil, err - } + sources, invalidprf := ResolveMatch(match, context) - matches, dlgerrs, unknowns, err := match.Select(sources) - if err != nil { - return nil, err - } + matches, dlgerrs, unknowns := match.Select(sources) var failedprf []InvalidClaim for _, matched := range matches { selector := matched.Prune(canissuer[Caveats]{canIssue: context.CanIssue}) if selector == nil { - return result.Ok[Authorization[Caveats], InvalidClaim](NewAuthorization(matched, nil)), nil - } else { - ar, err := Authorize(matched, context) - if err != nil { - return nil, err - } - auth := result.MatchResultR1( - ar, - func(a Authorization[Caveats]) Authorization[Caveats] { - return NewAuthorization(matched, []Authorization[Caveats]{a}) - }, - func(x InvalidClaim) Authorization[Caveats] { - failedprf = append(failedprf, x) - return nil - }, - ) - if auth != nil { - return result.Ok[Authorization[Caveats], InvalidClaim](auth), nil - } + return NewAuthorization(matched, nil), nil + } + + auth, err := Authorize(matched, context) + if err != nil { + failedprf = append(failedprf, err) + continue } + return auth, nil } - return result.Error[Authorization[Caveats], InvalidClaim]( - NewInvalidClaimError(match, dlgerrs, unknowns, invalidprf, failedprf), - ), nil + return nil, NewInvalidClaimError(match, dlgerrs, unknowns, invalidprf, failedprf) } -func ResolveMatch[Caveats any](match Match[Caveats], context ClaimContext) (sources []Source, errors []ProofError, err error) { +func ResolveMatch[Caveats any](match Match[Caveats], context ClaimContext) (sources []Source, errors []ProofError) { includes := map[string]struct{}{} var wg sync.WaitGroup var lock sync.RWMutex @@ -487,14 +415,10 @@ func ResolveMatch[Caveats any](match Match[Caveats], context ClaimContext) (sour includes[id] = struct{}{} wg.Add(1) go func(s Source) { - srcs, errs, rerr := ResolveSources(s, context) + srcs, errs := ResolveSources(s, context) lock.Lock() defer lock.Unlock() defer wg.Done() - if rerr != nil { - err = rerr - return - } sources = append(sources, srcs...) errors = append(errors, errs...) }(source) @@ -504,13 +428,14 @@ func ResolveMatch[Caveats any](match Match[Caveats], context ClaimContext) (sour return } -func ResolveSources(source Source, context ClaimContext) (sources []Source, errors []ProofError, err error) { +func ResolveSources(source Source, context ClaimContext) (sources []Source, errors []ProofError) { dlg := source.Delegation() var prfs []delegation.Delegation br, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(dlg.Blocks())) if err != nil { - return nil, nil, err + errors = append(errors, NewProofError(dlg.Link(), err)) + return } dlgs, failedprf := ResolveProofs( @@ -535,25 +460,20 @@ func ResolveSources(source Source, context ClaimContext) (sources []Source, erro } // In the second pass we attempt to proofs that were resolved and are aligned. for _, prf := range prfs { - validation, err := Validate(prf, prfs, context) - if err != nil { - return nil, nil, err - } + _, err := Validate(prf, prfs, context) + // If proof is not valid (expired, not active yet or has incorrect // signature) save a corresponding proof error. - // otherwise create source objects for it's capabilities, so we could + if err != nil { + errors = append(errors, NewProofError(prf.Link(), err)) + continue + } + + // Otherwise create source objects for it's capabilities, so we could // track which proof in which capability the are from. - result.MatchResultR0( - validation, - func(d delegation.Delegation) { - for _, cap := range prf.Capabilities() { - sources = append(sources, NewSource(cap, prf)) - } - }, - func(x InvalidProof) { - errors = append(errors, NewProofError(prf.Link(), x)) - }, - ) + for _, cap := range prf.Capabilities() { + sources = append(sources, NewSource(cap, prf)) + } } return } diff --git a/validator/lib_test.go b/validator/lib_test.go index 4722b1c..eba0fe7 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -12,7 +12,6 @@ import ( "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/invocation" - "github.com/storacha-network/go-ucanto/core/result" "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/principal" @@ -56,12 +55,11 @@ func newStoreAddCapability(t *testing.T) CapabilityParser[storeAddCaveats] { "store/add", schema.DIDString(), schema.Struct[storeAddCaveats](typ.TypeByName("StoreAddCaveats"), nil), - func(claimed, delegated ucan.Capability[storeAddCaveats]) result.Result[result.Unit, failure.Failure] { + func(claimed, delegated ucan.Capability[storeAddCaveats]) failure.Failure { if claimed.With() != delegated.With() { err := fmt.Errorf("Expected 'with: \"%s\"' instead got '%s'", delegated.With(), claimed.With()) - return result.Error[result.Unit](failure.FromError(err)) + return failure.FromError(err) } - if delegated.Nb().Link != nil && delegated.Nb().Link != claimed.Nb().Link { var err error if claimed.Nb().Link == nil { @@ -69,10 +67,9 @@ func newStoreAddCapability(t *testing.T) CapabilityParser[storeAddCaveats] { } else { err = fmt.Errorf("Link %s violates imposed %s constraint", claimed.Nb().Link, delegated.Nb().Link) } - return result.Error[result.Unit](failure.FromError(err)) + return failure.FromError(err) } - - return result.Ok[result.Unit, failure.Failure](struct{}{}) + return nil }, ) } @@ -80,9 +77,7 @@ func newStoreAddCapability(t *testing.T) CapabilityParser[storeAddCaveats] { func TestAccess(t *testing.T) { storeAdd := newStoreAddCapability(t) testLink := cidlink.Link{Cid: cid.MustParse("bafkqaaa")} - validateAuthOk := func(auth Authorization[any]) result.Result[result.Unit, Revoked] { - return result.Ok[result.Unit, Revoked](nil) - } + validateAuthOk := func(auth Authorization[any]) Revoked { return nil } parseEdPrincipal := func(str string) (principal.Verifier, error) { return verifier.Parse(str) } @@ -109,21 +104,12 @@ func TestAccess(t *testing.T) { FailDIDKeyResolution, ) - res, err := Access(inv, context) - require.NoError(t, err) - - result.MatchResultR0( - res, - func(a Authorization[storeAddCaveats]) { - require.Equal(t, storeAdd.Can(), a.Capability().Can()) - require.Equal(t, alice.DID().String(), a.Capability().With()) - require.Equal(t, alice.DID(), a.Issuer().DID()) - require.Equal(t, bob.DID(), a.Audience().DID()) - }, - func(x Unauthorized) { - t.Fatalf("unexpected unauthorized failure: %s", x) - }, - ) + a, x := Access(inv, context) + require.NoError(t, x) + require.Equal(t, storeAdd.Can(), a.Capability().Can()) + require.Equal(t, alice.DID().String(), a.Capability().With()) + require.Equal(t, alice.DID(), a.Issuer().DID()) + require.Equal(t, bob.DID(), a.Audience().DID()) }) }) @@ -151,23 +137,15 @@ func TestAccess(t *testing.T) { FailDIDKeyResolution, ) - res, err := Access(inv, context) - require.NoError(t, err) - - result.MatchResultR0( - res, - func(a Authorization[storeAddCaveats]) { - t.Fatalf("unexpected authorization: %+v", a) - }, - func(x Unauthorized) { - require.Equal(t, x.Name(), "Unauthorized") - msg := strings.Join([]string{ - fmt.Sprintf("Claim %s is not authorized", storeAdd), - fmt.Sprintf(" - Proof %s has expired on %s", inv.Link(), time.Unix(int64(exp), 0).Format(time.RFC3339)), - }, "\n") - require.Equal(t, msg, x.Error()) - }, - ) + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(" - Proof %s has expired on %s", inv.Link(), time.Unix(int64(exp), 0).Format(time.RFC3339)), + }, "\n") + require.Equal(t, msg, x.Error()) }) t.Run("not valid before", func(t *testing.T) { @@ -193,23 +171,15 @@ func TestAccess(t *testing.T) { FailDIDKeyResolution, ) - res, err := Access(inv, context) - require.NoError(t, err) - - result.MatchResultR0( - res, - func(a Authorization[storeAddCaveats]) { - t.Fatalf("unexpected authorization: %+v", a) - }, - func(x Unauthorized) { - require.Equal(t, x.Name(), "Unauthorized") - msg := strings.Join([]string{ - fmt.Sprintf("Claim %s is not authorized", storeAdd), - fmt.Sprintf(" - Proof %s is not valid before %s", inv.Link(), time.Unix(int64(nbf), 0).Format(time.RFC3339)), - }, "\n") - require.Equal(t, msg, x.Error()) - }, - ) + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(" - Proof %s is not valid before %s", inv.Link(), time.Unix(int64(nbf), 0).Format(time.RFC3339)), + }, "\n") + require.Equal(t, msg, x.Error()) }) t.Run("invalid signature", func(t *testing.T) { @@ -235,23 +205,15 @@ func TestAccess(t *testing.T) { FailDIDKeyResolution, ) - res, err := Access(inv, context) - require.NoError(t, err) - - result.MatchResultR0( - res, - func(a Authorization[storeAddCaveats]) { - t.Fatalf("unexpected authorization: %+v", a) - }, - func(x Unauthorized) { - require.Equal(t, x.Name(), "Unauthorized") - msg := strings.Join([]string{ - fmt.Sprintf("Claim %s is not authorized", storeAdd), - fmt.Sprintf(" - Proof %s does not has a valid signature from %s", inv.Link(), alice.DID()), - }, "\n") - require.Equal(t, msg, x.Error()) - }, - ) + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(" - Proof %s does not has a valid signature from %s", inv.Link(), alice.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) }) t.Run("unknown capability", func(t *testing.T) { @@ -278,25 +240,17 @@ func TestAccess(t *testing.T) { FailDIDKeyResolution, ) - res, err := Access(inv, context) - require.NoError(t, err) - - result.MatchResultR0( - res, - func(a Authorization[storeAddCaveats]) { - t.Fatalf("unexpected authorization: %+v", a) - }, - func(x Unauthorized) { - require.Equal(t, x.Name(), "Unauthorized") - msg := strings.Join([]string{ - fmt.Sprintf("Claim %s is not authorized", storeAdd), - " - No matching delegated capability found", - " - Encountered unknown capabilities", - fmt.Sprintf(" - {\"can\":\"store/write\",\"with\":\"%s\",\"nb\":{}}", alice.DID()), - }, "\n") - require.Equal(t, msg, x.Error()) - }, - ) + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + " - No matching delegated capability found", + " - Encountered unknown capabilities", + fmt.Sprintf(" - {\"can\":\"store/write\",\"with\":\"%s\",\"nb\":{}}", alice.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) }) }) } From 5d4eeb4616a095a7276dd3f2b864fa75a3f3d250 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Wed, 4 Sep 2024 22:37:32 +0100 Subject: [PATCH 23/37] fix: wip errors --- server/handler.go | 19 ++++++------ server/options.go | 16 +++------- server/server.go | 10 +++---- server/server_test.go | 20 ++++++++----- server/transaction/transaction.go | 35 +++++++--------------- validator/datamodel/errors.go | 23 +++++++++++++++ validator/datamodel/errors.ipldsch | 47 +++++++++++++++++++++--------- validator/error.go | 19 ++++++++++-- 8 files changed, 113 insertions(+), 76 deletions(-) diff --git a/server/handler.go b/server/handler.go index 24a1cad..efdc4d3 100644 --- a/server/handler.go +++ b/server/handler.go @@ -3,19 +3,20 @@ package server import ( "github.com/storacha-network/go-ucanto/core/invocation" "github.com/storacha-network/go-ucanto/core/ipld" + "github.com/storacha-network/go-ucanto/core/receipt" "github.com/storacha-network/go-ucanto/core/result" "github.com/storacha-network/go-ucanto/server/transaction" "github.com/storacha-network/go-ucanto/ucan" "github.com/storacha-network/go-ucanto/validator" ) -type HandlerFunc[C any, O any] func(capability ucan.Capability[C], invocation invocation.Invocation, context InvocationContext) (out O, fork []ipld.Link, join ipld.Link, err error) +type HandlerFunc[C any, O ipld.Builder] func(capability ucan.Capability[C], invocation invocation.Invocation, context InvocationContext) (out O, fx receipt.Effects, err error) // Provide is used to define given capability provider. It decorates the passed // handler and takes care of UCAN validation. It only calls the handler // when validation succeeds. -func Provide[C any, O any](capability validator.CapabilityParser[C], handler HandlerFunc[C, O]) ServiceMethod[O, any] { - return func(invocation invocation.Invocation, context InvocationContext) (transaction.Transaction[O, any], error) { +func Provide[C any, O ipld.Builder](capability validator.CapabilityParser[C], handler HandlerFunc[C, O]) ServiceMethod[O] { + return func(invocation invocation.Invocation, context InvocationContext) (transaction.Transaction[O, ipld.Builder], error) { vctx := validator.NewValidationContext( context.ID().Verifier(), capability, @@ -26,16 +27,16 @@ func Provide[C any, O any](capability validator.CapabilityParser[C], handler Han context.ResolveDIDKey, ) - authorization, err := validator.Access(invocation, vctx) - if err != nil { - return transaction.NewTransaction(result.Error[O](any(err))), err + authorization, aerr := validator.Access(invocation, vctx) + if aerr != nil { + return transaction.NewTransaction(result.Error[O, ipld.Builder](aerr)), nil } - o, fk, jn, herr := handler(authorization.Capability(), invocation, context) + o, fx, herr := handler(authorization.Capability(), invocation, context) if herr != nil { - + return nil, herr } - return transaction.NewTransaction(result.Ok[O, any](o)) + return transaction.NewTransaction(result.Ok[O, ipld.Builder](o), transaction.WithEffects(fx)), nil } } diff --git a/server/options.go b/server/options.go index 72bc8a9..86ddb30 100644 --- a/server/options.go +++ b/server/options.go @@ -14,7 +14,7 @@ type Option func(cfg *srvConfig) error type srvConfig struct { codec transport.InboundCodec - service map[string]ServiceMethod[ipld.Builder, ipld.Builder] + service Service validateAuthorization validator.RevocationCheckerFunc[any] canIssue validator.CanIssueFunc[any] resolveProof validator.ProofResolverFunc @@ -23,23 +23,15 @@ type srvConfig struct { catch ErrorHandlerFunc } -func WithServiceMethod[O, X ipld.Builder](can string, handleFunc ServiceMethod[O, X]) Option { +func WithServiceMethod[O ipld.Builder](can string, handleFunc ServiceMethod[O]) Option { return func(cfg *srvConfig) error { cfg.service[can] = func(input invocation.Invocation, context InvocationContext) (transaction.Transaction[ipld.Builder, ipld.Builder], error) { tx, err := handleFunc(input, context) if err != nil { return nil, err } - out := result.MapResultR0( - tx.Out(), - func(o O) ipld.Builder { return o }, - func(x X) ipld.Builder { return x }, - ) - var opts []transaction.Option - if tx.Fx() != nil { - opts = append(opts, transaction.WithForks(tx.Fx().Fork()), transaction.WithJoin(tx.Fx().Join())) - } - return transaction.NewTransaction(out, opts...), nil + out := result.MapOk(tx.Out(), func(o O) ipld.Builder { return o }) + return transaction.NewTransaction(out, transaction.WithEffects(tx.Fx())), nil } return nil } diff --git a/server/server.go b/server/server.go index 5d3061e..4a74723 100644 --- a/server/server.go +++ b/server/server.go @@ -38,11 +38,11 @@ type InvocationContext interface { } // ServiceMethod is an invocation handler. -type ServiceMethod[O, X ipld.Builder] func(input invocation.Invocation, context InvocationContext) (transaction.Transaction[O, X], error) +type ServiceMethod[O ipld.Builder] func(input invocation.Invocation, context InvocationContext) (transaction.Transaction[O, ipld.Builder], error) // Service is a mapping of service names to handlers, used to define a // service implementation. -type Service = map[string]ServiceMethod[ipld.Builder, ipld.Builder] +type Service = map[string]ServiceMethod[ipld.Builder] type ServiceInvocation = invocation.IssuedInvocation @@ -277,20 +277,20 @@ func Run(server Server, invocation ServiceInvocation) (receipt.AnyReceipt, error return receipt.Issue(server.ID(), result.NewFailure(err), ran.FromInvocation(invocation)) } - outcome, err := handle(invocation, server.Context()) + tx, err := handle(invocation, server.Context()) if err != nil { herr := NewHandlerExecutionError(err, cap) server.Catch(herr) return receipt.Issue(server.ID(), result.NewFailure(herr), ran.FromInvocation(invocation)) } - fx := outcome.Fx() + fx := tx.Fx() var opts []receipt.Option if fx != nil { opts = append(opts, receipt.WithJoin(fx.Join()), receipt.WithForks(fx.Fork())) } - rcpt, err := receipt.Issue(server.ID(), outcome.Out(), ran.FromInvocation(invocation), opts...) + rcpt, err := receipt.Issue(server.ID(), tx.Out(), ran.FromInvocation(invocation), opts...) if err != nil { herr := NewHandlerExecutionError(err, cap) server.Catch(herr) diff --git a/server/server_test.go b/server/server_test.go index c24cae2..cb44737 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -19,7 +19,6 @@ import ( "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/principal/ed25519/signer" sdm "github.com/storacha-network/go-ucanto/server/datamodel" - "github.com/storacha-network/go-ucanto/server/transaction" "github.com/storacha-network/go-ucanto/testing/helpers" "github.com/storacha-network/go-ucanto/transport/car/request" "github.com/storacha-network/go-ucanto/transport/car/response" @@ -121,10 +120,12 @@ func TestSimpleHandler(t *testing.T) { server := helpers.Must(NewServer( service, - WithServiceMethod(uploadadd.Can(), Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (transaction.Transaction[uploadAddSuccess, ipld.Builder], error) { - r := result.Ok[uploadAddSuccess, ipld.Builder](uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}) - return transaction.NewTransaction(r), nil - })), + WithServiceMethod( + uploadadd.Can(), + Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, receipt.Effects, error) { + return uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}, nil, nil + }), + ), )) conn := helpers.Must(client.NewConnection(service, server)) @@ -175,9 +176,12 @@ func TestHandlerExecutionError(t *testing.T) { server := helpers.Must(NewServer( service, - WithServiceMethod(uploadadd.Can(), Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (transaction.Transaction[uploadAddSuccess, ipld.Builder], error) { - return nil, fmt.Errorf("test error") - })), + WithServiceMethod( + uploadadd.Can(), + Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, receipt.Effects, error) { + return uploadAddSuccess{}, nil, fmt.Errorf("test error") + }), + ), )) conn := helpers.Must(client.NewConnection(service, server)) diff --git a/server/transaction/transaction.go b/server/transaction/transaction.go index 4d11b22..4bb699d 100644 --- a/server/transaction/transaction.go +++ b/server/transaction/transaction.go @@ -31,35 +31,29 @@ type effects struct { join ipld.Link } -func (fx *effects) Fork() []ipld.Link { +func (fx effects) Fork() []ipld.Link { return fx.fork } -func (fx *effects) Join() ipld.Link { +func (fx effects) Join() ipld.Link { return fx.join } -var _ receipt.Effects = (*effects)(nil) +func NewEffects(fork []ipld.Link, join ipld.Link) receipt.Effects { + return effects{fork, join} +} // Option is an option configuring a transaction. type Option func(cfg *txConfig) type txConfig struct { - fork []ipld.Link - join ipld.Link + fx receipt.Effects } -// WithForks configures the forks for the receipt. -func WithForks(fork []ipld.Link) Option { +// WithEffects configures the effects for the receipt. +func WithEffects(fx receipt.Effects) Option { return func(cfg *txConfig) { - cfg.fork = fork - } -} - -// WithJoin configures the join for the receipt. -func WithJoin(join ipld.Link) Option { - return func(cfg *txConfig) { - cfg.join = join + cfg.fx = fx } } @@ -68,14 +62,5 @@ func NewTransaction[O, X any](result result.Result[O, X], options ...Option) Tra for _, opt := range options { opt(&cfg) } - - fx := effects{} - if len(cfg.fork) > 0 { - fx.fork = cfg.fork - } - if cfg.join != nil { - fx.join = cfg.join - } - - return &transaction[O, X]{out: result, fx: &fx} + return transaction[O, X]{out: result, fx: cfg.fx} } diff --git a/validator/datamodel/errors.go b/validator/datamodel/errors.go index 995a8ca..122fc56 100644 --- a/validator/datamodel/errors.go +++ b/validator/datamodel/errors.go @@ -61,3 +61,26 @@ type NotValidBeforeModel struct { func NotValidBeforeType() schema.Type { return errorTypeSystem.TypeByName("NotValidBefore") } + +type FailureModel struct { + Name *string + Message string +} + +type CapabilityModel struct { + Can string + With string +} + +type EscalatedCapabilityModel struct { + Name *string + Message string + Claimed CapabilityModel + Delegated CapabilityModel + Cause FailureModel + Stack *string +} + +func EscalatedCapabilityType() schema.Type { + return errorTypeSystem.TypeByName("EscalatedCapability") +} diff --git a/validator/datamodel/errors.ipldsch b/validator/datamodel/errors.ipldsch index f57c6a7..d2790c3 100644 --- a/validator/datamodel/errors.ipldsch +++ b/validator/datamodel/errors.ipldsch @@ -1,25 +1,44 @@ type Delegation struct { - Audience String + audience String } type InvalidAudience struct { - Name optional String - Audience String - Delegation Delegation - Message String - Stack optional String + name optional String + audience String + delegation Delegation + message String + stack optional String } type Expired struct { - Name optional String - Message String - ExpiredAt Int - Stack optional String + name optional String + message String + expiredAt Int + stack optional String } type NotValidBefore struct { - Name optional String - Message String - ValidAt Int - Stack optional String + name optional String + message String + validAt Int + stack optional String +} + +type Capability struct { + can String + with String +} + +type Failure struct { + name optional String + message String +} + +type EscalatedCapability struct { + name optional String + message String + claimed Capability + delegated Capability + cause Failure + stack optional String } diff --git a/validator/error.go b/validator/error.go index 424de4b..d87445a 100644 --- a/validator/error.go +++ b/validator/error.go @@ -31,11 +31,11 @@ type InvalidProof interface { type EscalatedCapabilityError[Caveats any] struct { failure.NamedWithStackTrace claimed ucan.Capability[Caveats] - delegated interface{} + delegated ucan.Capability[Caveats] cause error } -func NewEscalatedCapabilityError[Caveats any](claimed ucan.Capability[Caveats], delegated interface{}, cause error) EscalatedCapabilityError[Caveats] { +func NewEscalatedCapabilityError[Caveats any](claimed ucan.Capability[Caveats], delegated ucan.Capability[Caveats], cause error) EscalatedCapabilityError[Caveats] { return EscalatedCapabilityError[Caveats]{failure.NamedWithCurrentStackTrace("EscalatedCapability"), claimed, delegated, cause} } @@ -47,9 +47,22 @@ func (ece EscalatedCapabilityError[Caveats]) Error() string { return fmt.Sprintf("Constraint violation: %s", ece.cause.Error()) } -func (ece EscalatedCapabilityError[Caveats]) isDelegationSubError() { +func (ece EscalatedCapabilityError[Caveats]) Build() (datamodel.Node, error) { + name := ece.Name() + stack := ece.Stack() + escalatedCapabilityModel := vdm.EscalatedCapabilityModel{ + Name: &name, + Message: ece.Error(), + Stack: &stack, + Claimed: vdm.CapabilityModel{Can: ece.claimed.Can(), With: ece.claimed.With()}, + Delegated: vdm.CapabilityModel{Can: ece.delegated.Can(), With: ece.delegated.With()}, + Cause: vdm.FailureModel{Message: ece.cause.Error()}, + } + return ipld.WrapWithRecovery(&escalatedCapabilityModel, vdm.EscalatedCapabilityType()) } +func (ece EscalatedCapabilityError[Caveats]) isDelegationSubError() {} + type DelegationError interface { failure.Failure Causes() []DelegationSubError From e3214a171cd1a294f8e8826b6f6254472978dc96 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 5 Sep 2024 13:04:34 +0100 Subject: [PATCH 24/37] test: more testing progress --- core/result/failure/datamodel/failure.go | 14 +- core/result/failure/faillure.go | 11 +- core/result/result.go | 2 +- server/handler.go | 7 +- server/server.go | 2 +- server/server_test.go | 371 ++++++++++-------- .../fixtures/fixtures.go | 11 +- validator/capability.go | 4 +- validator/datamodel/errors.go | 23 -- validator/datamodel/errors.ipldsch | 19 - validator/error.go | 29 +- validator/lib_test.go | 69 ++-- 12 files changed, 265 insertions(+), 297 deletions(-) rename validator/fixtures_test.go => testing/fixtures/fixtures.go (60%) diff --git a/core/result/failure/datamodel/failure.go b/core/result/failure/datamodel/failure.go index 694ae32..7c18e56 100644 --- a/core/result/failure/datamodel/failure.go +++ b/core/result/failure/datamodel/failure.go @@ -13,20 +13,18 @@ import ( //go:embed failure.ipldsch var failureSchema []byte -// Failure is a generic failure -type Failure struct { +// FailureModel is a generic failure +type FailureModel struct { Name *string Message string Stack *string } -func (f *Failure) Build() (ipld.Node, error) { +func (f *FailureModel) Build() (ipld.Node, error) { return ucanipld.WrapWithRecovery(f, typ) } -var ( - typ schema.Type -) +var typ schema.Type func init() { ts, err := ipld.LoadSchemaBytes(failureSchema) @@ -35,3 +33,7 @@ func init() { } typ = ts.TypeByName("Failure") } + +func Schema() []byte { + return failureSchema +} diff --git a/core/result/failure/faillure.go b/core/result/failure/faillure.go index 4d4ca4a..b999973 100644 --- a/core/result/failure/faillure.go +++ b/core/result/failure/faillure.go @@ -30,6 +30,11 @@ type Failure interface { Named } +type IPLDBuilderFailure interface { + IPLDConvertableError + Failure +} + type NamedWithStackTrace interface { Named WithStackTrace @@ -63,7 +68,7 @@ func NamedWithCurrentStackTrace(name string) NamedWithStackTrace { } type failure struct { - model datamodel.Failure + model datamodel.FailureModel build func() (ipld.Node, error) } @@ -90,8 +95,8 @@ func (f failure) Build() (ipld.Node, error) { return f.model.Build() } -func FromError(err error) Failure { - model := datamodel.Failure{Message: err.Error()} +func FromError(err error) IPLDBuilderFailure { + model := datamodel.FailureModel{Message: err.Error()} if named, ok := err.(Named); ok { name := named.Name() model.Name = &name diff --git a/core/result/result.go b/core/result/result.go index c24bb00..829becd 100644 --- a/core/result/result.go +++ b/core/result/result.go @@ -176,7 +176,7 @@ func NewFailure(err error) Result[ipld.Builder, ipld.Builder] { return Error[ipld.Builder, ipld.Builder](ipldConvertableError) } - model := datamodel.Failure{Message: err.Error()} + model := datamodel.FailureModel{Message: err.Error()} if named, ok := err.(failure.Named); ok { name := named.Name() model.Name = &name diff --git a/server/handler.go b/server/handler.go index efdc4d3..821bee9 100644 --- a/server/handler.go +++ b/server/handler.go @@ -5,6 +5,7 @@ import ( "github.com/storacha-network/go-ucanto/core/ipld" "github.com/storacha-network/go-ucanto/core/receipt" "github.com/storacha-network/go-ucanto/core/result" + "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/server/transaction" "github.com/storacha-network/go-ucanto/ucan" "github.com/storacha-network/go-ucanto/validator" @@ -27,12 +28,12 @@ func Provide[C any, O ipld.Builder](capability validator.CapabilityParser[C], ha context.ResolveDIDKey, ) - authorization, aerr := validator.Access(invocation, vctx) + auth, aerr := validator.Access(invocation, vctx) if aerr != nil { - return transaction.NewTransaction(result.Error[O, ipld.Builder](aerr)), nil + return transaction.NewTransaction(result.Error[O, ipld.Builder](failure.FromError(aerr))), nil } - o, fx, herr := handler(authorization.Capability(), invocation, context) + o, fx, herr := handler(auth.Capability(), invocation, context) if herr != nil { return nil, herr } diff --git a/server/server.go b/server/server.go index 4a74723..38b73c4 100644 --- a/server/server.go +++ b/server/server.go @@ -42,7 +42,7 @@ type ServiceMethod[O ipld.Builder] func(input invocation.Invocation, context Inv // Service is a mapping of service names to handlers, used to define a // service implementation. -type Service = map[string]ServiceMethod[ipld.Builder] +type Service = map[ucan.Ability]ServiceMethod[ipld.Builder] type ServiceInvocation = invocation.IssuedInvocation diff --git a/server/server_test.go b/server/server_test.go index cb44737..e8305e1 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -8,17 +8,20 @@ import ( "github.com/ipfs/go-cid" ipldprime "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" ipldschema "github.com/ipld/go-ipld-prime/schema" "github.com/storacha-network/go-ucanto/client" + "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/invocation" "github.com/storacha-network/go-ucanto/core/ipld" "github.com/storacha-network/go-ucanto/core/receipt" "github.com/storacha-network/go-ucanto/core/result" + fdm "github.com/storacha-network/go-ucanto/core/result/failure/datamodel" "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/principal/ed25519/signer" - sdm "github.com/storacha-network/go-ucanto/server/datamodel" + "github.com/storacha-network/go-ucanto/testing/fixtures" "github.com/storacha-network/go-ucanto/testing/helpers" "github.com/storacha-network/go-ucanto/transport/car/request" "github.com/storacha-network/go-ucanto/transport/car/response" @@ -35,9 +38,11 @@ type uploadAddCaveats struct { func (c uploadAddCaveats) Build() (ipld.Node, error) { np := basicnode.Prototype.Any nb := np.NewBuilder() - ma, _ := nb.BeginMap(2) - ma.AssembleKey().AssignString("root") - ma.AssembleValue().AssignLink(c.Root) + ma, _ := nb.BeginMap(1) + if c != (uploadAddCaveats{}) { + ma.AssembleKey().AssignString("root") + ma.AssembleValue().AssignLink(c.Root) + } ma.Finish() return nb.Build(), nil } @@ -68,184 +73,214 @@ func (ok uploadAddSuccess) Build() (ipld.Node, error) { return nb.Build(), nil } -func TestHandlerNotFound(t *testing.T) { - service := helpers.Must(signer.Generate()) - alice := helpers.Must(signer.Generate()) - space := helpers.Must(signer.Generate()) - - server := helpers.Must(NewServer(service)) - conn := helpers.Must(client.NewConnection(service, server)) - - rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} - capability := ucan.NewCapability( - "upload/add", - space.DID().String(), - uploadAddCaveats{Root: rt}, - ) - - invs := []invocation.Invocation{helpers.Must(invocation.Invoke(alice, service, capability))} - resp := helpers.Must(client.Execute(invs, conn)) - rcptlnk, ok := resp.Get(invs[0].Link()) - require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) - - rcptsch := bytes.Join([][]byte{sdm.Schema(), []byte(` - type Result union { - | Any "ok" - | HandlerNotFoundError "error" - } representation keyed - `)}, []byte("\n")) - - reader := helpers.Must(receipt.NewReceiptReader[ipld.Node, sdm.HandlerNotFoundErrorModel](rcptsch)) - rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) - - result.MatchResultR0(rcpt.Out(), func(ipld.Node) { - t.Fatalf("expected error: %s", invs[0].Link()) - }, func(rerr sdm.HandlerNotFoundErrorModel) { - fmt.Printf("%s %+v\n", *rerr.Name, rerr) - require.Equal(t, *rerr.Name, "HandlerNotFoundError") - }) +var rcptsch = []byte(` + type Result union { + | UploadAddSuccess "ok" + | Any "error" + } representation keyed + + type UploadAddSuccess struct { + root Link + status String + } +`) + +// asFailure binds the IPLD node to a FailureModel if possible. This works +// around IPLD requiring data to match the schema exactly +func asFailure(t testing.TB, n ipld.Node) fdm.FailureModel { + t.Helper() + require.Equal(t, n.Kind(), datamodel.Kind_Map) + f := fdm.FailureModel{} + + nn, err := n.LookupByString("name") + if err == nil { + name, err := nn.AsString() + require.NoError(t, err) + f.Name = &name + } + + mn, err := n.LookupByString("message") + require.NoError(t, err) + msg, err := mn.AsString() + require.NoError(t, err) + f.Message = msg + + sn, err := n.LookupByString("stack") + if err == nil { + stack, err := sn.AsString() + require.NoError(t, err) + f.Stack = &stack + } + + return f } -func TestSimpleHandler(t *testing.T) { - service := helpers.Must(signer.Generate()) - alice := helpers.Must(signer.Generate()) - space := helpers.Must(signer.Generate()) - - uploadadd := validator.NewCapability( - "upload/add", - schema.DIDString(), - schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), - nil, - ) - - server := helpers.Must(NewServer( - service, - WithServiceMethod( - uploadadd.Can(), - Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, receipt.Effects, error) { - return uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}, nil, nil - }), - ), - )) - - conn := helpers.Must(client.NewConnection(service, server)) - rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} - cap := uploadadd.New(space.DID().String(), uploadAddCaveats{Root: rt}) - invs := []invocation.Invocation{helpers.Must(invocation.Invoke(alice, service, cap))} - resp := helpers.Must(client.Execute(invs, conn)) - - // get the receipt link for the invocation from the response - rcptlnk, ok := resp.Get(invs[0].Link()) - require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) - - rcptsch := bytes.Join([][]byte{sdm.Schema(), []byte(` - type Result union { - | UploadAddSuccess "ok" - | Any "error" - } representation keyed - - type UploadAddSuccess struct { - root Link - status String - } - `)}, []byte("\n")) - - reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) - rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) +func TestExecute(t *testing.T) { + t.Run("simple", func(t *testing.T) { + space, err := signer.Generate() + require.NoError(t, err) + + uploadadd := validator.NewCapability( + "upload/add", + schema.DIDString(), + schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), + nil, + ) + + server := helpers.Must(NewServer( + fixtures.Service, + WithServiceMethod( + uploadadd.Can(), + Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, receipt.Effects, error) { + return uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}, nil, nil + }), + ), + )) + + conn := helpers.Must(client.NewConnection(fixtures.Service, server)) + rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} + cap := uploadadd.New(space.DID().String(), uploadAddCaveats{Root: rt}) + dgl, err := delegation.Delegate( + fixtures.Service, + fixtures.Alice, + []ucan.Capability[uploadAddCaveats]{ucan.NewCapability(uploadadd.Can(), space.DID().String(), uploadAddCaveats{})}, + ) + require.NoError(t, err) + + prfs := []delegation.Proof{delegation.FromDelegation(dgl)} + inv, err := invocation.Invoke(fixtures.Alice, fixtures.Service, cap, delegation.WithProofs(prfs)) + require.NoError(t, err) + + resp, err := client.Execute([]invocation.Invocation{inv}, conn) + require.NoError(t, err) + + // get the receipt link for the invocation from the response + rcptlnk, ok := resp.Get(inv.Link()) + require.True(t, ok, "missing receipt for invocation: %s", inv.Link()) + + reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) + rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) + + result.MatchResultR0(rcpt.Out(), func(ok uploadAddSuccess) { + fmt.Printf("%+v\n", ok) + require.Equal(t, ok.Root, rt) + require.Equal(t, ok.Status, "done") + }, func(x ipld.Node) { + f := asFailure(t, x) + fmt.Println(f.Message) + fmt.Println(*f.Stack) + require.Nil(t, f) + }) + }) - result.MatchResultR0(rcpt.Out(), func(ok uploadAddSuccess) { - fmt.Printf("%+v\n", ok) - require.Equal(t, ok.Root, rt) - require.Equal(t, ok.Status, "done") - }, func(rerr ipld.Node) { - t.Fatalf("unexpected error: %+v", rerr) + t.Run("not found", func(t *testing.T) { + space, err := signer.Generate() + require.NoError(t, err) + + server := helpers.Must(NewServer(fixtures.Service)) + conn := helpers.Must(client.NewConnection(fixtures.Service, server)) + + rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} + capability := ucan.NewCapability( + "upload/add", + space.DID().String(), + uploadAddCaveats{Root: rt}, + ) + + invs := []invocation.Invocation{helpers.Must(invocation.Invoke(fixtures.Alice, fixtures.Service, capability))} + resp := helpers.Must(client.Execute(invs, conn)) + rcptlnk, ok := resp.Get(invs[0].Link()) + require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) + + reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) + rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) + + result.MatchResultR0(rcpt.Out(), func(uploadAddSuccess) { + t.Fatalf("expected error: %s", invs[0].Link()) + }, func(x ipld.Node) { + f := asFailure(t, x) + fmt.Printf("%s %+v\n", *f.Name, f) + require.Equal(t, *f.Name, "HandlerNotFoundError") + }) }) -} -func TestHandlerExecutionError(t *testing.T) { - service := helpers.Must(signer.Generate()) - alice := helpers.Must(signer.Generate()) - space := helpers.Must(signer.Generate()) - - uploadadd := validator.NewCapability( - "upload/add", - schema.DIDString(), - schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), - nil, - ) - - server := helpers.Must(NewServer( - service, - WithServiceMethod( - uploadadd.Can(), - Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, receipt.Effects, error) { - return uploadAddSuccess{}, nil, fmt.Errorf("test error") - }), - ), - )) - - conn := helpers.Must(client.NewConnection(service, server)) - rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} - cap := uploadadd.New(space.DID().String(), uploadAddCaveats{Root: rt}) - invs := []invocation.Invocation{helpers.Must(invocation.Invoke(alice, service, cap))} - resp := helpers.Must(client.Execute(invs, conn)) - rcptlnk, ok := resp.Get(invs[0].Link()) - require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) - - rcptsch := bytes.Join([][]byte{sdm.Schema(), []byte(` - type Result union { - | Any "ok" - | HandlerExecutionError "error" - } representation keyed - `)}, []byte("\n")) - - reader := helpers.Must(receipt.NewReceiptReader[ipld.Node, sdm.HandlerExecutionErrorModel](rcptsch)) - rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) - - result.MatchResultR0(rcpt.Out(), func(ipld.Node) { - t.Fatalf("expected error: %s", invs[0].Link()) - }, func(rerr sdm.HandlerExecutionErrorModel) { - fmt.Printf("%s %+v\n", *rerr.Name, rerr) - require.Equal(t, *rerr.Name, "HandlerExecutionError") + t.Run("execution error", func(t *testing.T) { + space, err := signer.Generate() + require.NoError(t, err) + + uploadadd := validator.NewCapability( + "upload/add", + schema.DIDString(), + schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), + nil, + ) + + server := helpers.Must(NewServer( + fixtures.Service, + WithServiceMethod( + uploadadd.Can(), + Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, receipt.Effects, error) { + return uploadAddSuccess{}, nil, fmt.Errorf("test error") + }), + ), + )) + + conn := helpers.Must(client.NewConnection(fixtures.Service, server)) + rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} + cap := uploadadd.New(space.DID().String(), uploadAddCaveats{Root: rt}) + invs := []invocation.Invocation{helpers.Must(invocation.Invoke(fixtures.Alice, fixtures.Service, cap))} + resp := helpers.Must(client.Execute(invs, conn)) + rcptlnk, ok := resp.Get(invs[0].Link()) + require.True(t, ok, "missing receipt for invocation: %s", invs[0].Link()) + + reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) + rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) + + result.MatchResultR0(rcpt.Out(), func(uploadAddSuccess) { + t.Fatalf("expected error: %s", invs[0].Link()) + }, func(x ipld.Node) { + f := asFailure(t, x) + fmt.Printf("%s %+v\n", *f.Name, f) + require.Equal(t, *f.Name, "HandlerExecutionError") + }) }) } -func TestHandleContentTypeError(t *testing.T) { - service := helpers.Must(signer.Generate()) - server := helpers.Must(NewServer(service)) +func TestHandle(t *testing.T) { + t.Run("content type error", func(t *testing.T) { + server := helpers.Must(NewServer(fixtures.Service)) - hd := http.Header{} - hd.Set("Content-Type", "unsupported/media") - hd.Set("Accept", response.ContentType) + hd := http.Header{} + hd.Set("Content-Type", "unsupported/media") + hd.Set("Accept", response.ContentType) - req := thttp.NewHTTPRequest(bytes.NewReader([]byte{}), hd) - res := helpers.Must(Handle(server, req)) - require.Equal(t, res.Status(), http.StatusUnsupportedMediaType) -} + req := thttp.NewHTTPRequest(bytes.NewReader([]byte{}), hd) + res := helpers.Must(Handle(server, req)) + require.Equal(t, res.Status(), http.StatusUnsupportedMediaType) + }) -func TestHandleAcceptError(t *testing.T) { - service := helpers.Must(signer.Generate()) - server := helpers.Must(NewServer(service)) + t.Run("accept error", func(t *testing.T) { + server := helpers.Must(NewServer(fixtures.Service)) - hd := http.Header{} - hd.Set("Content-Type", request.ContentType) - hd.Set("Accept", "not/acceptable") + hd := http.Header{} + hd.Set("Content-Type", request.ContentType) + hd.Set("Accept", "not/acceptable") - req := thttp.NewHTTPRequest(bytes.NewReader([]byte{}), hd) - res := helpers.Must(Handle(server, req)) - require.Equal(t, res.Status(), http.StatusNotAcceptable) -} + req := thttp.NewHTTPRequest(bytes.NewReader([]byte{}), hd) + res := helpers.Must(Handle(server, req)) + require.Equal(t, res.Status(), http.StatusNotAcceptable) + }) -func TestHandleDecodeError(t *testing.T) { - service := helpers.Must(signer.Generate()) - server := helpers.Must(NewServer(service)) + t.Run("decode error", func(t *testing.T) { + server := helpers.Must(NewServer(fixtures.Service)) - hd := http.Header{} - hd.Set("Content-Type", request.ContentType) - hd.Set("Accept", request.ContentType) + hd := http.Header{} + hd.Set("Content-Type", request.ContentType) + hd.Set("Accept", request.ContentType) - // request with invalid payload - req := thttp.NewHTTPRequest(bytes.NewReader([]byte{}), hd) - res := helpers.Must(Handle(server, req)) - require.Equal(t, res.Status(), http.StatusBadRequest) + // request with invalid payload + req := thttp.NewHTTPRequest(bytes.NewReader([]byte{}), hd) + res := helpers.Must(Handle(server, req)) + require.Equal(t, res.Status(), http.StatusBadRequest) + }) } diff --git a/validator/fixtures_test.go b/testing/fixtures/fixtures.go similarity index 60% rename from validator/fixtures_test.go rename to testing/fixtures/fixtures.go index 8cdaa68..18827f0 100644 --- a/validator/fixtures_test.go +++ b/testing/fixtures/fixtures.go @@ -1,14 +1,15 @@ -package validator +package fixtures import "github.com/storacha-network/go-ucanto/principal/ed25519/signer" // did:key:z6Mkk89bC3JrVqKie71YEcc5M1SMVxuCgNx6zLZ8SYJsxALi -var alice, _ = signer.Parse("MgCZT5vOnYZoVAeyjnzuJIVY9J4LNtJ+f8Js0cTPuKUpFne0BVEDJjEu6quFIU8yp91/TY/+MYK8GvlKoTDnqOCovCVM=") +var Alice, _ = signer.Parse("MgCZT5vOnYZoVAeyjnzuJIVY9J4LNtJ+f8Js0cTPuKUpFne0BVEDJjEu6quFIU8yp91/TY/+MYK8GvlKoTDnqOCovCVM=") // did:key:z6MkffDZCkCTWreg8868fG1FGFogcJj5X6PY93pPcWDn9bob -var bob, _ = signer.Parse("MgCYbj5AJfVvdrjkjNCxB3iAUwx7RQHVQ7H1sKyHy46Iose0BEevXgL1V73PD9snOCIoONgb+yQ9sycYchQC8kygR4qY=") +var Bob, _ = signer.Parse("MgCYbj5AJfVvdrjkjNCxB3iAUwx7RQHVQ7H1sKyHy46Iose0BEevXgL1V73PD9snOCIoONgb+yQ9sycYchQC8kygR4qY=") // did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL -var mallory, _ = signer.Parse("'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=") +var Mallory, _ = signer.Parse("'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=") -var service, _ = signer.Parse("MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=") +// did:key:z6MkrZ1r5XBFZjBU34qyD8fueMbMRkKw17BZaq2ivKFjnz2z +var Service, _ = signer.Parse("MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=") diff --git a/validator/capability.go b/validator/capability.go index 5909906..50bd7b3 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -264,8 +264,10 @@ func ResolveCapability[Caveats any](descriptor Descriptor[Caveats], claimed ucan return nil, NewMalformedCapabilityError(source.Capability(), err) } + fmt.Printf("DELEGATED %+v\n", source.Capability().Nb()) + fmt.Printf("CLAIMED %+v\n", claimed.Nb()) // TODO: inherit missing fields - nb, err := descriptor.Nb().Read(claimed) + nb, err := descriptor.Nb().Read(claimed.Nb()) if err != nil { return nil, NewMalformedCapabilityError(source.Capability(), err) } diff --git a/validator/datamodel/errors.go b/validator/datamodel/errors.go index 122fc56..995a8ca 100644 --- a/validator/datamodel/errors.go +++ b/validator/datamodel/errors.go @@ -61,26 +61,3 @@ type NotValidBeforeModel struct { func NotValidBeforeType() schema.Type { return errorTypeSystem.TypeByName("NotValidBefore") } - -type FailureModel struct { - Name *string - Message string -} - -type CapabilityModel struct { - Can string - With string -} - -type EscalatedCapabilityModel struct { - Name *string - Message string - Claimed CapabilityModel - Delegated CapabilityModel - Cause FailureModel - Stack *string -} - -func EscalatedCapabilityType() schema.Type { - return errorTypeSystem.TypeByName("EscalatedCapability") -} diff --git a/validator/datamodel/errors.ipldsch b/validator/datamodel/errors.ipldsch index d2790c3..c0dc4f7 100644 --- a/validator/datamodel/errors.ipldsch +++ b/validator/datamodel/errors.ipldsch @@ -23,22 +23,3 @@ type NotValidBefore struct { validAt Int stack optional String } - -type Capability struct { - can String - with String -} - -type Failure struct { - name optional String - message String -} - -type EscalatedCapability struct { - name optional String - message String - claimed Capability - delegated Capability - cause Failure - stack optional String -} diff --git a/validator/error.go b/validator/error.go index d87445a..3008169 100644 --- a/validator/error.go +++ b/validator/error.go @@ -19,7 +19,7 @@ import ( // go hack for union type -- unexported method cannot be implemented outside module limiting satisfying types type DelegationSubError interface { - error + failure.Failure isDelegationSubError() } @@ -47,20 +47,6 @@ func (ece EscalatedCapabilityError[Caveats]) Error() string { return fmt.Sprintf("Constraint violation: %s", ece.cause.Error()) } -func (ece EscalatedCapabilityError[Caveats]) Build() (datamodel.Node, error) { - name := ece.Name() - stack := ece.Stack() - escalatedCapabilityModel := vdm.EscalatedCapabilityModel{ - Name: &name, - Message: ece.Error(), - Stack: &stack, - Claimed: vdm.CapabilityModel{Can: ece.claimed.Can(), With: ece.claimed.With()}, - Delegated: vdm.CapabilityModel{Can: ece.delegated.Can(), With: ece.delegated.With()}, - Cause: vdm.FailureModel{Message: ece.cause.Error()}, - } - return ipld.WrapWithRecovery(&escalatedCapabilityModel, vdm.EscalatedCapabilityType()) -} - func (ece EscalatedCapabilityError[Caveats]) isDelegationSubError() {} type DelegationError interface { @@ -302,19 +288,6 @@ func (pae PrincipalAlignmentError) Error() string { return fmt.Sprintf("Delegation audience is '%s' instead of '%s'", pae.delegation.Audience().DID(), pae.audience.DID()) } -func (pae PrincipalAlignmentError) Build() (datamodel.Node, error) { - name := pae.Name() - stack := pae.Stack() - invalidAudienceModel := vdm.InvalidAudienceModel{ - Name: &name, - Audience: pae.audience.DID().String(), - Delegation: vdm.Delegation{Audience: pae.delegation.Audience().DID().String()}, - Message: pae.Error(), - Stack: &stack, - } - return ipld.WrapWithRecovery(&invalidAudienceModel, vdm.InvalidAudienceType()) -} - func (pae PrincipalAlignmentError) isInvalidProof() {} // InvalidCapability is an error produced when parsing capabilities. diff --git a/validator/lib_test.go b/validator/lib_test.go index eba0fe7..a66f957 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -15,8 +15,8 @@ import ( "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/principal" - "github.com/storacha-network/go-ucanto/principal/ed25519/signer" "github.com/storacha-network/go-ucanto/principal/ed25519/verifier" + "github.com/storacha-network/go-ucanto/testing/fixtures" "github.com/storacha-network/go-ucanto/ucan" "github.com/stretchr/testify/require" ) @@ -85,17 +85,17 @@ func TestAccess(t *testing.T) { t.Run("authorized", func(t *testing.T) { t.Run("self-issued invocation", func(t *testing.T) { inv, err := invocation.Invoke( - alice, - bob, + fixtures.Alice, + fixtures.Bob, storeAdd.New( - alice.DID().String(), + fixtures.Alice.DID().String(), storeAddCaveats{Link: testLink}, ), ) require.NoError(t, err) context := NewValidationContext( - service.Verifier(), + fixtures.Service.Verifier(), storeAdd, IsSelfIssued, validateAuthOk, @@ -107,9 +107,9 @@ func TestAccess(t *testing.T) { a, x := Access(inv, context) require.NoError(t, x) require.Equal(t, storeAdd.Can(), a.Capability().Can()) - require.Equal(t, alice.DID().String(), a.Capability().With()) - require.Equal(t, alice.DID(), a.Issuer().DID()) - require.Equal(t, bob.DID(), a.Audience().DID()) + require.Equal(t, fixtures.Alice.DID().String(), a.Capability().With()) + require.Equal(t, fixtures.Alice.DID(), a.Issuer().DID()) + require.Equal(t, fixtures.Bob.DID(), a.Audience().DID()) }) }) @@ -117,10 +117,10 @@ func TestAccess(t *testing.T) { t.Run("expired invocation", func(t *testing.T) { exp := ucan.Now() - 5 inv, err := invocation.Invoke( - alice, - service, + fixtures.Alice, + fixtures.Service, storeAdd.New( - alice.DID().String(), + fixtures.Alice.DID().String(), storeAddCaveats{Link: testLink}, ), delegation.WithExpiration(exp), @@ -128,7 +128,7 @@ func TestAccess(t *testing.T) { require.NoError(t, err) context := NewValidationContext( - service.Verifier(), + fixtures.Service.Verifier(), storeAdd, IsSelfIssued, validateAuthOk, @@ -151,10 +151,10 @@ func TestAccess(t *testing.T) { t.Run("not valid before", func(t *testing.T) { nbf := ucan.Now() + 500 inv, err := invocation.Invoke( - alice, - service, + fixtures.Alice, + fixtures.Service, storeAdd.New( - alice.DID().String(), + fixtures.Alice.DID().String(), storeAddCaveats{Link: testLink}, ), delegation.WithNotBefore(nbf), @@ -162,7 +162,7 @@ func TestAccess(t *testing.T) { require.NoError(t, err) context := NewValidationContext( - service.Verifier(), + fixtures.Service.Verifier(), storeAdd, IsSelfIssued, validateAuthOk, @@ -184,19 +184,19 @@ func TestAccess(t *testing.T) { t.Run("invalid signature", func(t *testing.T) { inv, err := invocation.Invoke( - alice, - service, + fixtures.Alice, + fixtures.Service, storeAdd.New( - alice.DID().String(), + fixtures.Alice.DID().String(), storeAddCaveats{Link: testLink}, ), ) require.NoError(t, err) - inv.Data().Model().S = bob.Sign(inv.Root().Bytes()).Bytes() + inv.Data().Model().S = fixtures.Bob.Sign(inv.Root().Bytes()).Bytes() context := NewValidationContext( - service.Verifier(), + fixtures.Service.Verifier(), storeAdd, IsSelfIssued, validateAuthOk, @@ -211,7 +211,7 @@ func TestAccess(t *testing.T) { require.Equal(t, x.Name(), "Unauthorized") msg := strings.Join([]string{ fmt.Sprintf("Claim %s is not authorized", storeAdd), - fmt.Sprintf(" - Proof %s does not has a valid signature from %s", inv.Link(), alice.DID()), + fmt.Sprintf(" - Proof %s does not has a valid signature from %s", inv.Link(), fixtures.Alice.DID()), }, "\n") require.Equal(t, msg, x.Error()) }) @@ -220,18 +220,18 @@ func TestAccess(t *testing.T) { type storeWriteCaveats = storeAddCaveats inv, err := invocation.Invoke( - alice, - service, + fixtures.Alice, + fixtures.Service, ucan.NewCapability( "store/write", - alice.DID().String(), + fixtures.Alice.DID().String(), storeWriteCaveats{Link: testLink}, ), ) require.NoError(t, err) context := NewValidationContext( - service.Verifier(), + fixtures.Service.Verifier(), storeAdd, IsSelfIssued, validateAuthOk, @@ -248,7 +248,7 @@ func TestAccess(t *testing.T) { fmt.Sprintf("Claim %s is not authorized", storeAdd), " - No matching delegated capability found", " - Encountered unknown capabilities", - fmt.Sprintf(" - {\"can\":\"store/write\",\"with\":\"%s\",\"nb\":{}}", alice.DID()), + fmt.Sprintf(" - {\"can\":\"store/write\",\"with\":\"%s\",\"nb\":{}}", fixtures.Alice.DID()), }, "\n") require.Equal(t, msg, x.Error()) }) @@ -256,23 +256,14 @@ func TestAccess(t *testing.T) { } func TestIsSelfIssued(t *testing.T) { - alice, err := signer.Generate() - if err != nil { - t.Fatalf("generating key: %v", err) - } - bob, err := signer.Generate() - if err != nil { - t.Fatalf("generating key: %v", err) - } - - cap := ucan.NewCapability("upload/add", alice.DID().String(), struct{}{}) + cap := ucan.NewCapability("upload/add", fixtures.Alice.DID().String(), struct{}{}) - canIssue := IsSelfIssued(cap, alice.DID()) + canIssue := IsSelfIssued(cap, fixtures.Alice.DID()) if canIssue == false { t.Fatal("capability self issued by alice") } - canIssue = IsSelfIssued(cap, bob.DID()) + canIssue = IsSelfIssued(cap, fixtures.Bob.DID()) if canIssue == true { t.Fatal("capability not self issued by bob") } From 30191b3abf2994d7494fce99d866750d45fe0e31 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 5 Sep 2024 15:43:08 +0100 Subject: [PATCH 25/37] fix: server tests --- core/schema/struct.go | 4 +++ server/server_test.go | 64 ++++++++++++++++++++++++++++++++--------- validator/capability.go | 2 -- 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/core/schema/struct.go b/core/schema/struct.go index eccce1c..f7cf42f 100644 --- a/core/schema/struct.go +++ b/core/schema/struct.go @@ -13,6 +13,10 @@ type strukt[T any] struct { } func (s strukt[T]) Read(input any) (T, failure.Failure) { + if o, ok := input.(T); ok { + return o, nil + } + var bind T node, ok := input.(ipld.Node) if !ok { diff --git a/server/server_test.go b/server/server_test.go index e8305e1..9e973b9 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -20,7 +20,6 @@ import ( "github.com/storacha-network/go-ucanto/core/result" fdm "github.com/storacha-network/go-ucanto/core/result/failure/datamodel" "github.com/storacha-network/go-ucanto/core/schema" - "github.com/storacha-network/go-ucanto/principal/ed25519/signer" "github.com/storacha-network/go-ucanto/testing/fixtures" "github.com/storacha-network/go-ucanto/testing/helpers" "github.com/storacha-network/go-ucanto/transport/car/request" @@ -116,10 +115,53 @@ func asFailure(t testing.TB, n ipld.Node) fdm.FailureModel { } func TestExecute(t *testing.T) { - t.Run("simple", func(t *testing.T) { - space, err := signer.Generate() + t.Run("self-signed", func(t *testing.T) { + uploadadd := validator.NewCapability( + "upload/add", + schema.DIDString(), + schema.Struct[uploadAddCaveats](uploadAddCaveatsType(), nil), + nil, + ) + + server := helpers.Must(NewServer( + fixtures.Service, + WithServiceMethod( + uploadadd.Can(), + Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, receipt.Effects, error) { + return uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}, nil, nil + }), + ), + )) + + conn := helpers.Must(client.NewConnection(fixtures.Service, server)) + rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} + cap := uploadadd.New(fixtures.Service.DID().String(), uploadAddCaveats{Root: rt}) + inv, err := invocation.Invoke(fixtures.Service, fixtures.Service, cap) require.NoError(t, err) + resp, err := client.Execute([]invocation.Invocation{inv}, conn) + require.NoError(t, err) + + // get the receipt link for the invocation from the response + rcptlnk, ok := resp.Get(inv.Link()) + require.True(t, ok, "missing receipt for invocation: %s", inv.Link()) + + reader := helpers.Must(receipt.NewReceiptReader[uploadAddSuccess, ipld.Node](rcptsch)) + rcpt := helpers.Must(reader.Read(rcptlnk, resp.Blocks())) + + result.MatchResultR0(rcpt.Out(), func(ok uploadAddSuccess) { + fmt.Printf("%+v\n", ok) + require.Equal(t, ok.Root, rt) + require.Equal(t, ok.Status, "done") + }, func(x ipld.Node) { + f := asFailure(t, x) + fmt.Println(f.Message) + fmt.Println(*f.Stack) + require.Nil(t, f) + }) + }) + + t.Run("delegated", func(t *testing.T) { uploadadd := validator.NewCapability( "upload/add", schema.DIDString(), @@ -139,11 +181,13 @@ func TestExecute(t *testing.T) { conn := helpers.Must(client.NewConnection(fixtures.Service, server)) rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} - cap := uploadadd.New(space.DID().String(), uploadAddCaveats{Root: rt}) + cap := uploadadd.New(fixtures.Service.DID().String(), uploadAddCaveats{Root: rt}) dgl, err := delegation.Delegate( fixtures.Service, fixtures.Alice, - []ucan.Capability[uploadAddCaveats]{ucan.NewCapability(uploadadd.Can(), space.DID().String(), uploadAddCaveats{})}, + []ucan.Capability[uploadAddCaveats]{ + ucan.NewCapability(uploadadd.Can(), fixtures.Service.DID().String(), uploadAddCaveats{}), + }, ) require.NoError(t, err) @@ -174,16 +218,13 @@ func TestExecute(t *testing.T) { }) t.Run("not found", func(t *testing.T) { - space, err := signer.Generate() - require.NoError(t, err) - server := helpers.Must(NewServer(fixtures.Service)) conn := helpers.Must(client.NewConnection(fixtures.Service, server)) rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} capability := ucan.NewCapability( "upload/add", - space.DID().String(), + fixtures.Alice.DID().String(), uploadAddCaveats{Root: rt}, ) @@ -205,9 +246,6 @@ func TestExecute(t *testing.T) { }) t.Run("execution error", func(t *testing.T) { - space, err := signer.Generate() - require.NoError(t, err) - uploadadd := validator.NewCapability( "upload/add", schema.DIDString(), @@ -227,7 +265,7 @@ func TestExecute(t *testing.T) { conn := helpers.Must(client.NewConnection(fixtures.Service, server)) rt := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} - cap := uploadadd.New(space.DID().String(), uploadAddCaveats{Root: rt}) + cap := uploadadd.New(fixtures.Alice.DID().String(), uploadAddCaveats{Root: rt}) invs := []invocation.Invocation{helpers.Must(invocation.Invoke(fixtures.Alice, fixtures.Service, cap))} resp := helpers.Must(client.Execute(invs, conn)) rcptlnk, ok := resp.Get(invs[0].Link()) diff --git a/validator/capability.go b/validator/capability.go index 50bd7b3..5fd06f7 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -264,8 +264,6 @@ func ResolveCapability[Caveats any](descriptor Descriptor[Caveats], claimed ucan return nil, NewMalformedCapabilityError(source.Capability(), err) } - fmt.Printf("DELEGATED %+v\n", source.Capability().Nb()) - fmt.Printf("CLAIMED %+v\n", claimed.Nb()) // TODO: inherit missing fields nb, err := descriptor.Nb().Read(claimed.Nb()) if err != nil { From f23dceac4a57089ee705f7106b5310acc36d88b1 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 5 Sep 2024 17:23:30 +0100 Subject: [PATCH 26/37] fix: more validator tests --- .gitignore | 1 + core/delegation/delegate.go | 9 ++++ did/did.go | 5 ++ testing/fixtures/fixtures.go | 2 +- validator/capability.go | 4 -- validator/lib_test.go | 100 +++++++++++++++++++++++++++++++++-- 6 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa92975 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.out \ No newline at end of file diff --git a/core/delegation/delegate.go b/core/delegation/delegate.go index a176513..9caa863 100644 --- a/core/delegation/delegate.go +++ b/core/delegation/delegate.go @@ -67,6 +67,15 @@ func WithProofs(prf Proofs) Option { } } +// WithProof configures the proofs for the UCAN in the case where there is only +// a single proof. +func WithProof(prf Proof) Option { + return func(cfg *delegationConfig) error { + cfg.prf = Proofs{prf} + return nil + } +} + // Delegate creates a new signed token with a given `options.issuer`. If // expiration is not set it defaults to 30 seconds from now. Returns UCAN in // primary IPLD representation. diff --git a/did/did.go b/did/did.go index 483485c..4614b0a 100644 --- a/did/did.go +++ b/did/did.go @@ -49,6 +49,11 @@ func (d DID) String() string { return "did:" + d.str[MethodOffset:] } +// GoString formats the decentralized identity document (DID) as a string. +func (d DID) GoString() string { + return d.String() +} + func Decode(bytes []byte) (DID, error) { code, _, err := varint.FromUvarint(bytes) if err != nil { diff --git a/testing/fixtures/fixtures.go b/testing/fixtures/fixtures.go index 18827f0..a4bd7ea 100644 --- a/testing/fixtures/fixtures.go +++ b/testing/fixtures/fixtures.go @@ -9,7 +9,7 @@ var Alice, _ = signer.Parse("MgCZT5vOnYZoVAeyjnzuJIVY9J4LNtJ+f8Js0cTPuKUpFne0BVE var Bob, _ = signer.Parse("MgCYbj5AJfVvdrjkjNCxB3iAUwx7RQHVQ7H1sKyHy46Iose0BEevXgL1V73PD9snOCIoONgb+yQ9sycYchQC8kygR4qY=") // did:key:z6MktafZTREjJkvV5mfJxcLpNBoVPwDLhTuMg9ng7dY4zMAL -var Mallory, _ = signer.Parse("'MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=") +var Mallory, _ = signer.Parse("MgCYtH0AvYxiQwBG6+ZXcwlXywq9tI50G2mCAUJbwrrahkO0B0elFYkl3Ulf3Q3A/EvcVY0utb4etiSE8e6pi4H0FEmU=") // did:key:z6MkrZ1r5XBFZjBU34qyD8fueMbMRkKw17BZaq2ivKFjnz2z var Service, _ = signer.Parse("MgCYKXoHVy7Vk4/QjcEGi+MCqjntUiasxXJ8uJKY0qh11e+0Bs8WsdqGK7xothgrDzzWD0ME7ynPjz2okXDh8537lId8=") diff --git a/validator/capability.go b/validator/capability.go index 5fd06f7..8b5f330 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -187,10 +187,6 @@ func (c capability[Caveats]) New(with ucan.Resource, nb Caveats) ucan.Capability return ucan.NewCapability(c.descriptor.Can(), with, nb) } -// func (c capability[Caveats]) Invoke(issuer ucan.Signer, audience ucan.Principal, with ucan.Resource, nb Caveats, options ...delegation.Option) (invocation.IssuedInvocation, error) { -// return invocation.Invoke(issuer, audience, c.New(with, nb), options...) -// } - func NewCapability[Caveats any]( can ucan.Ability, with schema.Reader[string, ucan.Resource], diff --git a/validator/lib_test.go b/validator/lib_test.go index a66f957..22f0c4c 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -30,11 +30,13 @@ func (c storeAddCaveats) Build() (ipld.Node, error) { np := basicnode.Prototype.Any nb := np.NewBuilder() ma, _ := nb.BeginMap(2) - ma.AssembleKey().AssignString("link") - ma.AssembleValue().AssignLink(c.Link) - if c.Origin != nil { - ma.AssembleKey().AssignString("origin") - ma.AssembleValue().AssignLink(c.Origin) + if c != (storeAddCaveats{}) { + ma.AssembleKey().AssignString("link") + ma.AssembleValue().AssignLink(c.Link) + if c.Origin != nil { + ma.AssembleKey().AssignString("origin") + ma.AssembleValue().AssignLink(c.Origin) + } } ma.Finish() return nb.Build(), nil @@ -111,6 +113,94 @@ func TestAccess(t *testing.T) { require.Equal(t, fixtures.Alice.DID(), a.Issuer().DID()) require.Equal(t, fixtures.Bob.DID(), a.Audience().DID()) }) + + t.Run("delegated invocation", func(t *testing.T) { + dlg, err := delegation.Delegate( + fixtures.Alice, + fixtures.Bob, + []ucan.Capability[storeAddCaveats]{ + storeAdd.New(fixtures.Alice.DID().String(), storeAddCaveats{}), + }, + ) + require.NoError(t, err) + + inv, err := invocation.Invoke( + fixtures.Bob, + fixtures.Service, + storeAdd.New( + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + ), + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.NoError(t, x) + require.Equal(t, storeAdd.Can(), a.Capability().Can()) + require.Equal(t, fixtures.Alice.DID().String(), a.Capability().With()) + require.Equal(t, fixtures.Bob.DID(), a.Issuer().DID()) + require.Equal(t, fixtures.Service.DID(), a.Audience().DID()) + }) + + t.Run("delegation chain", func(t *testing.T) { + alice2bob, err := delegation.Delegate( + fixtures.Alice, + fixtures.Bob, + []ucan.Capability[storeAddCaveats]{ + storeAdd.New(fixtures.Alice.DID().String(), storeAddCaveats{}), + }, + ) + require.NoError(t, err) + + bob2mallory, err := delegation.Delegate( + fixtures.Bob, + fixtures.Mallory, + []ucan.Capability[storeAddCaveats]{ + storeAdd.New(fixtures.Alice.DID().String(), storeAddCaveats{}), + }, + delegation.WithProof(delegation.FromDelegation(alice2bob)), + ) + require.NoError(t, err) + + inv, err := invocation.Invoke( + fixtures.Mallory, + fixtures.Service, + storeAdd.New( + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + ), + delegation.WithProof(delegation.FromDelegation(bob2mallory)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.NoError(t, x) + require.Equal(t, storeAdd.Can(), a.Capability().Can()) + require.Equal(t, fixtures.Alice.DID().String(), a.Capability().With()) + require.Equal(t, fixtures.Mallory.DID(), a.Issuer().DID()) + require.Equal(t, fixtures.Service.DID(), a.Audience().DID()) + }) }) t.Run("unauthorized", func(t *testing.T) { From 85af3cd543da6158547b0092381a46344b70141b Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Thu, 5 Sep 2024 22:14:17 +0100 Subject: [PATCH 27/37] fix: more tests and fixes --- validator/error.go | 2 +- validator/lib.go | 20 ++--- validator/lib_test.go | 188 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 12 deletions(-) diff --git a/validator/error.go b/validator/error.go index 3008169..491ac6b 100644 --- a/validator/error.go +++ b/validator/error.go @@ -636,7 +636,7 @@ type ProofError struct { } func (pe ProofError) Error() string { - return fmt.Sprintf("Capability can not be derived from prf: %s because: %s\n", pe.proof, li(pe.cause.Error())) + return fmt.Sprintf("Capability can not be derived from prf: %s because:\n%s", pe.proof, li(pe.cause.Error())) } func (pe ProofError) Proof() ucan.Link { diff --git a/validator/lib.go b/validator/lib.go index 96f1654..30f5a27 100644 --- a/validator/lib.go +++ b/validator/lib.go @@ -293,11 +293,11 @@ func VerifyAuthorization(dlg delegation.Delegation, prfs []delegation.Delegation if err != nil { return nil, NewUnverifiableSignatureError(dlg, err) } - dlg, serr := VerifySignature(dlg, vfr) - if serr != nil { - return nil, serr - } - return dlg, nil + return VerifySignature(dlg, vfr) + } + + if dlg.Issuer().DID() == ctx.Authority().DID() { + return VerifySignature(dlg, ctx.Authority()) } // Attempt to resolve embedded authorization session from the authority @@ -319,10 +319,7 @@ func VerifyAuthorization(dlg delegation.Delegation, prfs []delegation.Delegation return nil, NewUnverifiableSignatureError(dlg, perr) } - _, verr := VerifySignature(dlg, vfr) - if verr != nil { - return nil, verr - } + return VerifySignature(dlg, vfr) } return dlg, nil @@ -394,12 +391,13 @@ func Authorize[Caveats any](match Match[Caveats], context ClaimContext) (Authori return NewAuthorization(matched, nil), nil } - auth, err := Authorize(matched, context) + auth, err := Authorize(selector, context) if err != nil { failedprf = append(failedprf, err) continue } - return auth, nil + + return NewAuthorization(matched, []Authorization[Caveats]{auth}), nil } return nil, NewInvalidClaimError(match, dlgerrs, unknowns, invalidprf, failedprf) diff --git a/validator/lib_test.go b/validator/lib_test.go index 22f0c4c..717d720 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -200,6 +200,16 @@ func TestAccess(t *testing.T) { require.Equal(t, fixtures.Alice.DID().String(), a.Capability().With()) require.Equal(t, fixtures.Mallory.DID(), a.Issuer().DID()) require.Equal(t, fixtures.Service.DID(), a.Audience().DID()) + + require.Equal(t, storeAdd.Can(), a.Proofs()[0].Capability().Can()) + require.Equal(t, fixtures.Alice.DID().String(), a.Proofs()[0].Capability().With()) + require.Equal(t, fixtures.Bob.DID(), a.Proofs()[0].Issuer().DID()) + require.Equal(t, fixtures.Mallory.DID(), a.Proofs()[0].Audience().DID()) + + require.Equal(t, storeAdd.Can(), a.Proofs()[0].Proofs()[0].Capability().Can()) + require.Equal(t, fixtures.Alice.DID().String(), a.Proofs()[0].Proofs()[0].Capability().With()) + require.Equal(t, fixtures.Alice.DID(), a.Proofs()[0].Proofs()[0].Issuer().DID()) + require.Equal(t, fixtures.Bob.DID(), a.Proofs()[0].Proofs()[0].Audience().DID()) }) }) @@ -343,6 +353,184 @@ func TestAccess(t *testing.T) { require.Equal(t, msg, x.Error()) }) }) + + t.Run("invalid claim", func(t *testing.T) { + t.Run("no proofs", func(t *testing.T) { + inv, err := invocation.Invoke( + fixtures.Alice, + fixtures.Bob, + storeAdd.New( + fixtures.Bob.DID().String(), + storeAddCaveats{Link: testLink}, + ), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Bob.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Alice.DID()), + " - Delegated capability not found", + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("expired", func(t *testing.T) { + exp := ucan.Now() - 5 + dlg, err := delegation.Delegate( + fixtures.Alice, + fixtures.Bob, + []ucan.Capability[storeAddCaveats]{ + storeAdd.New(fixtures.Alice.DID().String(), storeAddCaveats{}), + }, + delegation.WithExpiration(exp), + ) + require.NoError(t, err) + + inv, err := invocation.Invoke( + fixtures.Bob, + fixtures.Service, + storeAdd.New( + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + ), + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + fmt.Sprintf(" - Capability can not be derived from prf: %s because:", dlg.Link()), + fmt.Sprintf(" - Proof %s has expired on %s", dlg.Link(), time.Unix(int64(exp), 0).Format(time.RFC3339)), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("not valid before", func(t *testing.T) { + nbf := ucan.Now() + 60*60 + dlg, err := delegation.Delegate( + fixtures.Alice, + fixtures.Bob, + []ucan.Capability[storeAddCaveats]{ + storeAdd.New(fixtures.Alice.DID().String(), storeAddCaveats{}), + }, + delegation.WithNotBefore(nbf), + ) + require.NoError(t, err) + + inv, err := invocation.Invoke( + fixtures.Bob, + fixtures.Service, + storeAdd.New( + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + ), + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + fmt.Sprintf(" - Capability can not be derived from prf: %s because:", dlg.Link()), + fmt.Sprintf(" - Proof %s is not valid before %s", dlg.Link(), time.Unix(int64(nbf), 0).Format(time.RFC3339)), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + // t.Run("invalid signature", func(t *testing.T) { + // dlg, err := delegation.Delegate( + // fixtures.Alice, + // fixtures.Bob, + // []ucan.Capability[storeAddCaveats]{ + // storeAdd.New(fixtures.Alice.DID().String(), storeAddCaveats{}), + // }, + // ) + // require.NoError(t, err) + + // // TODO: mess up signature - cannot just change model as already encoded + // // blocks are used in the invocation... + + // inv, err := invocation.Invoke( + // fixtures.Bob, + // fixtures.Service, + // storeAdd.New( + // fixtures.Alice.DID().String(), + // storeAddCaveats{Link: testLink}, + // ), + // delegation.WithProof(delegation.FromDelegation(dlg)), + // ) + // require.NoError(t, err) + + // context := NewValidationContext( + // fixtures.Service.Verifier(), + // storeAdd, + // IsSelfIssued, + // validateAuthOk, + // ProofUnavailable, + // parseEdPrincipal, + // FailDIDKeyResolution, + // ) + + // a, x := Access(inv, context) + // require.Nil(t, a) + // require.Error(t, x) + // require.Equal(t, x.Name(), "Unauthorized") + // msg := strings.Join([]string{ + // fmt.Sprintf("Claim %s is not authorized", storeAdd), + // fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + // fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + // fmt.Sprintf(" - Capability can not be derived from prf: %s because:", dlg.Link()), + // fmt.Sprintf(" - Proof %s has an invalid signature from %s", dlg.Link(), fixtures.Alice.DID()), + // }, "\n") + // require.Equal(t, msg, x.Error()) + // }) + }) } func TestIsSelfIssued(t *testing.T) { From e627fba670abe35bb9f00ab63cab26df6f238c59 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Fri, 6 Sep 2024 12:09:53 +0100 Subject: [PATCH 28/37] test: more tests --- core/schema/did.go | 7 ++ core/schema/struct.go | 15 ++- ucan/caveats.go | 21 ++++ validator/capability.go | 2 - validator/error.go | 10 +- validator/lib_test.go | 259 ++++++++++++++++++++++++++++++++-------- 6 files changed, 251 insertions(+), 63 deletions(-) create mode 100644 ucan/caveats.go diff --git a/core/schema/did.go b/core/schema/did.go index b6ff250..d9d7b20 100644 --- a/core/schema/did.go +++ b/core/schema/did.go @@ -1,12 +1,19 @@ package schema import ( + "fmt" + "strings" + "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/did" ) var didreader = reader[string, did.DID]{ readFunc: func(input string) (did.DID, failure.Failure) { + pfx := "did:" + if !strings.HasPrefix(input, pfx) { + return did.Undef, NewSchemaError(fmt.Sprintf(`Expected a "%s" but got "%s" instead`, pfx, input)) + } d, err := did.Parse(input) if err != nil { return did.Undef, NewSchemaError(err.Error()) diff --git a/core/schema/struct.go b/core/schema/struct.go index f7cf42f..a4dfa1f 100644 --- a/core/schema/struct.go +++ b/core/schema/struct.go @@ -13,14 +13,19 @@ type strukt[T any] struct { } func (s strukt[T]) Read(input any) (T, failure.Failure) { - if o, ok := input.(T); ok { - return o, nil - } - var bind T node, ok := input.(ipld.Node) if !ok { - return bind, NewSchemaError("unexpected input: not an IPLD node") + // If input is not an IPLD node, can it be converted to one? + if builder, ok := input.(ipld.Builder); ok { + n, err := builder.Build() + if err != nil { + return bind, NewSchemaError(err.Error()) + } + node = n + } else { + return bind, NewSchemaError("unexpected input: not an IPLD node") + } } if s.policy != nil { diff --git a/ucan/caveats.go b/ucan/caveats.go new file mode 100644 index 0000000..9e93687 --- /dev/null +++ b/ucan/caveats.go @@ -0,0 +1,21 @@ +package ucan + +import ( + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/basicnode" +) + +// NoCaveats can be used when a capability has no additional domain specific +// details and/or restrictions. +type NoCaveats struct{} + +func (c NoCaveats) Build() (datamodel.Node, error) { + np := basicnode.Prototype.Any + nb := np.NewBuilder() + ma, err := nb.BeginMap(0) + if err != nil { + return nil, err + } + ma.Finish() + return nb.Build(), nil +} diff --git a/validator/capability.go b/validator/capability.go index 8b5f330..366b1d1 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -113,8 +113,6 @@ type CapabilityParser[Caveats any] interface { Can() ucan.Ability // New creates a new capability from the passed options. New(with ucan.Resource, nb Caveats) ucan.Capability[Caveats] - // Invoke creates an invocation of this capability. - // Invoke(with ucan.Resource, nb Caveats) (invocation.IssuedInvocation, error) } type Derivable[Caveats any] interface { diff --git a/validator/error.go b/validator/error.go index 491ac6b..1c64a35 100644 --- a/validator/error.go +++ b/validator/error.go @@ -67,7 +67,7 @@ func NewDelegationError(causes []DelegationSubError, context interface{}) Delega } func (de delegationError) Error() string { - return fmt.Sprintf("Cannot derive %s from delegated capabilities: %s", de.context, errors.Join(de.Unwrap()...).Error()) + return fmt.Sprintf("Cannot derive %s from delegated capabilities:\n%s", de.context, li(errors.Join(de.Unwrap()...).Error())) } func (de delegationError) Causes() []DelegationSubError { @@ -194,11 +194,11 @@ func (ise InvalidSignatureError) Error() string { issuer := ise.Issuer().DID() key := ise.verifier.DID() if strings.HasPrefix(issuer.String(), "did:key") { - return fmt.Sprintf(`Proof %s does not has a valid signature from %s`, ise.delegation.Link(), key) + return fmt.Sprintf(`Proof %s does not have a valid signature from %s`, ise.delegation.Link(), key) } return strings.Join([]string{ - fmt.Sprintf("Proof %s issued by %s does not has a valid signature from %s", ise.delegation.Link(), issuer, key), - " ℹ️ Probably issuer signed with a different key, which got rotated, invalidating delegations that were issued with prior keys", + fmt.Sprintf("Proof %s issued by %s does not have a valid signature from %s", ise.delegation.Link(), issuer, key), + " ℹ️ Issuer probably signed with a different key, which got rotated, invalidating delegations that were issued with prior keys", }, "\n") } @@ -232,7 +232,7 @@ func (upe UnavailableProofError) Link() ucan.Link { func (upe UnavailableProofError) Error() string { messages := []string{ - fmt.Sprintf("Linked proof '%s' is not included and could not be resolved", upe.link), + fmt.Sprintf(`Linked proof "%s" is not included and could not be resolved`, upe.link), } if upe.cause != nil { messages = append(messages, li(fmt.Sprintf("Proof resolution failed with: %s", upe.cause.Error()))) diff --git a/validator/lib_test.go b/validator/lib_test.go index 717d720..717d5a8 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -10,14 +10,19 @@ import ( "github.com/ipld/go-ipld-prime" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/storacha-network/go-ucanto/core/dag/blockstore" "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/invocation" + "github.com/storacha-network/go-ucanto/core/ipld/block" + "github.com/storacha-network/go-ucanto/core/ipld/codec/cbor" + "github.com/storacha-network/go-ucanto/core/ipld/hash/sha256" "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/principal" "github.com/storacha-network/go-ucanto/principal/ed25519/verifier" "github.com/storacha-network/go-ucanto/testing/fixtures" "github.com/storacha-network/go-ucanto/ucan" + udm "github.com/storacha-network/go-ucanto/ucan/datamodel/ucan" "github.com/stretchr/testify/require" ) @@ -311,21 +316,19 @@ func TestAccess(t *testing.T) { require.Equal(t, x.Name(), "Unauthorized") msg := strings.Join([]string{ fmt.Sprintf("Claim %s is not authorized", storeAdd), - fmt.Sprintf(" - Proof %s does not has a valid signature from %s", inv.Link(), fixtures.Alice.DID()), + fmt.Sprintf(" - Proof %s does not have a valid signature from %s", inv.Link(), fixtures.Alice.DID()), }, "\n") require.Equal(t, msg, x.Error()) }) t.Run("unknown capability", func(t *testing.T) { - type storeWriteCaveats = storeAddCaveats - inv, err := invocation.Invoke( fixtures.Alice, fixtures.Service, ucan.NewCapability( "store/write", fixtures.Alice.DID().String(), - storeWriteCaveats{Link: testLink}, + ucan.NoCaveats{}, ), ) require.NoError(t, err) @@ -483,53 +486,207 @@ func TestAccess(t *testing.T) { require.Equal(t, msg, x.Error()) }) - // t.Run("invalid signature", func(t *testing.T) { - // dlg, err := delegation.Delegate( - // fixtures.Alice, - // fixtures.Bob, - // []ucan.Capability[storeAddCaveats]{ - // storeAdd.New(fixtures.Alice.DID().String(), storeAddCaveats{}), - // }, - // ) - // require.NoError(t, err) - - // // TODO: mess up signature - cannot just change model as already encoded - // // blocks are used in the invocation... - - // inv, err := invocation.Invoke( - // fixtures.Bob, - // fixtures.Service, - // storeAdd.New( - // fixtures.Alice.DID().String(), - // storeAddCaveats{Link: testLink}, - // ), - // delegation.WithProof(delegation.FromDelegation(dlg)), - // ) - // require.NoError(t, err) - - // context := NewValidationContext( - // fixtures.Service.Verifier(), - // storeAdd, - // IsSelfIssued, - // validateAuthOk, - // ProofUnavailable, - // parseEdPrincipal, - // FailDIDKeyResolution, - // ) - - // a, x := Access(inv, context) - // require.Nil(t, a) - // require.Error(t, x) - // require.Equal(t, x.Name(), "Unauthorized") - // msg := strings.Join([]string{ - // fmt.Sprintf("Claim %s is not authorized", storeAdd), - // fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), - // fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), - // fmt.Sprintf(" - Capability can not be derived from prf: %s because:", dlg.Link()), - // fmt.Sprintf(" - Proof %s has an invalid signature from %s", dlg.Link(), fixtures.Alice.DID()), - // }, "\n") - // require.Equal(t, msg, x.Error()) - // }) + t.Run("invalid signature", func(t *testing.T) { + // In order to mess up the signature we need to reach deep in UCAN library + // to create a UCAN model, manually setting the signature to something bad + // and then encode it as the root block of the delegation. + nb, _ := storeAddCaveats{Link: testLink}.Build() + model := udm.UCANModel{ + V: "0.9.1", + S: fixtures.Alice.Sign([]byte{}).Bytes(), + Iss: fixtures.Alice.DID().Bytes(), + Aud: fixtures.Bob.DID().Bytes(), + Att: []udm.CapabilityModel{ + { + Can: storeAdd.Can(), + With: fixtures.Alice.DID().String(), + Nb: nb, + }, + }, + Exp: ucan.Now() + 30, + } + + rt, err := block.Encode(&model, udm.Type(), cbor.Codec, sha256.Hasher) + require.NoError(t, err) + + bs, err := blockstore.NewBlockStore(blockstore.WithBlocks([]block.Block{rt})) + require.NoError(t, err) + + dlg := delegation.NewDelegation(rt, bs) + + inv, err := invocation.Invoke( + fixtures.Bob, + fixtures.Service, + storeAdd.New( + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + ), + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + fmt.Sprintf(" - Capability can not be derived from prf: %s because:", dlg.Link()), + fmt.Sprintf(" - Proof %s does not have a valid signature from %s", dlg.Link(), fixtures.Alice.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("unknown capability", func(t *testing.T) { + dlg, err := delegation.Delegate( + fixtures.Alice, + fixtures.Bob, + []ucan.Capability[ucan.NoCaveats]{ + ucan.NewCapability("store/pin", fixtures.Alice.DID().String(), ucan.NoCaveats{}), + }, + ) + require.NoError(t, err) + + inv, err := invocation.Invoke( + fixtures.Bob, + fixtures.Service, + storeAdd.New( + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + ), + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + " - Delegated capability not found", + " - Encountered unknown capabilities", + fmt.Sprintf(` - {"can":"store/pin","with":"%s","nb":{}}`, fixtures.Alice.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("malformed capability", func(t *testing.T) { + badDID := fmt.Sprintf("bib:%s", fixtures.Alice.DID().String()[4:]) + dlg, err := delegation.Delegate( + fixtures.Alice, + fixtures.Bob, + []ucan.Capability[storeAddCaveats]{ + ucan.NewCapability("store/add", badDID, storeAddCaveats{}), + }, + ) + require.NoError(t, err) + + inv, err := invocation.Invoke( + fixtures.Bob, + fixtures.Service, + storeAdd.New( + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + ), + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + fmt.Sprintf(` - Cannot derive {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} from delegated capabilities:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(` - Encountered malformed '%s' capability: {"can":"%s","with":"%s","nb":{}}`, storeAdd.Can(), storeAdd.Can(), badDID), + fmt.Sprintf(` - Expected a "did:" but got "%s" instead`, badDID), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("unavailable proof", func(t *testing.T) { + dlg, err := delegation.Delegate( + fixtures.Alice, + fixtures.Bob, + []ucan.Capability[storeAddCaveats]{ + ucan.NewCapability("store/add", fixtures.Alice.DID().String(), storeAddCaveats{}), + }, + ) + require.NoError(t, err) + + inv, err := invocation.Invoke( + fixtures.Bob, + fixtures.Service, + storeAdd.New( + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + ), + delegation.WithProof(delegation.FromLink(dlg.Link())), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + fmt.Sprintf(` - Capability can not be derived from prf: %s because:`, dlg.Link()), + fmt.Sprintf(` - Linked proof "%s" is not included and could not be resolved`, dlg.Link()), + ` - Proof resolution failed with: no proof resolver configured`, + }, "\n") + require.Equal(t, msg, x.Error()) + }) }) } From 109e8f64e61fd1d602bf4f64a95db24cc58c7cd8 Mon Sep 17 00:00:00 2001 From: hannahhoward Date: Mon, 2 Sep 2024 17:48:48 -0700 Subject: [PATCH 29/37] feat(iterable): use built in iterators use go 1.23 built in iterators which are more memory efficient and easier to work with --- client/connection.go | 4 +- core/car/car.go | 65 ++++++++++------- core/car/car_test.go | 8 +-- core/dag/blockstore/blockstore.go | 86 +++++++++------------- core/delegation/delegation.go | 4 +- core/ipld/view.go | 4 +- core/iterable/iterable.go | 101 +++++++++----------------- core/iterable/mapiterable.go | 91 ------------------------ core/iterable/mapiterable_test.go | 114 ------------------------------ core/message/message.go | 4 +- core/receipt/receipt.go | 13 ++-- go.mod | 2 +- 12 files changed, 125 insertions(+), 371 deletions(-) delete mode 100644 core/iterable/mapiterable.go delete mode 100644 core/iterable/mapiterable_test.go diff --git a/client/connection.go b/client/connection.go index 7232cbc..6eb4f54 100644 --- a/client/connection.go +++ b/client/connection.go @@ -4,10 +4,10 @@ import ( "crypto/sha256" "fmt" "hash" + "iter" "github.com/storacha-network/go-ucanto/core/invocation" "github.com/storacha-network/go-ucanto/core/ipld/block" - "github.com/storacha-network/go-ucanto/core/iterable" "github.com/storacha-network/go-ucanto/core/message" "github.com/storacha-network/go-ucanto/transport" "github.com/storacha-network/go-ucanto/transport/car" @@ -96,7 +96,7 @@ func (c *conn) Hasher() hash.Hash { type ExecutionResponse interface { // Blocks returns an iterator of all the IPLD blocks that are included in // the response. - Blocks() iterable.Iterator[block.Block] + Blocks() iter.Seq2[block.Block, error] // Get returns a link to a receipt, given an invocation link. Get(inv ucan.Link) (ucan.Link, bool) } diff --git a/core/car/car.go b/core/car/car.go index 7aadf7e..95911f4 100644 --- a/core/car/car.go +++ b/core/car/car.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "io" + "iter" "github.com/ipfs/go-cid" cbor "github.com/ipfs/go-ipld-cbor" @@ -13,14 +14,13 @@ import ( "github.com/multiformats/go-varint" "github.com/storacha-network/go-ucanto/core/ipld" "github.com/storacha-network/go-ucanto/core/ipld/block" - "github.com/storacha-network/go-ucanto/core/iterable" ) // ContentType is the value the HTTP Content-Type header should have for CARs. // See https://www.iana.org/assignments/media-types/application/vnd.ipld.car const ContentType = "application/vnd.ipld.car" -func Encode(roots []ipld.Link, blocks iterable.Iterator[ipld.Block]) io.Reader { +func Encode(roots []ipld.Link, blocks iter.Seq2[ipld.Block, error]) io.Reader { reader, writer := io.Pipe() go func() { cids := []cid.Cid{} @@ -42,16 +42,16 @@ func Encode(roots []ipld.Link, blocks iterable.Iterator[ipld.Block]) io.Reader { return } util.LdWrite(writer, hb) - for { - block, err := blocks.Next() + for block, err := range blocks { + if err != nil { + writer.CloseWithError(fmt.Errorf("writing CAR blocks: %s", err)) + return + } + err = util.LdWrite(writer, []byte(block.Link().Binary()), block.Bytes()) if err != nil { - if err == io.EOF { - break - } writer.CloseWithError(fmt.Errorf("writing CAR blocks: %s", err)) return } - util.LdWrite(writer, []byte(block.Link().Binary()), block.Bytes()) } writer.Close() }() @@ -78,7 +78,7 @@ func (cb carBlock) Length() uint64 { return cb.length } -func Decode(reader io.Reader) ([]ipld.Link, iterable.Iterator[ipld.Block], error) { +func Decode(reader io.Reader) ([]ipld.Link, iter.Seq2[ipld.Block, error], error) { br := bufio.NewReader(reader) h, err := ipldcar.ReadHeader(br) @@ -100,27 +100,42 @@ func Decode(reader io.Reader) ([]ipld.Link, iterable.Iterator[ipld.Block], error roots = append(roots, cidlink.Link{Cid: r}) } - return roots, iterable.NewIterator(func() (ipld.Block, error) { - cid, bytes, err := util.ReadNode(br) - if err != nil { + r := &blkReader{br, offset} + return roots, func(yield func(ipld.Block, error) bool) { + for { + blk, err := r.next() if err == io.EOF { - br = nil + return + } + if !yield(blk, err) { + return } - return nil, err } + }, nil +} - hashed, err := cid.Prefix().Sum(bytes) - if err != nil { - return nil, err - } +type blkReader struct { + br *bufio.Reader + offset uint64 +} - if !hashed.Equals(cid) { - return nil, fmt.Errorf("mismatch in content integrity, name: %s, data: %s", cid, hashed) - } +func (r *blkReader) next() (CarBlock, error) { + cid, bytes, err := util.ReadNode(r.br) + if err != nil { + return nil, err + } + + hashed, err := cid.Prefix().Sum(bytes) + if err != nil { + return nil, err + } + + if !hashed.Equals(cid) { + return nil, fmt.Errorf("mismatch in content integrity, name: %s, data: %s", cid, hashed) + } - ss := uint64(cid.ByteLen()) + uint64(len(bytes)) - offset += uint64(varint.UvarintSize(ss)) + ss + ss := uint64(cid.ByteLen()) + uint64(len(bytes)) + r.offset += uint64(varint.UvarintSize(ss)) + ss - return carBlock{block.NewBlock(cidlink.Link{Cid: cid}, bytes), offset - uint64(len(bytes)), uint64(len(bytes))}, nil - }), nil + return carBlock{block.NewBlock(cidlink.Link{Cid: cid}, bytes), r.offset - uint64(len(bytes)), uint64(len(bytes))}, nil } diff --git a/core/car/car_test.go b/core/car/car_test.go index f37cf70..080de0b 100644 --- a/core/car/car_test.go +++ b/core/car/car_test.go @@ -48,17 +48,13 @@ func TestDecodeCAR(t *testing.T) { } var blks []CarBlock - for { - b, err := blocks.Next() + for b, err := range blocks { if err != nil { - if err == io.EOF { - break - } t.Fatalf("reading blocks: %s", err) } cb, ok := b.(CarBlock) if !ok { - t.Fatalf("did not return a CarBlock") + t.Fatalf("should have returned a car block") } blks = append(blks, cb) } diff --git a/core/dag/blockstore/blockstore.go b/core/dag/blockstore/blockstore.go index eedab90..b058356 100644 --- a/core/dag/blockstore/blockstore.go +++ b/core/dag/blockstore/blockstore.go @@ -2,16 +2,15 @@ package blockstore import ( "fmt" - "io" + "iter" "sync" "github.com/storacha-network/go-ucanto/core/ipld" - "github.com/storacha-network/go-ucanto/core/iterable" ) type BlockReader interface { Get(link ipld.Link) (ipld.Block, bool, error) - Iterator() iterable.Iterator[ipld.Block] + Iterator() iter.Seq2[ipld.Block, error] } type BlockWriter interface { @@ -33,20 +32,19 @@ func (br *blockreader) Get(link ipld.Link) (ipld.Block, bool, error) { return b, ok, nil } -func (br *blockreader) Iterator() iterable.Iterator[ipld.Block] { - i := 0 - return iterable.NewIterator(func() (ipld.Block, error) { - if len(br.keys) <= i { - return nil, io.EOF - } - k := br.keys[i] - v, ok := br.blks[k] - if !ok { - return nil, fmt.Errorf("missing block for key: %s", k) +func (br *blockreader) Iterator() iter.Seq2[ipld.Block, error] { + return func(yield func(ipld.Block, error) bool) { + for _, k := range br.keys { + v, ok := br.blks[k] + var err error + if !ok { + err = fmt.Errorf("missing block for key: %s", k) + } + if !yield(v, err) { + return + } } - i++ - return v, nil - }) + } } type blockstore struct { @@ -75,23 +73,21 @@ func (bs *blockstore) Get(link ipld.Link) (ipld.Block, bool, error) { return bs.blockreader.Get(link) } -func (bs *blockstore) Iterator() iterable.Iterator[ipld.Block] { +func (bs *blockstore) Iterator() iter.Seq2[ipld.Block, error] { bs.Lock() defer bs.Unlock() - keys := bs.keys[:] - i := 0 - return iterable.NewIterator(func() (ipld.Block, error) { - if len(keys) <= i { - return nil, io.EOF - } - k := keys[i] - v, ok := bs.blks[k] - if !ok { - return nil, fmt.Errorf("missing block for key: %s", k) + return func(yield func(ipld.Block, error) bool) { + for _, k := range bs.keys { + v, ok := bs.blks[k] + var err error + if !ok { + err = fmt.Errorf("missing block for key: %s", k) + } + if !yield(v, err) { + return + } } - i++ - return v, nil - }) + } } // Option is an option configuring a block reader/writer. @@ -99,7 +95,7 @@ type Option func(cfg *bsConfig) error type bsConfig struct { blks []ipld.Block - blksiter iterable.Iterator[ipld.Block] + blksiter iter.Seq2[ipld.Block, error] } // WithBlocks configures the blocks the blockstore should contain. @@ -111,7 +107,7 @@ func WithBlocks(blks []ipld.Block) Option { } // WithBlocksIterator configures the blocks the blockstore should contain. -func WithBlocksIterator(blks iterable.Iterator[ipld.Block]) Option { +func WithBlocksIterator(blks iter.Seq2[ipld.Block, error]) Option { return func(cfg *bsConfig) error { cfg.blksiter = blks return nil @@ -138,15 +134,11 @@ func NewBlockStore(options ...Option) (BlockStore, error) { } } if cfg.blksiter != nil { - for { - b, err := cfg.blksiter.Next() + for b, err := range cfg.blksiter { if err != nil { - if err == io.EOF { - break - } return nil, err } - err = bs.Put(b) + err := bs.Put(b) if err != nil { return nil, err } @@ -175,12 +167,8 @@ func NewBlockReader(options ...Option) (BlockReader, error) { keys = append(keys, b.Link().String()) } if cfg.blksiter != nil { - for { - b, err := cfg.blksiter.Next() + for b, err := range cfg.blksiter { if err != nil { - if err == io.EOF { - break - } return nil, err } _, ok := blks[b.Link().String()] @@ -196,16 +184,8 @@ func NewBlockReader(options ...Option) (BlockReader, error) { } func WriteInto(view ipld.View, bs BlockWriter) error { - blks := view.Blocks() - for { - b, err := blks.Next() - if err != nil { - if err == io.EOF { - break - } - return fmt.Errorf("reading proof blocks: %s", err) - } - err = bs.Put(b) + for b := range view.Blocks() { + err := bs.Put(b) if err != nil { return fmt.Errorf("putting proof block: %s", err) } diff --git a/core/delegation/delegation.go b/core/delegation/delegation.go index 7429c57..b8310de 100644 --- a/core/delegation/delegation.go +++ b/core/delegation/delegation.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "iter" "sync" "github.com/storacha-network/go-ucanto/core/car" @@ -13,7 +14,6 @@ import ( "github.com/storacha-network/go-ucanto/core/ipld/block" "github.com/storacha-network/go-ucanto/core/ipld/codec/cbor" "github.com/storacha-network/go-ucanto/core/ipld/hash/sha256" - "github.com/storacha-network/go-ucanto/core/iterable" "github.com/storacha-network/go-ucanto/ucan" "github.com/storacha-network/go-ucanto/ucan/crypto/signature" udm "github.com/storacha-network/go-ucanto/ucan/datamodel/ucan" @@ -64,7 +64,7 @@ func (d *delegation) Link() ucan.Link { return d.rt.Link() } -func (d *delegation) Blocks() iterable.Iterator[ipld.Block] { +func (d *delegation) Blocks() iter.Seq2[ipld.Block, error] { return d.blks.Iterator() } diff --git a/core/ipld/view.go b/core/ipld/view.go index 814a7c2..135fb59 100644 --- a/core/ipld/view.go +++ b/core/ipld/view.go @@ -1,7 +1,7 @@ package ipld import ( - "github.com/storacha-network/go-ucanto/core/iterable" + "iter" ) // View represents a materialized IPLD DAG View, which provides a generic @@ -19,7 +19,7 @@ type View interface { // // Iterator MUST include the root block otherwise it will lead encoders into // omitting it when encoding the view into a CAR archive. - Blocks() iterable.Iterator[Block] + Blocks() iter.Seq2[Block, error] } // ViewBuilder represents a materializable IPLD DAG View. It is a useful diff --git a/core/iterable/iterable.go b/core/iterable/iterable.go index b590ac1..f8183ac 100644 --- a/core/iterable/iterable.go +++ b/core/iterable/iterable.go @@ -1,88 +1,55 @@ package iterable import ( - "io" + "iter" ) -// Iterator returns items in a collection with every call to Next(). -// The error will be set to io.EOF when the iterator is complete. -type Iterator[T any] interface { - Next() (T, error) -} - -type iterator[T any] struct { - next func() (T, error) -} - -func (it *iterator[T]) Next() (T, error) { - return it.next() -} - -func NewIterator[T any](next func() (T, error)) Iterator[T] { - return &iterator[T]{next} -} +// TODO: remove when https://github.com/golang/go/issues/61898 lands -func From[Slice ~[]T, T any](slice Slice) Iterator[T] { - i := 0 - return NewIterator(func() (T, error) { - if i < len(slice) { - item := slice[i] - i++ - return item, nil +// Map returns an iterator over f applied to seq. +func Map[In, Out any](f func(In) Out, seq iter.Seq[In]) iter.Seq[Out] { + return func(yield func(Out) bool) { + for in := range seq { + if !yield(f(in)) { + return + } } - var undef T - return undef, io.EOF - }) + } } -func Collect[T any](it Iterator[T]) ([]T, error) { - var items []T - for { - item, err := it.Next() - if err != nil { - if err == io.EOF { - break +// Map2 returns an iterator over f applied to seq. +func Map2[KIn, VIn, KOut, VOut any](f func(KIn, VIn) (KOut, VOut), seq iter.Seq2[KIn, VIn]) iter.Seq2[KOut, VOut] { + return func(yield func(KOut, VOut) bool) { + for k, v := range seq { + if !yield(f(k, v)) { + return } - return nil, err } - items = append(items, item) } - return items, nil } -func Map[T, U any](it Iterator[T], mapFn func(T) U) Iterator[U] { - return NewIterator(func() (U, error) { - t, err := it.Next() - if err != nil { - var undef U - return undef, err +// Concat returns an iterator over the concatenation of the sequences. +func Concat[V any](seqs ...iter.Seq[V]) iter.Seq[V] { + return func(yield func(V) bool) { + for _, seq := range seqs { + for e := range seq { + if !yield(e) { + return + } + } } - return mapFn(t), nil - }) -} - -func Concat[T any](iterators ...Iterator[T]) Iterator[T] { - if len(iterators) == 0 { - return From([]T{}) } +} - i := 0 - iterator := iterators[i] - return NewIterator(func() (T, error) { - for { - item, err := iterator.Next() - if err != nil { - if err == io.EOF { - i++ - if i < len(iterators) { - iterator = iterators[i] - continue - } +// Concat2 returns an iterator over the concatenation of the sequences. +func Concat2[K, V any](seqs ...iter.Seq2[K, V]) iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + for _, seq := range seqs { + for k, v := range seq { + if !yield(k, v) { + return } - var undef T - return undef, err } - return item, nil } - }) + } } diff --git a/core/iterable/mapiterable.go b/core/iterable/mapiterable.go deleted file mode 100644 index b4b5bcd..0000000 --- a/core/iterable/mapiterable.go +++ /dev/null @@ -1,91 +0,0 @@ -package iterable - -import "io" - -// Iterator2 returns two values with every call to Next(). -// The error will be set to io.EOF when the iterator is complete. -type Iterator2[K any, V any] interface { - Next() (K, V, error) -} - -type iterator2[K any, V any] struct { - next func() (K, V, error) -} - -func (mit *iterator2[K, V]) Next() (K, V, error) { - return mit.next() -} - -func NewIterator2[K any, V any](next func() (K, V, error)) Iterator2[K, V] { - return &iterator2[K, V]{next} -} - -type mapEntry[K comparable, V any] struct { - k K - v V -} - -func FromMap[Map ~map[K]V, K comparable, V any](m Map) Iterator2[K, V] { - entries := make([]mapEntry[K, V], 0, len(m)) - for k, v := range m { - entries = append(entries, mapEntry[K, V]{k, v}) - } - i := 0 - return NewIterator2(func() (K, V, error) { - if i < len(entries) { - k := entries[i].k - v := entries[i].v - i++ - return k, v, nil - } - var k K - var v V - return k, v, io.EOF - }) -} - -func CollectMap[K comparable, V any](mit Iterator2[K, V]) (map[K]V, error) { - items := make(map[K]V) - for { - k, v, err := mit.Next() - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - items[k] = v - } - return items, nil -} - -func Concat2[K any, V any](iterators ...Iterator2[K, V]) Iterator2[K, V] { - if len(iterators) == 0 { - return NewIterator2(func() (K, V, error) { - var k K - var v V - return k, v, io.EOF - }) - } - - i := 0 - iterator := iterators[i] - return NewIterator2(func() (K, V, error) { - for { - k, v, err := iterator.Next() - if err != nil { - if err == io.EOF { - i++ - if i < len(iterators) { - iterator = iterators[i] - continue - } - } - var k K - var v V - return k, v, err - } - return k, v, nil - } - }) -} diff --git a/core/iterable/mapiterable_test.go b/core/iterable/mapiterable_test.go deleted file mode 100644 index 2ff157b..0000000 --- a/core/iterable/mapiterable_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package iterable_test - -import ( - "errors" - "fmt" - "io" - "testing" - - "github.com/storacha-network/go-ucanto/core/iterable" - "github.com/stretchr/testify/require" -) - -func TestCollectMap(t *testing.T) { - someErr := errors.New("some error") - testCases := []struct { - name string - iterator2 func() iterable.Iterator2[string, int] - expectedMap map[string]int - expectedErr error - }{ - { - name: "converts successful iterator to expected map", - iterator2: func() iterable.Iterator2[string, int] { - count := 0 - return iterable.NewIterator2(func() (string, int, error) { - defer func() { - count++ - }() - switch count { - case 0: - return "apples", 7, nil - case 1: - return "oranges", 4, nil - case 2: - return "", 0, io.EOF - default: - return "", 0, fmt.Errorf("too many calls to iterator: %d", count+1) - } - }) - }, - expectedMap: map[string]int{"apples": 7, "oranges": 4}, - }, - { - name: "fails when iterator fails", - iterator2: func() iterable.Iterator2[string, int] { - count := 0 - return iterable.NewIterator2(func() (string, int, error) { - defer func() { - count++ - }() - switch count { - case 0: - return "apples", 7, nil - default: - return "", 0, fmt.Errorf("mistake iterating: %w", someErr) - } - }) - }, - expectedMap: nil, - expectedErr: someErr, - }, - } - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - resultMap, err := iterable.CollectMap(testCase.iterator2()) - require.Equal(t, testCase.expectedMap, resultMap) - require.ErrorIs(t, err, testCase.expectedErr) - }) - } -} - -func TestFromMap(t *testing.T) { - verifyFromMap(t, "string -> int", map[string]int{"apples": 7, "oranges": 4}) - verifyFromMap(t, "int -> bool", map[int]bool{7: true, 4: false, 3: true}) - verifyFromMap(t, "any -> any", map[any]any{ - 7: true, - 4: "apples", - "oranges": struct{ head string }{head: "bucket"}, - }) -} - -func TestRoundtrip(t *testing.T) { - roundTrip(t, "string -> int", map[string]int{"apples": 7, "oranges": 4}) - roundTrip(t, "int -> bool", map[int]bool{7: true, 4: false, 3: true}) - roundTrip(t, "any -> any", map[any]any{ - 7: true, - 4: "apples", - "oranges": struct{ head string }{head: "bucket"}, - }) -} - -func verifyFromMap[K comparable, V any](t *testing.T, testCase string, inputMap map[K]V) { - t.Run(testCase, func(t *testing.T) { - iterator := iterable.FromMap(inputMap) - outputMap := make(map[K]V, len(inputMap)) - for { - k, v, err := iterator.Next() - if err != nil { - require.ErrorIs(t, err, io.EOF) - require.Equal(t, inputMap, outputMap) - return - } - outputMap[k] = v - } - }) -} - -func roundTrip[K comparable, V any](t *testing.T, testCase string, inputMap map[K]V) { - t.Run(testCase, func(t *testing.T) { - outputMap, err := iterable.CollectMap(iterable.FromMap(inputMap)) - require.NoError(t, err) - require.Equal(t, inputMap, outputMap) - }) -} diff --git a/core/message/message.go b/core/message/message.go index 5f7d85c..74d0d2a 100644 --- a/core/message/message.go +++ b/core/message/message.go @@ -2,6 +2,7 @@ package message import ( "fmt" + "iter" "github.com/storacha-network/go-ucanto/core/dag/blockstore" "github.com/storacha-network/go-ucanto/core/invocation" @@ -9,7 +10,6 @@ import ( "github.com/storacha-network/go-ucanto/core/ipld/block" "github.com/storacha-network/go-ucanto/core/ipld/codec/cbor" "github.com/storacha-network/go-ucanto/core/ipld/hash/sha256" - "github.com/storacha-network/go-ucanto/core/iterable" mdm "github.com/storacha-network/go-ucanto/core/message/datamodel" "github.com/storacha-network/go-ucanto/core/receipt" ) @@ -38,7 +38,7 @@ func (m *message) Root() ipld.Block { return m.root } -func (m *message) Blocks() iterable.Iterator[ipld.Block] { +func (m *message) Blocks() iter.Seq2[ipld.Block, error] { return m.blks.Iterator() } diff --git a/core/receipt/receipt.go b/core/receipt/receipt.go index 83cdc88..a4a95bf 100644 --- a/core/receipt/receipt.go +++ b/core/receipt/receipt.go @@ -4,6 +4,7 @@ import ( // for go:embed _ "embed" "fmt" + "iter" "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/schema" @@ -77,8 +78,8 @@ type receipt[O, X any] struct { var _ Receipt[any, any] = (*receipt[any, any])(nil) -func (r *receipt[O, X]) Blocks() iterable.Iterator[block.Block] { - var iterators []iterable.Iterator[block.Block] +func (r *receipt[O, X]) Blocks() iter.Seq2[block.Block, error] { + var iterators []iter.Seq2[block.Block, error] iterators = append(iterators, r.Ran().Blocks()) for _, prf := range r.Proofs() { @@ -87,9 +88,9 @@ func (r *receipt[O, X]) Blocks() iterable.Iterator[block.Block] { } } - iterators = append(iterators, iterable.From([]block.Block{r.Root()})) + iterators = append(iterators, func(yield func(block.Block, error) bool) { yield(r.Root(), nil) }) - return iterable.Concat(iterators...) + return iterable.Concat2(iterators...) } func (r *receipt[O, X]) Fx() Effects { @@ -165,14 +166,14 @@ func NewReceipt[O, X any](root ipld.Link, blocks blockstore.BlockReader, typ sch } type ReceiptReader[O, X any] interface { - Read(rcpt ipld.Link, blks iterable.Iterator[block.Block]) (Receipt[O, X], error) + Read(rcpt ipld.Link, blks iter.Seq2[block.Block, error]) (Receipt[O, X], error) } type receiptReader[O, X any] struct { typ schema.Type } -func (rr *receiptReader[O, X]) Read(rcpt ipld.Link, blks iterable.Iterator[block.Block]) (Receipt[O, X], error) { +func (rr *receiptReader[O, X]) Read(rcpt ipld.Link, blks iter.Seq2[block.Block, error]) (Receipt[O, X], error) { br, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(blks)) if err != nil { return nil, fmt.Errorf("creating block reader: %s", err) diff --git a/go.mod b/go.mod index 0cb5165..12eddcc 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/storacha-network/go-ucanto -go 1.21 +go 1.23 require ( github.com/ipfs/go-cid v0.4.1 From 525e4eb90f18258f829495d5011c9bafc7c923d6 Mon Sep 17 00:00:00 2001 From: hannahhoward Date: Sat, 14 Sep 2024 18:56:49 +0200 Subject: [PATCH 30/37] feat(schema): add mapped reader add mapped reader for doing additional conversion on schemas --- core/schema/mapped.go | 21 ++++++++++ core/schema/mapped_test.go | 81 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 core/schema/mapped.go create mode 100644 core/schema/mapped_test.go diff --git a/core/schema/mapped.go b/core/schema/mapped.go new file mode 100644 index 0000000..abdff7a --- /dev/null +++ b/core/schema/mapped.go @@ -0,0 +1,21 @@ +package schema + +import "github.com/storacha-network/go-ucanto/core/result/failure" + +type mapped[I, O, O2 any] struct { + reader Reader[I, O] + converter func(O) (O2, failure.Failure) +} + +func (m mapped[I, O, O2]) Read(i I) (O2, failure.Failure) { + o, err := m.reader.Read(i) + if err != nil { + var o2 O2 + return o2, err + } + return m.converter(o) +} + +func Mapped[I, O, O2 any](reader Reader[I, O], converter func(O) (O2, failure.Failure)) Reader[I, O2] { + return mapped[I, O, O2]{reader, converter} +} diff --git a/core/schema/mapped_test.go b/core/schema/mapped_test.go new file mode 100644 index 0000000..153dff1 --- /dev/null +++ b/core/schema/mapped_test.go @@ -0,0 +1,81 @@ +package schema_test + +import ( + "fmt" + "net/url" + "testing" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/storacha-network/go-ucanto/core/result/failure" + "github.com/storacha-network/go-ucanto/core/schema" + "github.com/storacha-network/go-ucanto/testing/helpers" + "github.com/stretchr/testify/require" +) + +func TestReadStruct(t *testing.T) { + type TestStruct struct { + Url string + } + + ts := helpers.Must(ipld.LoadSchemaBytes([]byte(` + type TestStruct struct { + url String + } + `))) + + type URLStruct struct { + Url url.URL + } + + converter := func(ts TestStruct) (URLStruct, failure.Failure) { + url, err := url.Parse(ts.Url) + if err != nil { + return URLStruct{}, failure.FromError(err) + } + return URLStruct{Url: *url}, nil + } + + t.Run("Success", func(t *testing.T) { + np := basicnode.Prototype.Any + nb := np.NewBuilder() + ma := helpers.Must(nb.BeginMap(2)) + ma.AssembleKey().AssignString("url") + ma.AssembleValue().AssignString("http://www.yahoo.com") + ma.Finish() + nd := nb.Build() + + res, err := schema.Mapped(schema.Struct[TestStruct](ts.TypeByName("TestStruct"), nil), converter).Read(nd) + require.NoError(t, err) + fmt.Printf("%+v\n", res) + require.Equal(t, res.Url.Host, "www.yahoo.com") + }) + + t.Run("Failure, underlying reader", func(t *testing.T) { + np := basicnode.Prototype.Any + nb := np.NewBuilder() + ma := helpers.Must(nb.BeginMap(2)) + ma.AssembleKey().AssignString("foo") + ma.AssembleValue().AssignString("bar") + ma.Finish() + nd := nb.Build() + + _, err := schema.Mapped(schema.Struct[TestStruct](ts.TypeByName("TestStruct"), nil), converter).Read(nd) + require.Error(t, err) + fmt.Printf("%+v\n", err) + require.Equal(t, err.Name(), "SchemaError") + }) + t.Run("Failure, conversion", func(t *testing.T) { + np := basicnode.Prototype.Any + nb := np.NewBuilder() + ma := helpers.Must(nb.BeginMap(2)) + ma.AssembleKey().AssignString("url") + ma.AssembleValue().AssignString(":apple") + ma.Finish() + nd := nb.Build() + + _, err := schema.Mapped(schema.Struct[TestStruct](ts.TypeByName("TestStruct"), nil), converter).Read(nd) + require.Error(t, err) + fmt.Printf("%+v\n", err) + }) +} From 3bed7025597b710ef862e298b5ecb7f1dd175862 Mon Sep 17 00:00:00 2001 From: hannahhoward Date: Sun, 15 Sep 2024 23:26:05 -0700 Subject: [PATCH 31/37] feat(schema): add link, URI, and or schema readers --- core/schema/link.go | 129 +++++++++++++++++++++++++++++++++++++++ core/schema/link_test.go | 110 +++++++++++++++++++++++++++++++++ core/schema/or.go | 60 ++++++++++++++++++ core/schema/uri.go | 51 ++++++++++++++++ core/schema/uri_test.go | 78 +++++++++++++++++++++++ 5 files changed, 428 insertions(+) create mode 100644 core/schema/link.go create mode 100644 core/schema/link_test.go create mode 100644 core/schema/or.go create mode 100644 core/schema/uri.go create mode 100644 core/schema/uri_test.go diff --git a/core/schema/link.go b/core/schema/link.go new file mode 100644 index 0000000..8d84437 --- /dev/null +++ b/core/schema/link.go @@ -0,0 +1,129 @@ +package schema + +import ( + "bytes" + "fmt" + + cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/multiformats/go-base32" + mh "github.com/multiformats/go-multihash" + "github.com/storacha-network/go-ucanto/core/ipld" + "github.com/storacha-network/go-ucanto/core/result/failure" +) + +type linkReader struct { + lc *linkCfg +} + +func (lr linkReader) Read(input any) (ipld.Link, failure.Failure) { + link, asLink := input.(ipld.Link) + if !asLink { + node, asNode := input.(ipld.Node) + if !asNode { + // If input is not an IPLD node, can it be converted to one? + if builder, ok := input.(ipld.Builder); ok { + n, err := builder.Build() + if err != nil { + return nil, NewSchemaError(err.Error()) + } + node = n + } else { + return nil, NewSchemaError("unexpected input: not an IPLD node or link") + } + } + var err error + link, err = node.AsLink() + if err != nil { + return nil, NewSchemaError(err.Error()) + } + } + + cidLink, ok := link.(cidlink.Link) + if !ok { + return nil, NewSchemaError("Unsupported Link Type") + } + cid := cidLink.Cid + if lr.lc.codec != nil && cid.Prefix().Codec != *lr.lc.codec { + return nil, NewSchemaError(fmt.Sprintf("Expected link to be CID with %X codec", *lr.lc.codec)) + } + + if lr.lc.version != nil && cid.Prefix().Version != *lr.lc.version { + return nil, NewSchemaError(fmt.Sprintf( + "Expected link to be CID version %d instead of %d", *lr.lc.version, cid.Prefix().Version)) + } + + if lr.lc.multihash != nil { + multihash := lr.lc.multihash + if multihash.code != nil && cid.Prefix().MhType != *multihash.code { + return nil, NewSchemaError(fmt.Sprintf("Expected link to be CID with %X hashing algorithm", *&multihash.code)) + } + if multihash.digest != nil { + decoded, err := mh.Decode(cid.Hash()) + if err != nil { + return nil, NewSchemaError(err.Error()) + } + + if bytes.Compare(decoded.Digest, *multihash.digest) != 0 { + return nil, NewSchemaError(fmt.Sprintf("Expected link with %s hash digest instead of %s", base32.StdEncoding.EncodeToString(*multihash.digest), base32.StdEncoding.EncodeToString(decoded.Digest))) + } + } + } + return link, nil +} + +type multihashConfig struct { + code *uint64 + digest *[]byte +} + +type MultihashOption func(*multihashConfig) + +func WithAlg(code uint64) MultihashOption { + return func(mc *multihashConfig) { + mc.code = &code + } +} + +func WithDigest(digest []byte) MultihashOption { + return func(mc *multihashConfig) { + mc.digest = &digest + } +} + +type linkCfg struct { + version *uint64 + codec *uint64 + multihash *multihashConfig +} + +type LinkOption func(*linkCfg) + +func WithVersion(version uint64) LinkOption { + return func(lc *linkCfg) { + lc.version = &version + } +} + +func WithCodec(codec uint64) LinkOption { + return func(lc *linkCfg) { + lc.codec = &codec + } +} + +func WithMultihashConfig(opts ...MultihashOption) LinkOption { + return func(lc *linkCfg) { + mc := &multihashConfig{} + for _, opt := range opts { + opt(mc) + } + lc.multihash = mc + } +} + +func Link(opts ...LinkOption) Reader[any, ipld.Link] { + lc := &linkCfg{} + for _, opt := range opts { + opt(lc) + } + return linkReader{lc} +} diff --git a/core/schema/link_test.go b/core/schema/link_test.go new file mode 100644 index 0000000..e9ad3b5 --- /dev/null +++ b/core/schema/link_test.go @@ -0,0 +1,110 @@ +package schema_test + +import ( + "fmt" + "iter" + "maps" + "regexp" + "testing" + + "github.com/ipfs/go-cid" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" + basicnode "github.com/ipld/go-ipld-prime/node/basic" + "github.com/multiformats/go-base32" + mh "github.com/multiformats/go-multihash" + "github.com/storacha-network/go-ucanto/core/iterable" + "github.com/storacha-network/go-ucanto/core/schema" + "github.com/storacha-network/go-ucanto/testing/helpers" + "github.com/stretchr/testify/require" +) + +func NewSet[T comparable](list iter.Seq[T]) iter.Seq[T] { + set := make(map[T]struct{}) + for elem := range list { + set[elem] = struct{}{} + } + return maps.Keys(set) +} +func TestReadLink(t *testing.T) { + fixtures := map[string]cid.Cid{ + "pb": cid.MustParse("QmTgnQBKj7eTV7ohraBCmh1DLwerUd2X9Rxzgf3gyMJbC8"), + "cbor": cid.MustParse("bafyreieuo63r3y2nuycaq4b3q2xvco3nprlxiwzcfp4cuupgaywat3z6mq"), + "rawIdentity": cid.MustParse("bafkqaaa"), + "ipns": cid.MustParse("k2k4r8kuj2bs2l996lhjx8rc727xlvthtak8o6eia3qm5adxvs5k84gf"), + "sha512": cid.MustParse("kgbuwaen1jrbjip6iwe9mqg54spvuucyz7f5jho2tkc2o0c7xzqwpxtogbyrwck57s9is6zqlwt9rsxbuvszym10nbaxt9jn7sf4eksqd"), + } + + links := maps.Values(fixtures) + versions := NewSet(iterable.Map(func(c cid.Cid) uint64 { return c.Version() }, maps.Values(fixtures))) + codecs := NewSet(iterable.Map(func(c cid.Cid) uint64 { return c.Prefix().Codec }, maps.Values(fixtures))) + algs := NewSet(iterable.Map(func(c cid.Cid) uint64 { return c.Prefix().MhType }, maps.Values(fixtures))) + digests := NewSet(iterable.Map(func(c cid.Cid) string { + h := c.Hash() + dh := helpers.Must(mh.Decode(h)) + return string(dh.Digest) + }, maps.Values(fixtures))) + + for link := range links { + t.Run(fmt.Sprintf("%s ➡ schema.Link()", link), func(t *testing.T) { + output, err := schema.Link().Read(basicnode.NewLink(cidlink.Link{Cid: link})) + require.NoError(t, err) + require.Equal(t, output, cidlink.Link{Cid: link}, link.String()) + }) + + for version := range versions { + t.Run(fmt.Sprintf("%s ➡ schema.Link(WithVersion(%d))", link, version), func(t *testing.T) { + reader := schema.Link(schema.WithVersion(version)) + output, err := reader.Read(basicnode.NewLink(cidlink.Link{Cid: link})) + if link.Version() == version { + require.NoError(t, err) + require.Equal(t, output, cidlink.Link{Cid: link}) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), "Expected link to be CID version") + } + }) + } + + for codec := range codecs { + t.Run(fmt.Sprintf("%s ➡ schema.Link(WithCodec(%d))", link, codec), func(t *testing.T) { + reader := schema.Link(schema.WithCodec(codec)) + output, err := reader.Read(basicnode.NewLink(cidlink.Link{Cid: link})) + if link.Prefix().Codec == codec { + require.NoError(t, err) + require.Equal(t, output, cidlink.Link{Cid: link}) + } else { + require.Error(t, err) + require.Regexp(t, helpers.Must(regexp.Compile("Expected link to be CID with .* code")), err.Error()) + } + }) + } + + for alg := range algs { + t.Run(fmt.Sprintf("%s ➡ schema.Link(WithMultihashConfig(WithAlg(%d)))", link, alg), func(t *testing.T) { + reader := schema.Link(schema.WithMultihashConfig(schema.WithAlg(alg))) + output, err := reader.Read(basicnode.NewLink(cidlink.Link{Cid: link})) + if link.Prefix().MhType == alg { + require.NoError(t, err) + require.Equal(t, output, cidlink.Link{Cid: link}) + } else { + require.Error(t, err) + require.Regexp(t, helpers.Must(regexp.Compile("Expected link to be CID with .* hashing algorithm")), err.Error()) + } + }) + } + + for digest := range digests { + t.Run(fmt.Sprintf("%s ➡ schema.Link(WithMultihashConfig(WithDigest(%s)))", link, base32.StdEncoding.EncodeToString([]byte(digest))), func(t *testing.T) { + reader := schema.Link(schema.WithMultihashConfig(schema.WithDigest([]byte(digest)))) + output, err := reader.Read(basicnode.NewLink(cidlink.Link{Cid: link})) + if string(helpers.Must(mh.Decode(link.Hash())).Digest) == digest { + require.NoError(t, err) + require.Equal(t, output, cidlink.Link{Cid: link}) + } else { + require.Error(t, err) + require.Regexp(t, helpers.Must(regexp.Compile("Expected link with .* hash digest")), err.Error()) + } + }) + } + } +} diff --git a/core/schema/or.go b/core/schema/or.go new file mode 100644 index 0000000..8d837dd --- /dev/null +++ b/core/schema/or.go @@ -0,0 +1,60 @@ +package schema + +import ( + "errors" + "fmt" + "strings" + + "github.com/storacha-network/go-ucanto/core/result/failure" +) + +type unionError struct { + failures []failure.Failure +} + +func (ue unionError) Unwrap() []error { + errors := make([]error, 0, len(ue.failures)) + for _, failure := range ue.failures { + errors = append(errors, failure) + } + return errors +} + +func indent(message string) string { + indent := " " + return indent + strings.Join(strings.Split(message, "\n"), "\n"+indent) +} + +func li(message string) string { + return indent("- " + message) +} + +func (ue unionError) Error() string { + return fmt.Sprintf("Value does not match any type of the union:\n%s", li(errors.Join(ue.Unwrap()...).Error())) +} + +func (ue unionError) Name() string { + return "Union Error" +} + +type orReader[I, O any] struct { + readers []Reader[I, O] +} + +func (or orReader[I, O]) Read(input I) (O, failure.Failure) { + failures := make([]failure.Failure, 0, len(or.readers)) + for _, reader := range or.readers { + o, err := reader.Read(input) + if err != nil { + failures = append(failures, err) + } else { + return o, nil + } + } + var o O + return o, failure.FromError(unionError{failures: failures}) +} + +func Or[I, O any](readers ...Reader[I, O]) Reader[I, O] { + return orReader[I, O]{readers} +} diff --git a/core/schema/uri.go b/core/schema/uri.go new file mode 100644 index 0000000..ed65e40 --- /dev/null +++ b/core/schema/uri.go @@ -0,0 +1,51 @@ +package schema + +import ( + "fmt" + "net/url" + + "github.com/storacha-network/go-ucanto/core/result/failure" +) + +type uriConfig struct { + protocol *string +} + +type uriReader struct { + uc *uriConfig +} + +type URIOption func(*uriConfig) + +func WithProtocol(protocol string) URIOption { + return func(uc *uriConfig) { + uc.protocol = &protocol + } +} + +func (ur uriReader) Read(input any) (url.URL, failure.Failure) { + asString, stringOk := input.(string) + asUrl, urlOk := input.(url.URL) + if !stringOk && !urlOk { + return url.URL{}, NewSchemaError(fmt.Sprintf("Expected URI but got %T", input)) + } + if !urlOk { + u, err := url.ParseRequestURI(asString) + if err != nil { + return url.URL{}, NewSchemaError("Invalid URI") + } + asUrl = *u + } + if ur.uc.protocol != nil && *ur.uc.protocol != asUrl.Scheme+":" { + return url.URL{}, NewSchemaError(fmt.Sprintf("Expected %s URI instead got %s", *ur.uc.protocol, asUrl.String())) + } + return asUrl, nil +} + +func URI(opts ...URIOption) Reader[any, url.URL] { + uc := &uriConfig{} + for _, opt := range opts { + opt(uc) + } + return uriReader{uc} +} diff --git a/core/schema/uri_test.go b/core/schema/uri_test.go new file mode 100644 index 0000000..35407ee --- /dev/null +++ b/core/schema/uri_test.go @@ -0,0 +1,78 @@ +package schema_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/storacha-network/go-ucanto/core/schema" + "github.com/stretchr/testify/require" +) + +func TestURIDefaultReader(t *testing.T) { + testCases := []struct { + source string + output string + errMatch *regexp.Regexp + }{ + { + source: "", + errMatch: regexp.MustCompile("Invalid URI"), + }, + { + source: "did:key:zAlice", + output: "did:key:zAlice", + }, + { + source: "mailto:alice@mail.net", + output: "mailto:alice@mail.net", + }, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("schema.URI.read(%s)", testCase.source), func(t *testing.T) { + output, err := schema.URI().Read(testCase.source) + if testCase.errMatch == nil { + require.NoError(t, err) + require.Equal(t, testCase.output, output.String()) + } else { + require.Regexp(t, testCase.errMatch, err.Error()) + } + }) + } +} + +func TestURIReaderWithProtocol(t *testing.T) { + testCases := []struct { + source any + protocol string + output string + errMatch *regexp.Regexp + }{ + {nil, "did:", "", regexp.MustCompile("Expected URI but got ")}, + {"", "did:", "", regexp.MustCompile("Invalid URI")}, + {"did:key:zAlice", "did:", "did:key:zAlice", nil}, + {"did:key:zAlice", "mailto:", "", regexp.MustCompile("Expected mailto: URI instead got did:key:zAlice")}, + {"mailto:alice@mail.net", "mailto:", "mailto:alice@mail.net", nil}, + {"mailto:alice@mail.net", "did:", "", regexp.MustCompile("Expected did: URI instead got mailto:alice@mail.net")}, + } + + for _, testCase := range testCases { + t.Run(fmt.Sprintf("schema.URI(WithProtocol(%s)).read(%s)", testCase.protocol, testCase.source), func(t *testing.T) { + output, err := schema.URI(schema.WithProtocol((testCase.protocol))).Read(testCase.source) + if testCase.errMatch == nil { + require.NoError(t, err) + require.Equal(t, testCase.output, output.String()) + } else { + require.Regexp(t, testCase.errMatch, err.Error()) + } + }) + } + // for (const [input, protocol, expect] of dataset) { + // test(`URI.match(${JSON.stringify({ + // protocol, + // })}).read(${JSON.stringify(input)})}}`, () => { + // matchResult(URI.match({ protocol }).read(input), expect) + // }) + // } +} From c9a9184a6e84a10688e1ef6c6606e8502fc50372 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 16 Sep 2024 13:08:07 +0100 Subject: [PATCH 32/37] feat: delegate and invoke from validator capability --- validator/capability.go | 22 +++++++ validator/lib_test.go | 141 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) diff --git a/validator/capability.go b/validator/capability.go index 366b1d1..a33b034 100644 --- a/validator/capability.go +++ b/validator/capability.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/storacha-network/go-ucanto/core/delegation" + "github.com/storacha-network/go-ucanto/core/invocation" "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/ucan" @@ -113,6 +114,11 @@ type CapabilityParser[Caveats any] interface { Can() ucan.Ability // New creates a new capability from the passed options. New(with ucan.Resource, nb Caveats) ucan.Capability[Caveats] + // Delegate creates a new signed token for this capability. If expiration is + // not set it defaults to 30 seconds from now. + Delegate(issuer ucan.Signer, audience ucan.Principal, with ucan.Resource, nb Caveats, options ...delegation.Option) (delegation.Delegation, error) + // Invoke creates an invocation of this capability. + Invoke(issuer ucan.Signer, audience ucan.Principal, with ucan.Resource, nb Caveats, options ...delegation.Option) (invocation.IssuedInvocation, error) } type Derivable[Caveats any] interface { @@ -185,6 +191,22 @@ func (c capability[Caveats]) New(with ucan.Resource, nb Caveats) ucan.Capability return ucan.NewCapability(c.descriptor.Can(), with, nb) } +func (c capability[Caveats]) Delegate(issuer ucan.Signer, audience ucan.Principal, with ucan.Resource, nb Caveats, options ...delegation.Option) (delegation.Delegation, error) { + if bc, ok := any(nb).(ucan.CaveatBuilder); ok { + caps := []ucan.Capability[ucan.CaveatBuilder]{ucan.NewCapability(c.Can(), with, bc)} + return delegation.Delegate(issuer, audience, caps, options...) + } + return nil, fmt.Errorf("not an IPLD builder: %v", nb) +} + +func (c capability[Caveats]) Invoke(issuer ucan.Signer, audience ucan.Principal, with ucan.Resource, nb Caveats, options ...delegation.Option) (invocation.IssuedInvocation, error) { + if bc, ok := any(nb).(ucan.CaveatBuilder); ok { + cap := ucan.NewCapability(c.Can(), with, bc) + return invocation.Invoke(issuer, audience, cap, options...) + } + return nil, fmt.Errorf("not an IPLD builder: %v", nb) +} + func NewCapability[Caveats any]( can ucan.Ability, with schema.Reader[string, ucan.Resource], diff --git a/validator/lib_test.go b/validator/lib_test.go index 717d5a8..cec5c75 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -687,6 +687,147 @@ func TestAccess(t *testing.T) { }, "\n") require.Equal(t, msg, x.Error()) }) + + t.Run("invalid audience", func(t *testing.T) { + dlg, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Alice.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + inv, err := storeAdd.Invoke( + fixtures.Mallory, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Mallory.DID()), + fmt.Sprintf(` - Capability can not be derived from prf: %s because:`, dlg.Link()), + fmt.Sprintf(` - Delegation audience is '%s' instead of '%s'`, fixtures.Bob.DID(), fixtures.Mallory.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("invalid claim", func(t *testing.T) { + dlg, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Mallory.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + nb := storeAddCaveats{Link: testLink} + inv, err := storeAdd.Invoke( + fixtures.Bob, + fixtures.Service, + fixtures.Alice.DID().String(), + nb, + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} is not authorized because:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + fmt.Sprintf(` - Cannot derive {"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}} from delegated capabilities:`, storeAdd.Can(), fixtures.Alice.DID(), testLink), + fmt.Sprintf(` - Constraint violation: Expected 'with: "%s"' instead got '%s'`, fixtures.Mallory.DID(), fixtures.Alice.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("invalid sub delegation", func(t *testing.T) { + prf, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Service.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + dlg, err := storeAdd.Delegate( + fixtures.Bob, + fixtures.Mallory, + fixtures.Service.DID().String(), + storeAddCaveats{}, + delegation.WithProof(delegation.FromDelegation(prf)), + ) + require.NoError(t, err) + + nb := storeAddCaveats{Link: testLink} + inv, err := storeAdd.Invoke( + fixtures.Mallory, + fixtures.Service, + fixtures.Service.DID().String(), + nb, + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + cstr := fmt.Sprintf(`{"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}}`, storeAdd.Can(), fixtures.Service.DID(), testLink) + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability %s is not authorized because:`, cstr), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Mallory.DID()), + fmt.Sprintf(` - Capability %s is not authorized because:`, cstr), + fmt.Sprintf(` - Capability can not be (self) issued by '%s'`, fixtures.Bob.DID()), + fmt.Sprintf(` - Capability %s is not authorized because:`, cstr), + fmt.Sprintf(` - Capability can not be (self) issued by '%s'`, fixtures.Alice.DID()), + " - Delegated capability not found", + }, "\n") + require.Equal(t, msg, x.Error()) + }) }) } From 3525500591d6d5f39d9669650e833fa56a89a8fe Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 16 Sep 2024 16:13:13 +0100 Subject: [PATCH 33/37] feat: use go-ucan for policy implementation --- core/policy/literal/literal.go | 52 --- core/policy/match.go | 162 ---------- core/policy/match_test.go | 414 ------------------------ core/policy/policy.go | 205 ------------ core/policy/selector/selector.go | 426 ------------------------ core/policy/selector/selector_test.go | 431 ------------------------- core/policy/selector/supported.json | 163 ---------- core/policy/selector/supported_test.go | 187 ----------- core/schema/struct.go | 2 +- go.mod | 42 ++- go.sum | 178 ++++++---- validator/lib.go | 6 +- 12 files changed, 151 insertions(+), 2117 deletions(-) delete mode 100644 core/policy/literal/literal.go delete mode 100644 core/policy/match.go delete mode 100644 core/policy/match_test.go delete mode 100644 core/policy/policy.go delete mode 100644 core/policy/selector/selector.go delete mode 100644 core/policy/selector/selector_test.go delete mode 100644 core/policy/selector/supported.json delete mode 100644 core/policy/selector/supported_test.go diff --git a/core/policy/literal/literal.go b/core/policy/literal/literal.go deleted file mode 100644 index d5fe54e..0000000 --- a/core/policy/literal/literal.go +++ /dev/null @@ -1,52 +0,0 @@ -package literal - -import ( - "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/node/basicnode" -) - -func Node(n ipld.Node) ipld.Node { - return n -} - -func Link(cid ipld.Link) ipld.Node { - nb := basicnode.Prototype.Link.NewBuilder() - nb.AssignLink(cid) - return nb.Build() -} - -func Bool(val bool) ipld.Node { - nb := basicnode.Prototype.Bool.NewBuilder() - nb.AssignBool(val) - return nb.Build() -} - -func Int(val int64) ipld.Node { - nb := basicnode.Prototype.Int.NewBuilder() - nb.AssignInt(val) - return nb.Build() -} - -func Float(val float64) ipld.Node { - nb := basicnode.Prototype.Float.NewBuilder() - nb.AssignFloat(val) - return nb.Build() -} - -func String(val string) ipld.Node { - nb := basicnode.Prototype.String.NewBuilder() - nb.AssignString(val) - return nb.Build() -} - -func Bytes(val []byte) ipld.Node { - nb := basicnode.Prototype.Bytes.NewBuilder() - nb.AssignBytes(val) - return nb.Build() -} - -func Null() ipld.Node { - nb := basicnode.Prototype.Any.NewBuilder() - nb.AssignNull() - return nb.Build() -} diff --git a/core/policy/match.go b/core/policy/match.go deleted file mode 100644 index 5cf07a5..0000000 --- a/core/policy/match.go +++ /dev/null @@ -1,162 +0,0 @@ -package policy - -import ( - "cmp" - "fmt" - - "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/datamodel" - "github.com/ipld/go-ipld-prime/must" - "github.com/storacha-network/go-ucanto/core/policy/selector" -) - -// Match determines if the IPLD node matches the policy document. -func Match(policy Policy, node ipld.Node) bool { - for _, stmt := range policy { - ok := matchStatement(stmt, node) - if !ok { - return false - } - } - return true -} - -func matchStatement(statement Statement, node ipld.Node) bool { - switch statement.Kind() { - case Kind_Equal: - if s, ok := statement.(EqualityStatement); ok { - one, _, err := selector.Select(s.Selector(), node) - if err != nil || one == nil { - return false - } - return datamodel.DeepEqual(s.Value(), one) - } - case Kind_GreaterThan: - if s, ok := statement.(InequalityStatement); ok { - one, _, err := selector.Select(s.Selector(), node) - if err != nil || one == nil { - return false - } - return isOrdered(s.Value(), one, gt) - } - case Kind_GreaterThanOrEqual: - if s, ok := statement.(InequalityStatement); ok { - one, _, err := selector.Select(s.Selector(), node) - if err != nil || one == nil { - return false - } - return isOrdered(s.Value(), one, gte) - } - case Kind_LessThan: - if s, ok := statement.(InequalityStatement); ok { - one, _, err := selector.Select(s.Selector(), node) - if err != nil || one == nil { - return false - } - return isOrdered(s.Value(), one, lt) - } - case Kind_LessThanOrEqual: - if s, ok := statement.(InequalityStatement); ok { - one, _, err := selector.Select(s.Selector(), node) - if err != nil || one == nil { - return false - } - return isOrdered(s.Value(), one, lte) - } - case Kind_Not: - if s, ok := statement.(NegationStatement); ok { - return !matchStatement(s.Value(), node) - } - case Kind_And: - if s, ok := statement.(ConjunctionStatement); ok { - for _, cs := range s.Value() { - r := matchStatement(cs, node) - if !r { - return false - } - } - return true - } - case Kind_Or: - if s, ok := statement.(DisjunctionStatement); ok { - if len(s.Value()) == 0 { - return true - } - for _, cs := range s.Value() { - r := matchStatement(cs, node) - if r { - return true - } - } - return false - } - case Kind_Like: - if s, ok := statement.(WildcardStatement); ok { - one, _, err := selector.Select(s.Selector(), node) - if err != nil || one == nil { - return false - } - v, err := one.AsString() - if err != nil { - return false - } - return s.Value().Match(v) - } - case Kind_All: - if s, ok := statement.(QuantifierStatement); ok { - _, many, err := selector.Select(s.Selector(), node) - if err != nil || many == nil { - return false - } - for _, n := range many { - ok := Match(s.Value(), n) - if !ok { - return false - } - } - return true - } - case Kind_Any: - if s, ok := statement.(QuantifierStatement); ok { - _, many, err := selector.Select(s.Selector(), node) - if err != nil || many == nil { - return false - } - for _, n := range many { - ok := Match(s.Value(), n) - if ok { - return true - } - } - return false - } - } - panic(fmt.Errorf("unimplemented statement kind: %s", statement.Kind())) -} - -func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool { - if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int { - a := must.Int(actual) - b := must.Int(expected) - return satisfies(cmp.Compare(a, b)) - } - - if expected.Kind() == ipld.Kind_Float && actual.Kind() == ipld.Kind_Float { - a, err := actual.AsFloat() - if err != nil { - panic(fmt.Errorf("extracting node float: %w", err)) - } - b, err := expected.AsFloat() - if err != nil { - panic(fmt.Errorf("extracting selector float: %w", err)) - } - return satisfies(cmp.Compare(a, b)) - } - - return false -} - -func gt(order int) bool { return order == 1 } -func gte(order int) bool { return order == 0 || order == 1 } -func lt(order int) bool { return order == -1 } -func lte(order int) bool { return order == 0 || order == -1 } diff --git a/core/policy/match_test.go b/core/policy/match_test.go deleted file mode 100644 index e7546a5..0000000 --- a/core/policy/match_test.go +++ /dev/null @@ -1,414 +0,0 @@ -package policy - -import ( - "fmt" - "testing" - - "github.com/gobwas/glob" - "github.com/ipfs/go-cid" - "github.com/ipld/go-ipld-prime" - cidlink "github.com/ipld/go-ipld-prime/linking/cid" - "github.com/ipld/go-ipld-prime/node/basicnode" - "github.com/storacha-network/go-ucanto/core/policy/literal" - "github.com/storacha-network/go-ucanto/core/policy/selector" - "github.com/stretchr/testify/require" -) - -func TestMatch(t *testing.T) { - t.Run("equality", func(t *testing.T) { - t.Run("string", func(t *testing.T) { - np := basicnode.Prototype.String - nb := np.NewBuilder() - nb.AssignString("test") - nd := nb.Build() - - pol := Policy{Equal(selector.MustParse("."), literal.String("test"))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.String("test2"))} - ok = Match(pol, nd) - require.False(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.Int(138))} - ok = Match(pol, nd) - require.False(t, ok) - }) - - t.Run("int", func(t *testing.T) { - np := basicnode.Prototype.Int - nb := np.NewBuilder() - nb.AssignInt(138) - nd := nb.Build() - - pol := Policy{Equal(selector.MustParse("."), literal.Int(138))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.Int(1138))} - ok = Match(pol, nd) - require.False(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.String("138"))} - ok = Match(pol, nd) - require.False(t, ok) - }) - - t.Run("float", func(t *testing.T) { - np := basicnode.Prototype.Float - nb := np.NewBuilder() - nb.AssignFloat(1.138) - nd := nb.Build() - - pol := Policy{Equal(selector.MustParse("."), literal.Float(1.138))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.Float(11.38))} - ok = Match(pol, nd) - require.False(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.String("138"))} - ok = Match(pol, nd) - require.False(t, ok) - }) - - t.Run("IPLD Link", func(t *testing.T) { - l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")} - l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")} - - np := basicnode.Prototype.Link - nb := np.NewBuilder() - nb.AssignLink(l0) - nd := nb.Build() - - pol := Policy{Equal(selector.MustParse("."), literal.Link(l0))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.Link(l1))} - ok = Match(pol, nd) - require.False(t, ok) - - pol = Policy{Equal(selector.MustParse("."), literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq"))} - ok = Match(pol, nd) - require.False(t, ok) - }) - - t.Run("string in map", func(t *testing.T) { - np := basicnode.Prototype.Map - nb := np.NewBuilder() - ma, _ := nb.BeginMap(1) - ma.AssembleKey().AssignString("foo") - ma.AssembleValue().AssignString("bar") - ma.Finish() - nd := nb.Build() - - pol := Policy{Equal(selector.MustParse(".foo"), literal.String("bar"))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{Equal(selector.MustParse(".[\"foo\"]"), literal.String("bar"))} - ok = Match(pol, nd) - require.True(t, ok) - - pol = Policy{Equal(selector.MustParse(".foo"), literal.String("baz"))} - ok = Match(pol, nd) - require.False(t, ok) - - pol = Policy{Equal(selector.MustParse(".foobar"), literal.String("bar"))} - ok = Match(pol, nd) - require.False(t, ok) - }) - - t.Run("string in list", func(t *testing.T) { - np := basicnode.Prototype.List - nb := np.NewBuilder() - la, _ := nb.BeginList(1) - la.AssembleValue().AssignString("foo") - la.Finish() - nd := nb.Build() - - pol := Policy{Equal(selector.MustParse(".[0]"), literal.String("foo"))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{Equal(selector.MustParse(".[1]"), literal.String("foo"))} - ok = Match(pol, nd) - require.False(t, ok) - }) - }) - - t.Run("inequality", func(t *testing.T) { - t.Run("gt int", func(t *testing.T) { - np := basicnode.Prototype.Int - nb := np.NewBuilder() - nb.AssignInt(138) - nd := nb.Build() - - pol := Policy{GreaterThan(selector.MustParse("."), literal.Int(1))} - ok := Match(pol, nd) - require.True(t, ok) - }) - - t.Run("gte int", func(t *testing.T) { - np := basicnode.Prototype.Int - nb := np.NewBuilder() - nb.AssignInt(138) - nd := nb.Build() - - pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(1))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(138))} - ok = Match(pol, nd) - require.True(t, ok) - }) - - t.Run("gt float", func(t *testing.T) { - np := basicnode.Prototype.Float - nb := np.NewBuilder() - nb.AssignFloat(1.38) - nd := nb.Build() - - pol := Policy{GreaterThan(selector.MustParse("."), literal.Float(1))} - ok := Match(pol, nd) - require.True(t, ok) - }) - - t.Run("gte float", func(t *testing.T) { - np := basicnode.Prototype.Float - nb := np.NewBuilder() - nb.AssignFloat(1.38) - nd := nb.Build() - - pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1.38))} - ok = Match(pol, nd) - require.True(t, ok) - }) - - t.Run("lt int", func(t *testing.T) { - np := basicnode.Prototype.Int - nb := np.NewBuilder() - nb.AssignInt(138) - nd := nb.Build() - - pol := Policy{LessThan(selector.MustParse("."), literal.Int(1138))} - ok := Match(pol, nd) - require.True(t, ok) - }) - - t.Run("lte int", func(t *testing.T) { - np := basicnode.Prototype.Int - nb := np.NewBuilder() - nb.AssignInt(138) - nd := nb.Build() - - pol := Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(1138))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(138))} - ok = Match(pol, nd) - require.True(t, ok) - }) - }) - - t.Run("negation", func(t *testing.T) { - np := basicnode.Prototype.Bool - nb := np.NewBuilder() - nb.AssignBool(false) - nd := nb.Build() - - pol := Policy{Not(Equal(selector.MustParse("."), literal.Bool(true)))} - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{Not(Equal(selector.MustParse("."), literal.Bool(false)))} - ok = Match(pol, nd) - require.False(t, ok) - }) - - t.Run("conjunction", func(t *testing.T) { - np := basicnode.Prototype.Int - nb := np.NewBuilder() - nb.AssignInt(138) - nd := nb.Build() - - pol := Policy{ - And( - GreaterThan(selector.MustParse("."), literal.Int(1)), - LessThan(selector.MustParse("."), literal.Int(1138)), - ), - } - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{ - And( - GreaterThan(selector.MustParse("."), literal.Int(1)), - Equal(selector.MustParse("."), literal.Int(1138)), - ), - } - ok = Match(pol, nd) - require.False(t, ok) - - pol = Policy{And()} - ok = Match(pol, nd) - require.True(t, ok) - }) - - t.Run("disjunction", func(t *testing.T) { - np := basicnode.Prototype.Int - nb := np.NewBuilder() - nb.AssignInt(138) - nd := nb.Build() - - pol := Policy{ - Or( - GreaterThan(selector.MustParse("."), literal.Int(138)), - LessThan(selector.MustParse("."), literal.Int(1138)), - ), - } - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{ - Or( - GreaterThan(selector.MustParse("."), literal.Int(138)), - Equal(selector.MustParse("."), literal.Int(1138)), - ), - } - ok = Match(pol, nd) - require.False(t, ok) - - pol = Policy{Or()} - ok = Match(pol, nd) - require.True(t, ok) - }) - - t.Run("wildcard", func(t *testing.T) { - glb, err := glob.Compile(`Alice\*, Bob*, Carol.`) - require.NoError(t, err) - - for _, s := range []string{ - "Alice*, Bob, Carol.", - "Alice*, Bob, Dan, Erin, Carol.", - "Alice*, Bob , Carol.", - "Alice*, Bob*, Carol.", - } { - func(s string) { - t.Run(fmt.Sprintf("pass %s", s), func(t *testing.T) { - np := basicnode.Prototype.String - nb := np.NewBuilder() - nb.AssignString(s) - nd := nb.Build() - - pol := Policy{Like(selector.MustParse("."), glb)} - ok := Match(pol, nd) - require.True(t, ok) - }) - }(s) - } - - for _, s := range []string{ - "Alice*, Bob, Carol", - "Alice*, Bob*, Carol!", - "Alice, Bob, Carol.", - " Alice*, Bob, Carol. ", - } { - func(s string) { - t.Run(fmt.Sprintf("fail %s", s), func(t *testing.T) { - np := basicnode.Prototype.String - nb := np.NewBuilder() - nb.AssignString(s) - nd := nb.Build() - - pol := Policy{Like(selector.MustParse("."), glb)} - ok := Match(pol, nd) - require.False(t, ok) - }) - }(s) - } - }) - - t.Run("quantification", func(t *testing.T) { - buildValueNode := func(v int64) ipld.Node { - np := basicnode.Prototype.Map - nb := np.NewBuilder() - ma, _ := nb.BeginMap(1) - ma.AssembleKey().AssignString("value") - ma.AssembleValue().AssignInt(v) - ma.Finish() - return nb.Build() - } - - t.Run("all", func(t *testing.T) { - np := basicnode.Prototype.List - nb := np.NewBuilder() - la, _ := nb.BeginList(5) - la.AssembleValue().AssignNode(buildValueNode(5)) - la.AssembleValue().AssignNode(buildValueNode(10)) - la.AssembleValue().AssignNode(buildValueNode(20)) - la.AssembleValue().AssignNode(buildValueNode(50)) - la.AssembleValue().AssignNode(buildValueNode(100)) - la.Finish() - nd := nb.Build() - - pol := Policy{ - All( - selector.MustParse(".[]"), - GreaterThan(selector.MustParse(".value"), literal.Int(2)), - ), - } - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{ - All( - selector.MustParse(".[]"), - GreaterThan(selector.MustParse(".value"), literal.Int(20)), - ), - } - ok = Match(pol, nd) - require.False(t, ok) - }) - - t.Run("any", func(t *testing.T) { - np := basicnode.Prototype.List - nb := np.NewBuilder() - la, _ := nb.BeginList(5) - la.AssembleValue().AssignNode(buildValueNode(5)) - la.AssembleValue().AssignNode(buildValueNode(10)) - la.AssembleValue().AssignNode(buildValueNode(20)) - la.AssembleValue().AssignNode(buildValueNode(50)) - la.AssembleValue().AssignNode(buildValueNode(100)) - la.Finish() - nd := nb.Build() - - pol := Policy{ - Any( - selector.MustParse(".[]"), - GreaterThan(selector.MustParse(".value"), literal.Int(10)), - LessThan(selector.MustParse(".value"), literal.Int(50)), - ), - } - ok := Match(pol, nd) - require.True(t, ok) - - pol = Policy{ - Any( - selector.MustParse(".[]"), - GreaterThan(selector.MustParse(".value"), literal.Int(100)), - ), - } - ok = Match(pol, nd) - require.False(t, ok) - }) - }) -} diff --git a/core/policy/policy.go b/core/policy/policy.go deleted file mode 100644 index 49f1f48..0000000 --- a/core/policy/policy.go +++ /dev/null @@ -1,205 +0,0 @@ -package policy - -// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy - -import ( - "github.com/gobwas/glob" - "github.com/ipld/go-ipld-prime" - "github.com/storacha-network/go-ucanto/core/policy/selector" -) - -const ( - Kind_Equal = "==" - Kind_GreaterThan = ">" - Kind_GreaterThanOrEqual = ">=" - Kind_LessThan = "<" - Kind_LessThanOrEqual = "<=" - Kind_Not = "not" - Kind_And = "and" - Kind_Or = "or" - Kind_Like = "like" - Kind_All = "all" - Kind_Any = "any" -) - -type Policy = []Statement - -type Statement interface { - Kind() string -} - -type EqualityStatement interface { - Statement - Selector() selector.Selector - Value() ipld.Node -} - -type InequalityStatement interface { - Statement - Selector() selector.Selector - Value() ipld.Node -} - -type WildcardStatement interface { - Statement - Selector() selector.Selector - Value() glob.Glob -} - -type ConnectiveStatement interface { - Statement -} - -type NegationStatement interface { - ConnectiveStatement - Value() Statement -} - -type ConjunctionStatement interface { - ConnectiveStatement - Value() []Statement -} - -type DisjunctionStatement interface { - ConnectiveStatement - Value() []Statement -} - -type QuantifierStatement interface { - Statement - Selector() selector.Selector - Value() Policy -} - -type equality struct { - kind string - selector selector.Selector - value ipld.Node -} - -func (e equality) Kind() string { - return e.kind -} - -func (e equality) Value() ipld.Node { - return e.value -} - -func (e equality) Selector() selector.Selector { - return e.selector -} - -func Equal(selector selector.Selector, value ipld.Node) EqualityStatement { - return equality{Kind_Equal, selector, value} -} - -func GreaterThan(selector selector.Selector, value ipld.Node) InequalityStatement { - return equality{Kind_GreaterThan, selector, value} -} - -func GreaterThanOrEqual(selector selector.Selector, value ipld.Node) InequalityStatement { - return equality{Kind_GreaterThanOrEqual, selector, value} -} - -func LessThan(selector selector.Selector, value ipld.Node) InequalityStatement { - return equality{Kind_LessThan, selector, value} -} - -func LessThanOrEqual(selector selector.Selector, value ipld.Node) InequalityStatement { - return equality{Kind_LessThanOrEqual, selector, value} -} - -type negation struct { - statement Statement -} - -func (n negation) Kind() string { - return Kind_Not -} - -func (n negation) Value() Statement { - return n.statement -} - -func Not(stmt Statement) NegationStatement { - return negation{stmt} -} - -type conjunction struct { - statements []Statement -} - -func (n conjunction) Kind() string { - return Kind_And -} - -func (n conjunction) Value() []Statement { - return n.statements -} - -func And(stmts ...Statement) ConjunctionStatement { - return conjunction{stmts} -} - -type disjunction struct { - statements []Statement -} - -func (n disjunction) Kind() string { - return Kind_Or -} - -func (n disjunction) Value() []Statement { - return n.statements -} - -func Or(stmts ...Statement) DisjunctionStatement { - return disjunction{stmts} -} - -type wildcard struct { - selector selector.Selector - glob glob.Glob -} - -func (n wildcard) Kind() string { - return Kind_Like -} - -func (n wildcard) Selector() selector.Selector { - return n.selector -} - -func (n wildcard) Value() glob.Glob { - return n.glob -} - -func Like(selector selector.Selector, glob glob.Glob) WildcardStatement { - return wildcard{selector, glob} -} - -type quantifier struct { - kind string - selector selector.Selector - policy Policy -} - -func (n quantifier) Kind() string { - return n.kind -} - -func (n quantifier) Selector() selector.Selector { - return n.selector -} - -func (n quantifier) Value() Policy { - return n.policy -} - -func All(selector selector.Selector, policy ...Statement) QuantifierStatement { - return quantifier{Kind_All, selector, policy} -} - -func Any(selector selector.Selector, policy ...Statement) QuantifierStatement { - return quantifier{Kind_Any, selector, policy} -} diff --git a/core/policy/selector/selector.go b/core/policy/selector/selector.go deleted file mode 100644 index 40a432a..0000000 --- a/core/policy/selector/selector.go +++ /dev/null @@ -1,426 +0,0 @@ -package selector - -import ( - "fmt" - "regexp" - "strconv" - "strings" - - "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/datamodel" - "github.com/ipld/go-ipld-prime/schema" -) - -// Selector describes a UCAN policy selector, as specified here: -// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#selectors -type Selector []Segment - -func (s Selector) String() string { - var str string - for _, seg := range s { - str += seg.String() - } - return str -} - -type Segment interface { - // Identity flags that this selector is the identity selector. - Identity() bool - // Optional flags that this selector is optional. - Optional() bool - // Iterator flags that this selector is an iterator segment. - Iterator() bool - // Slice flags that this segemnt targets a range of a slice. - Slice() []int - // Field is the name of a field in a struct/map. - Field() string - // Index is an index of a slice. - Index() int - // String returns the segment's string representation. - String() string -} - -var Identity = segment{".", true, false, false, nil, "", 0} - -var ( - indexRegex = regexp.MustCompile(`^-?\d+$`) - sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`) - fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`) -) - -type segment struct { - str string - identity bool - optional bool - iterator bool - slice []int - field string - index int -} - -func (s segment) String() string { - return s.str -} - -func (s segment) Identity() bool { - return s.identity -} - -func (s segment) Optional() bool { - return s.optional -} - -func (s segment) Iterator() bool { - return s.iterator -} - -func (s segment) Slice() []int { - return s.slice -} - -func (s segment) Field() string { - return s.field -} - -func (s segment) Index() int { - return s.index -} - -func Parse(str string) (Selector, error) { - if string(str[0]) != "." { - return nil, NewParseError("selector must start with identity segment '.'", str, 0, string(str[0])) - } - - col := 0 - var sel Selector - for _, tok := range tokenize(str) { - seg := tok - opt := strings.HasSuffix(tok, "?") - if opt { - seg = tok[0 : len(tok)-1] - } - switch seg { - case ".": - if len(sel) > 0 && sel[len(sel)-1].Identity() { - return nil, NewParseError("selector contains unsupported recursive descent segment: '..'", str, col, tok) - } - sel = append(sel, Identity) - case "[]": - sel = append(sel, segment{tok, false, opt, true, nil, "", 0}) - default: - if strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]") { - lookup := seg[1 : len(seg)-1] - - if indexRegex.MatchString(lookup) { // index - idx, err := strconv.Atoi(lookup) - if err != nil { - return nil, NewParseError("invalid index", str, col, tok) - } - sel = append(sel, segment{str: tok, optional: opt, index: idx}) - } else if strings.HasPrefix(lookup, "\"") && strings.HasSuffix(lookup, "\"") { // explicit field - sel = append(sel, segment{str: tok, optional: opt, field: lookup[1 : len(lookup)-1]}) - } else if sliceRegex.MatchString(lookup) { // slice [3:5] or [:5] or [3:] - var rng []int - splt := strings.Split(lookup, ":") - if splt[0] == "" { - rng = append(rng, 0) - } else { - i, err := strconv.Atoi(splt[0]) - if err != nil { - return nil, NewParseError("invalid slice index", str, col, tok) - } - rng = append(rng, i) - } - if splt[1] != "" { - i, err := strconv.Atoi(splt[1]) - if err != nil { - return nil, NewParseError("invalid slice index", str, col, tok) - } - rng = append(rng, i) - } - sel = append(sel, segment{str: tok, optional: opt, slice: rng}) - } else { - return nil, NewParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok) - } - } else if fieldRegex.MatchString(seg) { - sel = append(sel, segment{str: tok, optional: opt, field: seg[1:]}) - } else { - return nil, NewParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok) - } - } - col += len(tok) - } - return sel, nil -} - -func tokenize(str string) []string { - var toks []string - col := 0 - ofs := 0 - ctx := "" - - for col < len(str) { - char := string(str[col]) - - if char == "\"" && string(str[col-1]) != "\\" { - col++ - if ctx == "\"" { - ctx = "" - } else { - ctx = "\"" - } - continue - } - - if ctx == "\"" { - col++ - continue - } - - if char == "." || char == "[" { - if ofs < col { - toks = append(toks, str[ofs:col]) - } - ofs = col - } - col++ - } - - if ofs < col && ctx != "\"" { - toks = append(toks, str[ofs:col]) - } - - return toks -} - -type ParseError interface { - error - Name() string - Message() string - Source() string - Column() int - Token() string -} - -type parseerr struct { - msg string - src string - col int - tok string -} - -func (p parseerr) Name() string { - return "ParseError" -} - -func (p parseerr) Message() string { - return p.msg -} - -func (p parseerr) Column() int { - return p.col -} - -func (p parseerr) Error() string { - return p.msg -} - -func (p parseerr) Source() string { - return p.src -} - -func (p parseerr) Token() string { - return p.tok -} - -func NewParseError(message string, source string, column int, token string) error { - return parseerr{message, source, column, token} -} - -func MustParse(sel string) Selector { - s, err := Parse(sel) - if err != nil { - panic(err) - } - return s -} - -// Select uses a selector to extract an IPLD node or set of nodes from the -// passed subject node. -func Select(sel Selector, subject ipld.Node) (ipld.Node, []ipld.Node, error) { - return resolve(sel, subject, nil) -} - -func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.Node, error) { - cur := subject - for i, seg := range sel { - if seg.Identity() { - continue - } else if seg.Iterator() { - if cur != nil && cur.Kind() == datamodel.Kind_List { - var many []ipld.Node - it := cur.ListIterator() - for { - if it.Done() { - break - } - - k, v, err := it.Next() - if err != nil { - return nil, nil, err - } - - key := fmt.Sprintf("%d", k) - o, m, err := resolve(sel[i+1:], v, append(at[:], key)) - if err != nil { - return nil, nil, err - } - - if m != nil { - many = append(many, m...) - } else { - many = append(many, o) - } - } - return nil, many, nil - } else if cur != nil && cur.Kind() == datamodel.Kind_Map { - var many []ipld.Node - it := cur.MapIterator() - for { - if it.Done() { - break - } - - k, v, err := it.Next() - if err != nil { - return nil, nil, err - } - - key, _ := k.AsString() - o, m, err := resolve(sel[i+1:], v, append(at[:], key)) - if err != nil { - return nil, nil, err - } - - if m != nil { - many = append(many, m...) - } else { - many = append(many, o) - } - } - return nil, many, nil - } else if seg.Optional() { - cur = nil - } else { - return nil, nil, NewResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at) - } - - } else if seg.Field() != "" { - at = append(at, seg.Field()) - if cur != nil && cur.Kind() == datamodel.Kind_Map { - n, err := cur.LookupByString(seg.Field()) - if err != nil { - if isMissing(err) { - if seg.Optional() { - cur = nil - } else { - return nil, nil, NewResolutionError(fmt.Sprintf("object has no field named: %s", seg.Field()), at) - } - } else { - return nil, nil, err - } - } - cur = n - } else if seg.Optional() { - cur = nil - } else { - return nil, nil, NewResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at) - } - } else if seg.Slice() != nil { - if cur != nil && cur.Kind() == datamodel.Kind_List { - return nil, nil, NewResolutionError("list slice selection not yet implemented", at) - } else if cur != nil && cur.Kind() == datamodel.Kind_Bytes { - return nil, nil, NewResolutionError("bytes slice selection not yet implemented", at) - } else if seg.Optional() { - cur = nil - } else { - return nil, nil, NewResolutionError(fmt.Sprintf("can not index: %s on kind: %s", seg.Field(), kindString(cur)), at) - } - } else { - at = append(at, fmt.Sprintf("%d", seg.Index())) - if cur != nil && cur.Kind() == datamodel.Kind_List { - n, err := cur.LookupByIndex(int64(seg.Index())) - if err != nil { - if isMissing(err) { - if seg.Optional() { - cur = nil - } else { - return nil, nil, NewResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at) - } - } else { - return nil, nil, err - } - } - cur = n - } else if seg.Optional() { - cur = nil - } else { - return nil, nil, NewResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at) - } - } - } - - return cur, nil, nil -} - -func kindString(n datamodel.Node) string { - if n == nil { - return "null" - } - return n.Kind().String() -} - -func isMissing(err error) bool { - if _, ok := err.(datamodel.ErrNotExists); ok { - return true - } - if _, ok := err.(schema.ErrNoSuchField); ok { - return true - } - if _, ok := err.(schema.ErrInvalidKey); ok { - return true - } - return false -} - -type ResolutionError interface { - error - Name() string - Message() string - At() []string -} - -type resolutionerr struct { - msg string - at []string -} - -func (r resolutionerr) Name() string { - return "ResolutionError" -} - -func (r resolutionerr) Message() string { - return fmt.Sprintf("can not resolve path: .%s", strings.Join(r.at, ".")) -} - -func (r resolutionerr) At() []string { - return r.at -} - -func (r resolutionerr) Error() string { - return r.Message() -} - -func NewResolutionError(message string, at []string) error { - return resolutionerr{message, at} -} diff --git a/core/policy/selector/selector_test.go b/core/policy/selector/selector_test.go deleted file mode 100644 index b19282f..0000000 --- a/core/policy/selector/selector_test.go +++ /dev/null @@ -1,431 +0,0 @@ -package selector - -import ( - "fmt" - "testing" - - "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/must" - "github.com/ipld/go-ipld-prime/node/bindnode" - "github.com/ipld/go-ipld-prime/printer" - "github.com/stretchr/testify/require" -) - -func TestParse(t *testing.T) { - t.Run("identity", func(t *testing.T) { - sel, err := Parse(".") - require.NoError(t, err) - require.Equal(t, 1, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - }) - - t.Run("field", func(t *testing.T) { - sel, err := Parse(".foo") - require.NoError(t, err) - require.Equal(t, 1, len(sel)) - require.False(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Equal(t, sel[0].Field(), "foo") - require.Empty(t, sel[0].Index()) - }) - - t.Run("explicit field", func(t *testing.T) { - sel, err := Parse(`.["foo"]`) - require.NoError(t, err) - require.Equal(t, 2, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - require.False(t, sel[1].Identity()) - require.False(t, sel[1].Optional()) - require.False(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Equal(t, sel[1].Field(), "foo") - require.Empty(t, sel[1].Index()) - }) - - t.Run("index", func(t *testing.T) { - sel, err := Parse(".[138]") - require.NoError(t, err) - require.Equal(t, 2, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - require.False(t, sel[1].Identity()) - require.False(t, sel[1].Optional()) - require.False(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Empty(t, sel[1].Field()) - require.Equal(t, sel[1].Index(), 138) - }) - - t.Run("negative index", func(t *testing.T) { - sel, err := Parse(".[-138]") - require.NoError(t, err) - require.Equal(t, 2, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - require.False(t, sel[1].Identity()) - require.False(t, sel[1].Optional()) - require.False(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Empty(t, sel[1].Field()) - require.Equal(t, sel[1].Index(), -138) - }) - - t.Run("iterator", func(t *testing.T) { - sel, err := Parse(".[]") - require.NoError(t, err) - require.Equal(t, 2, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - require.False(t, sel[1].Identity()) - require.False(t, sel[1].Optional()) - require.True(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Empty(t, sel[1].Field()) - require.Empty(t, sel[1].Index()) - }) - - t.Run("optional field", func(t *testing.T) { - sel, err := Parse(".foo?") - require.NoError(t, err) - require.Equal(t, 1, len(sel)) - require.False(t, sel[0].Identity()) - require.True(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Equal(t, sel[0].Field(), "foo") - require.Empty(t, sel[0].Index()) - }) - - t.Run("optional explicit field", func(t *testing.T) { - sel, err := Parse(`.["foo"]?`) - require.NoError(t, err) - require.Equal(t, 2, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - require.False(t, sel[1].Identity()) - require.True(t, sel[1].Optional()) - require.False(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Equal(t, sel[1].Field(), "foo") - require.Empty(t, sel[1].Index()) - }) - - t.Run("optional index", func(t *testing.T) { - sel, err := Parse(".[138]?") - require.NoError(t, err) - require.Equal(t, 2, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - require.False(t, sel[1].Identity()) - require.True(t, sel[1].Optional()) - require.False(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Empty(t, sel[1].Field()) - require.Equal(t, sel[1].Index(), 138) - }) - - t.Run("optional iterator", func(t *testing.T) { - sel, err := Parse(".[]?") - require.NoError(t, err) - require.Equal(t, 2, len(sel)) - require.True(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Empty(t, sel[0].Field()) - require.Empty(t, sel[0].Index()) - require.False(t, sel[1].Identity()) - require.True(t, sel[1].Optional()) - require.True(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Empty(t, sel[1].Field()) - require.Empty(t, sel[1].Index()) - }) - - t.Run("nesting", func(t *testing.T) { - str := `.foo.["bar"].[138]?.baz[1:]` - sel, err := Parse(str) - require.NoError(t, err) - printSegments(sel) - require.Equal(t, str, sel.String()) - require.Equal(t, 7, len(sel)) - require.False(t, sel[0].Identity()) - require.False(t, sel[0].Optional()) - require.False(t, sel[0].Iterator()) - require.Empty(t, sel[0].Slice()) - require.Equal(t, sel[0].Field(), "foo") - require.Empty(t, sel[0].Index()) - require.True(t, sel[1].Identity()) - require.False(t, sel[1].Optional()) - require.False(t, sel[1].Iterator()) - require.Empty(t, sel[1].Slice()) - require.Empty(t, sel[1].Field()) - require.Empty(t, sel[1].Index()) - require.False(t, sel[2].Identity()) - require.False(t, sel[2].Optional()) - require.False(t, sel[2].Iterator()) - require.Empty(t, sel[2].Slice()) - require.Equal(t, sel[2].Field(), "bar") - require.Empty(t, sel[2].Index()) - require.True(t, sel[3].Identity()) - require.False(t, sel[3].Optional()) - require.False(t, sel[3].Iterator()) - require.Empty(t, sel[3].Slice()) - require.Empty(t, sel[3].Field()) - require.Empty(t, sel[3].Index()) - require.False(t, sel[4].Identity()) - require.True(t, sel[4].Optional()) - require.False(t, sel[4].Iterator()) - require.Empty(t, sel[4].Slice()) - require.Empty(t, sel[4].Field()) - require.Equal(t, sel[4].Index(), 138) - require.False(t, sel[5].Identity()) - require.False(t, sel[5].Optional()) - require.False(t, sel[5].Iterator()) - require.Empty(t, sel[5].Slice()) - require.Equal(t, sel[5].Field(), "baz") - require.Empty(t, sel[5].Index()) - require.False(t, sel[6].Identity()) - require.False(t, sel[6].Optional()) - require.False(t, sel[6].Iterator()) - require.Equal(t, sel[6].Slice(), []int{1}) - require.Empty(t, sel[6].Field()) - require.Empty(t, sel[6].Index()) - }) - - t.Run("non dotted", func(t *testing.T) { - _, err := Parse("foo") - require.NotNil(t, err) - fmt.Println(err) - }) - - t.Run("non quoted", func(t *testing.T) { - _, err := Parse(".[foo]") - require.NotNil(t, err) - fmt.Println(err) - }) -} - -func printSegments(s Selector) { - for i, seg := range s { - fmt.Printf("%d: %s\n", i, seg.String()) - } -} - -func TestSelect(t *testing.T) { - type name struct { - First string - Middle *string - Last string - } - type interest struct { - Name string - Outdoor bool - Experience int - } - type user struct { - Name name - Age int - Nationalities []string - Interests []interest - } - - ts, err := ipld.LoadSchemaBytes([]byte(` - type User struct { - name Name - age Int - nationalities [String] - interests [Interest] - } - type Name struct { - first String - middle optional String - last String - } - type Interest struct { - name String - outdoor Bool - experience Int - } - `)) - require.NoError(t, err) - typ := ts.TypeByName("User") - - am := "Joan" - alice := user{ - Name: name{First: "Alice", Middle: &am, Last: "Wonderland"}, - Age: 24, - Nationalities: []string{"British"}, - Interests: []interest{ - {Name: "Cycling", Outdoor: true, Experience: 4}, - {Name: "Chess", Outdoor: false, Experience: 2}, - }, - } - bob := user{ - Name: name{First: "Bob", Last: "Builder"}, - Age: 35, - Nationalities: []string{"Canadian", "South African"}, - Interests: []interest{ - {Name: "Snowboarding", Outdoor: true, Experience: 8}, - {Name: "Reading", Outdoor: false, Experience: 25}, - }, - } - - anode := bindnode.Wrap(&alice, typ) - bnode := bindnode.Wrap(&bob, typ) - - t.Run("identity", func(t *testing.T) { - sel, err := Parse(".") - require.NoError(t, err) - - one, many, err := Select(sel, anode) - require.NoError(t, err) - require.NotEmpty(t, one) - require.Empty(t, many) - - fmt.Println(printer.Sprint(one)) - - age := must.Int(must.Node(one.LookupByString("age"))) - require.Equal(t, int64(alice.Age), age) - }) - - t.Run("nested property", func(t *testing.T) { - sel, err := Parse(".name.first") - require.NoError(t, err) - - one, many, err := Select(sel, anode) - require.NoError(t, err) - require.NotEmpty(t, one) - require.Empty(t, many) - - fmt.Println(printer.Sprint(one)) - - name := must.String(one) - require.Equal(t, alice.Name.First, name) - - one, many, err = Select(sel, bnode) - require.NoError(t, err) - require.NotEmpty(t, one) - require.Empty(t, many) - - fmt.Println(printer.Sprint(one)) - - name = must.String(one) - require.Equal(t, bob.Name.First, name) - }) - - t.Run("optional nested property", func(t *testing.T) { - sel, err := Parse(".name.middle?") - require.NoError(t, err) - - one, many, err := Select(sel, anode) - require.NoError(t, err) - require.NotEmpty(t, one) - require.Empty(t, many) - - fmt.Println(printer.Sprint(one)) - - name := must.String(one) - require.Equal(t, *alice.Name.Middle, name) - - one, many, err = Select(sel, bnode) - require.NoError(t, err) - require.Empty(t, one) - require.Empty(t, many) - }) - - t.Run("not exists", func(t *testing.T) { - sel, err := Parse(".name.foo") - require.NoError(t, err) - - one, many, err := Select(sel, anode) - require.Error(t, err) - require.Empty(t, one) - require.Empty(t, many) - - fmt.Println(err) - - if _, ok := err.(ResolutionError); !ok { - t.Fatalf("error was not a resolution error") - } - }) - - t.Run("optional not exists", func(t *testing.T) { - sel, err := Parse(".name.foo?") - require.NoError(t, err) - - one, many, err := Select(sel, anode) - require.NoError(t, err) - require.Empty(t, one) - require.Empty(t, many) - }) - - t.Run("iterator", func(t *testing.T) { - sel, err := Parse(".interests[]") - require.NoError(t, err) - - one, many, err := Select(sel, anode) - require.NoError(t, err) - require.Empty(t, one) - require.NotEmpty(t, many) - - for _, n := range many { - fmt.Println(printer.Sprint(n)) - } - - iname := must.String(must.Node(many[0].LookupByString("name"))) - require.Equal(t, alice.Interests[0].Name, iname) - - iname = must.String(must.Node(many[1].LookupByString("name"))) - require.Equal(t, alice.Interests[1].Name, iname) - }) - - t.Run("map iterator", func(t *testing.T) { - sel, err := Parse(".interests[0][]") - require.NoError(t, err) - - one, many, err := Select(sel, anode) - require.NoError(t, err) - require.Empty(t, one) - require.NotEmpty(t, many) - - for _, n := range many { - fmt.Println(printer.Sprint(n)) - } - - require.Equal(t, alice.Interests[0].Name, must.String(many[0])) - require.Equal(t, alice.Interests[0].Experience, int(must.Int(many[2]))) - }) -} diff --git a/core/policy/selector/supported.json b/core/policy/selector/supported.json deleted file mode 100644 index e8c9781..0000000 --- a/core/policy/selector/supported.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "pass": [ - { - "name": "Identity", - "selector": ".", - "input": "{\"x\":1}", - "output": "{\"x\":1}" - }, - { - "name": "Iterator", - "selector": ".[]", - "input": "[1, 2]", - "output": "[1, 2]" - }, - { - "name": "Optional Null Iterator", - "selector": ".[]?", - "input": "null", - "output": "()" - }, - { - "name": "Optional Iterator", - "selector": ".[][]?", - "input": "[[1], 2, [3]]", - "output": "[1, 3]" - }, - { - "name": "Object Key", - "selector": ".x", - "input": "{\"x\": 1 }", - "output": "1" - }, - { - "name": "Quoted Key", - "selector": ".[\"x\"]", - "input": "{\"x\": 1}", - "output": "1" - }, - { - "name": "Index", - "selector": ".[0]", - "input": "[1, 2]", - "output": "1" - }, - { - "name": "Negative Index", - "selector": ".[-1]", - "input": "[1, 2]", - "output": "2" - }, - { - "name": "String Index", - "selector": ".[0]", - "input": "\"Hi\"", - "output": "\"H\"" - }, - { - "name": "Bytes Index", - "selector": ".[0]", - "input": "{\"/\":{\"bytes\":\"AAE\"}", - "output": "0" - }, - { - "name": "Array Slice", - "selector": ".[0:2]", - "input": "[0, 1, 2]", - "output": "[0, 1]" - }, - { - "name": "Array Slice", - "selector": ".[1:]", - "input": "[0, 1, 2]", - "output": "[1, 2]" - }, - { - "name": "Array Slice", - "selector": ".[:2]", - "input": "[0, 1, 2]", - "output": "[0, 1]" - }, - { - "name": "String Slice", - "selector": ".[0:2]", - "input": "\"hello\"", - "output": "\"he\"" - }, - { - "name": "Bytes Index", - "selector": ".[1:]", - "input": "{\"/\":{\"bytes\":\"AAEC\"}}", - "output": "{\"/\":{\"bytes\":\"AQI\"}}" - } - ], - "null": [ - { - "name": "Optional Missing Key", - "selector": ".x?", - "input": "{}" - }, - { - "name": "Optional Null Key", - "selector": ".x?", - "input": "null" - }, - { - "name": "Optional Array Key", - "selector": ".x?", - "input": "[]" - }, - { - "name": "Optional Quoted Key", - "selector": ".[\"x\"]?", - "input": "{}" - }, - { - "name": ".length?", - "selector": ".length?", - "input": "[1, 2]" - }, - { - "name": "Optional Index", - "selector": ".[4]?", - "input": "[0, 1]" - } - ], - "fail": [ - { - "name": "Null Iterator", - "selector": ".[]", - "input": "null" - }, - { - "name": "Nested Iterator", - "selector": ".[][]", - "input": "[[1], 2, [3]]" - }, - { - "name": "Missing Key", - "selector": ".x", - "input": "{}" - }, - { - "name": "Null Key", - "selector": ".x", - "input": "null" - }, - { - "name": "Array Key", - "selector": ".x", - "input": "[]" - }, - { - "name": ".length", - "selector": ".length", - "input": "[1, 2]" - }, - { - "name": "Out of bound Index", - "selector": ".[4]", - "input": "[0, 1]" - } - ] -} \ No newline at end of file diff --git a/core/policy/selector/supported_test.go b/core/policy/selector/supported_test.go deleted file mode 100644 index 8a29471..0000000 --- a/core/policy/selector/supported_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package selector_test - -import ( - "bytes" - _ "embed" - "encoding/json" - "fmt" - "strings" - "testing" - - "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/codec/dagjson" - "github.com/ipld/go-ipld-prime/datamodel" - basicnode "github.com/ipld/go-ipld-prime/node/basic" - "github.com/storacha-network/go-ucanto/core/policy/selector" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/wI2L/jsondiff" -) - -//go:embed supported.json -var supported []byte - -type Testcase struct { - Name string `json:"name"` - Selector string `json:"selector"` - Input string `json:"input"` -} - -func (tc Testcase) Select(t *testing.T) (datamodel.Node, []datamodel.Node, error) { - t.Helper() - - sel, err := selector.Parse(tc.Selector) - require.NoError(t, err) - - return selector.Select(sel, node(t, tc.Input)) -} - -type SuccessTestcase struct { - Testcase - Output *string `json:"output"` -} - -func (tc SuccessTestcase) SelectAndCompare(t *testing.T) { - t.Helper() - - exp := node(t, *tc.Output) - - node, nodes, err := tc.Select(t) - require.NoError(t, err) - require.NotEqual(t, node != nil, len(nodes) > 0) // XOR (only one of node or nodes should be set) - - if node == nil { - nb := basicnode.Prototype.List.NewBuilder() - la, err := nb.BeginList(int64(len(nodes))) - require.NoError(t, err) - - for _, n := range nodes { - // TODO: This code is probably not needed if the Select operation properly prunes nil values - e.g.: Optional Iterator - if n == nil { - n = datamodel.Null - } - - require.NoError(t, la.AssembleValue().AssignNode(n)) - } - - require.NoError(t, la.Finish()) - - node = nb.Build() - } - - equalIPLD(t, exp, node) -} - -type Testcases struct { - SuccessTestcases []SuccessTestcase `json:"pass"` - NullTestcases []Testcase `json:"null"` - ErrorTestcases []Testcase `json:"fail"` -} - -// TestSupported Forms runs tests against the Selector according to the -// proposed "Supported Forms" presented in this GitHub issue: -// https://github.com/ucan-wg/delegation/issues/5#issue-2154766496 -func TestSupportedForms(t *testing.T) { - t.Parallel() - - var testcases Testcases - - require.NoError(t, json.Unmarshal(supported, &testcases)) - - t.Run("node(s)", func(t *testing.T) { - t.Parallel() - - for _, testcase := range testcases.SuccessTestcases { - testcase := testcase - - t.Run(testcase.Name, func(t *testing.T) { - t.Parallel() - - // TODO: This test case panics during Select, though Parse works - reports - // "index out of range [-1]" so a bit of subtraction and some bounds checking - // should fix this testcase. - if testcase.Name == "Negative Index" { - t.Skip() - } - - testcase.SelectAndCompare(t) - }) - } - }) - - t.Run("null", func(t *testing.T) { - t.Parallel() - - for _, testcase := range testcases.NullTestcases { - testcase := testcase - - t.Run(testcase.Name, func(t *testing.T) { - t.Parallel() - - node, nodes, err := testcase.Select(t) - require.NoError(t, err) - // TODO: should Select return a single node which is sometimes a list or null? - // require.Equal(t, datamodel.Null, node) - assert.Nil(t, node) - assert.Empty(t, nodes) - }) - } - }) - - t.Run("error", func(t *testing.T) { - t.Parallel() - - for _, testcase := range testcases.ErrorTestcases { - testcase := testcase - - t.Run(testcase.Name, func(t *testing.T) { - t.Parallel() - - node, nodes, err := testcase.Select(t) - require.Error(t, err) - assert.Nil(t, node) - assert.Empty(t, nodes) - }) - } - }) -} - -func equalIPLD(t *testing.T, expected datamodel.Node, actual datamodel.Node, msgAndArgs ...interface{}) bool { - t.Helper() - - if !assert.ObjectsAreEqual(expected, actual) { - exp, act := &bytes.Buffer{}, &bytes.Buffer{} - if err := dagjson.Encode(expected, exp); err != nil { - return assert.Fail(t, "Failed to encode json for expected IPLD node") - } - - if err := dagjson.Encode(actual, act); err != nil { - return assert.Fail(t, "Failed to encode JSON for actual IPLD node") - } - - diff, err := jsondiff.CompareJSON(act.Bytes(), exp.Bytes()) - if err != nil { - return assert.Fail(t, "Failed to create diff of expected and actual IPLD nodes") - } - - return assert.Fail(t, fmt.Sprintf("Not equal: \n"+ - "expected: %s\n"+ - "actual: %s\n"+ - "diff: %s", exp, act, diff), msgAndArgs) - } - - return true -} - -func node(t *testing.T, json string) ipld.Node { - t.Helper() - - np := basicnode.Prototype.Any - nb := np.NewBuilder() - require.NoError(t, dagjson.Decode(nb, strings.NewReader(json))) - - node := nb.Build() - require.NotNil(t, node) - - return node -} diff --git a/core/schema/struct.go b/core/schema/struct.go index a4dfa1f..69b99e2 100644 --- a/core/schema/struct.go +++ b/core/schema/struct.go @@ -3,8 +3,8 @@ package schema import ( "github.com/ipld/go-ipld-prime/schema" "github.com/storacha-network/go-ucanto/core/ipld" - "github.com/storacha-network/go-ucanto/core/policy" "github.com/storacha-network/go-ucanto/core/result/failure" + "github.com/ucan-wg/go-ucan/capability/policy" ) type strukt[T any] struct { diff --git a/go.mod b/go.mod index 12eddcc..989d9d9 100644 --- a/go.mod +++ b/go.mod @@ -11,23 +11,36 @@ require ( github.com/multiformats/go-multihash v0.2.3 github.com/multiformats/go-varint v0.0.7 github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 + github.com/ucan-wg/go-ucan v0.0.0-20240916120445-37f52863156c ) require ( - github.com/tidwall/gjson v1.17.1 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.1 // indirect - github.com/tidwall/sjson v1.2.5 // indirect + github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect + github.com/flynn/noise v1.1.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/pion/datachannel v1.5.9 // indirect + github.com/pion/logging v0.2.2 // indirect + github.com/pion/sctp v1.8.33 // indirect + github.com/pion/webrtc/v3 v3.3.2 // indirect + github.com/prometheus/client_golang v1.20.3 // indirect + github.com/quic-go/quic-go v0.47.0 // indirect + github.com/quic-go/webtransport-go v0.8.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/net v0.28.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/gobwas/glob v0.2.3 + github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.3.1 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/ipfs/bbloom v0.0.4 // indirect github.com/ipfs/go-block-format v0.2.0 // indirect @@ -46,9 +59,9 @@ require ( github.com/ipfs/go-verifcid v0.0.2 // indirect github.com/ipld/go-codec-dagpb v1.6.0 // indirect github.com/jbenet/goprocess v0.1.4 // indirect - github.com/klauspost/cpuid/v2 v2.2.3 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/mattn/go-isatty v0.0.17 // indirect - github.com/minio/sha256-simd v1.0.0 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect @@ -56,17 +69,16 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/polydawn/refmt v0.89.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/wI2L/jsondiff v0.6.0 github.com/whyrusleeping/cbor-gen v0.0.0-20230818171029-f91ae536ca25 // indirect go.opentelemetry.io/otel v1.13.0 // indirect go.opentelemetry.io/otel/trace v1.13.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.6.0 // indirect - golang.org/x/sys v0.5.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/sys v0.23.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/protobuf v1.28.1 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - lukechampine.com/blake3 v1.1.7 // indirect + lukechampine.com/blake3 v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index bdd4f8b..75ac23c 100644 --- a/go.sum +++ b/go.sum @@ -2,38 +2,50 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ= +github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gxed/hashland/keccakpg v0.0.1/go.mod h1:kRzw3HkwxFU1mpmPP8v1WyQzwdGfmKFJ6tItnhQ67kU= @@ -114,6 +126,8 @@ github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOan github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -121,14 +135,15 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= -github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8= github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -139,36 +154,32 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= -github.com/libp2p/go-libp2p v0.22.0 h1:2Tce0kHOp5zASFKJbNzRElvh0iZwdtG5uZheNW8chIw= -github.com/libp2p/go-libp2p v0.22.0/go.mod h1:UDolmweypBSjQb2f7xutPnwZ/fxioLbMBxSjRksxxU4= +github.com/libp2p/go-libp2p v0.36.2 h1:BbqRkDaGC3/5xfaJakLV/BrpjlAuYqSB0lRvtzL3B/U= +github.com/libp2p/go-libp2p v0.36.2/go.mod h1:XO3joasRE4Eup8yCTTP/+kX+g92mOgRaadk46LmPhHY= github.com/libp2p/go-libp2p-asn-util v0.2.0 h1:rg3+Os8jbnO5DxkC7K/Utdi+DkY3q/d1/1q+8WeNAsw= github.com/libp2p/go-libp2p-asn-util v0.2.0/go.mod h1:WoaWxbHKBymSN41hWSq/lGKJEca7TNm58+gGJi2WsLI= github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= -github.com/libp2p/go-msgio v0.2.0 h1:W6shmB+FeynDrUVl2dgFQvzfBZcXiyqY4VmpQLu9FqU= -github.com/libp2p/go-msgio v0.2.0/go.mod h1:dBVM1gW3Jk9XqHkU4eKdGvVHdLa51hoGfll6jMJMSlY= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg= github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM= github.com/libp2p/go-netroute v0.2.0 h1:0FpsbsvuSnAhXFnCY0VLFbJOzaK0VnP0r1QT/o4nWRE= github.com/libp2p/go-netroute v0.2.0/go.mod h1:Vio7LTzZ+6hoT4CMZi5/6CpY3Snzh2vgZhWgxMNwlQI= -github.com/libp2p/go-openssl v0.1.0 h1:LBkKEcUv6vtZIQLVTegAil8jbNpJErQ9AnT+bWV+Ooo= -github.com/libp2p/go-openssl v0.1.0/go.mod h1:OiOxwPpL3n4xlenjx2h7AwSGaFSC/KZvf6gNdOBQMtc= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= -github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= github.com/minio/sha256-simd v0.0.0-20190131020904-2d45a736cd16/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= -github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= -github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= @@ -201,9 +212,46 @@ github.com/multiformats/go-multistream v0.3.3/go.mod h1:ODRoqamLUsETKS9BNcII4gcR github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI= +github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA= +github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/ice/v2 v2.3.34 h1:Ic1ppYCj4tUOcPAp76U6F3fVrlSw8A9JtRXLqw6BbUM= +github.com/pion/ice/v2 v2.3.34/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ= +github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M= +github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= +github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= +github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= +github.com/pion/rtp v1.8.7 h1:qslKkG8qxvQ7hqaxkmL7Pl0XcUm+/Er7nMnu6Vq+ZxM= +github.com/pion/rtp v1.8.7/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw= +github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM= +github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= +github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= +github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk= +github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc= +github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= +github.com/pion/webrtc/v3 v3.3.2 h1:9Cjt3n90UVG1am1JZ3I7qdIosc7F2rVBeJdhlfqn9R4= +github.com/pion/webrtc/v3 v3.3.2/go.mod h1:hVmrDJvwhEertRWObeb1xzulzHGeVUoPlWvxdGzcfU0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -211,38 +259,40 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= +github.com/prometheus/client_golang v1.20.3 h1:oPksm4K8B+Vt35tUhw6GbSNSgVlVSBH0qELP/7u83l4= +github.com/prometheus/client_golang v1.20.3/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y= +github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E= +github.com/quic-go/webtransport-go v0.8.0 h1:HxSrwun11U+LlmwpgM1kEqIqH90IT4N8auv/cD7QFJg= +github.com/quic-go/webtransport-go v0.8.0/go.mod h1:N99tjprW432Ut5ONql/aUhSLT0YVSlwHohQsuac9WaM= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= -github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= -github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= -github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= -github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= -github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ucan-wg/go-ucan v0.0.0-20240916120445-37f52863156c h1:A1pMNIlHPnJ6KROqNc6SKg7QlSiQA6umiEoy89Os4cM= +github.com/ucan-wg/go-ucan v0.0.0-20240916120445-37f52863156c/go.mod h1:IiRc1OKWUk7FziOTWmOo7iwbcEMr7ch0lgs3UrF13pU= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/wI2L/jsondiff v0.6.0 h1:zrsH3FbfVa3JO9llxrcDy/XLkYPLgoMX6Mz3T2PP2AI= -github.com/wI2L/jsondiff v0.6.0/go.mod h1:D6aQ5gKgPF9g17j+E9N7aasmU1O+XvfmWm1y8UMmNpw= github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s= github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= @@ -250,6 +300,8 @@ github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvS github.com/whyrusleeping/cbor-gen v0.0.0-20230818171029-f91ae536ca25 h1:yVYDLoN2gmB3OdBXFW8e1UwgVbmCvNlnAKhvHPaNARI= github.com/whyrusleeping/cbor-gen v0.0.0-20230818171029-f91ae536ca25/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM= +github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -264,6 +316,8 @@ go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= @@ -278,47 +332,55 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190227160552-c95aed5357e7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -330,21 +392,21 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -355,5 +417,5 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= -lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= +lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= diff --git a/validator/lib.go b/validator/lib.go index 30f5a27..31d48da 100644 --- a/validator/lib.go +++ b/validator/lib.go @@ -8,15 +8,15 @@ import ( "github.com/storacha-network/go-ucanto/core/dag/blockstore" "github.com/storacha-network/go-ucanto/core/delegation" "github.com/storacha-network/go-ucanto/core/invocation" - "github.com/storacha-network/go-ucanto/core/policy" - "github.com/storacha-network/go-ucanto/core/policy/literal" - "github.com/storacha-network/go-ucanto/core/policy/selector" "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/core/schema" "github.com/storacha-network/go-ucanto/did" "github.com/storacha-network/go-ucanto/principal" "github.com/storacha-network/go-ucanto/ucan" vdm "github.com/storacha-network/go-ucanto/validator/datamodel" + "github.com/ucan-wg/go-ucan/capability/policy" + "github.com/ucan-wg/go-ucan/capability/policy/literal" + "github.com/ucan-wg/go-ucan/capability/policy/selector" ) func IsSelfIssued[Caveats any](capability ucan.Capability[Caveats], issuer did.DID) bool { From 9117b55c450791ec999723dbb82a8a43403e9d99 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 16 Sep 2024 16:24:08 +0100 Subject: [PATCH 34/37] refactor: use capability invoke and delegate shorthand in tests --- validator/lib_test.go | 153 +++++++++++++++++------------------------- 1 file changed, 60 insertions(+), 93 deletions(-) diff --git a/validator/lib_test.go b/validator/lib_test.go index cec5c75..0aab5d5 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -91,13 +91,11 @@ func TestAccess(t *testing.T) { t.Run("authorized", func(t *testing.T) { t.Run("self-issued invocation", func(t *testing.T) { - inv, err := invocation.Invoke( + inv, err := storeAdd.Invoke( fixtures.Alice, fixtures.Bob, - storeAdd.New( - fixtures.Alice.DID().String(), - storeAddCaveats{Link: testLink}, - ), + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, ) require.NoError(t, err) @@ -120,22 +118,19 @@ func TestAccess(t *testing.T) { }) t.Run("delegated invocation", func(t *testing.T) { - dlg, err := delegation.Delegate( + dlg, err := storeAdd.Delegate( fixtures.Alice, fixtures.Bob, - []ucan.Capability[storeAddCaveats]{ - storeAdd.New(fixtures.Alice.DID().String(), storeAddCaveats{}), - }, + fixtures.Alice.DID().String(), + storeAddCaveats{}, ) require.NoError(t, err) - inv, err := invocation.Invoke( + inv, err := storeAdd.Invoke( fixtures.Bob, fixtures.Service, - storeAdd.New( - fixtures.Alice.DID().String(), - storeAddCaveats{Link: testLink}, - ), + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, delegation.WithProof(delegation.FromDelegation(dlg)), ) require.NoError(t, err) @@ -159,32 +154,28 @@ func TestAccess(t *testing.T) { }) t.Run("delegation chain", func(t *testing.T) { - alice2bob, err := delegation.Delegate( + alice2bob, err := storeAdd.Delegate( fixtures.Alice, fixtures.Bob, - []ucan.Capability[storeAddCaveats]{ - storeAdd.New(fixtures.Alice.DID().String(), storeAddCaveats{}), - }, + fixtures.Alice.DID().String(), + storeAddCaveats{}, ) require.NoError(t, err) - bob2mallory, err := delegation.Delegate( + bob2mallory, err := storeAdd.Delegate( fixtures.Bob, fixtures.Mallory, - []ucan.Capability[storeAddCaveats]{ - storeAdd.New(fixtures.Alice.DID().String(), storeAddCaveats{}), - }, + fixtures.Alice.DID().String(), + storeAddCaveats{}, delegation.WithProof(delegation.FromDelegation(alice2bob)), ) require.NoError(t, err) - inv, err := invocation.Invoke( + inv, err := storeAdd.Invoke( fixtures.Mallory, fixtures.Service, - storeAdd.New( - fixtures.Alice.DID().String(), - storeAddCaveats{Link: testLink}, - ), + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, delegation.WithProof(delegation.FromDelegation(bob2mallory)), ) require.NoError(t, err) @@ -221,13 +212,11 @@ func TestAccess(t *testing.T) { t.Run("unauthorized", func(t *testing.T) { t.Run("expired invocation", func(t *testing.T) { exp := ucan.Now() - 5 - inv, err := invocation.Invoke( + inv, err := storeAdd.Invoke( fixtures.Alice, fixtures.Service, - storeAdd.New( - fixtures.Alice.DID().String(), - storeAddCaveats{Link: testLink}, - ), + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, delegation.WithExpiration(exp), ) require.NoError(t, err) @@ -255,13 +244,11 @@ func TestAccess(t *testing.T) { t.Run("not valid before", func(t *testing.T) { nbf := ucan.Now() + 500 - inv, err := invocation.Invoke( + inv, err := storeAdd.Invoke( fixtures.Alice, fixtures.Service, - storeAdd.New( - fixtures.Alice.DID().String(), - storeAddCaveats{Link: testLink}, - ), + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, delegation.WithNotBefore(nbf), ) require.NoError(t, err) @@ -288,13 +275,11 @@ func TestAccess(t *testing.T) { }) t.Run("invalid signature", func(t *testing.T) { - inv, err := invocation.Invoke( + inv, err := storeAdd.Invoke( fixtures.Alice, fixtures.Service, - storeAdd.New( - fixtures.Alice.DID().String(), - storeAddCaveats{Link: testLink}, - ), + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, ) require.NoError(t, err) @@ -359,13 +344,11 @@ func TestAccess(t *testing.T) { t.Run("invalid claim", func(t *testing.T) { t.Run("no proofs", func(t *testing.T) { - inv, err := invocation.Invoke( + inv, err := storeAdd.Invoke( fixtures.Alice, fixtures.Bob, - storeAdd.New( - fixtures.Bob.DID().String(), - storeAddCaveats{Link: testLink}, - ), + fixtures.Bob.DID().String(), + storeAddCaveats{Link: testLink}, ) require.NoError(t, err) @@ -394,23 +377,20 @@ func TestAccess(t *testing.T) { t.Run("expired", func(t *testing.T) { exp := ucan.Now() - 5 - dlg, err := delegation.Delegate( + dlg, err := storeAdd.Delegate( fixtures.Alice, fixtures.Bob, - []ucan.Capability[storeAddCaveats]{ - storeAdd.New(fixtures.Alice.DID().String(), storeAddCaveats{}), - }, + fixtures.Alice.DID().String(), + storeAddCaveats{}, delegation.WithExpiration(exp), ) require.NoError(t, err) - inv, err := invocation.Invoke( + inv, err := storeAdd.Invoke( fixtures.Bob, fixtures.Service, - storeAdd.New( - fixtures.Alice.DID().String(), - storeAddCaveats{Link: testLink}, - ), + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, delegation.WithProof(delegation.FromDelegation(dlg)), ) require.NoError(t, err) @@ -441,23 +421,20 @@ func TestAccess(t *testing.T) { t.Run("not valid before", func(t *testing.T) { nbf := ucan.Now() + 60*60 - dlg, err := delegation.Delegate( + dlg, err := storeAdd.Delegate( fixtures.Alice, fixtures.Bob, - []ucan.Capability[storeAddCaveats]{ - storeAdd.New(fixtures.Alice.DID().String(), storeAddCaveats{}), - }, + fixtures.Alice.DID().String(), + storeAddCaveats{}, delegation.WithNotBefore(nbf), ) require.NoError(t, err) - inv, err := invocation.Invoke( + inv, err := storeAdd.Invoke( fixtures.Bob, fixtures.Service, - storeAdd.New( - fixtures.Alice.DID().String(), - storeAddCaveats{Link: testLink}, - ), + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, delegation.WithProof(delegation.FromDelegation(dlg)), ) require.NoError(t, err) @@ -514,13 +491,11 @@ func TestAccess(t *testing.T) { dlg := delegation.NewDelegation(rt, bs) - inv, err := invocation.Invoke( + inv, err := storeAdd.Invoke( fixtures.Bob, fixtures.Service, - storeAdd.New( - fixtures.Alice.DID().String(), - storeAddCaveats{Link: testLink}, - ), + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, delegation.WithProof(delegation.FromDelegation(dlg)), ) require.NoError(t, err) @@ -559,13 +534,11 @@ func TestAccess(t *testing.T) { ) require.NoError(t, err) - inv, err := invocation.Invoke( + inv, err := storeAdd.Invoke( fixtures.Bob, fixtures.Service, - storeAdd.New( - fixtures.Alice.DID().String(), - storeAddCaveats{Link: testLink}, - ), + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, delegation.WithProof(delegation.FromDelegation(dlg)), ) require.NoError(t, err) @@ -597,22 +570,19 @@ func TestAccess(t *testing.T) { t.Run("malformed capability", func(t *testing.T) { badDID := fmt.Sprintf("bib:%s", fixtures.Alice.DID().String()[4:]) - dlg, err := delegation.Delegate( + dlg, err := storeAdd.Delegate( fixtures.Alice, fixtures.Bob, - []ucan.Capability[storeAddCaveats]{ - ucan.NewCapability("store/add", badDID, storeAddCaveats{}), - }, + badDID, + storeAddCaveats{}, ) require.NoError(t, err) - inv, err := invocation.Invoke( + inv, err := storeAdd.Invoke( fixtures.Bob, fixtures.Service, - storeAdd.New( - fixtures.Alice.DID().String(), - storeAddCaveats{Link: testLink}, - ), + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, delegation.WithProof(delegation.FromDelegation(dlg)), ) require.NoError(t, err) @@ -643,22 +613,19 @@ func TestAccess(t *testing.T) { }) t.Run("unavailable proof", func(t *testing.T) { - dlg, err := delegation.Delegate( + dlg, err := storeAdd.Delegate( fixtures.Alice, fixtures.Bob, - []ucan.Capability[storeAddCaveats]{ - ucan.NewCapability("store/add", fixtures.Alice.DID().String(), storeAddCaveats{}), - }, + fixtures.Alice.DID().String(), + storeAddCaveats{}, ) require.NoError(t, err) - inv, err := invocation.Invoke( + inv, err := storeAdd.Invoke( fixtures.Bob, fixtures.Service, - storeAdd.New( - fixtures.Alice.DID().String(), - storeAddCaveats{Link: testLink}, - ), + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, delegation.WithProof(delegation.FromLink(dlg.Link())), ) require.NoError(t, err) From 0229a79ce3447c1712b02f19b73fc238dd0b97d3 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 16 Sep 2024 16:38:06 +0100 Subject: [PATCH 35/37] test: external proof --- validator/lib_test.go | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/validator/lib_test.go b/validator/lib_test.go index 0aab5d5..54f4490 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -207,6 +207,52 @@ func TestAccess(t *testing.T) { require.Equal(t, fixtures.Alice.DID(), a.Proofs()[0].Proofs()[0].Issuer().DID()) require.Equal(t, fixtures.Bob.DID(), a.Proofs()[0].Proofs()[0].Audience().DID()) }) + + t.Run("resolve external proof", func(t *testing.T) { + dlg, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Alice.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + inv, err := storeAdd.Invoke( + fixtures.Bob, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{Link: testLink}, + delegation.WithProof(delegation.FromDelegation(dlg)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + func(p ucan.Link) (delegation.Delegation, UnavailableProof) { + if p == dlg.Link() { + return dlg, nil + } + return nil, NewUnavailableProofError(p, fmt.Errorf("no proof resolver configured")) + }, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.NoError(t, x) + require.Equal(t, storeAdd.Can(), a.Capability().Can()) + require.Equal(t, fixtures.Alice.DID().String(), a.Capability().With()) + require.Equal(t, fixtures.Bob.DID(), a.Issuer().DID()) + require.Equal(t, fixtures.Service.DID(), a.Audience().DID()) + + require.Equal(t, storeAdd.Can(), a.Proofs()[0].Capability().Can()) + require.Equal(t, fixtures.Alice.DID().String(), a.Proofs()[0].Capability().With()) + require.Equal(t, fixtures.Alice.DID(), a.Proofs()[0].Issuer().DID()) + require.Equal(t, fixtures.Bob.DID(), a.Proofs()[0].Audience().DID()) + }) }) t.Run("unauthorized", func(t *testing.T) { From e1ac2a32ef7fcc9ac48c94e14030af255184374a Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Mon, 16 Sep 2024 22:27:06 +0100 Subject: [PATCH 36/37] test: more validator tests --- principal/signer/signer.go | 72 +++++++++++++ principal/verifier/verifier.go | 55 ++++++++++ validator/lib_test.go | 178 ++++++++++++++++++++++++++++++++- 3 files changed, 300 insertions(+), 5 deletions(-) create mode 100644 principal/signer/signer.go create mode 100644 principal/verifier/verifier.go diff --git a/principal/signer/signer.go b/principal/signer/signer.go new file mode 100644 index 0000000..7b1f2c7 --- /dev/null +++ b/principal/signer/signer.go @@ -0,0 +1,72 @@ +package signer + +import ( + "fmt" + "strings" + + "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/principal" + "github.com/storacha-network/go-ucanto/principal/verifier" + "github.com/storacha-network/go-ucanto/ucan/crypto/signature" +) + +type Unwrapper interface { + // Unwrap returns the unwrapped did:key of this signer. + Unwrap() principal.Signer +} + +type WrappedSigner interface { + principal.Signer + Unwrapper +} + +type wrapsgn struct { + key principal.Signer + verifier principal.Verifier +} + +func (w wrapsgn) Code() uint64 { + return w.key.Code() +} + +func (w wrapsgn) DID() did.DID { + return w.verifier.DID() +} + +func (w wrapsgn) Encode() []byte { + return w.key.Encode() +} + +func (w wrapsgn) Sign(msg []byte) signature.SignatureView { + return w.key.Sign(msg) +} + +func (w wrapsgn) SignatureAlgorithm() string { + return w.key.SignatureAlgorithm() +} + +func (w wrapsgn) SignatureCode() uint64 { + return w.key.SignatureCode() +} + +func (w wrapsgn) Unwrap() principal.Signer { + return w.key +} + +func (w wrapsgn) Verifier() principal.Verifier { + return w.verifier +} + +// Wrap the key of this signer into a signer with a different DID. This is +// primarily used to wrap a did:key signer with a signer that has a DID of +// a different method. +func Wrap(key principal.Signer, id did.DID) (WrappedSigner, error) { + if !strings.HasPrefix(key.DID().String(), "did:key:") { + return nil, fmt.Errorf("verifier is not a did:key") + } + vrf, err := verifier.Wrap(key.Verifier(), id) + if err != nil { + return nil, err + } + return wrapsgn{key, vrf}, nil +} diff --git a/principal/verifier/verifier.go b/principal/verifier/verifier.go new file mode 100644 index 0000000..41beeeb --- /dev/null +++ b/principal/verifier/verifier.go @@ -0,0 +1,55 @@ +package verifier + +import ( + "fmt" + "strings" + + "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/principal" + "github.com/storacha-network/go-ucanto/ucan/crypto/signature" +) + +type Unwrapper interface { + // Unwrap returns the unwrapped did:key of this signer. + Unwrap() principal.Verifier +} + +type WrappedVerifier interface { + principal.Verifier + Unwrapper +} + +type wrapvf struct { + id did.DID + key principal.Verifier +} + +func (w wrapvf) Code() uint64 { + return w.key.Code() +} + +func (w wrapvf) DID() did.DID { + return w.id +} + +func (w wrapvf) Encode() []byte { + return w.key.Encode() +} + +func (w wrapvf) Verify(msg []byte, sig signature.Signature) bool { + return w.key.Verify(msg, sig) +} + +func (w wrapvf) Unwrap() principal.Verifier { + return w.key +} + +// Wrap the key of this verifier into a verifier with a different DID. This is +// primarily used to wrap a did:key verifier with a verifier that has a DID of +// a different method. +func Wrap(key principal.Verifier, id did.DID) (WrappedVerifier, error) { + if !strings.HasPrefix(key.DID().String(), "did:key:") { + return nil, fmt.Errorf("verifier is not a did:key") + } + return wrapvf{id, key}, nil +} diff --git a/validator/lib_test.go b/validator/lib_test.go index 54f4490..5f31440 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -18,8 +18,10 @@ import ( "github.com/storacha-network/go-ucanto/core/ipld/hash/sha256" "github.com/storacha-network/go-ucanto/core/result/failure" "github.com/storacha-network/go-ucanto/core/schema" + "github.com/storacha-network/go-ucanto/did" "github.com/storacha-network/go-ucanto/principal" "github.com/storacha-network/go-ucanto/principal/ed25519/verifier" + "github.com/storacha-network/go-ucanto/principal/signer" "github.com/storacha-network/go-ucanto/testing/fixtures" "github.com/storacha-network/go-ucanto/ucan" udm "github.com/storacha-network/go-ucanto/ucan/datamodel/ucan" @@ -81,13 +83,14 @@ func newStoreAddCapability(t *testing.T) CapabilityParser[storeAddCaveats] { ) } +var testLink = cidlink.Link{Cid: cid.MustParse("bafkqaaa")} +var validateAuthOk = func(auth Authorization[any]) Revoked { return nil } +var parseEdPrincipal = func(str string) (principal.Verifier, error) { + return verifier.Parse(str) +} + func TestAccess(t *testing.T) { storeAdd := newStoreAddCapability(t) - testLink := cidlink.Link{Cid: cid.MustParse("bafkqaaa")} - validateAuthOk := func(auth Authorization[any]) Revoked { return nil } - parseEdPrincipal := func(str string) (principal.Verifier, error) { - return verifier.Parse(str) - } t.Run("authorized", func(t *testing.T) { t.Run("self-issued invocation", func(t *testing.T) { @@ -841,6 +844,171 @@ func TestAccess(t *testing.T) { }, "\n") require.Equal(t, msg, x.Error()) }) + + t.Run("principal alignment", func(t *testing.T) { + prf, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Bob, + fixtures.Alice.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + nb := storeAddCaveats{Link: testLink} + inv, err := storeAdd.Invoke( + fixtures.Mallory, + fixtures.Service, + fixtures.Alice.DID().String(), + nb, + delegation.WithProof(delegation.FromDelegation(prf)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + cstr := fmt.Sprintf(`{"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}}`, storeAdd.Can(), fixtures.Alice.DID(), testLink) + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability %s is not authorized because:`, cstr), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Mallory.DID()), + fmt.Sprintf(` - Capability can not be derived from prf: %s because:`, prf.Link()), + fmt.Sprintf(` - Delegation audience is '%s' instead of '%s'`, fixtures.Bob.DID(), fixtures.Mallory.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("invalid delegation chain", func(t *testing.T) { + space := fixtures.Alice + + prf, err := storeAdd.Delegate( + space, + fixtures.Service, + space.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + nb := storeAddCaveats{Link: testLink} + inv, err := storeAdd.Invoke( + fixtures.Bob, + fixtures.Service, + space.DID().String(), + nb, + delegation.WithProof(delegation.FromDelegation(prf)), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + cstr := fmt.Sprintf(`{"can":"%s","with":"%s","nb":{"Link":{"/":"%s"},"Origin":null}}`, storeAdd.Can(), space.DID(), testLink) + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Capability %s is not authorized because:`, cstr), + fmt.Sprintf(" - Capability can not be (self) issued by '%s'", fixtures.Bob.DID()), + fmt.Sprintf(` - Capability can not be derived from prf: %s because:`, prf.Link()), + fmt.Sprintf(` - Delegation audience is '%s' instead of '%s'`, fixtures.Service.DID(), fixtures.Bob.DID()), + }, "\n") + require.Equal(t, msg, x.Error()) + }) + }) +} + +func TestClaim(t *testing.T) { + storeAdd := newStoreAddCapability(t) + + t.Run("without a proof", func(t *testing.T) { + dlg, err := storeAdd.Delegate( + fixtures.Alice, + fixtures.Service, + fixtures.Alice.DID().String(), + storeAddCaveats{}, + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Claim(storeAdd, []delegation.Proof{delegation.FromLink(dlg.Link())}, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Linked proof "%s" is not included and could not be resolved`, dlg.Link()), + ` - Proof resolution failed with: no proof resolver configured`, + }, "\n") + require.Equal(t, msg, x.Error()) + }) + + t.Run("mismatched signature", func(t *testing.T) { + svcdid, err := did.Parse("did:web:w3.storage") + require.NoError(t, err) + + old, err := signer.Wrap(fixtures.Alice, svcdid) + require.NoError(t, err) + + new, err := signer.Wrap(fixtures.Bob, svcdid) + require.NoError(t, err) + + dlg, err := storeAdd.Delegate( + old, + old, + old.DID().String(), + storeAddCaveats{Link: testLink}, + ) + require.NoError(t, err) + + context := NewValidationContext( + new.Verifier(), + storeAdd, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Claim(storeAdd, []delegation.Proof{delegation.FromDelegation(dlg)}, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + msg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", storeAdd), + fmt.Sprintf(` - Proof %s issued by %s does not have a valid signature from %s`, dlg.Link(), new.DID(), new.DID()), + ` ℹ️ Issuer probably signed with a different key, which got rotated, invalidating delegations that were issued with prior keys`, + }, "\n") + require.Equal(t, msg, x.Error()) }) } From 21a740f176b98a15ae9c737f3ef95b06a3e19f21 Mon Sep 17 00:00:00 2001 From: Alan Shaw Date: Tue, 17 Sep 2024 12:43:07 +0100 Subject: [PATCH 37/37] test: some attestation tests --- core/delegation/delegate.go | 47 +++-- core/delegation/delegation.go | 2 +- core/schema/did.go | 66 +++--- principal/absentee/absentee.go | 33 +++ principal/absentee/absentee_test.go | 25 +++ ucan/crypto/signature/signature.go | 12 ++ ucan/crypto/signature/signature_test.go | 19 ++ ucan/datamodel/payload/payload.go | 4 +- ucan/datamodel/payload/payload.ipldsch | 2 +- ucan/datamodel/ucan/ucan.go | 4 +- ucan/datamodel/ucan/ucan.ipldsch | 2 +- ucan/lib.go | 53 +++-- ucan/ucan.go | 2 +- ucan/view.go | 6 +- validator/datamodel/attestation.go | 5 + validator/error.go | 6 +- validator/lib_test.go | 65 +++--- validator/session_test.go | 264 ++++++++++++++++++++++++ 18 files changed, 513 insertions(+), 104 deletions(-) create mode 100644 principal/absentee/absentee.go create mode 100644 principal/absentee/absentee_test.go create mode 100644 ucan/crypto/signature/signature_test.go create mode 100644 validator/session_test.go diff --git a/core/delegation/delegate.go b/core/delegation/delegate.go index 9caa863..a4a7866 100644 --- a/core/delegation/delegate.go +++ b/core/delegation/delegate.go @@ -15,25 +15,38 @@ import ( type Option func(cfg *delegationConfig) error type delegationConfig struct { - exp uint64 - nbf uint64 - nnc string - fct []ucan.FactBuilder - prf Proofs + exp *int + noexp bool + nbf int + nnc string + fct []ucan.FactBuilder + prf Proofs } // WithExpiration configures the expiration time in UTC seconds since Unix -// epoch. Set this to -1 for no expiration. -func WithExpiration(exp uint64) Option { +// epoch. +func WithExpiration(exp int) Option { return func(cfg *delegationConfig) error { - cfg.exp = exp + cfg.exp = &exp + cfg.noexp = false + return nil + } +} + +// WithNoExpiration configures the UCAN to never expire. +// +// WARNING: this will cause the delegation to be valid FOREVER, unless revoked. +func WithNoExpiration() Option { + return func(cfg *delegationConfig) error { + cfg.exp = nil + cfg.noexp = true return nil } } // WithNotBefore configures the time in UTC seconds since Unix epoch when the // UCAN will become valid. -func WithNotBefore(nbf uint64) Option { +func WithNotBefore(nbf int) Option { return func(cfg *delegationConfig) error { cfg.nbf = nbf return nil @@ -97,16 +110,20 @@ func Delegate[C ucan.CaveatBuilder](issuer ucan.Signer, audience ucan.Principal, return nil, err } - data, err := ucan.Issue( - issuer, - audience, - capabilities, - ucan.WithExpiration(cfg.exp), + opts := []ucan.Option{ ucan.WithFacts(cfg.fct), ucan.WithNonce(cfg.nnc), ucan.WithNotBefore(cfg.nbf), ucan.WithProofs(links), - ) + } + if cfg.noexp { + opts = append(opts, ucan.WithNoExpiration()) + } + if cfg.exp != nil { + opts = append(opts, ucan.WithExpiration(*cfg.exp)) + } + + data, err := ucan.Issue(issuer, audience, capabilities, opts...) if err != nil { return nil, fmt.Errorf("issuing UCAN: %s", err) } diff --git a/core/delegation/delegation.go b/core/delegation/delegation.go index b8310de..7a58e48 100644 --- a/core/delegation/delegation.go +++ b/core/delegation/delegation.go @@ -88,7 +88,7 @@ func (d *delegation) Capabilities() []ucan.Capability[any] { return d.Data().Capabilities() } -func (d *delegation) Expiration() ucan.UTCUnixTimestamp { +func (d *delegation) Expiration() *ucan.UTCUnixTimestamp { return d.Data().Expiration() } diff --git a/core/schema/did.go b/core/schema/did.go index d9d7b20..1a6c03e 100644 --- a/core/schema/did.go +++ b/core/schema/did.go @@ -8,35 +8,51 @@ import ( "github.com/storacha-network/go-ucanto/did" ) -var didreader = reader[string, did.DID]{ - readFunc: func(input string) (did.DID, failure.Failure) { - pfx := "did:" - if !strings.HasPrefix(input, pfx) { - return did.Undef, NewSchemaError(fmt.Sprintf(`Expected a "%s" but got "%s" instead`, pfx, input)) - } - d, err := did.Parse(input) - if err != nil { - return did.Undef, NewSchemaError(err.Error()) - } - return d, nil - }, +type didConfig struct { + method string } -func DID() Reader[string, did.DID] { - return didreader +type DIDOption func(*didConfig) + +func WithMethod(method string) DIDOption { + return func(c *didConfig) { + c.method = method + } } -// DIDString read a string that is in DID format. -func DIDString() Reader[string, string] { - return didstrreader +func DID(opts ...DIDOption) Reader[string, did.DID] { + c := &didConfig{} + for _, opt := range opts { + opt(c) + } + return reader[string, did.DID]{ + readFunc: func(input string) (did.DID, failure.Failure) { + pfx := "did:" + if c.method != "" { + pfx = fmt.Sprintf("%s%s:", pfx, c.method) + } + if !strings.HasPrefix(input, pfx) { + return did.Undef, NewSchemaError(fmt.Sprintf(`Expected a "%s" but got "%s" instead`, pfx, input)) + } + d, err := did.Parse(input) + if err != nil { + return did.Undef, NewSchemaError(err.Error()) + } + return d, nil + }, + } } -var didstrreader = reader[string, string]{ - readFunc: func(input string) (string, failure.Failure) { - d, err := DID().Read(input) - if err != nil { - return "", err - } - return d.String(), nil - }, +// DIDString read a string that is in DID format. +func DIDString(opts ...DIDOption) Reader[string, string] { + rdr := DID(opts...) + return reader[string, string]{ + readFunc: func(input string) (string, failure.Failure) { + d, err := rdr.Read(input) + if err != nil { + return "", err + } + return d.String(), nil + }, + } } diff --git a/principal/absentee/absentee.go b/principal/absentee/absentee.go new file mode 100644 index 0000000..7b030cc --- /dev/null +++ b/principal/absentee/absentee.go @@ -0,0 +1,33 @@ +package absentee + +import ( + "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/ucan" + "github.com/storacha-network/go-ucanto/ucan/crypto/signature" +) + +type absentee struct { + id did.DID +} + +func (a absentee) DID() did.DID { + return a.id +} + +func (a absentee) Sign(msg []byte) signature.SignatureView { + return signature.NewSignatureView(signature.NewNonStandard(a.SignatureAlgorithm(), []byte{})) +} + +func (a absentee) SignatureAlgorithm() string { + return "" +} + +func (a absentee) SignatureCode() uint64 { + return signature.NON_STANDARD +} + +// From creates a special type of signer that produces an absent signature, +// which signals that verifier needs to verify authorization interactively. +func From(id did.DID) ucan.Signer { + return absentee{id} +} diff --git a/principal/absentee/absentee_test.go b/principal/absentee/absentee_test.go new file mode 100644 index 0000000..dd40dfa --- /dev/null +++ b/principal/absentee/absentee_test.go @@ -0,0 +1,25 @@ +package absentee + +import ( + "testing" + + "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/ucan/crypto/signature" + "github.com/stretchr/testify/require" +) + +func TestAbsentee(t *testing.T) { + t.Run("it can sign", func(t *testing.T) { + alicedid, err := did.Parse("did:mailto:web.mail:alice") + require.NoError(t, err) + + signer := From(alicedid) + require.Equal(t, alicedid, signer.DID()) + require.Equal(t, "", signer.SignatureAlgorithm()) + require.Equal(t, signature.NON_STANDARD, int(signer.SignatureCode())) + + sig := signer.Sign([]byte("hello world")) + require.Equal(t, signature.NON_STANDARD, int(sig.Code())) + require.Equal(t, []byte{}, sig.Raw()) + }) +} diff --git a/ucan/crypto/signature/signature.go b/ucan/crypto/signature/signature.go index d6fa708..78e7ba6 100644 --- a/ucan/crypto/signature/signature.go +++ b/ucan/crypto/signature/signature.go @@ -86,6 +86,18 @@ func NewSignature(code uint64, raw []byte) Signature { return sig } +func NewNonStandard(name string, raw []byte) Signature { + code := uint64(NON_STANDARD) + cl := varint.UvarintSize(code) + rl := varint.UvarintSize(uint64(len(raw))) + sig := make(signature, cl+rl+len(raw)+len(name)) + varint.PutUvarint(sig, code) + varint.PutUvarint(sig[cl:], uint64(len(raw))) + copy(sig[cl+rl:], raw) + copy(sig[cl+rl+len(raw):], name) + return sig +} + func Encode(s Signature) []byte { return s.Bytes() } diff --git a/ucan/crypto/signature/signature_test.go b/ucan/crypto/signature/signature_test.go new file mode 100644 index 0000000..61bb8d2 --- /dev/null +++ b/ucan/crypto/signature/signature_test.go @@ -0,0 +1,19 @@ +package signature + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSignature(t *testing.T) { + t.Run("roundtrip", func(t *testing.T) { + raw, err := CodeName(EdDSA) + require.NoError(t, err) + + s := NewSignature(EdDSA, []byte(raw)) + d := Decode(Encode(s)) + require.Equal(t, EdDSA, int(d.Code())) + require.Equal(t, raw, string(d.Raw())) + }) +} diff --git a/ucan/datamodel/payload/payload.go b/ucan/datamodel/payload/payload.go index 2cde3ce..1dc3806 100644 --- a/ucan/datamodel/payload/payload.go +++ b/ucan/datamodel/payload/payload.go @@ -38,8 +38,8 @@ type PayloadModel struct { Aud string Att []udm.CapabilityModel Prf []string - Exp uint64 + Exp *int Fct []udm.FactModel Nnc *string - Nbf *uint64 + Nbf *int } diff --git a/ucan/datamodel/payload/payload.ipldsch b/ucan/datamodel/payload/payload.ipldsch index 220953d..30ea23c 100644 --- a/ucan/datamodel/payload/payload.ipldsch +++ b/ucan/datamodel/payload/payload.ipldsch @@ -6,7 +6,7 @@ type Payload struct { att [Capability] # String representation of a link to a UCAN. prf [String] (implied []) - exp Int + exp nullable Int fct optional [Fact] nnc optional String nbf optional Int diff --git a/ucan/datamodel/ucan/ucan.go b/ucan/datamodel/ucan/ucan.go index 310a05f..7c009fc 100644 --- a/ucan/datamodel/ucan/ucan.go +++ b/ucan/datamodel/ucan/ucan.go @@ -40,10 +40,10 @@ type UCANModel struct { S []byte Att []CapabilityModel Prf []ipld.Link - Exp uint64 + Exp *int Fct []FactModel Nnc *string - Nbf *uint64 + Nbf *int } type CapabilityModel struct { diff --git a/ucan/datamodel/ucan/ucan.ipldsch b/ucan/datamodel/ucan/ucan.ipldsch index 992dea2..b66deeb 100644 --- a/ucan/datamodel/ucan/ucan.ipldsch +++ b/ucan/datamodel/ucan/ucan.ipldsch @@ -11,7 +11,7 @@ type UCAN struct { # All proofs are links, however you could still inline proof # by using CID with identity hashing algorithm prf optional [&UCAN] - exp Int + exp nullable Int fct optional [Fact] nnc optional String diff --git a/ucan/lib.go b/ucan/lib.go index e8faa9e..e661bc5 100644 --- a/ucan/lib.go +++ b/ucan/lib.go @@ -17,25 +17,38 @@ const version = "0.9.1" type Option func(cfg *ucanConfig) error type ucanConfig struct { - exp uint64 - nbf uint64 - nnc string - fct []FactBuilder - prf []Link + exp *UTCUnixTimestamp + noexp bool + nbf UTCUnixTimestamp + nnc string + fct []FactBuilder + prf []Link } // WithExpiration configures the expiration time in UTC seconds since Unix // epoch. -func WithExpiration(exp uint64) Option { +func WithExpiration(exp UTCUnixTimestamp) Option { return func(cfg *ucanConfig) error { - cfg.exp = exp + cfg.exp = &exp + cfg.noexp = false + return nil + } +} + +// WithNoExpiration configures the UCAN to never expire. +// +// WARNING: this will cause the delegation to be valid FOREVER, unless revoked. +func WithNoExpiration() Option { + return func(cfg *ucanConfig) error { + cfg.exp = nil + cfg.noexp = true return nil } } // WithNotBefore configures the time in UTC seconds since Unix epoch when the // UCAN will become valid. -func WithNotBefore(nbf uint64) Option { +func WithNotBefore(nbf int) Option { return func(cfg *ucanConfig) error { cfg.nbf = nbf return nil @@ -88,8 +101,14 @@ func Issue[C CaveatBuilder](issuer Signer, audience Principal, capabilities []Ca } } - if cfg.exp == 0 { - cfg.exp = Now() + 30 + var exp *int + if !cfg.noexp { + if cfg.exp == nil { + in30s := int(Now() + 30) + exp = &in30s + } else { + exp = cfg.exp + } } var capsmdl []udm.CapabilityModel @@ -132,7 +151,7 @@ func Issue[C CaveatBuilder](issuer Signer, audience Principal, capabilities []Ca Aud: audience.DID().String(), Att: capsmdl, Prf: prfstrs, - Exp: cfg.exp, + Exp: exp, Fct: fctsmdl, } if cfg.nnc != "" { @@ -153,7 +172,7 @@ func Issue[C CaveatBuilder](issuer Signer, audience Principal, capabilities []Ca Aud: audience.DID().Bytes(), Att: capsmdl, Prf: cfg.prf, - Exp: cfg.exp, + Exp: exp, Fct: fctsmdl, } if cfg.nnc != "" { @@ -203,7 +222,11 @@ func VerifySignature(ucan View, verifier Verifier) (bool, error) { // IsExpired checks if a UCAN is expired. func IsExpired(ucan UCAN) bool { - return ucan.Expiration() <= Now() + exp := ucan.Expiration() + if exp == nil { + return false + } + return *exp <= Now() } // IsTooEarly checks if a UCAN is not active yet. @@ -214,6 +237,6 @@ func IsTooEarly(ucan UCAN) bool { // Now returns a UTC Unix timestamp for comparing it against time window of the // UCAN. -func Now() uint64 { - return uint64(time.Now().Unix()) +func Now() UTCUnixTimestamp { + return UTCUnixTimestamp(time.Now().Unix()) } diff --git a/ucan/ucan.go b/ucan/ucan.go index 2870718..6f567a3 100644 --- a/ucan/ucan.go +++ b/ucan/ucan.go @@ -44,7 +44,7 @@ type Link = ipld.Link type Version = string // UTCUnixTimestamp is a timestamp in milliseconds since the Unix epoch. -type UTCUnixTimestamp = uint64 +type UTCUnixTimestamp = int // https://github.com/ucan-wg/spec/#324-nonce type Nonce = string diff --git a/ucan/view.go b/ucan/view.go index fdd6e57..44b4656 100644 --- a/ucan/view.go +++ b/ucan/view.go @@ -19,7 +19,7 @@ type UCAN interface { Capabilities() []Capability[any] // Expiration is the time in seconds since the Unix epoch that the UCAN // becomes invalid. - Expiration() UTCUnixTimestamp + Expiration() *UTCUnixTimestamp // NotBefore is the time in seconds since the Unix epoch that the UCAN // becomes valid. NotBefore() UTCUnixTimestamp @@ -63,7 +63,7 @@ func (v *ucanView) Capabilities() []Capability[any] { return caps } -func (v *ucanView) Expiration() uint64 { +func (v *ucanView) Expiration() *UTCUnixTimestamp { return v.model.Exp } @@ -98,7 +98,7 @@ func (v *ucanView) Nonce() string { return *v.model.Nnc } -func (v *ucanView) NotBefore() uint64 { +func (v *ucanView) NotBefore() int { if v.model.Nbf == nil { return 0 } diff --git a/validator/datamodel/attestation.go b/validator/datamodel/attestation.go index d2c88f8..7e8258c 100644 --- a/validator/datamodel/attestation.go +++ b/validator/datamodel/attestation.go @@ -6,6 +6,7 @@ import ( "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/schema" + ucanipld "github.com/storacha-network/go-ucanto/core/ipld" ) //go:embed attestation.ipldsch @@ -27,3 +28,7 @@ func AttestationType() schema.Type { type AttestationModel struct { Proof ipld.Link } + +func (m AttestationModel) Build() (ipld.Node, error) { + return ucanipld.WrapWithRecovery(&m, AttestationType()) +} diff --git a/validator/error.go b/validator/error.go index 1c64a35..8070b98 100644 --- a/validator/error.go +++ b/validator/error.go @@ -367,17 +367,19 @@ func NewExpiredError(delegation delegation.Delegation) InvalidProof { } func (ee ExpiredError) Error() string { + exp := ee.delegation.Expiration() return fmt.Sprintf("Proof %s has expired on %s", ee.delegation.Link(), - time.Unix(int64(ee.delegation.Expiration()), 0).Format(time.RFC3339)) + time.Unix(int64(*exp), 0).Format(time.RFC3339)) } func (ee ExpiredError) Build() (datamodel.Node, error) { name := ee.Name() stack := ee.Stack() + exp := ee.delegation.Expiration() expiredModel := vdm.ExpiredModel{ Name: &name, Message: ee.Error(), - ExpiredAt: int64(ee.delegation.Expiration()), + ExpiredAt: int64(*exp), Stack: &stack, } return ipld.WrapWithRecovery(expiredModel, vdm.ExpiredType()) diff --git a/validator/lib_test.go b/validator/lib_test.go index 5f31440..e24f1f5 100644 --- a/validator/lib_test.go +++ b/validator/lib_test.go @@ -23,6 +23,7 @@ import ( "github.com/storacha-network/go-ucanto/principal/ed25519/verifier" "github.com/storacha-network/go-ucanto/principal/signer" "github.com/storacha-network/go-ucanto/testing/fixtures" + "github.com/storacha-network/go-ucanto/testing/helpers" "github.com/storacha-network/go-ucanto/ucan" udm "github.com/storacha-network/go-ucanto/ucan/datamodel/ucan" "github.com/stretchr/testify/require" @@ -49,39 +50,34 @@ func (c storeAddCaveats) Build() (ipld.Node, error) { return nb.Build(), nil } -func newStoreAddCapability(t *testing.T) CapabilityParser[storeAddCaveats] { - t.Helper() - - typ, err := ipld.LoadSchemaBytes([]byte(` - type StoreAddCaveats struct { - link Link - origin optional Link +var storeAddTyp = helpers.Must(ipld.LoadSchemaBytes([]byte(` + type StoreAddCaveats struct { + link Link + origin optional Link + } +`))) + +var storeAdd = NewCapability( + "store/add", + schema.DIDString(), + schema.Struct[storeAddCaveats](storeAddTyp.TypeByName("StoreAddCaveats"), nil), + func(claimed, delegated ucan.Capability[storeAddCaveats]) failure.Failure { + if claimed.With() != delegated.With() { + err := fmt.Errorf("Expected 'with: \"%s\"' instead got '%s'", delegated.With(), claimed.With()) + return failure.FromError(err) } - `)) - require.NoError(t, err) - - return NewCapability( - "store/add", - schema.DIDString(), - schema.Struct[storeAddCaveats](typ.TypeByName("StoreAddCaveats"), nil), - func(claimed, delegated ucan.Capability[storeAddCaveats]) failure.Failure { - if claimed.With() != delegated.With() { - err := fmt.Errorf("Expected 'with: \"%s\"' instead got '%s'", delegated.With(), claimed.With()) - return failure.FromError(err) + if delegated.Nb().Link != nil && delegated.Nb().Link != claimed.Nb().Link { + var err error + if claimed.Nb().Link == nil { + err = fmt.Errorf("Link violates imposed %s constraint", delegated.Nb().Link) + } else { + err = fmt.Errorf("Link %s violates imposed %s constraint", claimed.Nb().Link, delegated.Nb().Link) } - if delegated.Nb().Link != nil && delegated.Nb().Link != claimed.Nb().Link { - var err error - if claimed.Nb().Link == nil { - err = fmt.Errorf("Link violates imposed %s constraint", delegated.Nb().Link) - } else { - err = fmt.Errorf("Link %s violates imposed %s constraint", claimed.Nb().Link, delegated.Nb().Link) - } - return failure.FromError(err) - } - return nil - }, - ) -} + return failure.FromError(err) + } + return nil + }, +) var testLink = cidlink.Link{Cid: cid.MustParse("bafkqaaa")} var validateAuthOk = func(auth Authorization[any]) Revoked { return nil } @@ -90,8 +86,6 @@ var parseEdPrincipal = func(str string) (principal.Verifier, error) { } func TestAccess(t *testing.T) { - storeAdd := newStoreAddCapability(t) - t.Run("authorized", func(t *testing.T) { t.Run("self-issued invocation", func(t *testing.T) { inv, err := storeAdd.Invoke( @@ -517,6 +511,7 @@ func TestAccess(t *testing.T) { // to create a UCAN model, manually setting the signature to something bad // and then encode it as the root block of the delegation. nb, _ := storeAddCaveats{Link: testLink}.Build() + exp := ucan.Now() + 30 model := udm.UCANModel{ V: "0.9.1", S: fixtures.Alice.Sign([]byte{}).Bytes(), @@ -529,7 +524,7 @@ func TestAccess(t *testing.T) { Nb: nb, }, }, - Exp: ucan.Now() + 30, + Exp: &exp, } rt, err := block.Encode(&model, udm.Type(), cbor.Codec, sha256.Hasher) @@ -938,8 +933,6 @@ func TestAccess(t *testing.T) { } func TestClaim(t *testing.T) { - storeAdd := newStoreAddCapability(t) - t.Run("without a proof", func(t *testing.T) { dlg, err := storeAdd.Delegate( fixtures.Alice, diff --git a/validator/session_test.go b/validator/session_test.go new file mode 100644 index 0000000..5b2556a --- /dev/null +++ b/validator/session_test.go @@ -0,0 +1,264 @@ +package validator + +import ( + "encoding/base64" + "fmt" + "strings" + "testing" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/storacha-network/go-ucanto/core/delegation" + "github.com/storacha-network/go-ucanto/core/result/failure" + "github.com/storacha-network/go-ucanto/core/schema" + "github.com/storacha-network/go-ucanto/did" + "github.com/storacha-network/go-ucanto/principal/absentee" + "github.com/storacha-network/go-ucanto/principal/ed25519/signer" + "github.com/storacha-network/go-ucanto/testing/fixtures" + "github.com/storacha-network/go-ucanto/testing/helpers" + "github.com/storacha-network/go-ucanto/ucan" + "github.com/stretchr/testify/require" +) + +type debugEchoCaveats struct { + Message *string +} + +func (c debugEchoCaveats) Build() (ipld.Node, error) { + np := basicnode.Prototype.Any + nb := np.NewBuilder() + ma, _ := nb.BeginMap(1) + if c.Message != nil { + ma.AssembleKey().AssignString("message") + ma.AssembleValue().AssignString(*c.Message) + } + ma.Finish() + return nb.Build(), nil +} + +var debugEchoTyp = helpers.Must(ipld.LoadSchemaBytes([]byte(` + type DebugEchoCaveats struct { + message optional String + } +`))) + +var debugEcho = NewCapability( + "debug/echo", + schema.DIDString(schema.WithMethod("mailto")), + schema.Struct[debugEchoCaveats](debugEchoTyp.TypeByName("DebugEchoCaveats"), nil), + func(claimed, delegated ucan.Capability[debugEchoCaveats]) failure.Failure { + if claimed.With() != delegated.With() { + err := fmt.Errorf("Expected 'with: \"%s\"' instead got '%s'", delegated.With(), claimed.With()) + return failure.FromError(err) + } + return nil + }, +) + +type attestCaveats struct { + Proof ipld.Link +} + +func (c attestCaveats) Build() (ipld.Node, error) { + np := basicnode.Prototype.Any + nb := np.NewBuilder() + ma, _ := nb.BeginMap(1) + ma.AssembleKey().AssignString("proof") + ma.AssembleValue().AssignLink(c.Proof) + ma.Finish() + return nb.Build(), nil +} + +var attestTyp = helpers.Must(ipld.LoadSchemaBytes([]byte(` + type AttestCaveats struct { + proof Link + } +`))) + +var attest = NewCapability( + "ucan/attest", + schema.DIDString(), + schema.Struct[attestCaveats](attestTyp.TypeByName("AttestCaveats"), nil), + func(claimed, delegated ucan.Capability[attestCaveats]) failure.Failure { + if claimed.With() != delegated.With() { + err := fmt.Errorf("Expected 'with: \"%s\"' instead got '%s'", delegated.With(), claimed.With()) + return failure.FromError(err) + } + return nil + }, +) + +func TestSession(t *testing.T) { + t.Run("validate mailto", func(t *testing.T) { + agent := fixtures.Alice + account := absentee.From(helpers.Must(did.Parse("did:mailto:web.mail:alice"))) + + prf, err := debugEcho.Delegate( + account, + agent, + account.DID().String(), + debugEchoCaveats{}, + ) + require.NoError(t, err) + + session, err := attest.Delegate( + fixtures.Service, + agent, + fixtures.Service.DID().String(), + attestCaveats{Proof: prf.Link()}, + ) + require.NoError(t, err) + + msg := "Hello World" + nb := debugEchoCaveats{Message: &msg} + inv, err := debugEcho.Invoke( + agent, + fixtures.Service, + account.DID().String(), + nb, + delegation.WithProofs(delegation.Proofs{ + delegation.FromDelegation(prf), + delegation.FromDelegation(session), + }), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + debugEcho, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.NoError(t, x) + require.Equal(t, debugEcho.Can(), a.Capability().Can()) + require.Equal(t, account.DID().String(), a.Capability().With()) + require.Equal(t, nb, a.Capability().Nb()) + }) + + t.Run("delegated ucan attest", func(t *testing.T) { + agent := fixtures.Alice + account := absentee.From(helpers.Must(did.Parse("did:mailto:web.mail:alice"))) + + manager, err := signer.Generate() + require.NoError(t, err) + worker, err := signer.Generate() + require.NoError(t, err) + + authority, err := delegation.Delegate( + manager, + worker, + []ucan.Capability[ucan.NoCaveats]{ + ucan.NewCapability("*", fixtures.Service.DID().String(), ucan.NoCaveats{}), + }, + delegation.WithNoExpiration(), + delegation.WithProof( + delegation.FromDelegation( + helpers.Must( + delegation.Delegate( + fixtures.Service, + manager, + []ucan.Capability[ucan.NoCaveats]{ + ucan.NewCapability("*", fixtures.Service.DID().String(), ucan.NoCaveats{}), + }, + ), + ), + ), + ), + ) + require.NoError(t, err) + + prf, err := debugEcho.Delegate( + account, + agent, + account.DID().String(), + debugEchoCaveats{}, + delegation.WithNoExpiration(), + ) + require.NoError(t, err) + + require.Equal( + t, + helpers.Must(base64.RawStdEncoding.DecodeString("gKADAA")), + prf.Signature().Bytes(), + "should have blank signature", + ) + + session, err := attest.Delegate( + worker, + agent, + fixtures.Service.DID().String(), + attestCaveats{Proof: prf.Link()}, + delegation.WithProof(delegation.FromDelegation(authority)), + ) + require.NoError(t, err) + + msg := "Hello World" + nb := debugEchoCaveats{Message: &msg} + inv, err := debugEcho.Invoke( + agent, + fixtures.Service, + account.DID().String(), + nb, + delegation.WithProofs(delegation.Proofs{ + delegation.FromDelegation(session), + delegation.FromDelegation(prf), + }), + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + debugEcho, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.NoError(t, x) + require.Equal(t, debugEcho.Can(), a.Capability().Can()) + require.Equal(t, account.DID().String(), a.Capability().With()) + require.Equal(t, nb, a.Capability().Nb()) + }) + + t.Run("fail without proofs", func(t *testing.T) { + account := absentee.From(helpers.Must(did.Parse("did:mailto:web.mail:alice"))) + + msg := "Hello World" + nb := debugEchoCaveats{Message: &msg} + inv, err := debugEcho.Invoke( + account, + fixtures.Service, + account.DID().String(), + nb, + ) + require.NoError(t, err) + + context := NewValidationContext( + fixtures.Service.Verifier(), + debugEcho, + IsSelfIssued, + validateAuthOk, + ProofUnavailable, + parseEdPrincipal, + FailDIDKeyResolution, + ) + + a, x := Access(inv, context) + require.Nil(t, a) + require.Error(t, x) + require.Equal(t, x.Name(), "Unauthorized") + errmsg := strings.Join([]string{ + fmt.Sprintf("Claim %s is not authorized", debugEcho), + fmt.Sprintf(` - Unable to resolve '%s' key`, account.DID()), + }, "\n") + require.Equal(t, errmsg, x.Error()) + }) +}