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-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
|
||||
@echo ""
|
||||
@echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
|
|
|||
|
|
@ -396,7 +396,14 @@ static UnsandboxCredentials* get_credentials(const char *cli_pk, const char *cli
|
|||
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_sk = getenv("UNSANDBOX_SECRET_KEY");
|
||||
|
||||
|
|
@ -416,16 +423,12 @@ static UnsandboxCredentials* get_credentials(const char *cli_pk, const char *cli
|
|||
return creds;
|
||||
}
|
||||
|
||||
// Priority 3: Config file (~/.unsandbox/accounts.csv)
|
||||
// Use account_index from --account flag, or UNSANDBOX_ACCOUNT env var, or default to 0
|
||||
int csv_index = account_index;
|
||||
if (csv_index < 0) {
|
||||
const char *env_account = getenv("UNSANDBOX_ACCOUNT");
|
||||
if (env_account && strlen(env_account) > 0) {
|
||||
csv_index = atoi(env_account);
|
||||
} else {
|
||||
csv_index = 0;
|
||||
}
|
||||
// Priority 4: Config file (~/.unsandbox/accounts.csv)
|
||||
// Use UNSANDBOX_ACCOUNT env var, or default to account 0
|
||||
int csv_index = 0;
|
||||
const char *env_account = getenv("UNSANDBOX_ACCOUNT");
|
||||
if (env_account && strlen(env_account) > 0) {
|
||||
csv_index = atoi(env_account);
|
||||
}
|
||||
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.
|
||||
func NewClientFromEnv() (*Client, error) {
|
||||
creds, err := ResolveCredentials("", "")
|
||||
creds, err := ResolveCredentials("", "", -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,10 +161,12 @@ func loadCredentialsFromCsv(csvPath string, accountIndex int) *Credentials {
|
|||
//
|
||||
// Priority:
|
||||
// 1. Function arguments (publicKey, secretKey non-empty)
|
||||
// 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
// 3. ~/.unsandbox/accounts.csv
|
||||
// 4. ./accounts.csv
|
||||
func ResolveCredentials(publicKey, secretKey string) (*Credentials, error) {
|
||||
// 2. accountIndex >= 0 → load from accounts.csv row N (before env vars)
|
||||
// 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
// 4. Default CSV lookup (account 0 or UNSANDBOX_ACCOUNT env)
|
||||
//
|
||||
// Pass accountIndex = -1 to mean "not specified".
|
||||
func ResolveCredentials(publicKey, secretKey string, accountIndex int) (*Credentials, error) {
|
||||
// Tier 1: Function arguments
|
||||
if publicKey != "" && secretKey != "" {
|
||||
return &Credentials{
|
||||
|
|
@ -173,7 +175,23 @@ func ResolveCredentials(publicKey, secretKey string) (*Credentials, error) {
|
|||
}, 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")
|
||||
envSk := os.Getenv("UNSANDBOX_SECRET_KEY")
|
||||
if envPk != "" && envSk != "" {
|
||||
|
|
@ -183,35 +201,36 @@ func ResolveCredentials(publicKey, secretKey string) (*Credentials, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// Determine account index
|
||||
accountIndex := 0
|
||||
// Determine default account index from env
|
||||
defaultIndex := 0
|
||||
if envAccount := os.Getenv("UNSANDBOX_ACCOUNT"); envAccount != "" {
|
||||
var err error
|
||||
accountIndex, err = strconv.Atoi(envAccount)
|
||||
defaultIndex, err = strconv.Atoi(envAccount)
|
||||
if err != nil {
|
||||
accountIndex = 0
|
||||
defaultIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 3: ~/.unsandbox/accounts.csv
|
||||
// Tier 4: ~/.unsandbox/accounts.csv
|
||||
unsandboxDir, err := getUnsandboxDir()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 4: ./accounts.csv
|
||||
if creds := loadCredentialsFromCsv("accounts.csv", accountIndex); creds != nil {
|
||||
// Tier 5: ./accounts.csv
|
||||
if creds := loadCredentialsFromCsv("accounts.csv", defaultIndex); creds != nil {
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
return nil, &CredentialsError{
|
||||
Message: "No credentials found. Please provide via:\n" +
|
||||
" 1. Function arguments (publicKey, secretKey)\n" +
|
||||
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
|
||||
" 3. ~/.unsandbox/accounts.csv\n" +
|
||||
" 4. ./accounts.csv",
|
||||
" 2. --account N flag (CSV row N)\n" +
|
||||
" 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
|
||||
" 4. ~/.unsandbox/accounts.csv\n" +
|
||||
" 5. ./accounts.csv",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1647,18 +1666,19 @@ const (
|
|||
// CLIOptions holds parsed CLI arguments
|
||||
type CLIOptions struct {
|
||||
// Global options
|
||||
Shell string
|
||||
Env []string
|
||||
Files []string
|
||||
FilePaths []string
|
||||
Artifacts bool
|
||||
OutputDir string
|
||||
PublicKey string
|
||||
SecretKey string
|
||||
Network string
|
||||
VCPU int
|
||||
Yes bool
|
||||
Help bool
|
||||
Shell string
|
||||
Env []string
|
||||
Files []string
|
||||
FilePaths []string
|
||||
Artifacts bool
|
||||
OutputDir string
|
||||
PublicKey string
|
||||
SecretKey string
|
||||
Network string
|
||||
VCPU int
|
||||
Yes bool
|
||||
Help bool
|
||||
AccountIndex int // -1 means not specified
|
||||
|
||||
// Command
|
||||
Command string
|
||||
|
|
@ -3153,7 +3173,7 @@ func runLanguages(creds *Credentials, args []string) int {
|
|||
|
||||
// parseGlobalFlags parses global CLI options
|
||||
func parseGlobalFlags(args []string) (*CLIOptions, []string) {
|
||||
opts := &CLIOptions{}
|
||||
opts := &CLIOptions{AccountIndex: -1}
|
||||
remaining := []string{}
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
|
|
@ -3196,6 +3216,13 @@ func parseGlobalFlags(args []string) (*CLIOptions, []string) {
|
|||
opts.SecretKey = args[i+1]
|
||||
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":
|
||||
if i+1 < len(args) {
|
||||
opts.Network = args[i+1]
|
||||
|
|
@ -3249,7 +3276,7 @@ func CliMain() {
|
|||
}
|
||||
|
||||
// Resolve credentials
|
||||
creds, err := ResolveCredentials(opts.PublicKey, opts.SecretKey)
|
||||
creds, err := ResolveCredentials(opts.PublicKey, opts.SecretKey, opts.AccountIndex)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
||||
os.Exit(ExitAuthError)
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ func TestResolveCredentialsFromEnv(t *testing.T) {
|
|||
os.Setenv("UNSANDBOX_SECRET_KEY", testSK)
|
||||
|
||||
// Test
|
||||
creds, err := ResolveCredentials("", "")
|
||||
creds, err := ResolveCredentials("", "", -1)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveCredentials failed: %v", err)
|
||||
}
|
||||
|
|
@ -201,7 +201,7 @@ func TestResolveCredentialsFromArgs(t *testing.T) {
|
|||
testPK := "unsb-pk-arg1-arg2-arg3-arg4"
|
||||
testSK := "unsb-sk-arg11-arg22-arg33-arg44"
|
||||
|
||||
creds, err := ResolveCredentials(testPK, testSK)
|
||||
creds, err := ResolveCredentials(testPK, testSK, -1)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveCredentials failed: %v", err)
|
||||
}
|
||||
|
|
@ -218,7 +218,7 @@ func TestResolveCredentialsFromArgs(t *testing.T) {
|
|||
// ============================================================================
|
||||
|
||||
func getTestCredentials(t *testing.T) *Credentials {
|
||||
creds, err := ResolveCredentials("", "")
|
||||
creds, err := ResolveCredentials("", "", -1)
|
||||
if err != nil {
|
||||
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)
|
||||
|
||||
// Test
|
||||
creds, err := ResolveCredentials("", "")
|
||||
creds, err := ResolveCredentials("", "", -1)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveCredentials failed: %v", err)
|
||||
}
|
||||
|
|
@ -201,7 +201,7 @@ func TestResolveCredentialsFromArgs(t *testing.T) {
|
|||
testPK := "unsb-pk-arg1-arg2-arg3-arg4"
|
||||
testSK := "unsb-sk-arg11-arg22-arg33-arg44"
|
||||
|
||||
creds, err := ResolveCredentials(testPK, testSK)
|
||||
creds, err := ResolveCredentials(testPK, testSK, -1)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveCredentials failed: %v", err)
|
||||
}
|
||||
|
|
@ -218,7 +218,7 @@ func TestResolveCredentialsFromArgs(t *testing.T) {
|
|||
// ============================================================================
|
||||
|
||||
func getTestCredentials(t *testing.T) *Credentials {
|
||||
creds, err := ResolveCredentials("", "")
|
||||
creds, err := ResolveCredentials("", "", -1)
|
||||
if err != nil {
|
||||
t.Skip("No credentials available for functional tests")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,11 +25,12 @@
|
|||
* // Snapshot operations
|
||||
* String snapshotId = Un.sessionSnapshot(sessionId, publicKey, secretKey, "my-snapshot", false);
|
||||
*
|
||||
* Authentication Priority (4-tier):
|
||||
* Authentication Priority (5-tier):
|
||||
* 1. Method arguments (publicKey, secretKey)
|
||||
* 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
* 3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
|
||||
* 4. Local directory (./accounts.csv, line 0 by default)
|
||||
* 2. --account N flag / accountIndex >= 0 (load row N from accounts.csv)
|
||||
* 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
* 4. Config file (~/.unsandbox/accounts.csv, line 0 by default)
|
||||
* 5. Local directory (./accounts.csv, line 0 by default)
|
||||
*
|
||||
* Request Authentication (HMAC-SHA256):
|
||||
* Authorization: Bearer <public_key>
|
||||
|
|
@ -175,38 +176,58 @@ public class Un {
|
|||
}
|
||||
|
||||
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
|
||||
if (publicKey != null && !publicKey.isEmpty() && secretKey != null && !secretKey.isEmpty()) {
|
||||
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 envSk = System.getenv("UNSANDBOX_SECRET_KEY");
|
||||
if (envPk != null && !envPk.isEmpty() && envSk != null && !envSk.isEmpty()) {
|
||||
return new String[]{envPk, envSk};
|
||||
}
|
||||
|
||||
// Determine account index
|
||||
int accountIndex = 0;
|
||||
// Determine account index from env (default 0)
|
||||
int csvIndex = 0;
|
||||
String accountEnv = System.getenv("UNSANDBOX_ACCOUNT");
|
||||
if (accountEnv != null && !accountEnv.isEmpty()) {
|
||||
try {
|
||||
accountIndex = Integer.parseInt(accountEnv);
|
||||
csvIndex = Integer.parseInt(accountEnv);
|
||||
} catch (NumberFormatException e) {
|
||||
// Use default
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 3: ~/.unsandbox/accounts.csv
|
||||
// Tier 4: ~/.unsandbox/accounts.csv
|
||||
Path unsandboxDir = getUnsandboxDir();
|
||||
String[] creds = loadCredentialsFromCsv(unsandboxDir.resolve("accounts.csv"), accountIndex);
|
||||
String[] creds = loadCredentialsFromCsv(unsandboxDir.resolve("accounts.csv"), csvIndex);
|
||||
if (creds != null) {
|
||||
return creds;
|
||||
}
|
||||
|
||||
// Tier 4: ./accounts.csv
|
||||
creds = loadCredentialsFromCsv(Paths.get("accounts.csv"), accountIndex);
|
||||
// Tier 5: ./accounts.csv
|
||||
creds = loadCredentialsFromCsv(Paths.get("accounts.csv"), csvIndex);
|
||||
if (creds != null) {
|
||||
return creds;
|
||||
}
|
||||
|
|
@ -214,9 +235,10 @@ public class Un {
|
|||
throw new CredentialsException(
|
||||
"No credentials found. Please provide via:\n" +
|
||||
" 1. Method arguments (publicKey, secretKey)\n" +
|
||||
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
|
||||
" 3. ~/.unsandbox/accounts.csv\n" +
|
||||
" 4. ./accounts.csv"
|
||||
" 2. --account N flag (load row N from accounts.csv)\n" +
|
||||
" 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" +
|
||||
" 4. ~/.unsandbox/accounts.csv\n" +
|
||||
" 5. ./accounts.csv"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2860,6 +2882,7 @@ public class Un {
|
|||
String language = null;
|
||||
String networkMode = "zerotrust";
|
||||
int vcpu = 1;
|
||||
int accountIndex = -1;
|
||||
List<String> envVars = new ArrayList<>();
|
||||
List<String> files = new ArrayList<>();
|
||||
List<String> positionalArgs = new ArrayList<>();
|
||||
|
|
@ -2871,6 +2894,18 @@ public class Un {
|
|||
if (arg.equals("-h") || arg.equals("--help")) {
|
||||
showHelp = true;
|
||||
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")) {
|
||||
if (i + 1 >= args.length) {
|
||||
System.err.println("Error: -s/--shell requires an argument");
|
||||
|
|
@ -2936,6 +2971,14 @@ public class Un {
|
|||
|
||||
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
|
||||
switch (command) {
|
||||
case "session":
|
||||
|
|
@ -2982,6 +3025,7 @@ public class Un {
|
|||
System.out.println(" -f, --file FILE Add input file to /tmp/");
|
||||
System.out.println(" -p, --public-key KEY API public 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(" -v, --vcpu N vCPU count (1-8)");
|
||||
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:
|
||||
* 1. Function arguments
|
||||
* 2. Environment variables (Node.js)
|
||||
* 3. localStorage (Browser)
|
||||
* 4. ~/.unsandbox/accounts.csv (Node.js)
|
||||
* 5. ./accounts.csv (Node.js)
|
||||
* 2. accountIndex >= 0 → load from accounts.csv row N
|
||||
* 3. Environment variables (Node.js)
|
||||
* 4. localStorage (Browser)
|
||||
* 5. ~/.unsandbox/accounts.csv (default row, Node.js)
|
||||
* 6. ./accounts.csv (default row, Node.js)
|
||||
*/
|
||||
function resolveCredentials(publicKey, secretKey, accountIndex) {
|
||||
// Tier 1: Function arguments
|
||||
|
|
@ -342,7 +343,26 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
|
|||
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) {
|
||||
const envPk = process.env.UNSANDBOX_PUBLIC_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) {
|
||||
const storageCreds = loadCredentialsFromStorage();
|
||||
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) {
|
||||
// Determine account index
|
||||
if (accountIndex === undefined) {
|
||||
accountIndex = parseInt(process.env.UNSANDBOX_ACCOUNT || '0', 10);
|
||||
}
|
||||
const defaultIndex = parseInt(process.env.UNSANDBOX_ACCOUNT || '0', 10);
|
||||
|
||||
// Tier 4: ~/.unsandbox/accounts.csv
|
||||
// Tier 5: ~/.unsandbox/accounts.csv
|
||||
try {
|
||||
const unsandboxDir = getUnsandboxDir();
|
||||
const creds = loadCredentialsFromCsv(path.join(unsandboxDir, 'accounts.csv'), accountIndex);
|
||||
const creds = loadCredentialsFromCsv(path.join(unsandboxDir, 'accounts.csv'), defaultIndex);
|
||||
if (creds) {
|
||||
return creds;
|
||||
}
|
||||
|
|
@ -377,8 +394,8 @@ function resolveCredentials(publicKey, secretKey, accountIndex) {
|
|||
// Continue to next tier
|
||||
}
|
||||
|
||||
// Tier 5: ./accounts.csv
|
||||
const creds = loadCredentialsFromCsv('accounts.csv', accountIndex);
|
||||
// Tier 6: ./accounts.csv
|
||||
const creds = loadCredentialsFromCsv('accounts.csv', defaultIndex);
|
||||
if (creds) {
|
||||
return creds;
|
||||
}
|
||||
|
|
@ -2193,6 +2210,7 @@ function parseArgs(args) {
|
|||
output: null,
|
||||
publicKey: null,
|
||||
secretKey: null,
|
||||
accountIndex: undefined,
|
||||
network: 'zerotrust',
|
||||
vcpu: 1,
|
||||
yes: false,
|
||||
|
|
@ -2322,6 +2340,9 @@ function parseArgs(args) {
|
|||
} else if (arg === '-k' || arg === '--secret-key') {
|
||||
result.secretKey = args[++i];
|
||||
i++;
|
||||
} else if (arg === '--account') {
|
||||
result.accountIndex = parseInt(args[++i], 10);
|
||||
i++;
|
||||
} else if (arg === '-n' || arg === '--network') {
|
||||
result.network = args[++i];
|
||||
i++;
|
||||
|
|
@ -2509,8 +2530,7 @@ function formatTable(items, columns) {
|
|||
* Handle session commands.
|
||||
*/
|
||||
async function handleSession(opts) {
|
||||
const pk = opts.publicKey;
|
||||
const sk = opts.secretKey;
|
||||
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||
|
||||
// List sessions
|
||||
if (opts.list) {
|
||||
|
|
@ -2593,8 +2613,7 @@ async function handleSession(opts) {
|
|||
* Handle service commands.
|
||||
*/
|
||||
async function handleService(opts) {
|
||||
const pk = opts.publicKey;
|
||||
const sk = opts.secretKey;
|
||||
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||
|
||||
// Handle env subcommand
|
||||
if (opts.subcommand === 'env') {
|
||||
|
|
@ -2829,8 +2848,7 @@ async function handleService(opts) {
|
|||
* Handle snapshot commands.
|
||||
*/
|
||||
async function handleSnapshot(opts) {
|
||||
const pk = opts.publicKey;
|
||||
const sk = opts.secretKey;
|
||||
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||
|
||||
// List snapshots
|
||||
if (opts.list) {
|
||||
|
|
@ -2898,8 +2916,7 @@ async function handleSnapshot(opts) {
|
|||
* Handle image command.
|
||||
*/
|
||||
async function handleImage(opts) {
|
||||
const pk = opts.publicKey;
|
||||
const sk = opts.secretKey;
|
||||
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||
|
||||
// List images
|
||||
if (opts.list) {
|
||||
|
|
@ -3002,13 +3019,13 @@ async function handleImage(opts) {
|
|||
* Handle key command.
|
||||
*/
|
||||
async function handleKey(opts) {
|
||||
const [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||
try {
|
||||
const result = await validateKeys(opts.publicKey, opts.secretKey);
|
||||
const result = await validateKeys(pk, sk);
|
||||
console.log('API Key Status:');
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (err) {
|
||||
// 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('Key validation endpoint returned error - key may still be valid.');
|
||||
}
|
||||
|
|
@ -3018,7 +3035,8 @@ async function handleKey(opts) {
|
|||
* Handle languages command.
|
||||
*/
|
||||
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) {
|
||||
// Output as JSON array
|
||||
|
|
@ -3035,8 +3053,7 @@ async function handleLanguages(opts) {
|
|||
* Handle execute command (default).
|
||||
*/
|
||||
async function handleExecute(opts) {
|
||||
const pk = opts.publicKey;
|
||||
const sk = opts.secretKey;
|
||||
let [pk, sk] = resolveCredentials(opts.publicKey, opts.secretKey, opts.accountIndex);
|
||||
|
||||
let code;
|
||||
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 $defaultSecretKey = null;
|
||||
private int $accountIndex = 0;
|
||||
private bool $accountIndexExplicit = false;
|
||||
|
||||
/**
|
||||
* Create a new Unsandbox client.
|
||||
|
|
@ -1693,17 +1694,18 @@ class Unsandbox {
|
|||
* Resolve credentials from 4-tier priority system.
|
||||
*
|
||||
* Priority:
|
||||
* 1. Method arguments
|
||||
* 2. Environment variables
|
||||
* 3. ~/.unsandbox/accounts.csv
|
||||
* 4. ./accounts.csv
|
||||
* 1. Method arguments / constructor defaults
|
||||
* 2. $accountIndex >= 0 → load from accounts.csv row N
|
||||
* 3. Environment variables (UNSANDBOX_PUBLIC_KEY / UNSANDBOX_SECRET_KEY)
|
||||
* 4. Default CSV lookup (account 0)
|
||||
*
|
||||
* @param string|null $publicKey Public 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]
|
||||
* @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
|
||||
if (!empty($publicKey) && !empty($secretKey)) {
|
||||
return [$publicKey, $secretKey];
|
||||
|
|
@ -1714,29 +1716,50 @@ class Unsandbox {
|
|||
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');
|
||||
$envSk = getenv('UNSANDBOX_SECRET_KEY');
|
||||
if (!empty($envPk) && !empty($envSk)) {
|
||||
return [$envPk, $envSk];
|
||||
}
|
||||
|
||||
// Determine account index
|
||||
$accountIndex = $this->accountIndex;
|
||||
$envAccount = getenv('UNSANDBOX_ACCOUNT');
|
||||
if ($envAccount !== false && $envAccount !== '') {
|
||||
$accountIndex = (int)$envAccount;
|
||||
}
|
||||
|
||||
// Tier 3: ~/.unsandbox/accounts.csv
|
||||
// Tier 4: Default CSV lookup (account 0)
|
||||
$unsandboxDir = $this->getUnsandboxDir();
|
||||
$creds = $this->loadCredentialsFromCsv($unsandboxDir . '/accounts.csv', $accountIndex);
|
||||
$creds = $this->loadCredentialsFromCsv($unsandboxDir . '/accounts.csv', 0);
|
||||
if ($creds !== null) {
|
||||
return $creds;
|
||||
}
|
||||
|
||||
// Tier 4: ./accounts.csv
|
||||
$creds = $this->loadCredentialsFromCsv('./accounts.csv', $accountIndex);
|
||||
$creds = $this->loadCredentialsFromCsv('./accounts.csv', 0);
|
||||
if ($creds !== null) {
|
||||
return $creds;
|
||||
}
|
||||
|
|
@ -1744,9 +1767,9 @@ class Unsandbox {
|
|||
throw new CredentialsException(
|
||||
"No credentials found. Please provide via:\n" .
|
||||
" 1. Method arguments (publicKey, secretKey)\n" .
|
||||
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" .
|
||||
" 3. ~/.unsandbox/accounts.csv\n" .
|
||||
" 4. ./accounts.csv"
|
||||
" 2. --account N flag or UNSANDBOX_ACCOUNT env var (CSV row N)\n" .
|
||||
" 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n" .
|
||||
" 4. ~/.unsandbox/accounts.csv or ./accounts.csv (row 0)"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2138,6 +2161,10 @@ class Unsandbox {
|
|||
if (!empty($opts['secret_key'])) {
|
||||
$this->defaultSecretKey = $opts['secret_key'];
|
||||
}
|
||||
if ($opts['account'] !== null) {
|
||||
$this->accountIndex = $opts['account'];
|
||||
$this->accountIndexExplicit = true;
|
||||
}
|
||||
|
||||
// Determine the command
|
||||
if (empty($args)) {
|
||||
|
|
@ -2209,6 +2236,7 @@ class Unsandbox {
|
|||
'vcpu' => 1,
|
||||
'yes' => false,
|
||||
'help' => false,
|
||||
'account' => null,
|
||||
];
|
||||
$args = [];
|
||||
|
||||
|
|
@ -2255,6 +2283,11 @@ class Unsandbox {
|
|||
$opts['yes'] = true;
|
||||
} elseif ($arg === '-h' || $arg === '--help') {
|
||||
$opts['help'] = true;
|
||||
} elseif ($arg === '--account') {
|
||||
$i++;
|
||||
if (isset($argv[$i])) {
|
||||
$opts['account'] = (int)$argv[$i];
|
||||
}
|
||||
} elseif (strpos($arg, '-') !== 0) {
|
||||
$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.
|
||||
|
||||
Priority:
|
||||
1. Function arguments
|
||||
2. Environment variables
|
||||
3. ~/.unsandbox/accounts.csv
|
||||
4. ./accounts.csv
|
||||
1. Function arguments (public_key, secret_key)
|
||||
2. Explicit account_index (>= 0) -> load from accounts.csv
|
||||
3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
4. Default CSV lookup (account 0 or UNSANDBOX_ACCOUNT env)
|
||||
"""
|
||||
# Tier 1: Function arguments
|
||||
if public_key and 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_sk = os.environ.get("UNSANDBOX_SECRET_KEY")
|
||||
if env_pk and env_sk:
|
||||
return (env_pk, env_sk)
|
||||
|
||||
# Determine account index
|
||||
if account_index is None:
|
||||
account_index = int(os.environ.get("UNSANDBOX_ACCOUNT", "0"))
|
||||
|
||||
# Tier 3: ~/.unsandbox/accounts.csv
|
||||
# Tier 4: Default CSV lookup (UNSANDBOX_ACCOUNT env or index 0)
|
||||
default_index = int(os.environ.get("UNSANDBOX_ACCOUNT", "0"))
|
||||
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:
|
||||
return creds
|
||||
|
||||
# Tier 4: ./accounts.csv
|
||||
creds = _load_credentials_from_csv(Path("accounts.csv"), account_index)
|
||||
creds = _load_credentials_from_csv(Path("accounts.csv"), default_index)
|
||||
if creds:
|
||||
return creds
|
||||
|
||||
raise CredentialsError(
|
||||
"No credentials found. Please provide via:\n"
|
||||
" 1. Function arguments (public_key, secret_key)\n"
|
||||
" 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n"
|
||||
" 3. ~/.unsandbox/accounts.csv\n"
|
||||
" 4. ./accounts.csv"
|
||||
" 2. --account N (select row from accounts.csv)\n"
|
||||
" 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)\n"
|
||||
" 4. ~/.unsandbox/accounts.csv\n"
|
||||
" 5. ./accounts.csv"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -2584,6 +2594,8 @@ Examples:
|
|||
metavar="N", help="vCPU count (1-8, default: 1)")
|
||||
parser.add_argument("-y", "--yes", action="store_true",
|
||||
help="Skip confirmation prompts")
|
||||
parser.add_argument("--account", type=int, metavar="N",
|
||||
help="Select account row N from accounts.csv (overrides env vars)")
|
||||
|
||||
# Subcommands
|
||||
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
||||
|
|
@ -2765,7 +2777,7 @@ def cli_main():
|
|||
# Resolve credentials
|
||||
try:
|
||||
public_key, secret_key = _resolve_credentials(
|
||||
args.public_key, args.secret_key
|
||||
args.public_key, args.secret_key, args.account
|
||||
)
|
||||
except CredentialsError as e:
|
||||
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):
|
||||
# 1. Method arguments (public_key:, secret_key:)
|
||||
# 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
# 3. Config file (~/.unsandbox/accounts.csv, line 0 by default)
|
||||
# 4. Local directory (./accounts.csv, line 0 by default)
|
||||
# 2. --account N flag (accounts.csv row N, overrides env vars)
|
||||
# 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
# 4. Config file (~/.unsandbox/accounts.csv, line 0 by default)
|
||||
# 5. Local directory (./accounts.csv, line 0 by default)
|
||||
#
|
||||
# Request Authentication (HMAC-SHA256):
|
||||
# Authorization: Bearer <public_key>
|
||||
|
|
@ -1415,7 +1416,11 @@ module Un
|
|||
|
||||
class << self
|
||||
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
|
||||
@cli_account_index = -1
|
||||
|
||||
# Get the SDK version string
|
||||
#
|
||||
|
|
@ -1562,41 +1567,53 @@ module Un
|
|||
#
|
||||
# Priority:
|
||||
# 1. Method arguments
|
||||
# 2. Environment variables
|
||||
# 3. ~/.unsandbox/accounts.csv
|
||||
# 4. ./accounts.csv
|
||||
# 2. account_index >= 0 (explicit --account N flag) → CSV row N
|
||||
# 3. Environment variables
|
||||
# 4. Default CSV lookup (account 0 or UNSANDBOX_ACCOUNT env)
|
||||
#
|
||||
# @param public_key [String, nil] Explicit public 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]
|
||||
# @raise [CredentialsError] If no credentials found
|
||||
def resolve_credentials(public_key = nil, secret_key = nil, account_index = nil)
|
||||
# Tier 1: Method arguments
|
||||
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_sk = ENV['UNSANDBOX_SECRET_KEY']
|
||||
return [env_pk, env_sk] if env_pk && env_sk
|
||||
|
||||
# Determine account index
|
||||
account_index ||= ENV.fetch('UNSANDBOX_ACCOUNT', '0').to_i
|
||||
# Tier 4: Default CSV lookup (UNSANDBOX_ACCOUNT env or row 0)
|
||||
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'), account_index)
|
||||
creds = load_credentials_from_csv(File.join(unsandbox_dir, 'accounts.csv'), default_index)
|
||||
return creds if creds
|
||||
|
||||
# Tier 4: ./accounts.csv
|
||||
creds = load_credentials_from_csv('accounts.csv', account_index)
|
||||
creds = load_credentials_from_csv('accounts.csv', default_index)
|
||||
return creds if creds
|
||||
|
||||
raise CredentialsError, <<~MSG
|
||||
No credentials found. Please provide via:
|
||||
1. Method arguments (public_key:, secret_key:)
|
||||
2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
3. ~/.unsandbox/accounts.csv
|
||||
4. ./accounts.csv
|
||||
2. --account N flag (CSV row N)
|
||||
3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
4. ~/.unsandbox/accounts.csv
|
||||
5. ./accounts.csv
|
||||
MSG
|
||||
end
|
||||
|
||||
|
|
@ -1913,6 +1930,7 @@ module Un
|
|||
file_paths: [],
|
||||
public_key: nil,
|
||||
secret_key: nil,
|
||||
account_index: -1,
|
||||
network: 'zerotrust',
|
||||
vcpu: 1,
|
||||
yes: false,
|
||||
|
|
@ -1920,6 +1938,19 @@ module Un
|
|||
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
|
||||
if ARGV.empty?
|
||||
cli_show_help
|
||||
|
|
@ -1990,6 +2021,7 @@ module Un
|
|||
-o, --output DIR Output directory for artifacts
|
||||
-p, --public-key KEY API public 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
|
||||
-v, --vcpu N vCPU count (1-8)
|
||||
-y, --yes Skip confirmation prompts
|
||||
|
|
@ -2031,6 +2063,9 @@ module Un
|
|||
opts.on('-k', '--secret-key KEY', 'API secret key') do |v|
|
||||
options[:secret_key] = v
|
||||
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|
|
||||
options[:network] = v
|
||||
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};
|
||||
//
|
||||
// // Resolve credentials (4-tier priority)
|
||||
// let creds = resolve_credentials(None, None)?;
|
||||
// let creds = resolve_credentials(None, None, None)?;
|
||||
//
|
||||
// // Execute code synchronously
|
||||
// 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
|
||||
/// 1. Function arguments (if both provided)
|
||||
/// 2. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
/// 3. ~/.unsandbox/accounts.csv
|
||||
/// 4. ./accounts.csv
|
||||
/// 2. account_index Some(n) → load from accounts.csv row n (before env vars)
|
||||
/// 3. Environment variables (UNSANDBOX_PUBLIC_KEY, UNSANDBOX_SECRET_KEY)
|
||||
/// 4. ~/.unsandbox/accounts.csv (default row via UNSANDBOX_ACCOUNT or 0)
|
||||
/// 5. ./accounts.csv (default row via UNSANDBOX_ACCOUNT or 0)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `public_key` - Optional public 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
|
||||
/// Credentials if found, UnsandboxError::NoCredentials otherwise
|
||||
|
|
@ -716,17 +718,22 @@ fn load_credentials_from_csv(path: &PathBuf, account_index: usize) -> Option<Cre
|
|||
/// # Examples
|
||||
/// ```ignore
|
||||
/// // Use environment variables or config file
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
///
|
||||
/// // Use explicit credentials
|
||||
/// let creds = resolve_credentials(
|
||||
/// 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(
|
||||
public_key: Option<&str>,
|
||||
secret_key: Option<&str>,
|
||||
account_index: Option<usize>,
|
||||
) -> Result<Credentials> {
|
||||
// Tier 1: Function arguments
|
||||
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_sk = env::var("UNSANDBOX_SECRET_KEY").ok();
|
||||
if let (Some(pk), Some(sk)) = (env_pk, env_sk) {
|
||||
|
|
@ -744,23 +765,23 @@ pub fn resolve_credentials(
|
|||
}
|
||||
}
|
||||
|
||||
// Determine account index
|
||||
let account_index: usize = env::var("UNSANDBOX_ACCOUNT")
|
||||
// Determine default account index from env
|
||||
let default_index: usize = env::var("UNSANDBOX_ACCOUNT")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// Tier 3: ~/.unsandbox/accounts.csv
|
||||
// Tier 4: ~/.unsandbox/accounts.csv
|
||||
if let Some(dir) = get_unsandbox_dir() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 4: ./accounts.csv
|
||||
// Tier 5: ./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);
|
||||
}
|
||||
|
||||
|
|
@ -1065,7 +1086,7 @@ fn save_languages_cache(languages: &[String]) {
|
|||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
/// let result = execute_code("python", r#"print("Hello, World!")"#, &creds)?;
|
||||
/// println!("Output: {}", result.output);
|
||||
/// println!("Exit code: {}", result.exit_code);
|
||||
|
|
@ -1422,7 +1443,7 @@ pub fn clone_snapshot(snapshot_id: &str, name: &str, creds: &Credentials) -> Res
|
|||
///
|
||||
/// # Examples
|
||||
/// ```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)?;
|
||||
/// println!("Published image: {}", image.image_id);
|
||||
/// ```
|
||||
|
|
@ -1461,7 +1482,7 @@ pub fn image_publish(
|
|||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
///
|
||||
/// // List owned images (default)
|
||||
/// let my_images = list_images(None, &creds)?;
|
||||
|
|
@ -1550,7 +1571,7 @@ pub fn unlock_image(image_id: &str, creds: &Credentials) -> Result<LxdImage> {
|
|||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
///
|
||||
/// // Make image public
|
||||
/// 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
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
///
|
||||
/// // Spawn with default options
|
||||
/// let result = spawn_from_image("img-abc123", "my-service", None, None, None, &creds)?;
|
||||
|
|
@ -1745,7 +1766,7 @@ pub fn clone_image(
|
|||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
/// let sessions = list_sessions(&creds)?;
|
||||
/// for session in sessions {
|
||||
/// 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
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
///
|
||||
/// // Create a basic bash session
|
||||
/// let session = create_session("bash", &creds, None)?;
|
||||
|
|
@ -1907,7 +1928,7 @@ pub fn unboost_session(session_id: &str, creds: &Credentials) -> Result<Session>
|
|||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
/// let result = shell_session("session-123", "ls -la", &creds)?;
|
||||
/// println!("Output: {}", result.output);
|
||||
/// println!("Exit code: {}", result.exit_code);
|
||||
|
|
@ -1934,7 +1955,7 @@ pub fn shell_session(session_id: &str, command: &str, creds: &Credentials) -> Re
|
|||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
/// let services = list_services(&creds)?;
|
||||
/// for service in services {
|
||||
/// println!("{}: {} ({}) - {}", service.service_id, service.name, service.status, service.url);
|
||||
|
|
@ -1959,7 +1980,7 @@ pub fn list_services(creds: &Credentials) -> Result<Vec<Service>> {
|
|||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
///
|
||||
/// // Create a simple web service
|
||||
/// let service = create_service(
|
||||
|
|
@ -2317,7 +2338,7 @@ pub fn redeploy_service(
|
|||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
/// let service = resize_service("service-123", 4, &creds)?;
|
||||
/// println!("Service now has {} vCPUs", service.vcpu);
|
||||
/// ```
|
||||
|
|
@ -2341,7 +2362,7 @@ pub fn resize_service(service_id: &str, vcpu: u32, creds: &Credentials) -> Resul
|
|||
///
|
||||
/// # Examples
|
||||
/// ```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)?;
|
||||
/// println!("Output: {}", result.output);
|
||||
/// ```
|
||||
|
|
@ -2372,7 +2393,7 @@ pub fn execute_in_service(
|
|||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
/// let result = validate_keys(&creds)?;
|
||||
/// if result.valid {
|
||||
/// println!("Keys valid for account: {}", result.account_id);
|
||||
|
|
@ -2432,7 +2453,7 @@ pub fn validate_keys(creds: &Credentials) -> Result<KeysValid> {
|
|||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
/// let result = image("A sunset over mountains", &creds, None)?;
|
||||
/// for img in result.images {
|
||||
/// println!("Image: {}", img);
|
||||
|
|
@ -2502,7 +2523,7 @@ pub struct LogsEntry {
|
|||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
///
|
||||
/// // Fetch last 100 lines from all sources
|
||||
/// let opts = LogsFetchOptions {
|
||||
|
|
@ -2568,7 +2589,7 @@ pub type LogCallback = fn(source: &str, line: &str);
|
|||
///
|
||||
/// # Examples
|
||||
/// ```ignore
|
||||
/// let creds = resolve_credentials(None, None)?;
|
||||
/// let creds = resolve_credentials(None, None, None)?;
|
||||
///
|
||||
/// fn handle_log(source: &str, line: &str) {
|
||||
/// println!("[{}] {}", source, line);
|
||||
|
|
@ -2766,6 +2787,7 @@ struct CliOptions {
|
|||
output_dir: Option<String>, // -o, --output
|
||||
public_key: Option<String>, // -p, --public-key
|
||||
secret_key: Option<String>, // -k, --secret-key
|
||||
account_index: Option<usize>, // --account N
|
||||
network: Option<String>, // -n, --network
|
||||
vcpu: Option<u32>, // -v, --vcpu
|
||||
yes: bool, // -y, --yes
|
||||
|
|
@ -2867,6 +2889,7 @@ GLOBAL OPTIONS:
|
|||
-o, --output DIR Output directory for artifacts
|
||||
-p, --public-key KEY API public 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
|
||||
-v, --vcpu N vCPU count (1-8)
|
||||
-y, --yes Skip confirmation prompts
|
||||
|
|
@ -3014,6 +3037,14 @@ fn parse_args(args: &[String]) -> CliOptions {
|
|||
i += 1;
|
||||
}
|
||||
}
|
||||
"--account" => {
|
||||
if i + 1 < args.len() {
|
||||
opts.account_index = args[i + 1].parse().ok();
|
||||
i += 2;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"-n" | "--network" => {
|
||||
if i + 1 < args.len() {
|
||||
opts.network = Some(args[i + 1].clone());
|
||||
|
|
@ -3358,6 +3389,7 @@ fn get_credentials(opts: &CliOptions) -> Result<Credentials> {
|
|||
resolve_credentials(
|
||||
opts.public_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