From 8dedbc94e213b623d1e35f7ffbcf879473ef7770 Mon Sep 17 00:00:00 2001 From: Marcus Goldschmidt Date: Mon, 8 Dec 2025 16:03:55 -0400 Subject: [PATCH 1/7] add validation for queries --- pkg/bsql/config.go | 22 ++++--- pkg/bsql/query.go | 14 +++++ pkg/bsql/sql_syncer.go | 69 ++++++++++++++++++++++ pkg/bsql/validate.go | 116 +++++++++++++++++++++++++++++++++++++ pkg/bsql/validate_test.go | 51 ++++++++++++++++ pkg/connector/connector.go | 18 +++++- 6 files changed, 280 insertions(+), 10 deletions(-) create mode 100644 pkg/bsql/validate.go create mode 100644 pkg/bsql/validate_test.go diff --git a/pkg/bsql/config.go b/pkg/bsql/config.go index b580b77d..8c8c9cad 100644 --- a/pkg/bsql/config.go +++ b/pkg/bsql/config.go @@ -13,6 +13,10 @@ import ( v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" ) +type staticValidator interface { + StaticValidate(ctx context.Context, s *SQLSyncer) error +} + // Config represents the overall connector configuration. type Config struct { // AppName is the application name that identifies the connector. @@ -42,27 +46,27 @@ type DatabaseConfig struct { // DSN is the Database Source Name connection string (optional if using structured fields). // Supports environment variable expansion via ${VAR_NAME} syntax. // Example: "postgres://${DB_HOST}:${DB_PORT}/${DB_DATABASE}?sslmode=disable" - DSN string `yaml:"dsn" json:"dsn"` + DSN string `yaml:"dsn" json:"dsn" validate:"required"` // Structured connection fields (optional, override DSN components when set) // Scheme is the database type (e.g., "postgres", "mysql", "sqlserver", "oracle", "hdb") - Scheme string `yaml:"scheme" json:"scheme"` + Scheme string `yaml:"scheme" json:"scheme" validate:"required"` // Host is the database server hostname or IP address (may include port for some databases) - Host string `yaml:"host" json:"host"` + Host string `yaml:"host" json:"host" validate:"required"` // Port is the database server port number - Port string `yaml:"port" json:"port"` + Port string `yaml:"port" json:"port" validate:"required"` // Database is the name of the database to connect to - Database string `yaml:"database" json:"database"` + Database string `yaml:"database" json:"database" validate:"required"` // User is the database username used for authentication - User string `yaml:"user" json:"user"` + User string `yaml:"user" json:"user" validate:"required"` // Password is the database password used for authentication - Password string `yaml:"password" json:"password"` + Password string `yaml:"password" json:"password" validate:"required"` // Params contains additional connection parameters (e.g., {"sslmode": "disable", "timeout": "30s"}) Params map[string]string `yaml:"params" json:"params"` @@ -105,7 +109,7 @@ type ListQuery struct { Vars map[string]string `yaml:"vars,omitempty" json:"vars,omitempty"` // Query is the SQL statement used to fetch a list of resources. - Query string `yaml:"query" json:"query"` + Query string `yaml:"query" json:"query" validate:"required"` // Pagination defines the pagination strategy and settings for the list query. Pagination *Pagination `yaml:"pagination" json:"pagination"` @@ -241,7 +245,7 @@ type EntitlementsQuery struct { Vars map[string]string `yaml:"vars,omitempty" json:"vars,omitempty"` // Query is the SQL statement used to fetch dynamic entitlements. - Query string `yaml:"query" json:"query"` + Query string `yaml:"query" json:"query" validate:"required"` // Pagination defines how pagination should be handled for the entitlements query. Pagination *Pagination `yaml:"pagination" json:"pagination"` diff --git a/pkg/bsql/query.go b/pkg/bsql/query.go index 4a6358b4..b2da3689 100644 --- a/pkg/bsql/query.go +++ b/pkg/bsql/query.go @@ -95,6 +95,20 @@ func parseToken(token string) (*queryTokenOpts, error) { return opts, nil } +func (s *SQLSyncer) queryVars(query string) ([]string, error) { + result := make([]string, 0) + + for _, token := range queryOptRegex.FindAllString(query, -1) { + opts, err := parseToken(token) + if err != nil { + return nil, err + } + result = append(result, opts.Key) + } + + return result, nil +} + func (s *SQLSyncer) parseQueryOpts(pCtx *paginationContext, query string, vars map[string]any) (string, []interface{}, bool, error) { if vars == nil { vars = make(map[string]any) diff --git a/pkg/bsql/sql_syncer.go b/pkg/bsql/sql_syncer.go index e907243e..fcc99487 100644 --- a/pkg/bsql/sql_syncer.go +++ b/pkg/bsql/sql_syncer.go @@ -3,6 +3,7 @@ package bsql import ( "context" "database/sql" + "fmt" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/connectorbuilder" @@ -68,3 +69,71 @@ func NewActionSyncer(ctx context.Context, db *sql.DB, dbEngine database.DbEngine fullConfig: fullConfig, }, nil } + +func (s *SQLSyncer) validateInternal(ctx context.Context, anyV any) error { + if anyV == nil { + return nil + } + + if v, ok := anyV.(staticValidator); ok { + err := v.StaticValidate(ctx, s) + if err != nil { + return err + } + } + + return nil +} + +func (s *SQLSyncer) validateFormatErr(field string, err error) error { + rsTypeId := s.resourceType.Id + + return fmt.Errorf("validation error for resource type %q, field %q: %w", rsTypeId, field, err) +} + +func (s *SQLSyncer) Validate(ctx context.Context) error { + if s.fullConfig.Actions != nil { + for key, action := range s.fullConfig.Actions { + err := s.validateInternal(ctx, &action) + if err != nil { + return s.validateFormatErr(fmt.Sprintf("Action[%s]", key), err) + } + } + } + + if err := s.validateInternal(ctx, s.config.List); err != nil { + return s.validateFormatErr("list", err) + } + + if s.config.Entitlements != nil { + if err := s.validateInternal(ctx, s.config.Entitlements); err != nil { + return s.validateFormatErr("entitlements", err) + } + } + + if s.config.StaticEntitlements != nil { + if err := s.validateInternal(ctx, s.config.StaticEntitlements); err != nil { + return s.validateFormatErr("static_entitlements", err) + } + } + + if s.config.Grants != nil { + if err := s.validateInternal(ctx, s.config.Grants); err != nil { + return s.validateFormatErr("grants", err) + } + } + + if s.config.AccountProvisioning != nil { + if err := s.validateInternal(ctx, s.config.AccountProvisioning); err != nil { + return s.validateFormatErr("account_provisioning", err) + } + } + + if s.config.CredentialRotation != nil { + if err := s.validateInternal(ctx, s.config.CredentialRotation); err != nil { + return s.validateFormatErr("credential_rotation", err) + } + } + + return nil +} diff --git a/pkg/bsql/validate.go b/pkg/bsql/validate.go new file mode 100644 index 00000000..ed5c214f --- /dev/null +++ b/pkg/bsql/validate.go @@ -0,0 +1,116 @@ +package bsql + +import ( + "context" + "fmt" +) + +func validateVarsInQuery(s *SQLSyncer, query string, vars map[string]string) error { + if query == "" { + return fmt.Errorf("list query is required") + } + + usedVars, err := s.queryVars(query) + if err != nil { + return fmt.Errorf("failed to parse list query for variables: %w", err) + } + + if vars == nil { + vars = make(map[string]string) + } + + for _, v := range usedVars { + if _, ok := vars[v]; !ok { + if v == "limit" || v == "offset" || v == "cursor" { + continue + } + return fmt.Errorf("list query uses variable '%s' which is not defined in vars", v) + } + } + + return nil +} + +func (l *ListQuery) StaticValidate(ctx context.Context, s *SQLSyncer) error { + return validateVarsInQuery(s, l.Query, l.Vars) +} + +func (l *EntitlementsQuery) StaticValidate(ctx context.Context, s *SQLSyncer) error { + return validateVarsInQuery(s, l.Query, l.Vars) +} + +func (l *EntitlementMapping) StaticValidate(ctx context.Context, s *SQLSyncer) error { + if l.Provisioning == nil { + return nil + } + + if l.Provisioning.Grant != nil { + for _, query := range l.Provisioning.Grant.Queries { + err := validateVarsInQuery(s, query, l.Provisioning.Vars) + if err != nil { + return err + } + } + } + + if l.Provisioning.Revoke != nil { + for _, query := range l.Provisioning.Revoke.Queries { + err := validateVarsInQuery(s, query, l.Provisioning.Vars) + if err != nil { + return err + } + } + } + + return nil +} + +func (l *GrantsQuery) StaticValidate(ctx context.Context, s *SQLSyncer) error { + return validateVarsInQuery(s, l.Query, l.Vars) +} + +func (l *AccountProvisioning) StaticValidate(ctx context.Context, s *SQLSyncer) error { + if l.Create != nil { + for _, query := range l.Create.Queries { + err := validateVarsInQuery(s, query, l.Create.Vars) + if err != nil { + return err + } + } + } + + if l.Validate != nil { + err := validateVarsInQuery(s, l.Validate.Query, l.Validate.Vars) + if err != nil { + return err + } + } + + return nil +} + +func (l *CredentialRotation) StaticValidate(ctx context.Context, s *SQLSyncer) error { + if l.Update != nil { + for _, query := range l.Update.Queries { + err := validateVarsInQuery(s, query, l.Update.Vars) + if err != nil { + return err + } + } + } + + return nil +} + +func (l *ActionConfig) StaticValidate(ctx context.Context, s *SQLSyncer) error { + availableVars := make(map[string]string) + for k, v := range l.Vars { + availableVars[k] = v + } + + for k, config := range l.Arguments { + availableVars[k] = config.Name + } + + return validateVarsInQuery(s, l.Query, availableVars) +} diff --git a/pkg/bsql/validate_test.go b/pkg/bsql/validate_test.go new file mode 100644 index 00000000..34ca9b5a --- /dev/null +++ b/pkg/bsql/validate_test.go @@ -0,0 +1,51 @@ +package bsql + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidate(t *testing.T) { + tcases := []struct { + name string + validator staticValidator + expectErr bool + }{ + { + name: "valid list query", + validator: &ListQuery{ + Query: "SELECT * FROM users WHERE id = ? LIMIT ? OFFSET ?", + Vars: map[string]string{ + "userid": "string", + }, + }, + expectErr: false, + }, + { + name: "invalid list query", + validator: &ListQuery{ + Query: "SELECT * FROM users WHERE id = ? LIMIT ? OFFSET ?", + Vars: map[string]string{ + "userid": "string", + }, + }, + expectErr: true, + }, + } + + for _, tc := range tcases { + t.Run(tc.name, func(t *testing.T) { + ctx := t.Context() + + syncer := &SQLSyncer{} + + err := tc.validator.StaticValidate(ctx, syncer) + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index f078a09a..4a9b6131 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -76,7 +76,23 @@ func (c *Connector) Metadata(ctx context.Context) (*v2.ConnectorMetadata, error) // Validate is called to ensure that the connector is properly configured. It should exercise any API credentials // to be sure that they are valid. func (c *Connector) Validate(ctx context.Context) (annotations.Annotations, error) { - err := c.db.PingContext(ctx) + syncers, err := c.config.GetSQLSyncers(ctx, c.db, c.dbEngine, c.celEnv) + if err != nil { + return nil, err + } + + for _, syncer := range syncers { + if v, ok := syncer.(interface { + Validate(ctx context.Context) error + }); ok { + err := v.Validate(ctx) + if err != nil { + return nil, err + } + } + } + + err = c.db.PingContext(ctx) if err != nil { return nil, err } From b5d586002913b217104804c75ed7a74f0c84b05f Mon Sep 17 00:00:00 2001 From: Marcus Goldschmidt Date: Mon, 8 Dec 2025 16:20:39 -0400 Subject: [PATCH 2/7] add validation for credentials --- pkg/bsql/validate.go | 46 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/pkg/bsql/validate.go b/pkg/bsql/validate.go index ed5c214f..29f89eed 100644 --- a/pkg/bsql/validate.go +++ b/pkg/bsql/validate.go @@ -2,6 +2,7 @@ package bsql import ( "context" + "errors" "fmt" ) @@ -70,22 +71,51 @@ func (l *GrantsQuery) StaticValidate(ctx context.Context, s *SQLSyncer) error { } func (l *AccountProvisioning) StaticValidate(ctx context.Context, s *SQLSyncer) error { - if l.Create != nil { - for _, query := range l.Create.Queries { - err := validateVarsInQuery(s, query, l.Create.Vars) - if err != nil { - return err - } + + if l.Credentials == nil { + return errors.New("no credentials defined") + } + + if l.Credentials.EncryptedPassword == nil && + l.Credentials.RandomPassword == nil && + l.Credentials.NoPassword == nil { + return errors.New("no credential method defined") + } + + if l.Credentials.RandomPassword != nil { + if l.Credentials.RandomPassword.MaxLength <= 0 { + return errors.New("random password max_length must be greater than zero") + } + + if l.Credentials.RandomPassword.MinLength <= 0 { + return errors.New("random password min_length must be greater than zero") + } + + if l.Credentials.RandomPassword.MinLength > l.Credentials.RandomPassword.MaxLength { + return errors.New("random password min_length cannot be greater than max_length") } } - if l.Validate != nil { - err := validateVarsInQuery(s, l.Validate.Query, l.Validate.Vars) + if l.Create == nil { + return errors.New("no create functions defined") + } + + for _, query := range l.Create.Queries { + err := validateVarsInQuery(s, query, l.Create.Vars) if err != nil { return err } } + if l.Validate == nil { + return errors.New("no validate functions defined") + } + + err := validateVarsInQuery(s, l.Validate.Query, l.Validate.Vars) + if err != nil { + return err + } + return nil } From 3b04ca45c9691e4b442d071dadd73ecdf8d547eb Mon Sep 17 00:00:00 2001 From: Marcus Goldschmidt Date: Mon, 8 Dec 2025 16:40:53 -0400 Subject: [PATCH 3/7] sanitize auto injection --- pkg/bsql/query.go | 10 ++++++++-- pkg/bsql/query_test.go | 17 +++++++++++++++++ pkg/bsql/validate.go | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/pkg/bsql/query.go b/pkg/bsql/query.go index b2da3689..66ccf7f0 100644 --- a/pkg/bsql/query.go +++ b/pkg/bsql/query.go @@ -30,6 +30,12 @@ type executor interface { ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) } +var identSanitizer = regexp.MustCompile(`[^a-zA-Z0-9_]+`) + +func SanitizeIdentifier(s string) string { + return identSanitizer.ReplaceAllString(s, "") +} + type paginationContext struct { Strategy string Limit int64 @@ -149,7 +155,7 @@ func (s *SQLSyncer) parseQueryOpts(pCtx *paginationContext, query string, vars m // If the value is unquoted, directly insert the value as a string if opts.Unquoted { - return fmt.Sprintf("%v", val) + return SanitizeIdentifier(fmt.Sprintf("%v", val)) } qArgs = append(qArgs, val) @@ -294,7 +300,7 @@ func (s *SQLSyncer) prepareProvisioningQuery(query string, vars map[string]any) } if opts.Unquoted { - return fmt.Sprintf("%v", v) + return SanitizeIdentifier(fmt.Sprintf("%v", v)) } qArgs = append(qArgs, v) diff --git a/pkg/bsql/query_test.go b/pkg/bsql/query_test.go index 4fcae4a7..6e2a3bd2 100644 --- a/pkg/bsql/query_test.go +++ b/pkg/bsql/query_test.go @@ -358,6 +358,23 @@ func Test_parseQueryOpts(t *testing.T) { false, false, }, + { + "Test sql injection attempt with unquoted table name var substitution", + database.MySQL, + args{ + t.Context(), + "SELECT * FROM ? WHERE test = ?", + nil, + map[string]any{ + "table_name": `example_table; DROP TABLE users; --`, + "foo": "test example", + }, + }, + "SELECT * FROM example_tableDROPTABLEusers WHERE test = ?", + []interface{}{"test example"}, + false, + false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/bsql/validate.go b/pkg/bsql/validate.go index 29f89eed..2275faa9 100644 --- a/pkg/bsql/validate.go +++ b/pkg/bsql/validate.go @@ -22,7 +22,7 @@ func validateVarsInQuery(s *SQLSyncer, query string, vars map[string]string) err for _, v := range usedVars { if _, ok := vars[v]; !ok { - if v == "limit" || v == "offset" || v == "cursor" { + if v == limitKey || v == offsetKey || v == cursorKey { continue } return fmt.Errorf("list query uses variable '%s' which is not defined in vars", v) From 6597e7ff7482835a8a0b29fad505ca4bb9d900fa Mon Sep 17 00:00:00 2001 From: Marcus Goldschmidt Date: Mon, 8 Dec 2025 17:21:09 -0400 Subject: [PATCH 4/7] fix linter --- pkg/bsql/validate.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/bsql/validate.go b/pkg/bsql/validate.go index 2275faa9..04fd902e 100644 --- a/pkg/bsql/validate.go +++ b/pkg/bsql/validate.go @@ -8,12 +8,12 @@ import ( func validateVarsInQuery(s *SQLSyncer, query string, vars map[string]string) error { if query == "" { - return fmt.Errorf("list query is required") + return fmt.Errorf("query is required") } usedVars, err := s.queryVars(query) if err != nil { - return fmt.Errorf("failed to parse list query for variables: %w", err) + return fmt.Errorf("failed to parse query for vars: %w", err) } if vars == nil { @@ -25,7 +25,7 @@ func validateVarsInQuery(s *SQLSyncer, query string, vars map[string]string) err if v == limitKey || v == offsetKey || v == cursorKey { continue } - return fmt.Errorf("list query uses variable '%s' which is not defined in vars", v) + return fmt.Errorf("query uses variable '%s' which is not defined in vars", v) } } @@ -71,7 +71,6 @@ func (l *GrantsQuery) StaticValidate(ctx context.Context, s *SQLSyncer) error { } func (l *AccountProvisioning) StaticValidate(ctx context.Context, s *SQLSyncer) error { - if l.Credentials == nil { return errors.New("no credentials defined") } @@ -139,7 +138,7 @@ func (l *ActionConfig) StaticValidate(ctx context.Context, s *SQLSyncer) error { } for k, config := range l.Arguments { - availableVars[k] = config.Name + availableVars[k] = config.Type } return validateVarsInQuery(s, l.Query, availableVars) From 282b6f74cd735e6645e11fe686dd517054945b89 Mon Sep 17 00:00:00 2001 From: Marcus Goldschmidt Date: Mon, 8 Dec 2025 17:22:27 -0400 Subject: [PATCH 5/7] fix postgres test --- examples/postgres-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/postgres-test.yml b/examples/postgres-test.yml index d9cd236f..1ea60bfc 100644 --- a/examples/postgres-test.yml +++ b/examples/postgres-test.yml @@ -187,7 +187,7 @@ resource_types: queries: # Update the user with generated password (PostgreSQL style) - | - UPDATE users SET password_hash = crypt(?, gen_salt('bf')) WHERE username = ? + UPDATE users SET password_hash = crypt(?, gen_salt('bf')) WHERE username = ? # Configuration for "role" resources role: From dbc7a21e69cb32f75eed767601f61b6da8b4d0b6 Mon Sep 17 00:00:00 2001 From: Marcus Goldschmidt Date: Tue, 16 Dec 2025 19:19:29 -0400 Subject: [PATCH 6/7] remove required and fix validation for entitlements --- pkg/bsql/config.go | 20 ++++++++++---------- pkg/bsql/sql_syncer.go | 28 +++++++++++++++++----------- pkg/bsql/validate.go | 38 +++++++++++++++++++++++++++++++------- pkg/bsql/validate_test.go | 2 +- 4 files changed, 59 insertions(+), 29 deletions(-) diff --git a/pkg/bsql/config.go b/pkg/bsql/config.go index 8c8c9cad..b173f5ac 100644 --- a/pkg/bsql/config.go +++ b/pkg/bsql/config.go @@ -14,7 +14,7 @@ import ( ) type staticValidator interface { - StaticValidate(ctx context.Context, s *SQLSyncer) error + staticValidate(ctx context.Context, s *SQLSyncer) error } // Config represents the overall connector configuration. @@ -46,27 +46,27 @@ type DatabaseConfig struct { // DSN is the Database Source Name connection string (optional if using structured fields). // Supports environment variable expansion via ${VAR_NAME} syntax. // Example: "postgres://${DB_HOST}:${DB_PORT}/${DB_DATABASE}?sslmode=disable" - DSN string `yaml:"dsn" json:"dsn" validate:"required"` + DSN string `yaml:"dsn" json:"dsn"` // Structured connection fields (optional, override DSN components when set) // Scheme is the database type (e.g., "postgres", "mysql", "sqlserver", "oracle", "hdb") - Scheme string `yaml:"scheme" json:"scheme" validate:"required"` + Scheme string `yaml:"scheme" json:"scheme"` // Host is the database server hostname or IP address (may include port for some databases) - Host string `yaml:"host" json:"host" validate:"required"` + Host string `yaml:"host" json:"host"` // Port is the database server port number - Port string `yaml:"port" json:"port" validate:"required"` + Port string `yaml:"port" json:"port"` // Database is the name of the database to connect to - Database string `yaml:"database" json:"database" validate:"required"` + Database string `yaml:"database" json:"database"` // User is the database username used for authentication - User string `yaml:"user" json:"user" validate:"required"` + User string `yaml:"user" json:"user"` // Password is the database password used for authentication - Password string `yaml:"password" json:"password" validate:"required"` + Password string `yaml:"password" json:"password"` // Params contains additional connection parameters (e.g., {"sslmode": "disable", "timeout": "30s"}) Params map[string]string `yaml:"params" json:"params"` @@ -109,7 +109,7 @@ type ListQuery struct { Vars map[string]string `yaml:"vars,omitempty" json:"vars,omitempty"` // Query is the SQL statement used to fetch a list of resources. - Query string `yaml:"query" json:"query" validate:"required"` + Query string `yaml:"query" json:"query"` // Pagination defines the pagination strategy and settings for the list query. Pagination *Pagination `yaml:"pagination" json:"pagination"` @@ -245,7 +245,7 @@ type EntitlementsQuery struct { Vars map[string]string `yaml:"vars,omitempty" json:"vars,omitempty"` // Query is the SQL statement used to fetch dynamic entitlements. - Query string `yaml:"query" json:"query" validate:"required"` + Query string `yaml:"query" json:"query"` // Pagination defines how pagination should be handled for the entitlements query. Pagination *Pagination `yaml:"pagination" json:"pagination"` diff --git a/pkg/bsql/sql_syncer.go b/pkg/bsql/sql_syncer.go index fcc99487..3669b09f 100644 --- a/pkg/bsql/sql_syncer.go +++ b/pkg/bsql/sql_syncer.go @@ -70,22 +70,24 @@ func NewActionSyncer(ctx context.Context, db *sql.DB, dbEngine database.DbEngine }, nil } -func (s *SQLSyncer) validateInternal(ctx context.Context, anyV any) error { - if anyV == nil { +func (s *SQLSyncer) validateInternal(ctx context.Context, validator staticValidator) error { + if validator == nil { return nil } - if v, ok := anyV.(staticValidator); ok { - err := v.StaticValidate(ctx, s) - if err != nil { - return err - } + err := validator.staticValidate(ctx, s) + if err != nil { + return err } return nil } func (s *SQLSyncer) validateFormatErr(field string, err error) error { + if s.resourceType != nil { + return fmt.Errorf("validation error for resource type %q, field %q: %w", s.resourceType.Id, field, err) + } + rsTypeId := s.resourceType.Id return fmt.Errorf("validation error for resource type %q, field %q: %w", rsTypeId, field, err) @@ -112,14 +114,18 @@ func (s *SQLSyncer) Validate(ctx context.Context) error { } if s.config.StaticEntitlements != nil { - if err := s.validateInternal(ctx, s.config.StaticEntitlements); err != nil { - return s.validateFormatErr("static_entitlements", err) + for _, entitlement := range s.config.StaticEntitlements { + if err := s.validateInternal(ctx, entitlement); err != nil { + return s.validateFormatErr("static_entitlements", err) + } } } if s.config.Grants != nil { - if err := s.validateInternal(ctx, s.config.Grants); err != nil { - return s.validateFormatErr("grants", err) + for _, grant := range s.config.Grants { + if err := s.validateInternal(ctx, grant); err != nil { + return s.validateFormatErr("grants", err) + } } } diff --git a/pkg/bsql/validate.go b/pkg/bsql/validate.go index 04fd902e..42572181 100644 --- a/pkg/bsql/validate.go +++ b/pkg/bsql/validate.go @@ -32,15 +32,39 @@ func validateVarsInQuery(s *SQLSyncer, query string, vars map[string]string) err return nil } -func (l *ListQuery) StaticValidate(ctx context.Context, s *SQLSyncer) error { +func (l *ListQuery) staticValidate(ctx context.Context, s *SQLSyncer) error { return validateVarsInQuery(s, l.Query, l.Vars) } -func (l *EntitlementsQuery) StaticValidate(ctx context.Context, s *SQLSyncer) error { +func (l *EntitlementsQuery) staticValidate(ctx context.Context, s *SQLSyncer) error { + for _, mapping := range l.Map { + if mapping.Provisioning == nil { + continue + } + + if mapping.Provisioning.Grant != nil { + for _, query := range mapping.Provisioning.Grant.Queries { + err := validateVarsInQuery(s, query, mapping.Provisioning.Vars) + if err != nil { + return err + } + } + } + + if mapping.Provisioning.Revoke != nil { + for _, query := range mapping.Provisioning.Revoke.Queries { + err := validateVarsInQuery(s, query, mapping.Provisioning.Vars) + if err != nil { + return err + } + } + } + } + return validateVarsInQuery(s, l.Query, l.Vars) } -func (l *EntitlementMapping) StaticValidate(ctx context.Context, s *SQLSyncer) error { +func (l *EntitlementMapping) staticValidate(ctx context.Context, s *SQLSyncer) error { if l.Provisioning == nil { return nil } @@ -66,11 +90,11 @@ func (l *EntitlementMapping) StaticValidate(ctx context.Context, s *SQLSyncer) e return nil } -func (l *GrantsQuery) StaticValidate(ctx context.Context, s *SQLSyncer) error { +func (l *GrantsQuery) staticValidate(ctx context.Context, s *SQLSyncer) error { return validateVarsInQuery(s, l.Query, l.Vars) } -func (l *AccountProvisioning) StaticValidate(ctx context.Context, s *SQLSyncer) error { +func (l *AccountProvisioning) staticValidate(ctx context.Context, s *SQLSyncer) error { if l.Credentials == nil { return errors.New("no credentials defined") } @@ -118,7 +142,7 @@ func (l *AccountProvisioning) StaticValidate(ctx context.Context, s *SQLSyncer) return nil } -func (l *CredentialRotation) StaticValidate(ctx context.Context, s *SQLSyncer) error { +func (l *CredentialRotation) staticValidate(ctx context.Context, s *SQLSyncer) error { if l.Update != nil { for _, query := range l.Update.Queries { err := validateVarsInQuery(s, query, l.Update.Vars) @@ -131,7 +155,7 @@ func (l *CredentialRotation) StaticValidate(ctx context.Context, s *SQLSyncer) e return nil } -func (l *ActionConfig) StaticValidate(ctx context.Context, s *SQLSyncer) error { +func (l *ActionConfig) staticValidate(ctx context.Context, s *SQLSyncer) error { availableVars := make(map[string]string) for k, v := range l.Vars { availableVars[k] = v diff --git a/pkg/bsql/validate_test.go b/pkg/bsql/validate_test.go index 34ca9b5a..7db3cf40 100644 --- a/pkg/bsql/validate_test.go +++ b/pkg/bsql/validate_test.go @@ -40,7 +40,7 @@ func TestValidate(t *testing.T) { syncer := &SQLSyncer{} - err := tc.validator.StaticValidate(ctx, syncer) + err := tc.validator.staticValidate(ctx, syncer) if tc.expectErr { require.Error(t, err) } else { From f5aea0f30548f980ddd5ff2d8c5afa16aeae9f95 Mon Sep 17 00:00:00 2001 From: Marcus Goldschmidt Date: Tue, 6 Jan 2026 11:51:04 -0400 Subject: [PATCH 7/7] fix validation format err --- pkg/bsql/sql_syncer.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pkg/bsql/sql_syncer.go b/pkg/bsql/sql_syncer.go index 3669b09f..dae8011f 100644 --- a/pkg/bsql/sql_syncer.go +++ b/pkg/bsql/sql_syncer.go @@ -84,13 +84,11 @@ func (s *SQLSyncer) validateInternal(ctx context.Context, validator staticValida } func (s *SQLSyncer) validateFormatErr(field string, err error) error { - if s.resourceType != nil { - return fmt.Errorf("validation error for resource type %q, field %q: %w", s.resourceType.Id, field, err) + if s.resourceType == nil { + return fmt.Errorf("validation error for action config, field %q: %w", field, err) } - rsTypeId := s.resourceType.Id - - return fmt.Errorf("validation error for resource type %q, field %q: %w", rsTypeId, field, err) + return fmt.Errorf("validation error for resource type %q, field %q: %w", s.resourceType.Id, field, err) } func (s *SQLSyncer) Validate(ctx context.Context) error {