I'm having issues implementing basic security rules with Firebase (I read documentation on Firebase and StackExchange but cannot make security rules work):
The model (Emberjs representation of the model):
App.User = DS.Model.extend({
uid: DS.attr('string'),
displayName: DS.attr('string'),
books: DS.hasMany('statistic', { inverse: 'user', async: true}),
actions: DS.hasMany('action', { inverse: 'user', async: true}),
});
App.Action = DS.Model.extend({
date: DS.attr('date'),
actionType: DS.attr('string'),
comment: DS.attr('string'),
user: DS.belongsTo('user', {inverse: 'actions', async: true} )
});
App.Book = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
user: DS.belongsTo('user', { inverse: 'books', async: true} )
});
The 3 nodes (models) are stored directly in the root of the Firebase app. The Book and Action models have a user field (property).
What are the rules to writes so that:
Only the user identified in the user field of the Book and Action models (nodes) can have read and write access to their own data? (The value of the user field in Book and Action must be equal to the value of auth.uid in Firebase for the user to be granted the read and write privileges.)
That the users can only access the information of the User model (node) that pertain to them?
Thanks
It is important to understand the structure of data in the Firebase.
Basically, there are two ways to write security rules. You either set up security rules right under books/ or you write security rules for each model attribute separately. Or a combination of both but make sure you understand the top-down principle first.
I prefer to write rules for each attribute separately, it is more easily maintainable and testable.
But in your case, because other users don't need to acces some part of books or users it is easy to write the rules for the whole model:
"rules" :{
"books": {
"$book_id": {
".read": "data.child('user').val() === auth.uid && auth !== null",
".write": "!data.exists() && newData.child('user').val() === auth.uid || data.child('user').val() === newData.child('uid').val() && auth !== null"
},
"users": {
"$user_id": {
".read": "data.child('uid') === auth.uid",
".write": "!data.exists() && newData.child('uid').val() === auth.uid || data.child('uid').val() === newData.child('uid').val()"
}
}
}
}
I did not test these rules, they can contain flaws, please use the simulator tool to make them bulletproof :]
Check my medium post for more info: https://medium.com/#martinmalinda/emberfire-is-awesome-but-querying-data-and-writing-security-rules-can-be-a-pain-f5370f4decb
Related
I have a survey app built with Ember JS and a Firebase backend with Emberfire adapter.
Here are the relevant portions of my user model:
//app/models/user.js
export default DS.Model.extend({
name: DS.attr(),
sessionuid: DS.attr(),
surveysToTake: DS.hasMany('survey', { async: true, inverse: 'respondents' }),
surveysCreated: DS.hasMany('survey', { async: true, inverse: 'creator' }),
responseSetsSubmitted: DS.hasMany('response-set', {async: true, inverse: 'respondentid'}),
respSetSurveyLookup: DS.attr({defaultValue: function() { return []; }})
});
And my responseSet (which is a collection of responses from a user)
//app/models/response-set.js
export default DS.Model.extend({
responses: DS.hasMany('response', { async: true, inverse: 'responseSet' }),
survey: DS.belongsTo('survey', { async: true, inverse: 'responseSets' }),
respondentid: DS.belongsTo('user', {async: true, inverse: 'responseSetsSubmitted'}),
});
And the survey model looks like this:
//app/models/survey.js
export default DS.Model.extend({
title: DS.attr(),
questions: DS.hasMany('question', { async: true, inverse: 'survey' }),
respondents: DS.hasMany('user', { async: true, inverse: 'surveysToTake'}),
responseSets: DS.hasMany('response-set', { async: true, inverse: 'survey' })
});
Now, when creating a user we do something like this ...
//routes/addusers.js (in the actions hook after getting properties from the controller)
var user = store.createRecord('user', {
id: userData.uid,
name: name
});
var responseSet = store.createRecord('response-set', {
survey: survey,
respondentid: user
});
user.setProperties({
'respSetSurveyLookup': [{'surveyId': _this.currentModel.get('id'),
'respSetId': responseSet.get('id')}],
'surveysToTake': [_this.currentModel]
});
_this.currentModel.get('respondents').pushObject(user);
Ember.RSVP.all([user.save(), _this.currentModel.save(), responseSet.save()])
.then(function(){
controller.setProperties({ multipleUsers: '', showAddUsers: false});
});
The user gets added as expected. However, in my template (which shows up on the same route as the 'add users' section,) the same record shows up multiple times.
Additional info:
I'm using Ember 1.13.11 with Ember Data 1.13.11 and EmberFire
1.6.3
Refreshing the page or reloading (with a 'location.reload()' ) causes
the user records to show up as expected.
This is possibly related to an Ember data issue which appears to be now closed and an upgrade of Ember data may resolve this in the future. However, is there anything I can do in the meantime to handle this issue ?
I have a json like
{
"meta":{
"per":20,
"page":1,
"total":2
},
"users":[
{
"id":119506,
"first_name":"erglk",
"last_name":"wfe",
"email":"bent#exemple.com",
"groups":[
{
"id":5282,
"name":"test"
},
{
"id":8880,
"name":"everybody"
}
]
},
{
"id":119507,
"first_name":"eriglk",
"last_name":"wife",
"email":"benit#exemple.com",
"groups":[
{
"id":5284,
"name":"testf"
},
{
"id":8880,
"name":"everybody"
}
]
}
]
}
For the moment no problem to access the user but I have some difficulties to access the groups array. I've tried hasMany and belongsTo without success. I had errors.
I've read few articles about EmbededRecordMixin but without any success.
If I declare in my models :
export default DS.Model.extend({
first_name: DS.attr('string'),
last_name: DS.attr('string'),
email: DS.attr('string'),
groups: DS.attr('group')
});
I get : Error while processing route: users Assertion Failed: Unable to find transform for 'group' Error: Assertion Failed: Unable to find transform for 'group'
We use DS.attr to tell Ember that this field is an attribute of a model, and optionally we can specify a type of this attribute. By default, only allowed types are string, number, boolean, and date. To support custom type, special class (transform) should be defined. That's what Embers is trying to tell you with this error message. How to define such class, you may find here
But, you don't need to define a custom transform for your task. You need to define a relationship:
export default DS.Model.extend({
first_name: DS.attr('string'),
last_name: DS.attr('string'),
email: DS.attr('string'),
groups: DS.hasMany('group', {async: false})
});
And use an EmbeddedRecordMixin, as described in official docs. I can assure you that it works as described there.
I ran into the same issue, and figured out a fix given Gennady & Beni's responses, but it still took some time for me to get up and running.
see http://emberjs.com/api/data/classes/DS.EmbeddedRecordsMixin.html
I created app/serializers/user.js:
import DS from 'ember-data';
export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
isNewSerializerAPI: true,
attrs: {
groups: { embedded: 'always' },
}
});
and in app/model/user.js
export default DS.Model.extend({
groups: DS.hasMany('group', {async: false}),
});
And then the model loaded the embedded properties right up!
After seeming to bang my head on a wall for what seems to be a simple problem I thought it best to ask for some help.
I am creating an EmberFire application that allows users to authenticate using the simple login. Once authenticated the user can store particular items for later retrieval.
I have models defined as so:
USER:
export default DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
dateOfBirth: DS.attr('date'),
registeredAt: DS.attr('date'),
compentencies: DS.hasMany('competency', { async: true })
});
COMPETENCY:
export default DS.Model.extend({
title: DS.attr('string'),
endDate: DS.attr('date'),
user: DS.belongsTo('user', { async: true })
});
These are stored in the firebase db in a denormalised for as I would like them to be.
My issue arises on retrieval of persisted data. I am not sure how I am supposed to lock the competencies down to a particular user?
The way the rules cascade in FireBase I am not sure if this is even possible in this form but it does not seem right that I would have to store each competency in an embedded form under each user node.
I know that I could just allow read access to all 'Competency' models with this.store.find('competency') and then filter them on the client but that does not fit the security I would expect.
Any help would be great thanks.
Ryan
To only allow read access to a competency if the competency belongs to the user who is currently logged in you can use $competencyId (you can replace "competencyId" with any valid string). Using $competencyId limits access to a specific node under competencies/ instead of allowing access to the entire tree.
"rules": {
"competencies": {
"$competencyId": {
".read": "data.child('user').val() === auth.uid"
}
}
}
Questions:
This line of code _activeAuthor.get('books').pushObject(book).save(); is processed without error in Chrome but the book is not added to the books property of the _activeAuthor instance of Ember-Data. I don't understand why?
The below code add the created Book to the Book property of the Chapter instance (see comment). It is a one-to-many relationship (see de Models). Ember-Data seems to automatically populate the related record on the Book instance. Is this a normal behaviour of Ember-Data? Should I let Ember-Data populate the related side of a relationship one-to-many or should I specify both sides and persist both instances?
I suspect that one of the issue of the below code is that the I do not handle promises properly. This code: this.modelFor('user').get('latestChapter'); seems to return a promise. How should I handle promisses with get()?
Code:
createChapter: function() {
//Getting the Author of the latestChapter or getting the first Author in the array
var _activeAuthor = null;
var authors = this.modelFor('user').get('authors').toArray();
var latestChapter = this.modelFor('user').get('latestChapter');
var latestAuthor = latestChapter.get('author');
if (latestChapter.content) {
_activeAuthor = latestAuthor;
} else {
_activeAuthor= authors[0];
}
var book = this.store.createRecord('book', {
title: 'click here to name your book',
author: _activeAuthor,
});
var chapter = this.store.createRecord('chapter', {
title: 'Click here to name your chapter',
book: book, // Add the created Book to the Book property of the Chapter instance
});
_activeAuthor.get('books').pushObject(book).save();
chapter.save();
book.save();
this.modelFor('user').set('latestChapter', chapter).save() //Identifying the latest created chapter at the lastestChapter;
console.log('New chapter created: ' + chapter.get('id'));
},
Models:
App.Author = DS.Model.extend({
type: DS.attr('string'),
authorTitle: DS.attr('string'),
userTitle: DS.attr('string'),
description: DS.attr('string'),
user: DS.belongsTo('user', {inverse: 'authors', async: true}),
books: DS.hasMany('book', { inverse: 'author', async: true}),
});
App.Book = DS.Model.extend({
title: DS.attr('string'),
icon: DS.attr('string'),
description: DS.attr('string'),
frequency: DS.attr('string'),
chapters: DS.hasMany('chapter', { inverse: 'book', async: true}),
author: DS.belongsTo('author', { inverse: 'books', async: true}),
});
App.Chapter = DS.Model.extend({
title: DS.attr('string'),
description: DS.attr('string'),
frequency: DS.attr('string'),
unit: DS.attr('string'),
aggregationMode: DS.attr('string'),
dashboard: DS.attr('boolean'),
statData : DS.attr('array'),
book: DS.belongsTo('book', { inverse: 'chapters', async: true}),
});
Thanks!
1.
author.get('books') will return a promise, so probably what you want to do is
author.get('books').then(function(books) {
books.pushObject(book)
});
author.save();
If this is not an issue, could you give a jsfiddle with the whole app code? Then, it'll be easier to help! :)
2.
Every time you get a model's property that is async and is not isLoaded (not synced with server), ember will ask the server and yes, will populate the records in your store which is a desired behaviour :)
3.
If you have an async model property, then you always get a promise, so you should handle it in for example this way:
chapter.get('book').then(function(book) {
// here's a book
});
BTW var latestAuthor = latestChapter.get('author'); -> chapter doesn't have author property :)
I have the following models:
App.Company = DS.Model.extend({
name: DS.attr('string'),
accounts: DS.hasMany('App.Account', {
inverse: 'company'
})
});
App.Account = DS.Model.extend({
login: DS.attr('string'),
first_name: DS.attr('string'),
last_name: DS.attr('string'),
email: DS.attr('string'),
password: DS.attr('string'),
password_confirmation: DS.attr('string'),
company: DS.belongsTo('App.Company')
});
The company is defined as being embedded in the account:
DS.RESTAdapter.map('App.Account', {
company: { embedded: 'always' }
});
When I create a new account, the company data is correctly embedded in the account data and I'm seeing the POST request that I expect on the server side:
Started POST "/accounts" for 127.0.0.1 at 2013-06-27 13:30:53 +0200
Processing by AdminUsersController#create as JSON
Parameters: {"account"=>{"login"=>"fsdfdf", "first_name"=>"fgdfgh", "last_name"=>"fsfdsfdsfsd#fgfdgdfgf.de", "email"=>"dsfdsgds#frgdfgfgdfg.de", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "company"=>{"name"=>"gfdhgtrhzrh"}}}
However, I'm also seeing an additional POST request for the company itself:
Started POST "/companies" for 127.0.0.1 at 2013-06-27 13:30:53 +0200
Processing by CompaniesController#create as JSON
Parameters: {"company"=>{"name"=>"gfdhgtrhzrh"}}
I'm setting up the models as follows:
this.transaction = this.get('store').transaction();
var account = this.transaction.createRecord(App.Account, {});
account.set('company', this.transaction.createRecord(App.Company, {}));
When the user clicks save, I simply commit the transaction:
this.transaction.commit();
Is that a bug or am I doing something wrong? Spent quite some time on that already...
Thanks for help!
this.transaction.createRecord(App.Company, {})
The code fragment creates the separate company entity. Is it really such a surprise there is a post action for it?
As far as I remember that was never actually supported in the (old) version of Ember Data I used back then. Newer versions handle that case differently anyway so I'd say this is outdated and close it.