How can I set Firebase Database Security Rules for Android App - android

I'm making android app which will collect useful info from user. Here is the code
Firebase postRef = mRef.child("marks");
Map<String, String> marks = new HashMap<String, String>();
marks.put("Name", fname);
marks.put("Phone", fphone);
marks.put("Year", fyear);
marks.put("Email", femail);
String uid=postRef.getKey();
Toast.makeText(im.this,"Thanks!",Toast.LENGTH_LONG).show();
This will result in getting the data from every user of my Android App. How should I set specific rules by which only authenticated users can get access of their information. Not other users info.
I tried this in Firebase Rules:
{
"rules":{
"$uid":{
".read":"auth.uid==$uid",
".write":"auth.uid==$uid",
".validate":"newData.hasChildren(['uid','Email','Name'])"
}
}
}
But its not working. Kindly tell me specific solution.

If I read your question and comment correctly you are putting your data under the "marks" node. That means you also have to include that in your security rules like this:
{
"rules":{
"marks":{
"$uid":{
".read":"auth.uid==$uid",
".write":"auth.uid==$uid",
".validate":"newData.hasChildren(['uid','Email','Name'])"
}
}
}
}

Related

Mongodb- How to get data from a partition that was created by other user?

I am using Realm MongoDB for my android app, and I have a problem:
I have different users in my app, and each user has his "cards". The partition of each user's cards is:
"Card=userID".
So, I want to be able to send a card from one user to the other. I do it via a link that includes userID and specific cardID.
So my code looks something like:
Realm.init(this);
mainApp = new App(new AppConfiguration.Builder(APP_ID).defaultSyncErrorHandler((session, error) ->
Log.e("TAG()", "Sync error: ${error.errorMessage}")
).build());
//TEMP CODE
String partition = "Card=611d7n582w36796ce34af106"; //test partition of another user
if(mainApp.currentUser() != null) {
SyncConfiguration config = new SyncConfiguration.Builder(
mainApp.currentUser(),
partition)
.build();
Realm realmLinkCard = Realm.getInstance(config);
Log.d(TAG, "onCreate: cards found- " + realmLinkCard.where(Card.class).findAll().size());
}
The last log always shows 0. I know there are cards for sure because if the user that created the corresponding partition is signed in then it does find the cards.
permissions are set to true for both read and write for the whole sync.
What can the problem be?
You cannot access a Realm by a user who has a different partition.
Instead you can create a mongodb function and call it from your user.
Make your function here:
Check here on How to create a function
And call it by checking here on How to call a function from client
Quick example of a realm function:
exports = async function funcName(partition) {
const cluster = context.services.get('myclustername');
const mycollection = cluster.db('mydbname').collection('mycollectionname');
let result = [];
try {
result = mycollection.findOne({
_partition: partition,
});
} catch (e) {
result.push(e);
return result;
}
return result;
};
To call it, please see above for the documentation as I'm not an Android developper.

Adding data to FirebaseDatabase when there is no Internet connection

