github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/tuple/tuple.go (about) 1 package tuple 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "maps" 7 "reflect" 8 "regexp" 9 "slices" 10 11 v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" 12 "github.com/jzelinskie/stringz" 13 "google.golang.org/protobuf/proto" 14 "google.golang.org/protobuf/types/known/structpb" 15 16 core "github.com/authzed/spicedb/pkg/proto/core/v1" 17 ) 18 19 const ( 20 // Ellipsis is the Ellipsis relation in v0 style subjects. 21 Ellipsis = "..." 22 23 // PublicWildcard is the wildcard value for subject object IDs that indicates public access 24 // for the subject type. 25 PublicWildcard = "*" 26 ) 27 28 const ( 29 namespaceNameExpr = "([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]" 30 resourceIDExpr = "([a-zA-Z0-9/_|\\-=+]{1,})" 31 subjectIDExpr = "([a-zA-Z0-9/_|\\-=+]{1,})|\\*" 32 relationExpr = "[a-z][a-z0-9_]{1,62}[a-z0-9]" 33 caveatNameExpr = "([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]" 34 ) 35 36 var onrExpr = fmt.Sprintf( 37 `(?P<resourceType>(%s)):(?P<resourceID>%s)#(?P<resourceRel>%s)`, 38 namespaceNameExpr, 39 resourceIDExpr, 40 relationExpr, 41 ) 42 43 var subjectExpr = fmt.Sprintf( 44 `(?P<subjectType>(%s)):(?P<subjectID>%s)(#(?P<subjectRel>%s|\.\.\.))?`, 45 namespaceNameExpr, 46 subjectIDExpr, 47 relationExpr, 48 ) 49 50 var caveatExpr = fmt.Sprintf(`\[(?P<caveatName>(%s))(:(?P<caveatContext>(\{(.+)\})))?\]`, caveatNameExpr) 51 52 var ( 53 onrRegex = regexp.MustCompile(fmt.Sprintf("^%s$", onrExpr)) 54 subjectRegex = regexp.MustCompile(fmt.Sprintf("^%s$", subjectExpr)) 55 resourceIDRegex = regexp.MustCompile(fmt.Sprintf("^%s$", resourceIDExpr)) 56 subjectIDRegex = regexp.MustCompile(fmt.Sprintf("^%s$", subjectIDExpr)) 57 ) 58 59 var parserRegex = regexp.MustCompile( 60 fmt.Sprintf( 61 `^%s@%s(%s)?$`, 62 onrExpr, 63 subjectExpr, 64 caveatExpr, 65 ), 66 ) 67 68 // ValidateResourceID ensures that the given resource ID is valid. Returns an error if not. 69 func ValidateResourceID(objectID string) error { 70 if !resourceIDRegex.MatchString(objectID) { 71 return fmt.Errorf("invalid resource id; must match %s", resourceIDExpr) 72 } 73 if len(objectID) > 1024 { 74 return fmt.Errorf("invalid resource id; must be <= 1024 characters") 75 } 76 77 return nil 78 } 79 80 // ValidateSubjectID ensures that the given object ID (under a subject reference) is valid. Returns an error if not. 81 func ValidateSubjectID(subjectID string) error { 82 if !subjectIDRegex.MatchString(subjectID) { 83 return fmt.Errorf("invalid subject id; must be alphanumeric and between 1 and 127 characters or a star for public") 84 } 85 if len(subjectID) > 1024 { 86 return fmt.Errorf("invalid resource id; must be <= 1024 characters") 87 } 88 89 return nil 90 } 91 92 // MustString converts a tuple to a string. If the tuple is nil or empty, returns empty string. 93 func MustString(tpl *core.RelationTuple) string { 94 tplString, err := String(tpl) 95 if err != nil { 96 panic(err) 97 } 98 return tplString 99 } 100 101 // String converts a tuple to a string. If the tuple is nil or empty, returns empty string. 102 func String(tpl *core.RelationTuple) (string, error) { 103 if tpl == nil || tpl.ResourceAndRelation == nil || tpl.Subject == nil { 104 return "", nil 105 } 106 107 caveatString, err := StringCaveat(tpl.Caveat) 108 if err != nil { 109 return "", err 110 } 111 112 return fmt.Sprintf("%s@%s%s", StringONR(tpl.ResourceAndRelation), StringONR(tpl.Subject), caveatString), nil 113 } 114 115 // StringWithoutCaveat converts a tuple to a string, without its caveat included. 116 func StringWithoutCaveat(tpl *core.RelationTuple) string { 117 if tpl == nil || tpl.ResourceAndRelation == nil || tpl.Subject == nil { 118 return "" 119 } 120 121 return fmt.Sprintf("%s@%s", StringONR(tpl.ResourceAndRelation), StringONR(tpl.Subject)) 122 } 123 124 func MustStringCaveat(caveat *core.ContextualizedCaveat) string { 125 caveatString, err := StringCaveat(caveat) 126 if err != nil { 127 panic(err) 128 } 129 return caveatString 130 } 131 132 // StringCaveat converts a contextualized caveat to a string. If the caveat is nil or empty, returns empty string. 133 func StringCaveat(caveat *core.ContextualizedCaveat) (string, error) { 134 if caveat == nil || caveat.CaveatName == "" { 135 return "", nil 136 } 137 138 contextString, err := StringCaveatContext(caveat.Context) 139 if err != nil { 140 return "", err 141 } 142 143 if len(contextString) > 0 { 144 contextString = ":" + contextString 145 } 146 147 return fmt.Sprintf("[%s%s]", caveat.CaveatName, contextString), nil 148 } 149 150 // StringCaveatContext converts the context of a caveat to a string. If the context is nil or empty, returns an empty string. 151 func StringCaveatContext(context *structpb.Struct) (string, error) { 152 if context == nil || len(context.Fields) == 0 { 153 return "", nil 154 } 155 156 contextBytes, err := context.MarshalJSON() 157 if err != nil { 158 return "", err 159 } 160 return string(contextBytes), nil 161 } 162 163 // MustRelString converts a relationship into a string. Will panic if 164 // the Relationship does not validate. 165 func MustRelString(rel *v1.Relationship) string { 166 if err := rel.Validate(); err != nil { 167 panic(fmt.Sprintf("invalid relationship: %#v %s", rel, err)) 168 } 169 return MustStringRelationship(rel) 170 } 171 172 // MustParse wraps Parse such that any failures panic rather than returning 173 // nil. 174 func MustParse(tpl string) *core.RelationTuple { 175 if parsed := Parse(tpl); parsed != nil { 176 return parsed 177 } 178 panic("failed to parse tuple") 179 } 180 181 // Parse unmarshals the string form of a Tuple and returns nil if there is a 182 // failure. 183 // 184 // This function treats both missing and Ellipsis relations equally. 185 func Parse(tpl string) *core.RelationTuple { 186 groups := parserRegex.FindStringSubmatch(tpl) 187 if len(groups) == 0 { 188 return nil 189 } 190 191 subjectRelation := Ellipsis 192 subjectRelIndex := slices.Index(parserRegex.SubexpNames(), "subjectRel") 193 if len(groups[subjectRelIndex]) > 0 { 194 subjectRelation = groups[subjectRelIndex] 195 } 196 197 caveatName := groups[slices.Index(parserRegex.SubexpNames(), "caveatName")] 198 var optionalCaveat *core.ContextualizedCaveat 199 if caveatName != "" { 200 optionalCaveat = &core.ContextualizedCaveat{ 201 CaveatName: caveatName, 202 } 203 204 caveatContextString := groups[slices.Index(parserRegex.SubexpNames(), "caveatContext")] 205 if len(caveatContextString) > 0 { 206 contextMap := make(map[string]any, 1) 207 err := json.Unmarshal([]byte(caveatContextString), &contextMap) 208 if err != nil { 209 return nil 210 } 211 212 caveatContext, err := structpb.NewStruct(contextMap) 213 if err != nil { 214 return nil 215 } 216 217 optionalCaveat.Context = caveatContext 218 } 219 } 220 221 resourceID := groups[slices.Index(parserRegex.SubexpNames(), "resourceID")] 222 if err := ValidateResourceID(resourceID); err != nil { 223 return nil 224 } 225 226 subjectID := groups[slices.Index(parserRegex.SubexpNames(), "subjectID")] 227 if err := ValidateSubjectID(subjectID); err != nil { 228 return nil 229 } 230 231 return &core.RelationTuple{ 232 ResourceAndRelation: &core.ObjectAndRelation{ 233 Namespace: groups[slices.Index(parserRegex.SubexpNames(), "resourceType")], 234 ObjectId: resourceID, 235 Relation: groups[slices.Index(parserRegex.SubexpNames(), "resourceRel")], 236 }, 237 Subject: &core.ObjectAndRelation{ 238 Namespace: groups[slices.Index(parserRegex.SubexpNames(), "subjectType")], 239 ObjectId: subjectID, 240 Relation: subjectRelation, 241 }, 242 Caveat: optionalCaveat, 243 } 244 } 245 246 func ParseRel(rel string) *v1.Relationship { 247 tpl := Parse(rel) 248 if tpl == nil { 249 return nil 250 } 251 return ToRelationship(tpl) 252 } 253 254 func Create(tpl *core.RelationTuple) *core.RelationTupleUpdate { 255 return &core.RelationTupleUpdate{ 256 Operation: core.RelationTupleUpdate_CREATE, 257 Tuple: tpl, 258 } 259 } 260 261 func Touch(tpl *core.RelationTuple) *core.RelationTupleUpdate { 262 return &core.RelationTupleUpdate{ 263 Operation: core.RelationTupleUpdate_TOUCH, 264 Tuple: tpl, 265 } 266 } 267 268 func Delete(tpl *core.RelationTuple) *core.RelationTupleUpdate { 269 return &core.RelationTupleUpdate{ 270 Operation: core.RelationTupleUpdate_DELETE, 271 Tuple: tpl, 272 } 273 } 274 275 // Equal returns true if the two relationships are exactly the same. 276 func Equal(lhs, rhs *core.RelationTuple) bool { 277 return OnrEqual(lhs.ResourceAndRelation, rhs.ResourceAndRelation) && OnrEqual(lhs.Subject, rhs.Subject) && caveatEqual(lhs.Caveat, rhs.Caveat) 278 } 279 280 func caveatEqual(lhs, rhs *core.ContextualizedCaveat) bool { 281 if lhs == nil && rhs == nil { 282 return true 283 } 284 285 if lhs == nil || rhs == nil { 286 return false 287 } 288 289 return lhs.CaveatName == rhs.CaveatName && proto.Equal(lhs.Context, rhs.Context) 290 } 291 292 // MustToRelationship converts a RelationTuple into a Relationship. Will panic if 293 // the RelationTuple does not validate. 294 func MustToRelationship(tpl *core.RelationTuple) *v1.Relationship { 295 if err := tpl.Validate(); err != nil { 296 panic(fmt.Sprintf("invalid tuple: %#v %s", tpl, err)) 297 } 298 299 return ToRelationship(tpl) 300 } 301 302 // ToRelationship converts a RelationTuple into a Relationship. 303 func ToRelationship(tpl *core.RelationTuple) *v1.Relationship { 304 var caveat *v1.ContextualizedCaveat 305 if tpl.Caveat != nil { 306 caveat = &v1.ContextualizedCaveat{ 307 CaveatName: tpl.Caveat.CaveatName, 308 Context: tpl.Caveat.Context, 309 } 310 } 311 return &v1.Relationship{ 312 Resource: &v1.ObjectReference{ 313 ObjectType: tpl.ResourceAndRelation.Namespace, 314 ObjectId: tpl.ResourceAndRelation.ObjectId, 315 }, 316 Relation: tpl.ResourceAndRelation.Relation, 317 Subject: &v1.SubjectReference{ 318 Object: &v1.ObjectReference{ 319 ObjectType: tpl.Subject.Namespace, 320 ObjectId: tpl.Subject.ObjectId, 321 }, 322 OptionalRelation: stringz.Default(tpl.Subject.Relation, "", Ellipsis), 323 }, 324 OptionalCaveat: caveat, 325 } 326 } 327 328 // NewRelationship creates a new Relationship value with all its required child structures allocated 329 func NewRelationship() *v1.Relationship { 330 return &v1.Relationship{ 331 Resource: &v1.ObjectReference{}, 332 Subject: &v1.SubjectReference{ 333 Object: &v1.ObjectReference{}, 334 }, 335 } 336 } 337 338 // MustToRelationshipMutating sets target relationship to all the values provided in the source tuple. 339 func MustToRelationshipMutating(source *core.RelationTuple, targetRel *v1.Relationship, targetCaveat *v1.ContextualizedCaveat) { 340 targetRel.Resource.ObjectType = source.ResourceAndRelation.Namespace 341 targetRel.Resource.ObjectId = source.ResourceAndRelation.ObjectId 342 targetRel.Relation = source.ResourceAndRelation.Relation 343 targetRel.Subject.Object.ObjectType = source.Subject.Namespace 344 targetRel.Subject.Object.ObjectId = source.Subject.ObjectId 345 targetRel.Subject.OptionalRelation = stringz.Default(source.Subject.Relation, "", Ellipsis) 346 targetRel.OptionalCaveat = nil 347 348 if source.Caveat != nil { 349 if targetCaveat == nil { 350 panic("expected a provided target caveat") 351 } 352 targetCaveat.CaveatName = source.Caveat.CaveatName 353 targetCaveat.Context = source.Caveat.Context 354 targetRel.OptionalCaveat = targetCaveat 355 } 356 } 357 358 // MustToFilter converts a RelationTuple into a RelationshipFilter. Will panic if 359 // the RelationTuple does not validate. 360 func MustToFilter(tpl *core.RelationTuple) *v1.RelationshipFilter { 361 if err := tpl.Validate(); err != nil { 362 panic(fmt.Sprintf("invalid tuple: %#v %s", tpl, err)) 363 } 364 365 return ToFilter(tpl) 366 } 367 368 // ToFilter converts a RelationTuple into a RelationshipFilter. 369 func ToFilter(tpl *core.RelationTuple) *v1.RelationshipFilter { 370 return &v1.RelationshipFilter{ 371 ResourceType: tpl.ResourceAndRelation.Namespace, 372 OptionalResourceId: tpl.ResourceAndRelation.ObjectId, 373 OptionalRelation: tpl.ResourceAndRelation.Relation, 374 OptionalSubjectFilter: UsersetToSubjectFilter(tpl.Subject), 375 } 376 } 377 378 // UsersetToSubjectFilter converts a userset to the equivalent exact SubjectFilter. 379 func UsersetToSubjectFilter(userset *core.ObjectAndRelation) *v1.SubjectFilter { 380 return &v1.SubjectFilter{ 381 SubjectType: userset.Namespace, 382 OptionalSubjectId: userset.ObjectId, 383 OptionalRelation: &v1.SubjectFilter_RelationFilter{ 384 Relation: stringz.Default(userset.Relation, "", Ellipsis), 385 }, 386 } 387 } 388 389 // RelToFilter converts a Relationship into a RelationshipFilter. 390 func RelToFilter(rel *v1.Relationship) *v1.RelationshipFilter { 391 return &v1.RelationshipFilter{ 392 ResourceType: rel.Resource.ObjectType, 393 OptionalResourceId: rel.Resource.ObjectId, 394 OptionalRelation: rel.Relation, 395 OptionalSubjectFilter: &v1.SubjectFilter{ 396 SubjectType: rel.Subject.Object.ObjectType, 397 OptionalSubjectId: rel.Subject.Object.ObjectId, 398 OptionalRelation: &v1.SubjectFilter_RelationFilter{ 399 Relation: rel.Subject.OptionalRelation, 400 }, 401 }, 402 } 403 } 404 405 // UpdatesToRelationshipUpdates converts a slice of RelationTupleUpdate into a 406 // slice of RelationshipUpdate. 407 func UpdatesToRelationshipUpdates(updates []*core.RelationTupleUpdate) []*v1.RelationshipUpdate { 408 relationshipUpdates := make([]*v1.RelationshipUpdate, 0, len(updates)) 409 410 for _, update := range updates { 411 relationshipUpdates = append(relationshipUpdates, UpdateToRelationshipUpdate(update)) 412 } 413 414 return relationshipUpdates 415 } 416 417 func UpdateFromRelationshipUpdates(updates []*v1.RelationshipUpdate) []*core.RelationTupleUpdate { 418 relationshipUpdates := make([]*core.RelationTupleUpdate, 0, len(updates)) 419 420 for _, update := range updates { 421 relationshipUpdates = append(relationshipUpdates, UpdateFromRelationshipUpdate(update)) 422 } 423 424 return relationshipUpdates 425 } 426 427 // UpdateToRelationshipUpdate converts a RelationTupleUpdate into a 428 // RelationshipUpdate. 429 func UpdateToRelationshipUpdate(update *core.RelationTupleUpdate) *v1.RelationshipUpdate { 430 var op v1.RelationshipUpdate_Operation 431 switch update.Operation { 432 case core.RelationTupleUpdate_CREATE: 433 op = v1.RelationshipUpdate_OPERATION_CREATE 434 case core.RelationTupleUpdate_DELETE: 435 op = v1.RelationshipUpdate_OPERATION_DELETE 436 case core.RelationTupleUpdate_TOUCH: 437 op = v1.RelationshipUpdate_OPERATION_TOUCH 438 default: 439 panic("unknown tuple mutation") 440 } 441 442 return &v1.RelationshipUpdate{ 443 Operation: op, 444 Relationship: ToRelationship(update.Tuple), 445 } 446 } 447 448 // MustFromRelationship converts a Relationship into a RelationTuple. 449 func MustFromRelationship[R objectReference, S subjectReference[R], C caveat](r relationship[R, S, C]) *core.RelationTuple { 450 if err := r.Validate(); err != nil { 451 panic(fmt.Sprintf("invalid relationship: %#v %s", r, err)) 452 } 453 return FromRelationship(r) 454 } 455 456 // MustFromRelationships converts a slice of Relationship's into a slice of RelationTuple's. 457 func MustFromRelationships[R objectReference, S subjectReference[R], C caveat](rels []relationship[R, S, C]) []*core.RelationTuple { 458 tuples := make([]*core.RelationTuple, 0, len(rels)) 459 for _, rel := range rels { 460 tpl := MustFromRelationship(rel) 461 tuples = append(tuples, tpl) 462 } 463 return tuples 464 } 465 466 // FromRelationship converts a Relationship into a RelationTuple. 467 func FromRelationship[T objectReference, S subjectReference[T], C caveat](r relationship[T, S, C]) *core.RelationTuple { 468 rel := &core.RelationTuple{ 469 ResourceAndRelation: &core.ObjectAndRelation{}, 470 Subject: &core.ObjectAndRelation{}, 471 Caveat: &core.ContextualizedCaveat{}, 472 } 473 474 CopyRelationshipToRelationTuple(r, rel) 475 476 return rel 477 } 478 479 func CopyRelationshipToRelationTuple[T objectReference, S subjectReference[T], C caveat](r relationship[T, S, C], dst *core.RelationTuple) { 480 if !reflect.ValueOf(r.GetOptionalCaveat()).IsZero() { 481 dst.Caveat.CaveatName = r.GetOptionalCaveat().GetCaveatName() 482 dst.Caveat.Context = r.GetOptionalCaveat().GetContext() 483 } else { 484 dst.Caveat = nil 485 } 486 487 dst.ResourceAndRelation.Namespace = r.GetResource().GetObjectType() 488 dst.ResourceAndRelation.ObjectId = r.GetResource().GetObjectId() 489 dst.ResourceAndRelation.Relation = r.GetRelation() 490 dst.Subject.Namespace = r.GetSubject().GetObject().GetObjectType() 491 dst.Subject.ObjectId = r.GetSubject().GetObject().GetObjectId() 492 dst.Subject.Relation = stringz.DefaultEmpty(r.GetSubject().GetOptionalRelation(), Ellipsis) 493 } 494 495 // CopyRelationTupleToRelationship copies a source core.RelationTuple to a 496 // destination v1.Relationship without allocating new memory. It requires that 497 // the structure for the destination be pre-allocated for the fixed parts, and 498 // an optional caveat context be provided for use when the source contains a 499 // caveat. 500 func CopyRelationTupleToRelationship( 501 src *core.RelationTuple, 502 dst *v1.Relationship, 503 dstCaveat *v1.ContextualizedCaveat, 504 ) { 505 dst.Resource.ObjectType = src.ResourceAndRelation.Namespace 506 dst.Resource.ObjectId = src.ResourceAndRelation.ObjectId 507 dst.Relation = src.ResourceAndRelation.Relation 508 dst.Subject.Object.ObjectType = src.Subject.Namespace 509 dst.Subject.Object.ObjectId = src.Subject.ObjectId 510 dst.Subject.OptionalRelation = stringz.Default(src.Subject.Relation, "", Ellipsis) 511 512 if src.Caveat != nil { 513 dst.OptionalCaveat = dstCaveat 514 dst.OptionalCaveat.CaveatName = src.Caveat.CaveatName 515 dst.OptionalCaveat.Context = src.Caveat.Context 516 } else { 517 dst.OptionalCaveat = nil 518 } 519 } 520 521 // UpdateFromRelationshipUpdate converts a RelationshipUpdate into a 522 // RelationTupleUpdate. 523 func UpdateFromRelationshipUpdate(update *v1.RelationshipUpdate) *core.RelationTupleUpdate { 524 var op core.RelationTupleUpdate_Operation 525 switch update.Operation { 526 case v1.RelationshipUpdate_OPERATION_CREATE: 527 op = core.RelationTupleUpdate_CREATE 528 case v1.RelationshipUpdate_OPERATION_DELETE: 529 op = core.RelationTupleUpdate_DELETE 530 case v1.RelationshipUpdate_OPERATION_TOUCH: 531 op = core.RelationTupleUpdate_TOUCH 532 default: 533 panic("unknown tuple mutation") 534 } 535 536 return &core.RelationTupleUpdate{ 537 Operation: op, 538 Tuple: FromRelationship[*v1.ObjectReference, *v1.SubjectReference, *v1.ContextualizedCaveat](update.Relationship), 539 } 540 } 541 542 // MustWithCaveat adds the given caveat name to the tuple. This is for testing only. 543 func MustWithCaveat(tpl *core.RelationTuple, caveatName string, contexts ...map[string]any) *core.RelationTuple { 544 wc, err := WithCaveat(tpl, caveatName, contexts...) 545 if err != nil { 546 panic(err) 547 } 548 return wc 549 } 550 551 // WithCaveat adds the given caveat name to the tuple. This is for testing only. 552 func WithCaveat(tpl *core.RelationTuple, caveatName string, contexts ...map[string]any) (*core.RelationTuple, error) { 553 var context *structpb.Struct 554 555 if len(contexts) > 0 { 556 combined := map[string]any{} 557 for _, current := range contexts { 558 maps.Copy(combined, current) 559 } 560 561 contextStruct, err := structpb.NewStruct(combined) 562 if err != nil { 563 return nil, err 564 } 565 context = contextStruct 566 } 567 568 tpl = tpl.CloneVT() 569 tpl.Caveat = &core.ContextualizedCaveat{ 570 CaveatName: caveatName, 571 Context: context, 572 } 573 return tpl, nil 574 }