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  }