Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ It centralizes execution logic, logging, and auditing—all accessible via API o

Commands may also restrict invocation via `allowed_callers` — a list of anchored regex patterns matched against `X-Heimdall-User`. Omitted/empty means open; non-matching callers are rejected at submit.

Command, cluster, and job `context` keys `password`, `private_key`, and `token` are redacted in API responses. Extra names can be added via `sensitive_context_keys`.

---

## 📦 API Overview
Expand Down
36 changes: 20 additions & 16 deletions internal/pkg/heimdall/heimdall.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/patterninc/heimdall/internal/pkg/pool"
"github.com/patterninc/heimdall/internal/pkg/rbac"
"github.com/patterninc/heimdall/internal/pkg/server"
heimdallContext "github.com/patterninc/heimdall/pkg/context"
"github.com/patterninc/heimdall/pkg/object/cluster"
"github.com/patterninc/heimdall/pkg/object/command"
"github.com/patterninc/heimdall/pkg/object/job"
Expand All @@ -42,26 +43,29 @@ const (
)

type Heimdall struct {
Server *server.Server `yaml:"server,omitempty" json:"server,omitempty"`
Commands command.Commands `yaml:"commands,omitempty" json:"commands,omitempty"`
Clusters cluster.Clusters `yaml:"clusters,omitempty" json:"clusters,omitempty"`
RBACs rbac.RBACs `yaml:"rbacs,omitempty" json:"rbacs,omitempty"`
JobsDirectory string `yaml:"jobs_directory,omitempty" json:"jobs_directory,omitempty"`
ArchiveDirectory string `yaml:"archive_directory,omitempty" json:"archive_directory,omitempty"`
ResultDirectory string `yaml:"result_directory,omitempty" json:"result_directory,omitempty"`
PluginsDirectory string `yaml:"plugin_directory,omitempty" json:"plugin_directory,omitempty"`
Database *database.Database `yaml:"database,omitempty" json:"database,omitempty"`
Pool *pool.Pool[*job.Job] `yaml:"pool,omitempty" json:"pool,omitempty"`
Auth *auth.Auth `yaml:"auth,omitempty" json:"auth,omitempty"`
Janitor *janitor.Janitor `yaml:"janitor,omitempty" json:"janitor,omitempty"`
HealthCheck *healthCheckConfig `yaml:"health_check,omitempty" json:"health_check,omitempty"`
Version string `yaml:"-" json:"-"`
agentName string
commandHandlers map[string]plugin.Handler
Server *server.Server `yaml:"server,omitempty" json:"server,omitempty"`
Commands command.Commands `yaml:"commands,omitempty" json:"commands,omitempty"`
Clusters cluster.Clusters `yaml:"clusters,omitempty" json:"clusters,omitempty"`
RBACs rbac.RBACs `yaml:"rbacs,omitempty" json:"rbacs,omitempty"`
JobsDirectory string `yaml:"jobs_directory,omitempty" json:"jobs_directory,omitempty"`
ArchiveDirectory string `yaml:"archive_directory,omitempty" json:"archive_directory,omitempty"`
ResultDirectory string `yaml:"result_directory,omitempty" json:"result_directory,omitempty"`
PluginsDirectory string `yaml:"plugin_directory,omitempty" json:"plugin_directory,omitempty"`
Database *database.Database `yaml:"database,omitempty" json:"database,omitempty"`
Pool *pool.Pool[*job.Job] `yaml:"pool,omitempty" json:"pool,omitempty"`
Auth *auth.Auth `yaml:"auth,omitempty" json:"auth,omitempty"`
Janitor *janitor.Janitor `yaml:"janitor,omitempty" json:"janitor,omitempty"`
HealthCheck *healthCheckConfig `yaml:"health_check,omitempty" json:"health_check,omitempty"`
SensitiveContextKeys []string `yaml:"sensitive_context_keys,omitempty" json:"sensitive_context_keys,omitempty"`
Version string `yaml:"-" json:"-"`
agentName string
commandHandlers map[string]plugin.Handler
}

func (h *Heimdall) Init() error {

heimdallContext.AddSensitiveKeys(h.SensitiveContextKeys)

// set jobs directory if not set
if h.JobsDirectory == `` {
h.JobsDirectory = defaultJobsDirectory
Expand Down
89 changes: 85 additions & 4 deletions pkg/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,26 @@ package context

import (
"encoding/json"
"strings"
)

const redactedValue = `REDACTED`

// sensitiveKeys are context field names whose values must never leave the
// process via JSON APIs. Matching is case-insensitive. Values are retained
// in-memory and when persisting via String().
var sensitiveKeys = map[string]struct{}{
`password`: {},
`private_key`: {},
`token`: {},
}
Comment thread
shashank-iitbhu marked this conversation as resolved.

type Context map[string]any

func New(v any) *Context {

data, err := json.Marshal(v)
// Avoid Context.MarshalJSON so secrets are not redacted when cloning.
data, err := marshalRaw(v)
if err != nil {
panic(`cannot marshal json`)
}
Expand All @@ -23,6 +36,31 @@ func New(v any) *Context {

}

func marshalRaw(v any) ([]byte, error) {
switch t := v.(type) {
case Context:
return json.Marshal(map[string]any(t))
case *Context:
if t == nil {
return []byte(`null`), nil
}
return json.Marshal(map[string]any(*t))
default:
return json.Marshal(v)
}
}

// AddSensitiveKeys registers additional context keys that should be redacted
// from JSON responses. Keys are matched case-insensitively.
func AddSensitiveKeys(keys []string) {
for _, key := range keys {
key = strings.ToLower(strings.TrimSpace(key))
if key != `` {
sensitiveKeys[key] = struct{}{}
}
}
}

func (c *Context) UnmarshalYAML(unmarshal func(any) error) error {

value := make(map[string]any)
Expand Down Expand Up @@ -51,10 +89,20 @@ func (c *Context) UnmarshalJSON(data []byte) error {

}

// MarshalJSON redacts sensitive fields for API responses. Internal helpers
// (String, Unmarshal) marshal the underlying map so plugins and DB storage
// still see real secrets.
func (c Context) MarshalJSON() ([]byte, error) {
if c == nil {
return []byte(`null`), nil
}
return json.Marshal(redactMap(c))
}

func (c *Context) Unmarshal(v any) error {

// let's marshal our data first
data, err := json.Marshal(*c)
// Marshal the underlying map so sensitive values are not redacted.
data, err := json.Marshal(map[string]any(*c))

if err != nil {
return err
Expand All @@ -70,8 +118,41 @@ func (c *Context) String() string {
return ``
}

data, _ := json.Marshal(*c)
// Persist the real context; do not go through MarshalJSON redaction.
data, _ := json.Marshal(map[string]any(*c))

return string(data)

}

func isSensitiveKey(key string) bool {
_, ok := sensitiveKeys[strings.ToLower(key)]
return ok
}

func redactMap(m map[string]any) map[string]any {
out := make(map[string]any, len(m))
for k, v := range m {
if isSensitiveKey(k) {
out[k] = redactedValue
continue
}
out[k] = redactValue(v)
}
return out
}

func redactValue(v any) any {
switch t := v.(type) {
case map[string]any:
return redactMap(t)
case []any:
out := make([]any, len(t))
for i, item := range t {
out[i] = redactValue(item)
}
return out
default:
return v
}
}
Loading