Skip to main content

Manage authentication accounts

Start with the local API client example. These commands use its api-demo working directory, central configuration, auth-db node and WATERWALL_REPO checkout path. The AuthenticationServer management API reference is authoritative for request fields, response schemas, limits and error codes. The examples below show a workflow rather than a second schema reference. For commands that construct these requests for you, use the administration CLI. Both clients use the same TCP module.

Grant each operator its own permissions​

The example monitor can read status, users and lists. Creation, configuration editing and password changes each require a separate explicit permission. Stop the demo WaterWall process cleanly, then add three independent operator tokens to its configuration:

umask 077
python3 - <<'PY'
import json
from pathlib import Path
import secrets

path = Path("config.json")
config = json.loads(path.read_text(encoding="utf-8"))
api = next(node for node in config["nodes"] if node["name"] == "management-api")
roles = {
"creator": ["get-user", "create-user"],
"editor": ["get-user", "update-user"],
"password-admin": ["get-user", "change-user-password"],
}
existing = {client["name"] for client in api["settings"]["clients"]}
if existing.intersection(roles):
raise SystemExit("These example roles already exist; keep their existing tokens.")
for name, operations in roles.items():
token = secrets.token_hex(32)
Path(name + "-token").write_text(token + "\n", encoding="ascii")
api["settings"]["clients"].append({"name": name, "token": token, "allow": [{
"config": "central", "node": "auth-db", "operations": operations
}]})
path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
PY

Restart WaterWall with the same core.json. These static permissions are loaded at startup. API tokens are independent of traffic-user passwords and producer credentials; producer allow-user-write grants no API permission. A server restart invalidates old edit tokens and cursors, so take fresh reads afterward.

Read accounts and pages​

For an existing account, write lookup.json containing {"id":"42"} and run:

python3 "$WATERWALL_REPO/tunnels/APIServer/examples/api_client.py" \
--client monitor --token-file monitor-token \
--config central --node auth-db --operation get-user \
--body-file lookup.json > account.json

An empty database returns not_found; create the example account below first. Account IDs remain strings even when they exceed JavaScript's exact integer range. get-user returns permitted account details and a current edit_token, with credentials redacted. Server usage includes accepted reports; an offline AuthenticationClient may still have usage to report.

For the first list page, write list.json containing {"limit":50}:

python3 "$WATERWALL_REPO/tunnels/APIServer/examples/api_client.py" \
--client monitor --token-file monitor-token \
--config central --node auth-db --operation list-users \
--body-file list.json > page.json

Inspect page.json. If next_cursor is not null, prepare the next request using that exact opaque value:

import json
from pathlib import Path

page = json.loads(Path("page.json").read_text(encoding="utf-8"))
if page["next_cursor"] is None:
raise SystemExit("Traversal complete.")
Path("list.json").write_text(json.dumps({"limit": 50, "cursor": page["next_cursor"]}), encoding="utf-8")

Repeat the list command for that page. Configuration changes make earlier cursors stale: on stale_cursor, restart with {"limit":50}. Statistics-only updates leave the cursor usable, so each page can contain newer usage totals. The list is not a frozen usage snapshot. Summaries may abbreviate names; use get-user for full permitted details. busy is a bounded sampling refusal, not an empty list. Check the command's exit status before reading its output as a successful account or page; the client never retries automatically.

Create an account​

Choose a permanent unique numeric ID and initial password. In a private editor, create create.json with this example shape, replacing the password in the file:

{"id":"42","password":"replace-in-this-private-file","config":{"name":"alice","enabled":false}}

Submit once:

python3 "$WATERWALL_REPO/tunnels/APIServer/examples/api_client.py" \
--client creator --token-file creator-token \
--config central --node auth-db --operation create-user \
--body-file create.json

Creation needs no edit token. The explicit enabled:false stages the account: omitted or empty config instead creates an enabled account with the default unlimited/unset limits and expiry. Consult create-user before choosing its policy. An existing ID returns id_exists and is never replaced.

