Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"firebase init" loads Firestore security rules from non-default database #7981

Open
aemelyanovff opened this issue Nov 21, 2024 · 3 comments

Comments

@aemelyanovff
Copy link

[REQUIRED] Environment info

firebase-tools: 13.26.0

Platform: macOS

[REQUIRED] Test case

When there are two Firestore databases in one Firebase project, firebase init loads security rules from the non-default database. Expected: load rules from the default database.

[REQUIRED] Steps to reproduce

Prerequisite - setup project with different rules in different databases

Take a Firebase project with enabled Firestore and enabled billing. Mine is called test-non-default-rules.

  • firebase init firestore --project test-non-default-rules.
  • Create non-default database: firebase --project test-non-default-rules firestore:databases:create non-default --location=nam5
  • Duplicate rules and indexes:
    • cp firestore.rules firestore-non-default.rules
    • cp firestore.indexes.json firestore-indexes-non-default.indexes.json
  • Make firestore.rules and firestore-non-default.rules different. E.g.
    firestore.rules:
rules_version = '2';

// Default database rules
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

firestore-non-default.rules:

rules_version = '2';

// Non-default database rules
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}
  • Update firebase.json to include the second database:
{
  "firestore": 
    [
      {
        "database": "(default)",
        "rules": "firestore.rules",
        "indexes": "firestore.indexes.json"
      },
      {
        "database": "non-default",
        "rules": "firestore-non-default.rules",
        "indexes": "firestore-non-default.indexes.json"
      }
  ]
}

Run firebase init again

In a different folder:

  • firebase init firestore --project test-non-default-rules.
  • cat firestore.rules

[REQUIRED] Expected behavior

Expect to see rules for the default database.

It'd be even better if there was a way to choose the database, but if there isn't then it should use the default one.

[REQUIRED] Actual behavior

I see rules for the non-default database:

rules_version = '2';

// Non-default database rules
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

firebase init firestore --project test-non-default-rules --debug:

