diff --git a/scripts/newuser.sh b/scripts/newuser.sh index 7009172..140ef23 100755 --- a/scripts/newuser.sh +++ b/scripts/newuser.sh @@ -617,9 +617,12 @@ else password="$(openssl rand -hex 24)" log 'creating persistent Mosquitto credential' + # Mark the transaction as owning this username before invoking the mutating + # command. If it changes the passwd file and then exits unsuccessfully, EXIT + # rollback must still remove the partial credential. + credential_created=1 docker exec "$MOSQUITTO_CONTAINER" \ mosquitto_passwd -b /mosquitto/config/passwd "$username" "$password" - credential_created=1 # mosquitto only re-reads password_file on SIGHUP; the reconciler reloads # every ~60s but a device may connect before then, so reload immediately. docker exec "$MOSQUITTO_CONTAINER" sh -c 'kill -HUP 1' >/dev/null 2>&1 \ @@ -715,10 +718,11 @@ else new_owner_map=$new_entry fi log 'adding deduplicated owner grant to OWNER_MQTT_USERNAME_MAP' -replace_owner_map "$new_owner_map" env_changed=1 +replace_owner_map "$new_owner_map" log 'inserting verified owner account and node grant rows' +db_changed=1 docker exec -i "$TIMESCALEDB_CONTAINER" \ psql -X -q -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$OWNER_DATABASE" \ -v "mqtt_username=${username}" -v "node_ids=${key_csv}" >/dev/null <<'SQL' @@ -759,7 +763,6 @@ SET verification_method = 'operator-config', updated_at = NOW(); COMMIT; SQL -db_changed=1 log 'recreating only the backend to reconcile owner grants and reload the ACL' apply_backend_config diff --git a/scripts/test-newuser.sh b/scripts/test-newuser.sh index 3ec34cd..60484da 100755 --- a/scripts/test-newuser.sh +++ b/scripts/test-newuser.sh @@ -227,4 +227,31 @@ expected_map="OWNER_MQTT_USERNAME_MAP=alice=${NODE_A},bob=${NODE_C},charlie=${NO [[ $(<"$SANDBOX/repo/.env") == "$expected_map" ]] \ || { printf 'concurrent owner map lost a grant: %s\n' "$(<"$SANDBOX/repo/.env")" >&2; exit 1; } -printf 'newuser concurrent-discovery regression test passed\n' +# Inject failures after each mutation class and check that EXIT rollback is +# idempotent and restores the mocked broker/config/database state. +for stage in credential environment db acl; do + init_sandbox "rollback-${stage}" "alice=${NODE_A}" + if run_newuser "$SANDBOX" "$SANDBOX/output.log" alice alice "$stage" dave "$NODE_B"; then + printf 'expected injected %s failure\n' "$stage" >&2 + exit 1 + fi + [[ $(<"$SANDBOX/repo/.env") == "OWNER_MQTT_USERNAME_MAP=alice=${NODE_A}" ]] \ + || { printf '%s failure did not restore the environment map\n' "$stage" >&2; exit 1; } + [[ ! -e $SANDBOX/credential-state ]] \ + || { printf '%s failure left a broker credential behind\n' "$stage" >&2; exit 1; } + [[ ! -e $SANDBOX/db-state ]] \ + || { printf '%s failure left owner database state behind\n' "$stage" >&2; exit 1; } + if [[ $stage == credential ]]; then + assert_file_contains "$SANDBOX/docker.log" 'CREDENTIAL_DELETE dave' + fi + if [[ $stage == db || $stage == acl ]]; then + assert_file_contains "$SANDBOX/docker.log" 'DB_ROLLBACK dave' + fi + if [[ $stage == acl ]]; then + compose_count="$(grep -c '^COMPOSE_BACKEND$' "$SANDBOX/docker.log")" + [[ $compose_count -ge 2 ]] \ + || { printf 'ACL failure did not reapply restored backend config\n' >&2; exit 1; } + fi +done + +printf 'newuser concurrency and rollback regression tests passed\n'