github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/typesystem/reachabilitygraphbuilder.go (about) 1 package typesystem 2 3 import ( 4 "context" 5 "fmt" 6 7 "github.com/authzed/spicedb/pkg/spiceerrors" 8 "github.com/authzed/spicedb/pkg/tuple" 9 10 core "github.com/authzed/spicedb/pkg/proto/core/v1" 11 ) 12 13 type reachabilityOption int 14 15 const ( 16 reachabilityFull reachabilityOption = iota 17 reachabilityOptimized 18 ) 19 20 func computeReachability(ctx context.Context, ts *TypeSystem, relationName string, option reachabilityOption) (*core.ReachabilityGraph, error) { 21 targetRelation, ok := ts.relationMap[relationName] 22 if !ok { 23 return nil, fmt.Errorf("relation `%s` not found under type `%s` missing when computing reachability", relationName, ts.nsDef.Name) 24 } 25 26 if !ts.HasTypeInformation(relationName) && targetRelation.GetUsersetRewrite() == nil { 27 return nil, fmt.Errorf("relation `%s` missing type information when computing reachability for namespace `%s`", relationName, ts.nsDef.Name) 28 } 29 30 graph := &core.ReachabilityGraph{ 31 EntrypointsBySubjectType: map[string]*core.ReachabilityEntrypoints{}, 32 EntrypointsBySubjectRelation: map[string]*core.ReachabilityEntrypoints{}, 33 } 34 35 usersetRewrite := targetRelation.GetUsersetRewrite() 36 if usersetRewrite != nil { 37 return graph, computeRewriteReachability(ctx, graph, usersetRewrite, core.ReachabilityEntrypoint_DIRECT_OPERATION_RESULT, targetRelation, ts, option) 38 } 39 40 // If there is no userRewrite, then we have a relation and its entrypoints will all be 41 // relation entrypoints. 42 return graph, addSubjectLinks(graph, core.ReachabilityEntrypoint_DIRECT_OPERATION_RESULT, targetRelation, ts) 43 } 44 45 func computeRewriteReachability(ctx context.Context, graph *core.ReachabilityGraph, rewrite *core.UsersetRewrite, operationResultState core.ReachabilityEntrypoint_EntrypointResultStatus, targetRelation *core.Relation, ts *TypeSystem, option reachabilityOption) error { 46 switch rw := rewrite.RewriteOperation.(type) { 47 case *core.UsersetRewrite_Union: 48 return computeRewriteOpReachability(ctx, rw.Union.Child, operationResultState, graph, targetRelation, ts, option) 49 50 case *core.UsersetRewrite_Intersection: 51 // If optimized mode is set, only return the first child of the intersection. 52 if option == reachabilityOptimized { 53 return computeRewriteOpReachability(ctx, rw.Intersection.Child[0:1], core.ReachabilityEntrypoint_REACHABLE_CONDITIONAL_RESULT, graph, targetRelation, ts, option) 54 } 55 56 return computeRewriteOpReachability(ctx, rw.Intersection.Child, core.ReachabilityEntrypoint_REACHABLE_CONDITIONAL_RESULT, graph, targetRelation, ts, option) 57 58 case *core.UsersetRewrite_Exclusion: 59 // If optimized mode is set, only return the first child of the exclusion. 60 if option == reachabilityOptimized { 61 return computeRewriteOpReachability(ctx, rw.Exclusion.Child[0:1], core.ReachabilityEntrypoint_REACHABLE_CONDITIONAL_RESULT, graph, targetRelation, ts, option) 62 } 63 64 return computeRewriteOpReachability(ctx, rw.Exclusion.Child, core.ReachabilityEntrypoint_REACHABLE_CONDITIONAL_RESULT, graph, targetRelation, ts, option) 65 66 default: 67 return fmt.Errorf("unknown kind of userset rewrite in reachability computation: %T", rw) 68 } 69 } 70 71 func computeRewriteOpReachability(ctx context.Context, children []*core.SetOperation_Child, operationResultState core.ReachabilityEntrypoint_EntrypointResultStatus, graph *core.ReachabilityGraph, targetRelation *core.Relation, ts *TypeSystem, option reachabilityOption) error { 72 rr := &core.RelationReference{ 73 Namespace: ts.nsDef.Name, 74 Relation: targetRelation.Name, 75 } 76 77 for _, childOneof := range children { 78 switch child := childOneof.ChildType.(type) { 79 case *core.SetOperation_Child_XThis: 80 return fmt.Errorf("use of _this is unsupported; please rewrite your schema") 81 82 case *core.SetOperation_Child_ComputedUserset: 83 // A computed userset adds an entrypoint indicating that the relation is rewritten. 84 err := addSubjectEntrypoint(graph, ts.nsDef.Name, child.ComputedUserset.Relation, &core.ReachabilityEntrypoint{ 85 Kind: core.ReachabilityEntrypoint_COMPUTED_USERSET_ENTRYPOINT, 86 TargetRelation: rr, 87 ResultStatus: operationResultState, 88 }) 89 if err != nil { 90 return err 91 } 92 93 case *core.SetOperation_Child_UsersetRewrite: 94 err := computeRewriteReachability(ctx, graph, child.UsersetRewrite, operationResultState, targetRelation, ts, option) 95 if err != nil { 96 return err 97 } 98 99 case *core.SetOperation_Child_TupleToUserset: 100 tuplesetRelation := child.TupleToUserset.Tupleset.Relation 101 directRelationTypes, err := ts.AllowedDirectRelationsAndWildcards(tuplesetRelation) 102 if err != nil { 103 return err 104 } 105 106 computedUsersetRelation := child.TupleToUserset.ComputedUserset.Relation 107 for _, allowedRelationType := range directRelationTypes { 108 // For each namespace allowed to be found on the right hand side of the 109 // tupleset relation, include the *computed userset* relation as an entrypoint. 110 // 111 // For example, given a schema: 112 // 113 // ``` 114 // definition user {} 115 // 116 // definition parent1 { 117 // relation somerel: user 118 // } 119 // 120 // definition parent2 { 121 // relation somerel: user 122 // } 123 // 124 // definition child { 125 // relation parent: parent1 | parent2 126 // permission someperm = parent->somerel 127 // } 128 // ``` 129 // 130 // We will add an entrypoint for the arrow itself, keyed to the relation type 131 // included from the computed userset. 132 // 133 // Using the above example, this will add entrypoints for `parent1#somerel` 134 // and `parent2#somerel`, which are the subjects reached after resolving the 135 // right side of the arrow. 136 137 // Check if the relation does exist on the allowed type, and only add the entrypoint if present. 138 relTypeSystem, err := ts.TypeSystemForNamespace(ctx, allowedRelationType.Namespace) 139 if err != nil { 140 return err 141 } 142 143 if relTypeSystem.HasRelation(computedUsersetRelation) { 144 err := addSubjectEntrypoint(graph, allowedRelationType.Namespace, computedUsersetRelation, &core.ReachabilityEntrypoint{ 145 Kind: core.ReachabilityEntrypoint_TUPLESET_TO_USERSET_ENTRYPOINT, 146 TargetRelation: rr, 147 ResultStatus: operationResultState, 148 TuplesetRelation: tuplesetRelation, 149 }) 150 if err != nil { 151 return err 152 } 153 } 154 } 155 156 case *core.SetOperation_Child_XNil: 157 // nil has no entrypoints. 158 return nil 159 160 default: 161 return fmt.Errorf("unknown set operation child `%T` in reachability graph building", child) 162 } 163 } 164 165 return nil 166 } 167 168 func addSubjectEntrypoint(graph *core.ReachabilityGraph, namespaceName string, relationName string, entrypoint *core.ReachabilityEntrypoint) error { 169 key := tuple.JoinRelRef(namespaceName, relationName) 170 if relationName == "" { 171 return spiceerrors.MustBugf("found empty relation name for subject entrypoint") 172 } 173 174 if graph.EntrypointsBySubjectRelation[key] == nil { 175 graph.EntrypointsBySubjectRelation[key] = &core.ReachabilityEntrypoints{ 176 Entrypoints: []*core.ReachabilityEntrypoint{}, 177 SubjectRelation: &core.RelationReference{ 178 Namespace: namespaceName, 179 Relation: relationName, 180 }, 181 } 182 } 183 184 graph.EntrypointsBySubjectRelation[key].Entrypoints = append( 185 graph.EntrypointsBySubjectRelation[key].Entrypoints, 186 entrypoint, 187 ) 188 189 return nil 190 } 191 192 func addSubjectLinks(graph *core.ReachabilityGraph, operationResultState core.ReachabilityEntrypoint_EntrypointResultStatus, relation *core.Relation, ts *TypeSystem) error { 193 typeInfo := relation.GetTypeInformation() 194 if typeInfo == nil { 195 return fmt.Errorf("missing type information for relation %s#%s", ts.nsDef.Name, relation.Name) 196 } 197 198 rr := &core.RelationReference{ 199 Namespace: ts.nsDef.Name, 200 Relation: relation.Name, 201 } 202 203 allowedDirectRelations := typeInfo.GetAllowedDirectRelations() 204 for _, directRelation := range allowedDirectRelations { 205 // If the allowed relation is a wildcard, add it as a subject *type* entrypoint, rather than 206 // a subject relation. 207 if directRelation.GetPublicWildcard() != nil { 208 if graph.EntrypointsBySubjectType[directRelation.Namespace] == nil { 209 graph.EntrypointsBySubjectType[directRelation.Namespace] = &core.ReachabilityEntrypoints{ 210 Entrypoints: []*core.ReachabilityEntrypoint{}, 211 SubjectType: directRelation.Namespace, 212 } 213 } 214 215 graph.EntrypointsBySubjectType[directRelation.Namespace].Entrypoints = append( 216 graph.EntrypointsBySubjectType[directRelation.Namespace].Entrypoints, 217 &core.ReachabilityEntrypoint{ 218 Kind: core.ReachabilityEntrypoint_RELATION_ENTRYPOINT, 219 TargetRelation: rr, 220 ResultStatus: operationResultState, 221 }, 222 ) 223 continue 224 } 225 226 err := addSubjectEntrypoint(graph, directRelation.Namespace, directRelation.GetRelation(), &core.ReachabilityEntrypoint{ 227 Kind: core.ReachabilityEntrypoint_RELATION_ENTRYPOINT, 228 TargetRelation: rr, 229 ResultStatus: operationResultState, 230 }) 231 if err != nil { 232 return err 233 } 234 } 235 236 return nil 237 }