mirror of
https://github.com/r3-team/r3.git
synced 2026-02-05 17:58:31 -06:00
240 lines
7.8 KiB
Go
240 lines
7.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"slices"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
)
|
|
|
|
type errExpected struct {
|
|
context string
|
|
matchRx *regexp.Regexp // regex that matches the expected error message
|
|
number int
|
|
}
|
|
|
|
const (
|
|
// legacy, to be replaced
|
|
ErrAuthFailed = "authentication failed"
|
|
ErrBruteforceBlock = "blocked assumed bruteforce attempt"
|
|
ErrGeneral = "general error"
|
|
ErrUnauthorized = "unauthorized"
|
|
ErrWsClientChanFull = "client channel is full, dropping response"
|
|
|
|
// error contexts
|
|
ErrContextApp = "APP"
|
|
ErrContextCsv = "CSV"
|
|
ErrContextDbs = "DBS"
|
|
ErrContextLic = "LIC"
|
|
ErrContextSec = "SEC"
|
|
|
|
// error codes
|
|
ErrCodeAppUnknown int = 1
|
|
ErrCodeAppContextExceeded int = 2
|
|
ErrCodeAppContextCanceled int = 3
|
|
ErrCodeAppPresetProtected int = 4
|
|
ErrCodeAppNameEmpty int = 5
|
|
ErrCodeAppNameInvalid int = 6
|
|
ErrCodeAppUnknownModule int = 7
|
|
ErrCodeAppUnknownRelation int = 8
|
|
ErrCodeAppUnknownAttribute int = 9
|
|
ErrCodeCsvParseInt int = 1
|
|
ErrCodeCsvParseFloat int = 2
|
|
ErrCodeCsvParseDateTime int = 3
|
|
ErrCodeCsvBadAttributeType int = 4
|
|
ErrCodeCsvWrongFieldNumber int = 5
|
|
ErrCodeCsvEncryptedAttribute int = 6
|
|
ErrCodeDbsFunctionMessage int = 1
|
|
ErrCodeDbsConstraintUnique int = 2
|
|
ErrCodeDbsConstraintUniqueLogin int = 3
|
|
ErrCodeDbsConstraintFk int = 4
|
|
ErrCodeDbsConstraintNotNull int = 5
|
|
ErrCodeDbsIndexFailUnique int = 6 // special: is applied on frontend only, if ErrCodeDbsConstraintUnique is used but ID is unknown
|
|
ErrCodeDbsInvalidTypeSyntax int = 7
|
|
ErrCodeDbsChangedCachePlan int = 8
|
|
ErrCodeLicValidityExpired int = 1
|
|
ErrCodeLicLoginsReached int = 2
|
|
ErrCodeSecUnauthorized int = 1
|
|
ErrCodeSecDataKeysNotAvailable int = 5
|
|
ErrCodeSecNoPublicKeys int = 6
|
|
)
|
|
|
|
var (
|
|
// errors
|
|
errContexts = []string{ErrContextApp, ErrContextCsv, ErrContextDbs, ErrContextLic, ErrContextSec}
|
|
errCodeDbsCache = regexp.MustCompile(fmt.Sprintf("^{ERR_DBS_%03d}", ErrCodeDbsChangedCachePlan))
|
|
errCodeLicRx = regexp.MustCompile(`^{ERR_LIC_(\d{3})}`)
|
|
errCodeRx = regexp.MustCompile(`^{ERR_([A-Z]{3})_(\d{3})}`)
|
|
errExpectedList = []errExpected{
|
|
|
|
// security/access
|
|
{ // unauthorized
|
|
context: ErrContextSec,
|
|
matchRx: regexp.MustCompile(fmt.Sprintf(`^%s$`, ErrUnauthorized)),
|
|
number: ErrCodeSecUnauthorized,
|
|
},
|
|
|
|
// application
|
|
{ // context deadline reached
|
|
context: ErrContextApp,
|
|
matchRx: regexp.MustCompile(`^timeout\: context deadline exceeded$`),
|
|
number: ErrCodeAppContextExceeded,
|
|
},
|
|
{ // context canceled
|
|
context: ErrContextApp,
|
|
matchRx: regexp.MustCompile(`^timeout\: context canceled$`),
|
|
number: ErrCodeAppContextCanceled,
|
|
},
|
|
|
|
// CSV handling
|
|
{ // wrong number of fields (error originates from encoding/csv package)
|
|
context: ErrContextCsv,
|
|
matchRx: regexp.MustCompile(`^record on line \d+\: wrong number of fields`),
|
|
number: ErrCodeCsvWrongFieldNumber,
|
|
},
|
|
}
|
|
)
|
|
|
|
// creates standardized error code, to be interpreted and translated on the frontend
|
|
// context is the general error context: APP (application), DBS (database system), SEC (security/access), ...
|
|
// number is the unique error code, used to convert to a translated error message
|
|
// example error code: {ERR_DBS_069}
|
|
func CreateErrCode(context string, number int) error {
|
|
if !slices.Contains(errContexts, context) {
|
|
return errors.New("{INVALID_ERROR_CONTEXT}")
|
|
}
|
|
return fmt.Errorf("{ERR_%s_%03d}", context, number)
|
|
}
|
|
|
|
// as CreateErrCode, but appends JSON encoded data to the string
|
|
func CreateErrCodeWithData(context string, number int, data interface{}) error {
|
|
code := CreateErrCode(context, number)
|
|
|
|
j, err := json.Marshal(data)
|
|
if err != nil {
|
|
return code
|
|
}
|
|
return fmt.Errorf("%s%s", code, j)
|
|
}
|
|
|
|
// converts expected errors to error codes to be parsed/translated by requestor
|
|
// can optionally convert to generic 'unknown error' if error cannot be identified
|
|
// returns whether the error was identified
|
|
func ConvertToErrCode(err error, anonymizeIfUnexpected bool) (error, bool) {
|
|
|
|
var processUnexpectedErr = func(err error) error {
|
|
if anonymizeIfUnexpected {
|
|
return CreateErrCode(ErrContextApp, ErrCodeAppUnknown)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// already an error code, return as is
|
|
if errCodeRx.MatchString(err.Error()) {
|
|
return err, true
|
|
}
|
|
|
|
// check for PGX error
|
|
var pgxErr *pgconn.PgError
|
|
if errors.As(err, &pgxErr) {
|
|
|
|
switch pgxErr.Code {
|
|
case "0A000": // error in prepared statement cache due to changed schema
|
|
return CreateErrCode(ErrContextDbs, ErrCodeDbsChangedCachePlan), true
|
|
case "23502": // NOT NULL constraint failure
|
|
return CreateErrCodeWithData(ErrContextDbs, ErrCodeDbsConstraintNotNull, struct {
|
|
ModuleName string `json:"moduleName"`
|
|
RelationName string `json:"relationName"`
|
|
AttributeName string `json:"attributeName"`
|
|
}{
|
|
pgxErr.SchemaName,
|
|
pgxErr.TableName,
|
|
pgxErr.ColumnName,
|
|
}), true
|
|
case "23503": // foreign key constraint failure
|
|
|
|
// foreign key constraint names have this format: "fk_[UUID]"
|
|
if pgxErr.ConstraintName == "" || pgxErr.ConstraintName[0:3] != "fk_" {
|
|
return processUnexpectedErr(err), false
|
|
}
|
|
|
|
return CreateErrCodeWithData(ErrContextDbs, ErrCodeDbsConstraintFk, struct {
|
|
AttributeId string `json:"attributeId"`
|
|
}{pgxErr.ConstraintName[3:]}), true
|
|
case "23505": // unique index constraint failure
|
|
|
|
// special case: login name index
|
|
if pgxErr.ConstraintName == "login_name_key" {
|
|
return CreateErrCode(ErrContextDbs, ErrCodeDbsConstraintUniqueLogin), true
|
|
}
|
|
|
|
// unique index constraint names have this format: "ind_[UUID]"
|
|
if pgxErr.ConstraintName == "" || pgxErr.ConstraintName[0:4] != "ind_" {
|
|
return processUnexpectedErr(err), false
|
|
}
|
|
|
|
return CreateErrCodeWithData(ErrContextDbs, ErrCodeDbsConstraintUnique, struct {
|
|
PgIndexId string `json:"pgIndexId"`
|
|
}{pgxErr.ConstraintName[4:]}), true
|
|
case "22P02": // invalid type syntax
|
|
return CreateErrCode(ErrContextDbs, ErrCodeDbsInvalidTypeSyntax), true
|
|
case "P0001": // exception raised
|
|
if pgxErr.Message == "" || pgxErr.Message[0:6] != "R3_MSG" {
|
|
return processUnexpectedErr(err), false
|
|
}
|
|
return CreateErrCodeWithData(ErrContextDbs, ErrCodeDbsFunctionMessage, struct {
|
|
Message string `json:"message"`
|
|
}{pgxErr.Message[8:]}), true
|
|
}
|
|
}
|
|
|
|
// check for match against expected errors
|
|
for _, expErr := range errExpectedList {
|
|
if expErr.matchRx.MatchString(err.Error()) {
|
|
return CreateErrCode(expErr.context, expErr.number), true
|
|
}
|
|
}
|
|
return processUnexpectedErr(err), false
|
|
}
|
|
|
|
// error code checker
|
|
func CheckForLicenseErrCode(err error) bool {
|
|
return errCodeLicRx.MatchString(err.Error())
|
|
}
|
|
func CheckForDbsCacheErrCode(err error) bool {
|
|
return errCodeDbsCache.MatchString(err.Error())
|
|
}
|
|
|
|
// default schema errors
|
|
func ErrSchemaUnknownModule(id uuid.UUID) error {
|
|
return fmt.Errorf("unknown module '%s'", id)
|
|
}
|
|
func ErrSchemaUnknownRelation(id uuid.UUID) error {
|
|
return fmt.Errorf("unknown relation '%s'", id)
|
|
}
|
|
func ErrSchemaUnknownAttribute(id uuid.UUID) error {
|
|
return fmt.Errorf("unknown attribute '%s'", id)
|
|
}
|
|
func ErrSchemaUnknownFunction(id uuid.UUID) error {
|
|
return fmt.Errorf("unknown function '%s'", id)
|
|
}
|
|
func ErrSchemaUnknownPolicyAction(name string) error {
|
|
return fmt.Errorf("unknown policy action '%s'", name)
|
|
}
|
|
func ErrSchemaUnknownClientEvent(id uuid.UUID) error {
|
|
return fmt.Errorf("unknown client event '%s'", id)
|
|
}
|
|
func ErrSchemaUnknownPgFunction(id uuid.UUID) error {
|
|
return fmt.Errorf("unknown backend function '%s'", id)
|
|
}
|
|
func ErrSchemaTriggerPgFunctionCall(id uuid.UUID) error {
|
|
return fmt.Errorf("backend function '%s' is a trigger function, it cannot be called directly", id)
|
|
}
|
|
func ErrSchemaBadFrontendExecPgFunctionCall(id uuid.UUID) error {
|
|
return fmt.Errorf("backend function '%s' may not be called from the frontend", id)
|
|
}
|