github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/relationships/validation.go (about)

     1  package relationships
     2  
     3  import (
     4  	"context"
     5  
     6  	"github.com/samber/lo"
     7  
     8  	"github.com/authzed/spicedb/internal/namespace"
     9  	"github.com/authzed/spicedb/pkg/caveats"
    10  	"github.com/authzed/spicedb/pkg/datastore"
    11  	"github.com/authzed/spicedb/pkg/genutil/mapz"
    12  	ns "github.com/authzed/spicedb/pkg/namespace"
    13  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    14  	"github.com/authzed/spicedb/pkg/spiceerrors"
    15  	"github.com/authzed/spicedb/pkg/tuple"
    16  	"github.com/authzed/spicedb/pkg/typesystem"
    17  )
    18  
    19  // ValidateRelationshipUpdates performs validation on the given relationship updates, ensuring that
    20  // they can be applied against the datastore.
    21  func ValidateRelationshipUpdates(
    22  	ctx context.Context,
    23  	reader datastore.Reader,
    24  	updates []*core.RelationTupleUpdate,
    25  ) error {
    26  	rels := lo.Map(updates, func(item *core.RelationTupleUpdate, _ int) *core.RelationTuple {
    27  		return item.Tuple
    28  	})
    29  
    30  	// Load namespaces and caveats.
    31  	referencedNamespaceMap, referencedCaveatMap, err := loadNamespacesAndCaveats(ctx, rels, reader)
    32  	if err != nil {
    33  		return err
    34  	}
    35  
    36  	// Validate each updates's types.
    37  	for _, update := range updates {
    38  		option := ValidateRelationshipForCreateOrTouch
    39  		if update.Operation == core.RelationTupleUpdate_DELETE {
    40  			option = ValidateRelationshipForDeletion
    41  		}
    42  
    43  		if err := ValidateOneRelationship(
    44  			referencedNamespaceMap,
    45  			referencedCaveatMap,
    46  			update.Tuple,
    47  			option,
    48  		); err != nil {
    49  			return err
    50  		}
    51  	}
    52  
    53  	return nil
    54  }
    55  
    56  // ValidateRelationshipsForCreateOrTouch performs validation on the given relationships to be written, ensuring that
    57  // they can be applied against the datastore.
    58  //
    59  // NOTE: This method *cannot* be used for relationships that will be deleted.
    60  func ValidateRelationshipsForCreateOrTouch(
    61  	ctx context.Context,
    62  	reader datastore.Reader,
    63  	rels []*core.RelationTuple,
    64  ) error {
    65  	// Load namespaces and caveats.
    66  	referencedNamespaceMap, referencedCaveatMap, err := loadNamespacesAndCaveats(ctx, rels, reader)
    67  	if err != nil {
    68  		return err
    69  	}
    70  
    71  	// Validate each relationship's types.
    72  	for _, rel := range rels {
    73  		if err := ValidateOneRelationship(
    74  			referencedNamespaceMap,
    75  			referencedCaveatMap,
    76  			rel,
    77  			ValidateRelationshipForCreateOrTouch,
    78  		); err != nil {
    79  			return err
    80  		}
    81  	}
    82  
    83  	return nil
    84  }
    85  
    86  func loadNamespacesAndCaveats(ctx context.Context, rels []*core.RelationTuple, reader datastore.Reader) (map[string]*typesystem.TypeSystem, map[string]*core.CaveatDefinition, error) {
    87  	referencedNamespaceNames := mapz.NewSet[string]()
    88  	referencedCaveatNamesWithContext := mapz.NewSet[string]()
    89  	for _, rel := range rels {
    90  		referencedNamespaceNames.Insert(rel.ResourceAndRelation.Namespace)
    91  		referencedNamespaceNames.Insert(rel.Subject.Namespace)
    92  		if hasNonEmptyCaveatContext(rel) {
    93  			referencedCaveatNamesWithContext.Insert(rel.Caveat.CaveatName)
    94  		}
    95  	}
    96  
    97  	var referencedNamespaceMap map[string]*typesystem.TypeSystem
    98  	var referencedCaveatMap map[string]*core.CaveatDefinition
    99  
   100  	if !referencedNamespaceNames.IsEmpty() {
   101  		foundNamespaces, err := reader.LookupNamespacesWithNames(ctx, referencedNamespaceNames.AsSlice())
   102  		if err != nil {
   103  			return nil, nil, err
   104  		}
   105  
   106  		referencedNamespaceMap = make(map[string]*typesystem.TypeSystem, len(foundNamespaces))
   107  		for _, nsDef := range foundNamespaces {
   108  			nts, err := typesystem.NewNamespaceTypeSystem(nsDef.Definition, typesystem.ResolverForDatastoreReader(reader))
   109  			if err != nil {
   110  				return nil, nil, err
   111  			}
   112  
   113  			referencedNamespaceMap[nsDef.Definition.Name] = nts
   114  		}
   115  	}
   116  
   117  	if !referencedCaveatNamesWithContext.IsEmpty() {
   118  		foundCaveats, err := reader.LookupCaveatsWithNames(ctx, referencedCaveatNamesWithContext.AsSlice())
   119  		if err != nil {
   120  			return nil, nil, err
   121  		}
   122  
   123  		referencedCaveatMap = make(map[string]*core.CaveatDefinition, len(foundCaveats))
   124  		for _, caveatDef := range foundCaveats {
   125  			referencedCaveatMap[caveatDef.Definition.Name] = caveatDef.Definition
   126  		}
   127  	}
   128  	return referencedNamespaceMap, referencedCaveatMap, nil
   129  }
   130  
   131  // ValidationRelationshipRule is the rule to use for the validation.
   132  type ValidationRelationshipRule int
   133  
   134  const (
   135  	// ValidateRelationshipForCreateOrTouch indicates that the validation should occur for a CREATE or TOUCH operation.
   136  	ValidateRelationshipForCreateOrTouch ValidationRelationshipRule = 0
   137  
   138  	// ValidateRelationshipForDeletion indicates that the validation should occur for a DELETE operation.
   139  	ValidateRelationshipForDeletion ValidationRelationshipRule = 1
   140  )
   141  
   142  // ValidateOneRelationship validates a single relationship for CREATE/TOUCH or DELETE.
   143  func ValidateOneRelationship(
   144  	namespaceMap map[string]*typesystem.TypeSystem,
   145  	caveatMap map[string]*core.CaveatDefinition,
   146  	rel *core.RelationTuple,
   147  	rule ValidationRelationshipRule,
   148  ) error {
   149  	// Validate the IDs of the resource and subject.
   150  	if err := tuple.ValidateResourceID(rel.ResourceAndRelation.ObjectId); err != nil {
   151  		return err
   152  	}
   153  
   154  	if err := tuple.ValidateSubjectID(rel.Subject.ObjectId); err != nil {
   155  		return err
   156  	}
   157  
   158  	// Validate the namespace and relation for the resource.
   159  	resourceTS, ok := namespaceMap[rel.ResourceAndRelation.Namespace]
   160  	if !ok {
   161  		return namespace.NewNamespaceNotFoundErr(rel.ResourceAndRelation.Namespace)
   162  	}
   163  
   164  	if !resourceTS.HasRelation(rel.ResourceAndRelation.Relation) {
   165  		return namespace.NewRelationNotFoundErr(rel.ResourceAndRelation.Namespace, rel.ResourceAndRelation.Relation)
   166  	}
   167  
   168  	// Validate the namespace and relation for the subject.
   169  	subjectTS, ok := namespaceMap[rel.Subject.Namespace]
   170  	if !ok {
   171  		return namespace.NewNamespaceNotFoundErr(rel.Subject.Namespace)
   172  	}
   173  
   174  	if rel.Subject.Relation != tuple.Ellipsis {
   175  		if !subjectTS.HasRelation(rel.Subject.Relation) {
   176  			return namespace.NewRelationNotFoundErr(rel.Subject.Namespace, rel.Subject.Relation)
   177  		}
   178  	}
   179  
   180  	// Validate that the relationship is not writing to a permission.
   181  	if resourceTS.IsPermission(rel.ResourceAndRelation.Relation) {
   182  		return NewCannotWriteToPermissionError(rel)
   183  	}
   184  
   185  	// Validate the subject against the allowed relation(s).
   186  	var caveat *core.AllowedCaveat
   187  	if rel.Caveat != nil {
   188  		caveat = ns.AllowedCaveat(rel.Caveat.CaveatName)
   189  	}
   190  
   191  	var relationToCheck *core.AllowedRelation
   192  	if rel.Subject.ObjectId == tuple.PublicWildcard {
   193  		relationToCheck = ns.AllowedPublicNamespaceWithCaveat(rel.Subject.Namespace, caveat)
   194  	} else {
   195  		relationToCheck = ns.AllowedRelationWithCaveat(
   196  			rel.Subject.Namespace,
   197  			rel.Subject.Relation,
   198  			caveat)
   199  	}
   200  
   201  	switch {
   202  	case rule == ValidateRelationshipForCreateOrTouch || caveat != nil:
   203  		// For writing or when the caveat was specified, the caveat must be a direct match.
   204  		isAllowed, err := resourceTS.HasAllowedRelation(
   205  			rel.ResourceAndRelation.Relation,
   206  			relationToCheck)
   207  		if err != nil {
   208  			return err
   209  		}
   210  
   211  		if isAllowed != typesystem.AllowedRelationValid {
   212  			return NewInvalidSubjectTypeError(rel, relationToCheck)
   213  		}
   214  
   215  	case rule == ValidateRelationshipForDeletion && caveat == nil:
   216  		// For deletion, the caveat *can* be ignored if not specified.
   217  		if rel.Subject.ObjectId == tuple.PublicWildcard {
   218  			isAllowed, err := resourceTS.IsAllowedPublicNamespace(rel.ResourceAndRelation.Relation, rel.Subject.Namespace)
   219  			if err != nil {
   220  				return err
   221  			}
   222  
   223  			if isAllowed != typesystem.PublicSubjectAllowed {
   224  				return NewInvalidSubjectTypeError(rel, relationToCheck)
   225  			}
   226  		} else {
   227  			isAllowed, err := resourceTS.IsAllowedDirectRelation(rel.ResourceAndRelation.Relation, rel.Subject.Namespace, rel.Subject.Relation)
   228  			if err != nil {
   229  				return err
   230  			}
   231  
   232  			if isAllowed != typesystem.DirectRelationValid {
   233  				return NewInvalidSubjectTypeError(rel, relationToCheck)
   234  			}
   235  		}
   236  
   237  	default:
   238  		return spiceerrors.MustBugf("unknown validate rule")
   239  	}
   240  
   241  	// Validate caveat and its context, if applicable.
   242  	if hasNonEmptyCaveatContext(rel) {
   243  		caveat, ok := caveatMap[rel.Caveat.CaveatName]
   244  		if !ok {
   245  			// Should ideally never happen since the caveat is type checked above, but just in case.
   246  			return NewCaveatNotFoundError(rel)
   247  		}
   248  
   249  		// Verify that the provided context information matches the types of the parameters defined.
   250  		_, err := caveats.ConvertContextToParameters(
   251  			rel.Caveat.Context.AsMap(),
   252  			caveat.ParameterTypes,
   253  			caveats.ErrorForUnknownParameters,
   254  		)
   255  		if err != nil {
   256  			return err
   257  		}
   258  	}
   259  
   260  	return nil
   261  }
   262  
   263  func hasNonEmptyCaveatContext(update *core.RelationTuple) bool {
   264  	return update.Caveat != nil &&
   265  		update.Caveat.CaveatName != "" &&
   266  		update.Caveat.Context != nil &&
   267  		len(update.Caveat.Context.GetFields()) > 0
   268  }