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  }