Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions capture_tests/cbrain_cli_commands
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ cbrain --json version
cbrain whoami
cbrain --json whoami

# Named session management
cbrain session list

# Bourreaux
cbrain remote-resource list
cbrain --json remote-resource list
Expand Down Expand Up @@ -120,6 +123,7 @@ cbrain task operation hold # missing --task-id / --batch-id

# ToolConfigs, as admin user
./switch_session admin
cbrain session list
cbrain tool-config list
cbrain --json tool-config list
cbrain --jsonl tool-config list
Expand All @@ -128,8 +132,9 @@ cbrain tool-config show 19 # not visible to normal user norm
cbrain --json tool-config show 19
cbrain --jsonl tool-config show 19

# ToolConfigs, as normal user
./switch_session norm
# ToolConfigs, as normal user (CLI switch_session; both sessions already planted)
cbrain switch_session norm
cbrain session list
cbrain tool-config list
cbrain --json tool-config list
cbrain --jsonl tool-config list
Expand Down
59 changes: 54 additions & 5 deletions capture_tests/expected_captures.txt
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,21 @@ Stdout:
Stderr:
(No output)

############################
Command: cbrain session list
Status: 0
Stdout: 337 bytes
Stderr: 0 bytes

Stdout:
# SESSION USERNAME USER ID SERVER TIMESTAMP
------------------------------------------------------------------------------------------
*1 norm norm 2 http://localhost:3000 2222-22-22T44:44

Active session: norm (* = active)
Stderr:
(No output)

############################
Command: cbrain remote-resource list
Status: 0
Expand Down Expand Up @@ -1073,6 +1088,22 @@ Stdout:
Stderr:
(No output)

############################
Command: cbrain session list
Status: 0
Stdout: 448 bytes
Stderr: 0 bytes

Stdout:
# SESSION USERNAME USER ID SERVER TIMESTAMP
------------------------------------------------------------------------------------------
1 norm norm 2 http://localhost:3000 2222-22-22T44:44
*2 admin admin 1 http://localhost:3000 2222-22-22T44:44

Active session: admin (* = active)
Stderr:
(No output)

############################
Command: cbrain tool-config list
Status: 0
Expand Down Expand Up @@ -1179,13 +1210,29 @@ Stderr:
(No output)

############################
Command: ./switch_session norm
Command: cbrain switch_session norm
Status: 0
Stdout: 0 bytes
Stdout: 71 bytes
Stderr: 0 bytes

Stdout:
Switched to session 'norm'. All future commands will use this session.
Stderr:
(No output)

############################
Command: cbrain session list
Status: 0
Stdout: 447 bytes
Stderr: 0 bytes

Stdout:
# SESSION USERNAME USER ID SERVER TIMESTAMP
------------------------------------------------------------------------------------------
*1 norm norm 2 http://localhost:3000 2222-22-22T44:44
2 admin admin 1 http://localhost:3000 2222-22-22T44:44

Active session: norm (* = active)
Stderr:
(No output)

Expand Down Expand Up @@ -1720,12 +1767,14 @@ Stderr:
############################
Command: cbrain logout
Status: 0
Stdout: 116 bytes
Stdout: 266 bytes
Stderr: 0 bytes

Stdout:
Successfully logged out from CBRAIN server.
Local session removed from /home/runner/.config/cbrain/credentials.json
Successfully logged out from CBRAIN server as norm.
Local session 'norm' removed from /home/runner/.config/cbrain/credentials.json.
Successfully logged out from CBRAIN server as admin.
Local session 'admin' removed from /home/runner/.config/cbrain/credentials.json.
Stderr:
(No output)

Expand Down
43 changes: 39 additions & 4 deletions capture_tests/switch_session
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ DEL_TOKEN="0123456789abcdefffffffffffffffff";

# cbrain client JSON file with credentials to update
cbrain_cred_file=$HOME/.config/cbrain/credentials.json
mkdir -p "$(dirname "$cbrain_cred_file")"

# Timestamp as expected by the cbrain client program
timestamp=$(date +"%Y-%m-%dT%H:%M:%S")
Expand All @@ -27,8 +28,15 @@ timestamp=$(date +"%Y-%m-%dT%H:%M:%S")
# thus logging out the cbrain command.
testses="$1"

# Stdout from now on goes into the JSON
exec 1> $cbrain_cred_file
# Keep prior named sessions so planting admin after norm does not wipe norm.
existing="{}"
if test -s "$cbrain_cred_file" ; then
existing=$(cat "$cbrain_cred_file")
fi

# Stdout from now on goes into a temp JSON blob for this session only
tmp_cred=$(mktemp)
exec 1> "$tmp_cred"

# Session for normal user
if test "X$1" = "Xnorm" ; then
Expand All @@ -37,6 +45,7 @@ if test "X$1" = "Xnorm" ; then
"cbrain_url": "http://localhost:3000",
"api_token": "$NORMAL_TOKEN",
"user_id": 2,
"username": "norm",
"timestamp": "$timestamp"
}
CREDJSON
Expand All @@ -49,6 +58,7 @@ if test "X$1" = "Xadmin" ; then
"cbrain_url": "http://localhost:3000",
"api_token": "$ADMIN_TOKEN",
"user_id": 1,
"username": "admin",
"timestamp": "$timestamp"
}
CREDJSON
Expand All @@ -61,6 +71,7 @@ if test "X$1" = "Xnormdel" ; then
"cbrain_url": "http://localhost:3000",
"api_token": "$DEL_TOKEN",
"user_id": 2,
"username": "norm",
"timestamp": "$timestamp"
}
CREDJSON
Expand All @@ -70,6 +81,30 @@ fi
exec 1>& -

