utils/firebase/render_rules.sh
Krzysztof kuhy Rudnicki 4c024a24f0
Some checks failed
file length / file-length (push) Waiting to run
palette drift / palette (push) Waiting to run
sync_settings_ui tests / test (push) Has been cancelled
firebase: make the rules template impossible to deploy verbatim
database.rules.json carries YOUR_FIREBASE_UID on purpose -- this repo is
public and the uid is the one value that must not be in it. But nothing
stopped anyone publishing the template as-is, and rules pinned to a uid that
matches no account deny every read and write. The symptom is every device
failing at once with a bare permission error, which reads like an outage
rather than a bad paste.

render_rules.sh substitutes the uid from ~/.config/crdt-sync/firebase.json
and fails closed on anything unsafe: a surviving placeholder, missing
`auth != null`, a missing uid pin (that one matters -- `auth != null` alone
lets any stranger who self-registers through the public Web API key read
everything), or output that is not JSON.

Verified: its output is byte-identical to the deployed
~/.config/crdt-sync/database.rules.json, and the live database still answers
an unauthenticated read with 401. All three failure paths exercised.

Also corrects the README, which described the in-repo copy as "the deployed
rules, with the real uid filled in" -- it never was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 19:10:10 +02:00

86 lines
2.8 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# Render deployable Realtime Database rules from the public template.
#
# The template in this repo carries a placeholder on purpose: the repository
# is public and the uid is the one value that must not appear in it. That
# leaves a trap -- publishing the template verbatim yields rules that match
# no account, which locks every app out of the shared project until someone
# notices. This script closes it: it substitutes the real uid from the local
# config and refuses to emit anything still containing a placeholder.
# ============================================================================
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME
readonly SCRIPT_DIR
readonly TEMPLATE="$SCRIPT_DIR/database.rules.json"
readonly CONFIG_DIR="${CRDT_SYNC_CONFIG_DIR:-$HOME/.config/crdt-sync}"
readonly PLACEHOLDER="YOUR_FIREBASE_UID"
usage() {
echo "Usage: $SCRIPT_NAME [--check]"
echo " (no args) print deployable rules with the real uid substituted"
echo " --check verify the rendered rules are sane; print nothing"
exit 0
}
# The uid lives in firebase.json, the single source of truth for this machine.
read_uid() {
local config="$CONFIG_DIR/firebase.json"
if [[ ! -r "$config" ]]; then
echo "Error: $config is missing or unreadable." >&2
echo "It holds the uid these rules pin; see README.md." >&2
exit 1
fi
python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['uid'])" \
"$config"
}
render() {
local uid="$1"
sed "s/$PLACEHOLDER/$uid/g" "$TEMPLATE"
}
# Fail closed: a rules file that still names the placeholder, or that any
# unauthenticated request would satisfy, must never reach the console.
validate() {
local rendered="$1"
if [[ "$rendered" == *"$PLACEHOLDER"* ]]; then
echo "Error: rendered rules still contain $PLACEHOLDER." >&2
exit 1
fi
if ! grep -q 'auth != null' <<< "$rendered"; then
echo "Error: rendered rules do not require authentication." >&2
exit 1
fi
if ! grep -q "auth.uid ===" <<< "$rendered"; then
echo "Error: rendered rules do not pin a uid; 'auth != null' alone" >&2
echo "lets any stranger who self-registers read and write." >&2
exit 1
fi
python3 -c "import json,sys; json.loads(sys.stdin.read())" <<< "$rendered"
}
main() {
local check_only=false
[[ "${1:-}" == "--check" ]] && check_only=true
[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage
local uid rendered
uid="$(read_uid)"
rendered="$(render "$uid")"
validate "$rendered"
if [[ "$check_only" == true ]]; then
echo "Rules render cleanly and pin a uid." >&2
else
printf '%s\n' "$rendered"
fi
}
main "$@"