Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/models/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ type GetAllFoldersParameters struct {
FoldersPath string
InfisicalToken string
UniversalAuthAccessToken string
ProjectConfigFilePath string
}

type CreateFolderParameters struct {
Expand Down
27 changes: 27 additions & 0 deletions packages/util/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
123 changes: 123 additions & 0 deletions packages/util/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package util

import (
"os"
"path/filepath"
"testing"
)

Expand Down Expand Up @@ -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")
}
})
}

6 changes: 5 additions & 1 deletion packages/util/folders.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
134 changes: 134 additions & 0 deletions packages/util/machine_identity_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
6 changes: 5 additions & 1 deletion packages/util/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down