# Remove outright if it's empty
if ! test -s "$cbrain_cred_file" ; then
rm -f "$cbrain_cred_file"
if ! test -s "$tmp_cred" ; then
rm -f "$tmp_cred" "$cbrain_cred_file"
exit 0
fi

# Merge this session into the named credentials map (multi-session).
python3 - "$cbrain_cred_file" "$testses" "$existing" "$tmp_cred" <<'PY'
import json, sys

path, name, existing_raw, new_path = sys.argv[1:5]
with open(new_path) as f:
new = json.load(f)
try:
data = json.loads(existing_raw) if existing_raw.strip() else {}
except json.JSONDecodeError:
data = {}
# Promote legacy flat file to named map.
if "api_token" in data or "cbrain_url" in data:
if not any(isinstance(v, dict) and "api_token" in v for k, v in data.items() if k != "_active_session"):
data = {"default": {k: v for k, v in data.items() if k != "_active_session"}}
data[name] = new
data["_active_session"] = name
with open(path, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
PY
rm -f "$tmp_cred"
34 changes: 32 additions & 2 deletions cbrain_cli/cli_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,38 @@
from pathlib import Path

from cbrain_cli import config as cbrain_config
from cbrain_cli.config import DEFAULT_HEADERS, DEFAULT_TIMEOUT, auth_headers
from cbrain_cli.config import (
ACTIVE_SESSION_KEY,
DEFAULT_HEADERS,
DEFAULT_TIMEOUT,
auth_headers,
resolve_session_credentials,
)

_debug = False

# Session name priority: --session flag > _active_session in credentials > "default"
session_name = "default"
session_specified = False
for i, arg in enumerate(sys.argv):
if arg == "--session" and i + 1 < len(sys.argv):
session_name = sys.argv[i + 1]
session_specified = True
elif arg.startswith("--session="):
session_name = arg.split("=", 1)[1]
session_specified = True

_all = cbrain_config.load_credentials() or {}
if not session_specified:
session_name = _all.get(ACTIVE_SESSION_KEY, "default") or "default"
all_credentials = dict(_all)
all_credentials.pop(ACTIVE_SESSION_KEY, None)
_resolved = resolve_session_credentials(_all, session_name if session_specified else None)
cbrain_url = _resolved.get("cbrain_url")
api_token = _resolved.get("api_token")
user_id = _resolved.get("user_id")
cbrain_timestamp = _resolved.get("timestamp")


def set_debug(flag: bool) -> None:
"""Enable or disable debug output."""
Expand Down Expand Up @@ -44,7 +72,9 @@ def from_credentials(cls, timeout=None):
"""
Build a client from the saved credentials file.
"""
creds = cbrain_config.load_credentials() or {}
all_creds = cbrain_config.load_credentials() or {}
name = session_name if session_specified else None
creds = resolve_session_credentials(all_creds, name)
return cls(
creds.get("cbrain_url", ""),
creds.get("api_token", ""),
Expand Down
60 changes: 60 additions & 0 deletions cbrain_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
except ValueError:
DEFAULT_TIMEOUT = 30

# Key used inside credentials.json to track the currently active session.
# Prefixed with "_" so it is clearly not a session name.
ACTIVE_SESSION_KEY = "_active_session"

# HTTP headers.
DEFAULT_HEADERS = {
"Content-Type": "application/x-www-form-urlencoded",
Expand Down Expand Up @@ -56,6 +60,62 @@ def load_credentials():
return None


def is_flat_credentials(data):
"""True when file is single-session (api_token at top level), not named map."""
if not isinstance(data, dict) or not data:
return False
if "api_token" in data or "cbrain_url" in data:
return not any(
k != ACTIVE_SESSION_KEY
and isinstance(v, dict)
and ("api_token" in v or "cbrain_url" in v)
for k, v in data.items()
)
return False


def get_named_sessions(data):
"""Return {name: creds} for flat or multi-session files."""
if not data:
return {}
if is_flat_credentials(data):
return {"default": {k: v for k, v in data.items() if k != ACTIVE_SESSION_KEY}}
return {k: v for k, v in data.items() if k != ACTIVE_SESSION_KEY and isinstance(v, dict)}


def resolve_session_credentials(data, session_name=None):
"""Pick active session dict from flat or multi-session credentials file."""
if not data:
return {}
if is_flat_credentials(data):
return {k: v for k, v in data.items() if k != ACTIVE_SESSION_KEY}
name = session_name or data.get(ACTIVE_SESSION_KEY) or "default"
entry = data.get(name)
return dict(entry) if isinstance(entry, dict) else {}


def update_active_credentials(updates=None, remove_keys=None, session_name=None):
"""Patch fields on the active (or named) session; supports flat + nested files."""
data = load_credentials()
if data is None:
return
updates = updates or {}
remove_keys = remove_keys or []
if is_flat_credentials(data):
data.update(updates)
for k in remove_keys:
data.pop(k, None)
save_credentials(data)
return
name = session_name or data.get(ACTIVE_SESSION_KEY) or "default"
entry = dict(data.get(name) or {})
entry.update(updates)
for k in remove_keys:
entry.pop(k, None)
data[name] = entry
save_credentials(data)


def save_credentials(credentials):
"""
Save credentials to the session file.
Expand Down
Loading
Loading