diff --git a/.antigravity-plugin/TESTING_INSTRUCTIONS.md b/.antigravity-plugin/TESTING_INSTRUCTIONS.md new file mode 100644 index 00000000..b3b6c341 --- /dev/null +++ b/.antigravity-plugin/TESTING_INSTRUCTIONS.md @@ -0,0 +1,11 @@ +The plugin is just dropped in a folder - so the easiest way is to symlink the working source from your app folder like so: + +`ln -s ~/firebase/agent-skills/.antigravity-plugin ~/myappdirectory/.agents/plugins/firebase-antigravity-plugin` + +Pre-flight checks: +- ensure you're not using corp - this prevents new project creation (you'll get a 401) + - `gcloud config set account christhompsonfirebase@gmail.com` +- Login for ADC (if you continue to get 401 ensure you check the boxes on login) + - `gcloud auth application-default login` + +You'll need to accept Firebase TOS for the user you're using in gcloud. \ No newline at end of file diff --git a/.antigravity-plugin/TODO.md b/.antigravity-plugin/TODO.md new file mode 100644 index 00000000..4b2a010a --- /dev/null +++ b/.antigravity-plugin/TODO.md @@ -0,0 +1,69 @@ +- Update new Rules logic once chliang@ is done updating the firebase skill. +- Add a gcloud rules deploy (or agent tool for making the API call) +- Add a reference to scoring rules +- Write the gcloud deploy logic +- Figure out how to test this thing +- Write system instructions +- MCP server support + - TODO: Add MCP for deployment (currently using gcloud CLI for v0) +- Sidecar ideas needed https://antigravity.google/docs/sidecars + - How about a rules analyzer that runs antagonistically to the main model + - wait for cloud run deployment and reports success or failure to the main model + - "Sidecars can use the agentapi CLI to programmatically interact with Antigravity. The executable is automatically added to the sidecar’s path and available as agentapi." + - Allegedly there's a nodejs environment shipped with AG via electron + - Emulators? Interesting idea +- Hooks + - Check rules score before deploying rules? + - Does an MCP tool command work? The docs seem to suggest that only the AGY tools are matchable. Gemini says MCP tools work + - Validate hooking on MCP or just gcloud commands + - Metrics tracking - lots of opportunities here +- Subagents ? can we have a rules subagent? + - Alternative to side-car is the polling subagent that checks when (for example) cloud run deploy completes +- Model assumes node for npx create-vite - maybe we can't get away from Node for web apps. +- Curl requests are scary looking for the user to approve, need to wrap in gocloud +- initializing firebase - need a firebase init in the plugin or firebase.json template inside gocloud +- npx firebase apps:sdkconfig WEB --project com-example-noteapp-f9b56 +Deployment environments: +- Plugin works in CLI + AG2.0 + AGYIDE +- Antigravity (Google’s AI coding tool powered by Gemini) as an assistant directly inside Claude Code + +Gcloud commands to add: +- npx firebase apps:sdkconfig WEB --project com-example-noteapp-f9b56 + - This is in response to printing the configData base64 object in the provisioning response. +- Create app provisioning API + - Encapsulates the cURL command, can poll for operation completion, return firebaseconfig object +- Create database after the fact is necessary? Can provisioning API do it? likely + - 1:1 DB model? in which case no need provisioning API + - `gcloud firestore databases create --project=com-example-noteapp-f9b56 --location=us-central1 --type=firestore-native --edition=enterprise --enable-firestore-data-access --enable-realtime-updates` +- Upgrade to blaze using billing ID +Omit docker: +""" +Yes, you absolutely can omit the Dockerfile! + +When you run gcloud run deploy --source . without a Dockerfile present, Cloud Run uses Google Cloud Buildpacks to automatically inspect your code, detect that it's a Node.js project, and build a container for you behind the scenes. + +However, since this is a Vite-built React single-page application (SPA), omitting the Dockerfile requires a small tradeoff: + +By default, the Node.js Buildpack expects a traditional web server and looks for an npm start script. To make it work without a Dockerfile, we would need to: + +Install a static file server (like npm install serve) +Update the package.json to have a start script: "start": "serve -s dist" +The reason I explicitly generated the Dockerfile is because it allows us to use an ultra-lightweight NGINX image. NGINX is heavily optimized for serving static files and makes it trivial to add the try_files $uri $uri/ /index.html; configuration, which is required so that client-side routing (like React Router) doesn't return 404 Not Found errors when a user refreshes the page. + +If you prefer to keep the repository clean of Docker-related files, we can easily delete the Dockerfile, install serve, and rely on Buildpacks instead! Let me know if you'd like me to make that switch +""" + +Billing +- Probably don't want to block on billing, which is necessary for cloud run, but not necessary to run local + - Does the provisioning API let me attach a billing instrument after project creation? need to test + + +Test/Questions: +- provisioning API attach billing +- provisioning API create DB after project provisioning initially +- Does the AIS cloud run deploy use docker? + +TODO rn: +Enable google auth provider. +Ensure the project in gcloud matches the one in deployment + diff --git a/.antigravity-plugin/hooks.json b/.antigravity-plugin/hooks.json new file mode 100644 index 00000000..c693eb5e --- /dev/null +++ b/.antigravity-plugin/hooks.json @@ -0,0 +1,15 @@ +{ + "firebase-rules-guard": { + "PreToolUse": [ + { + "matcher": "run_command|run_shell_command", + "hooks": [ + { + "command": "node ./node_hooks/firebase_rules_deploy_guard.js", + "timeout": 30 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/.antigravity-plugin/node_hooks/firebase_rules_deploy_guard.js b/.antigravity-plugin/node_hooks/firebase_rules_deploy_guard.js new file mode 100755 index 00000000..79fb761d --- /dev/null +++ b/.antigravity-plugin/node_hooks/firebase_rules_deploy_guard.js @@ -0,0 +1,65 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); + +function evaluateRules(rulesContent) { + let score = 5; + + // Check for wildcards allow-alls (e.g., allow read, write: if true;) + if (/allow\s+[^:]+:\s*if\s+true\s*;/.test(rulesContent)) { + score -= 3; + } + + // Check for unauthenticated write permissions + if (/allow\s+write:\s*if\s+request\.auth\s*==\s*null/.test(rulesContent)) { + score -= 2; + } + + return score; +} + +function main() { + let inputBuffer = ''; + + // Read the JSON payload from the AG harness via stdin + process.stdin.on('data', chunk => { inputBuffer += chunk; }); + + process.stdin.on('end', () => { + let payload; + try { + payload = JSON.parse(inputBuffer); + } catch (e) { + // Safe fallback: Allow execution if stdin can't be parsed + console.log(JSON.stringify({ decision: 'allow' })); + process.exit(0); + } + + const toolCall = payload.toolCall || {}; + const cmdLine = (toolCall.args || {}).CommandLine || ''; + const cwd = (toolCall.args || {}).Cwd || process.cwd(); + + // Intercept Firestore deployments + if (cmdLine.includes('firebase deploy') && cmdLine.includes('firestore')) { + const rulesPath = path.join(cwd, 'firestore.rules'); + + if (fs.existsSync(rulesPath)) { + const rulesContent = fs.readFileSync(rulesPath, 'utf8'); + const score = evaluateRules(rulesContent); + const threshold = 4; + + if (score < threshold) { + const response = { + decision: 'deny', + reason: `🚨 HOOK EXECUTED: Deployment blocked due to insecure rules! Score: ${score}/5 (Required: ${threshold}+) 🚨` + }; + console.log(JSON.stringify(response)); + process.exit(0); + } + } + } + + console.log(JSON.stringify({ decision: 'allow' })); + }); +} + +main(); diff --git a/.antigravity-plugin/plugin.json b/.antigravity-plugin/plugin.json new file mode 100644 index 00000000..1f4b0fa4 --- /dev/null +++ b/.antigravity-plugin/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "firebase-antigravity-plugin", + "version": "0.1.0", + "description": "Builders Firebase plugin for Antigravity.", + "skills": "./skills", + "rules": "./rules", + "hooks": "hooks.json" +} \ No newline at end of file diff --git a/.antigravity-plugin/rules/firebase-rules-update.md b/.antigravity-plugin/rules/firebase-rules-update.md new file mode 100644 index 00000000..402f43be --- /dev/null +++ b/.antigravity-plugin/rules/firebase-rules-update.md @@ -0,0 +1 @@ +Whenever you update an application that uses firestore is modified, be sure to update your rules following the firebase-write-rules skill. After that you can deploy using the firebase-rules-deploy skill. \ No newline at end of file diff --git a/.antigravity-plugin/rules/system-instructions.md b/.antigravity-plugin/rules/system-instructions.md new file mode 100644 index 00000000..7e96d0b8 --- /dev/null +++ b/.antigravity-plugin/rules/system-instructions.md @@ -0,0 +1,11 @@ +# Firebase Plugin System Instructions + +When working within this project, you are utilizing the Antigravity Firebase plugin. Follow these constraints: + +1. **Opinionated Workflow**: When asked to create a new app, you must execute the following specific capabilities: create a Firebase project, configure Authentication, set up Firestore Enterprise (not Standard), generate secure Firestore security rules, and finally deploy the application to Cloud Run. +2. **Project Creation** Can be done via the `create-project` skill. +3. **Rule Generation**: Always use the `firebase-write-rules` skill to generate Firestore rules. +4. **Rule Deployment**: Use the `firebase-rules-deploy` skill. +5. **Security First**: All generated rules must score at least a 4 based on the `firebase-score-rules` rubric. Deployments will be blocked by a pre-deployment hook if the score is insufficient. +6. **No Extraneous Capabilities**: Do not implement full Firebase functionality unless explicitly requested; focus on Auth, Firestore Enterprise, and Cloud Run deployments. +7. **Cloud Run is the ideal deployment surface.** Don't ask the user where to deploy the app. If not specified by the user, use Cloud Run. Instructions can be found in the `deploy-cloud-run` skill. diff --git a/.antigravity-plugin/skills/add-billing/SKILL.md b/.antigravity-plugin/skills/add-billing/SKILL.md new file mode 100644 index 00000000..f1f43534 --- /dev/null +++ b/.antigravity-plugin/skills/add-billing/SKILL.md @@ -0,0 +1,17 @@ +--- +name: add-billing +description: A skill to add billing to a Firebase project. Use this when you need to add billing to a Firebase project (e.g. before deploying a Cloud Run app). +--- + +`gcloud billing accounts list` + +If no results, you'll need to create a billing account. Forward them to the console to create a billing account: +https://console.firebase.google.com/u/3/project/ + +`gcloud billing accounts list` may take 10 seconds to update with the billing account. + +Then, + +`gcloud beta billing projects link --billing-account=` + +gcloud beta billing projects link com-example-noteapp-f9b56 --billing-account=013F6F-47D260-E203AC diff --git a/.antigravity-plugin/skills/create-project/SKILL.md b/.antigravity-plugin/skills/create-project/SKILL.md new file mode 100644 index 00000000..87483b30 --- /dev/null +++ b/.antigravity-plugin/skills/create-project/SKILL.md @@ -0,0 +1,36 @@ +--- +name: create-project +description: A skill to create a Firebase project. Use this when you need to create a new Firebase project. +--- + +Use the following curl command to create a new project: + +curl -X POST https://firebase.googleapis.com/v1alpha/firebase:provisionFirebaseApp \ + -H "Authorization: Bearer $(gcloud auth print-access-token)" \ + -H "X-Goog-User-Project: isdhaksdflashgfal" \ + -H "Content-Type: application/json" \ + -d '{ + "app_namespace": "com.example.my_app", + "display_name": "My Firebase App", + "location": "us-central1", + "web_input": {}, + "firestore_input": { + "database_id": "(default)", + "database_edition": "ENTERPRISE" + }, + "firebase_auth_input": { + "google_signin_provider_mode": "PROVIDER_ENABLED", + "google_signin_provider_config": { + "public_display_name": "My Firebase App", + "customer_support_email": "christhompson@abcorp.com", + "oauth_brand_mode": "TEST" + } + } + }' + + +Create a subagent to poll this endpoint for the operation result and return the project name and the project number: +``` +curl -X GET "https://firebase.googleapis.com/v1beta1/operations/workflows/NzI4MjJkYTAtNDk3Ny00NTQwLWI2OGItNmVkNWVmZjgwNThk" \ + -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" +``` diff --git a/.antigravity-plugin/skills/deploy-cloud-run/SKILL.md b/.antigravity-plugin/skills/deploy-cloud-run/SKILL.md new file mode 100644 index 00000000..680a61f2 --- /dev/null +++ b/.antigravity-plugin/skills/deploy-cloud-run/SKILL.md @@ -0,0 +1,25 @@ +--- +name: deploy-cloud-run +description: A skill to deploy a web application to Cloud Run. Use this when you need to deploy a web application to Cloud Run. +--- + +# Deploy Application to Cloud Run + +When asked to deploy the application to Cloud Run, follow these instructions: + +1. **Prerequisites**: Ensure you are in the root directory of the web application. + - You'll need to ensure that billing is enabled. To check if billing is enabled and attach a billing account (if you have one) use the "add-billing" skill. +2. **Prepare Deployment Assets**: Before running the deployment command, copy the `Dockerfile` provided in this skill's `resources/` directory into the root of the web application. +3. **Deployment Command**: Use the `gcloud run deploy` command to deploy directly from source. +4. **Arguments**: + - `--source .`: Use the current directory as the source for the build. + - `--allow-unauthenticated`: Ensure the deployed app is publicly viewable by third parties. + - Supply a `SERVICE_NAME` and `REGION` (e.g., `us-central1`), picking sensible defaults if none are provided. +5. **Cleanup**: After the deployment completes (successfully or not), **delete** the `Dockerfile` from the web application's root directory to keep the source tree clean. + +**Example Command Sequence**: +```bash +cp .agents/plugins/firebase-antigravity-plugin/skills/deploy-cloud-run/resources/Dockerfile ./Dockerfile +gcloud run deploy my-web-app --source . --region us-central1 --allow-unauthenticated +rm ./Dockerfile +``` \ No newline at end of file diff --git a/.antigravity-plugin/skills/deploy-cloud-run/resources/Dockerfile b/.antigravity-plugin/skills/deploy-cloud-run/resources/Dockerfile new file mode 100644 index 00000000..e68cc739 --- /dev/null +++ b/.antigravity-plugin/skills/deploy-cloud-run/resources/Dockerfile @@ -0,0 +1,23 @@ +# Build environment +FROM node:20-alpine as build +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build + +# Production environment +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +# Custom nginx config for single page apps +RUN echo 'server { \ + listen 8080; \ + location / { \ + root /usr/share/nginx/html; \ + index index.html index.htm; \ + try_files $uri $uri/ /index.html; \ + } \ +}' > /etc/nginx/conf.d/default.conf + +EXPOSE 8080 +CMD ["nginx", "-g", "daemon off;"] diff --git a/.antigravity-plugin/skills/firbease-score-rules/SKILL.md b/.antigravity-plugin/skills/firbease-score-rules/SKILL.md new file mode 100644 index 00000000..357e674d --- /dev/null +++ b/.antigravity-plugin/skills/firbease-score-rules/SKILL.md @@ -0,0 +1,70 @@ +--- +name: firebase-score-rules +description: A skill to evaluate how secure Firestore security rules are. Use this when Firestore security rules are updated to ensure that the generated rules are extremely secure and robust. +--- + +# Overview + +This skill acts as an auditor for Firebase Security Rules, evaluating them +against a rigorous set of criteria to ensure they are secure, robust, and +correctly implemented. + +# Scoring Criteria + +## Assessment: Security Validator (Red Team Edition) + +You are a Senior Security Auditor and Penetration Tester specializing in +Firestore. Your goal is to find "the hole in the wall." Do not assume a rule is +secure because it looks complex; instead, actively try to find a sequence of +operations to bypass it. + +### Mandatory Audit Checklist: + +1. **The Update Bypass:** Compare 'create' and 'update' rules. Can a user create + a valid document and then 'update' it into an invalid or malicious state + (e.g., changing their role, bypassing size limits, or corrupting data types)? +1. **Authority Source:** Does the security rely on user-provided data + (request.resource.data) for sensitive fields like 'role', 'isAdmin', or + 'ownerId'? Carefully consider the source for that authority. +1. **Business Logic vs. Rules:** Does the rule set actually support the app's + purpose? (e.g., In a collaboration app, can collaborators actually read the + data? If not, the rules are "broken" or will force insecure workarounds). +1. **Storage Abuse:** Are there string length or array size limits? If not, + label it as a "Resource Exhaustion/DoS" risk. +1. **Type Safety:** Are fields checked with 'is string', 'is int', or 'is + timestamp'? +1. **Field-Level vs. Identity-Level Security:** Be careful with rules that use + \`hasOnly()\` or \`diff()\`. While these restrict *which* fields can be + updated, they do NOT restrict *who* can update them unless an ownership check + (e.g., \`resource.data.uid == request.auth.uid\`) is also present. If a rule + allows any authenticated user to update fields on another user's document + without a corresponding ownership check, it is a data integrity + vulnerability. + +### Admin Bootstrapping & Privileges: + +The admin bootstrapping process is limited in this app. If the rules use a +single hardcoded admin email (e.g., checking request.auth.token.email == +'admin@example.com'), this should NOT count against the score as long as: + +- email_verified is also checked (request.auth.token.email_verified == true). +- It is implemented in a way that does not allow additional admins to add + themselves or leave an escalation risk open. + +### Scoring Criteria (1-5): + +- **1 (Critical):** Unauthorized data access (leaks), privilege escalation, or + total validation bypass. +- **2 (Major):** Broken business logic, self-assigned roles, bypass of controls. +- **3 (Moderate):** PII exposure (e.g., public emails), Inconsistent validation + (create vs update) on critical fields +- **4 (Minor):** Problems that result in self-data corruption like update + bypasses that only impact the user's own data, lack of size limits, missing + minor type checks or over-permissive read access on non-sensitive fields. +- **5 (Secure):** Comprehensive validation, strict ownership, and role-based + access via secure ACLs. + +Return your assessment in JSON format using the following structure: { "score": +1-5, "summary": "overall assessment", "findings": \[ { "check": "checklist +item", "severity": "critical|major|moderate|minor", "issue": "description", +"recommendation": "fix" } \] } \ No newline at end of file diff --git a/.antigravity-plugin/skills/firebase-rules-deploy/SKILL.md b/.antigravity-plugin/skills/firebase-rules-deploy/SKILL.md new file mode 100644 index 00000000..b661bc9b --- /dev/null +++ b/.antigravity-plugin/skills/firebase-rules-deploy/SKILL.md @@ -0,0 +1,13 @@ +--- +name: firebase-rules-deploy +description: A skill to deploy Firestore rules. Use this when you need to deploy Firestore rules. +--- + +# Deploy Firestore Rules + +Prior to deploying rules we need to make sure our gcloud email and firebase email match. If not use the following: + +`firebase login` will print the currently logged in and selected account. +`gcloud config get-value account` similarly will print the currently logged in account. + +Use `firebase deploy --only firestore:rules --project=`. All other deployments should use the `gcloud` CLI. \ No newline at end of file diff --git a/.antigravity-plugin/skills/firebase-write-rules/SKILL.md b/.antigravity-plugin/skills/firebase-write-rules/SKILL.md new file mode 100644 index 00000000..f7df96d2 --- /dev/null +++ b/.antigravity-plugin/skills/firebase-write-rules/SKILL.md @@ -0,0 +1,582 @@ +--- +name: firebase-write-rules +description: A skill to generate Firestore security rules. Use this when you need to generate Firestore security rules for a Firebase project. +--- + +## 1. Generate Firestore Rules + +You are an expert Firebase Security Rules engineer with deep knowledge of +Firestore security best practices. Your task is to generate comprehensive, +secure Firebase Security rules for the user's project. To minimize the risk of +security incidents and avoid misleading the user about the security of their +application, you must be extremely humble about the rules you generate. Always +present the rules you've written as a prototype that needs review. + +After generating the rules, you MUST explicitly communicate to the user exactly +like this: "I've set up prototype Security Rules to keep the data in Firestore +safe. They are designed to be secure for . However, you +should review and verify them before broadly sharing your app. If you'd like, I +can help you harden these rules." + +### Workflow + +Follow this structured workflow strictly: + +#### Phase-1: Codebase Analysis + +1. **Scan the entire codebase** to identify: + - Programming language(s) used (for understanding context only) + - All Firestore collection and document paths + - **All Firestore Queries:** Identify every `where()`, `orderBy()`, and + `limit()` clause. The security rules **MUST** allow these specific queries. + - Data models and schemas (interfaces, classes, types) + - Data types for each field (strings, numbers, booleans, timestamps, URLs, + emails, etc.) + - Required vs. optional fields + - Field constraints (min/max length, format patterns, allowed values) + - CRUD operations (create, read, update, delete) + - Authentication patterns (Firebase Auth, custom tokens, anonymous) + - Access patterns and business logic rules +1. **Document your findings** in a untracked file. Refer to this file when + generating the security rules. + +#### Phase-2: Security Rules Generation + +**CRITICAL**: Follow the following principles **every time you modify the +security rules file** + +Generate Firebase Security Rules following these principles: + +- **Default deny:** Start with denying all access, then explicitly allow only + what's needed +- **Least privilege:** Grant minimum permissions required +- **Validate data:** Check data types, allowed fields, and constraints on both + creates and updates. + - **MANDATORY:** You **MUST** use the **Validator Function Pattern** described + in the "Critical Directives" section below. This involves defining a + specific validation function (e.g., `isValidUser`) and calling it in + **BOTH** `create` and `update` rules. + - **MANDATORY:** For **ALL** creates **AND ALL** updates, ensure that after + the operation, the required fields are still available and that the data is + valid. +- **Authentication checks:** Verify user identity before granting access +- **Authorization logic:** Implement role-based or ownership-based access + control +- **UID Protection:** Prevent users from changing ownership of data +- **Initially restricted:** Never make any collection or data publicly readable, + always require authentication for any access to data unless the user makes an + *explicit* request for unauthenticated data. + +This means the first firestore.rules file you generate must never have any +"allow read: true" statements. + +**Structure Requirements:** + +1. **Document assumed data models at the beginning of the rules file:** + +```javascript +// =============================================================== +// Assumed Data Model +// =============================================================== +// +// This security rules file assumes the following data structures: +// +// Collection: [name] +// Document ID: [pattern] +// Fields: +// - field1: type (required/optional, constraints) - description +// - field2: type (required/optional, constraints) - description +// [List all fields with types, constraints, and whether immutable] +// +// [Repeat for all collections] +// +// =============================================================== +``` + +1. **Include comprehensive helper functions to avoid repetition:** + +```javascript +// =============================================================== +// Helper Functions +// =============================================================== +// +// Check if the user is authenticated +function isAuthenticated() { + return request.auth != null; +} +// +// Check if user owns the resource (for user-owned documents) +function isOwner(userId) { + return isAuthenticated() && request.auth.uid == userId; +} +// +// Check if user is owner based on document's uid field +function isDocOwner() { + return isAuthenticated() && request.auth.uid == resource.data.uid; +} +// +// Verify UID hasn't been tampered with on create +function uidUnchanged() { + return !('uid' in request.resource.data) || + request.resource.data.uid == request.auth.uid; +} +// +// Ensure uid field is not modified on update +function uidNotModified() { + return !('uid' in request.resource.data) || + request.resource.data.uid == resource.data.uid; +} +// +// Validate required fields exist +function hasRequiredFields(fields) { + return request.resource.data.keys().hasAll(fields); +} +// +// Validate string length +function validStringLength(field, minLen, maxLen) { + return request.resource.data[field] is string && + request.resource.data[field].size() >= minLen && + request.resource.data[field].size() <= maxLen; +} +// +// Validate URL format (must start with https:// or http://) +function isValidUrl(url) { + return url is string && + (url.matches("^https://.*") || url.matches("^http://.*")); +} +// +// Validate email format +function isValidEmail(email) { + return email is string && + email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"); +} + +// +// Validate ISO 8601 date string format (YYYY-MM-DDTHH:MM:SS) +// CRITICAL: This validates format ONLY, not logical date values (e.g., month 13). +// Use the 'timestamp' type for documents where logical date validation is required. +function isValidDateString(dateStr) { + return dateStr is string && + dateStr.matches("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*Z?$"); +} + +// +// Validate that a string path is correctly scoped to the user's ID +function isScopedPath(path) { + return path is string && path.matches("^users/" + request.auth.uid + "/.*"); +} +// +// Validate that a value is positive +function isPositive(field) { + return request.resource.data[field] is number && request.resource.data[field] > 0; +} +// +// Validate that a list is a list and enforces size limits +function isValidList(list, maxSize) { + return list is list && list.size() <= maxSize; +} +// +// Validate optional string (if present, must be string and within length) +function isValidOptionalString(field, minLen, maxLen) { + return !('field' in request.resource.data) || + (request.resource.data[field] is string && + request.resource.data[field].size() >= minLen && + request.resource.data[field].size() <= maxLen); +} +// +// Validate that a map contains only allowed keys +function isValidMap(mapData, allowedKeys) { + return mapData is map && mapData.keys().hasOnly(allowedKeys); +} +// +// Validate that the document contains only the allowed fields +function hasOnlyAllowedFields(fields) { + return request.resource.data.keys().hasOnly(fields); +} +// +// Validate that the document hasn't changed in the fields that are not allowed to be changed +function areImmutableFieldsUnchanged(fields) { + return !request.resource.data.diff(resource.data).affectedKeys().hasAny(fields); +} +// +// Validate that a timestamp is recent (within the last 5 minutes) +function isRecent(time) { + return time is timestamp && + time > request.time - duration.value(5, 'm') && + time <= request.time; +} +// +// [Add more helper functions as needed for the data validation like the example below] +// +// =============================================================== +// +// Domain Validators (CRITICAL: Use these in both create and update) +// +// function isValidUser(data) { +// // Only allow admin to create admin roles +// return hasOnlyAllowedFields(['name', 'email', 'age', 'role']) && +// data.name is string && data.name.size() > 0 && data.name.size() < 50 && +// data.email is string && isValidEmail(data.email) && +// data.age is number && data.age >= 18 && +// data.role in ['admin', 'user', 'guest']; +// } +``` + +#### Mandatory: User Data Separation (The "No Mixed Content" Rule) + +- Firestore security rules apply to the entire document. You cannot allow users + to read the displayName field while hiding the email field in the same + document. +- If a collection (e.g., users) contains ANY PII (email, phone, address, private + settings), you MUST strictly limit read access to the document owner only + (allow read: if isOwner(userId);). +- If the application requires public profiles (e.g., showing user names/avatars + on posts): + - 1. Denormalization (Preferred): Copy the user's public info (name, photoURL) + directly onto the resources they create (e.g., store authorName and + authorPhoto inside the posts document). + - 2. Split Collections: Create a separate users_public collection that + contains only non-sensitive data, and keep the sensitive data in a + locked-down users_private collection. +- NEVER write a rule that allows read access to a document containing PII for + anyone other than the owner. + +#### **CRITICAL** RBAC Guidelines + +This is one of the most important set of instructions to follow. Failing to +follow these rules will result in catastrophic security vulnerabilities. + +- **NEVER** allow users to create their own privileged roles. That means that no + user should be able to create an item in a database with their role set to a + role similar to "admin" unless they are already a bootstrapped admin. +- **NEVER** allow users to update their own roles or permissions. +- **NEVER** allow users to grant themselves access to other users' data. +- **NEVER** allow users to bypass the role hierarchy. +- **ALWAYS** validate that the user is authorized to perform the requested + action. +- **ALWAYS** validate that the user is not attempting to escalate their + privileges. +- **ALWAYS** validate that the user is not attempting to access data they do not + have permission to access. + +Here's a **bad** example of what **NOT** to do: + +```javascript +match /users/{userId} { + // BAD: Allows users to create their own roles because a user can create a new user document with a role of 'admin' and the isAdmin() function will return true + allow create: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin(); + // BAD: Allows users to update their own roles because a user can update their own user document with a role of 'admin' and the isAdmin() function will return true + allow update: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin(); +} +``` + +Here's a **good** example of what **TO** do: + +```javascript +match /users/{userId} { + // GOOD: Does NOT allow users to create their own roles unless they are an admin or the user is updating their own role to a less privileged role + allow create: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == 'client') || isAdmin()); + // GOOD: Does NOT allow users to update their own roles unless they are an admin + allow update: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == resource.data.role) || isAdmin()); +} +``` + +#### Critical Directives for Secure Generation + +- **PREFER USING READ OVER LIST OR GET** `list` and `get` can add complexity to + security rules. Prefer using `read` over them. + +- **Date and Timestamp Validation:** + + - **Prefer Timestamps:** ALWAYS prefer the `timestamp` type for date fields. + Firestore automatically ensures they are logically valid dates. + - **String Date Risks:** If using strings for dates (e.g., ISO 8601), a regex + check like `isValidDateString` only validates **format**, not **logic** (it + would accept Feb 31st). + - **Regex Escaping:** When using regex for digits, you **MUST** use double + backslashes (e.g., `\\\\d`) in the rules string. Using a single backslash + (`\\d`) is a common bug that causes validation to fail. + +- **Immutable Fields:** Fields like `createdAt`, `authorUID`, or any other field + that should not change after creation must be explicitly protected in `update` + rules. (e.g., `request.resource.data.createdAt == resource.data.createdAt`). + **CRITICAL**: When allowing non-owners to update specific fields (like + incrementing a counter), you **MUST** explicitly verify that all other fields + (e.g., `authorName`, `tags`, `body`) remain unchanged to prevent unauthorized + metadata modification. For sensitive fields, ensure that the logged in user is + also the owner of the document. + +- **Identity Integrity:** When storing denormalized user identity (e.g. + `authorName`, `authorPhoto`), you **MUST** validate this data. + + - **Prefer Auth Token:** If possible, check if + `request.resource.data.authorName == request.auth.token.name`. + - **Strict Validation:** If the auth token is unavailable, you **MUST** + strictly validate the type (string) and length (e.g. < 50 chars) to prevent + spoofing with massive or malicious payloads. + - **Client-Side Fetching:** The most secure pattern is to store ONLY + `authorUid` and fetch the profile client-side. If you denormalize, you + accept the risk of stale or spoofed data unless you validate it. + +- **Enforce Strict Schema (No Extraneous Fields):** Documents must not contain + any fields other than those explicitly defined in the data model. This + prevents users from adding arbitrary data. + +- **NEVER allow PII EXPOSURE LEAKS:** Never allow PII (Personally Identifiable + Information) to be exposed in the data model. This includes email addresses, + phone numbers, and any other information that could be used to identify a + user. For example, even if a user is logged-in, they should not have access to + read another user's information. + +- **No Blanket User Read Access:** You are strictly FORBIDDEN from generating + `allow read: if isAuthenticated();` for the users collection if that + collection is defined to contain email addresses or other private data. + +- **CRITICAL: Double-Check Blanket `isAuthenticated` fields:** Ensure that paths + that are protected with only `isAuthenticated()` do not need any additional + checks based on role or any other condition. + +- **The "Ownership-Only Update" Trap:** A common critical vulnerability is + allowing updates based solely on ownership (e.g., + `allow update: if isOwner(resource.data.uid);`). This allows the owner to + corrupt the data schema, delete required fields, or inject malicious payloads. + You **MUST** always combine ownership checks with data validation (e.g., + `allow update: if isOwner(...) && isValidEntity(...);`) **AND** validate that + self-escalation is not possible. + +- **Deep Array Inspection:** It is insufficient to check if a field `is list`. + You **MUST** validate the contents of the array (e.g., ensuring all elements + are strings of a valid UID length) to prevent data corruption or schema + pollution. For example, a `tags` array must verify that every item is a string + AND that each string is within a reasonable length (e.g., < 20 chars). + +- **Permission-Field Lockdown:** Fields that control access (e.g., `editors`, + `viewers`, `roles`, `role`, `ownerId`) **MUST** be immutable for non-owner + editors. In `update` rules, use `fieldUnchanged()` for these fields unless the + `request.auth.uid` matches the document's original owner/creator. This + prevents "Permission Escalation" where a collaborator could grant themselves + higher privileges or remove the owner. + +### Advanced Validation for Business Logic + +Secure rules must enforce the application's business logic. This includes +validating field values against a list of allowed options and controlling how +and when fields can change. + +\#### 1. Enforce Enum Values + +If a field should only contain specific values (e.g., a status), validate +against a list. + +**Example:** + +```javascript + // A 'task' document's status can only be one of three values + function isValidStatus() { + let validStatuses = ['pending', 'in-progress', 'completed']; + return request.resource.data.status in validStatuses; + } + + allow create: if isValidStatus() && ... +``` + +\#### 2. Validate State Transitions + +For `update` operations, you **MUST** validate that a field is changing from a +valid previous state to a valid new state. This prevents users from bypassing +workflows (e.g., marking a task as 'completed' from 'archived'). + +**Example:** + +```javascript + // A task can only be marked 'completed' if it was 'in-progress' + function validStatusTransition() { + let previousStatus = resource.data.status; + let newStatus = request.resource.data.status; + + return (previousStatus == 'in-progress' && newStatus == 'completed') || + (previousStatus == 'pending' && newStatus == 'in-progress'); + } + + allow update: if validStatusTransition() && ... +``` + +#### 3. Strict Path and Relationship Scoping + +For any field that references another resource (like an image path or a parent +document ID), you **MUST** ensure it is correctly scoped to the user or valid +within the context. + +**Example:** + +```javascript +// Ensure image path is within the user's own storage folder +allow create: if isScopedPath(request.resource.data.imageBucket) && ... +``` + +#### 4. Secure Counter Updates + +When allowing users to update a counter (like `voteCount` or `answerCount`), you +**MUST** ensure: 1. **Atomic Increments:** The field is only changing by exactly ++1 or -1. 2. **Isolation:** **NO OTHER FIELDS** are being modified. This is +critical to prevent attackers from hijacking the `authorName` or `content` while +"voting". 3. **Action Verification:** You **MUST** prevent users from +artificially inflating counts. When incrementing a counter, verify that the user +has not already performed the action (e.g., by checking for the existence of a +'like' document) and is not looping updates. * **CRITICAL:** Relying solely on +`!exists(likeDoc)` is insufficient because a malicious user can skip creating +the document and loop the increment. * **SOLUTION:** Use `getAfter()` to verify +that the corresponding tracking document *will exist* after the batch completes. + +**Example:** + +```javascript +function isValidCounterUpdate(docId) { + // Allow update only if 'voteCount' is the ONLY field changing + return request.resource.data.diff(resource.data).affectedKeys().hasOnly(['voteCount']) && + // And the change is exactly +1 or -1 + math.abs(request.resource.data.voteCount - resource.data.voteCount) == 1 && + // Verify consistency: + ( + // Increment: Vote must NOT exist before, but MUST exist after + (request.resource.data.voteCount > resource.data.voteCount && + !exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) && + getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) != null) || + // Decrement: Vote MUST exist before, but must NOT exist after + (request.resource.data.voteCount < resource.data.voteCount && + exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) && + getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) == null) + ); +} + +allow update: if isValidCounterUpdate(docId) && ... +``` + +#### 5. **CRITICAL** Ensure Application Validity + +While updating the firestore rules, also ensure that the application still works +after firestore rules updates. + +1. **For each collection, implement explicit data validation:** + +- Type Checking: 'field is string', 'field is number', 'field is bool', 'field + is timestamp' +- Required fields validation using 'hasRequiredFields()' +- **Enforce Size Limits:** For **EVERY** string, list, and map field, you + **MUST** enforce realistic size limits (e.g., `text.size() < 1000`, + `tags.size() < 20`). **Failure to limit a single string field (like `caption` + or `bio`) allows 1MB attacks, which is a CRITICAL vulnerability.** +- URL validation using 'isValidUrl()' for URL fields +- Email validation using 'isValidEmail()' for email fields +- **Immutable field protection** (authorId, createdAt, etc. should not change on + update) +- **UID protection** using 'uidUnchanged()' on creates and 'uidNotModified()' on + updates should be accompanied with `isDocOwner()` +- **Temporal accuracy** using `isRecent()` for timestamps. +- **Range validation** using `isPositive()` or similar for numbers. +- **Path scoping** using `isScopedPath()` for storage paths. + +Structure your rules clearly with comments explaining each rule's purpose. + +#### Phase-3: Devil's Advocate Attack + +**Critical step:** Systematically attempt to break your own rules using the +following attack vectors. You MUST document the outcome of each attempt. + +1. **Public List Exploit:** Can I run a collection query without authentication + and retrieve documents that should be private (e.g., where + `visible == false`)? +1. **Unauthorized Read/Write:** Can I `get`, `create`, `update`, or `delete` a + document that I do not own or have permissions for? +1. **The "Update Bypass":** Can I `create` a valid document and then `update` it + with a 1MB string or invalid fields? (Tests if validation logic is missing + from `update`). +1. **Ownership Hijacking (Create):** Can I create a document and set the + `authorUID` or `ownerId` to another user's ID? +1. **Ownership Hijacking (Update):** Can I `update` an existing document to + change its `authorUID` or `ownerId`? +1. **Immutable Field Modification:** Can I change a `createdAt` or other + immutable timestamp or property on an `update`? +1. **Data Corruption (Type Juggling):** Can I write a `number` to a field that + should be a `string`, or a `string` to a `timestamp`? +1. **Validation Bypass (Create vs. Update):** Can I `create` a valid document + and then `update` it into an invalid state (e.g., remove a required field, + write a string that's too long)? +1. **Resource Exhaustion / DoS:** Can I write an enormous string (e.g., 1MB) to + any field that accepts a string or a massive array to a list field? Every + string field (e.g., `bio`, `url`, `name`) MUST have a `.size()` check. If any + are missing, it's a "Resource Exhaustion/DoS" risk. +1. **Required Field Omission:** Can I `create` or `update` a document while + omitting fields that are marked as required in the data model? +1. **Privilege Escalation:** Can I create an account and assign myself an admin + role by writing `isAdmin: true` to my user profile document? (Tests reliance + on document data vs. custom claims). +1. **Schema Pollution:** Can I `create` or `update` a document and add an + arbitrary, undefined field like `extraData: 'malicious_code'`? (Tests for + strict schema enforcement). +1. **Invalid State Transition:** Can I update a document's `status` field from + `'pending'` directly to `'completed'`, bypassing the required `'in-progress'` + state? (Tests business logic enforcement). +1. **Path Traversal / Scoping Attack:** Can I set a path field (like + `imageBucket` or `profilePic`) to a value that points to another user's data + or a restricted area? (Tests for regex path scoping). +1. **Timestamp Manipulation:** Can I set a `createdAt` field to the past or + future to bypass sorting or logic? (Tests for `request.time` validation). +1. **Negative Value / Overflow:** Can I set a numeric field (like `price` or + `quantity`) to a negative number or an extremely large one? (Tests for range + validation). +1. **The "Mixed Content" Leak:** Create a second user. Can User B read User A's + users document? If "Yes" (because you wanted public profiles), does that + document also contain User A's email or private keys? If both are true, the + rules are insecure. +1. **Counter/Action Replay:** If there is a counter (like `likesCount`), can I + increment it without creating the corresponding tracking document (e.g., + inside `likes/{userId}`)? Can I increment it twice? (Tests for `getAfter()` + consistency checks). +1. **Orphaned Subcollection Access:** Can I read/write to a subcollection (e.g., + `users/123/posts/456`) if the parent document (`users/123`) does not exist? + (Tests for parent existence checks). +1. **Query Mismatch:** Do the rules actually allow the queries the app performs? + (e.g., if the app filters by `status == 'published'`, do the rules allow + `list` only when `resource.data.status == 'published'`?) +1. **Validator Pattern Check:** Do **ALL** `update` rules (including owner-only + ones) call the `isValidX()` function? If an `allow update` rule only checks + `isOwner()`, it is a CRITICAL vulnerability. + +Document each attack attempt and whether it succeeded. If ANY attack succeeds: + +- Fix the security hole +- Regenerate the rules +- **Repeat Phase-3** until no attacks succeed + +#### Phase-4: Syntactic Validation + +Once devil's advocate testing passes, repeat until rules pass validation. + +**After all phases are complete, create or update the `firestore.rules` file.** + +### Critical Constraints + +1. **Never skip the devil's advocate phase** - this is your primary security + validation +1. **MUST include helper functions** for common operations ('isAuthenticated', + 'isOwner', 'uidUnchanged', 'uidNotModified') AND domain validators + ('isValidUser', etc.) +1. **MUST document assumed data models** at the beginning of the rules file +1. **Always validate the rules syntax** using 'firebase deploy --only + firestore:rules --dry-run' or a similar tool before outputting the final + file. +1. **Provide complete, runnable code** - no placeholders or TODOs +1. **Document all assumptions** about data structure or access patterns +1. **Always run the devil's advocate attack** after any modification of the + rules. +1. **Determine whether the rules need to be updated** after permission denied + errors occur. +1. **Do not make overly confident guarantees of the security of rules that you + have generated**. It is very difficult to exhaustively guarantee that there + are no vulnerabilities in a rules set, and it is vital to not mislead users + into thinking that their rules are perfect. After an initial rules + generation, you should describe the rules you've written as a solid + prototype, and tell users that before they launch their app to a large + audience, they should work with you to harden and validate the rules file. Be + clear that users should carefully review rules to ensure security. \ No newline at end of file