[2024-11-21T18:09:22.137Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
[2024-11-21T18:09:22.137Z] > authorizing via signed-in user ([email protected])

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:

  /Users/redacted/tmp/fb/reinit


=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

[2024-11-21T18:09:22.138Z] Using project from CLI flag: test-non-default-rules
[2024-11-21T18:09:22.139Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:22.139Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:22.139Z] >>> [apiv2][query] GET https://firebase.googleapis.com/v1beta1/projects/test-non-default-rules [none]
[2024-11-21T18:09:22.531Z] <<< [apiv2][status] GET https://firebase.googleapis.com/v1beta1/projects/test-non-default-rules 200
[2024-11-21T18:09:22.531Z] <<< [apiv2][body] GET https://firebase.googleapis.com/v1beta1/projects/test-non-default-rules {"projectId":"test-non-default-rules","projectNumber":"845181815027","displayName":"test-non-default-rules","name":"projects/test-non-default-rules","resources":{"hostingSite":"test-non-default-rules"},"state":"ACTIVE","etag":"1_8f9c418a-7741-42d5-b0c6-b12899191c20"}
i  Using project test-non-default-rules (test-non-default-rules) 

=== Firestore Setup
[2024-11-21T18:09:22.542Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:22.542Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:22.542Z] >>> [apiv2][query] GET https://serviceusage.googleapis.com/v1/projects/test-non-default-rules/services/firestore.googleapis.com [none]
[2024-11-21T18:09:22.542Z] >>> [apiv2][(partial)header] GET https://serviceusage.googleapis.com/v1/projects/test-non-default-rules/services/firestore.googleapis.com x-goog-quota-user=projects/test-non-default-rules
[2024-11-21T18:09:22.993Z] <<< [apiv2][status] GET https://serviceusage.googleapis.com/v1/projects/test-non-default-rules/services/firestore.googleapis.com 200
[2024-11-21T18:09:22.994Z] <<< [apiv2][body] GET https://serviceusage.googleapis.com/v1/projects/test-non-default-rules/services/firestore.googleapis.com [omitted]
[2024-11-21T18:09:22.994Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:22.994Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:22.995Z] >>> [apiv2][query] GET https://firestore.googleapis.com/v1/projects/test-non-default-rules/databases/(default) [none]
[2024-11-21T18:09:23.316Z] <<< [apiv2][status] GET https://firestore.googleapis.com/v1/projects/test-non-default-rules/databases/(default) 200
[2024-11-21T18:09:23.317Z] <<< [apiv2][body] GET https://firestore.googleapis.com/v1/projects/test-non-default-rules/databases/(default) {"name":"projects/test-non-default-rules/databases/(default)","uid":"01dad914-35b5-4808-919b-655b2421581f","createTime":"2024-11-21T17:47:12.821827Z","updateTime":"2024-11-21T17:47:12.821827Z","locationId":"nam5","type":"FIRESTORE_NATIVE","concurrencyMode":"PESSIMISTIC","versionRetentionPeriod":"3600s","earliestVersionTime":"2024-11-21T17:47:12.821827Z","appEngineIntegrationMode":"DISABLED","keyPrefix":"s","pointInTimeRecoveryEnablement":"POINT_IN_TIME_RECOVERY_DISABLED","deleteProtectionState":"DELETE_PROTECTION_DISABLED","etag":"IMXEjNSC7okDMJmYgd/97YkD"}
[2024-11-21T18:09:23.317Z] database_type: FIRESTORE_NATIVE
[2024-11-21T18:09:23.317Z] database_type: FIRESTORE_NATIVE
[2024-11-21T18:09:23.318Z] > command requires scopes: ["email","openid","https://www.googleapis.com/auth/cloudplatformprojects.readonly","https://www.googleapis.com/auth/firebase","https://www.googleapis.com/auth/cloud-platform"]
[2024-11-21T18:09:23.318Z] > authorizing via signed-in user ([email protected])
[2024-11-21T18:09:23.318Z] [iam] checking project test-non-default-rules for permissions ["firebase.projects.get"]
[2024-11-21T18:09:23.319Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:23.319Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:23.319Z] >>> [apiv2][query] POST https://cloudresourcemanager.googleapis.com/v1/projects/test-non-default-rules:testIamPermissions [none]
[2024-11-21T18:09:23.319Z] >>> [apiv2][(partial)header] POST https://cloudresourcemanager.googleapis.com/v1/projects/test-non-default-rules:testIamPermissions x-goog-quota-user=projects/test-non-default-rules
[2024-11-21T18:09:23.319Z] >>> [apiv2][body] POST https://cloudresourcemanager.googleapis.com/v1/projects/test-non-default-rules:testIamPermissions {"permissions":["firebase.projects.get"]}
[2024-11-21T18:09:23.516Z] <<< [apiv2][status] POST https://cloudresourcemanager.googleapis.com/v1/projects/test-non-default-rules:testIamPermissions 200
[2024-11-21T18:09:23.517Z] <<< [apiv2][body] POST https://cloudresourcemanager.googleapis.com/v1/projects/test-non-default-rules:testIamPermissions {"permissions":["firebase.projects.get"]}

Firestore Security Rules allow you to define how and when to allow
requests. You can keep these rules in your project directory
and publish them with firebase deploy.

? What file should be used for Firestore Rules? (firestore.rules) �[66D�[66C�[2K�[G? What file should be used for Firestore Rules? firestore.rules�[63D�[63C
[2024-11-21T18:09:30.665Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:30.665Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:30.666Z] >>> [apiv2][query] GET https://firebaserules.googleapis.com/v1/projects/test-non-default-rules/releases pageSize=10&pageToken=
[2024-11-21T18:09:30.874Z] <<< [apiv2][status] GET https://firebaserules.googleapis.com/v1/projects/test-non-default-rules/releases 200
[2024-11-21T18:09:30.875Z] <<< [apiv2][body] GET https://firebaserules.googleapis.com/v1/projects/test-non-default-rules/releases {"releases":[{"name":"projects/test-non-default-rules/releases/cloud.firestore","rulesetName":"projects/test-non-default-rules/rulesets/d1e2cacb-ebc6-43ec-8992-b7af319e405d","createTime":"2024-11-21T17:47:14.297744Z","updateTime":"2024-11-21T17:56:38.723378Z"},{"name":"projects/test-non-default-rules/releases/cloud.firestore/non-default","rulesetName":"projects/test-non-default-rules/rulesets/7d3651a8-88d9-4f01-9872-35768adcfe2c","createTime":"2024-11-21T17:56:38.907657Z","updateTime":"2024-11-21T17:56:38.907657Z"}]}
[2024-11-21T18:09:30.888Z] Found ruleset: projects/test-non-default-rules/rulesets/7d3651a8-88d9-4f01-9872-35768adcfe2c
[2024-11-21T18:09:30.889Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:30.889Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:30.889Z] >>> [apiv2][query] GET https://firebaserules.googleapis.com/v1/projects/test-non-default-rules/rulesets/7d3651a8-88d9-4f01-9872-35768adcfe2c [none]
[2024-11-21T18:09:31.024Z] <<< [apiv2][status] GET https://firebaserules.googleapis.com/v1/projects/test-non-default-rules/rulesets/7d3651a8-88d9-4f01-9872-35768adcfe2c 200
[2024-11-21T18:09:31.024Z] <<< [apiv2][body] GET https://firebaserules.googleapis.com/v1/projects/test-non-default-rules/rulesets/7d3651a8-88d9-4f01-9872-35768adcfe2c [omitted]

Firestore indexes allow you to perform complex queries while
maintaining performance that scales with the size of the result
set. You can keep index definitions in your project directory
and publish them with firebase deploy.

? What file should be used for Firestore indexes? (firestore.indexes.json) �[75D�[75C�[2K�[G? What file should be used for Firestore indexes? firestore.indexes.json�[72D�[72C
[2024-11-21T18:09:31.625Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:31.626Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:31.626Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:31.626Z] Checked if tokens are valid: true, expires at: 1732213850733
[2024-11-21T18:09:31.626Z] >>> [apiv2][query] GET https://firestore.googleapis.com/v1/projects/test-non-default-rules/databases/(default)/collectionGroups/-/indexes [none]
[2024-11-21T18:09:31.629Z] >>> [apiv2][query] GET https://firestore.googleapis.com/v1/projects/test-non-default-rules/databases/(default)/collectionGroups/-/fields?filter=indexConfig.usesAncestorConfig=false OR ttlConfig:* [none]
[2024-11-21T18:09:32.005Z] <<< [apiv2][status] GET https://firestore.googleapis.com/v1/projects/test-non-default-rules/databases/(default)/collectionGroups/-/fields?filter=indexConfig.usesAncestorConfig=false OR ttlConfig:* 200
[2024-11-21T18:09:32.006Z] <<< [apiv2][body] GET https://firestore.googleapis.com/v1/projects/test-non-default-rules/databases/(default)/collectionGroups/-/fields?filter=indexConfig.usesAncestorConfig=false OR ttlConfig:* {"fields":[{"name":"projects/test-non-default-rules/databases/(default)/collectionGroups/__default__/fields/*","indexConfig":{"indexes":[{"queryScope":"COLLECTION","fields":[{"fieldPath":"*","order":"ASCENDING"}],"state":"READY"},{"queryScope":"COLLECTION","fields":[{"fieldPath":"*","order":"DESCENDING"}],"state":"READY"},{"queryScope":"COLLECTION","fields":[{"fieldPath":"*","arrayConfig":"CONTAINS"}],"state":"READY"}]}}]}
[2024-11-21T18:09:32.007Z] <<< [apiv2][status] GET https://firestore.googleapis.com/v1/projects/test-non-default-rules/databases/(default)/collectionGroups/-/indexes 200
[2024-11-21T18:09:32.007Z] <<< [apiv2][body] GET https://firestore.googleapis.com/v1/projects/test-non-default-rules/databases/(default)/collectionGroups/-/indexes {}

i  Writing configuration info to firebase.json... 
i  Writing project information to .firebaserc... 

✔  Firebase initialization complete! 
@puf
Copy link

puf commented Nov 22, 2024

I dug into the code a bit today, and it seems that this is the code that is handling this flow.

      return getRulesFromConsole(setup.projectId).then((contents: any) => {
        return config.writeProjectFile(setup.config.firestore.rules, contents);
      });

That getRulesFromConsole executes this snippet to get the rules from the server:

function getRulesFromConsole(projectId: string): Promise<any> {
  return gcp.rules
    .getLatestRulesetName(projectId, "cloud.firestore")
    .then((name) => {
      if (!name) {
        logger.debug("No rulesets found, using default.");
        return [{ name: DEFAULT_RULES_FILE, content: getDefaultRules() }];
      }

And the getLatestRulesetName in there is this snippet to get the latest ruleset from the server:

export async function getLatestRulesetName(
  projectId: string,
  service: string,
): Promise<string | null> {
  const releases = await listAllReleases(projectId);
  const prefix = `projects/${projectId}/releases/${service}`;
  const release = releases.find((r) => r.name.startsWith(prefix));

  if (!release) {
    return null;
  }
  return release.rulesetName;
}

I'm not an expert here, but in none of this code there is any consideration for the current database or even the fact that a project can have multiple databases, so I suspect it just returned whatever ruleset was last deployed to the project (rather than to the current database).

@aemelyanovff Can you confirm this hypothesis, by deploying another set of rules to the default database in your terminal tab and checking if you then indeed get those rules in the second tab?

If that is indeed the problem, the code here needs to be updated to handle the fact that there can be multiple Firestore databases in a project and that each of those has their own history of rulesets.

@aemelyanovff
Copy link
Author

@aemelyanovff Can you confirm this hypothesis, by deploying another set of rules to the default database in your terminal tab and checking if you then indeed get those rules in the second tab?

No, I always get rules from the non-default database...

@aalej
Copy link
Contributor

aalej commented Nov 26, 2024

Hey @aemelyanovff, apologies for the issue this has caused and thanks for the detailed report. I'm able to reproduce this, let me raise this to our engineering team so they can take a look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants