From 37233a0e9522ee70499b103059d44b769afee0e7 Mon Sep 17 00:00:00 2001 From: SK Akram Date: Mon, 17 Aug 2026 06:42:02 +0000 Subject: [PATCH] fix(machine-identity): fall back to .infisical.json for project ID --- packages/models/cli.go | 1 + packages/util/config.go | 27 +++++ packages/util/config_test.go | 123 +++++++++++++++++++++++ packages/util/folders.go | 6 +- packages/util/machine_identity_test.go | 134 +++++++++++++++++++++++++ packages/util/secrets.go | 6 +- 6 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 packages/util/machine_identity_test.go diff --git a/packages/models/cli.go b/packages/models/cli.go index a4ab86ad..36c857f7 100644 --- a/packages/models/cli.go +++ b/packages/models/cli.go @@ -158,6 +158,7 @@ type GetAllFoldersParameters struct { FoldersPath string InfisicalToken string UniversalAuthAccessToken string + ProjectConfigFilePath string } type CreateFolderParameters struct { diff --git a/packages/util/config.go b/packages/util/config.go index 99bfb47d..9aaf95e4 100644 --- a/packages/util/config.go +++ b/packages/util/config.go @@ -139,6 +139,33 @@ func GetWorkSpaceFromFilePath(configFileDir string) (models.WorkspaceConfigFile, return workspaceConfigFile, nil } +func ResolveWorkspaceIdForMachineIdentity(projectConfigFilePath, explicitWorkspaceId string) (string, error) { + if explicitWorkspaceId != "" { + return explicitWorkspaceId, nil + } + + var workspaceConfig models.WorkspaceConfigFile + if projectConfigFilePath != "" { + cfg, err := GetWorkSpaceFromFilePath(projectConfigFilePath) + if err != nil { + return "", fmt.Errorf("no project id found in %s: %w", projectConfigFilePath, err) + } + workspaceConfig = cfg + } else { + cfg, err := GetWorkSpaceFromFile() + if err != nil { + return "", fmt.Errorf("no project id found in .infisical.json: %w", err) + } + workspaceConfig = cfg + } + + if workspaceConfig.WorkspaceId == "" { + return "", fmt.Errorf("project id is missing in .infisical.json") + } + + return workspaceConfig.WorkspaceId, nil +} + // FindWorkspaceConfigFile searches for a .infisical.json file in the current directory and all parent directories. func FindWorkspaceConfigFile() (string, error) { dir, err := os.Getwd() diff --git a/packages/util/config_test.go b/packages/util/config_test.go index cc7e6af8..a2c8e4b5 100644 --- a/packages/util/config_test.go +++ b/packages/util/config_test.go @@ -2,6 +2,7 @@ package util import ( "os" + "path/filepath" "testing" ) @@ -70,3 +71,125 @@ func TestGetEnvDomain(t *testing.T) { }) } } + +func TestResolveWorkspaceIdForMachineIdentity(t *testing.T) { + t.Run("explicit value wins over file", func(t *testing.T) { + dir := t.TempDir() + t.Chdir(dir) + if err := os.WriteFile(filepath.Join(dir, ".infisical.json"), []byte(`{"workspaceId":"fromfile"}`), 0o600); err != nil { + t.Fatalf("write workspace: %v", err) + } + + got, err := ResolveWorkspaceIdForMachineIdentity("", "fromflag") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "fromflag" { + t.Fatalf("got %q, want fromflag", got) + } + }) + + t.Run("falls back to .infisical.json in cwd", func(t *testing.T) { + dir := t.TempDir() + t.Chdir(dir) + if err := os.WriteFile(filepath.Join(dir, ".infisical.json"), []byte(`{"workspaceId":"fromfile"}`), 0o600); err != nil { + t.Fatalf("write workspace: %v", err) + } + + got, err := ResolveWorkspaceIdForMachineIdentity("", "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "fromfile" { + t.Fatalf("got %q, want fromfile", got) + } + }) + + t.Run("falls back to .infisical.json in parent directory", func(t *testing.T) { + parent := t.TempDir() + if err := os.WriteFile(filepath.Join(parent, ".infisical.json"), []byte(`{"workspaceId":"fromparent"}`), 0o600); err != nil { + t.Fatalf("write workspace: %v", err) + } + child := filepath.Join(parent, "nested", "dir") + if err := os.MkdirAll(child, 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + t.Chdir(child) + + got, err := ResolveWorkspaceIdForMachineIdentity("", "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "fromparent" { + t.Fatalf("got %q, want fromparent", got) + } + }) + + t.Run("uses explicit projectConfigFilePath when provided", func(t *testing.T) { + // cwd is empty; the file lives in a different dir passed explicitly. + t.Chdir(t.TempDir()) + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, ".infisical.json"), []byte(`{"workspaceId":"fromexplicitpath"}`), 0o600); err != nil { + t.Fatalf("write workspace: %v", err) + } + + got, err := ResolveWorkspaceIdForMachineIdentity(dir, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "fromexplicitpath" { + t.Fatalf("got %q, want fromexplicitpath", got) + } + }) + + t.Run("explicit path takes precedence over cwd file", func(t *testing.T) { + // cwd has a file with one id, the explicit path has another. + cwd := t.TempDir() + t.Chdir(cwd) + if err := os.WriteFile(filepath.Join(cwd, ".infisical.json"), []byte(`{"workspaceId":"fromcwd"}`), 0o600); err != nil { + t.Fatalf("write workspace: %v", err) + } + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, ".infisical.json"), []byte(`{"workspaceId":"fromexplicitpath"}`), 0o600); err != nil { + t.Fatalf("write workspace: %v", err) + } + + got, err := ResolveWorkspaceIdForMachineIdentity(dir, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "fromexplicitpath" { + t.Fatalf("got %q, want fromexplicitpath", got) + } + }) + + t.Run("error when no file and no explicit value", func(t *testing.T) { + t.Chdir(t.TempDir()) + _, err := ResolveWorkspaceIdForMachineIdentity("", "") + if err == nil { + t.Fatal("expected error, got nil") + } + }) + + t.Run("error when explicit path file is missing", func(t *testing.T) { + t.Chdir(t.TempDir()) + _, err := ResolveWorkspaceIdForMachineIdentity(t.TempDir(), "") + if err == nil { + t.Fatal("expected error, got nil") + } + }) + + t.Run("error when workspaceId is empty in config file", func(t *testing.T) { + dir := t.TempDir() + t.Chdir(dir) + if err := os.WriteFile(filepath.Join(dir, ".infisical.json"), []byte(`{"workspaceId":""}`), 0o600); err != nil { + t.Fatalf("write workspace: %v", err) + } + + _, err := ResolveWorkspaceIdForMachineIdentity("", "") + if err == nil { + t.Fatal("expected error when workspaceId is empty, got nil") + } + }) +} + diff --git a/packages/util/folders.go b/packages/util/folders.go index fb4f2a32..fc4ecb60 100644 --- a/packages/util/folders.go +++ b/packages/util/folders.go @@ -49,7 +49,11 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder log.Debug().Msg("GetAllFolders: Trying to fetch folders using universal auth") if params.WorkspaceId == "" { - PrintErrorMessageAndExit("Project ID is required when using machine identity") + resolved, err := ResolveWorkspaceIdForMachineIdentity(params.ProjectConfigFilePath, params.WorkspaceId) + if err != nil { + PrintErrorMessageAndExit("Project ID is required when using machine identity") + } + params.WorkspaceId = resolved } // get folders via machine identity diff --git a/packages/util/machine_identity_test.go b/packages/util/machine_identity_test.go new file mode 100644 index 00000000..3f4e10be --- /dev/null +++ b/packages/util/machine_identity_test.go @@ -0,0 +1,134 @@ +package util + +import ( + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "sync/atomic" + "testing" + + "github.com/Infisical/infisical-merge/packages/config" + "github.com/Infisical/infisical-merge/packages/models" +) + +func TestGetAllEnvironmentVariables_MachineIdentityFallsBackToConfig(t *testing.T) { + const fileWorkspaceID = "ws-from-config-file" + + var seenProjectID atomic.Value + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + seenProjectID.Store(r.URL.Query().Get("projectId")) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"secrets":[]}`)) + })) + defer srv.Close() + + parsed, err := url.Parse(srv.URL) + if err != nil { + t.Fatalf("parse server url: %v", err) + } + prevURL := config.INFISICAL_URL + config.INFISICAL_URL = parsed.Scheme + "://" + parsed.Host + "/api" + t.Cleanup(func() { config.INFISICAL_URL = prevURL }) + + dir := t.TempDir() + t.Chdir(dir) + if err := os.WriteFile(filepath.Join(dir, ".infisical.json"), []byte(`{"workspaceId":"`+fileWorkspaceID+`"}`), 0o600); err != nil { + t.Fatalf("write workspace: %v", err) + } + + _, err = GetAllEnvironmentVariables(models.GetAllSecretsParameters{ + UniversalAuthAccessToken: "fake-universal-auth-token", + Environment: "dev", + }, "") + if err != nil { + t.Fatalf("GetAllEnvironmentVariables: %v", err) + } + + got, _ := seenProjectID.Load().(string) + if got != fileWorkspaceID { + t.Fatalf("workspace id sent to API = %q, want %q (issue #365: --projectId missing should fall back to .infisical.json)", got, fileWorkspaceID) + } +} + +func TestGetAllEnvironmentVariables_MachineIdentityFlagWins(t *testing.T) { + const flagWorkspaceID = "ws-from-flag" + const fileWorkspaceID = "ws-from-config-file" + + var seenProjectID atomic.Value + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + seenProjectID.Store(r.URL.Query().Get("projectId")) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"secrets":[]}`)) + })) + defer srv.Close() + + parsed, err := url.Parse(srv.URL) + if err != nil { + t.Fatalf("parse server url: %v", err) + } + prevURL := config.INFISICAL_URL + config.INFISICAL_URL = parsed.Scheme + "://" + parsed.Host + "/api" + t.Cleanup(func() { config.INFISICAL_URL = prevURL }) + + dir := t.TempDir() + t.Chdir(dir) + if err := os.WriteFile(filepath.Join(dir, ".infisical.json"), []byte(`{"workspaceId":"`+fileWorkspaceID+`"}`), 0o600); err != nil { + t.Fatalf("write workspace: %v", err) + } + + _, err = GetAllEnvironmentVariables(models.GetAllSecretsParameters{ + UniversalAuthAccessToken: "fake-universal-auth-token", + WorkspaceId: flagWorkspaceID, + Environment: "dev", + }, "") + if err != nil { + t.Fatalf("GetAllEnvironmentVariables: %v", err) + } + + got, _ := seenProjectID.Load().(string) + if got != flagWorkspaceID { + t.Fatalf("workspace id sent to API = %q, want %q (--projectId should win over .infisical.json)", got, flagWorkspaceID) + } +} + + +func TestGetAllFolders_MachineIdentityFallsBackToConfig(t *testing.T) { + const fileWorkspaceID = "ws-from-config-file" + + var seenWorkspaceID atomic.Value + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + seenWorkspaceID.Store(r.URL.Query().Get("workspaceId")) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"folders":[]}`)) + })) + defer srv.Close() + + parsed, err := url.Parse(srv.URL) + if err != nil { + t.Fatalf("parse server url: %v", err) + } + prevURL := config.INFISICAL_URL + config.INFISICAL_URL = parsed.Scheme + "://" + parsed.Host + "/api" + t.Cleanup(func() { config.INFISICAL_URL = prevURL }) + + dir := t.TempDir() + t.Chdir(dir) + if err := os.WriteFile(filepath.Join(dir, ".infisical.json"), []byte(`{"workspaceId":"`+fileWorkspaceID+`"}`), 0o600); err != nil { + t.Fatalf("write workspace: %v", err) + } + + _, err = GetAllFolders(models.GetAllFoldersParameters{ + UniversalAuthAccessToken: "fake-universal-auth-token", + Environment: "dev", + }) + if err != nil { + t.Fatalf("GetAllFolders: %v", err) + } + + got, _ := seenWorkspaceID.Load().(string) + if got != fileWorkspaceID { + t.Fatalf("workspace id sent to API = %q, want %q (issue #365: --projectId missing should fall back to .infisical.json)", got, fileWorkspaceID) + } +} diff --git a/packages/util/secrets.go b/packages/util/secrets.go index c1d458ee..f10566fd 100644 --- a/packages/util/secrets.go +++ b/packages/util/secrets.go @@ -367,7 +367,11 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo } else if params.UniversalAuthAccessToken != "" { if params.WorkspaceId == "" { - PrintErrorMessageAndExit("Project ID is required when using machine identity") + resolved, err := ResolveWorkspaceIdForMachineIdentity(projectConfigFilePath, params.WorkspaceId) + if err != nil { + PrintErrorMessageAndExit("Project ID is required when using machine identity") + } + params.WorkspaceId = resolved } log.Debug().Msg("Trying to fetch secrets using universal auth")