web-dev-qa-db-fra.com

Restreindre l'accès des enfants/champs avec des règles de sécurité

J'écris une application qui permet aux utilisateurs de soumettre des nominations qui sont modérées avant d'être affichées à d'autres utilisateurs. Cela nécessite un certain nombre de restrictions que je n'ai jusqu'à présent pas réussi à mettre en œuvre avec des règles de sécurité:

  1. Masquer les candidatures qui n'ont pas encore été approuvées
  2. Masquer les champs privés de la soumission (téléphone, statut d'approbation, date de création, etc.)

Mes règles actuelles sont les suivantes:

{
    "rules": {
        "nominations": {
            ".read": true,

            "$nominationId": {
                ".read": "data.child('state').val() == 'approved' || auth != null", // Only read approved nominations if not authenticated
                ".write": "!data.exists()", // Only allow new nominations to be created

                "phone": {
                    ".read": "auth != null" // Only allow authenticated users to read phone number
                },

                "state": {
                    ".read": "auth != null", // Only allow authenticated users to read approval state
                    ".write": "auth != null" // Only allow authenticated users to change state
                }
            }
        }
    }
}

Les règles enfants (par exemple, $nomination) n'empêchent pas la lecture complète de l'enfant à partir du parent. Si j'écoute child_added sur https://my.firebaseio.com/nominations , il renvoie avec joie tous les enfants et toutes leurs données, même avec les règles de sécurité susmentionnées.

Mon solution actuelle consiste à conserver un nœud distinct nommé approved et à déplacer simplement les données entre les listes chaque fois que quelqu'un approuve ou refuse une candidature, mais cela semble être une approche horriblement cassée.

Mettre à jour

Après l’excellent commentaire de Michael Lehenbauer , j’ai repris l’idée initiale avec un minimum d’efforts.

La nouvelle structure de données est la suivante:

my-firebase
    |
    `- nominations
        |
        `- entries
        |   |
        |   `- private
        |   `- public
        |
        `- status
            |
            `- pending
            `- approved
            `- rejected

Chaque candidature est stockée sous entries avec des données privées telles que numéro de téléphone, adresse électronique, etc. sous private et les données accessibles au public sous public.

Les règles mises à jour sont les suivantes:

{
    "rules": {
        "nominations": {
            "entries": {
                "$id": {
                    ".write": "!data.exists()",

                    "public": {
                        ".read": true,
                    },

                    "private": {
                        ".read": "auth != null"
                    }
                }
            },

            "status": {
                "pending": {
                    ".read": "auth != null",

                    "$id": {
                        ".write": "root.child('nominations/entries').child($id).exists() && (auth != null || newData.val() == true)"
                    }
                },

                "approved": {
                    ".read": true,

                    "$id": {
                        ".write": "root.child('nominations/entries').child($id).exists() && auth != null"
                    }
                },


                "rejected": {
                    ".read": "auth != null",

                    "$id": {
                        ".write": "root.child('nominations/entries').child($id).exists() && auth != null"
                    }
                }
            }
        }
    }
}

Et l'implémentation de JavaScript:

var db = new Firebase('https://my.firebaseio.com')
var nominations = db.child('nominations')

var entries = nominations.child('entries')

var status = nominations.child('status')
var pending = status.child('pending')
var approved = status.child('approved')
var rejected = status.child('rejected')

// Create nomination via form input (not shown)
var createNomination = function() {
    var data = {
        public: {
            name: 'Foo',
            age: 20
        },

        private: {
            createdAt: new Date().getTime(),
            phone: 123456
        }
    }

    var nomination = entries.Push()
    nomination.setWithPriority(data, data.private.createdAt)

    pending.child(nomination.name()).set(true)    
}

// Retrieve current nomination status
var getStatus = function(id, callback) {
    approved.child(id).once('value', function(snapshot) {
        if (snapshot.val()) {
            callback(id, 'approved')
        } else {
            rejected.child(id).once('value', function(snapshot) {
                callback(id, snapshot.val() ? 'rejected' : 'pending')
            })
        }
    })
}

// Change status of nomination
var changeStatus = function(id, from, to) {
    status.child(from).child(id).remove()
    status.child(to).child(id).set(true)
}

La seule partie de la mise en œuvre qui me pose problème concerne les changements de statut. Mon approche actuelle peut sûrement être améliorée:

_.each([pending, approved, rejected], function(status) {
    status.on('child_added', function(snapshot) {
        $('#' + snapshot.name()).removeClass('pending approved rejected').addClass(status.name())
    })
})

J'avais prévu d'utiliser child_changed sur nominations/status mais je n'ai pas réussi à le faire fonctionner de manière fiable.

49
Simen Brekken

Kato a raison. Il est important de comprendre que les règles de sécurité ne doivent jamais filter data. Quel que soit le lieu, vous pourrez lire toutes les données (ou leurs enfants), voire aucune. Ainsi, dans le cas de vos règles, un ".read": true sous "nominations" annule toutes vos autres règles.

Donc, l’approche que je recommanderais ici est d’avoir 3 listes. Un contenant les données de candidature, un pour contenir la liste des candidatures approuvées et un pour contenir la liste des candidatures en attente.

Vos règles pourraient être comme suit:

{
  "rules": {
    // The actual nominations.  Each will be stored with a unique ID.
    "nominations": {
      "$id": {
        ".write": "!data.exists()", // anybody can create new nominations, but not overwrite existing ones.
        "public_data": {
          ".read": true // everybody can read the public data.
        },
        "phone": {
          ".read": "auth != null", // only authenticated users can read the phone number.
        }
      }
    },
    "approved_list": {
      ".read": true, // everybody can read the approved nominations list.
      "$id": {
        // Authenticated users can add the id of a nomination to the approved list 
        // by creating a child with the nomination id as the name and true as the value.
        ".write": "auth != null && root.child('nominations').child($id).exists() && newData.val() == true"
      }
    },
    "pending_list": {
      ".read": "auth != null", // Only authenticated users can read the pending list.
      "$id": {
        // Any user can add a nomination to the pending list, to be moderated by
        // an authenticated user (who can then delete it from this list).
        ".write": "root.child('nominations').child($id).exists() && (newData.val() == true || auth != null)"
      }
    }
  }
}

Un utilisateur non authentifié pourrait ajouter une nouvelle nomination avec:

var id = ref.child('nominations').Push({ public_data: "whatever", phone: "555-1234" });
ref.child('pending_list').child(id).set(true);

Un utilisateur authentifié pourrait approuver un message avec:

ref.child('pending_list').child(id).remove();
ref.child('approved_list').child(id).set(true);

Et pour rendre les listes approuvées et en attente, vous utiliseriez du code comme suit:

ref.child('approved_list').on('child_added', function(childSnapshot) {
  var nominationId = childSnapshot.name();
  ref.child('nominations').child(nominationId).child('public_data').on('value', function(nominationDataSnap) {
    console.log(nominationDataSnap.val());
  });
});

De cette manière, vous approuvezlist_pert etlist_attendr_list en tant que listes légères pouvant être énumérées (par des utilisateurs non authentifiés et authentifiés respectivement) et stockez toutes les données de nomination réelles dans la liste de nominations (que personne ne peut directement énumérer).

46
Michael Lehenbauer

Si je comprends parfaitement le fonctionnement des règles de sécurité (je les apprends moi-même), lorsqu'une règle autorise l'accès, l'accès est accordé. Ainsi, ils se lisent comme suit:

  • nominations ".read": vraies, ACCÈS ACCORDÉ
  • autres règles: pas lu

De plus, si cette règle est supprimée, $nominationId ".read" autorise l'accès si l'enregistrement est approuvé; Par conséquent, le .read dans phone et le state deviennent superflus chaque fois qu'il est approuvé.

Il serait probablement plus simple de décomposer cela en enfants public/ et private/, comme ceci:

nominations/unapproved/          # only visible to logged in users
nominations/approved/            # visible to anyone (move record here after approval)
nominations/approved/public/     # things everyone can see
nominations/approved/restricted/ # things like phone number, which are restricted

METTRE À JOUR

En y réfléchissant encore plus, je pense que vous rencontrerez toujours un problème pour rendre approved/ public, ce qui vous permettra de répertorier les enregistrements et d'avoir approved/restricted/ privé. Les données restreintes peuvent également nécessiter leur propre chemin d'accès dans ce cas d'utilisation.

4
Kato

ce fil est un peu obsolète et il peut avoir une solution via des règles, mais comme le dit la vidéo, c’est une astuce: https://youtu.be/5hYMDfDoHpI?t=8m50s

Ce n'est peut-être pas une bonne pratique car les documents de Firebase indiquent que les règles ne sont pas des filtres: https://firebase.google.com/docs/database/security/securing-data

Je ne suis pas un spécialiste de la sécurité mais j'ai testé le truc et ça a bien fonctionné pour moi. :)

J'espère donc une meilleure compréhension des problèmes de sécurité autour de cette implémentation.

0
Rovel