github.com/openfga/openfga@v1.5.4-rc1/internal/graph/graph.go (about) 1 // Package graph contains code related to evaluation of authorization models through graph traversals. 2 package graph 3 4 import ( 5 "context" 6 "errors" 7 "fmt" 8 "strings" 9 "sync/atomic" 10 11 openfgav1 "github.com/openfga/api/proto/openfga/v1" 12 13 "github.com/openfga/openfga/pkg/tuple" 14 "github.com/openfga/openfga/pkg/typesystem" 15 ) 16 17 type ctxKey string 18 19 const ( 20 resolutionDepthCtxKey ctxKey = "resolution-depth" 21 ) 22 23 var ( 24 ErrResolutionDepthExceeded = errors.New("resolution depth exceeded") 25 ) 26 27 type findEdgeOption int 28 29 const ( 30 resolveAllEdges findEdgeOption = iota 31 resolveAnyEdge 32 ) 33 34 // ContextWithResolutionDepth attaches the provided graph resolution depth to the parent context. 35 func ContextWithResolutionDepth(parent context.Context, depth uint32) context.Context { 36 return context.WithValue(parent, resolutionDepthCtxKey, depth) 37 } 38 39 // ResolutionDepthFromContext returns the current graph resolution depth from the provided context (if any). 40 func ResolutionDepthFromContext(ctx context.Context) (uint32, bool) { 41 depth, ok := ctx.Value(resolutionDepthCtxKey).(uint32) 42 return depth, ok 43 } 44 45 type ResolveCheckRequestMetadata struct { 46 // Thinking of a Check as a tree of evaluations, 47 // Depth is the current level in the tree in the current path that we are exploring. 48 // When we jump one level, we decrement 1. If it hits 0, we throw ErrResolutionDepthExceeded. 49 Depth uint32 50 51 // Number of calls to ReadUserTuple + ReadUsersetTuples + Read accumulated so far, before this request is solved. 52 DatastoreQueryCount uint32 53 54 // DispatchCounter is the address to a shared counter that keeps track of how many calls to ResolveCheck we had to do 55 // to solve the root/parent problem. 56 // The contents of this counter will be written by concurrent goroutines. 57 // After the root problem has been solved, this value can be read. 58 DispatchCounter *atomic.Uint32 59 60 // WasThrottled indicates whether the request was throttled 61 WasThrottled *atomic.Bool 62 } 63 64 func NewCheckRequestMetadata(maxDepth uint32) *ResolveCheckRequestMetadata { 65 return &ResolveCheckRequestMetadata{ 66 Depth: maxDepth, 67 DatastoreQueryCount: 0, 68 DispatchCounter: new(atomic.Uint32), 69 WasThrottled: new(atomic.Bool), 70 } 71 } 72 73 type ResolveCheckResponseMetadata struct { 74 // Number of calls to ReadUserTuple + ReadUsersetTuples + Read accumulated after this request is solved. 75 // Thinking of a Check as a tree of evaluations, 76 // If the solution is "allowed=true", one path was found. This is the value in the leaf node of that path, plus the sum of the paths that were 77 // evaluated and potentially discarded 78 // If the solution is "allowed=false", no paths were found. This is the sum of all the reads in all the paths that had to be evaluated 79 DatastoreQueryCount uint32 80 81 // Indicates if the ResolveCheck subproblem that was evaluated involved 82 // a cycle in the evaluation. 83 CycleDetected bool 84 } 85 86 type RelationshipEdgeType int 87 88 const ( 89 // DirectEdge defines a direct connection between a source object reference 90 // and some target user reference. 91 DirectEdge RelationshipEdgeType = iota 92 // TupleToUsersetEdge defines a connection between a source object reference 93 // and some target user reference that is co-dependent upon the lookup of a third object reference. 94 TupleToUsersetEdge 95 // ComputedUsersetEdge defines a direct connection between a source object reference 96 // and some target user reference. The difference with DirectEdge is that DirectEdge will involve 97 // a read of tuples and this one will not. 98 ComputedUsersetEdge 99 ) 100 101 func (r RelationshipEdgeType) String() string { 102 switch r { 103 case DirectEdge: 104 return "direct" 105 case ComputedUsersetEdge: 106 return "computed_userset" 107 case TupleToUsersetEdge: 108 return "ttu" 109 default: 110 return "undefined" 111 } 112 } 113 114 type EdgeCondition int 115 116 // RelationshipEdge represents a possible relationship between some source object reference 117 // and a target user reference. The possibility is realized depending on the tuples and on the edge's type. 118 type RelationshipEdge struct { 119 Type RelationshipEdgeType 120 121 // The edge is directed towards this node, which can be like group:*, or group, or group:member 122 TargetReference *openfgav1.RelationReference 123 124 // If the type is TupleToUsersetEdge, this defines the TTU condition 125 TuplesetRelation string 126 127 TargetReferenceInvolvesIntersectionOrExclusion bool 128 } 129 130 func (r RelationshipEdge) String() string { 131 // TODO also print the condition 132 var val string 133 if r.TuplesetRelation != "" { 134 val = fmt.Sprintf("userset %s, type %s, tupleset %s", r.TargetReference.String(), r.Type.String(), r.TuplesetRelation) 135 } else { 136 val = fmt.Sprintf("userset %s, type %s", r.TargetReference.String(), r.Type.String()) 137 } 138 return strings.ReplaceAll(val, " ", " ") 139 } 140 141 // RelationshipGraph represents a graph of relationships and the connectivity between 142 // object and relation references within the graph through direct or indirect relationships. 143 type RelationshipGraph struct { 144 typesystem *typesystem.TypeSystem 145 } 146 147 // New returns a RelationshipGraph from an authorization model. The RelationshipGraph should be used to introspect what kind of relationships between 148 // object types can exist. To visualize this graph, use https://github.com/jon-whit/openfga-graphviz-gen 149 func New(typesystem *typesystem.TypeSystem) *RelationshipGraph { 150 return &RelationshipGraph{ 151 typesystem: typesystem, 152 } 153 } 154 155 // GetRelationshipEdges finds all paths from a source to a target and then returns all the edges at distance 0 or 1 of the source in those paths. 156 func (g *RelationshipGraph) GetRelationshipEdges(target *openfgav1.RelationReference, source *openfgav1.RelationReference) ([]*RelationshipEdge, error) { 157 return g.getRelationshipEdges(target, source, map[string]struct{}{}, resolveAllEdges) 158 } 159 160 // GetPrunedRelationshipEdges finds all paths from a source to a target and then returns all the edges at distance 0 or 1 of the source in those paths. 161 // If the edges from the source to the target pass through a relationship involving intersection or exclusion (directly or indirectly), 162 // then GetPrunedRelationshipEdges will just return the first-most edge involved in that rewrite. 163 // 164 // Consider the following model: 165 // 166 // type user 167 // type document 168 // 169 // relations 170 // define allowed: [user] 171 // define viewer: [user] and allowed 172 // 173 // The pruned relationship edges from the 'user' type to 'document#viewer' returns only the edge from 'user' to 'document#viewer' and with a 'RequiresFurtherEvalCondition'. 174 // This is because when evaluating relationships involving intersection or exclusion we choose to only evaluate one operand of the rewrite rule, and for each result found 175 // we call Check on the result to evaluate the sub-condition on the 'and allowed' bit. 176 func (g *RelationshipGraph) GetPrunedRelationshipEdges(target *openfgav1.RelationReference, source *openfgav1.RelationReference) ([]*RelationshipEdge, error) { 177 return g.getRelationshipEdges(target, source, map[string]struct{}{}, resolveAnyEdge) 178 } 179 180 func (g *RelationshipGraph) getRelationshipEdges( 181 target *openfgav1.RelationReference, 182 source *openfgav1.RelationReference, 183 visited map[string]struct{}, 184 findEdgeOption findEdgeOption, 185 ) ([]*RelationshipEdge, error) { 186 key := tuple.ToObjectRelationString(target.GetType(), target.GetRelation()) 187 if _, ok := visited[key]; ok { 188 // We've already visited the target so no need to do so again. 189 return nil, nil 190 } 191 visited[key] = struct{}{} 192 193 relation, err := g.typesystem.GetRelation(target.GetType(), target.GetRelation()) 194 if err != nil { 195 return nil, err 196 } 197 198 return g.getRelationshipEdgesWithTargetRewrite( 199 target, 200 source, 201 relation.GetRewrite(), 202 visited, 203 findEdgeOption, 204 ) 205 } 206 207 // getRelationshipEdgesWithTargetRewrite does a BFS on the graph starting at `target` and trying to reach `source`. 208 func (g *RelationshipGraph) getRelationshipEdgesWithTargetRewrite( 209 target *openfgav1.RelationReference, 210 source *openfgav1.RelationReference, 211 targetRewrite *openfgav1.Userset, 212 visited map[string]struct{}, 213 findEdgeOption findEdgeOption, 214 ) ([]*RelationshipEdge, error) { 215 switch t := targetRewrite.GetUserset().(type) { 216 case *openfgav1.Userset_This: // e.g. define viewer:[user] 217 var res []*RelationshipEdge 218 directlyRelated, _ := g.typesystem.IsDirectlyRelated(target, source) 219 publiclyAssignable, _ := g.typesystem.IsPubliclyAssignable(target, source.GetType()) 220 221 if directlyRelated || publiclyAssignable { 222 // if source=user, or define viewer:[user:*] 223 res = append(res, &RelationshipEdge{ 224 Type: DirectEdge, 225 TargetReference: typesystem.DirectRelationReference(target.GetType(), target.GetRelation()), 226 TargetReferenceInvolvesIntersectionOrExclusion: false, 227 }) 228 } 229 230 typeRestrictions, _ := g.typesystem.GetDirectlyRelatedUserTypes(target.GetType(), target.GetRelation()) 231 232 for _, typeRestriction := range typeRestrictions { 233 if typeRestriction.GetRelation() != "" { // e.g. define viewer:[team#member] 234 // recursively sub-collect any edges for (team#member, source) 235 edges, err := g.getRelationshipEdges(typeRestriction, source, visited, findEdgeOption) 236 if err != nil { 237 return nil, err 238 } 239 240 res = append(res, edges...) 241 } 242 } 243 244 return res, nil 245 case *openfgav1.Userset_ComputedUserset: // e.g. target = define viewer: writer 246 247 var edges []*RelationshipEdge 248 249 // if source=document#writer 250 sourceRelMatchesRewritten := target.GetType() == source.GetType() && t.ComputedUserset.GetRelation() == source.GetRelation() 251 252 if sourceRelMatchesRewritten { 253 edges = append(edges, &RelationshipEdge{ 254 Type: ComputedUsersetEdge, 255 TargetReference: typesystem.DirectRelationReference(target.GetType(), target.GetRelation()), 256 TargetReferenceInvolvesIntersectionOrExclusion: false, 257 }) 258 } 259 260 collected, err := g.getRelationshipEdges( 261 typesystem.DirectRelationReference(target.GetType(), t.ComputedUserset.GetRelation()), 262 source, 263 visited, 264 findEdgeOption, 265 ) 266 if err != nil { 267 return nil, err 268 } 269 270 edges = append( 271 edges, 272 collected..., 273 ) 274 return edges, nil 275 case *openfgav1.Userset_TupleToUserset: // e.g. type document, define viewer: writer from parent 276 tupleset := t.TupleToUserset.GetTupleset().GetRelation() // parent 277 computedUserset := t.TupleToUserset.GetComputedUserset().GetRelation() // writer 278 279 var res []*RelationshipEdge 280 // e.g. type document, define parent:[user, group] 281 tuplesetTypeRestrictions, _ := g.typesystem.GetDirectlyRelatedUserTypes(target.GetType(), tupleset) 282 283 for _, typeRestriction := range tuplesetTypeRestrictions { 284 r, err := g.typesystem.GetRelation(typeRestriction.GetType(), computedUserset) 285 if err != nil { 286 if errors.Is(err, typesystem.ErrRelationUndefined) { 287 continue 288 } 289 290 return nil, err 291 } 292 293 if typeRestriction.GetType() == source.GetType() && computedUserset == source.GetRelation() { 294 involvesIntersection, err := g.typesystem.RelationInvolvesIntersection(typeRestriction.GetType(), r.GetName()) 295 if err != nil { 296 return nil, err 297 } 298 299 involvesExclusion, err := g.typesystem.RelationInvolvesExclusion(typeRestriction.GetType(), r.GetName()) 300 if err != nil { 301 return nil, err 302 } 303 304 res = append(res, &RelationshipEdge{ 305 Type: TupleToUsersetEdge, 306 TargetReference: typesystem.DirectRelationReference(target.GetType(), target.GetRelation()), 307 TuplesetRelation: tupleset, 308 TargetReferenceInvolvesIntersectionOrExclusion: involvesIntersection || involvesExclusion, 309 }) 310 } 311 312 subResults, err := g.getRelationshipEdges( 313 typesystem.DirectRelationReference(typeRestriction.GetType(), computedUserset), 314 source, 315 visited, 316 findEdgeOption, 317 ) 318 if err != nil { 319 return nil, err 320 } 321 322 res = append(res, subResults...) 323 } 324 325 return res, nil 326 case *openfgav1.Userset_Union: // e.g. target = define viewer: self or writer 327 var res []*RelationshipEdge 328 for _, child := range t.Union.GetChild() { 329 // we recurse through each child rewrite 330 childResults, err := g.getRelationshipEdgesWithTargetRewrite(target, source, child, visited, findEdgeOption) 331 if err != nil { 332 return nil, err 333 } 334 res = append(res, childResults...) 335 } 336 return res, nil 337 case *openfgav1.Userset_Intersection: 338 339 if findEdgeOption == resolveAnyEdge { 340 child := t.Intersection.GetChild()[0] 341 342 childresults, err := g.getRelationshipEdgesWithTargetRewrite(target, source, child, visited, findEdgeOption) 343 if err != nil { 344 return nil, err 345 } 346 347 for _, childresult := range childresults { 348 childresult.TargetReferenceInvolvesIntersectionOrExclusion = true 349 } 350 351 return childresults, nil 352 } 353 354 var edges []*RelationshipEdge 355 for _, child := range t.Intersection.GetChild() { 356 res, err := g.getRelationshipEdgesWithTargetRewrite(target, source, child, visited, findEdgeOption) 357 if err != nil { 358 return nil, err 359 } 360 361 edges = append(edges, res...) 362 } 363 364 if len(edges) > 0 { 365 edges[0].TargetReferenceInvolvesIntersectionOrExclusion = true 366 } 367 368 return edges, nil 369 case *openfgav1.Userset_Difference: 370 371 if findEdgeOption == resolveAnyEdge { 372 // if we have 'a but not b', then we prune 'b' and only resolve 'a' with a 373 // condition that requires further evaluation. It's more likely the blacklist 374 // on 'but not b' is a larger set than the base set 'a', and so pruning the 375 // subtracted set is generally going to be a better choice. 376 377 child := t.Difference.GetBase() 378 379 childresults, err := g.getRelationshipEdgesWithTargetRewrite(target, source, child, visited, findEdgeOption) 380 if err != nil { 381 return nil, err 382 } 383 384 for _, childresult := range childresults { 385 childresult.TargetReferenceInvolvesIntersectionOrExclusion = true 386 } 387 388 return childresults, nil 389 } 390 391 var edges []*RelationshipEdge 392 393 baseRewrite := t.Difference.GetBase() 394 395 baseEdges, err := g.getRelationshipEdgesWithTargetRewrite(target, source, baseRewrite, visited, findEdgeOption) 396 if err != nil { 397 return nil, err 398 } 399 400 if len(baseEdges) > 0 { 401 baseEdges[0].TargetReferenceInvolvesIntersectionOrExclusion = true 402 } 403 404 edges = append(edges, baseEdges...) 405 406 subtractRewrite := t.Difference.GetSubtract() 407 408 subEdges, err := g.getRelationshipEdgesWithTargetRewrite(target, source, subtractRewrite, visited, findEdgeOption) 409 if err != nil { 410 return nil, err 411 } 412 edges = append(edges, subEdges...) 413 414 return edges, nil 415 default: 416 panic("unexpected userset rewrite encountered") 417 } 418 } 419 420 // NewLayeredCheckResolver constructs a CheckResolver that is composed of various CheckResolver layers. 421 // Specifically, it constructs a CheckResolver with the following composition: 422 // 423 // CycleDetectionCheckResolver <-----| 424 // CachedCheckResolver | 425 // LocalChecker | 426 // CycleDetectionCheckResolver -| 427 // 428 // The returned CheckResolverCloser should be used to close all resolvers involved in the 429 // composition after you are done with the CheckResolver. 430 func NewLayeredCheckResolver( 431 localResolverOpts []LocalCheckerOption, 432 cacheEnabled bool, 433 cachedResolverOpts []CachedCheckResolverOpt, 434 ) (CheckResolver, CheckResolverCloser) { 435 cycleDetectionCheckResolver := NewCycleDetectionCheckResolver() 436 localCheckResolver := NewLocalChecker(localResolverOpts...) 437 438 cycleDetectionCheckResolver.SetDelegate(localCheckResolver) 439 440 var cachedCheckResolver *CachedCheckResolver 441 if cacheEnabled { 442 cachedCheckResolver = NewCachedCheckResolver(cachedResolverOpts...) 443 cycleDetectionCheckResolver.SetDelegate(cachedCheckResolver) 444 cachedCheckResolver.SetDelegate(localCheckResolver) 445 } 446 447 localCheckResolver.SetDelegate(cycleDetectionCheckResolver) 448 449 return cycleDetectionCheckResolver, func() { 450 localCheckResolver.Close() 451 452 if cachedCheckResolver != nil { 453 cachedCheckResolver.Close() 454 } 455 456 cycleDetectionCheckResolver.Close() 457 } 458 }