0

Update 2019-11-03: Added a live minimal reproduction of the error. After loading the link in Chrome, hit ctrl+shift+i and select the console to see the output. I have tried hard to make sure this is doing exactly what my original project's code is doing; we'll see if that's the case, eh? The rules file for the shard is the same as the original post below. The source is available on GitHub.

<!DOCTYPE html>
<html>
<body>
 <script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-app.js"></script>
 <script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-auth.js"></script>
 <script src="https://www.gstatic.com/firebasejs/7.2.3/firebase-database.js"></script>
 <script>
  const config={
   apiKey: "AIzaSyDLMc0GUf5n2nQa3aqpELQu7lziprQOGs8",
   authDomain: "shardautherror.firebaseapp.com",
   databaseURL: "https://shardautherror.firebaseio.com",
   projectId: "shardautherror",
   storageBucket: "shardautherror.appspot.com",
   messagingSenderId: "841096336504",
   appId: "1:841096336504:web:9899961c8250caa552498d"
  };

  const shard="https://shardautherror-1e9ed.firebaseio.com/";

  async function init(){
   try{
    firebase.database.enableLogging(true);
    const defaultApp=firebase.initializeApp(config);
    const auth=defaultApp.auth();
    const s="alice@example.com";
    await auth.signInWithEmailAndPassword(s,s);
    const uid= auth.currentUser.uid;
    const shardApp=firebase.initializeApp({databaseURL:shard},'dbAppShard');
    const db=firebase.database(shardApp);
    const ref= db.ref("/chat/"+uid+"/fail/"+uid);
    const time= firebase.database.ServerValue.TIMESTAMP;
    ref.set({time});
   } catch(e) {
    console.error("init failed",e);
   }
  }

  init();
 </script>
</body>
</html>

Original Post:

These rules work in the simulator, but not in my real web app. The simulator path and payload are the same as shown in the database logging output below.

database.rules.json (main targets both shards to use this rules file; I verified on deploy)

{
 "rules":{
  "chat":{
   "$ownerId":{
    "fail":{
     "$pId":{
      ".write": "$pId== auth.uid&& $ownerId== auth.uid",
      "time":{".validate": "newData.val()== now"},
      "$other":{".validate": "newData.isString()&& newData.val().length>= 28"}
     }
    }
   }
  }
 }
}

Firebase logging output of set command that is failing. It just writes a single value called time. This is my first time trying to use rtdb. I have it set up with sharding. It acquires the shard name from firestore right before it tryies to access the realtime database, but it does not seem like a race condition (despite the logging output) for reasons I'll outline below.

index.esm.js:81 [2019-10-19T03:02:53.281Z]  @firebase/database: 0: set 
 {"path":"/chat/rpNIK41hNpWkYY2KqndkwCzPJuF3/fail/rpNIK41hNpWkYY2KqndkwCzPJuF3",
  "value":{"time":{".sv":"timestamp"}},"priority":null} 
22:02:53.285 index.esm.js:81 [2019-10-19T03:02:53.285Z]  @firebase/database:
 p:0: Buffering put: /chat/rpNIK41hNpWkYY2KqndkwCzPJuF3/fail/rpNIK41hNpWkYY2KqndkwCzPJuF3 
22:02:53.293 index.esm.js:81 [2019-10-19T03:02:53.293Z]  @firebase/database:
 p:0: Making a connection attempt 
22:02:53.294 index.esm.js:81 [2019-10-19T03:02:53.294Z]  @firebase/database:
 getToken() completed. Creating connection. 
22:02:53.295 index.esm.js:81 [2019-10-19T03:02:53.295Z]  @firebase/database:
 c:0:0: Connection created 
22:02:53.296 index.esm.js:81 [2019-10-19T03:02:53.296Z]  @firebase/database:
 p:0: Auth token refreshed 
22:02:53.298 index.esm.js:81 [2019-10-19T03:02:53.298Z]  @firebase/database:
 c:0:0:0 Websocket connecting to wss://quickstart-1551998385825-7f7a6.firebaseio.com/.ws?v=5 
22:02:53.534 index.esm.js:81 [2019-10-19T03:02:53.534Z]  @firebase/database:
 c:0:0:0 Websocket connected. 
22:02:53.539 index.esm.js:81 [2019-10-19T03:02:53.539Z]  @firebase/database:
 c:0:0: Realtime connection established. 