If I try to send data to FirebaseDatabase in the absence of an Internet connection, then, as expected, nothing happens. If you turn on the Internet, then this data is added themselves, and even if I restarted the application. And they are added to the very end of the database. Even if you add data after this, the offline post will still be the last.
I think to solve the problem by checking the Internet before sending. Maybe there are any other solution methods?
I send data in a simple way:
final String phone_val = etPhone.getText().toString().trim();
final String comment_val = etComment.getText().toString().trim();
DatabaseReference newTrip = mDatabase.push();
newTrip.child("phone").setValue(phone_val);
newTrip.child("comment").setValue(comment_val);
newTrip.child("type").setValue(1);
startActivity(new Intent(AddDriverActivity.this, TripActivity.class));
finishAffinity();
Firstly, if you haven't already, read through the offline capabilities documentation so you have a general grasp of how Firebase behaves while offline.
Next, we'll clean up your write operations so that they are a single atomic operation rather than a few separate write operations.
HashMap<String, Object> tripData = new HashMap<>();
tripData.put("phone", phone_val);
tripData.put("comment", comment_val);
tripData.put("type", 1);
DatabaseReference newTrip = mDatabase.push();
newTrip.setValue(tripData);
As stated in the offline capabilities documentation, you can check whether your app is offline by checking the special database location /.info/connected which returns the current connection state. This value will be either true or false.
While you could check this value before posting your trips, the connection state may change while you are sending the data.
Even if you add data after this, the offline post will still be the last.
This is the trickier part to manage. I think the easiest way to deal with this is to have a "staging" section of your database and then move data as it is created at this location to the main storage location using a Cloud Function for Firebase.
Client Side
Let's say you are storing these trips in /trips/someTripId.
private DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference();
private DatabaseReference mAllTripsRef = mDatabase.child('trips');
To add a new trip, you would use:
HashMap<String, Object> tripData = new HashMap<>();
tripData.put("phone", phone_val);
tripData.put("comment", comment_val);
tripData.put("type", 1);
DatabaseReference mNewTripRef = mAllTripsRef.push();
mNewTripRef.setValue(tripData);
Because references created by push() are based on the estimated time of the Firebase servers, they will be ordered by when they were created rather than when they are received by the Firebase servers. But if you wanted to preserve that offline trips are always last, instead of writing new trips to /trips, you would instead write to /trips-staging.
private DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference();
// trips that have been posted to "online" database
private DatabaseReference mAllTripsRef = mDatabase.child('trips');
// trips yet to be posted online
private DatabaseReference mStagingTripsRef = mDatabase.child('trips-staging');
New data would be added using:
HashMap<String, Object> tripData = new HashMap<>();
tripData.put("phone", phone_val);
tripData.put("comment", comment_val);
tripData.put("type", 1);
DatabaseReference mNewTripRef = stagingTripsRef.push();
mNewTripRef.setValue(tripData);
Now that we have the reference to the trip waiting to be posted, mNewTripRef, we can add a listener to it to see when it has been posted.
In the cloud side below, we are going to make it so that if there is data at /trips-staging/someTripId and it is just a string, then the trip has been received and posted by the server to the location /trips/<string-value>.
ValueEventListener stagingTripListener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
// Get trip data
Object tripData = dataSnapshot.getValue();
if (tripData == null) {
// Data has been deleted!
// Disconnect this listener
mNewTripRef.removeEventListener(this);
// TODO: What now?
} else if (tripData instanceof String) {
// Data has been moved!
DatabaseReference postedTripRef = mAllTripsRef.child((String) tripData);
// Disconnect this listener
mNewTripRef.removeEventListener(this);
Log.i(TAG, "stagingTripListener:onDataChange", "New trip has been successfully posted as trip '" + mNewTripRef.getKey() + "'");
// TODO: do something with postedTripRef
} else {
// ignore - the trip hasn't been moved yet, continue waiting
// tripData is a Map<string, Object> with our original data
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
// Getting this trip failed, log a message
Log.w(TAG, "stagingTripListener:onCancelled", databaseError.toException());
}
};
mNewTripRef.addValueEventListener(stagingTripListener);
Cloud Side
Now, we need to move these new trips over to /trips once they are received on the server. For this we can use a Cloud Function for Firebase that listens to Realtime Database events. For our use case, we want to exclusively listen to data creation events that happen on our /trips-staging location.
When the Cloud Function is triggered, it should take the data at /trips-staging/someId and move it to /trips/someNewId. It is probably also a good idea to store where we moved the data to at the old location if it is ever needed but also so we can tell when the trip has been received by the server.
After following the Getting Started documentation up to Step 4, you can use the following code as your index.js or index.ts file and then deploy it using firebase deploy --only functions.
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(); // use defaults
export const moveTripsFromStaging = functions.database.ref('/trips-staging/{stagingTripId}')
.onCreate((snapshot, context) => {
const stagingTripId = snapshot.key;
const tripData = snapshot.val();
// get reference to the root of the database
let dbRootRef = admin.database().ref();
let allTripsRef = dbRootRef.child('trips');
let newTripRef = allTripsRef.push();
return dbRootRef.update({
// move data to it's new home
['trips/' + newTripRef.key]: tripData,
// save where we moved data to as a simple string containing the new key in /trips
['trips-staging/' + stagingTripId]: newTripRef.key // or set to null to delete it
});
})
Once deployed, you should see new trips that are uploaded to /trips-staging be received by the server and then moved across to /trips in the order that server receives them.

Node gets created with blank field on firebase [duplicate]

