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 }