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:
russell@unturf.com 2026-03-23 14:56:00 -04:00
parent 8093386568
commit ed6c52b001
20 changed files with 1243 additions and 174 deletions

View file

@ -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");

View 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