I have recently followed a tutorial over on Thinkster for creating a web app using Angular and Firebase.
The tutorial uses the Firebase simpleLogin method allows a 'profile' to be created that includes a username.
Factory:
app.factory('Auth', function($firebaseSimpleLogin, $firebase, FIREBASE_URL, $rootScope) {
var ref = new Firebase(FIREBASE_URL);
var auth = $firebaseSimpleLogin(ref);
var Auth = {
register: function(user) {
return auth.$createUser(user.email, user.password);
},
createProfile: function(user) {
var profile = {
username: user.username,
md5_hash: user.md5_hash
};
var profileRef = $firebase(ref.child('profile'));
return profileRef.$set(user.uid, profile);
},
login: function(user) {
return auth.$login('password', user);
},
logout: function() {
auth.$logout();
},
resolveUser: function() {
return auth.$getCurrentUser();
},
signedIn: function() {
return !!Auth.user.provider;
},
user: {}
};
$rootScope.$on('$firebaseSimpleLogin:login', function(e, user) {
angular.copy(user, Auth.user);
Auth.user.profile = $firebase(ref.child('profile').child(Auth.user.uid)).$asObject();
console.log(Auth.user);
});
$rootScope.$on('$firebaseSimpleLogin:logout', function() {
console.log('logged out');
if (Auth.user && Auth.user.profile) {
Auth.user.profile.$destroy();
}
angular.copy({}, Auth.user);
});
return Auth;
});
Controller:
$scope.register = function() {
Auth.register($scope.user).then(function(user) {
return Auth.login($scope.user).then(function() {
user.username = $scope.user.username;
return Auth.createProfile(user);
}).then(function() {
$location.path('/');
});
}, function(error) {
$scope.error = error.toString();
});
};
At the very end of the tutorial there is a 'next steps' section which includes:
Enforce username uniqueness-- this one is tricky, check out Firebase priorities and see if you can use them to query user profiles by username
I have searched and searched but can't find a clear explanation of how to do this, particularly in terms of the setPriority() function of Firebase
I'm quite the Firebase newbie so any help here would be gratefully recieved.
There are a few similar questions, but I can't seem to get my head around how to sort this out.
Enormous thanks in advance.
EDIT
From Marein's answer I have updated the register function in my controller to:
$scope.register = function() {
var ref = new Firebase(FIREBASE_URL);
var q = ref.child('profile').orderByChild('username').equalTo($scope.user.username);
q.once('value', function(snapshot) {
if (snapshot.val() === null) {
Auth.register($scope.user).then(function(user) {
return Auth.login($scope.user).then(function() {
user.username = $scope.user.username;
return Auth.createProfile(user);
}).then(function() {
$location.path('/');
});
}, function(error) {
$scope.error = error.toString();
});
} else {
// username already exists, ask user for a different name
}
});
};
But it is throwing an 'undefined is not a function' error in the line var q = ref.child('profile').orderByChild('username').equalTo($scope.user.username);. I have commented out the code after and tried just console.log(q) but still no joy.
EDIT 2
The issue with the above was that the Thinkster tutorial uses Firebase 0.8 and orderByChild is available only in later versions. Updated and Marein's answer is perfect.
There are two things to do here, a client-side check and a server-side rule.
At the client side, you want to check whether the username already exists, so that you can tell the user that their input is invalid, before sending it to the server. Where exactly you implement this up to you, but the code would look something like this:
var ref = new Firebase('https://YourFirebase.firebaseio.com');
var q = ref.child('profiles').orderByChild('username').equalTo(newUsername);
q.once('value', function(snapshot) {
if (snapshot.val() === null) {
// username does not yet exist, go ahead and add new user
} else {
// username already exists, ask user for a different name
}
});
You can use this to check before writing to the server. However, what if a user is malicious and decides to use the JS console to write to the server anyway? To prevent this you need server-side security.
I tried to come up with an example solution but I ran into a problem. Hopefully someone more knowledgeable will come along. My problem is as follows. Let's say your database structure looks like this:
{
"profiles" : {
"profile1" : {
"username" : "Nick",
"md5_hash" : "..."
},
"profile2" : {
"username" : "Marein",
"md5_hash" : "..."
}
}
}
When adding a new profile, you'd want to have a rule ensuring that no profile object with the same username property exists. However, as far as I know the Firebase security language does not support this, with this data structure.
A solution would be to change the datastructure to use username as the key for each profile (instead of profile1, profile2, ...). That way there can only ever be one object with that username, automatically. Database structure would be:
{
"profiles" : {
"Nick" : {
"md5_hash" : "..."
},
"Marein" : {
"md5_hash" : "..."
}
}
}
This might be a viable solution in this case. However, what if not only the username, but for example also the email has to be unique? They can't both be the object key (unless we use string concatenation...).
One more thing that comes to mind is to, in addition to the list of profiles, keep a separate list of usernames and a separate list of emails as well. Then those can be used easily in security rules to check whether the given username and email already exist. The rules would look something like this:
{
"rules" : {
".write" : true,
".read" : true,
"profiles" : {
"$profile" : {
"username" : {
".validate" : "!root.child('usernames').child(newData.val()).exists()"
}
}
},
"usernames" : {
"$username" : {
".validate" : "newData.isString()"
}
}
}
}
However now we run into another problem; how to ensure that when a new profile is created, the username (and email) are also placed into these new lists? [1]
This in turn can be solved by taking the profile creation code out of the client and placing it on a server instead. The client would then need to ask the server to create a new profile, and the server would ensure that all the necessary tasks are executed.
However, it seems we have gone very far down a hole to answer this question. Perhaps I have overlooked something and things are simpler than they seem. Any thoughts are appreciated.
Also, apologies if this answer is more like a question than an answer, I'm new to SO and not sure yet what is appropriate as an answer.
[1] Although maybe you could argue that this does not need to be ensured, as a malicious user would only harm themselves by not claiming their unique identity?
I had a similar problem. But it was after registering the user with password and email. In the user profile could save a user name that must be unique and I have found a solution, maybe this can serve you.
Query for username unique in Firebase
var ref = new Firebase(FIREBASE_URL + '/users');
ref.orderByChild("username").equalTo(profile.username).on("child_added", function(snapshot) {
if (currentUser != snapshot.key()) {
scope.used = true;
}
});
ref.orderByChild("username").equalTo(profile.username).once("value", function(snap) {
//console.log("initial data loaded!", Object.keys(snap.val()).length === count);
if (scope.used) {
console.log('username already exists');
scope.used = false;
}else{
console.log('username doesnt exists, update it');
userRef.child('username').set(profile.username);
}
});
};

