Fix --account N credential priority across all SDKs
--account N was silently ignored when UNSANDBOX_PUBLIC_KEY/SECRET_KEY env vars were set. The credential resolution checked env vars at tier 2 before ever reaching the CSV lookup, so the explicit flag had no effect. Correct priority order (all 8 SDKs): 1. CLI -p/-k flags (explicit key args) 2. --account N → direct CSV row lookup (bypasses env vars) 3. UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars 4. ~/.unsandbox/accounts.csv default (row 0 or UNSANDBOX_ACCOUNT) SDKs updated: C, Python, Go, JavaScript, Ruby, PHP, Java, Rust Also adds: - --account N flag to CLI parsers in all 8 SDKs (Go, PHP, Java, Rust previously had no flag at all; Python/JS/Ruby had the parameter but never wired it to the CLI) - test_account_flag.sh integration test for each SDK verifying the priority behavior with real and garbage credentials - test-integration Makefile target for the C SDK
This commit is contained in:
parent
8093386568
commit
ed6c52b001
20 changed files with 1243 additions and 174 deletions
|
|
@ -108,6 +108,14 @@ test: build $(TEST_DIR)/test_library
|
||||||
|
|
||||||
test-library: test
|
test-library: test
|
||||||
|
|
||||||
|
test-integration: build
|
||||||
|
@echo ""
|
||||||
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
@echo "INTEGRATION: Testing --account flag priority"
|
||||||
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
@echo ""
|
||||||
|
@bash $(TEST_DIR)/test_account_flag.sh
|
||||||
|
|
||||||
test-functional: build $(TEST_DIR)/test_functional
|
test-functional: build $(TEST_DIR)/test_functional
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
|
||||||
|
|
@ -396,7 +396,14 @@ static UnsandboxCredentials* get_credentials(const char *cli_pk, const char *cli
|
||||||
return creds;
|
return creds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 2: Environment variables (keys)
|
// Priority 2: --account N flag → explicit CSV lookup
|
||||||
|
// When the user explicitly selects an account, go straight to accounts.csv.
|
||||||
|
// Env vars are intentionally bypassed — an explicit flag must win over ambient env.
|
||||||
|
if (account_index >= 0) {
|
||||||
|
return load_credentials_from_csv(account_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Environment variables (keys)
|
||||||
const char *env_pk = getenv("UNSANDBOX_PUBLIC_KEY");
|
const char *env_pk = getenv("UNSANDBOX_PUBLIC_KEY");
|
||||||
const char *env_sk = getenv("UNSANDBOX_SECRET_KEY");
|
const char *env_sk = getenv("UNSANDBOX_SECRET_KEY");
|
||||||
|
|
||||||
|
|
@ -416,16 +423,12 @@ static UnsandboxCredentials* get_credentials(const char *cli_pk, const char *cli
|
||||||
return creds;
|
return creds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 3: Config file (~/.unsandbox/accounts.csv)
|
// Priority 4: Config file (~/.unsandbox/accounts.csv)
|
||||||
// Use account_index from --account flag, or UNSANDBOX_ACCOUNT env var, or default to 0
|
// Use UNSANDBOX_ACCOUNT env var, or default to account 0
|
||||||
int csv_index = account_index;
|
int csv_index = 0;
|
||||||
if (csv_index < 0) {
|
const char *env_account = getenv("UNSANDBOX_ACCOUNT");
|
||||||
const char *env_account = getenv("UNSANDBOX_ACCOUNT");
|
if (env_account && strlen(env_account) > 0) {
|
||||||
if (env_account && strlen(env_account) > 0) {
|
csv_index = atoi(env_account);
|
||||||
csv_index = atoi(env_account);
|
|
||||||
} else {
|
|
||||||
csv_index = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return load_credentials_from_csv(csv_index);
|
return load_credentials_from_csv(csv_index);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
111
clients/c/tests/test_account_flag.sh
Executable file
111
clients/c/tests/test_account_flag.sh
Executable file
|
|
@ -0,0 +1,111 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Integration test: --account N flag must take priority over env vars
|
||||||
|
#
|
||||||
|
# Requires: UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY (real credentials)
|
||||||
|
# Run: make test-integration OR bash tests/test_account_flag.sh
|
||||||
|
#
|
||||||
|
# The defect this guards against: get_credentials() checked env vars before
|
||||||
|
# account_index, so --account N was silently ignored when env vars existed.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
UN_BIN="$SCRIPT_DIR/../un"
|
||||||
|
|
||||||
|
RED='\033[31m'
|
||||||
|
GREEN='\033[32m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
check() {
|
||||||
|
local desc="$1" result="$2"
|
||||||
|
if [ "$result" = "pass" ]; then
|
||||||
|
printf " ${GREEN}✓${NC} %s\n" "$desc"
|
||||||
|
pass=$((pass + 1))
|
||||||
|
else
|
||||||
|
printf " ${RED}✗${NC} %s\n" "$desc"
|
||||||
|
fail=$((fail + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Require real credentials to be available
|
||||||
|
if [ -z "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -z "${UNSANDBOX_SECRET_KEY:-}" ]; then
|
||||||
|
echo "SKIP: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$UN_BIN" ]; then
|
||||||
|
echo "FAIL: UN binary not found at $UN_BIN — run make first"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REAL_PK="$UNSANDBOX_PUBLIC_KEY"
|
||||||
|
REAL_SK="$UNSANDBOX_SECRET_KEY"
|
||||||
|
|
||||||
|
# Temporary HOME with accounts.csv:
|
||||||
|
# index 0: garbage credentials (will always 401)
|
||||||
|
# index 1: real credentials (will succeed)
|
||||||
|
TMPHOME="$(mktemp -d)"
|
||||||
|
mkdir -p "$TMPHOME/.unsandbox"
|
||||||
|
trap 'rm -rf "$TMPHOME"' EXIT
|
||||||
|
|
||||||
|
cat > "$TMPHOME/.unsandbox/accounts.csv" <<CSV
|
||||||
|
unsb-pk-fake-0000-0000-0000,unsb-sk-fake0-00000-00000-00000
|
||||||
|
${REAL_PK},${REAL_SK}
|
||||||
|
CSV
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "INTEGRATION: --account flag priority test"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
# --- Test 1: --account 1 should use CSV row 1 (real creds), ignoring env vars ---
|
||||||
|
# Set env vars to GARBAGE so the test fails if env vars win.
|
||||||
|
# Auth success = no 401/unauthorized (429 rate-limit means creds passed auth, just hit concurrency cap).
|
||||||
|
OUT=$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY=unsb-pk-fake-0000-0000-0000 \
|
||||||
|
UNSANDBOX_SECRET_KEY=unsb-sk-fake0-00000-00000-00000 \
|
||||||
|
"$UN_BIN" --account 1 key 2>&1 || true)
|
||||||
|
|
||||||
|
if ! echo "$OUT" | grep -qi "401\|unauthorized\|invalid_credential\|No credentials"; then
|
||||||
|
check "--account 1 uses CSV row 1 (real creds) over garbage env vars" "pass"
|
||||||
|
else
|
||||||
|
check "--account 1 uses CSV row 1 (real creds) over garbage env vars" "fail"
|
||||||
|
echo " output: $OUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Test 2: --account 0 should use CSV row 0 (garbage creds) → 401 ---
|
||||||
|
# Even though real env vars are set, explicit --account 0 should pick garbage creds
|
||||||
|
OUT=$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$REAL_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$REAL_SK" \
|
||||||
|
"$UN_BIN" --account 0 key 2>&1 || true)
|
||||||
|
|
||||||
|
if echo "$OUT" | grep -qi "401\|unauthorized\|invalid_credential\|invalid key"; then
|
||||||
|
check "--account 0 uses CSV row 0 (garbage creds) despite real env vars, 401 as expected" "pass"
|
||||||
|
else
|
||||||
|
check "--account 0 uses CSV row 0 (garbage creds) despite real env vars, 401 as expected" "fail"
|
||||||
|
echo " output: $OUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Test 3: no --account flag, real env vars → env vars win over garbage CSV row 0 ---
|
||||||
|
OUT=$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$REAL_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$REAL_SK" \
|
||||||
|
"$UN_BIN" key 2>&1 || true)
|
||||||
|
|
||||||
|
if ! echo "$OUT" | grep -qi "401\|unauthorized\|invalid_credential\|No credentials"; then
|
||||||
|
check "No --account flag: env vars used, succeeds" "pass"
|
||||||
|
else
|
||||||
|
check "No --account flag: env vars used, succeeds" "fail"
|
||||||
|
echo " output: $OUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
printf "Passed: ${GREEN}%d${NC} Failed: ${RED}%d${NC}\n" "$pass" "$fail"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
|
|
@ -20,7 +20,7 @@ func NewClient(publicKey, secretKey string) *Client {
|
||||||
|
|
||||||
// NewClientFromEnv resolves credentials from the 4-tier priority system.
|
// NewClientFromEnv resolves credentials from the 4-tier priority system.
|
||||||
func NewClientFromEnv() (*Client, error) {
|
func NewClientFromEnv() (*Client, error) {
|
||||||
creds, err := ResolveCredentials("", "")
|
creds, err := ResolveCredentials("", "", -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -161,10 +161,12 @@ func loadCredentialsFromCsv(csvPath string, accountIndex int) *Credentials {
|
||||||
//
|
//
|
||||||
// Priority:
|
// Priority:
|
||||||
// 1. Function arguments (publicKey, secretKey non-empty)
|
// 1. Function arguments (publicKey, secretKey non-empty)
|
||||||
// 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
// 2. accountIndex >= 0 → load from accounts.csv row N (before env vars)
|
||||||
// 3. ~/.unsandbox/accounts.csv
|
// 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
// 4. ./accounts.csv
|
// 4. Default CSV lookup (account 0 or UNSANDBOX_ACCOUNT env)
|
||||||
func ResolveCredentials(publicKey, secretKey string) (*Credentials, error) {
|
//
|
||||||
|
// Pass accountIndex = -1 to mean "not specified".
|
||||||
|
func ResolveCredentials(publicKey, secretKey string, accountIndex int) (*Credentials, error) {
|
||||||
// Tier 1: Function arguments
|
// Tier 1: Function arguments
|
||||||
if publicKey != "" && secretKey != "" {
|
if publicKey != "" && secretKey != "" {
|
||||||
return &Credentials{
|
return &Credentials{
|
||||||
|
|
@ -173,7 +175,23 @@ func ResolveCredentials(publicKey, secretKey string) (*Credentials, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 2: Environment variables
|
// Tier 2: Explicit account index → load from CSV before checking env vars
|
||||||
|
if accountIndex >= 0 {
|
||||||
|
unsandboxDir, err := getUnsandboxDir()
|
||||||
|
if err == nil {
|
||||||
|
if creds := loadCredentialsFromCsv(filepath.Join(unsandboxDir, "accounts.csv"), accountIndex); creds != nil {
|
||||||
|
return creds, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if creds := loadCredentialsFromCsv("accounts.csv", accountIndex); creds != nil {
|
||||||
|
return creds, nil
|
||||||
|
}
|
||||||
|
return nil, &CredentialsError{
|
||||||
|
Message: fmt.Sprintf("No credentials found at account index %d in accounts.csv", accountIndex),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 3: Environment variables
|
||||||
envPk := os.Getenv("UNSANDBOX_PUBLIC_KEY")
|
envPk := os.Getenv("UNSANDBOX_PUBLIC_KEY")
|
||||||
envSk := os.Getenv("UNSANDBOX_SECRET_KEY")
|
envSk := os.Getenv("UNSANDBOX_SECRET_KEY")
|
||||||
if envPk != "" && envSk != "" {
|
if envPk != "" && envSk != "" {
|
||||||
|
|
@ -183,35 +201,36 @@ func ResolveCredentials(publicKey, secretKey string) (*Credentials, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine account index
|
// Determine default account index from env
|
||||||
accountIndex := 0
|
defaultIndex := 0
|
||||||
if envAccount := os.Getenv("UNSANDBOX_ACCOUNT"); envAccount != "" {
|
if envAccount := os.Getenv("UNSANDBOX_ACCOUNT"); envAccount != "" {
|
||||||
var err error
|
var err error
|
||||||
accountIndex, err = strconv.Atoi(envAccount)
|
defaultIndex, err = strconv.Atoi(envAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
accountIndex = 0
|
defaultIndex = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 3: ~/.unsandbox/accounts.csv
|
// Tier 4: ~/.unsandbox/accounts.csv
|
||||||
unsandboxDir, err := getUnsandboxDir()
|
unsandboxDir, err := getUnsandboxDir()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if creds := loadCredentialsFromCsv(filepath.Join(unsandboxDir, "accounts.csv"), accountIndex); creds != nil {
|
if creds := loadCredentialsFromCsv(filepath.Join(unsandboxDir, "accounts.csv"), defaultIndex); creds != nil {
|
||||||
return creds, nil
|
return creds, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 4: ./accounts.csv
|
// Tier 5: ./accounts.csv
|
||||||
if creds := loadCredentialsFromCsv("accounts.csv", accountIndex); creds != nil {
|
if creds := loadCredentialsFromCsv("accounts.csv", defaultIndex); creds != nil {
|
||||||
return creds, nil
|
return creds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, &CredentialsError{
|
return nil, &CredentialsError{
|
||||||
Message: "No credentials found. Please provide via:\n" +
|
Message: "No credentials found. Please provide via:\n" +
|
||||||
" 1. Function arguments (publicKey, secretKey)\n" +
|
" 1. Function arguments (publicKey, secretKey)\n" +
|
||||||
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
|
" 2. --account N flag (CSV row N)\n" +
|
||||||
" 3. ~/.unsandbox/accounts.csv\n" +
|
" 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
|
||||||
" 4. ./accounts.csv",
|
" 4. ~/.unsandbox/accounts.csv\n" +
|
||||||
|
" 5. ./accounts.csv",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1647,18 +1666,19 @@ const (
|
||||||
// CLIOptions holds parsed CLI arguments
|
// CLIOptions holds parsed CLI arguments
|
||||||
type CLIOptions struct {
|
type CLIOptions struct {
|
||||||
// Global options
|
// Global options
|
||||||
Shell string
|
Shell string
|
||||||
Env []string
|
Env []string
|
||||||
Files []string
|
Files []string
|
||||||
FilePaths []string
|
FilePaths []string
|
||||||
Artifacts bool
|
Artifacts bool
|
||||||
OutputDir string
|
OutputDir string
|
||||||
PublicKey string
|
PublicKey string
|
||||||
SecretKey string
|
SecretKey string
|
||||||
Network string
|
Network string
|
||||||
VCPU int
|
VCPU int
|
||||||
Yes bool
|
Yes bool
|
||||||
Help bool
|
Help bool
|
||||||
|
AccountIndex int // -1 means not specified
|
||||||
|
|
||||||
// Command
|
// Command
|
||||||
Command string
|
Command string
|
||||||
|
|
@ -3153,7 +3173,7 @@ func runLanguages(creds *Credentials, args []string) int {
|
||||||
|
|
||||||
// parseGlobalFlags parses global CLI options
|
// parseGlobalFlags parses global CLI options
|
||||||
func parseGlobalFlags(args []string) (*CLIOptions, []string) {
|
func parseGlobalFlags(args []string) (*CLIOptions, []string) {
|
||||||
opts := &CLIOptions{}
|
opts := &CLIOptions{AccountIndex: -1}
|
||||||
remaining := []string{}
|
remaining := []string{}
|
||||||
|
|
||||||
for i := 0; i < len(args); i++ {
|
for i := 0; i < len(args); i++ {
|
||||||
|
|
@ -3196,6 +3216,13 @@ func parseGlobalFlags(args []string) (*CLIOptions, []string) {
|
||||||
opts.SecretKey = args[i+1]
|
opts.SecretKey = args[i+1]
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
case arg == "--account":
|
||||||
|
if i+1 < len(args) {
|
||||||
|
if v, err := strconv.Atoi(args[i+1]); err == nil {
|
||||||
|
opts.AccountIndex = v
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
case arg == "-n" || arg == "--network":
|
case arg == "-n" || arg == "--network":
|
||||||
if i+1 < len(args) {
|
if i+1 < len(args) {
|
||||||
opts.Network = args[i+1]
|
opts.Network = args[i+1]
|
||||||
|
|
@ -3249,7 +3276,7 @@ func CliMain() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve credentials
|
// Resolve credentials
|
||||||
creds, err := ResolveCredentials(opts.PublicKey, opts.SecretKey)
|
creds, err := ResolveCredentials(opts.PublicKey, opts.SecretKey, opts.AccountIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
||||||
os.Exit(ExitAuthError)
|
os.Exit(ExitAuthError)
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ func TestResolveCredentialsFromEnv(t *testing.T) {
|
||||||
os.Setenv("UNSANDBOX_SECRET_KEY", testSK)
|
os.Setenv("UNSANDBOX_SECRET_KEY", testSK)
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
creds, err := ResolveCredentials("", "")
|
creds, err := ResolveCredentials("", "", -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ResolveCredentials failed: %v", err)
|
t.Fatalf("ResolveCredentials failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +201,7 @@ func TestResolveCredentialsFromArgs(t *testing.T) {
|
||||||
testPK := "unsb-pk-arg1-arg2-arg3-arg4"
|
testPK := "unsb-pk-arg1-arg2-arg3-arg4"
|
||||||
testSK := "unsb-sk-arg11-arg22-arg33-arg44"
|
testSK := "unsb-sk-arg11-arg22-arg33-arg44"
|
||||||
|
|
||||||
creds, err := ResolveCredentials(testPK, testSK)
|
creds, err := ResolveCredentials(testPK, testSK, -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ResolveCredentials failed: %v", err)
|
t.Fatalf("ResolveCredentials failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -218,7 +218,7 @@ func TestResolveCredentialsFromArgs(t *testing.T) {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
func getTestCredentials(t *testing.T) *Credentials {
|
func getTestCredentials(t *testing.T) *Credentials {
|
||||||
creds, err := ResolveCredentials("", "")
|
creds, err := ResolveCredentials("", "", -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skip("No credentials available for functional tests")
|
t.Skip("No credentials available for functional tests")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
120
clients/go/sync/tests/test_account_flag.sh
Executable file
120
clients/go/sync/tests/test_account_flag.sh
Executable file
|
|
@ -0,0 +1,120 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Integration test: --account N flag must take priority over env vars
|
||||||
|
#
|
||||||
|
# Requires: UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY (real credentials)
|
||||||
|
# Run: make test-integration OR bash tests/test_account_flag.sh
|
||||||
|
#
|
||||||
|
# The defect this guards against: ResolveCredentials() checked env vars before
|
||||||
|
# account_index, so --account N was silently ignored when env vars existed.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SRC_DIR="$SCRIPT_DIR/../src"
|
||||||
|
UN_BIN="$SCRIPT_DIR/../un"
|
||||||
|
|
||||||
|
RED='\033[31m'
|
||||||
|
GREEN='\033[32m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
check() {
|
||||||
|
local desc="$1" result="$2"
|
||||||
|
if [ "$result" = "pass" ]; then
|
||||||
|
printf " ${GREEN}✓${NC} %s\n" "$desc"
|
||||||
|
pass=$((pass + 1))
|
||||||
|
else
|
||||||
|
printf " ${RED}✗${NC} %s\n" "$desc"
|
||||||
|
fail=$((fail + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Require real credentials to be available
|
||||||
|
if [ -z "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -z "${UNSANDBOX_SECRET_KEY:-}" ]; then
|
||||||
|
echo "SKIP: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build the binary if it doesn't exist or source is newer
|
||||||
|
if [ ! -x "$UN_BIN" ] || [ "$SRC_DIR/un.go" -nt "$UN_BIN" ]; then
|
||||||
|
echo "Building Go binary..."
|
||||||
|
(cd "$SRC_DIR" && go build -o "$UN_BIN" .) || {
|
||||||
|
echo "FAIL: go build failed"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$UN_BIN" ]; then
|
||||||
|
echo "FAIL: UN binary not found at $UN_BIN — run go build first"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REAL_PK="$UNSANDBOX_PUBLIC_KEY"
|
||||||
|
REAL_SK="$UNSANDBOX_SECRET_KEY"
|
||||||
|
|
||||||
|
# Temporary HOME with accounts.csv:
|
||||||
|
# index 0: garbage credentials (will always 401)
|
||||||
|
# index 1: real credentials (will succeed)
|
||||||
|
TMPHOME="$(mktemp -d)"
|
||||||
|
mkdir -p "$TMPHOME/.unsandbox"
|
||||||
|
trap 'rm -rf "$TMPHOME"' EXIT
|
||||||
|
|
||||||
|
cat > "$TMPHOME/.unsandbox/accounts.csv" <<CSV
|
||||||
|
unsb-pk-fake-0000-0000-0000,unsb-sk-fake0-00000-00000-00000
|
||||||
|
${REAL_PK},${REAL_SK}
|
||||||
|
CSV
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "INTEGRATION: --account flag priority test"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
# --- Test 1: --account 1 should use CSV row 1 (real creds), ignoring env vars ---
|
||||||
|
# Set env vars to GARBAGE so the test fails if env vars win
|
||||||
|
OUT=$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY=unsb-pk-fake-0000-0000-0000 \
|
||||||
|
UNSANDBOX_SECRET_KEY=unsb-sk-fake0-00000-00000-00000 \
|
||||||
|
"$UN_BIN" --account 1 key 2>&1 || true)
|
||||||
|
|
||||||
|
if ! echo "$OUT" | grep -qi "401\|unauthorized\|invalid_credential\|No credentials"; then
|
||||||
|
check "--account 1 uses CSV row 1 (real creds) over garbage env vars" "pass"
|
||||||
|
else
|
||||||
|
check "--account 1 uses CSV row 1 (real creds) over garbage env vars" "fail"
|
||||||
|
echo " output: $OUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Test 2: --account 0 should use CSV row 0 (garbage creds) → 401 ---
|
||||||
|
# Even though real env vars are set, explicit --account 0 should pick garbage creds
|
||||||
|
OUT=$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$REAL_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$REAL_SK" \
|
||||||
|
"$UN_BIN" --account 0 key 2>&1 || true)
|
||||||
|
|
||||||
|
if echo "$OUT" | grep -qi "401\|unauthorized\|invalid\|error"; then
|
||||||
|
check "--account 0 uses CSV row 0 (garbage creds) despite real env vars, 401 as expected" "pass"
|
||||||
|
else
|
||||||
|
check "--account 0 uses CSV row 0 (garbage creds) despite real env vars, 401 as expected" "fail"
|
||||||
|
echo " output: $OUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Test 3: no --account flag, real env vars → env vars win over garbage CSV row 0 ---
|
||||||
|
OUT=$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$REAL_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$REAL_SK" \
|
||||||
|
"$UN_BIN" key 2>&1 || true)
|
||||||
|
|
||||||
|
if ! echo "$OUT" | grep -qi "401\|unauthorized\|invalid_credential\|No credentials"; then
|
||||||
|
check "No --account flag: env vars used, succeeds" "pass"
|
||||||
|
else
|
||||||
|
check "No --account flag: env vars used, succeeds" "fail"
|
||||||
|
echo " output: $OUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
printf "Passed: ${GREEN}%d${NC} Failed: ${RED}%d${NC}\n" "$pass" "$fail"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
|
|
@ -173,7 +173,7 @@ func TestResolveCredentialsFromEnv(t *testing.T) {
|
||||||
os.Setenv("UNSANDBOX_SECRET_KEY", testSK)
|
os.Setenv("UNSANDBOX_SECRET_KEY", testSK)
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
creds, err := ResolveCredentials("", "")
|
creds, err := ResolveCredentials("", "", -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ResolveCredentials failed: %v", err)
|
t.Fatalf("ResolveCredentials failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +201,7 @@ func TestResolveCredentialsFromArgs(t *testing.T) {
|
||||||
testPK := "unsb-pk-arg1-arg2-arg3-arg4"
|
testPK := "unsb-pk-arg1-arg2-arg3-arg4"
|
||||||
testSK := "unsb-sk-arg11-arg22-arg33-arg44"
|
testSK := "unsb-sk-arg11-arg22-arg33-arg44"
|
||||||
|
|
||||||
creds, err := ResolveCredentials(testPK, testSK)
|
creds, err := ResolveCredentials(testPK, testSK, -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ResolveCredentials failed: %v", err)
|
t.Fatalf("ResolveCredentials failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -218,7 +218,7 @@ func TestResolveCredentialsFromArgs(t *testing.T) {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
func getTestCredentials(t *testing.T) *Credentials {
|
func getTestCredentials(t *testing.T) *Credentials {
|
||||||
creds, err := ResolveCredentials("", "")
|
creds, err := ResolveCredentials("", "", -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Skip("No credentials available for functional tests")
|
t.Skip("No credentials available for functional tests")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,12 @@
|
||||||
* // Snapshot operations
|
* // Snapshot operations
|
||||||
* String snapshotId = Un.sessionSnapshot(sessionId, publicKey, secretKey, "my-snapshot", false);
|
* String snapshotId = Un.sessionSnapshot(sessionId, publicKey, secretKey, "my-snapshot", false);
|
||||||
*
|
*
|
||||||
* Authentication Priority (4-tier):
|
* Authentication Priority (5-tier):
|
||||||
* 1. Method arguments (publicKey, secretKey)
|
* 1. Method arguments (publicKey, secretKey)
|
||||||
* 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
* 2. --account N flag / accountIndex >= 0 (load row N from accounts.csv)
|
||||||
* 3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
|
* 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
* 4. Local directory (./accounts.csv, line 0 by default)
|
* 4. Config file (~/.unsandbox/accounts.csv, line 0 by default)
|
||||||
|
* 5. Local directory (./accounts.csv, line 0 by default)
|
||||||
*
|
*
|
||||||
* Request Authentication (HMAC-SHA256):
|
* Request Authentication (HMAC-SHA256):
|
||||||
* Authorization: Bearer <public_key>
|
* Authorization: Bearer <public_key>
|
||||||
|
|
@ -175,38 +176,58 @@ public class Un {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String[] resolveCredentials(String publicKey, String secretKey) {
|
private static String[] resolveCredentials(String publicKey, String secretKey) {
|
||||||
|
return resolveCredentials(publicKey, secretKey, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String[] resolveCredentials(String publicKey, String secretKey, int accountIndex) {
|
||||||
// Tier 1: Method arguments
|
// Tier 1: Method arguments
|
||||||
if (publicKey != null && !publicKey.isEmpty() && secretKey != null && !secretKey.isEmpty()) {
|
if (publicKey != null && !publicKey.isEmpty() && secretKey != null && !secretKey.isEmpty()) {
|
||||||
return new String[]{publicKey, secretKey};
|
return new String[]{publicKey, secretKey};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 2: Environment variables
|
// Tier 2: Explicit account index (e.g. --account N from CLI)
|
||||||
|
if (accountIndex >= 0) {
|
||||||
|
Path unsandboxDir = getUnsandboxDir();
|
||||||
|
String[] creds = loadCredentialsFromCsv(unsandboxDir.resolve("accounts.csv"), accountIndex);
|
||||||
|
if (creds != null) {
|
||||||
|
return creds;
|
||||||
|
}
|
||||||
|
creds = loadCredentialsFromCsv(Paths.get("accounts.csv"), accountIndex);
|
||||||
|
if (creds != null) {
|
||||||
|
return creds;
|
||||||
|
}
|
||||||
|
throw new CredentialsException(
|
||||||
|
"No credentials found at account index " + accountIndex + " in accounts.csv"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 3: Environment variables
|
||||||
String envPk = System.getenv("UNSANDBOX_PUBLIC_KEY");
|
String envPk = System.getenv("UNSANDBOX_PUBLIC_KEY");
|
||||||
String envSk = System.getenv("UNSANDBOX_SECRET_KEY");
|
String envSk = System.getenv("UNSANDBOX_SECRET_KEY");
|
||||||
if (envPk != null && !envPk.isEmpty() && envSk != null && !envSk.isEmpty()) {
|
if (envPk != null && !envPk.isEmpty() && envSk != null && !envSk.isEmpty()) {
|
||||||
return new String[]{envPk, envSk};
|
return new String[]{envPk, envSk};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine account index
|
// Determine account index from env (default 0)
|
||||||
int accountIndex = 0;
|
int csvIndex = 0;
|
||||||
String accountEnv = System.getenv("UNSANDBOX_ACCOUNT");
|
String accountEnv = System.getenv("UNSANDBOX_ACCOUNT");
|
||||||
if (accountEnv != null && !accountEnv.isEmpty()) {
|
if (accountEnv != null && !accountEnv.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
accountIndex = Integer.parseInt(accountEnv);
|
csvIndex = Integer.parseInt(accountEnv);
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
// Use default
|
// Use default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 3: ~/.unsandbox/accounts.csv
|
// Tier 4: ~/.unsandbox/accounts.csv
|
||||||
Path unsandboxDir = getUnsandboxDir();
|
Path unsandboxDir = getUnsandboxDir();
|
||||||
String[] creds = loadCredentialsFromCsv(unsandboxDir.resolve("accounts.csv"), accountIndex);
|
String[] creds = loadCredentialsFromCsv(unsandboxDir.resolve("accounts.csv"), csvIndex);
|
||||||
if (creds != null) {
|
if (creds != null) {
|
||||||
return creds;
|
return creds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 4: ./accounts.csv
|
// Tier 5: ./accounts.csv
|
||||||
creds = loadCredentialsFromCsv(Paths.get("accounts.csv"), accountIndex);
|
creds = loadCredentialsFromCsv(Paths.get("accounts.csv"), csvIndex);
|
||||||
if (creds != null) {
|
if (creds != null) {
|
||||||
return creds;
|
return creds;
|
||||||
}
|
}
|
||||||
|
|
@ -214,9 +235,10 @@ public class Un {
|
||||||
throw new CredentialsException(
|
throw new CredentialsException(
|
||||||
"No credentials found. Please provide via:\n" +
|
"No credentials found. Please provide via:\n" +
|
||||||
" 1. Method arguments (publicKey, secretKey)\n" +
|
" 1. Method arguments (publicKey, secretKey)\n" +
|
||||||
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
|
" 2. --account N flag (load row N from accounts.csv)\n" +
|
||||||
" 3. ~/.unsandbox/accounts.csv\n" +
|
" 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
|
||||||
" 4. ./accounts.csv"
|
" 4. ~/.unsandbox/accounts.csv\n" +
|
||||||
|
" 5. ./accounts.csv"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2860,6 +2882,7 @@ public class Un {
|
||||||
String language = null;
|
String language = null;
|
||||||
String networkMode = "zerotrust";
|
String networkMode = "zerotrust";
|
||||||
int vcpu = 1;
|
int vcpu = 1;
|
||||||
|
int accountIndex = -1;
|
||||||
List<String> envVars = new ArrayList<>();
|
List<String> envVars = new ArrayList<>();
|
||||||
List<String> files = new ArrayList<>();
|
List<String> files = new ArrayList<>();
|
||||||
List<String> positionalArgs = new ArrayList<>();
|
List<String> positionalArgs = new ArrayList<>();
|
||||||
|
|
@ -2871,6 +2894,18 @@ public class Un {
|
||||||
if (arg.equals("-h") || arg.equals("--help")) {
|
if (arg.equals("-h") || arg.equals("--help")) {
|
||||||
showHelp = true;
|
showHelp = true;
|
||||||
i++;
|
i++;
|
||||||
|
} else if (arg.equals("--account")) {
|
||||||
|
if (i + 1 >= args.length) {
|
||||||
|
System.err.println("Error: --account requires an argument");
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
accountIndex = Integer.parseInt(args[++i]);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
System.err.println("Error: --account requires an integer argument");
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
} else if (arg.equals("-s") || arg.equals("--shell")) {
|
} else if (arg.equals("-s") || arg.equals("--shell")) {
|
||||||
if (i + 1 >= args.length) {
|
if (i + 1 >= args.length) {
|
||||||
System.err.println("Error: -s/--shell requires an argument");
|
System.err.println("Error: -s/--shell requires an argument");
|
||||||
|
|
@ -2936,6 +2971,14 @@ public class Un {
|
||||||
|
|
||||||
String command = positionalArgs.get(0);
|
String command = positionalArgs.get(0);
|
||||||
|
|
||||||
|
// Pre-resolve credentials so --account N is honoured by all subcommands.
|
||||||
|
// Only resolve if explicit keys were not supplied via -p/-k flags.
|
||||||
|
if (publicKey == null || publicKey.isEmpty() || secretKey == null || secretKey.isEmpty()) {
|
||||||
|
String[] creds = resolveCredentials(publicKey, secretKey, accountIndex);
|
||||||
|
publicKey = creds[0];
|
||||||
|
secretKey = creds[1];
|
||||||
|
}
|
||||||
|
|
||||||
// Route to subcommand handlers
|
// Route to subcommand handlers
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "session":
|
case "session":
|
||||||
|
|
@ -2982,6 +3025,7 @@ public class Un {
|
||||||
System.out.println(" -f, --file FILE Add input file to /tmp/");
|
System.out.println(" -f, --file FILE Add input file to /tmp/");
|
||||||
System.out.println(" -p, --public-key KEY API public key");
|
System.out.println(" -p, --public-key KEY API public key");
|
||||||
System.out.println(" -k, --secret-key KEY API secret key");
|
System.out.println(" -k, --secret-key KEY API secret key");
|
||||||
|
System.out.println(" --account N Use row N from accounts.csv (overrides env vars)");
|
||||||
System.out.println(" -n, --network MODE Network: zerotrust or semitrusted");
|
System.out.println(" -n, --network MODE Network: zerotrust or semitrusted");
|
||||||
System.out.println(" -v, --vcpu N vCPU count (1-8)");
|
System.out.println(" -v, --vcpu N vCPU count (1-8)");
|
||||||
System.out.println(" -h, --help Show help");
|
System.out.println(" -h, --help Show help");
|
||||||
|
|
|
||||||
113
clients/java/sync/tests/test_account_flag.sh
Executable file
113
clients/java/sync/tests/test_account_flag.sh
Executable file
|
|
@ -0,0 +1,113 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Integration test: --account N credential selection in Java SDK CLI
|
||||||
|
#
|
||||||
|
# Tests that --account N loads row N from accounts.csv and takes priority
|
||||||
|
# over environment variables UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY.
|
||||||
|
#
|
||||||
|
# Requires: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY set in the environment.
|
||||||
|
# SKIP if not set.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SRC_DIR="$(cd "$SCRIPT_DIR/../src" && pwd)"
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
SKIP=0
|
||||||
|
|
||||||
|
pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
|
||||||
|
fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||||
|
skip() { echo "SKIP: $1"; SKIP=$((SKIP + 1)); }
|
||||||
|
|
||||||
|
# ---- Prerequisites ----
|
||||||
|
|
||||||
|
if [ -z "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -z "${UNSANDBOX_SECRET_KEY:-}" ]; then
|
||||||
|
skip "UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY not set"
|
||||||
|
echo ""
|
||||||
|
echo "Results: PASS=$PASS FAIL=$FAIL SKIP=$SKIP"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compile Un.java if needed
|
||||||
|
CLASS_FILE="$SRC_DIR/Un.class"
|
||||||
|
if [ ! -f "$CLASS_FILE" ] || [ "$SRC_DIR/Un.java" -nt "$CLASS_FILE" ]; then
|
||||||
|
echo "Compiling Un.java..."
|
||||||
|
if ! javac -cp "$SRC_DIR" "$SRC_DIR/Un.java" 2>&1; then
|
||||||
|
fail "javac compilation failed"
|
||||||
|
echo ""
|
||||||
|
echo "Results: PASS=$PASS FAIL=$FAIL SKIP=$SKIP"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- Temp home setup ----
|
||||||
|
|
||||||
|
TMPHOME="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMPHOME"' EXIT
|
||||||
|
|
||||||
|
mkdir -p "$TMPHOME/.unsandbox"
|
||||||
|
# Row 0: garbage credentials
|
||||||
|
# Row 1: real credentials from environment
|
||||||
|
printf 'garbage-pk,garbage-sk\n%s,%s\n' \
|
||||||
|
"$UNSANDBOX_PUBLIC_KEY" "$UNSANDBOX_SECRET_KEY" \
|
||||||
|
> "$TMPHOME/.unsandbox/accounts.csv"
|
||||||
|
|
||||||
|
# ---- Test 1: --account 1 loads real creds, env vars set to garbage ----
|
||||||
|
# With HOME=TMPHOME, env set to garbage, --account 1 should pick real creds
|
||||||
|
# and the 'key' subcommand should succeed (validateKeys returns 200).
|
||||||
|
|
||||||
|
RESULT=$(
|
||||||
|
HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="garbage-pk-env" \
|
||||||
|
UNSANDBOX_SECRET_KEY="garbage-sk-env" \
|
||||||
|
java -cp "$SRC_DIR" Un --account 1 key 2>&1
|
||||||
|
) && RC=$? || RC=$?
|
||||||
|
|
||||||
|
if [ $RC -eq 0 ]; then
|
||||||
|
pass "--account 1 loads row 1 from accounts.csv (real creds), ignores garbage env vars"
|
||||||
|
else
|
||||||
|
fail "--account 1 should have succeeded but exited $RC: $RESULT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- Test 2: --account 0 loads garbage creds, should get 401/error ----
|
||||||
|
# With env vars set to real creds, --account 0 should use garbage row 0
|
||||||
|
# and the API call should fail (unauthorized).
|
||||||
|
|
||||||
|
RESULT=$(
|
||||||
|
HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$UNSANDBOX_PUBLIC_KEY" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$UNSANDBOX_SECRET_KEY" \
|
||||||
|
java -cp "$SRC_DIR" Un --account 0 key 2>&1
|
||||||
|
) && RC=$? || RC=$?
|
||||||
|
|
||||||
|
if [ $RC -ne 0 ]; then
|
||||||
|
pass "--account 0 loads garbage creds from row 0, API rejects them (env vars ignored)"
|
||||||
|
else
|
||||||
|
fail "--account 0 should have failed (garbage creds) but succeeded: $RESULT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- Test 3: no --account flag, env vars set to real creds → success ----
|
||||||
|
|
||||||
|
RESULT=$(
|
||||||
|
HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$UNSANDBOX_PUBLIC_KEY" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$UNSANDBOX_SECRET_KEY" \
|
||||||
|
java -cp "$SRC_DIR" Un key 2>&1
|
||||||
|
) && RC=$? || RC=$?
|
||||||
|
|
||||||
|
if [ $RC -eq 0 ]; then
|
||||||
|
pass "no --account flag uses env vars (real creds), succeeds"
|
||||||
|
else
|
||||||
|
fail "no --account flag should succeed with real env var creds but exited $RC: $RESULT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- Summary ----
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Results: PASS=$PASS FAIL=$FAIL SKIP=$SKIP"
|
||||||
|
|
||||||
|
if [ $FAIL -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
|
@ -331,10 +331,11 @@ function loadCredentialsFromStorage() {
|
||||||
*
|
*
|
||||||
* Priority:
|
* Priority:
|
||||||
* 1. Function arguments
|
* 1. Function arguments
|
||||||
* 2. Environment variables (Node.js)
|
* 2. accountIndex >= 0 → load from accounts.csv row N
|
||||||
* 3. localStorage (Browser)
|
* 3. Environment variables (Node.js)
|
||||||
* 4. ~/.unsandbox/accounts.csv (Node.js)
|
* 4. localStorage (Browser)
|
||||||
* 5. ./accounts.csv (Node.js)
|
* 5. ~/.unsandbox/accounts.csv (default row, Node.js)
|
||||||
|
* 6. ./accounts.csv (default row, Node.js)
|
||||||
*/
|
*/
|
||||||
function resolveCredentials(publicKey, secretKey, accountIndex) {
|
function resolveCredentials(publicKey, secretKey, accountIndex) {
|
||||||
// Tier 1: Function arguments
|
// Tier 1: Function arguments
|
||||||
|
|
@ -342,7 +343,26 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
|
||||||
return [publicKey, secretKey];
|
return [publicKey, secretKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 2: Environment variables (Node.js only)
|
// Tier 2: Explicit accountIndex → load from accounts.csv row N (Node.js only)
|
||||||
|
if (IS_NODE && fs && path && accountIndex !== undefined && accountIndex >= 0) {
|
||||||
|
// ~/.unsandbox/accounts.csv first
|
||||||
|
try {
|
||||||
|
const unsandboxDir = getUnsandboxDir();
|
||||||
|
const creds = loadCredentialsFromCsv(path.join(unsandboxDir, 'accounts.csv'), accountIndex);
|
||||||
|
if (creds) {
|
||||||
|
return creds;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue to next location
|
||||||
|
}
|
||||||
|
// ./accounts.csv fallback
|
||||||
|
const creds = loadCredentialsFromCsv('accounts.csv', accountIndex);
|
||||||
|
if (creds) {
|
||||||
|
return creds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 3: Environment variables (Node.js only)
|
||||||
if (IS_NODE) {
|
if (IS_NODE) {
|
||||||
const envPk = process.env.UNSANDBOX_PUBLIC_KEY;
|
const envPk = process.env.UNSANDBOX_PUBLIC_KEY;
|
||||||
const envSk = process.env.UNSANDBOX_SECRET_KEY;
|
const envSk = process.env.UNSANDBOX_SECRET_KEY;
|
||||||
|
|
@ -351,7 +371,7 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 3: localStorage (Browser only)
|
// Tier 4: localStorage (Browser only)
|
||||||
if (IS_BROWSER) {
|
if (IS_BROWSER) {
|
||||||
const storageCreds = loadCredentialsFromStorage();
|
const storageCreds = loadCredentialsFromStorage();
|
||||||
if (storageCreds) {
|
if (storageCreds) {
|
||||||
|
|
@ -359,17 +379,14 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 4 & 5: File-based credentials (Node.js only)
|
// Tier 5 & 6: File-based credentials with default index (Node.js only)
|
||||||
if (IS_NODE && fs && path) {
|
if (IS_NODE && fs && path) {
|
||||||
// Determine account index
|
const defaultIndex = parseInt(process.env.UNSANDBOX_ACCOUNT || '0', 10);
|
||||||
if (accountIndex === undefined) {
|
|
||||||
accountIndex = parseInt(process.env.UNSANDBOX_ACCOUNT || '0', 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tier 4: ~/.unsandbox/accounts.csv
|
// Tier 5: ~/.unsandbox/accounts.csv
|
||||||
try {
|
try {
|
||||||
const unsandboxDir = getUnsandboxDir();
|
const unsandboxDir = getUnsandboxDir();
|
||||||
const creds = loadCredentialsFromCsv(path.join(unsandboxDir, 'accounts.csv'), accountIndex);
|
const creds = loadCredentialsFromCsv(path.join(unsandboxDir, 'accounts.csv'), defaultIndex);
|
||||||
if (creds) {
|
if (creds) {
|
||||||
return creds;
|
return creds;
|
||||||
}
|
}
|
||||||
|
|
@ -377,8 +394,8 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
|
||||||
// Continue to next tier
|
// Continue to next tier
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 5: ./accounts.csv
|
// Tier 6: ./accounts.csv
|
||||||
const creds = loadCredentialsFromCsv('accounts.csv', accountIndex);
|
const creds = loadCredentialsFromCsv('accounts.csv', defaultIndex);
|
||||||
if (creds) {
|
if (creds) {
|
||||||
return creds;
|
return creds;
|
||||||
}
|
}
|
||||||
|
|
@ -2193,6 +2210,7 @@ function parseArgs(args) {
|
||||||
output: null,
|
output: null,
|
||||||
publicKey: null,
|
publicKey: null,
|
||||||
secretKey: null,
|
secretKey: null,
|
||||||
|
accountIndex: undefined,
|
||||||
network: 'zerotrust',
|
network: 'zerotrust',
|
||||||
vcpu: 1,
|
vcpu: 1,
|
||||||
yes: false,
|
yes: false,
|
||||||
|
|
@ -2322,6 +2340,9 @@ function parseArgs(args) {
|
||||||
} else if (arg === '-k' || arg === '--secret-key') {
|
} else if (arg === '-k' || arg === '--secret-key') {
|
||||||
result.secretKey = args[++i];
|
result.secretKey = args[++i];
|
||||||
i++;
|
i++;
|
||||||
|
} else if (arg === '--account') {
|
||||||
|
result.accountIndex = parseInt(args[++i], 10);
|
||||||
|
i++;
|
||||||
} else if (arg === '-n' || arg === '--network') {
|
} else if (arg === '-n' || arg === '--network') {
|
||||||
result.network = args[++i];
|
result.network = args[++i];
|
||||||
i++;
|
i++;
|
||||||
|
|
@ -2509,8 +2530,7 @@ function formatTable(items, columns) {
|
||||||
* Handle session commands.
|
* Handle session commands.
|
||||||
*/
|
*/
|
||||||
async function handleSession(opts) {
|
async function handleSession(opts) {
|
||||||
const pk = opts.publicKey;
|
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||||
const sk = opts.secretKey;
|
|
||||||
|
|
||||||
// List sessions
|
// List sessions
|
||||||
if (opts.list) {
|
if (opts.list) {
|
||||||
|
|
@ -2593,8 +2613,7 @@ async function handleSession(opts) {
|
||||||
* Handle service commands.
|
* Handle service commands.
|
||||||
*/
|
*/
|
||||||
async function handleService(opts) {
|
async function handleService(opts) {
|
||||||
const pk = opts.publicKey;
|
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||||
const sk = opts.secretKey;
|
|
||||||
|
|
||||||
// Handle env subcommand
|
// Handle env subcommand
|
||||||
if (opts.subcommand === 'env') {
|
if (opts.subcommand === 'env') {
|
||||||
|
|
@ -2829,8 +2848,7 @@ async function handleService(opts) {
|
||||||
* Handle snapshot commands.
|
* Handle snapshot commands.
|
||||||
*/
|
*/
|
||||||
async function handleSnapshot(opts) {
|
async function handleSnapshot(opts) {
|
||||||
const pk = opts.publicKey;
|
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||||
const sk = opts.secretKey;
|
|
||||||
|
|
||||||
// List snapshots
|
// List snapshots
|
||||||
if (opts.list) {
|
if (opts.list) {
|
||||||
|
|
@ -2898,8 +2916,7 @@ async function handleSnapshot(opts) {
|
||||||
* Handle image command.
|
* Handle image command.
|
||||||
*/
|
*/
|
||||||
async function handleImage(opts) {
|
async function handleImage(opts) {
|
||||||
const pk = opts.publicKey;
|
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||||
const sk = opts.secretKey;
|
|
||||||
|
|
||||||
// List images
|
// List images
|
||||||
if (opts.list) {
|
if (opts.list) {
|
||||||
|
|
@ -3002,13 +3019,13 @@ async function handleImage(opts) {
|
||||||
* Handle key command.
|
* Handle key command.
|
||||||
*/
|
*/
|
||||||
async function handleKey(opts) {
|
async function handleKey(opts) {
|
||||||
|
const [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||||
try {
|
try {
|
||||||
const result = await validateKeys(opts.publicKey, opts.secretKey);
|
const result = await validateKeys(pk, sk);
|
||||||
console.log('API Key Status:');
|
console.log('API Key Status:');
|
||||||
console.log(JSON.stringify(result, null, 2));
|
console.log(JSON.stringify(result, null, 2));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If validate endpoint doesn't exist, just show that credentials were resolved
|
// If validate endpoint doesn't exist, just show that credentials were resolved
|
||||||
const [pk] = resolveCredentials(opts.publicKey, opts.secretKey);
|
|
||||||
console.log(`Public Key: ${pk}`);
|
console.log(`Public Key: ${pk}`);
|
||||||
console.log('Key validation endpoint returned error - key may still be valid.');
|
console.log('Key validation endpoint returned error - key may still be valid.');
|
||||||
}
|
}
|
||||||
|
|
@ -3018,7 +3035,8 @@ async function handleKey(opts) {
|
||||||
* Handle languages command.
|
* Handle languages command.
|
||||||
*/
|
*/
|
||||||
async function handleLanguages(opts) {
|
async function handleLanguages(opts) {
|
||||||
const languages = await getLanguages(opts.publicKey, opts.secretKey);
|
const [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||||
|
const languages = await getLanguages(pk, sk);
|
||||||
|
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
// Output as JSON array
|
// Output as JSON array
|
||||||
|
|
@ -3035,8 +3053,7 @@ async function handleLanguages(opts) {
|
||||||
* Handle execute command (default).
|
* Handle execute command (default).
|
||||||
*/
|
*/
|
||||||
async function handleExecute(opts) {
|
async function handleExecute(opts) {
|
||||||
const pk = opts.publicKey;
|
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||||
const sk = opts.secretKey;
|
|
||||||
|
|
||||||
let code;
|
let code;
|
||||||
let language;
|
let language;
|
||||||
|
|
|
||||||
108
clients/javascript/sync/tests/test_account_flag.sh
Executable file
108
clients/javascript/sync/tests/test_account_flag.sh
Executable file
|
|
@ -0,0 +1,108 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Integration test for --account N credential selection in un.js.
|
||||||
|
#
|
||||||
|
# Tests that --account N selects the correct row from accounts.csv,
|
||||||
|
# taking priority over environment variables.
|
||||||
|
#
|
||||||
|
# Requires UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY to be set.
|
||||||
|
# Skips if credentials are not available.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
UN_JS="$SCRIPT_DIR/../src/un.js"
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
SKIP=0
|
||||||
|
|
||||||
|
pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
|
||||||
|
fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||||
|
skip() { echo "SKIP: $1"; SKIP=$((SKIP + 1)); }
|
||||||
|
|
||||||
|
# Require real credentials from environment
|
||||||
|
if [ -z "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -z "${UNSANDBOX_SECRET_KEY:-}" ]; then
|
||||||
|
skip "UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY not set"
|
||||||
|
echo "Results: PASS=$PASS FAIL=$FAIL SKIP=$SKIP"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
REAL_PK="$UNSANDBOX_PUBLIC_KEY"
|
||||||
|
REAL_SK="$UNSANDBOX_SECRET_KEY"
|
||||||
|
GARBAGE_PK="unsb-pk-0000-0000-0000-garbage"
|
||||||
|
GARBAGE_SK="unsb-sk-00000-00000-00000-garbage"
|
||||||
|
|
||||||
|
# Build a temporary HOME with accounts.csv: row 0 = garbage, row 1 = real creds
|
||||||
|
TMPHOME="$(mktemp -d)"
|
||||||
|
mkdir -p "$TMPHOME/.unsandbox"
|
||||||
|
# Header + row 0 (garbage) + row 1 (real)
|
||||||
|
printf 'public_key,secret_key\n%s,%s\n%s,%s\n' \
|
||||||
|
"$GARBAGE_PK" "$GARBAGE_SK" \
|
||||||
|
"$REAL_PK" "$REAL_SK" \
|
||||||
|
> "$TMPHOME/.unsandbox/accounts.csv"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$TMPHOME"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 1: HOME=TMPHOME, env vars = garbage, --account 1 => row 1 = real creds
|
||||||
|
# Expect: key command succeeds (no auth error)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$GARBAGE_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$GARBAGE_SK" \
|
||||||
|
node "$UN_JS" --account 1 key 2>&1 | grep -qiE 'Public Key|key_id|account|status'; then
|
||||||
|
pass "Test 1: --account 1 selects row 1 (real creds) over env garbage"
|
||||||
|
else
|
||||||
|
# Also accept a successful JSON response (key validates)
|
||||||
|
output="$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$GARBAGE_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$GARBAGE_SK" \
|
||||||
|
node "$UN_JS" --account 1 key 2>&1 || true)"
|
||||||
|
if echo "$output" | grep -qiE '401|invalid|unauthorized|authentication'; then
|
||||||
|
fail "Test 1: --account 1 selected row 1 but auth failed (real creds may be invalid)"
|
||||||
|
else
|
||||||
|
pass "Test 1: --account 1 selects row 1 (real creds) over env garbage"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 2: HOME=TMPHOME, env vars = real, --account 0 => row 0 = garbage creds
|
||||||
|
# Expect: 401 / auth error (garbage creds used despite real env vars)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
output2="$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$REAL_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$REAL_SK" \
|
||||||
|
node "$UN_JS" --account 0 key 2>&1 || true)"
|
||||||
|
if echo "$output2" | grep -qiE '401|invalid|unauthorized|authentication|error'; then
|
||||||
|
pass "Test 2: --account 0 selects row 0 (garbage) over env real creds (expected auth failure)"
|
||||||
|
else
|
||||||
|
fail "Test 2: --account 0 should use garbage creds and fail auth, but got: $output2"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 3: HOME=TMPHOME, env vars = real, no --account => env vars win (real creds)
|
||||||
|
# Expect: key command succeeds
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
output3="$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$REAL_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$REAL_SK" \
|
||||||
|
node "$UN_JS" key 2>&1 || true)"
|
||||||
|
if echo "$output3" | grep -qiE '401|invalid|unauthorized|authentication failed'; then
|
||||||
|
fail "Test 3: without --account, env real creds should succeed but got auth error: $output3"
|
||||||
|
else
|
||||||
|
pass "Test 3: without --account, env vars (real creds) are used"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "Results: PASS=$PASS FAIL=$FAIL SKIP=$SKIP"
|
||||||
|
|
||||||
|
if [ "$FAIL" -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
|
@ -138,6 +138,7 @@ class Unsandbox {
|
||||||
private ?string $defaultPublicKey = null;
|
private ?string $defaultPublicKey = null;
|
||||||
private ?string $defaultSecretKey = null;
|
private ?string $defaultSecretKey = null;
|
||||||
private int $accountIndex = 0;
|
private int $accountIndex = 0;
|
||||||
|
private bool $accountIndexExplicit = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Unsandbox client.
|
* Create a new Unsandbox client.
|
||||||
|
|
@ -1693,17 +1694,18 @@ class Unsandbox {
|
||||||
* Resolve credentials from 4-tier priority system.
|
* Resolve credentials from 4-tier priority system.
|
||||||
*
|
*
|
||||||
* Priority:
|
* Priority:
|
||||||
* 1. Method arguments
|
* 1. Method arguments / constructor defaults
|
||||||
* 2. Environment variables
|
* 2. $accountIndex >= 0 → load from accounts.csv row N
|
||||||
* 3. ~/.unsandbox/accounts.csv
|
* 3. Environment variables (UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY)
|
||||||
* 4. ./accounts.csv
|
* 4. Default CSV lookup (account 0)
|
||||||
*
|
*
|
||||||
* @param string|null $publicKey Public key from method argument
|
* @param string|null $publicKey Public key from method argument
|
||||||
* @param string|null $secretKey Secret key from method argument
|
* @param string|null $secretKey Secret key from method argument
|
||||||
|
* @param int|null $accountIndex Explicit account index (overrides env and default)
|
||||||
* @return array [publicKey, secretKey]
|
* @return array [publicKey, secretKey]
|
||||||
* @throws CredentialsException If no credentials found
|
* @throws CredentialsException If no credentials found
|
||||||
*/
|
*/
|
||||||
private function resolveCredentials(?string $publicKey, ?string $secretKey): array {
|
private function resolveCredentials(?string $publicKey, ?string $secretKey, ?int $accountIndex = null): array {
|
||||||
// Tier 1: Method arguments
|
// Tier 1: Method arguments
|
||||||
if (!empty($publicKey) && !empty($secretKey)) {
|
if (!empty($publicKey) && !empty($secretKey)) {
|
||||||
return [$publicKey, $secretKey];
|
return [$publicKey, $secretKey];
|
||||||
|
|
@ -1714,29 +1716,50 @@ class Unsandbox {
|
||||||
return [$this->defaultPublicKey, $this->defaultSecretKey];
|
return [$this->defaultPublicKey, $this->defaultSecretKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 2: Environment variables
|
// Tier 2: Explicit account index (--account N flag or constructor accountIndex != 0)
|
||||||
|
// Resolve the effective account index: explicit arg > UNSANDBOX_ACCOUNT env > $this->accountIndex
|
||||||
|
$effectiveIndex = null;
|
||||||
|
if ($accountIndex !== null && $accountIndex >= 0) {
|
||||||
|
$effectiveIndex = $accountIndex;
|
||||||
|
} elseif ($this->accountIndexExplicit) {
|
||||||
|
// --account flag was used on CLI (may be 0, so can't rely on != 0 check)
|
||||||
|
$effectiveIndex = $this->accountIndex;
|
||||||
|
} else {
|
||||||
|
$envAccount = getenv('UNSANDBOX_ACCOUNT');
|
||||||
|
if ($envAccount !== false && $envAccount !== '') {
|
||||||
|
$effectiveIndex = (int)$envAccount;
|
||||||
|
} elseif ($this->accountIndex !== 0) {
|
||||||
|
$effectiveIndex = $this->accountIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($effectiveIndex !== null) {
|
||||||
|
$unsandboxDir = $this->getUnsandboxDir();
|
||||||
|
$creds = $this->loadCredentialsFromCsv($unsandboxDir . '/accounts.csv', $effectiveIndex);
|
||||||
|
if ($creds !== null) {
|
||||||
|
return $creds;
|
||||||
|
}
|
||||||
|
$creds = $this->loadCredentialsFromCsv('./accounts.csv', $effectiveIndex);
|
||||||
|
if ($creds !== null) {
|
||||||
|
return $creds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 3: Environment variables
|
||||||
$envPk = getenv('UNSANDBOX_PUBLIC_KEY');
|
$envPk = getenv('UNSANDBOX_PUBLIC_KEY');
|
||||||
$envSk = getenv('UNSANDBOX_SECRET_KEY');
|
$envSk = getenv('UNSANDBOX_SECRET_KEY');
|
||||||
if (!empty($envPk) && !empty($envSk)) {
|
if (!empty($envPk) && !empty($envSk)) {
|
||||||
return [$envPk, $envSk];
|
return [$envPk, $envSk];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine account index
|
// Tier 4: Default CSV lookup (account 0)
|
||||||
$accountIndex = $this->accountIndex;
|
|
||||||
$envAccount = getenv('UNSANDBOX_ACCOUNT');
|
|
||||||
if ($envAccount !== false && $envAccount !== '') {
|
|
||||||
$accountIndex = (int)$envAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tier 3: ~/.unsandbox/accounts.csv
|
|
||||||
$unsandboxDir = $this->getUnsandboxDir();
|
$unsandboxDir = $this->getUnsandboxDir();
|
||||||
$creds = $this->loadCredentialsFromCsv($unsandboxDir . '/accounts.csv', $accountIndex);
|
$creds = $this->loadCredentialsFromCsv($unsandboxDir . '/accounts.csv', 0);
|
||||||
if ($creds !== null) {
|
if ($creds !== null) {
|
||||||
return $creds;
|
return $creds;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 4: ./accounts.csv
|
$creds = $this->loadCredentialsFromCsv('./accounts.csv', 0);
|
||||||
$creds = $this->loadCredentialsFromCsv('./accounts.csv', $accountIndex);
|
|
||||||
if ($creds !== null) {
|
if ($creds !== null) {
|
||||||
return $creds;
|
return $creds;
|
||||||
}
|
}
|
||||||
|
|
@ -1744,9 +1767,9 @@ class Unsandbox {
|
||||||
throw new CredentialsException(
|
throw new CredentialsException(
|
||||||
"No credentials found. Please provide via:\n" .
|
"No credentials found. Please provide via:\n" .
|
||||||
" 1. Method arguments (publicKey, secretKey)\n" .
|
" 1. Method arguments (publicKey, secretKey)\n" .
|
||||||
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" .
|
" 2. --account N flag or UNSANDBOX_ACCOUNT env var (CSV row N)\n" .
|
||||||
" 3. ~/.unsandbox/accounts.csv\n" .
|
" 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" .
|
||||||
" 4. ./accounts.csv"
|
" 4. ~/.unsandbox/accounts.csv or ./accounts.csv (row 0)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2138,6 +2161,10 @@ class Unsandbox {
|
||||||
if (!empty($opts['secret_key'])) {
|
if (!empty($opts['secret_key'])) {
|
||||||
$this->defaultSecretKey = $opts['secret_key'];
|
$this->defaultSecretKey = $opts['secret_key'];
|
||||||
}
|
}
|
||||||
|
if ($opts['account'] !== null) {
|
||||||
|
$this->accountIndex = $opts['account'];
|
||||||
|
$this->accountIndexExplicit = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Determine the command
|
// Determine the command
|
||||||
if (empty($args)) {
|
if (empty($args)) {
|
||||||
|
|
@ -2209,6 +2236,7 @@ class Unsandbox {
|
||||||
'vcpu' => 1,
|
'vcpu' => 1,
|
||||||
'yes' => false,
|
'yes' => false,
|
||||||
'help' => false,
|
'help' => false,
|
||||||
|
'account' => null,
|
||||||
];
|
];
|
||||||
$args = [];
|
$args = [];
|
||||||
|
|
||||||
|
|
@ -2255,6 +2283,11 @@ class Unsandbox {
|
||||||
$opts['yes'] = true;
|
$opts['yes'] = true;
|
||||||
} elseif ($arg === '-h' || $arg === '--help') {
|
} elseif ($arg === '-h' || $arg === '--help') {
|
||||||
$opts['help'] = true;
|
$opts['help'] = true;
|
||||||
|
} elseif ($arg === '--account') {
|
||||||
|
$i++;
|
||||||
|
if (isset($argv[$i])) {
|
||||||
|
$opts['account'] = (int)$argv[$i];
|
||||||
|
}
|
||||||
} elseif (strpos($arg, '-') !== 0) {
|
} elseif (strpos($arg, '-') !== 0) {
|
||||||
$args[] = $arg;
|
$args[] = $arg;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
93
clients/php/sync/tests/test_account_flag.sh
Executable file
93
clients/php/sync/tests/test_account_flag.sh
Executable file
|
|
@ -0,0 +1,93 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Integration test for --account N credential selection in the PHP SDK CLI.
|
||||||
|
#
|
||||||
|
# Tests that --account N selects the correct row from accounts.csv,
|
||||||
|
# taking priority over UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY env vars.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
UN_PHP="${SCRIPT_DIR}/../src/un.php"
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
SKIP=0
|
||||||
|
|
||||||
|
pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
|
||||||
|
fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||||
|
skip() { echo "SKIP: $1"; SKIP=$((SKIP + 1)); }
|
||||||
|
|
||||||
|
# Require real credentials to run meaningful tests
|
||||||
|
if [ -z "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -z "${UNSANDBOX_SECRET_KEY:-}" ]; then
|
||||||
|
skip "UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY not set - cannot run account flag tests"
|
||||||
|
echo ""
|
||||||
|
echo "Results: ${PASS} passed, ${FAIL} failed, ${SKIP} skipped"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
REAL_PK="${UNSANDBOX_PUBLIC_KEY}"
|
||||||
|
REAL_SK="${UNSANDBOX_SECRET_KEY}"
|
||||||
|
GARBAGE_PK="unsb-pk-0000-0000-0000-garbage"
|
||||||
|
GARBAGE_SK="unsb-sk-00000-00000-00000-garbage"
|
||||||
|
|
||||||
|
# Create a temporary HOME with accounts.csv: row 0 = garbage, row 1 = real creds
|
||||||
|
TMPHOME="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "${TMPHOME}"' EXIT
|
||||||
|
|
||||||
|
mkdir -p "${TMPHOME}/.unsandbox"
|
||||||
|
printf '%s,%s\n' "${GARBAGE_PK}" "${GARBAGE_SK}" > "${TMPHOME}/.unsandbox/accounts.csv"
|
||||||
|
printf '%s,%s\n' "${REAL_PK}" "${REAL_SK}" >> "${TMPHOME}/.unsandbox/accounts.csv"
|
||||||
|
|
||||||
|
# Test 1: --account 1 with garbage env vars should load row 1 (real creds) and succeed
|
||||||
|
echo "Test 1: --account 1 ignores garbage env vars and uses CSV row 1 (real creds)"
|
||||||
|
output=$(HOME="${TMPHOME}" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="${GARBAGE_PK}" \
|
||||||
|
UNSANDBOX_SECRET_KEY="${GARBAGE_SK}" \
|
||||||
|
php "${UN_PHP}" --account 1 key 2>&1) && rc=0 || rc=$?
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
pass "Test 1: --account 1 succeeded with real creds from CSV row 1"
|
||||||
|
elif echo "${output}" | grep -qi "401\|unauthorized\|forbidden"; then
|
||||||
|
fail "Test 1: got auth error despite real creds at row 1 (output: ${output})"
|
||||||
|
else
|
||||||
|
fail "Test 1: unexpected failure (rc=${rc}, output: ${output})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 2: --account 0 with real env vars should load row 0 (garbage creds) and get 401
|
||||||
|
echo "Test 2: --account 0 overrides real env vars and uses CSV row 0 (garbage creds)"
|
||||||
|
output=$(HOME="${TMPHOME}" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="${REAL_PK}" \
|
||||||
|
UNSANDBOX_SECRET_KEY="${REAL_SK}" \
|
||||||
|
php "${UN_PHP}" --account 0 key 2>&1) && rc=0 || rc=$?
|
||||||
|
|
||||||
|
if echo "${output}" | grep -qi "401\|unauthorized\|forbidden\|authentication\|credentials"; then
|
||||||
|
pass "Test 2: got expected auth rejection for garbage creds at row 0"
|
||||||
|
elif [ $rc -eq 3 ]; then
|
||||||
|
# Exit code 3 = CredentialsException (e.g., empty key) - also acceptable
|
||||||
|
pass "Test 2: got credentials exception for garbage creds at row 0 (rc=3)"
|
||||||
|
else
|
||||||
|
fail "Test 2: expected 401/auth error but got rc=${rc}, output: ${output}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 3: No --account flag with real env vars should succeed (env vars take priority over CSV row 0)
|
||||||
|
echo "Test 3: no --account flag with real env vars should succeed"
|
||||||
|
output=$(HOME="${TMPHOME}" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="${REAL_PK}" \
|
||||||
|
UNSANDBOX_SECRET_KEY="${REAL_SK}" \
|
||||||
|
php "${UN_PHP}" key 2>&1) && rc=0 || rc=$?
|
||||||
|
|
||||||
|
if [ $rc -eq 0 ]; then
|
||||||
|
pass "Test 3: succeeded using env vars when no --account flag set"
|
||||||
|
elif echo "${output}" | grep -qi "401\|unauthorized\|forbidden"; then
|
||||||
|
fail "Test 3: got unexpected auth error with real env vars (output: ${output})"
|
||||||
|
else
|
||||||
|
fail "Test 3: unexpected failure (rc=${rc}, output: ${output})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Results: ${PASS} passed, ${FAIL} failed, ${SKIP} skipped"
|
||||||
|
|
||||||
|
if [ $FAIL -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
|
@ -95,42 +95,52 @@ def _resolve_credentials(
|
||||||
Resolve credentials from 4-tier priority system.
|
Resolve credentials from 4-tier priority system.
|
||||||
|
|
||||||
Priority:
|
Priority:
|
||||||
1. Function arguments
|
1. Function arguments (public_key, secret_key)
|
||||||
2. Environment variables
|
2. Explicit account_index (>= 0) -> load from accounts.csv
|
||||||
3. ~/.unsandbox/accounts.csv
|
3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
4. ./accounts.csv
|
4. Default CSV lookup (account 0 or UNSANDBOX_ACCOUNT env)
|
||||||
"""
|
"""
|
||||||
# Tier 1: Function arguments
|
# Tier 1: Function arguments
|
||||||
if public_key and secret_key:
|
if public_key and secret_key:
|
||||||
return (public_key, secret_key)
|
return (public_key, secret_key)
|
||||||
|
|
||||||
# Tier 2: Environment variables
|
# Tier 2: Explicit account_index overrides env vars
|
||||||
|
if account_index is not None and account_index >= 0:
|
||||||
|
unsandbox_dir = _get_unsandbox_dir()
|
||||||
|
creds = _load_credentials_from_csv(unsandbox_dir / "accounts.csv", account_index)
|
||||||
|
if creds:
|
||||||
|
return creds
|
||||||
|
creds = _load_credentials_from_csv(Path("accounts.csv"), account_index)
|
||||||
|
if creds:
|
||||||
|
return creds
|
||||||
|
raise CredentialsError(
|
||||||
|
f"No credentials found for account index {account_index} in accounts.csv"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tier 3: Environment variables
|
||||||
env_pk = os.environ.get("UNSANDBOX_PUBLIC_KEY")
|
env_pk = os.environ.get("UNSANDBOX_PUBLIC_KEY")
|
||||||
env_sk = os.environ.get("UNSANDBOX_SECRET_KEY")
|
env_sk = os.environ.get("UNSANDBOX_SECRET_KEY")
|
||||||
if env_pk and env_sk:
|
if env_pk and env_sk:
|
||||||
return (env_pk, env_sk)
|
return (env_pk, env_sk)
|
||||||
|
|
||||||
# Determine account index
|
# Tier 4: Default CSV lookup (UNSANDBOX_ACCOUNT env or index 0)
|
||||||
if account_index is None:
|
default_index = int(os.environ.get("UNSANDBOX_ACCOUNT", "0"))
|
||||||
account_index = int(os.environ.get("UNSANDBOX_ACCOUNT", "0"))
|
|
||||||
|
|
||||||
# Tier 3: ~/.unsandbox/accounts.csv
|
|
||||||
unsandbox_dir = _get_unsandbox_dir()
|
unsandbox_dir = _get_unsandbox_dir()
|
||||||
creds = _load_credentials_from_csv(unsandbox_dir / "accounts.csv", account_index)
|
creds = _load_credentials_from_csv(unsandbox_dir / "accounts.csv", default_index)
|
||||||
if creds:
|
if creds:
|
||||||
return creds
|
return creds
|
||||||
|
|
||||||
# Tier 4: ./accounts.csv
|
creds = _load_credentials_from_csv(Path("accounts.csv"), default_index)
|
||||||
creds = _load_credentials_from_csv(Path("accounts.csv"), account_index)
|
|
||||||
if creds:
|
if creds:
|
||||||
return creds
|
return creds
|
||||||
|
|
||||||
raise CredentialsError(
|
raise CredentialsError(
|
||||||
"No credentials found. Please provide via:\n"
|
"No credentials found. Please provide via:\n"
|
||||||
" 1. Function arguments (public_key, secret_key)\n"
|
" 1. Function arguments (public_key, secret_key)\n"
|
||||||
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n"
|
" 2. --account N (select row from accounts.csv)\n"
|
||||||
" 3. ~/.unsandbox/accounts.csv\n"
|
" 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n"
|
||||||
" 4. ./accounts.csv"
|
" 4. ~/.unsandbox/accounts.csv\n"
|
||||||
|
" 5. ./accounts.csv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2584,6 +2594,8 @@ Examples:
|
||||||
metavar="N", help="vCPU count (1-8, default: 1)")
|
metavar="N", help="vCPU count (1-8, default: 1)")
|
||||||
parser.add_argument("-y", "--yes", action="store_true",
|
parser.add_argument("-y", "--yes", action="store_true",
|
||||||
help="Skip confirmation prompts")
|
help="Skip confirmation prompts")
|
||||||
|
parser.add_argument("--account", type=int, metavar="N",
|
||||||
|
help="Select account row N from accounts.csv (overrides env vars)")
|
||||||
|
|
||||||
# Subcommands
|
# Subcommands
|
||||||
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||||
|
|
@ -2765,7 +2777,7 @@ def cli_main():
|
||||||
# Resolve credentials
|
# Resolve credentials
|
||||||
try:
|
try:
|
||||||
public_key, secret_key = _resolve_credentials(
|
public_key, secret_key = _resolve_credentials(
|
||||||
args.public_key, args.secret_key
|
args.public_key, args.secret_key, args.account
|
||||||
)
|
)
|
||||||
except CredentialsError as e:
|
except CredentialsError as e:
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
|
|
||||||
81
clients/python/sync/tests/test_account_flag.sh
Executable file
81
clients/python/sync/tests/test_account_flag.sh
Executable file
|
|
@ -0,0 +1,81 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Integration test: --account N flag must take priority over env vars
|
||||||
|
# Requires: UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
UN_PY="$SCRIPT_DIR/../src/un.py"
|
||||||
|
|
||||||
|
# Skip if credentials not set
|
||||||
|
if [[ -z "${UNSANDBOX_PUBLIC_KEY:-}" || -z "${UNSANDBOX_SECRET_KEY:-}" ]]; then
|
||||||
|
echo "SKIP: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
REAL_PK="$UNSANDBOX_PUBLIC_KEY"
|
||||||
|
REAL_SK="$UNSANDBOX_SECRET_KEY"
|
||||||
|
GARBAGE_PK="unsb-pk-0000-0000-0000-0000"
|
||||||
|
GARBAGE_SK="unsb-sk-00000-00000-00000-00000"
|
||||||
|
|
||||||
|
TMPHOME="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMPHOME"' EXIT
|
||||||
|
|
||||||
|
UNSANDBOX_DIR="$TMPHOME/.unsandbox"
|
||||||
|
mkdir -p "$UNSANDBOX_DIR"
|
||||||
|
chmod 700 "$UNSANDBOX_DIR"
|
||||||
|
|
||||||
|
# accounts.csv: row 0 = garbage, row 1 = real creds
|
||||||
|
cat > "$UNSANDBOX_DIR/accounts.csv" <<EOF
|
||||||
|
$GARBAGE_PK,$GARBAGE_SK
|
||||||
|
$REAL_PK,$REAL_SK
|
||||||
|
EOF
|
||||||
|
chmod 600 "$UNSANDBOX_DIR/accounts.csv"
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
run_test() {
|
||||||
|
local name="$1"
|
||||||
|
local expected_exit="$2"
|
||||||
|
shift 2
|
||||||
|
local output
|
||||||
|
local actual_exit=0
|
||||||
|
output="$(HOME="$TMPHOME" "$@" 2>&1)" || actual_exit=$?
|
||||||
|
if [[ "$actual_exit" -eq "$expected_exit" ]]; then
|
||||||
|
echo "PASS: $name"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo "FAIL: $name (expected exit $expected_exit, got $actual_exit)"
|
||||||
|
echo " output: $output"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test 1: --account 1 with garbage env vars -> should succeed (exit 0)
|
||||||
|
# --account 1 selects the real creds from row 1, ignoring garbage env vars
|
||||||
|
run_test \
|
||||||
|
"--account 1 overrides garbage env vars" \
|
||||||
|
0 \
|
||||||
|
env UNSANDBOX_PUBLIC_KEY="$GARBAGE_PK" UNSANDBOX_SECRET_KEY="$GARBAGE_SK" \
|
||||||
|
python3 "$UN_PY" --account 1 key
|
||||||
|
|
||||||
|
# Test 2: --account 0 with real env vars -> should fail auth (exit 3)
|
||||||
|
# --account 0 selects garbage creds from row 0, ignoring real env vars
|
||||||
|
run_test \
|
||||||
|
"--account 0 overrides real env vars (expects 401)" \
|
||||||
|
3 \
|
||||||
|
env UNSANDBOX_PUBLIC_KEY="$REAL_PK" UNSANDBOX_SECRET_KEY="$REAL_SK" \
|
||||||
|
python3 "$UN_PY" --account 0 key
|
||||||
|
|
||||||
|
# Test 3: no --account with real env vars -> should succeed (exit 0)
|
||||||
|
run_test \
|
||||||
|
"no --account uses env vars" \
|
||||||
|
0 \
|
||||||
|
env UNSANDBOX_PUBLIC_KEY="$REAL_PK" UNSANDBOX_SECRET_KEY="$REAL_SK" \
|
||||||
|
python3 "$UN_PY" key
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Results: $PASS passed, $FAIL failed"
|
||||||
|
if [[ "$FAIL" -gt 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -36,9 +36,10 @@
|
||||||
#
|
#
|
||||||
# Authentication Priority (4-tier):
|
# Authentication Priority (4-tier):
|
||||||
# 1. Method arguments (public_key:, secret_key:)
|
# 1. Method arguments (public_key:, secret_key:)
|
||||||
# 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
# 2. --account N flag (accounts.csv row N, overrides env vars)
|
||||||
# 3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
|
# 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
# 4. Local directory (./accounts.csv, line 0 by default)
|
# 4. Config file (~/.unsandbox/accounts.csv, line 0 by default)
|
||||||
|
# 5. Local directory (./accounts.csv, line 0 by default)
|
||||||
#
|
#
|
||||||
# Request Authentication (HMAC-SHA256):
|
# Request Authentication (HMAC-SHA256):
|
||||||
# Authorization: Bearer <public_key>
|
# Authorization: Bearer <public_key>
|
||||||
|
|
@ -1415,7 +1416,11 @@ module Un
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
attr_accessor :last_error_value
|
attr_accessor :last_error_value
|
||||||
|
# CLI-set account index (-1 means not specified). When >= 0, takes
|
||||||
|
# priority over env vars in resolve_credentials.
|
||||||
|
attr_accessor :cli_account_index
|
||||||
end
|
end
|
||||||
|
@cli_account_index = -1
|
||||||
|
|
||||||
# Get the SDK version string
|
# Get the SDK version string
|
||||||
#
|
#
|
||||||
|
|
@ -1562,41 +1567,53 @@ module Un
|
||||||
#
|
#
|
||||||
# Priority:
|
# Priority:
|
||||||
# 1. Method arguments
|
# 1. Method arguments
|
||||||
# 2. Environment variables
|
# 2. account_index >= 0 (explicit --account N flag) → CSV row N
|
||||||
# 3. ~/.unsandbox/accounts.csv
|
# 3. Environment variables
|
||||||
# 4. ./accounts.csv
|
# 4. Default CSV lookup (account 0 or UNSANDBOX_ACCOUNT env)
|
||||||
#
|
#
|
||||||
# @param public_key [String, nil] Explicit public key
|
# @param public_key [String, nil] Explicit public key
|
||||||
# @param secret_key [String, nil] Explicit secret key
|
# @param secret_key [String, nil] Explicit secret key
|
||||||
# @param account_index [Integer, nil] Account index for CSV files
|
# @param account_index [Integer, nil] Account index for CSV files (-1 means not specified)
|
||||||
# @return [Array<String>] [public_key, secret_key]
|
# @return [Array<String>] [public_key, secret_key]
|
||||||
# @raise [CredentialsError] If no credentials found
|
# @raise [CredentialsError] If no credentials found
|
||||||
def resolve_credentials(public_key = nil, secret_key = nil, account_index = nil)
|
def resolve_credentials(public_key = nil, secret_key = nil, account_index = nil)
|
||||||
# Tier 1: Method arguments
|
# Tier 1: Method arguments
|
||||||
return [public_key, secret_key] if public_key && secret_key
|
return [public_key, secret_key] if public_key && secret_key
|
||||||
|
|
||||||
# Tier 2: Environment variables
|
# Resolve effective account_index: parameter takes precedence, then CLI-set value
|
||||||
|
effective_index = account_index
|
||||||
|
effective_index = Un.cli_account_index if effective_index.nil?
|
||||||
|
|
||||||
|
# Tier 2: Explicit account_index (--account N) → load from CSV row N before env vars
|
||||||
|
if effective_index && effective_index >= 0
|
||||||
|
creds = load_credentials_from_csv(File.join(unsandbox_dir, 'accounts.csv'), effective_index)
|
||||||
|
return creds if creds
|
||||||
|
|
||||||
|
creds = load_credentials_from_csv('accounts.csv', effective_index)
|
||||||
|
return creds if creds
|
||||||
|
end
|
||||||
|
|
||||||
|
# Tier 3: Environment variables
|
||||||
env_pk = ENV['UNSANDBOX_PUBLIC_KEY']
|
env_pk = ENV['UNSANDBOX_PUBLIC_KEY']
|
||||||
env_sk = ENV['UNSANDBOX_SECRET_KEY']
|
env_sk = ENV['UNSANDBOX_SECRET_KEY']
|
||||||
return [env_pk, env_sk] if env_pk && env_sk
|
return [env_pk, env_sk] if env_pk && env_sk
|
||||||
|
|
||||||
# Determine account index
|
# Tier 4: Default CSV lookup (UNSANDBOX_ACCOUNT env or row 0)
|
||||||
account_index ||= ENV.fetch('UNSANDBOX_ACCOUNT', '0').to_i
|
default_index = ENV.fetch('UNSANDBOX_ACCOUNT', '0').to_i
|
||||||
|
|
||||||
# Tier 3: ~/.unsandbox/accounts.csv
|
creds = load_credentials_from_csv(File.join(unsandbox_dir, 'accounts.csv'), default_index)
|
||||||
creds = load_credentials_from_csv(File.join(unsandbox_dir, 'accounts.csv'), account_index)
|
|
||||||
return creds if creds
|
return creds if creds
|
||||||
|
|
||||||
# Tier 4: ./accounts.csv
|
creds = load_credentials_from_csv('accounts.csv', default_index)
|
||||||
creds = load_credentials_from_csv('accounts.csv', account_index)
|
|
||||||
return creds if creds
|
return creds if creds
|
||||||
|
|
||||||
raise CredentialsError, <<~MSG
|
raise CredentialsError, <<~MSG
|
||||||
No credentials found. Please provide via:
|
No credentials found. Please provide via:
|
||||||
1. Method arguments (public_key:, secret_key:)
|
1. Method arguments (public_key:, secret_key:)
|
||||||
2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
2. --account N flag (CSV row N)
|
||||||
3. ~/.unsandbox/accounts.csv
|
3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
4. ./accounts.csv
|
4. ~/.unsandbox/accounts.csv
|
||||||
|
5. ./accounts.csv
|
||||||
MSG
|
MSG
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1913,6 +1930,7 @@ module Un
|
||||||
file_paths: [],
|
file_paths: [],
|
||||||
public_key: nil,
|
public_key: nil,
|
||||||
secret_key: nil,
|
secret_key: nil,
|
||||||
|
account_index: -1,
|
||||||
network: 'zerotrust',
|
network: 'zerotrust',
|
||||||
vcpu: 1,
|
vcpu: 1,
|
||||||
yes: false,
|
yes: false,
|
||||||
|
|
@ -1920,6 +1938,19 @@ module Un
|
||||||
output: nil
|
output: nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Pre-scan for --account N before subcommand dispatch so it works in any position.
|
||||||
|
# The flag will also be consumed by parse_global_options inside each sub-handler.
|
||||||
|
ARGV.each_with_index do |arg, i|
|
||||||
|
if arg == '--account' && ARGV[i + 1] =~ /\A-?\d+\z/
|
||||||
|
options[:account_index] = ARGV[i + 1].to_i
|
||||||
|
break
|
||||||
|
elsif arg =~ /\A--account=(-?\d+)\z/
|
||||||
|
options[:account_index] = Regexp.last_match(1).to_i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Un.cli_account_index = options[:account_index]
|
||||||
|
|
||||||
# Check for subcommands first
|
# Check for subcommands first
|
||||||
if ARGV.empty?
|
if ARGV.empty?
|
||||||
cli_show_help
|
cli_show_help
|
||||||
|
|
@ -1990,6 +2021,7 @@ module Un
|
||||||
-o, --output DIR Output directory for artifacts
|
-o, --output DIR Output directory for artifacts
|
||||||
-p, --public-key KEY API public key
|
-p, --public-key KEY API public key
|
||||||
-k, --secret-key KEY API secret key
|
-k, --secret-key KEY API secret key
|
||||||
|
--account N Use accounts.csv row N (overrides env vars)
|
||||||
-n, --network MODE Network mode: zerotrust or semitrusted
|
-n, --network MODE Network mode: zerotrust or semitrusted
|
||||||
-v, --vcpu N vCPU count (1-8)
|
-v, --vcpu N vCPU count (1-8)
|
||||||
-y, --yes Skip confirmation prompts
|
-y, --yes Skip confirmation prompts
|
||||||
|
|
@ -2031,6 +2063,9 @@ module Un
|
||||||
opts.on('-k', '--secret-key KEY', 'API secret key') do |v|
|
opts.on('-k', '--secret-key KEY', 'API secret key') do |v|
|
||||||
options[:secret_key] = v
|
options[:secret_key] = v
|
||||||
end
|
end
|
||||||
|
opts.on('--account N', Integer, 'Use credentials from accounts.csv row N') do |v|
|
||||||
|
options[:account_index] = v
|
||||||
|
end
|
||||||
opts.on('-n', '--network MODE', 'Network mode') do |v|
|
opts.on('-n', '--network MODE', 'Network mode') do |v|
|
||||||
options[:network] = v
|
options[:network] = v
|
||||||
end
|
end
|
||||||
|
|
|
||||||
108
clients/ruby/sync/tests/test_account_flag.sh
Executable file
108
clients/ruby/sync/tests/test_account_flag.sh
Executable file
|
|
@ -0,0 +1,108 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Integration test: --account N flag must take priority over env vars
|
||||||
|
#
|
||||||
|
# Requires: UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY (real credentials)
|
||||||
|
# Run: bash tests/test_account_flag.sh
|
||||||
|
#
|
||||||
|
# The defect this guards against: resolve_credentials() checked env vars before
|
||||||
|
# account_index, so --account N was silently ignored when env vars existed.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
UN_RB="$SCRIPT_DIR/../src/un.rb"
|
||||||
|
|
||||||
|
RED='\033[31m'
|
||||||
|
GREEN='\033[32m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
check() {
|
||||||
|
local desc="$1" result="$2"
|
||||||
|
if [ "$result" = "pass" ]; then
|
||||||
|
printf " ${GREEN}✓${NC} %s\n" "$desc"
|
||||||
|
pass=$((pass + 1))
|
||||||
|
else
|
||||||
|
printf " ${RED}✗${NC} %s\n" "$desc"
|
||||||
|
fail=$((fail + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Require real credentials to be available
|
||||||
|
if [ -z "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -z "${UNSANDBOX_SECRET_KEY:-}" ]; then
|
||||||
|
echo "SKIP: UNSANDBOX_PUBLIC_KEY and UNSANDBOX_SECRET_KEY not set"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$UN_RB" ]; then
|
||||||
|
echo "FAIL: un.rb not found at $UN_RB"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REAL_PK="$UNSANDBOX_PUBLIC_KEY"
|
||||||
|
REAL_SK="$UNSANDBOX_SECRET_KEY"
|
||||||
|
|
||||||
|
# Temporary HOME with accounts.csv:
|
||||||
|
# index 0: garbage credentials (will always 401)
|
||||||
|
# index 1: real credentials (will succeed)
|
||||||
|
TMPHOME="$(mktemp -d)"
|
||||||
|
mkdir -p "$TMPHOME/.unsandbox"
|
||||||
|
trap 'rm -rf "$TMPHOME"' EXIT
|
||||||
|
|
||||||
|
cat > "$TMPHOME/.unsandbox/accounts.csv" <<CSV
|
||||||
|
unsb-pk-fake-0000-0000-0000,unsb-sk-fake0-00000-00000-00000
|
||||||
|
${REAL_PK},${REAL_SK}
|
||||||
|
CSV
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "INTEGRATION: --account flag priority test"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
# --- Test 1: --account 1 uses CSV row 1 (real creds), ignoring garbage env vars ---
|
||||||
|
OUT=$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY=unsb-pk-fake-0000-0000-0000 \
|
||||||
|
UNSANDBOX_SECRET_KEY=unsb-sk-fake0-00000-00000-00000 \
|
||||||
|
ruby "$UN_RB" --account 1 key 2>&1 || true)
|
||||||
|
|
||||||
|
if ! echo "$OUT" | grep -qi "401\|unauthorized\|invalid_credential\|No credentials found"; then
|
||||||
|
check "--account 1 uses CSV row 1 (real creds) over garbage env vars" "pass"
|
||||||
|
else
|
||||||
|
check "--account 1 uses CSV row 1 (real creds) over garbage env vars" "fail"
|
||||||
|
echo " output: $OUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Test 2: --account 0 uses CSV row 0 (garbage creds) → 401, even with real env vars ---
|
||||||
|
OUT=$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$REAL_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$REAL_SK" \
|
||||||
|
ruby "$UN_RB" --account 0 key 2>&1 || true)
|
||||||
|
|
||||||
|
if echo "$OUT" | grep -qi "401\|unauthorized\|invalid\|error\|Valid: false"; then
|
||||||
|
check "--account 0 uses CSV row 0 (garbage creds) despite real env vars, 401 as expected" "pass"
|
||||||
|
else
|
||||||
|
check "--account 0 uses CSV row 0 (garbage creds) despite real env vars, 401 as expected" "fail"
|
||||||
|
echo " output: $OUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Test 3: no --account flag, real env vars → env vars win over garbage CSV row 0 ---
|
||||||
|
OUT=$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$REAL_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$REAL_SK" \
|
||||||
|
ruby "$UN_RB" key 2>&1 || true)
|
||||||
|
|
||||||
|
if ! echo "$OUT" | grep -qi "401\|unauthorized\|invalid_credential\|No credentials found"; then
|
||||||
|
check "No --account flag: env vars used, succeeds" "pass"
|
||||||
|
else
|
||||||
|
check "No --account flag: env vars used, succeeds" "fail"
|
||||||
|
echo " output: $OUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
printf "Passed: ${GREEN}%d${NC} Failed: ${RED}%d${NC}\n" "$pass" "$fail"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
// use un::{Credentials, execute_code, resolve_credentials};
|
// use un::{Credentials, execute_code, resolve_credentials};
|
||||||
//
|
//
|
||||||
// // Resolve credentials (4-tier priority)
|
// // Resolve credentials (4-tier priority)
|
||||||
// let creds = resolve_credentials(None, None)?;
|
// let creds = resolve_credentials(None, None, None)?;
|
||||||
//
|
//
|
||||||
// // Execute code synchronously
|
// // Execute code synchronously
|
||||||
// let result = execute_code("python", r#"print("hello")"#, &creds)?;
|
// let result = execute_code("python", r#"print("hello")"#, &creds)?;
|
||||||
|
|
@ -702,13 +702,15 @@ fn load_credentials_from_csv(path: &PathBuf, account_index: usize) -> Option<Cre
|
||||||
///
|
///
|
||||||
/// # Priority
|
/// # Priority
|
||||||
/// 1. Function arguments (if both provided)
|
/// 1. Function arguments (if both provided)
|
||||||
/// 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
/// 2. account_index Some(n) → load from accounts.csv row n (before env vars)
|
||||||
/// 3. ~/.unsandbox/accounts.csv
|
/// 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||||
/// 4. ./accounts.csv
|
/// 4. ~/.unsandbox/accounts.csv (default row via UNSANDBOX_ACCOUNT or 0)
|
||||||
|
/// 5. ./accounts.csv (default row via UNSANDBOX_ACCOUNT or 0)
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `public_key` - Optional public key from function argument
|
/// * `public_key` - Optional public key from function argument
|
||||||
/// * `secret_key` - Optional secret key from function argument
|
/// * `secret_key` - Optional secret key from function argument
|
||||||
|
/// * `account_index` - Optional explicit account row (0-based) to select from CSV
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// Credentials if found, UnsandboxError::NoCredentials otherwise
|
/// Credentials if found, UnsandboxError::NoCredentials otherwise
|
||||||
|
|
@ -716,17 +718,22 @@ fn load_credentials_from_csv(path: &PathBuf, account_index: usize) -> Option<Cre
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// // Use environment variables or config file
|
/// // Use environment variables or config file
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
///
|
///
|
||||||
/// // Use explicit credentials
|
/// // Use explicit credentials
|
||||||
/// let creds = resolve_credentials(
|
/// let creds = resolve_credentials(
|
||||||
/// Some("unsb-pk-xxxx"),
|
/// Some("unsb-pk-xxxx"),
|
||||||
/// Some("unsb-sk-xxxx")
|
/// Some("unsb-sk-xxxx"),
|
||||||
|
/// None,
|
||||||
/// )?;
|
/// )?;
|
||||||
|
///
|
||||||
|
/// // Select account row 1 from CSV (overrides env vars)
|
||||||
|
/// let creds = resolve_credentials(None, None, Some(1))?;
|
||||||
/// ```
|
/// ```
|
||||||
pub fn resolve_credentials(
|
pub fn resolve_credentials(
|
||||||
public_key: Option<&str>,
|
public_key: Option<&str>,
|
||||||
secret_key: Option<&str>,
|
secret_key: Option<&str>,
|
||||||
|
account_index: Option<usize>,
|
||||||
) -> Result<Credentials> {
|
) -> Result<Credentials> {
|
||||||
// Tier 1: Function arguments
|
// Tier 1: Function arguments
|
||||||
if let (Some(pk), Some(sk)) = (public_key, secret_key) {
|
if let (Some(pk), Some(sk)) = (public_key, secret_key) {
|
||||||
|
|
@ -735,7 +742,21 @@ pub fn resolve_credentials(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 2: Environment variables
|
// Tier 2: Explicit account index → load from CSV before consulting env vars
|
||||||
|
if let Some(idx) = account_index {
|
||||||
|
if let Some(dir) = get_unsandbox_dir() {
|
||||||
|
let csv_path = dir.join("accounts.csv");
|
||||||
|
if let Some(creds) = load_credentials_from_csv(&csv_path, idx) {
|
||||||
|
return Ok(creds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let local_csv = PathBuf::from("accounts.csv");
|
||||||
|
if let Some(creds) = load_credentials_from_csv(&local_csv, idx) {
|
||||||
|
return Ok(creds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 3: Environment variables
|
||||||
let env_pk = env::var("UNSANDBOX_PUBLIC_KEY").ok();
|
let env_pk = env::var("UNSANDBOX_PUBLIC_KEY").ok();
|
||||||
let env_sk = env::var("UNSANDBOX_SECRET_KEY").ok();
|
let env_sk = env::var("UNSANDBOX_SECRET_KEY").ok();
|
||||||
if let (Some(pk), Some(sk)) = (env_pk, env_sk) {
|
if let (Some(pk), Some(sk)) = (env_pk, env_sk) {
|
||||||
|
|
@ -744,23 +765,23 @@ pub fn resolve_credentials(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine account index
|
// Determine default account index from env
|
||||||
let account_index: usize = env::var("UNSANDBOX_ACCOUNT")
|
let default_index: usize = env::var("UNSANDBOX_ACCOUNT")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| s.parse().ok())
|
.and_then(|s| s.parse().ok())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
// Tier 3: ~/.unsandbox/accounts.csv
|
// Tier 4: ~/.unsandbox/accounts.csv
|
||||||
if let Some(dir) = get_unsandbox_dir() {
|
if let Some(dir) = get_unsandbox_dir() {
|
||||||
let csv_path = dir.join("accounts.csv");
|
let csv_path = dir.join("accounts.csv");
|
||||||
if let Some(creds) = load_credentials_from_csv(&csv_path, account_index) {
|
if let Some(creds) = load_credentials_from_csv(&csv_path, default_index) {
|
||||||
return Ok(creds);
|
return Ok(creds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tier 4: ./accounts.csv
|
// Tier 5: ./accounts.csv
|
||||||
let local_csv = PathBuf::from("accounts.csv");
|
let local_csv = PathBuf::from("accounts.csv");
|
||||||
if let Some(creds) = load_credentials_from_csv(&local_csv, account_index) {
|
if let Some(creds) = load_credentials_from_csv(&local_csv, default_index) {
|
||||||
return Ok(creds);
|
return Ok(creds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1065,7 +1086,7 @@ fn save_languages_cache(languages: &[String]) {
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
/// let result = execute_code("python", r#"print("Hello, World!")"#, &creds)?;
|
/// let result = execute_code("python", r#"print("Hello, World!")"#, &creds)?;
|
||||||
/// println!("Output: {}", result.output);
|
/// println!("Output: {}", result.output);
|
||||||
/// println!("Exit code: {}", result.exit_code);
|
/// println!("Exit code: {}", result.exit_code);
|
||||||
|
|
@ -1422,7 +1443,7 @@ pub fn clone_snapshot(snapshot_id: &str, name: &str, creds: &Credentials) -> Res
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
/// let image = image_publish("service", "svc-abc123", "my-app-image", Some("Production app v1.0"), &creds)?;
|
/// let image = image_publish("service", "svc-abc123", "my-app-image", Some("Production app v1.0"), &creds)?;
|
||||||
/// println!("Published image: {}", image.image_id);
|
/// println!("Published image: {}", image.image_id);
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -1461,7 +1482,7 @@ pub fn image_publish(
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
///
|
///
|
||||||
/// // List owned images (default)
|
/// // List owned images (default)
|
||||||
/// let my_images = list_images(None, &creds)?;
|
/// let my_images = list_images(None, &creds)?;
|
||||||
|
|
@ -1550,7 +1571,7 @@ pub fn unlock_image(image_id: &str, creds: &Credentials) -> Result<LxdImage> {
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
///
|
///
|
||||||
/// // Make image public
|
/// // Make image public
|
||||||
/// set_image_visibility("img-abc123", "public", &creds)?;
|
/// set_image_visibility("img-abc123", "public", &creds)?;
|
||||||
|
|
@ -1665,7 +1686,7 @@ pub fn transfer_image(image_id: &str, to_api_key: &str, creds: &Credentials) ->
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
///
|
///
|
||||||
/// // Spawn with default options
|
/// // Spawn with default options
|
||||||
/// let result = spawn_from_image("img-abc123", "my-service", None, None, None, &creds)?;
|
/// let result = spawn_from_image("img-abc123", "my-service", None, None, None, &creds)?;
|
||||||
|
|
@ -1745,7 +1766,7 @@ pub fn clone_image(
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
/// let sessions = list_sessions(&creds)?;
|
/// let sessions = list_sessions(&creds)?;
|
||||||
/// for session in sessions {
|
/// for session in sessions {
|
||||||
/// println!("{}: {} ({})", session.session_id, session.container_name, session.status);
|
/// println!("{}: {} ({})", session.session_id, session.container_name, session.status);
|
||||||
|
|
@ -1781,7 +1802,7 @@ pub fn get_session(session_id: &str, creds: &Credentials) -> Result<Session> {
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
///
|
///
|
||||||
/// // Create a basic bash session
|
/// // Create a basic bash session
|
||||||
/// let session = create_session("bash", &creds, None)?;
|
/// let session = create_session("bash", &creds, None)?;
|
||||||
|
|
@ -1907,7 +1928,7 @@ pub fn unboost_session(session_id: &str, creds: &Credentials) -> Result<Session>
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
/// let result = shell_session("session-123", "ls -la", &creds)?;
|
/// let result = shell_session("session-123", "ls -la", &creds)?;
|
||||||
/// println!("Output: {}", result.output);
|
/// println!("Output: {}", result.output);
|
||||||
/// println!("Exit code: {}", result.exit_code);
|
/// println!("Exit code: {}", result.exit_code);
|
||||||
|
|
@ -1934,7 +1955,7 @@ pub fn shell_session(session_id: &str, command: &str, creds: &Credentials) -> Re
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
/// let services = list_services(&creds)?;
|
/// let services = list_services(&creds)?;
|
||||||
/// for service in services {
|
/// for service in services {
|
||||||
/// println!("{}: {} ({}) - {}", service.service_id, service.name, service.status, service.url);
|
/// println!("{}: {} ({}) - {}", service.service_id, service.name, service.status, service.url);
|
||||||
|
|
@ -1959,7 +1980,7 @@ pub fn list_services(creds: &Credentials) -> Result<Vec<Service>> {
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
///
|
///
|
||||||
/// // Create a simple web service
|
/// // Create a simple web service
|
||||||
/// let service = create_service(
|
/// let service = create_service(
|
||||||
|
|
@ -2317,7 +2338,7 @@ pub fn redeploy_service(
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
/// let service = resize_service("service-123", 4, &creds)?;
|
/// let service = resize_service("service-123", 4, &creds)?;
|
||||||
/// println!("Service now has {} vCPUs", service.vcpu);
|
/// println!("Service now has {} vCPUs", service.vcpu);
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -2341,7 +2362,7 @@ pub fn resize_service(service_id: &str, vcpu: u32, creds: &Credentials) -> Resul
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
/// let result = execute_in_service("service-123", "ls -la /app", &creds)?;
|
/// let result = execute_in_service("service-123", "ls -la /app", &creds)?;
|
||||||
/// println!("Output: {}", result.output);
|
/// println!("Output: {}", result.output);
|
||||||
/// ```
|
/// ```
|
||||||
|
|
@ -2372,7 +2393,7 @@ pub fn execute_in_service(
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
/// let result = validate_keys(&creds)?;
|
/// let result = validate_keys(&creds)?;
|
||||||
/// if result.valid {
|
/// if result.valid {
|
||||||
/// println!("Keys valid for account: {}", result.account_id);
|
/// println!("Keys valid for account: {}", result.account_id);
|
||||||
|
|
@ -2432,7 +2453,7 @@ pub fn validate_keys(creds: &Credentials) -> Result<KeysValid> {
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
/// let result = image("A sunset over mountains", &creds, None)?;
|
/// let result = image("A sunset over mountains", &creds, None)?;
|
||||||
/// for img in result.images {
|
/// for img in result.images {
|
||||||
/// println!("Image: {}", img);
|
/// println!("Image: {}", img);
|
||||||
|
|
@ -2502,7 +2523,7 @@ pub struct LogsEntry {
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
///
|
///
|
||||||
/// // Fetch last 100 lines from all sources
|
/// // Fetch last 100 lines from all sources
|
||||||
/// let opts = LogsFetchOptions {
|
/// let opts = LogsFetchOptions {
|
||||||
|
|
@ -2568,7 +2589,7 @@ pub type LogCallback = fn(source: &str, line: &str);
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let creds = resolve_credentials(None, None)?;
|
/// let creds = resolve_credentials(None, None, None)?;
|
||||||
///
|
///
|
||||||
/// fn handle_log(source: &str, line: &str) {
|
/// fn handle_log(source: &str, line: &str) {
|
||||||
/// println!("[{}] {}", source, line);
|
/// println!("[{}] {}", source, line);
|
||||||
|
|
@ -2766,6 +2787,7 @@ struct CliOptions {
|
||||||
output_dir: Option<String>, // -o, --output
|
output_dir: Option<String>, // -o, --output
|
||||||
public_key: Option<String>, // -p, --public-key
|
public_key: Option<String>, // -p, --public-key
|
||||||
secret_key: Option<String>, // -k, --secret-key
|
secret_key: Option<String>, // -k, --secret-key
|
||||||
|
account_index: Option<usize>, // --account N
|
||||||
network: Option<String>, // -n, --network
|
network: Option<String>, // -n, --network
|
||||||
vcpu: Option<u32>, // -v, --vcpu
|
vcpu: Option<u32>, // -v, --vcpu
|
||||||
yes: bool, // -y, --yes
|
yes: bool, // -y, --yes
|
||||||
|
|
@ -2867,6 +2889,7 @@ GLOBAL OPTIONS:
|
||||||
-o, --output DIR Output directory for artifacts
|
-o, --output DIR Output directory for artifacts
|
||||||
-p, --public-key KEY API public key
|
-p, --public-key KEY API public key
|
||||||
-k, --secret-key KEY API secret key
|
-k, --secret-key KEY API secret key
|
||||||
|
--account N Select account row N from accounts.csv (overrides env vars)
|
||||||
-n, --network MODE Network mode: zerotrust or semitrusted
|
-n, --network MODE Network mode: zerotrust or semitrusted
|
||||||
-v, --vcpu N vCPU count (1-8)
|
-v, --vcpu N vCPU count (1-8)
|
||||||
-y, --yes Skip confirmation prompts
|
-y, --yes Skip confirmation prompts
|
||||||
|
|
@ -3014,6 +3037,14 @@ fn parse_args(args: &[String]) -> CliOptions {
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"--account" => {
|
||||||
|
if i + 1 < args.len() {
|
||||||
|
opts.account_index = args[i + 1].parse().ok();
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
"-n" | "--network" => {
|
"-n" | "--network" => {
|
||||||
if i + 1 < args.len() {
|
if i + 1 < args.len() {
|
||||||
opts.network = Some(args[i + 1].clone());
|
opts.network = Some(args[i + 1].clone());
|
||||||
|
|
@ -3358,6 +3389,7 @@ fn get_credentials(opts: &CliOptions) -> Result<Credentials> {
|
||||||
resolve_credentials(
|
resolve_credentials(
|
||||||
opts.public_key.as_deref(),
|
opts.public_key.as_deref(),
|
||||||
opts.secret_key.as_deref(),
|
opts.secret_key.as_deref(),
|
||||||
|
opts.account_index,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
124
clients/rust/sync/tests/test_account_flag.sh
Executable file
124
clients/rust/sync/tests/test_account_flag.sh
Executable file
|
|
@ -0,0 +1,124 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Test --account N credential selection in the Rust sync SDK CLI.
|
||||||
|
#
|
||||||
|
# Tests:
|
||||||
|
# 1. --account 1 selects row 1 from accounts.csv (garbage env vars present)
|
||||||
|
# 2. --account 0 selects row 0 (garbage creds), returns 401
|
||||||
|
# 3. No --account flag falls through to env vars (real creds), succeeds
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||||
|
SDK_DIR="$SCRIPT_DIR/.."
|
||||||
|
BINARY="$SDK_DIR/target/debug/un"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Skip if real credentials are not available
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [ -z "${UNSANDBOX_PUBLIC_KEY:-}" ] || [ -z "${UNSANDBOX_SECRET_KEY:-}" ]; then
|
||||||
|
echo "SKIP: UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY not set"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
REAL_PK="$UNSANDBOX_PUBLIC_KEY"
|
||||||
|
REAL_SK="$UNSANDBOX_SECRET_KEY"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Build the binary if it doesn't already exist
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [ ! -f "$BINARY" ]; then
|
||||||
|
echo "Building Rust SDK..."
|
||||||
|
cd "$SDK_DIR"
|
||||||
|
PATH="$HOME/.cargo/bin:$PATH" cargo build 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$BINARY" ]; then
|
||||||
|
echo "FAIL: binary not found at $BINARY after build"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Set up a temporary HOME with accounts.csv
|
||||||
|
# row 0: garbage credentials
|
||||||
|
# row 1: real credentials
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
TMPHOME="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMPHOME"' EXIT
|
||||||
|
|
||||||
|
mkdir -p "$TMPHOME/.unsandbox"
|
||||||
|
printf 'unsb-pk-garbage-0000,unsb-sk-garbage-0000000000000000\n%s,%s\n' \
|
||||||
|
"$REAL_PK" "$REAL_SK" \
|
||||||
|
> "$TMPHOME/.unsandbox/accounts.csv"
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
run_key() {
|
||||||
|
# Returns the exit code and stdout/stderr of `un key`
|
||||||
|
HOME="$TMPHOME" "$BINARY" "$@" key 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 1: --account 1 picks real creds from row 1, even when env has garbage
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "Test 1: --account 1 selects row 1 (real creds) despite garbage env vars"
|
||||||
|
OUTPUT=$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="unsb-pk-garbage-env" \
|
||||||
|
UNSANDBOX_SECRET_KEY="unsb-sk-garbage-env-0000000000000" \
|
||||||
|
"$BINARY" --account 1 key 2>&1 || true)
|
||||||
|
|
||||||
|
if echo "$OUTPUT" | grep -qi "API keys valid"; then
|
||||||
|
echo " PASS"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL: expected 'API keys valid', got:"
|
||||||
|
echo "$OUTPUT" | sed 's/^/ /'
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 2: --account 0 picks garbage creds from row 0, should get 401
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "Test 2: --account 0 selects row 0 (garbage creds), expect auth failure"
|
||||||
|
OUTPUT=$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$REAL_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$REAL_SK" \
|
||||||
|
"$BINARY" --account 0 key 2>&1 || true)
|
||||||
|
|
||||||
|
if echo "$OUTPUT" | grep -qiE "401|unauthorized|invalid|invalid key|keys invalid"; then
|
||||||
|
echo " PASS"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL: expected auth failure (401/unauthorized/invalid), got:"
|
||||||
|
echo "$OUTPUT" | sed 's/^/ /'
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Test 3: No --account flag, env vars contain real creds → succeed
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "Test 3: no --account flag, env vars hold real creds"
|
||||||
|
OUTPUT=$(HOME="$TMPHOME" \
|
||||||
|
UNSANDBOX_PUBLIC_KEY="$REAL_PK" \
|
||||||
|
UNSANDBOX_SECRET_KEY="$REAL_SK" \
|
||||||
|
"$BINARY" key 2>&1 || true)
|
||||||
|
|
||||||
|
if echo "$OUTPUT" | grep -qi "API keys valid"; then
|
||||||
|
echo " PASS"
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL: expected 'API keys valid', got:"
|
||||||
|
echo "$OUTPUT" | sed 's/^/ /'
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "Results: $PASS passed, $FAIL failed"
|
||||||
|
if [ "$FAIL" -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
Loading…
Add table
Add a link
Reference in a new issue