diff --git a/internal/pkg/object/command/snowflake/snowflake.go b/internal/pkg/object/command/snowflake/snowflake.go index cf850db..9fb4be3 100644 --- a/internal/pkg/object/command/snowflake/snowflake.go +++ b/internal/pkg/object/command/snowflake/snowflake.go @@ -25,10 +25,14 @@ const ( var ( ErrFailedToDecodePEMBlock = fmt.Errorf(`failed to decode PEM block`) - ErrInvalidKeyType = fmt.Errorf(`invalida key type`) + ErrInvalidKeyType = fmt.Errorf(`invalid key type`) ) -type commandContext struct{} +// commandContext represents command-level configuration from the YAML config file. +// Role is defined here for security - users cannot override +type commandContext struct { + Role string `yaml:"role,omitempty" json:"role,omitempty"` +} type jobContext struct { Query string `yaml:"query,omitempty" json:"query,omitempty"` @@ -66,8 +70,16 @@ func parsePrivateKey(privateKeyBytes []byte) (*rsa.PrivateKey, error) { } -func New(_ *heimdallContext.Context) (plugin.Handler, error) { +func New(cmdCtx *heimdallContext.Context) (plugin.Handler, error) { s := &commandContext{} + + // Parse command context from YAML config (contains role configuration) + if cmdCtx != nil { + if err := cmdCtx.Unmarshal(s); err != nil { + return nil, err + } + } + return s.handler, nil } @@ -98,11 +110,13 @@ func (s *commandContext) handler(ctx context.Context, r *plugin.Runtime, j *job. return err } + // s.Role from command context; empty string = Snowflake uses user's default role dsn, err := sf.DSN(&sf.Config{ Account: clusterContext.Account, User: clusterContext.User, Database: clusterContext.Database, Warehouse: clusterContext.Warehouse, + Role: s.Role, Authenticator: sf.AuthTypeJwt, PrivateKey: privateKey, Application: r.UserAgent, diff --git a/plugins/snowflake/README.md b/plugins/snowflake/README.md index 0ccf9c6..b2036c7 100644 --- a/plugins/snowflake/README.md +++ b/plugins/snowflake/README.md @@ -27,6 +27,8 @@ You must define both a **command** and a **cluster**. The command represents the plugin: snowflake version: 0.0.1 description: Query user metrics from Snowflake + context: + role: DATA_ENGINEER tags: - type:snowflake cluster_tags: @@ -35,6 +37,9 @@ You must define both a **command** and a **cluster**. The command represents the ### 🔸 Cluster Configuration +The cluster configuration represents **identity** (who/where), not **permissions** (what they can do). +Role is intentionally excluded to prevent configuration duplication across clusters. + ```yaml - name: snowflake-prod-cluster status: active @@ -77,6 +82,18 @@ Jobs must include a single SQL statement via the `context.query` field. --- +## 🎭 Role Configuration + +The Snowflake role determines what permissions the connection has. Role is **decoupled from cluster configuration** and defined at the **command level** for security: +- Prevent configuration duplication across clusters +- **Prevent per-job role escalation** - users cannot override roles in job submissions + +### ⚠️ Default Behavior + +If no role is specified in the command context, Snowflake uses the **user's default role**. + +--- + ## 📊 Returning Query Results If your query returns data, Heimdall captures it as structured output accessible via: diff --git a/plugins/snowflake/snowflake.go b/plugins/snowflake/snowflake.go index 8dfce18..9a6f96c 100644 --- a/plugins/snowflake/snowflake.go +++ b/plugins/snowflake/snowflake.go @@ -6,6 +6,6 @@ import ( "github.com/patterninc/heimdall/pkg/plugin" ) -func New(_ *context.Context) (plugin.Handler, error) { - return snowflake.New(nil) +func New(ctx *context.Context) (plugin.Handler, error) { + return snowflake.New(ctx) }