cloudBackend.setCredential not setting createdBy/updatedBy/owner properties of CloudEntity

I am using mobile backend starter and I am trying to update an entity when using the secured by id setting. I keep getting the error
com.google.api.client.googleapis.json.GoogleJsonResponseException: 401 Unauthorized
{
"code": 401,
"errors": [
{
"domain": "global",
"location": "Authorization",
"locationType": "header",
"message": "Insuffient permission for updating a CloudEntity: CE:123456 by: USER:123456",
"reason": "required"
}
],
"message": "Insuffient permission for updating a CloudEntity: CE: 123456 by: USER:123456"
}
The documentation (https://cloud.google.com/developers/articles/mobile-backend-starter-api-reference/#ciagaa) states
In the code below, the backend allows the call in “Secured by Client
ID” mode. It also sets createdBy/updatedBy/owner properties of
CloudEntity automatically
GoogleAccountCredential credential =
GoogleAccountCredential.usingAudience(this, "<Web Client ID>");
credential.setSelectedAccountName("<Google Account Name>");
cloudBackend.setCredential(credential);
So I wrote the following code
mCloudBackend = new CloudBackendMessaging(this);
GoogleAccountCredential credential = GoogleAccountCredential.usingAudience(this, Consts.AUTH_AUDIENCE);
mCloudBackend.setCredential(credential);
String accountName =
mCloudBackend.getSharedPreferences().getString(
Consts.PREF_KEY_ACCOUNT_NAME, null);
credential.setSelectedAccountName(accountName);
mCloudBackend.setCredential(credential);
newPost.setId(updateRide);
mCloudBackend.update(newPost, handler);
Unfortunately this is giving the error above. However, the update is going through as I can see changes in entity when I query the datastore. The problem seems to come from the fact that the createdBy/updatedBy/owner properties are set to null and so are not being set automatically.
I have seen other questions where the answer has been to query the entity prior to the update, use this to set the aforementioned properties and then perform the update. I would rather avoid this as it seems like an unnecessary call to the datastore. So my question is how to I get the GoogleAccount createdBy updatedBy and owner properties?
I faced similar problem while playing with the mobile backend starter source.And even though I followed the answers provided in this link,I wasn't able to resolve the issue.However,what I did was to grab the mobile backend source code and make some modifications.Grab the code and in the CrudOperations.java file,you will see this method
private Map<String, Entity> findAndUpdateExistingEntities(EntityListDto cdl, User user)
throws UnauthorizedException{
// create a list of CEs with existing Id
EntityListDto entitiesWithIds = new EntityListDto();
Map<String, EntityDto> entitiesWithIdMap = new HashMap<String, EntityDto>();
for (EntityDto cd : cdl.getEntries()) {
if (cd.getId() != null) {
entitiesWithIds.add(cd);
entitiesWithIdMap.put(cd.getId(), cd);
}
}
// try to get existing CEs
Map<String, Entity> existingEntities = getAllEntitiesByKeyList(entitiesWithIds
.readKeyList(user));
// update existing entities
for (String id : existingEntities.keySet()) {
// check ACL
Entity e = existingEntities.get(id);
SecurityChecker.getInstance().checkAclForWrite(e, user);
// update metadata
EntityDto cd1 = entitiesWithIdMap.get(id);
cd1.setUpdatedAt(new Date());
if (user != null) {
cd1.setUpdatedBy(user.getEmail());
}
// update the entity
cd1.copyPropValuesToEntity(e);
}
return existingEntities;
}
From the above code,comment out SecurityChecker.getInstance().checkAclForWrite(e, user);
and the throws UnAuthorizedException lines and redeploy your backend.Doing so will make all users of your app able to make updates to the concerned entity.This could be risky if you are strictly concerned about ownership.So,consider your security concerns before taking this approach.Haven done that,you can now freely update the concerned cloud entity.Remember to make as default your newly deployed backend on the server side.

Create/Login User from a mobile device using Symfony2 and FOSUserBundle

Firstly, I want to create a user sending a post-request from my android app to the server, which uses Symfony2 and the FOSUserBundle.
Finally, I want to login a user from the mobile app and then communicate data with the server.
I know how to implement a post-request on the android-device. But I don't know how I need to configure the FOSUserBundle and security.yml etc to fit my needs. Although I might need a _csrf_token or something and I dont know where to get it from.
I already changed the authentication method from form_login to http_basic and think that this will be the easiest way of doing the authentication (using https to secure the passwords).
But now.. what do I need to do, to achieve the creating and logging in actions without forms? What do I need to put in the post-request on the mobile device?
Thanks for any ideas, comments and solutions!!
A late answer, but it might help.
I'm working on a similar situation and I got this:
In security.yml
security:
providers:
fos_userbundle:
id: fos_user.user_manager
firewalls:
main:
pattern: ^/
stateless: true
http_basic:
realm: "API"
access_control:
- { path: /, role: ROLE_USER }
role_hierarchy:
ROLE_OWNER: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
In config.yml:
fos_user:
db_driver: orm
firewall_name: main
user_class: <your user class>
In my test-method:
Reference: Authentication for a Symfony2 api (for mobile app use)
public function testAuthentication()
{
$client = $this->createClient();
// not authenticated
$client->request('GET', '<url>');
$this->assertEquals(401, $client->getResponse()->getStatusCode());
// authenticated
$client->request('GET', '<url>', array(), array(), array(
'PHP_AUTH_USER' => '<username from your database>',
'PHP_AUTH_PW' => '<password>'
));
$this->assertEquals(200, $client->getResponse()->getStatusCode());
}
For communication with that API, I'd suggest cURL or Buzz
Hope this helps!
Cheers,
Dieter
I had the same problem but I found the solution for registration : (the user enter the username , email and password)
In the UserController of your UserBundle (src/Project/UserBundle/Controller/DefaultController)
define a new function registerAction():
public function registerAction()
{
$user = new User();
$request = $this->getRequest();
$username = $request->request->get('username');
$password= $request->request->get('password');
$email= $request->request->get('email');
$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword($password, $user->getSalt());
$user->setPassword($password);
$user->setUsername($username);
$user->setUsernameCanonical($username);
$user->setEmail($email);
$user->setEmailCanonical($email);
$user->setEnabled(true);
$user->setLocked(false);
$user->setExpired(false);
$user->setCredentialsExpired(false);
$em = $this->get('doctrine')->getEntityManager();
$em->persist($user);
$em->flush();
/* $response = new Response(json_encode(array('user' => $tes)));
$response->headers->set('Content-Type', 'application/json');
return $response;*/
return new JsonResponse('good');
}
}
and don't forgot to import :
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder;
use Telifoon\UserBundle\Entity\User;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
in UserBundle/Resources/config/routing.yml add follwoing route:
inscription_post:
pattern: /v1/api/register
defaults: { _controller: ProjectUserBundle:Default:register }
requirements:
_method: POST
My entity ( src/Project/UserBUndle/Entity/User) is :
use FOS\UserBundle\Model\User as BaseUser;
/**
* User
*/
class User extends BaseUser
{
public function __construct()
{
parent::__construct();
// your own logic
}
}
If test the user is added correctely to my database :)

Categories

Resources