github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/graph/resourcesubjectsmap.go (about) 1 package graph 2 3 import ( 4 "sort" 5 "sync" 6 7 "github.com/authzed/spicedb/pkg/genutil/mapz" 8 core "github.com/authzed/spicedb/pkg/proto/core/v1" 9 v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" 10 "github.com/authzed/spicedb/pkg/spiceerrors" 11 "github.com/authzed/spicedb/pkg/tuple" 12 ) 13 14 type syncONRSet struct { 15 items sync.Map 16 } 17 18 func (s *syncONRSet) Add(onr *core.ObjectAndRelation) bool { 19 key := tuple.StringONR(onr) 20 _, existed := s.items.LoadOrStore(key, struct{}{}) 21 return !existed 22 } 23 24 // resourcesSubjectMap is a multimap which tracks mappings from found resource IDs 25 // to the subject IDs (may be more than one) for each, as well as whether the mapping 26 // is conditional due to the use of a caveat on the relationship which formed the mapping. 27 type resourcesSubjectMap struct { 28 resourceType *core.RelationReference 29 resourcesAndSubjects *mapz.MultiMap[string, subjectInfo] 30 } 31 32 // subjectInfo is the information about a subject contained in a resourcesSubjectMap. 33 type subjectInfo struct { 34 subjectID string 35 isCaveated bool 36 } 37 38 func newResourcesSubjectMap(resourceType *core.RelationReference) resourcesSubjectMap { 39 return resourcesSubjectMap{ 40 resourceType: resourceType, 41 resourcesAndSubjects: mapz.NewMultiMap[string, subjectInfo](), 42 } 43 } 44 45 func newResourcesSubjectMapWithCapacity(resourceType *core.RelationReference, capacity uint32) resourcesSubjectMap { 46 return resourcesSubjectMap{ 47 resourceType: resourceType, 48 resourcesAndSubjects: mapz.NewMultiMapWithCap[string, subjectInfo](capacity), 49 } 50 } 51 52 func subjectIDsToResourcesMap(resourceType *core.RelationReference, subjectIDs []string) resourcesSubjectMap { 53 rsm := newResourcesSubjectMap(resourceType) 54 for _, subjectID := range subjectIDs { 55 rsm.addSubjectIDAsFoundResourceID(subjectID) 56 } 57 return rsm 58 } 59 60 // addRelationship adds the relationship to the resource subject map, recording a mapping from 61 // the resource of the relationship to the subject, as well as whether the relationship was caveated. 62 func (rsm resourcesSubjectMap) addRelationship(rel *core.RelationTuple) error { 63 if rel.ResourceAndRelation.Namespace != rsm.resourceType.Namespace || 64 rel.ResourceAndRelation.Relation != rsm.resourceType.Relation { 65 return spiceerrors.MustBugf("invalid relationship for addRelationship. expected: %v, found: %v", rsm.resourceType, rel.ResourceAndRelation) 66 } 67 68 rsm.resourcesAndSubjects.Add(rel.ResourceAndRelation.ObjectId, subjectInfo{rel.Subject.ObjectId, rel.Caveat != nil && rel.Caveat.CaveatName != ""}) 69 return nil 70 } 71 72 // addSubjectIDAsFoundResourceID adds a subject ID directly as a found subject for itself as the resource, 73 // with no associated caveat. 74 func (rsm resourcesSubjectMap) addSubjectIDAsFoundResourceID(subjectID string) { 75 rsm.resourcesAndSubjects.Add(subjectID, subjectInfo{subjectID, false}) 76 } 77 78 // asReadOnly returns a read-only dispatchableResourcesSubjectMap for dispatching for the 79 // resources in this map (if any). 80 func (rsm resourcesSubjectMap) asReadOnly() dispatchableResourcesSubjectMap { 81 return dispatchableResourcesSubjectMap{rsm.resourceType, rsm.resourcesAndSubjects.AsReadOnly()} 82 } 83 84 func (rsm resourcesSubjectMap) len() int { 85 return rsm.resourcesAndSubjects.Len() 86 } 87 88 // dispatchableResourcesSubjectMap is a read-only, frozen version of the resourcesSubjectMap that 89 // can be used for mapping conditionals once calls have been dispatched. This is read-only due to 90 // its use by concurrent callers. 91 type dispatchableResourcesSubjectMap struct { 92 resourceType *core.RelationReference 93 resourcesAndSubjects mapz.ReadOnlyMultimap[string, subjectInfo] 94 } 95 96 func (rsm dispatchableResourcesSubjectMap) isEmpty() bool { 97 return rsm.resourcesAndSubjects.IsEmpty() 98 } 99 100 func (rsm dispatchableResourcesSubjectMap) resourceIDs() []string { 101 return rsm.resourcesAndSubjects.Keys() 102 } 103 104 // filterSubjectIDsToDispatch returns the set of subject IDs that have not yet been 105 // dispatched, by adding them to the dispatched set. 106 func (rsm dispatchableResourcesSubjectMap) filterSubjectIDsToDispatch(dispatched *syncONRSet, dispatchSubjectType *core.RelationReference) []string { 107 resourceIDs := rsm.resourceIDs() 108 filtered := make([]string, 0, len(resourceIDs)) 109 for _, resourceID := range resourceIDs { 110 if dispatched.Add(&core.ObjectAndRelation{ 111 Namespace: dispatchSubjectType.Namespace, 112 ObjectId: resourceID, 113 Relation: dispatchSubjectType.Relation, 114 }) { 115 filtered = append(filtered, resourceID) 116 } 117 } 118 119 return filtered 120 } 121 122 // asReachableResources converts the resources found in the map into a slice of ReachableResource 123 // messages, with isDirectEntrypoint and each subject's caveat indicating whether the resource 124 // is directly found or requires an additional Check operation. 125 func (rsm dispatchableResourcesSubjectMap) asReachableResources(isDirectEntrypoint bool) []*v1.ReachableResource { 126 resources := make([]*v1.ReachableResource, 0, rsm.resourcesAndSubjects.Len()) 127 128 // Sort for stability. 129 sortedResourceIds := rsm.resourcesAndSubjects.Keys() 130 sort.Strings(sortedResourceIds) 131 132 for _, resourceID := range sortedResourceIds { 133 status := v1.ReachableResource_REQUIRES_CHECK 134 if isDirectEntrypoint { 135 status = v1.ReachableResource_HAS_PERMISSION 136 } 137 138 subjectInfos, _ := rsm.resourcesAndSubjects.Get(resourceID) 139 subjectIDs := make([]string, 0, len(subjectInfos)) 140 allCaveated := true 141 nonCaveatedSubjectIDs := make([]string, 0, len(subjectInfos)) 142 143 for _, info := range subjectInfos { 144 subjectIDs = append(subjectIDs, info.subjectID) 145 if !info.isCaveated { 146 allCaveated = false 147 nonCaveatedSubjectIDs = append(nonCaveatedSubjectIDs, info.subjectID) 148 } 149 } 150 151 // Sort for stability. 152 sort.Strings(subjectIDs) 153 154 // If all the incoming edges are caveated, then the entire status has to be marked as a check 155 // is required. Otherwise, if there is at least *one* non-caveated incoming edge, then we can 156 // return the existing status as a short-circuit for those non-caveated found subjects. 157 if allCaveated { 158 resources = append(resources, &v1.ReachableResource{ 159 ResourceId: resourceID, 160 ForSubjectIds: subjectIDs, 161 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 162 }) 163 } else { 164 resources = append(resources, &v1.ReachableResource{ 165 ResourceId: resourceID, 166 ForSubjectIds: nonCaveatedSubjectIDs, 167 ResultStatus: status, 168 }) 169 } 170 } 171 return resources 172 } 173 174 // mapFoundResource takes in a found resource and maps it via its parent relationship to 175 // the resulting found resource. 176 func (rsm dispatchableResourcesSubjectMap) mapFoundResource(foundResource *v1.ReachableResource, isDirectEntrypoint bool) (*v1.ReachableResource, error) { 177 // For the found resource, lookup the associated entry(s) for the "ForSubjectIDs" and 178 // check if *all* are conditional. If so, then the overall status *must* be conditional. 179 // Otherwise, the status depends on the status of the incoming result and whether the result 180 // was for a direct entrypoint. 181 // Start with the status from the found resource. 182 status := foundResource.ResultStatus 183 184 // If not a direct entrypoint, then the status, by definition, is to require a check. 185 if !isDirectEntrypoint { 186 status = v1.ReachableResource_REQUIRES_CHECK 187 } 188 189 forSubjectIDs := mapz.NewSet[string]() 190 nonCaveatedSubjectIDs := mapz.NewSet[string]() 191 for _, forSubjectID := range foundResource.ForSubjectIds { 192 // Map from the incoming subject ID to the subject ID(s) that caused the dispatch. 193 infos, ok := rsm.resourcesAndSubjects.Get(forSubjectID) 194 if !ok { 195 return nil, spiceerrors.MustBugf("missing for subject ID") 196 } 197 198 for _, info := range infos { 199 forSubjectIDs.Insert(info.subjectID) 200 if !info.isCaveated { 201 nonCaveatedSubjectIDs.Insert(info.subjectID) 202 } 203 } 204 } 205 206 // If there are some non-caveated IDs, return those and mark as the parent status. 207 if nonCaveatedSubjectIDs.Len() > 0 { 208 return &v1.ReachableResource{ 209 ResourceId: foundResource.ResourceId, 210 ForSubjectIds: nonCaveatedSubjectIDs.AsSlice(), 211 ResultStatus: status, 212 }, nil 213 } 214 215 // Otherwise, everything is caveated, so return the full set of subject IDs and mark 216 // as a check is required. 217 return &v1.ReachableResource{ 218 ResourceId: foundResource.ResourceId, 219 ForSubjectIds: forSubjectIDs.AsSlice(), 220 ResultStatus: v1.ReachableResource_REQUIRES_CHECK, 221 }, nil 222 }