github.com/storacha/go-ucanto@v0.7.2/validator/capability.go (about) 1 package validator 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/storacha/go-ucanto/core/delegation" 8 "github.com/storacha/go-ucanto/core/invocation" 9 "github.com/storacha/go-ucanto/core/result/failure" 10 "github.com/storacha/go-ucanto/core/schema" 11 "github.com/storacha/go-ucanto/ucan" 12 ) 13 14 type Source interface { 15 Capability() ucan.Capability[any] 16 Delegation() delegation.Delegation 17 } 18 19 type source struct { 20 capability ucan.Capability[any] 21 delegation delegation.Delegation 22 } 23 24 func (s source) Capability() ucan.Capability[any] { 25 return s.capability 26 } 27 28 func (s source) Delegation() delegation.Delegation { 29 return s.delegation 30 } 31 32 func NewSource(capability ucan.Capability[any], delegation delegation.Delegation) Source { 33 return source{capability, delegation} 34 } 35 36 type Matcher[Caveats any] interface { 37 Match(source Source) (Match[Caveats], InvalidCapability) 38 } 39 40 type Selector[Caveats any] interface { 41 Select(sources []Source) ([]Match[Caveats], []DelegationError, []ucan.Capability[any]) 42 } 43 44 type Match[Caveats any] interface { 45 Selector[Caveats] 46 Source() []Source 47 Value() ucan.Capability[Caveats] 48 Proofs() []delegation.Delegation 49 Prune(context CanIssuer[Caveats]) Match[Caveats] 50 } 51 52 type match[Caveats any] struct { 53 sources []Source 54 value ucan.Capability[Caveats] 55 descriptor Descriptor[Caveats] 56 } 57 58 func (m match[Caveats]) Proofs() []delegation.Delegation { 59 return []delegation.Delegation{m.sources[0].Delegation()} 60 } 61 62 func (m match[Caveats]) Prune(context CanIssuer[Caveats]) Match[Caveats] { 63 if context.CanIssue(m.value, m.sources[0].Delegation().Issuer().DID()) { 64 return nil 65 } 66 return m 67 } 68 69 func (m match[Caveats]) Source() []Source { 70 return m.sources 71 } 72 73 func (m match[Caveats]) Value() ucan.Capability[Caveats] { 74 return m.value 75 } 76 77 func (m match[Caveats]) Select(sources []Source) (matches []Match[Caveats], errors []DelegationError, unknowns []ucan.Capability[any]) { 78 for _, source := range sources { 79 cap, err := ResolveCapability(m.descriptor, m.value, source) 80 if err != nil { 81 if uerr, ok := err.(UnknownCapability); ok { 82 unknowns = append(unknowns, uerr.Capability()) 83 } else if merr, ok := err.(MalformedCapability); ok { 84 errors = append(errors, NewDelegationError([]DelegationSubError{merr}, m)) 85 } else { 86 panic(fmt.Errorf("unexpected error type in resolved capability result: %w", err)) 87 } 88 continue 89 } 90 91 derr := m.descriptor.Derives(m.value, cap) 92 if derr != nil { 93 errors = append(errors, NewDelegationError([]DelegationSubError{NewEscalatedCapabilityError(m.value, cap, derr)}, m)) 94 continue 95 } 96 97 matches = append(matches, NewMatch(source, cap, m.descriptor)) 98 } 99 return 100 } 101 102 func (m match[Caveats]) String() string { 103 s, _ := m.value.MarshalJSON() 104 return string(s) 105 } 106 107 func NewMatch[Caveats any](source Source, capability ucan.Capability[Caveats], descriptor Descriptor[Caveats]) Match[Caveats] { 108 return match[Caveats]{[]Source{source}, capability, descriptor} 109 } 110 111 type CapabilityParser[Caveats any] interface { 112 Matcher[Caveats] 113 Selector[Caveats] 114 Can() ucan.Ability 115 // New creates a new capability from the passed options. 116 New(with ucan.Resource, nb Caveats) ucan.Capability[Caveats] 117 // Delegate creates a new signed token for this capability. If expiration is 118 // not set it defaults to 30 seconds from now. 119 Delegate(issuer ucan.Signer, audience ucan.Principal, with ucan.Resource, nb Caveats, options ...delegation.Option) (delegation.Delegation, error) 120 // Invoke creates an invocation of this capability. 121 Invoke(issuer ucan.Signer, audience ucan.Principal, with ucan.Resource, nb Caveats, options ...delegation.Option) (invocation.IssuedInvocation, error) 122 } 123 124 type Derivable[Caveats any] interface { 125 // Derives determines if a capability is derivable from another. Return `nil` 126 // to indicate the delegated capability can be derived from the claimed 127 // capability. 128 Derives(claimed, delegated ucan.Capability[Caveats]) failure.Failure 129 } 130 131 // DerivesFunc determines if a capability is derivable from another. Return 132 // `nil` to indicate the delegated capability can be derived from the claimed 133 // capability. 134 type DerivesFunc[Caveats any] func(claimed, delegated ucan.Capability[Caveats]) failure.Failure 135 136 type Descriptor[Caveats any] interface { 137 Derivable[Caveats] 138 Can() ucan.Ability 139 With() schema.Reader[string, ucan.Resource] 140 Nb() schema.Reader[any, Caveats] 141 } 142 143 type descriptor[Caveats any] struct { 144 can ucan.Ability 145 with schema.Reader[string, ucan.Resource] 146 nb schema.Reader[any, Caveats] 147 derives DerivesFunc[Caveats] 148 } 149 150 func (d descriptor[C]) Can() ucan.Ability { 151 return d.can 152 } 153 154 func (d descriptor[C]) With() schema.Reader[string, ucan.Resource] { 155 return d.with 156 } 157 158 func (d descriptor[C]) Nb() schema.Reader[any, C] { 159 return d.nb 160 } 161 162 func (d descriptor[C]) Derives(parent, child ucan.Capability[C]) failure.Failure { 163 return d.derives(parent, child) 164 } 165 166 type capability[Caveats any] struct { 167 descriptor Descriptor[Caveats] 168 } 169 170 func (c capability[Caveats]) Can() ucan.Ability { 171 return c.descriptor.Can() 172 } 173 174 func (c capability[Caveats]) Select(capabilities []Source) ([]Match[Caveats], []DelegationError, []ucan.Capability[any]) { 175 return Select(c, capabilities) 176 } 177 178 func (c capability[Caveats]) Match(source Source) (Match[Caveats], InvalidCapability) { 179 cap, err := ParseCapability(c.descriptor, source) 180 if err != nil { 181 return nil, err 182 } 183 return NewMatch(source, cap, c.descriptor), nil 184 } 185 186 func (c capability[Caveats]) String() string { 187 return fmt.Sprintf(`{can:"%s"}`, c.Can()) 188 } 189 190 func (c capability[Caveats]) New(with ucan.Resource, nb Caveats) ucan.Capability[Caveats] { 191 return ucan.NewCapability(c.descriptor.Can(), with, nb) 192 } 193 194 func (c capability[Caveats]) Delegate(issuer ucan.Signer, audience ucan.Principal, with ucan.Resource, nb Caveats, options ...delegation.Option) (delegation.Delegation, error) { 195 if bc, ok := any(nb).(ucan.CaveatBuilder); ok { 196 caps := []ucan.Capability[ucan.CaveatBuilder]{ucan.NewCapability(c.Can(), with, bc)} 197 return delegation.Delegate(issuer, audience, caps, options...) 198 } 199 return nil, fmt.Errorf("not an IPLD builder: %v", nb) 200 } 201 202 func (c capability[Caveats]) Invoke(issuer ucan.Signer, audience ucan.Principal, with ucan.Resource, nb Caveats, options ...delegation.Option) (invocation.IssuedInvocation, error) { 203 if bc, ok := any(nb).(ucan.CaveatBuilder); ok { 204 cap := ucan.NewCapability(c.Can(), with, bc) 205 return invocation.Invoke(issuer, audience, cap, options...) 206 } 207 return nil, fmt.Errorf("not an IPLD builder: %v", nb) 208 } 209 210 func NewCapability[Caveats any]( 211 can ucan.Ability, 212 with schema.Reader[string, ucan.Resource], 213 nb schema.Reader[any, Caveats], 214 derives DerivesFunc[Caveats], 215 ) CapabilityParser[Caveats] { 216 if derives == nil { 217 derives = DefaultDerives 218 } 219 d := descriptor[Caveats]{can, with, nb, derives} 220 return &capability[Caveats]{descriptor: d} 221 } 222 223 func ParseCapability[O any](descriptor Descriptor[O], source Source) (ucan.Capability[O], InvalidCapability) { 224 cap := source.Capability() 225 226 if descriptor.Can() != cap.Can() { 227 return nil, NewUnknownCapabilityError(cap) 228 } 229 230 uri, err := descriptor.With().Read(cap.With()) 231 if err != nil { 232 return nil, NewMalformedCapabilityError(cap, err) 233 } 234 235 nb, err := descriptor.Nb().Read(cap.Nb()) 236 if err != nil { 237 return nil, NewMalformedCapabilityError(cap, err) 238 } 239 240 return ucan.NewCapability(cap.Can(), uri, nb), nil 241 } 242 243 func Select[Caveats any](matcher Matcher[Caveats], capabilities []Source) (matches []Match[Caveats], errors []DelegationError, unknowns []ucan.Capability[any]) { 244 for _, capability := range capabilities { 245 match, err := matcher.Match(capability) 246 if err != nil { 247 if ux, ok := err.(UnknownCapability); ok { 248 unknowns = append(unknowns, ux.Capability()) 249 } else if sx, ok := err.(DelegationSubError); ok { 250 errors = append(errors, NewDelegationError([]DelegationSubError{sx}, capability.Capability())) 251 } else { 252 panic(fmt.Errorf("unexpected error type in match result: %w", err)) 253 } 254 continue 255 } 256 matches = append(matches, match) 257 } 258 return 259 } 260 261 // ResolveCapability resolves delegated capability `source` from the `claimed` 262 // capability using provided capability `parser`. It is similar to 263 // [ParseCapability] except `source` here is treated as capability pattern which 264 // is matched against the `claimed` capability. This means we resolve `can` and 265 // `with` fields from the `claimed` capability and... 266 // TODO: inherit all missing `nb` fields from the claimed capability. 267 func ResolveCapability[Caveats any](descriptor Descriptor[Caveats], claimed ucan.Capability[Caveats], source Source) (ucan.Capability[Caveats], InvalidCapability) { 268 can := ResolveAbility(source.Capability().Can(), claimed.Can()) 269 if can == "" { 270 return nil, NewUnknownCapabilityError(source.Capability()) 271 } 272 273 resource := ResolveResource(source.Capability().With(), claimed.With()) 274 if resource == "" { 275 resource = source.Capability().With() 276 } 277 278 uri, err := descriptor.With().Read(resource) 279 if err != nil { 280 return nil, NewMalformedCapabilityError(source.Capability(), err) 281 } 282 283 // TODO: inherit missing fields 284 nb, err := descriptor.Nb().Read(claimed.Nb()) 285 if err != nil { 286 return nil, NewMalformedCapabilityError(source.Capability(), err) 287 } 288 289 return ucan.NewCapability(can, uri, nb), nil 290 } 291 292 // ResolveAbility resolves ability `pattern` of the delegated capability from 293 // the ability of the claimed capability. If pattern matches returns claimed 294 // ability otherwise returns "". 295 // 296 // - pattern "*" can "store/add" → "store/add" 297 // - pattern "store/*" can "store/add" → "store/add" 298 // - pattern "*" can "store/add" → "store/add" 299 // - pattern "*" can "store/add" → "" 300 // - pattern "*" can "store/add" → "" 301 // - pattern "*" can "store/add" → "" 302 func ResolveAbility(pattern string, can ucan.Ability) ucan.Ability { 303 if pattern == can || pattern == "*" { 304 return can 305 } 306 if strings.HasSuffix(pattern, "/*") && strings.HasPrefix(can, pattern[0:len(pattern)-1]) { 307 return can 308 } 309 return "" 310 } 311 312 // ResolveResource resolves `source` resource of the delegated capability from 313 // the resource `uri` of the claimed capability. If `source` is `"ucan:*""` or 314 // matches `uri` then it returns `uri` back otherwise it returns "". 315 // 316 // - source "ucan:*" uri "did:key:zAlice" → "did:key:zAlice" 317 // - source "ucan:*" uri "https://example.com" → "https://example.com" 318 // - source "did:*" uri "did:key:zAlice" → "" 319 // - source "did:key:zAlice" uri "did:key:zAlice" → "did:key:zAlice" 320 func ResolveResource(source string, uri ucan.Resource) ucan.Resource { 321 if source == uri || source == "ucan:*" { 322 return uri 323 } 324 return "" 325 } 326 327 func DefaultDerives[Caveats any](claimed, delegated ucan.Capability[Caveats]) failure.Failure { 328 dres := delegated.With() 329 cres := claimed.With() 330 331 if strings.HasSuffix(dres, "*") { 332 if !strings.HasPrefix(cres, dres[0:len(dres)-1]) { 333 return schema.NewSchemaError(fmt.Sprintf("Resource %s does not match delegated %s", cres, dres)) 334 } 335 } else if dres != cres { 336 return schema.NewSchemaError(fmt.Sprintf("Resource %s is not contained by %s", cres, dres)) 337 } 338 339 // TODO: is it possible to ensure claimed caveats match delegated caveats? 340 return nil 341 }