22:02:53.539 index.esm.js:81 [2019-10-19T03:02:53.539Z]  @firebase/database:
 p:0: connection ready 
22:02:53.542 index.esm.js:81 [2019-10-19T03:02:53.541Z]  @firebase/database:
 p:0: reportStats {"c":{"sdk.js.7-0-0":1}} 
22:02:53.542 index.esm.js:81 [2019-10-19T03:02:53.542Z]  @firebase/database:
 p:0: {"r":1,"a":"s","b":{"c":{"sdk.js.7-0-0":1}}} 
22:02:53.546 index.esm.js:81 [2019-10-19T03:02:53.546Z]  @firebase/database:
 p:0: {"r":2,"a":"p","b":{"p":"/chat/rpNIK41hNpWkYY2KqndkwCzPJuF3/fail/rpNIK41hNpWkYY2KqndkwCzPJuF3",
  "d":{"time":{".sv":"timestamp"}}}} 
22:02:53.591 index.esm.js:81 [2019-10-19T03:02:53.591Z]  @firebase/database:
 p:0: from server: {"r":1,"b":{"s":"ok","d":""}} 
22:02:53.595 index.esm.js:81 [2019-10-19T03:02:53.595Z]  @firebase/database:
 c:0:0: Primary connection is healthy. 
22:02:53.596 index.esm.js:81 [2019-10-19T03:02:53.596Z]  @firebase/database:
 p:0: from server: {"r":2,"b":{"s":"permission_denied","d":"Permission denied"}} 
22:02:53.597 index.esm.js:81 [2019-10-19T03:02:53.597Z]  @firebase/database:
 p:0: p response {"s":"permission_denied","d":"Permission denied"} 

So, after this, if I update the rule to ".write": true, the write of the timestamp succeeds. In the log it shows "r":3 ..., so I know it didn't throw away the connection and restart. If I then change it to ".write": "auth.uid != null", or ".write": "auth != null", (thus, not checking ownership, just whether the client logs in, unlike above) it denies permission again with "r":4 ..." indicating the 4th request. So, it seems like I have a total failure of the client to authenticate to the shard.

Simulator output: simulator shows success with the same rules

Recommendations? I'm sure I'm doing something wrong.

By the way, the user documentation is all over the place... Are all of these actually valid?

"baskets": {
  ".read": "auth.uid != null &&// auth.uid!= null from https://firebase.google.com/docs/database/security/securing-data
".read": "auth != null && auth.uid == $uid" // auth != null from https://firebase.google.com/docs/database/security/user-security
".write": "$user_id === auth.uid" // triple equal from https://firebase.google.com/docs/database/security/user-security
 ".write": "request.auth.uid == uid" // request.auth from realtime database tab of content owner access from https://firebase.google.com/docs/rules/basics
Chris Chiasson
  • 547
  • 8
  • 17
  • To be clear, are you using both Firestore as well as the Real Time Database? Also, and I am sure you checked, but did you verify that when running the web app you're actually authenticated? – Jay Oct 19 '19 at 14:17
  • The rtdb shard URL is acquired from firestore as mentioned in the question. Other than that, firestore plays no role. The reason it was mentioned at all is because in the debug output of the rtdb access shows the acknowledgement of r1 coming back after r2 is sent, which may(?) not happen if the app established the rtdb connection way earlier. – Chris Chiasson Oct 19 '19 at 17:30
  • @Jay I added a minimal reproduction to the top of the original post. – Chris Chiasson Nov 04 '19 at 01:56
  • As usual, links break and if they do, it can invalidate that part of the question. All I get with that link is a blank page - no error or other info. – Jay Nov 04 '19 at 17:57
  • @Jay The only output is in the console, as mentioned. – Chris Chiasson Nov 04 '19 at 17:58
  • @Jay also, the contents of the html file at the link was included in full below the paragraph. – Chris Chiasson Nov 05 '19 at 01:44
  • Do you have 200,000 simultaneous connections? – Jay Nov 05 '19 at 16:11

2 Answers2

1

There are two questions here and the first part of the question needs more information.

The second part of the question

By the way, the user documentation is all over the place... Are all of these actually valid?

The documentation isn't really all over the place. Each of the rule samples you included were from a different use case.

For example the .read rule in "baskets" applies to that specific node "baskets" ensuring that only authenticated users can read the baskets node. And it will allow any auth'd user to read that node. There's an extra && in that line so not sure what the rest of the rule was.

The second read rule would apply to whatever node it's in and would ensure that the user is authenticated and that only the authenticated user can read that node (i.e. it's their node and nobody else can access it)

