github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/graph/membershipset.go (about) 1 package graph 2 3 import ( 4 "github.com/authzed/spicedb/internal/caveats" 5 core "github.com/authzed/spicedb/pkg/proto/core/v1" 6 v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" 7 ) 8 9 var ( 10 caveatOr = caveats.Or 11 caveatAnd = caveats.And 12 caveatSub = caveats.Subtract 13 wrapCaveat = caveats.CaveatAsExpr 14 ) 15 16 // CheckResultsMap defines a type that is a map from resource ID to ResourceCheckResult. 17 // This must match that defined in the DispatchCheckResponse for the `results_by_resource_id` 18 // field. 19 type CheckResultsMap map[string]*v1.ResourceCheckResult 20 21 // NewMembershipSet constructs a new helper set for tracking the membership found for a dispatched 22 // check request. 23 func NewMembershipSet() *MembershipSet { 24 return &MembershipSet{ 25 hasDeterminedMember: false, 26 membersByID: map[string]*core.CaveatExpression{}, 27 } 28 } 29 30 func membershipSetFromMap(mp map[string]*core.CaveatExpression) *MembershipSet { 31 ms := NewMembershipSet() 32 for resourceID, result := range mp { 33 ms.addMember(resourceID, result) 34 } 35 return ms 36 } 37 38 // MembershipSet is a helper set that trackes the membership results for a dispatched Check 39 // request, including tracking of the caveats associated with found resource IDs. 40 type MembershipSet struct { 41 membersByID map[string]*core.CaveatExpression 42 hasDeterminedMember bool 43 } 44 45 // AddDirectMember adds a resource ID that was *directly* found for the dispatched check, with 46 // optional caveat found on the relationship. 47 func (ms *MembershipSet) AddDirectMember(resourceID string, caveat *core.ContextualizedCaveat) { 48 ms.addMember(resourceID, wrapCaveat(caveat)) 49 } 50 51 // AddMemberViaRelationship adds a resource ID that was found via another relationship, such 52 // as the result of an arrow operation. The `parentRelationship` is the relationship that was 53 // followed before the resource itself was resolved. This method will properly apply the caveat(s) 54 // from both the parent relationship and the resource's result itself, assuming either have a caveat 55 // associated. 56 func (ms *MembershipSet) AddMemberViaRelationship( 57 resourceID string, 58 resourceCaveatExpression *core.CaveatExpression, 59 parentRelationship *core.RelationTuple, 60 ) { 61 intersection := caveatAnd(wrapCaveat(parentRelationship.Caveat), resourceCaveatExpression) 62 ms.addMember(resourceID, intersection) 63 } 64 65 func (ms *MembershipSet) addMember(resourceID string, caveatExpr *core.CaveatExpression) { 66 existing, ok := ms.membersByID[resourceID] 67 if !ok { 68 ms.hasDeterminedMember = ms.hasDeterminedMember || caveatExpr == nil 69 ms.membersByID[resourceID] = caveatExpr 70 return 71 } 72 73 // If a determined membership result has already been found (i.e. there is no caveat), 74 // then nothing more to do. 75 if existing == nil { 76 return 77 } 78 79 // If the new caveat expression is nil, then we are adding a determined result. 80 if caveatExpr == nil { 81 ms.hasDeterminedMember = true 82 ms.membersByID[resourceID] = nil 83 return 84 } 85 86 // Otherwise, the caveats get unioned together. 87 ms.membersByID[resourceID] = caveatOr(existing, caveatExpr) 88 } 89 90 // UnionWith combines the results found in the given map with the members of this set. 91 // The changes are made in-place. 92 func (ms *MembershipSet) UnionWith(resultsMap CheckResultsMap) { 93 for resourceID, details := range resultsMap { 94 ms.addMember(resourceID, details.Expression) 95 } 96 } 97 98 // IntersectWith intersects the results found in the given map with the members of this set. 99 // The changes are made in-place. 100 func (ms *MembershipSet) IntersectWith(resultsMap CheckResultsMap) { 101 for resourceID := range ms.membersByID { 102 if _, ok := resultsMap[resourceID]; !ok { 103 delete(ms.membersByID, resourceID) 104 } 105 } 106 107 ms.hasDeterminedMember = false 108 for resourceID, details := range resultsMap { 109 existing, ok := ms.membersByID[resourceID] 110 if !ok { 111 continue 112 } 113 if existing == nil && details.Expression == nil { 114 ms.hasDeterminedMember = true 115 continue 116 } 117 118 ms.membersByID[resourceID] = caveatAnd(existing, details.Expression) 119 } 120 } 121 122 // Subtract subtracts the results found in the given map with the members of this set. 123 // The changes are made in-place. 124 func (ms *MembershipSet) Subtract(resultsMap CheckResultsMap) { 125 ms.hasDeterminedMember = false 126 for resourceID, expression := range ms.membersByID { 127 if details, ok := resultsMap[resourceID]; ok { 128 // If the incoming member has no caveat, then this removal is absolute. 129 if details.Expression == nil { 130 delete(ms.membersByID, resourceID) 131 continue 132 } 133 134 // Otherwise, the caveat expression gets combined with an intersection of the inversion 135 // of the expression. 136 ms.membersByID[resourceID] = caveatSub(expression, details.Expression) 137 } else { 138 if expression == nil { 139 ms.hasDeterminedMember = true 140 } 141 } 142 } 143 } 144 145 // HasConcreteResourceID returns whether the resourceID was found in the set 146 // and has no caveat attached. 147 func (ms *MembershipSet) HasConcreteResourceID(resourceID string) bool { 148 if ms == nil { 149 return false 150 } 151 152 found, ok := ms.membersByID[resourceID] 153 return ok && found == nil 154 } 155 156 // Size returns the number of elements in the membership set. 157 func (ms *MembershipSet) Size() int { 158 if ms == nil { 159 return 0 160 } 161 162 return len(ms.membersByID) 163 } 164 165 // IsEmpty returns true if the set is empty. 166 func (ms *MembershipSet) IsEmpty() bool { 167 if ms == nil { 168 return true 169 } 170 171 return len(ms.membersByID) == 0 172 } 173 174 // HasDeterminedMember returns whether there exists at least one non-caveated member of the set. 175 func (ms *MembershipSet) HasDeterminedMember() bool { 176 if ms == nil { 177 return false 178 } 179 180 return ms.hasDeterminedMember 181 } 182 183 // AsCheckResultsMap converts the membership set back into a CheckResultsMap for placement into 184 // a DispatchCheckResult. 185 func (ms *MembershipSet) AsCheckResultsMap() CheckResultsMap { 186 resultsMap := make(CheckResultsMap, len(ms.membersByID)) 187 for resourceID, caveat := range ms.membersByID { 188 membership := v1.ResourceCheckResult_MEMBER 189 if caveat != nil { 190 membership = v1.ResourceCheckResult_CAVEATED_MEMBER 191 } 192 193 resultsMap[resourceID] = &v1.ResourceCheckResult{ 194 Membership: membership, 195 Expression: caveat, 196 } 197 } 198 199 return resultsMap 200 }