From 00605f950bf192d15cf4429889a440be34b8a8cb Mon Sep 17 00:00:00 2001 From: Christophe Vila Date: Sat, 25 Jul 2026 18:55:53 +0200 Subject: [PATCH] config: default GarminTokenStoreRoot instead of leaving it empty Per-user profile isolation namespaces each user's mcp-garmin session cache under {GarminTokenStoreRoot}/{userID} (api.Server.garminFor), but Load() left GarminTokenStoreRoot ("" when GARMIN_TOKENSTORE is unset) with no required-var check and no safe default. In that state garminFor's `if cfg.TokenStorePath != ""` guard skips the per-user join entirely, so every user's subprocess would fall back to the same default token cache -- a cross-user Garmin-session collision risk in any deployment that forgets to set GARMIN_TOKENSTORE. Default it to a "garmin-tokenstores" directory next to DBPath when unset, so every deployment gets per-user isolation automatically, while GARMIN_TOKENSTORE can still override it explicitly. Log the derived default. Update the field's doc comment (no longer "if set") and add config_test.go cases covering the default derivation and the explicit-override precedence. Co-Authored-By: Claude Sonnet 5 --- backend/internal/config/config.go | 20 +++++++++++------ backend/internal/config/config_test.go | 30 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 261fb65..944108c 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -7,7 +7,9 @@ package config import ( "fmt" + "log" "os" + "path/filepath" "strconv" "strings" "time" @@ -24,12 +26,13 @@ type Config struct { GarminPythonPath string // GarminServerPath is mcp-garmin's server.py. GarminServerPath string - // GarminTokenStoreRoot, if set, is the root directory under which each - // user's mcp-garmin session cache lives (one subdirectory per user id, - // e.g. "/3"), overriding mcp-garmin's default ~/.garth. Read from - // the same GARMIN_TOKENSTORE env var as before Task 1's per-user - // scoping -- only its meaning changed (a root directory rather than a - // single path). + // GarminTokenStoreRoot is the root directory under which each user's + // mcp-garmin session cache lives (one subdirectory per user id, e.g. + // "/3"). Read from the GARMIN_TOKENSTORE env var; if unset, it + // defaults to a "garmin-tokenstores" directory next to DBPath so every + // deployment gets per-user isolation automatically -- multi-tenant + // operation always relies on this being a real, distinct-per-user path + // (see api.Server.garminFor), so it can never be silently left empty. GarminTokenStoreRoot string MinConfidence float64 @@ -78,6 +81,11 @@ func Load() (Config, error) { LegacyOwnerOIDCSub: os.Getenv("GENIUSRUN_LEGACY_OWNER_OIDC_SUB"), } + if cfg.GarminTokenStoreRoot == "" { + cfg.GarminTokenStoreRoot = filepath.Join(filepath.Dir(cfg.DBPath), "garmin-tokenstores") + log.Printf("GARMIN_TOKENSTORE not set, defaulting to %q", cfg.GarminTokenStoreRoot) + } + if cfg.GarminPythonPath == "" { return cfg, fmt.Errorf("MCP_GARMIN_PYTHON is required (path to mcp-garmin's venv python)") } diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go index 0a4f470..1e50def 100644 --- a/backend/internal/config/config_test.go +++ b/backend/internal/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "path/filepath" "testing" "time" ) @@ -80,6 +81,35 @@ func TestLoad_SessionSecretTooShort(t *testing.T) { } } +func TestLoad_GarminTokenStoreRootDefaultsNextToDBPath(t *testing.T) { + setRequiredEnv(t) + t.Setenv("GARMIN_TOKENSTORE", "") + t.Setenv("GENIUSRUN_DB_PATH", "/var/lib/geniusrun/geniusrun.db") + + cfg, err := Load() + if err != nil { + t.Fatalf("Load: %v", err) + } + want := filepath.Join("/var/lib/geniusrun", "garmin-tokenstores") + if cfg.GarminTokenStoreRoot != want { + t.Errorf("GarminTokenStoreRoot = %q, want %q", cfg.GarminTokenStoreRoot, want) + } +} + +func TestLoad_GarminTokenStoreRootExplicitOverridesDefault(t *testing.T) { + setRequiredEnv(t) + t.Setenv("GARMIN_TOKENSTORE", "/custom/tokenstores") + t.Setenv("GENIUSRUN_DB_PATH", "/var/lib/geniusrun/geniusrun.db") + + cfg, err := Load() + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.GarminTokenStoreRoot != "/custom/tokenstores" { + t.Errorf("GarminTokenStoreRoot = %q, want explicit override to take precedence", cfg.GarminTokenStoreRoot) + } +} + func TestLoad_CustomRoleAndDuration(t *testing.T) { setRequiredEnv(t) t.Setenv("GENIUSRUN_OIDC_REQUIRED_ROLE", "admin")