skip to Main Content

Background

I have a website with a public messaging board and want authenticated users to add posts to the collection (writing). However I do not want the users to overwrite posts that dont belong to them (posts from other users).
The only way I know of is using the $uid===$uid parameter in a rule like below:

{
  "rules": {         
    "posts":{
      "$uid": {
        ".read": "auth != null && auth.token.email_verified == true",
        ".write": "auth !== null && auth.uid === $uid",
      }

Problem

Using the $uid parameter requires the uid to already exist in the posts array. So I am confused about this circular reference. Basically you need the data to exist in the collection to be able to write to it but you cant write to the collection because collection doesn’t contain the data (with the matching uid).

Would apprecite any ideas or pointers for how I can proceed forwards to break this circular reference. How do other apps that use firebase in the backend, secure their write rules with such a conundrum?

For reference, this is a print screen of my database collection when a user adds a post. enter image description here

Edited on 18th September to clear any confusion in the comment threads below. I am including the write rules which have worked and to further demonstrate the problem I am having with uid===$uid rule

  1. this write rule works but is very insecure because it allows anyone to write/edit other user data.
{
  "rules": {         
    "posts":{
      "$uid": {
        ".read": "auth != null && auth.token.email_verified == true",
        ".write": true
      }

  1. this write rule works but is still insecure because authenticated users can write/edit other user data.
{
  "rules": {         
    "posts":{
      "$uid": {
        ".read": "auth != null && auth.token.email_verified == true",
        ".write": "auth != null && auth.token.email_verified == true"
      }

  1. The write rule which I am having trouble with, is already in the Background section above.

To refine the question and so that we can all get as clear and straight an answer as possible. Why is this 3rd rule which has the $uid parameter interfering with my ability to write compared to rules 1 and 2 above?

Edited as requested to include the relevant Back-end code that the rule is supposed to work against:

import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import {
  ComponentSize,
  UserNoLongerActive,
} from '../../../../libs/constants/constants';
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import { OjasPointsComponent } from '../../../../libs/components/ojas-points/ojas-points.component';
import { RouterLink } from '@angular/router';
import { TribeLogoComponent } from '../../../../libs/components/semen-components/tribe-logo/tribe-logo.component';
import {
  OjasPointsOperation,
  ShareExperiencePayload,
} from '../../../../libs/types/types';
import { BenefitUserExperienceService } from '../benefit-user-experience.service';
import { OjasPointsService } from '../../../../libs/services/ojas-points.service';
import { UserService } from '../../../../libs/services/user.service';

@Component({
  selector: 'app-benefit-user-experience',
  standalone: true,
  imports: [
    CommonModule,
    NgbTooltip,
    OjasPointsComponent,
    RouterLink,
    TribeLogoComponent,
  ],
  templateUrl: './benefit-user-experience.component.html',
  styleUrls: ['./benefit-user-experience.component.scss'],
})
export class BenefitUserExperienceComponent {
  @Input() public experience: ShareExperiencePayload;

  public username: string;
  protected readonly ComponentSize = ComponentSize;

  constructor(
    private readonly benefitUserExperienceService: BenefitUserExperienceService,
    private readonly ojasPointsService: OjasPointsService,
    private userService: UserService,
  ) {}

  ngOnInit() {
    this.username =
      this.userService.getUserDataFromLocalStorage().userInfo.username;
  }

  public incrementVoteCounter($event: any) {
    $event.stopPropagation();
    let updatedExperience: ShareExperiencePayload | undefined = this.experience;

    if (updatedExperience) {
      updatedExperience.voteCounter++;
      updatedExperience.usersListWhoLikedTheExperience.push(this.username);
      updatedExperience.usersListWhoDislikedTheExperience =
        updatedExperience.usersListWhoDislikedTheExperience.filter(
          (item) => item !== this.username,
        );

      this.benefitUserExperienceService
        .updateBenefitUserExperience(updatedExperience)
        .then(async () => {
          if (updatedExperience?.userId) {
            await this.ojasPointsService.UpdateOjasPoints({
              userUid: updatedExperience?.userId,
              operation: OjasPointsOperation.increment,
              amount: 0.5,
            });
          }
        })
        .catch((error) => {
          console.error('error occurred while updating database', error);
        });
    }
  }

  public decrementVoteCounter($event: any) {
    $event.stopPropagation();
    let updatedExperience: ShareExperiencePayload | undefined = this.experience;

    if (updatedExperience) {
      updatedExperience.voteCounter--;
      updatedExperience.usersListWhoDislikedTheExperience.push(this.username);
      updatedExperience.usersListWhoLikedTheExperience =
        updatedExperience.usersListWhoLikedTheExperience.filter(
          (item) => item !== this.username,
        );

      this.benefitUserExperienceService
        .updateBenefitUserExperience(updatedExperience)
        .then(async () => {
          if (updatedExperience?.userId) {
            await this.ojasPointsService.UpdateOjasPoints({
              userUid: updatedExperience?.userId,
              operation: OjasPointsOperation.decrement,
              amount: 0.5,
            });
          }
        })
        .catch((error) => {
          console.error('error occurred while updating database', error);
        });
    }
  }

  protected readonly UserNoLongerActive = UserNoLongerActive;
}

2

Answers


  1. Following the comments under your question, and in particular "Do you mind answering with an example of how this can be done? As I cant find an example from google’s documentation for "create" in security rules."

    Your following write rule

    {
      "rules": {         
        "posts":{
          "$uid": {
            ".read": "auth != null && auth.token.email_verified == true",
            ".write": "auth !== null && auth.uid === $uid",
          }
        }
      }
    }
    

    does allow:

    1. Creating (i.e. setting) data of a RTDB node under the posts node with a key that is equal to the user’s uid.
    2. Updating such a node

    Of course the user must be authenticated in order for this rule to work.


    Note that the write rules type actually covers creating and updating. There isn’t any rules type specifically dedicated to creation, nor another type specifically dedicated to updating, like we find in Firestore security rules.

    Login or Signup to reply.
  2. Just want to clarify the missing rules not mentioned here.

    Consider not any of those 3 rules, but the following:

    {
      "rules": {         
        "posts": {
          "$uid": {
            ".read": "auth != null && auth.token.email_verified == true",
            ".write": "auth != null && auth.token.email_verified == true && !data.exists()"
          }
        }
      }
    }
    

    Here you will be able to create new entry only, without the ability to edit. Notice that "$uid" key is not used here.

    Now let’s add another condition to also allow user to edit

    {
      "rules": {         
        "posts": {
          "$uid": {
            ".read": "auth != null && auth.token.email_verified == true",
            ".write": "auth != null && auth.token.email_verified == true && (!data.exists() || auth.uid === $uid)"
          }
        }
      }
    }
    

    Now we have rule that allow us to create and also edit existing "posts" for the particular $uid key.

    Now for the sake of learning purposes, let’s take a look of the following rule:

    {
      "rules": {         
        "posts":{
          ".read": "auth != null && auth.token.email_verified == true",
          ".write": "!data.exists()"
        }
      }
    }
    

    Notice the missing child key $uid, it is now directly under the "posts" object itself. This entry actually will make you can’t create anything under the posts if you have existing "posts" object already, because the "posts" data already exist in that particular closure.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search