github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/services/shared/errors.go (about)

     1  package shared
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strconv"
     8  
     9  	"github.com/rs/zerolog"
    10  	"google.golang.org/genproto/googleapis/rpc/errdetails"
    11  	"google.golang.org/grpc/codes"
    12  	"google.golang.org/grpc/status"
    13  
    14  	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
    15  
    16  	"github.com/authzed/spicedb/internal/dispatch"
    17  	"github.com/authzed/spicedb/internal/graph"
    18  	log "github.com/authzed/spicedb/internal/logging"
    19  	"github.com/authzed/spicedb/internal/sharederrors"
    20  	"github.com/authzed/spicedb/pkg/cursor"
    21  	"github.com/authzed/spicedb/pkg/datastore"
    22  	dispatchv1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
    23  	"github.com/authzed/spicedb/pkg/schemadsl/compiler"
    24  	"github.com/authzed/spicedb/pkg/spiceerrors"
    25  	"github.com/authzed/spicedb/pkg/typesystem"
    26  )
    27  
    28  // ErrServiceReadOnly is an extended GRPC error returned when a service is in read-only mode.
    29  var ErrServiceReadOnly = mustMakeStatusReadonly()
    30  
    31  func mustMakeStatusReadonly() error {
    32  	status, err := status.New(codes.Unavailable, "service read-only").WithDetails(&errdetails.ErrorInfo{
    33  		Reason: v1.ErrorReason_name[int32(v1.ErrorReason_ERROR_REASON_SERVICE_READ_ONLY)],
    34  		Domain: spiceerrors.Domain,
    35  	})
    36  	if err != nil {
    37  		panic("error constructing shared error type")
    38  	}
    39  	return status.Err()
    40  }
    41  
    42  // NewSchemaWriteDataValidationError creates a new error representing that a schema write cannot be
    43  // completed due to existing data that would be left unreferenced.
    44  func NewSchemaWriteDataValidationError(message string, args ...any) ErrSchemaWriteDataValidation {
    45  	return ErrSchemaWriteDataValidation{
    46  		error: fmt.Errorf(message, args...),
    47  	}
    48  }
    49  
    50  // ErrSchemaWriteDataValidation occurs when a schema cannot be applied due to leaving data unreferenced.
    51  type ErrSchemaWriteDataValidation struct {
    52  	error
    53  }
    54  
    55  // MarshalZerologObject implements zerolog object marshalling.
    56  func (err ErrSchemaWriteDataValidation) MarshalZerologObject(e *zerolog.Event) {
    57  	e.Err(err.error)
    58  }
    59  
    60  // GRPCStatus implements retrieving the gRPC status for the error.
    61  func (err ErrSchemaWriteDataValidation) GRPCStatus() *status.Status {
    62  	return spiceerrors.WithCodeAndDetails(
    63  		err,
    64  		codes.InvalidArgument,
    65  		spiceerrors.ForReason(
    66  			v1.ErrorReason_ERROR_REASON_SCHEMA_TYPE_ERROR,
    67  			map[string]string{},
    68  		),
    69  	)
    70  }
    71  
    72  // MaxDepthExceededError is an error returned when the maximum depth for dispatching has been exceeded.
    73  type MaxDepthExceededError struct {
    74  	error
    75  
    76  	// AllowedMaximumDepth is the configured allowed maximum depth.
    77  	AllowedMaximumDepth uint32
    78  }
    79  
    80  // GRPCStatus implements retrieving the gRPC status for the error.
    81  func (err MaxDepthExceededError) GRPCStatus() *status.Status {
    82  	return spiceerrors.WithCodeAndDetails(
    83  		err,
    84  		codes.ResourceExhausted,
    85  		spiceerrors.ForReason(
    86  			v1.ErrorReason_ERROR_REASON_MAXIMUM_DEPTH_EXCEEDED,
    87  			map[string]string{
    88  				"maximum_depth_allowed": strconv.Itoa(int(err.AllowedMaximumDepth)),
    89  			},
    90  		),
    91  	)
    92  }
    93  
    94  // NewMaxDepthExceededError creates a new MaxDepthExceededError.
    95  func NewMaxDepthExceededError(allowedMaximumDepth uint32, isCheckRequest bool) error {
    96  	if isCheckRequest {
    97  		return MaxDepthExceededError{
    98  			fmt.Errorf("the check request has exceeded the allowable maximum depth of %d: this usually indicates a recursive or too deep data dependency. Try running zed with --explain to see the dependency. See: https://spicedb.dev/d/debug-max-depth-check", allowedMaximumDepth),
    99  			allowedMaximumDepth,
   100  		}
   101  	}
   102  
   103  	return MaxDepthExceededError{
   104  		fmt.Errorf("the request has exceeded the allowable maximum depth of %d: this usually indicates a recursive or too deep data dependency. See: https://spicedb.dev/d/debug-max-depth", allowedMaximumDepth),
   105  		allowedMaximumDepth,
   106  	}
   107  }
   108  
   109  func AsValidationError(err error) *ErrSchemaWriteDataValidation {
   110  	var validationErr ErrSchemaWriteDataValidation
   111  	if errors.As(err, &validationErr) {
   112  		return &validationErr
   113  	}
   114  	return nil
   115  }
   116  
   117  type ConfigForErrors struct {
   118  	MaximumAPIDepth uint32
   119  }
   120  
   121  func RewriteErrorWithoutConfig(ctx context.Context, err error) error {
   122  	return RewriteError(ctx, err, nil)
   123  }
   124  
   125  func RewriteError(ctx context.Context, err error, config *ConfigForErrors) error {
   126  	// Check if the error can be directly used.
   127  	if _, ok := status.FromError(err); ok {
   128  		return err
   129  	}
   130  
   131  	// Otherwise, convert any graph/datastore errors.
   132  	var nsNotFoundError sharederrors.UnknownNamespaceError
   133  	var relationNotFoundError sharederrors.UnknownRelationError
   134  
   135  	var compilerError compiler.BaseCompilerError
   136  	var sourceError spiceerrors.ErrorWithSource
   137  	var typeError typesystem.TypeError
   138  	var maxDepthError dispatch.MaxDepthExceededError
   139  
   140  	switch {
   141  	case errors.As(err, &typeError):
   142  		return spiceerrors.WithCodeAndReason(err, codes.FailedPrecondition, v1.ErrorReason_ERROR_REASON_SCHEMA_TYPE_ERROR)
   143  	case errors.As(err, &compilerError):
   144  		return spiceerrors.WithCodeAndReason(err, codes.InvalidArgument, v1.ErrorReason_ERROR_REASON_SCHEMA_PARSE_ERROR)
   145  	case errors.As(err, &sourceError):
   146  		return spiceerrors.WithCodeAndReason(err, codes.InvalidArgument, v1.ErrorReason_ERROR_REASON_SCHEMA_PARSE_ERROR)
   147  
   148  	case errors.Is(err, cursor.ErrHashMismatch):
   149  		return spiceerrors.WithCodeAndReason(err, codes.FailedPrecondition, v1.ErrorReason_ERROR_REASON_INVALID_CURSOR)
   150  
   151  	case errors.As(err, &nsNotFoundError):
   152  		return spiceerrors.WithCodeAndReason(err, codes.FailedPrecondition, v1.ErrorReason_ERROR_REASON_UNKNOWN_DEFINITION)
   153  	case errors.As(err, &relationNotFoundError):
   154  		return spiceerrors.WithCodeAndReason(err, codes.FailedPrecondition, v1.ErrorReason_ERROR_REASON_UNKNOWN_RELATION_OR_PERMISSION)
   155  
   156  	case errors.As(err, &maxDepthError):
   157  		if config == nil {
   158  			return spiceerrors.MustBugf("missing config for API error")
   159  		}
   160  
   161  		_, isCheckRequest := maxDepthError.Request.(*dispatchv1.DispatchCheckRequest)
   162  		return NewMaxDepthExceededError(config.MaximumAPIDepth, isCheckRequest)
   163  
   164  	case errors.As(err, &datastore.ErrReadOnly{}):
   165  		return ErrServiceReadOnly
   166  	case errors.As(err, &datastore.ErrInvalidRevision{}):
   167  		return status.Errorf(codes.OutOfRange, "invalid zedtoken: %s", err)
   168  	case errors.As(err, &datastore.ErrReadOnly{}):
   169  		return ErrServiceReadOnly
   170  	case errors.As(err, &datastore.ErrCaveatNameNotFound{}):
   171  		return spiceerrors.WithCodeAndReason(err, codes.FailedPrecondition, v1.ErrorReason_ERROR_REASON_UNKNOWN_CAVEAT)
   172  	case errors.As(err, &datastore.ErrWatchDisabled{}):
   173  		return status.Errorf(codes.FailedPrecondition, "%s", err)
   174  
   175  	case errors.As(err, &graph.ErrInvalidArgument{}):
   176  		return status.Errorf(codes.InvalidArgument, "%s", err)
   177  	case errors.As(err, &graph.ErrRelationMissingTypeInfo{}):
   178  		return status.Errorf(codes.FailedPrecondition, "failed precondition: %s", err)
   179  	case errors.As(err, &graph.ErrAlwaysFail{}):
   180  		log.Ctx(ctx).Err(err).Msg("received internal error")
   181  		return status.Errorf(codes.Internal, "internal error: %s", err)
   182  	case errors.As(err, &graph.ErrUnimplemented{}):
   183  		return status.Errorf(codes.Unimplemented, "%s", err)
   184  	case errors.Is(err, context.DeadlineExceeded):
   185  		return status.Errorf(codes.DeadlineExceeded, "%s", err)
   186  	case errors.Is(err, context.Canceled):
   187  		err := context.Cause(ctx)
   188  		if err != nil {
   189  			if _, ok := status.FromError(err); ok {
   190  				return err
   191  			}
   192  		}
   193  
   194  		return status.Errorf(codes.Canceled, "%s", err)
   195  	default:
   196  		log.Ctx(ctx).Err(err).Msg("received unexpected error")
   197  		return err
   198  	}
   199  }