github.com/openfga/openfga@v1.5.4-rc1/pkg/tuple/tuple.go (about) 1 // Package tuple contains code to manipulate tuples and errors related to tuples. 2 package tuple 3 4 import ( 5 "fmt" 6 "regexp" 7 "strings" 8 9 openfgav1 "github.com/openfga/api/proto/openfga/v1" 10 "google.golang.org/protobuf/types/known/structpb" 11 ) 12 13 type TupleWithCondition interface { 14 TupleWithoutCondition 15 GetCondition() *openfgav1.RelationshipCondition 16 } 17 18 type TupleWithoutCondition interface { 19 GetUser() string 20 GetObject() string 21 GetRelation() string 22 String() string 23 } 24 25 type UserType string 26 27 const ( 28 User UserType = "user" 29 UserSet UserType = "userset" 30 ) 31 32 const Wildcard = "*" 33 34 var ( 35 userIDRegex = regexp.MustCompile(`^[^:#\s]+$`) 36 objectRegex = regexp.MustCompile(`^[^:#\s]+:[^#:\s]+$`) 37 userSetRegex = regexp.MustCompile(`^[^:#\s]+:[^#\s]+#[^:#\s]+$`) 38 relationRegex = regexp.MustCompile(`^[^:#@\s]+$`) 39 ) 40 41 func ConvertCheckRequestTupleKeyToTupleKey(tk *openfgav1.CheckRequestTupleKey) *openfgav1.TupleKey { 42 return &openfgav1.TupleKey{ 43 Object: tk.GetObject(), 44 Relation: tk.GetRelation(), 45 User: tk.GetUser(), 46 } 47 } 48 49 func ConvertAssertionTupleKeyToTupleKey(tk *openfgav1.AssertionTupleKey) *openfgav1.TupleKey { 50 return &openfgav1.TupleKey{ 51 Object: tk.GetObject(), 52 Relation: tk.GetRelation(), 53 User: tk.GetUser(), 54 } 55 } 56 57 func ConvertReadRequestTupleKeyToTupleKey(tk *openfgav1.ReadRequestTupleKey) *openfgav1.TupleKey { 58 return &openfgav1.TupleKey{ 59 Object: tk.GetObject(), 60 Relation: tk.GetRelation(), 61 User: tk.GetUser(), 62 } 63 } 64 65 func TupleKeyToTupleKeyWithoutCondition(tk *openfgav1.TupleKey) *openfgav1.TupleKeyWithoutCondition { 66 return &openfgav1.TupleKeyWithoutCondition{ 67 Object: tk.GetObject(), 68 Relation: tk.GetRelation(), 69 User: tk.GetUser(), 70 } 71 } 72 73 func TupleKeyWithoutConditionToTupleKey(tk *openfgav1.TupleKeyWithoutCondition) *openfgav1.TupleKey { 74 return &openfgav1.TupleKey{ 75 Object: tk.GetObject(), 76 Relation: tk.GetRelation(), 77 User: tk.GetUser(), 78 } 79 } 80 81 func TupleKeysWithoutConditionToTupleKeys(tks ...*openfgav1.TupleKeyWithoutCondition) []*openfgav1.TupleKey { 82 converted := make([]*openfgav1.TupleKey, 0, len(tks)) 83 for _, tk := range tks { 84 converted = append(converted, TupleKeyWithoutConditionToTupleKey(tk)) 85 } 86 87 return converted 88 } 89 90 func NewTupleKey(object, relation, user string) *openfgav1.TupleKey { 91 return &openfgav1.TupleKey{ 92 Object: object, 93 Relation: relation, 94 User: user, 95 } 96 } 97 98 func NewTupleKeyWithCondition( 99 object, relation, user, conditionName string, 100 context *structpb.Struct, 101 ) *openfgav1.TupleKey { 102 return &openfgav1.TupleKey{ 103 Object: object, 104 Relation: relation, 105 User: user, 106 Condition: NewRelationshipCondition(conditionName, context), 107 } 108 } 109 110 func NewRelationshipCondition(name string, context *structpb.Struct) *openfgav1.RelationshipCondition { 111 if name == "" { 112 return nil 113 } 114 115 if context == nil { 116 return &openfgav1.RelationshipCondition{ 117 Name: name, 118 Context: &structpb.Struct{}, 119 } 120 } 121 122 return &openfgav1.RelationshipCondition{ 123 Name: name, 124 Context: context, 125 } 126 } 127 128 func NewAssertionTupleKey(object, relation, user string) *openfgav1.AssertionTupleKey { 129 return &openfgav1.AssertionTupleKey{ 130 Object: object, 131 Relation: relation, 132 User: user, 133 } 134 } 135 136 func NewCheckRequestTupleKey(object, relation, user string) *openfgav1.CheckRequestTupleKey { 137 return &openfgav1.CheckRequestTupleKey{ 138 Object: object, 139 Relation: relation, 140 User: user, 141 } 142 } 143 144 func NewExpandRequestTupleKey(object, relation string) *openfgav1.ExpandRequestTupleKey { 145 return &openfgav1.ExpandRequestTupleKey{ 146 Object: object, 147 Relation: relation, 148 } 149 } 150 151 // ObjectKey returns the canonical key for the provided Object. The ObjectKey of an object 152 // is the string 'objectType:objectId'. 153 func ObjectKey(obj *openfgav1.Object) string { 154 return BuildObject(obj.GetType(), obj.GetId()) 155 } 156 157 type UserString = string 158 159 // UserProtoToString returns a string from a User proto. Ex: 'user:maria' or 'group:fga#member'. It is 160 // the opposite from StringToUserProto function. 161 func UserProtoToString(obj *openfgav1.User) UserString { 162 switch obj.GetUser().(type) { 163 case *openfgav1.User_Wildcard: 164 return fmt.Sprintf("%s:*", obj.GetWildcard().GetType()) 165 case *openfgav1.User_Userset: 166 us := obj.GetUser().(*openfgav1.User_Userset) 167 return fmt.Sprintf("%s:%s#%s", us.Userset.GetType(), us.Userset.GetId(), us.Userset.GetRelation()) 168 case *openfgav1.User_Object: 169 us := obj.GetUser().(*openfgav1.User_Object) 170 return fmt.Sprintf("%s:%s", us.Object.GetType(), us.Object.GetId()) 171 default: 172 panic("unsupported type") 173 } 174 } 175 176 // StringToUserProto returns a User proto from a string. Ex: 'user:maria#member'. 177 // It is the opposite from FromUserProto function. 178 func StringToUserProto(userKey UserString) *openfgav1.User { 179 userObj, userRel := SplitObjectRelation(userKey) 180 userObjType, userObjID := SplitObject(userObj) 181 if userRel == "" && userObjID == "*" { 182 return &openfgav1.User{User: &openfgav1.User_Wildcard{ 183 Wildcard: &openfgav1.TypedWildcard{ 184 Type: userObjType, 185 }, 186 }} 187 } 188 if userRel == "" { 189 return &openfgav1.User{User: &openfgav1.User_Object{Object: &openfgav1.Object{ 190 Type: userObjType, 191 Id: userObjID, 192 }}} 193 } 194 return &openfgav1.User{User: &openfgav1.User_Userset{Userset: &openfgav1.UsersetUser{ 195 Type: userObjType, 196 Id: userObjID, 197 Relation: userRel, 198 }}} 199 } 200 201 // SplitObject splits an object into an objectType and an objectID. If no type is present, it returns the empty string 202 // and the original object. 203 func SplitObject(object string) (string, string) { 204 switch i := strings.IndexByte(object, ':'); i { 205 case -1: 206 return "", object 207 case len(object) - 1: 208 return object[0:i], "" 209 default: 210 return object[0:i], object[i+1:] 211 } 212 } 213 214 func BuildObject(objectType, objectID string) string { 215 return fmt.Sprintf("%s:%s", objectType, objectID) 216 } 217 218 // GetObjectRelationAsString returns a string like "object#relation". If there is no relation it returns "object". 219 func GetObjectRelationAsString(objectRelation *openfgav1.ObjectRelation) string { 220 if objectRelation.GetRelation() != "" { 221 return fmt.Sprintf("%s#%s", objectRelation.GetObject(), objectRelation.GetRelation()) 222 } 223 return objectRelation.GetObject() 224 } 225 226 // SplitObjectRelation splits an object relation string into an object ID and relation name. If no relation is present, 227 // it returns the original string and an empty relation. 228 func SplitObjectRelation(objectRelation string) (string, string) { 229 switch i := strings.LastIndexByte(objectRelation, '#'); i { 230 case -1: 231 return objectRelation, "" 232 case len(objectRelation) - 1: 233 return objectRelation[0:i], "" 234 default: 235 return objectRelation[0:i], objectRelation[i+1:] 236 } 237 } 238 239 // GetType returns the type from a supplied Object identifier or an empty string if the object id does not contain a 240 // type. 241 func GetType(objectID string) string { 242 t, _ := SplitObject(objectID) 243 return t 244 } 245 246 // GetRelation returns the 'relation' portion of an object relation string (e.g. `object#relation`), which may be empty if the input is malformed 247 // (or does not contain a relation). 248 func GetRelation(objectRelation string) string { 249 _, relation := SplitObjectRelation(objectRelation) 250 return relation 251 } 252 253 // IsObjectRelation returns true if the given string specifies a valid object and relation. 254 func IsObjectRelation(userset string) bool { 255 return GetType(userset) != "" && GetRelation(userset) != "" 256 } 257 258 // ToObjectRelationString formats an object/relation pair as an object#relation string. This is the inverse of 259 // SplitObjectRelation. 260 func ToObjectRelationString(object, relation string) string { 261 return fmt.Sprintf("%s#%s", object, relation) 262 } 263 264 // GetUserTypeFromUser returns the type of user (userset or user). 265 func GetUserTypeFromUser(user string) UserType { 266 if IsObjectRelation(user) || IsWildcard(user) { 267 return UserSet 268 } 269 return User 270 } 271 272 // TupleKeyToString converts a tuple key into its string representation. It assumes the tupleKey is valid 273 // (i.e. no forbidden characters). 274 func TupleKeyToString(tk TupleWithoutCondition) string { 275 return fmt.Sprintf("%s#%s@%s", tk.GetObject(), tk.GetRelation(), tk.GetUser()) 276 } 277 278 // TupleKeyWithConditionToString converts a tuple key with condition into its string representation. It assumes the tupleKey is valid 279 // (i.e. no forbidden characters). 280 func TupleKeyWithConditionToString(tk TupleWithCondition) string { 281 return fmt.Sprintf("%s#%s@%s (condition %s)", tk.GetObject(), tk.GetRelation(), tk.GetUser(), tk.GetCondition()) 282 } 283 284 // IsValidObject determines if a string s is a valid object. A valid object contains exactly one `:` and no `#` or spaces. 285 func IsValidObject(s string) bool { 286 return objectRegex.MatchString(s) 287 } 288 289 // IsValidRelation determines if a string s is a valid relation. This means it does not contain any `:`, `#`, or spaces. 290 func IsValidRelation(s string) bool { 291 return relationRegex.MatchString(s) 292 } 293 294 // IsValidUser determines if a string is a valid user. A valid user contains at most one `:`, at most one `#` and no spaces. 295 func IsValidUser(user string) bool { 296 if strings.Count(user, ":") > 1 || strings.Count(user, "#") > 1 { 297 return false 298 } 299 if user == Wildcard || userIDRegex.MatchString(user) || objectRegex.MatchString(user) || userSetRegex.MatchString(user) { 300 return true 301 } 302 303 return false 304 } 305 306 // IsWildcard returns true if the string 's' could be interpreted as a typed or untyped wildcard (e.g. '*' or 'type:*'). 307 func IsWildcard(s string) bool { 308 return s == Wildcard || IsTypedWildcard(s) 309 } 310 311 // IsTypedWildcard returns true if the string 's' is a typed wildcard. A typed wildcard 312 // has the form 'type:*'. 313 func IsTypedWildcard(s string) bool { 314 if IsValidObject(s) { 315 _, id := SplitObject(s) 316 if id == Wildcard { 317 return true 318 } 319 } 320 321 return false 322 } 323 324 // TypedPublicWildcard returns the string tuple representation for a given object type (ex: "user:*"). 325 func TypedPublicWildcard(objectType string) string { 326 return BuildObject(objectType, Wildcard) 327 } 328 329 // MustParseTupleString attempts to parse a relationship tuple specified 330 // in string notation and return the protobuf TupleKey for it. If parsing 331 // of the string fails this function will panic. It is meant for testing 332 // purposes. 333 // 334 // Given string 'document:1#viewer@user:jon', return the protobuf TupleKey 335 // for it. 336 func MustParseTupleString(s string) *openfgav1.TupleKey { 337 t, err := ParseTupleString(s) 338 if err != nil { 339 panic(err) 340 } 341 342 return t 343 } 344 345 func MustParseTupleStrings(tupleStrs ...string) []*openfgav1.TupleKey { 346 tuples := make([]*openfgav1.TupleKey, 0, len(tupleStrs)) 347 for _, tupleStr := range tupleStrs { 348 tuples = append(tuples, MustParseTupleString(tupleStr)) 349 } 350 351 return tuples 352 } 353 354 // ParseTupleString attempts to parse a relationship tuple specified 355 // in string notation and return the protobuf TupleKey for it. If parsing 356 // of the string fails this function returns an err. 357 // 358 // Given string 'document:1#viewer@user:jon', return the protobuf TupleKey 359 // for it or an error. 360 func ParseTupleString(s string) (*openfgav1.TupleKey, error) { 361 object, rhs, found := strings.Cut(s, "#") 362 if !found { 363 return nil, fmt.Errorf("expected at least one '#' separating the object and relation") 364 } 365 366 if !IsValidObject(object) { 367 return nil, fmt.Errorf("invalid tuple 'object' field format") 368 } 369 370 relation, user, found := strings.Cut(rhs, "@") 371 if !found { 372 return nil, fmt.Errorf("expected at least one '@' separating the relation and user") 373 } 374 375 if !IsValidRelation(relation) { 376 return nil, fmt.Errorf("invalid tuple 'relation' field format") 377 } 378 379 if !IsValidUser(user) { 380 return nil, fmt.Errorf("invalid tuple 'user' field format") 381 } 382 383 return &openfgav1.TupleKey{ 384 Object: object, 385 Relation: relation, 386 User: user, 387 }, nil 388 }