github.com/openfga/openfga@v1.5.4-rc1/pkg/server/commands/write.go (about) 1 package commands 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 8 openfgav1 "github.com/openfga/api/proto/openfga/v1" 9 "google.golang.org/protobuf/proto" 10 11 "github.com/openfga/openfga/internal/server/config" 12 "github.com/openfga/openfga/internal/validation" 13 "github.com/openfga/openfga/pkg/logger" 14 serverErrors "github.com/openfga/openfga/pkg/server/errors" 15 "github.com/openfga/openfga/pkg/storage" 16 tupleUtils "github.com/openfga/openfga/pkg/tuple" 17 "github.com/openfga/openfga/pkg/typesystem" 18 ) 19 20 // WriteCommand is used to Write and Delete tuples. Instances may be safely shared by multiple goroutines. 21 type WriteCommand struct { 22 logger logger.Logger 23 datastore storage.OpenFGADatastore 24 conditionContextByteLimit int 25 } 26 27 type WriteCommandOption func(*WriteCommand) 28 29 func WithWriteCmdLogger(l logger.Logger) WriteCommandOption { 30 return func(wc *WriteCommand) { 31 wc.logger = l 32 } 33 } 34 35 func WithConditionContextByteLimit(limit int) WriteCommandOption { 36 return func(wc *WriteCommand) { 37 wc.conditionContextByteLimit = limit 38 } 39 } 40 41 // NewWriteCommand creates a WriteCommand with specified storage.OpenFGADatastore to use for storage. 42 func NewWriteCommand(datastore storage.OpenFGADatastore, opts ...WriteCommandOption) *WriteCommand { 43 cmd := &WriteCommand{ 44 datastore: datastore, 45 logger: logger.NewNoopLogger(), 46 conditionContextByteLimit: config.DefaultWriteContextByteLimit, 47 } 48 49 for _, opt := range opts { 50 opt(cmd) 51 } 52 return cmd 53 } 54 55 // Execute deletes and writes the specified tuples. Deletes are applied first, then writes. 56 func (c *WriteCommand) Execute(ctx context.Context, req *openfgav1.WriteRequest) (*openfgav1.WriteResponse, error) { 57 if err := c.validateWriteRequest(ctx, req); err != nil { 58 return nil, err 59 } 60 61 err := c.datastore.Write( 62 ctx, 63 req.GetStoreId(), 64 req.GetDeletes().GetTupleKeys(), 65 req.GetWrites().GetTupleKeys(), 66 ) 67 if err != nil { 68 return nil, serverErrors.HandleError("", err) 69 } 70 71 return &openfgav1.WriteResponse{}, nil 72 } 73 74 func (c *WriteCommand) validateWriteRequest(ctx context.Context, req *openfgav1.WriteRequest) error { 75 ctx, span := tracer.Start(ctx, "validateWriteRequest") 76 defer span.End() 77 78 store := req.GetStoreId() 79 modelID := req.GetAuthorizationModelId() 80 deletes := req.GetDeletes().GetTupleKeys() 81 writes := req.GetWrites().GetTupleKeys() 82 83 if len(deletes) == 0 && len(writes) == 0 { 84 return serverErrors.InvalidWriteInput 85 } 86 87 if len(writes) > 0 { 88 authModel, err := c.datastore.ReadAuthorizationModel(ctx, store, modelID) 89 if err != nil { 90 if errors.Is(err, storage.ErrNotFound) { 91 return serverErrors.AuthorizationModelNotFound(modelID) 92 } 93 return err 94 } 95 96 if !typesystem.IsSchemaVersionSupported(authModel.GetSchemaVersion()) { 97 return serverErrors.ValidationError(typesystem.ErrInvalidSchemaVersion) 98 } 99 100 typesys := typesystem.New(authModel) 101 102 for _, tk := range writes { 103 err := validation.ValidateTuple(typesys, tk) 104 if err != nil { 105 return serverErrors.ValidationError(err) 106 } 107 108 err = c.validateNotImplicit(tk) 109 if err != nil { 110 return err 111 } 112 113 contextSize := proto.Size(tk.GetCondition().GetContext()) 114 if contextSize > c.conditionContextByteLimit { 115 return serverErrors.ValidationError(&tupleUtils.InvalidTupleError{ 116 Cause: fmt.Errorf("condition context size limit exceeded: %d bytes exceeds %d bytes", contextSize, c.conditionContextByteLimit), 117 TupleKey: tk, 118 }) 119 } 120 } 121 } 122 123 for _, tk := range deletes { 124 if ok := tupleUtils.IsValidUser(tk.GetUser()); !ok { 125 return serverErrors.ValidationError( 126 &tupleUtils.InvalidTupleError{ 127 Cause: fmt.Errorf("the 'user' field is malformed"), 128 TupleKey: tk, 129 }, 130 ) 131 } 132 } 133 134 if err := c.validateNoDuplicatesAndCorrectSize(deletes, writes); err != nil { 135 return err 136 } 137 138 return nil 139 } 140 141 // validateNoDuplicatesAndCorrectSize ensures the deletes and writes contain no duplicates and length fits. 142 func (c *WriteCommand) validateNoDuplicatesAndCorrectSize( 143 deletes []*openfgav1.TupleKeyWithoutCondition, 144 writes []*openfgav1.TupleKey, 145 ) error { 146 tuples := map[string]struct{}{} 147 148 for _, tk := range deletes { 149 key := tupleUtils.TupleKeyToString(tk) 150 if _, ok := tuples[key]; ok { 151 return serverErrors.DuplicateTupleInWrite(tk) 152 } 153 tuples[key] = struct{}{} 154 } 155 156 for _, tk := range writes { 157 key := tupleUtils.TupleKeyToString(tk) 158 if _, ok := tuples[key]; ok { 159 return serverErrors.DuplicateTupleInWrite(tk) 160 } 161 tuples[key] = struct{}{} 162 } 163 164 if len(tuples) > c.datastore.MaxTuplesPerWrite() { 165 return serverErrors.ExceededEntityLimit("write operations", c.datastore.MaxTuplesPerWrite()) 166 } 167 return nil 168 } 169 170 // validateNotImplicit ensures the tuple to be written (not deleted) is not of the form `object:id # relation @ object:id#relation`. 171 func (c *WriteCommand) validateNotImplicit( 172 tk *openfgav1.TupleKey, 173 ) error { 174 userObject, userRelation := tupleUtils.SplitObjectRelation(tk.GetUser()) 175 if tk.GetRelation() == userRelation && tk.GetObject() == userObject { 176 return serverErrors.ValidationError(&tupleUtils.InvalidTupleError{ 177 Cause: fmt.Errorf("cannot write a tuple that is implicit"), 178 TupleKey: tk, 179 }) 180 } 181 return nil 182 }