For the write's the === (triple equal) is covered in the documentation and says

Note:: == IS TREATED AS ===. If you use == in your security rules, it will be translated to === when the rules are run.

The last write is simply checking that the uid of the request is the currently auth'd user.

Jay
  • 34,438
  • 18
  • 52
  • 81
  • Baskets was included because I was just showing where on the page the rule occurred. The comments to the right indicate what I was pointing out for each line. They seem to use auth != null and auth.uid != null interchangeably, == and === interchangeably (as you addressed, which seems different than in firestore), as well as request.auth and auth interchangeably. – Chris Chiasson Oct 19 '19 at 17:36
  • Understood. The second link where this `auth != null && auth.uid == $uid` is used is read like this; *ensure the auth variable is not null before attempting to read the uid property. That's done to ensure there is an authenticated user *before* trying to read it's uid - attempting to read something from nil us usually bad. On the first statement `auth.uid != null` - if you continue in that example in the link, later on, this happens `auth.uid` again, verifying the property is not nil before trying to read that property is best practice. – Jay Oct 20 '19 at 13:54
  • I understand how to read it. I am saying that taken together, the examples for the pseudo javascript structures they have introduced are not consistent. If they are checking whether auth is falsy first in some rules, they should be doing it everywhere (otherwise, the javascript analogy falls apart; they would be trying to access a property of a possible null). If they are saying that auth is part of request in some rules, they should be doing it everywhere, for the same reason, unless they are also bringing in the concept of globals, treating `request` like `window` in normal javascript. – Chris Chiasson Oct 20 '19 at 22:46
  • By the way, I answered your request for more information in a comment to the original post a little over a day ago, but I forgot to use the @ thing, so it probably didn't notify you. Sorry about that. – Chris Chiasson Oct 20 '19 at 22:56
  • @ChrisChiasson That wasn't what I was saying - let me clarify. In some cases we just want to verify the user is auth'd - so check for auth == nil. In other cases we want to verify the user is auth'd and then we are going to further check to see if the user trying to access the node is *this* user, so we ensure auth.uid is not nil and very that uid = this users uid (for example). If you think the docs are vacuous or inconsistent (which happens!), please let the firebase team know via a bug report as we cannot fix them here. – Jay Oct 21 '19 at 14:11
  • You suggested contacting Firebase. I put Firebase support's replies regarding the rules at the end of my answer. Thanks for your time on this question. Take care. – Chris Chiasson Nov 16 '19 at 18:53
1

This line

const shardApp=firebase.initializeApp({databaseURL:shard},'dbAppShard');

needs to change to:

const shardApp=firebase.initializeApp({...config,databaseURL:shard},'dbAppShard');

This will work if the client has already signed into the shardApp's auth() object recently. If not, it can be accomplished via functions like shardApp.auth().signInWithEmailAndPassword(email,pw) and shardApp.auth().updateCurrentUser(user), where user could be from e.g. the default app's auth().currentUser object. Alternatively, it is still presently possible to to just use the default app, and then call defaultApp.database(shardURL), but at the time of writing I don't know if this method will remain in Firebase.

The current firebase sharding documentation just shows the client passing only the databaseURL parameter to initializeApp, which is not sufficient to work with auth based rules. Unlike this admin Functions answer, and this older-syntax multi-database sharding intro blog post, the current docs require the client to construct and manage multiple app objects. As Firebase support and I found out, the non-default apps do not currently pull the API key and other parameters from the default app object (aka the first app, created with no 2nd argument to initializeApp).

Firebase support has answered the side questions I posed about rules:

  • A) RTDB uses auth, whereas Firestore uses request.auth
  • B) Omitting the auth!=null pre-check before checking auth.uid!=null is ok in this case, because dereference errors on an object deny the write.
  • C) It is confirmed that == is converted to === in the RTDB case.

Note that A) means this example from the documentation is wrong, because it uses request.auth in RTDB:

{
  "rules": {
    "some_path": {
      "$uid": {
        // Allow only authenticated content owners access to their data
        ".read": "request.auth.uid == uid"
        ".write": "request.auth.uid == uid"
      }
    }
  }
}
Chris Chiasson
  • 547
  • 8
  • 17