diff --git a/internal/appconfig/config.go b/internal/appconfig/config.go index 2b9211fcf076fedc806d8b746b70985da5077371..b77112c3922fb9a7783733d5bb58fe9093bd5104 100644 --- a/internal/appconfig/config.go +++ b/internal/appconfig/config.go @@ -46,8 +46,8 @@ type CookedConfig struct { // MCPConfig contains MCP transport configuration. type MCPConfig struct { - HTTPAddr string `toml:"http_addr"` - HTTPToken string `toml:"http_token"` + HTTPAddr string `toml:"http_addr"` + HTTPToken *string `toml:"http_token"` } // Config is fully resolved runtime configuration. @@ -99,12 +99,17 @@ func Load(ctx context.Context, path string, resolver *configvalue.Resolver) (Con if envHTTPAddr := os.Getenv("COOKED_MCP_HTTP_ADDR"); envHTTPAddr != "" { httpAddr = envHTTPAddr } + httpToken, err := httpToken(ctx, resolver, fileConfig.MCP.HTTPToken) + if err != nil { + return Config{}, err + } return Config{ - Username: username, - Password: password, - BaseURL: baseURL, - HTTPAddr: httpAddr, + Username: username, + Password: password, + BaseURL: baseURL, + HTTPAddr: httpAddr, + HTTPToken: httpToken, }, nil } @@ -197,6 +202,29 @@ func baseURL(ctx context.Context, resolver *configvalue.Resolver, config string) return parsed, nil } +func httpToken(ctx context.Context, resolver *configvalue.Resolver, config *string) (string, error) { + if value, ok := os.LookupEnv("COOKED_MCP_HTTP_TOKEN"); ok { + if value == "" { + return "", fmt.Errorf("MCP HTTP token must not be empty") + } + + return value, nil + } + if config == nil { + return "", nil + } + + value, err := resolver.ResolveOrThrow(ctx, *config, "MCP HTTP token") + if err != nil { + return "", err + } + if value == "" { + return "", fmt.Errorf("MCP HTTP token must not be empty") + } + + return value, nil +} + func isLoopbackHost(host string) bool { if host == "localhost" { return true diff --git a/internal/appconfig/config_test.go b/internal/appconfig/config_test.go index cfaec5a46383def424efdcadccfaa6ad0b2cf610..eb7dda49f5a4a396ffc425a1fe630e8d2c659d29 100644 --- a/internal/appconfig/config_test.go +++ b/internal/appconfig/config_test.go @@ -120,6 +120,91 @@ http_addr = "127.0.0.1:9000" } } +// server.SECURITY.4 server.SECURITY.9 +func TestLoadACIDServerSecurity4And9EnvironmentOverridesTOMLHTTPToken(t *testing.T) { + t.Setenv("COOKED_MCP_HTTP_TOKEN", "env-token") + + configPath := filepath.Join(t.TempDir(), "config.toml") + writeConfig(t, configPath, `[user] +name = "toml-user" +password = "toml-password" + +[mcp] +http_token = "toml-token" +`) + + got, err := Load(context.Background(), configPath, configvalue.NewResolver()) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + + if got.HTTPToken != "env-token" { + t.Fatalf("HTTPToken = %q, want environment token", got.HTTPToken) + } +} + +// server.SECURITY.5 server.SECURITY.10 server.SECURITY.12 server.SECURITY.13 +func TestLoadACIDServerSecurity5And10ResolvesTOMLHTTPTokenConfigValue(t *testing.T) { + t.Setenv("TEST_HTTP_TOKEN", "shell-token") + + configPath := filepath.Join(t.TempDir(), "config.toml") + writeConfig(t, configPath, `[user] +name = "toml-user" +password = "toml-password" + +[mcp] +http_token = "$TEST_HTTP_TOKEN" +`) + + got, err := Load(context.Background(), configPath, configvalue.NewResolver()) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + + if got.HTTPToken != "shell-token" { + t.Fatalf("HTTPToken = %q, want resolved TOML token", got.HTTPToken) + } +} + +// server.SECURITY.11 +func TestLoadACIDServerSecurity11RejectsEmptyEnvironmentHTTPToken(t *testing.T) { + t.Setenv("COOKED_MCP_HTTP_TOKEN", "") + + configPath := filepath.Join(t.TempDir(), "config.toml") + writeConfig(t, configPath, `[user] +name = "toml-user" +password = "toml-password" +`) + + _, err := Load(context.Background(), configPath, configvalue.NewResolver()) + if err == nil { + t.Fatal("Load() error = nil, want empty HTTP token error") + } + if !strings.Contains(err.Error(), "MCP HTTP token must not be empty") { + t.Fatalf("Load() error = %v, want empty HTTP token error", err) + } +} + +// server.SECURITY.11 +func TestLoadACIDServerSecurity11RejectsEmptyTOMLHTTPToken(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config.toml") + writeConfig(t, configPath, `[user] +name = "toml-user" +password = "toml-password" + +[mcp] +http_token = "" +`) + + _, err := Load(context.Background(), configPath, configvalue.NewResolver()) + if err == nil { + t.Fatal("Load() error = nil, want empty HTTP token error") + } + if !strings.Contains(err.Error(), "MCP HTTP token must not be empty") { + t.Fatalf("Load() error = %v, want empty HTTP token error", err) + } +} + func TestLoadRejectsMissingExplicitConfigFile(t *testing.T) { _, err := Load(context.Background(), filepath.Join(t.TempDir(), "missing.toml"), configvalue.NewResolver()) if err == nil {