diff --git a/firebase/README.md b/firebase/README.md index 71436d8..bf15ffc 100644 --- a/firebase/README.md +++ b/firebase/README.md @@ -34,8 +34,23 @@ through the identitytoolkit REST API to anyone holding the Web API key, and that key ships inside the Android APKs. Pinning the uid means a stranger who registers an account still reads and writes nothing. -To deploy, substitute the real uid (Firebase console → Authentication → Users -→ User UID) and paste into **Realtime Database → Rules → Publish**. +The uid here is a **placeholder on purpose** — this repo is public, and the +uid is the one value that must not appear in it. Publishing this file +verbatim yields rules matching no account, which locks every app out of the +project until someone notices; the failure looks like a permission error +from every device at once. + +So do not hand-substitute. Render it: + +```bash +./render_rules.sh # prints deployable rules, real uid filled in +./render_rules.sh --check # validates without printing the uid +``` + +It reads the uid from `~/.config/crdt-sync/firebase.json` and refuses to emit +anything still containing the placeholder, missing `auth != null`, missing a +uid pin, or not parsing as JSON. Paste its output into **Realtime Database → +Rules → Publish**. Verify the rules are live — an unauthenticated request must be refused: @@ -53,7 +68,7 @@ outside version control: |---|---| | `firebase.json` | `apiKey`, `databaseUrl`, `projectId`, `uid`, `email` | | `password` | the sync account's password, no trailing newline | -| `database.rules.json` | the deployed rules, with the real uid filled in | +| `database.rules.json` | the rendered rules, real uid filled in (see `render_rules.sh`) | The Web API key is **not** a secret — it is a public project identifier that ships inside client apps. The password and refresh tokens are the real diff --git a/firebase/render_rules.sh b/firebase/render_rules.sh new file mode 100755 index 0000000..0b6d8f3 --- /dev/null +++ b/firebase/render_rules.sh @@ -0,0 +1,85 @@ +#!/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 "$@"