Restringir el acceso de niños/campo con reglas de seguridad
Estoy escribiendo una aplicación que permite a los usuarios enviar nominaciones que se moderan antes de mostrarlas a otros usuarios. Esto requiere una serie de restricciones que hasta ahora no he logrado implementar con reglas de seguridad:
- Ocultar cualquier nominación que aún no haya sido aprobada
- Ocultar campos privados del envío (teléfono, estado de aprobación, fecha de creación, etc.)
Mis reglas actuales son las siguientes:
{
"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
}
}
}
}
}
Las reglas secundarias (p. ej. $nomination
) no impiden que el padre lea al niño completo. Si escucho child_added
en https://my.firebaseio.com/nominations , felizmente devuelve a todos los niños y todos sus datos, incluso con las reglas de seguridad anteriores implementadas.
Mi idea alternativa actual para esto es mantener un nodo separado con nombre approved
y simplemente mover los datos entre listas cada vez que alguien aprueba o rechaza una nominación, pero parece un enfoque terriblemente fallido.
Actualizar
Siguiendo el excelente comentario de Michael Lehenbauer , volví a implementar la idea inicial con el mínimo esfuerzo.
La nueva estructura de datos es la siguiente:
my-firebase
|
`- nominations
|
`- entries
| |
| `- private
| `- public
|
`- status
|
`- pending
`- approved
`- rejected
Cada nominación se almacena entries
con datos privados como número de teléfono, correo electrónico, etc. private
y datos visibles públicamente en public
.
Las reglas actualizadas son las siguientes:
{
"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"
}
}
}
}
}
}
Y la implementación 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 única parte de la implementación con la que tengo problemas es el manejo de los cambios de estado; mi enfoque actual seguramente se puede mejorar:
_.each([pending, approved, rejected], function(status) {
status.on('child_added', function(snapshot) {
$('#' + snapshot.name()).removeClass('pending approved rejected').addClass(status.name())
})
})
Estaba planeando usarlo child_changed
pero nominations/status
no he podido hacerlo funcionar de manera confiable.
Kato tiene razón. Es importante comprender que las reglas de seguridad nunca filtran datos. Para cualquier ubicación, podrá leer todos los datos (incluidos los secundarios) o ninguno. Entonces, en el caso de sus reglas, tener un ".read": verdadero en "nominaciones" niega todas las demás reglas.
Entonces, el enfoque que recomendaría aquí es tener 3 listas. Uno que contiene los datos de las nominaciones, otro que contiene la lista de nominaciones aprobadas y otro que contiene la lista de nominaciones pendientes.
Tus reglas podrían ser así:
{
"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 usuario no autenticado podría agregar una nueva nominación con:
var id = ref.child('nominations').push({ public_data: "whatever", phone: "555-1234" });
ref.child('pending_list').child(id).set(true);
Un usuario autenticado podría aprobar un mensaje con:
ref.child('pending_list').child(id).remove();
ref.child('approved_list').child(id).set(true);
Y para representar las listas aprobadas y pendientes, usarías un código como:
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 esta manera, utiliza lista_aprobada y lista_pendiente como listas ligeras que pueden ser enumeradas (por usuarios autenticados y no autenticados, respectivamente) y almacena todos los datos de nominación reales en la lista de nominaciones (que nadie puede enumerar directamente).
Si asimilo completamente la forma en que funcionan las reglas de seguridad (apenas las estoy aprendiendo yo mismo), entonces, cuando alguna regla permite el acceso, se concede el acceso. Así, se leen de la siguiente manera:
- nominaciones ".read": true, ACCESO CONCEDIDO
- otras reglas: no leer
Además, si se elimina esa regla, $nominationId
".read" otorga acceso si se aprueba el registro; por lo tanto, el .read
in phone
y state
se vuelve superfluo cada vez que se aprueba.
Probablemente sería más sencillo dividir esto en public/
y private/
niños, así:
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
ACTUALIZAR
Pensando en esto aún más, creo que todavía encontrará un problema al hacer approved/
públicos, lo que le permitirá enumerar los registros, y tenerlos approved/restricted/
privados. Es posible que los datos restringidos también necesiten su propia ruta en este caso de uso.