From 92479ff69fa7574d7d85d5cab055195f499dfbfe Mon Sep 17 00:00:00 2001 From: KCarretto Date: Sun, 21 Jan 2024 22:09:18 +0000 Subject: [PATCH] added basic validation for parameters --- tavern/internal/c2/c2test/ent.go | 4 +- tavern/internal/ent/quest/quest.go | 2 + tavern/internal/ent/quest_create.go | 5 + tavern/internal/ent/quest_update.go | 10 ++ tavern/internal/ent/runtime/runtime.go | 8 ++ tavern/internal/ent/schema/quest.go | 2 + tavern/internal/ent/schema/tome.go | 4 +- tavern/internal/ent/schema/validators/json.go | 40 ++++++++ .../ent/schema/validators/json_test.go | 97 +++++++++++++++++++ tavern/internal/ent/tome.go | 2 +- tavern/internal/ent/tome/tome.go | 2 + tavern/internal/ent/tome_create.go | 5 + tavern/internal/ent/tome_update.go | 10 ++ .../graphql/generated/root_.generated.go | 4 +- tavern/internal/graphql/schema.graphql | 4 +- tavern/internal/graphql/schema/ent.graphql | 4 +- tavern/internal/www/schema.graphql | 4 +- tavern/tomes/parse.go | 35 ++++++- 18 files changed, 226 insertions(+), 16 deletions(-) create mode 100644 tavern/internal/ent/schema/validators/json.go create mode 100644 tavern/internal/ent/schema/validators/json_test.go diff --git a/tavern/internal/c2/c2test/ent.go b/tavern/internal/c2/c2test/ent.go index c541dd8db..b5a6958b9 100644 --- a/tavern/internal/c2/c2test/ent.go +++ b/tavern/internal/c2/c2test/ent.go @@ -94,14 +94,14 @@ func NewRandomAssignedTask(ctx context.Context, graph *ent.Client, beaconIdentif SetName(namegen.NewComplex()). SetEldritch(fmt.Sprintf(`print("%s")`, namegen.NewComplex())). SetDescription(string(newRandomBytes(120))). - SetParamDefs(`{"test":"string"}`). + SetParamDefs(`[{"name":"test-param","label":"Test","type":"string","placeholder":"Enter text..."}]`). AddFiles(files...). SaveX(ctx) quest := graph.Quest.Create(). SetName(namegen.NewComplex()). SetBundle(bundle). SetTome(tome). - SetParameters(fmt.Sprintf(`{"test":"%v"}`, namegen.NewComplex())). + SetParameters(fmt.Sprintf(`{"test-param":"%v"}`, namegen.NewComplex())). SaveX(ctx) return graph.Task.Create(). diff --git a/tavern/internal/ent/quest/quest.go b/tavern/internal/ent/quest/quest.go index 75adb562f..b1f41e7f1 100644 --- a/tavern/internal/ent/quest/quest.go +++ b/tavern/internal/ent/quest/quest.go @@ -103,6 +103,8 @@ var ( UpdateDefaultLastModifiedAt func() time.Time // NameValidator is a validator for the "name" field. It is called by the builders before save. NameValidator func(string) error + // ParametersValidator is a validator for the "parameters" field. It is called by the builders before save. + ParametersValidator func(string) error ) // OrderOption defines the ordering options for the Quest queries. diff --git a/tavern/internal/ent/quest_create.go b/tavern/internal/ent/quest_create.go index 43743f788..889e6e85a 100644 --- a/tavern/internal/ent/quest_create.go +++ b/tavern/internal/ent/quest_create.go @@ -199,6 +199,11 @@ func (qc *QuestCreate) check() error { return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Quest.name": %w`, err)} } } + if v, ok := qc.mutation.Parameters(); ok { + if err := quest.ParametersValidator(v); err != nil { + return &ValidationError{Name: "parameters", err: fmt.Errorf(`ent: validator failed for field "Quest.parameters": %w`, err)} + } + } if _, ok := qc.mutation.TomeID(); !ok { return &ValidationError{Name: "tome", err: errors.New(`ent: missing required edge "Quest.tome"`)} } diff --git a/tavern/internal/ent/quest_update.go b/tavern/internal/ent/quest_update.go index 146e1e8a6..ac8ec2109 100644 --- a/tavern/internal/ent/quest_update.go +++ b/tavern/internal/ent/quest_update.go @@ -215,6 +215,11 @@ func (qu *QuestUpdate) check() error { return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Quest.name": %w`, err)} } } + if v, ok := qu.mutation.Parameters(); ok { + if err := quest.ParametersValidator(v); err != nil { + return &ValidationError{Name: "parameters", err: fmt.Errorf(`ent: validator failed for field "Quest.parameters": %w`, err)} + } + } if _, ok := qu.mutation.TomeID(); qu.mutation.TomeCleared() && !ok { return errors.New(`ent: clearing a required unique edge "Quest.tome"`) } @@ -593,6 +598,11 @@ func (quo *QuestUpdateOne) check() error { return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Quest.name": %w`, err)} } } + if v, ok := quo.mutation.Parameters(); ok { + if err := quest.ParametersValidator(v); err != nil { + return &ValidationError{Name: "parameters", err: fmt.Errorf(`ent: validator failed for field "Quest.parameters": %w`, err)} + } + } if _, ok := quo.mutation.TomeID(); quo.mutation.TomeCleared() && !ok { return errors.New(`ent: clearing a required unique edge "Quest.tome"`) } diff --git a/tavern/internal/ent/runtime/runtime.go b/tavern/internal/ent/runtime/runtime.go index 57804764e..476fa0d24 100644 --- a/tavern/internal/ent/runtime/runtime.go +++ b/tavern/internal/ent/runtime/runtime.go @@ -102,6 +102,10 @@ func init() { questDescName := questFields[0].Descriptor() // quest.NameValidator is a validator for the "name" field. It is called by the builders before save. quest.NameValidator = questDescName.Validators[0].(func(string) error) + // questDescParameters is the schema descriptor for parameters field. + questDescParameters := questFields[1].Descriptor() + // quest.ParametersValidator is a validator for the "parameters" field. It is called by the builders before save. + quest.ParametersValidator = questDescParameters.Validators[0].(func(string) error) tagFields := schema.Tag{}.Fields() _ = tagFields // tagDescName is the schema descriptor for name field. @@ -152,6 +156,10 @@ func init() { tomeDescName := tomeFields[0].Descriptor() // tome.NameValidator is a validator for the "name" field. It is called by the builders before save. tome.NameValidator = tomeDescName.Validators[0].(func(string) error) + // tomeDescParamDefs is the schema descriptor for param_defs field. + tomeDescParamDefs := tomeFields[2].Descriptor() + // tome.ParamDefsValidator is a validator for the "param_defs" field. It is called by the builders before save. + tome.ParamDefsValidator = tomeDescParamDefs.Validators[0].(func(string) error) // tomeDescHash is the schema descriptor for hash field. tomeDescHash := tomeFields[3].Descriptor() // tome.HashValidator is a validator for the "hash" field. It is called by the builders before save. diff --git a/tavern/internal/ent/schema/quest.go b/tavern/internal/ent/schema/quest.go index 80a30313e..8ea748455 100644 --- a/tavern/internal/ent/schema/quest.go +++ b/tavern/internal/ent/schema/quest.go @@ -7,6 +7,7 @@ import ( "entgo.io/ent/schema" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" + "realm.pub/tavern/internal/ent/schema/validators" ) // Quest holds the schema definition for the Quest entity. @@ -24,6 +25,7 @@ func (Quest) Fields() []ent.Field { ). Comment("Name of the quest"), field.String("parameters"). + Validate(validators.NewJSONStringString()). SchemaType(map[string]string{ dialect.MySQL: "LONGTEXT", // Override MySQL, improve length maximum }). diff --git a/tavern/internal/ent/schema/tome.go b/tavern/internal/ent/schema/tome.go index 53caacae1..a5758f187 100644 --- a/tavern/internal/ent/schema/tome.go +++ b/tavern/internal/ent/schema/tome.go @@ -12,6 +12,7 @@ import ( "entgo.io/ent/schema/field" "golang.org/x/crypto/sha3" "realm.pub/tavern/internal/ent/hook" + "realm.pub/tavern/internal/ent/schema/validators" ) // Tome holds the schema definition for the Tome entity. @@ -32,11 +33,12 @@ func (Tome) Fields() []ent.Field { field.String("description"). Comment("Information about the tome"), field.String("param_defs"). + Validate(validators.NewTomeParameterDefinitions()). Optional(). SchemaType(map[string]string{ dialect.MySQL: "LONGTEXT", // Override MySQL, improve length maximum }). - Comment("JSON string describing what parameters are used with the tome"), + Comment("JSON string describing what parameters are used with the tome. Requires a list of JSON objects, one for each parameter."), field.String("hash"). MaxLen(100). Annotations( diff --git a/tavern/internal/ent/schema/validators/json.go b/tavern/internal/ent/schema/validators/json.go new file mode 100644 index 000000000..da373277c --- /dev/null +++ b/tavern/internal/ent/schema/validators/json.go @@ -0,0 +1,40 @@ +package validators + +import ( + "encoding/json" + + "realm.pub/tavern/tomes" +) + +// NewJSONStringString returns a validator that errors if the string field has a value that cannot be JSON unmarshalled to a map[string]string. +func NewJSONStringString() func(string) error { + return func(data string) error { + if data == "" { + return nil + } + var dataMap map[string]string + return json.Unmarshal([]byte(data), &dataMap) + } +} + +// NewTomeParameterDefinitions returns a validator that errors if the string field has a value that cannot be JSON unmarshalled to a []tomes.TomeParamDefinition. +func NewTomeParameterDefinitions() func(string) error { + return func(data string) error { + if data == "" { + return nil + } + var paramDefs []tomes.ParamDefinition + if err := json.Unmarshal([]byte(data), ¶mDefs); err != nil { + return err + } + + // Validate parameters + for _, paramDef := range paramDefs { + if err := paramDef.Validate(); err != nil { + return err + } + } + + return nil + } +} diff --git a/tavern/internal/ent/schema/validators/json_test.go b/tavern/internal/ent/schema/validators/json_test.go new file mode 100644 index 000000000..004a16fb4 --- /dev/null +++ b/tavern/internal/ent/schema/validators/json_test.go @@ -0,0 +1,97 @@ +package validators_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "realm.pub/tavern/internal/ent/schema/validators" +) + +func TestNewJSONStringString(t *testing.T) { + tests := []struct { + name string + data string + wantErr error + }{ + { + name: "Empty", + data: ``, + wantErr: nil, + }, + { + name: "Valid", + data: `{"data":"stuff"}`, + wantErr: nil, + }, + { + name: "Invalid", + data: `blah`, + wantErr: &json.SyntaxError{}, + }, + { + name: "Partial", + data: `{"blah":"stuff"`, + wantErr: &json.SyntaxError{}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := validators.NewJSONStringString()(tc.data) + if tc.wantErr == nil { + assert.NoError(t, err) + return + } + assert.ErrorAs(t, err, &tc.wantErr) + }) + } +} + +func TestNewTomeParameterDefinitions(t *testing.T) { + tests := []struct { + name string + data string + wantErr error + }{ + { + name: "Empty", + data: ``, + wantErr: nil, + }, + { + name: "Int32", + data: `[{"name":"an-int","type": "int32"}]`, + wantErr: nil, + }, + { + name: "Multiple", + data: `[{"name":"an-int","type":"int32"},{"name":"a-str","type": "string"}]`, + wantErr: nil, + }, + { + name: "Valid", + data: `[{"name":"stuff","type":"string"}]`, + wantErr: nil, + }, + { + name: "Invalid", + data: `blah`, + wantErr: &json.SyntaxError{}, + }, + { + name: "Partial", + data: `{"blah":"stuff"`, + wantErr: &json.SyntaxError{}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := validators.NewTomeParameterDefinitions()(tc.data) + if tc.wantErr == nil { + assert.NoError(t, err) + return + } + assert.ErrorAs(t, err, &tc.wantErr) + }) + } +} diff --git a/tavern/internal/ent/tome.go b/tavern/internal/ent/tome.go index 65e5ee622..54f362f43 100644 --- a/tavern/internal/ent/tome.go +++ b/tavern/internal/ent/tome.go @@ -25,7 +25,7 @@ type Tome struct { Name string `json:"name,omitempty"` // Information about the tome Description string `json:"description,omitempty"` - // JSON string describing what parameters are used with the tome + // JSON string describing what parameters are used with the tome. Requires a list of JSON objects, one for each parameter. ParamDefs string `json:"param_defs,omitempty"` // A SHA3 digest of the eldritch field Hash string `json:"hash,omitempty"` diff --git a/tavern/internal/ent/tome/tome.go b/tavern/internal/ent/tome/tome.go index fb87a1186..474e543d6 100644 --- a/tavern/internal/ent/tome/tome.go +++ b/tavern/internal/ent/tome/tome.go @@ -83,6 +83,8 @@ var ( UpdateDefaultLastModifiedAt func() time.Time // NameValidator is a validator for the "name" field. It is called by the builders before save. NameValidator func(string) error + // ParamDefsValidator is a validator for the "param_defs" field. It is called by the builders before save. + ParamDefsValidator func(string) error // HashValidator is a validator for the "hash" field. It is called by the builders before save. HashValidator func(string) error ) diff --git a/tavern/internal/ent/tome_create.go b/tavern/internal/ent/tome_create.go index 5742431e4..8b39f323d 100644 --- a/tavern/internal/ent/tome_create.go +++ b/tavern/internal/ent/tome_create.go @@ -177,6 +177,11 @@ func (tc *TomeCreate) check() error { if _, ok := tc.mutation.Description(); !ok { return &ValidationError{Name: "description", err: errors.New(`ent: missing required field "Tome.description"`)} } + if v, ok := tc.mutation.ParamDefs(); ok { + if err := tome.ParamDefsValidator(v); err != nil { + return &ValidationError{Name: "param_defs", err: fmt.Errorf(`ent: validator failed for field "Tome.param_defs": %w`, err)} + } + } if _, ok := tc.mutation.Hash(); !ok { return &ValidationError{Name: "hash", err: errors.New(`ent: missing required field "Tome.hash"`)} } diff --git a/tavern/internal/ent/tome_update.go b/tavern/internal/ent/tome_update.go index ebaa87d93..83469d3b3 100644 --- a/tavern/internal/ent/tome_update.go +++ b/tavern/internal/ent/tome_update.go @@ -169,6 +169,11 @@ func (tu *TomeUpdate) check() error { return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Tome.name": %w`, err)} } } + if v, ok := tu.mutation.ParamDefs(); ok { + if err := tome.ParamDefsValidator(v); err != nil { + return &ValidationError{Name: "param_defs", err: fmt.Errorf(`ent: validator failed for field "Tome.param_defs": %w`, err)} + } + } if v, ok := tu.mutation.Hash(); ok { if err := tome.HashValidator(v); err != nil { return &ValidationError{Name: "hash", err: fmt.Errorf(`ent: validator failed for field "Tome.hash": %w`, err)} @@ -428,6 +433,11 @@ func (tuo *TomeUpdateOne) check() error { return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Tome.name": %w`, err)} } } + if v, ok := tuo.mutation.ParamDefs(); ok { + if err := tome.ParamDefsValidator(v); err != nil { + return &ValidationError{Name: "param_defs", err: fmt.Errorf(`ent: validator failed for field "Tome.param_defs": %w`, err)} + } + } if v, ok := tuo.mutation.Hash(); ok { if err := tome.HashValidator(v); err != nil { return &ValidationError{Name: "hash", err: fmt.Errorf(`ent: validator failed for field "Tome.hash": %w`, err)} diff --git a/tavern/internal/graphql/generated/root_.generated.go b/tavern/internal/graphql/generated/root_.generated.go index a27fe9e9f..982852990 100644 --- a/tavern/internal/graphql/generated/root_.generated.go +++ b/tavern/internal/graphql/generated/root_.generated.go @@ -1223,7 +1223,7 @@ input CreateTomeInput { name: String! """Information about the tome""" description: String! - """JSON string describing what parameters are used with the tome""" + """JSON string describing what parameters are used with the tome. Requires a list of JSON objects, one for each parameter.""" paramDefs: String """Eldritch script that will be executed when the tome is run""" eldritch: String! @@ -1854,7 +1854,7 @@ type Tome implements Node { name: String! """Information about the tome""" description: String! - """JSON string describing what parameters are used with the tome""" + """JSON string describing what parameters are used with the tome. Requires a list of JSON objects, one for each parameter.""" paramDefs: String """Eldritch script that will be executed when the tome is run""" eldritch: String! diff --git a/tavern/internal/graphql/schema.graphql b/tavern/internal/graphql/schema.graphql index 26f3e5769..6fb72a042 100644 --- a/tavern/internal/graphql/schema.graphql +++ b/tavern/internal/graphql/schema.graphql @@ -173,7 +173,7 @@ input CreateTomeInput { name: String! """Information about the tome""" description: String! - """JSON string describing what parameters are used with the tome""" + """JSON string describing what parameters are used with the tome. Requires a list of JSON objects, one for each parameter.""" paramDefs: String """Eldritch script that will be executed when the tome is run""" eldritch: String! @@ -804,7 +804,7 @@ type Tome implements Node { name: String! """Information about the tome""" description: String! - """JSON string describing what parameters are used with the tome""" + """JSON string describing what parameters are used with the tome. Requires a list of JSON objects, one for each parameter.""" paramDefs: String """Eldritch script that will be executed when the tome is run""" eldritch: String! diff --git a/tavern/internal/graphql/schema/ent.graphql b/tavern/internal/graphql/schema/ent.graphql index e2e8e3fe5..0037981e3 100644 --- a/tavern/internal/graphql/schema/ent.graphql +++ b/tavern/internal/graphql/schema/ent.graphql @@ -168,7 +168,7 @@ input CreateTomeInput { name: String! """Information about the tome""" description: String! - """JSON string describing what parameters are used with the tome""" + """JSON string describing what parameters are used with the tome. Requires a list of JSON objects, one for each parameter.""" paramDefs: String """Eldritch script that will be executed when the tome is run""" eldritch: String! @@ -799,7 +799,7 @@ type Tome implements Node { name: String! """Information about the tome""" description: String! - """JSON string describing what parameters are used with the tome""" + """JSON string describing what parameters are used with the tome. Requires a list of JSON objects, one for each parameter.""" paramDefs: String """Eldritch script that will be executed when the tome is run""" eldritch: String! diff --git a/tavern/internal/www/schema.graphql b/tavern/internal/www/schema.graphql index 26f3e5769..6fb72a042 100644 --- a/tavern/internal/www/schema.graphql +++ b/tavern/internal/www/schema.graphql @@ -173,7 +173,7 @@ input CreateTomeInput { name: String! """Information about the tome""" description: String! - """JSON string describing what parameters are used with the tome""" + """JSON string describing what parameters are used with the tome. Requires a list of JSON objects, one for each parameter.""" paramDefs: String """Eldritch script that will be executed when the tome is run""" eldritch: String! @@ -804,7 +804,7 @@ type Tome implements Node { name: String! """Information about the tome""" description: String! - """JSON string describing what parameters are used with the tome""" + """JSON string describing what parameters are used with the tome. Requires a list of JSON objects, one for each parameter.""" paramDefs: String """Eldritch script that will be executed when the tome is run""" eldritch: String! diff --git a/tavern/tomes/parse.go b/tavern/tomes/parse.go index f84a5a22b..27729ef48 100644 --- a/tavern/tomes/parse.go +++ b/tavern/tomes/parse.go @@ -12,17 +12,37 @@ import ( "realm.pub/tavern/internal/ent/tome" ) -type tomeParamDef struct { +// ErrParamNameInvalid occurs when a parameter definition specifies an invalid parameter name. +// ErrParamTypeUnsupported occurs when a parameter definition specifies an unsupported parameter type. +var ( + ErrParamNameInvalid = fmt.Errorf("invalid name in parameter definition") + ErrParamTypeUnsupported = fmt.Errorf("unsupported type in parameter definition") +) + +// ParamDefinition provides structured information for a tome to define a parameter. +type ParamDefinition struct { Name string `yaml:"name" json:"name"` Label string `yaml:"label" json:"label"` Type string `yaml:"type" json:"type"` Placeholder string `yaml:"placeholder" json:"placeholder"` } -type tomeMetadata struct { +// Validate the parameter definition, returning an error if an invalid definition has been defined. +func (paramDef ParamDefinition) Validate() error { + if paramDef.Name == "" { + return fmt.Errorf("%w: %q", ErrParamNameInvalid, paramDef.Name) + } + // TODO: Support Types + // if paramDef.Type != "string" { + // return fmt.Errorf("%w: %v is of type %v", ErrParamTypeUnsupported, paramDef.Name, paramDef.Type) + // } + return nil +} + +type metadataDefinition struct { Name string Description string - ParamDefs []tomeParamDef + ParamDefs []ParamDefinition } // UploadTomes traverses the provided filesystem and creates tomes using the provided graph. @@ -55,7 +75,7 @@ func UploadTomes(ctx context.Context, graph *ent.Client, fileSystem fs.ReadDirFS continue } - var metadata tomeMetadata + var metadata metadataDefinition var eldritch string var tomeFiles []*ent.File if err := fs.WalkDir(fileSystem, entry.Name(), func(path string, d fs.DirEntry, err error) error { @@ -81,6 +101,13 @@ func UploadTomes(ctx context.Context, graph *ent.Client, fileSystem fs.ReadDirFS return nil } + // Validate Params + for _, paramDef := range metadata.ParamDefs { + if err := paramDef.Validate(); err != nil { + return rollback(tx, fmt.Errorf("failed to validate tome parameter definition: %w", err)) + } + } + // Parse main.eldritch if filepath.Base(path) == "main.eldritch" { eldritch = string(content)