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  }