mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 18:07:24 +01:00
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
This commit is contained in:
parent
db3d8f827f
commit
4c024a24f0
@ -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
|
that key ships inside the Android APKs. Pinning the uid means a stranger who
|
||||||
registers an account still reads and writes nothing.
|
registers an account still reads and writes nothing.
|
||||||
|
|
||||||
To deploy, substitute the real uid (Firebase console → Authentication → Users
|
The uid here is a **placeholder on purpose** — this repo is public, and the
|
||||||
→ User UID) and paste into **Realtime Database → Rules → Publish**.
|
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:
|
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` |
|
| `firebase.json` | `apiKey`, `databaseUrl`, `projectId`, `uid`, `email` |
|
||||||
| `password` | the sync account's password, no trailing newline |
|
| `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
|
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
|
ships inside client apps. The password and refresh tokens are the real
|
||||||
|
|||||||
85
firebase/render_rules.sh
Executable file
85
firebase/render_rules.sh
Executable file
@ -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 "$@"
|
||||||
Loading…
Reference in New Issue
Block a user