After a lost reply or post-dispatch timeout, retain the original ID and inputs and read that ID. Existence does not prove who created it or that its redacted password matches. If absent, explicitly decide whether to retry the same ID and inputs while the earlier request may still run. Do not automatically choose a new ID, overwrite the account, reset its password or interpret id_exists as proof of an earlier success.

Edit limits or disable an account​

Read the account with the editor's own token:

python3 "$WATERWALL_REPO/tunnels/APIServer/examples/api_client.py" \
--client editor --token-file editor-token \
--config central --node auth-db --operation get-user \
--body-file lookup.json > account.json

Review the result. Build a sparse patch from its sampled token; do not submit the complete returned account as a patch:

import json
from pathlib import Path

account = json.loads(Path("account.json").read_text(encoding="utf-8"))
request = {"id": account["user"]["id"], "if_match": account["edit_token"],
"patch": {"enabled": True, "limit": {"traffic": {"total": "1000000000"}}}}
Path("update.json").write_text(json.dumps(request), encoding="utf-8")
python3 "$WATERWALL_REPO/tunnels/APIServer/examples/api_client.py" \
--client editor --token-file editor-token \
--config central --node auth-db --operation update-user \
--body-file update.json

To disable the account later, repeat the fresh read and use "patch": {"enabled": false} instead. Omitted fields remain unchanged. This operation preserves usage, first-use history and credentials. The update-user reference lists editable fields and their units; storing a limit does not make every traffic node enforce it.

An edit token covers the server instance and server-wide configuration revision. Another account's edit, creation or password change can invalidate it. A node conflict means this invocation did not commit: reread and reconsider the patch. A lost reply, invalid reply or late gateway timeout leaves the outcome unknown. Reread and compare intended values, including normalized values where relevant, before deciding whether another patch is appropriate. Never automatically fetch a new token and replay the old patch.

Change an account password​

The password administrator first obtains a current token with its own permission:

python3 "$WATERWALL_REPO/tunnels/APIServer/examples/api_client.py" \
--client password-admin --token-file password-admin-token \
--config central --node auth-db --operation get-user \
--body-file lookup.json > account.json

In a private editor create password-change.json with exactly id, the returned edit_token as if_match, and the desired password. Keep the chosen secret in that file rather than command arguments or shell history:

{"id":"42","if_match":"copy-the-current-edit-token-here","password":"replace-in-this-private-file"}
python3 "$WATERWALL_REPO/tunnels/APIServer/examples/api_client.py" \
--client password-admin --token-file password-admin-token \
--config central --node auth-db --operation change-user-password \
--body-file password-change.json

The operation changes credentials together while preserving the account ID, configuration, usage, first-use history and runtime reservations. Exact password bytes matter. UUID-shaped passwords have additional identity semantics; read change-user-password before changing an account used by UUID-based protocols. Assigning the same password still requires a current token and advances the configuration revision.

After an uncertain reply, retain the intended password securely and reread for a current token. Public reads cannot prove which password is installed: neither account existence nor an advanced revision proves that this request succeeded. Explicitly decide whether to resubmit the desired password with the fresh token. A successful resubmission establishes that password at its commit point. A conflict requires another read and decision. The client does not refresh tokens, retry writes or generate a replacement password automatically; the server keeps no request retry records.

What a successful write confirms​

The compact acknowledgement contains id, the transaction's config_revision and its edit_token. It confirms an in-memory commit, not a disk save or client refresh. Periodic/final saving and normal synchronization continue separately; crash recovery is outside this API workflow.

Disconnected AuthenticationClients keep cached users, credentials and policy until a valid refresh. A password change does not force existing connections to disconnect, and changing policy does not guarantee immediate enforcement on an offline client. Traffic accounting continues through ordinary synchronization.

A returned node-operation failure means that invocation did not commit. It says nothing about an earlier request whose reply was lost. Request IDs only correlate responses. See the protocol's error and delivery rules when integrating these workflows into another administration program.