realm-object-server icon indicating copy to clipboard operation
realm-object-server copied to clipboard

Help: creating a global Realm file to allow all users to write/read from it

Open ojarabo opened this issue 8 years ago • 13 comments

Goals

My app is a collaborative app where users should be able to write/read into a global Realm file to share data among them. I would also like to include some initial data (realmObjects) in the Realm file which will be common to all users.

Expected Results

User will sign up to, then, be able to create data and share the data with all the users. The initial data, created only once, should be available to all users.

Actual Results

I am not able to assign user permissions (write/read) to each new user once they sign-up/login in the app. The common data is created multiple time since the query (realm.where(realmObject.class).findall() returns no results, even though it has been created.

Code Sample


public static final String AUTH_URL     = "http://" + BuildConfig.OBJECT_SERVER_IP + ":9080/auth";
public static final String REALM_URL    = "realm://" + BuildConfig.OBJECT_SERVER_IP + ":9080/default";

UserManager Class

public class UserManager {

    // Supported authentication mode
    public enum AUTH_MODE {
        PASSWORD,
        FACEBOOK,
        GOOGLE
    }
    private static AUTH_MODE mode = AUTH_MODE.PASSWORD; // default

    public static void setAuthMode(AUTH_MODE m) {
        mode = m;
    }

    public static void logoutActiveUser() {
        switch (mode) {
            case PASSWORD: {
                break;
            }
            case FACEBOOK: {
                LoginManager.getInstance().logOut();
                break;
            }
            case GOOGLE: {
                break;
            }
        }
        SyncUser.currentUser().logout();
    }

    // Configure Realm for the current active user
    public static void setActiveUser(SyncUser user) {
        Realm.removeDefaultConfiguration();
        SyncConfiguration defaultConfig = new SyncConfiguration.Builder(user, REALM_URL).name("Realm").schemaVersion(0).build();
        Realm.setDefaultConfiguration(defaultConfig);
        setDefaultPermissionsRealm(user);
    }

    private static void setDefaultPermissionsRealm(SyncUser user){
        if (user.isAdmin()){

            Realm realm = user.getManagementRealm();
            realm.executeTransaction(new Realm.Transaction() {
                @Override
                public void execute(Realm realm) {
                    Boolean mayRead = true; // Grant read access
                    Boolean mayWrite = true; // Keep current permission
                    Boolean mayManage = false; // Revoke management access
                    PermissionChange change = new PermissionChange(REALM_URL,
                            "*",
                            mayRead,
                            mayWrite,
                            mayManage);
                    realm.insert(change);
                }
            });
        }
    }
}

SplashActivity

SyncUser.loginAsync(SyncCredentials.usernamePassword(eMail, password, true), AUTH_URL, new SyncUser.Callback() {
                @Override
                public void onSuccess(SyncUser syncUser) {
                    loginRegistrationSuccess = true;
                    registrationComplete(syncUser);
                }

                @Override
                public void onError(ObjectServerError error) {
                    loginRegistrationSuccess = false;
                }
            });

facebookAuthRegistration    = new FacebookAuth((LoginButton) findViewById(R.id.sign_up_facebook), this) {
           @Override
           public void onRegistrationComplete(final LoginResult loginResult, User userInfo) {
               UserManager.setAuthMode(UserManager.AUTH_MODE.FACEBOOK);
               SyncCredentials credentials = SyncCredentials.facebook(loginResult.getAccessToken().getToken());
               SyncUser.loginAsync(credentials, AUTH_URL, SplashScreenActivity.this);
           }
        };

        googleAuthRegistration      = new GoogleAuth((Button) findViewById(R.id.sign_up_google), this, googleAuthLogin.getmGoogleApiClient()) {
            @Override
            public void onRegistrationComplete(GoogleSignInResult result) {
                UserManager.setAuthMode(UserManager.AUTH_MODE.GOOGLE);
                GoogleSignInAccount acct    = result.getSignInAccount();
                SyncCredentials credentials = SyncCredentials.google(acct.getIdToken());
                SyncUser.loginAsync(credentials, AUTH_URL, SplashScreenActivity.this);
            }
        };

@Override
    public void onSuccess(SyncUser syncUser) {
        loginRegistrationSuccess = true;
        showProgress(false);
        registrationComplete(syncUser);
    }

private void registrationComplete(SyncUser syncUser) {
        UserManager.setActiveUser(syncUser);
        Intent mainIntent = new Intent(SplashScreenActivity.this, MainActivity.class);
        mainIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        SplashScreenActivity.this.startActivity(mainIntent);
        SplashScreenActivity.this.finish();
    }

Version of Realm and Tooling

  • Realm Object Server Version: 3.3.0
  • Flavor:
    • [x] Developer
    • [ ] Professional
    • [ ] Enterprise
  • Server OS & Version: Ubuntu 16.04
  • Client SDK Version: Android Studio 2.3.2 (compileSdkVersion 25)
  • Client OS & Version:

Could you please help me out with this? Thanks

ojarabo avatar May 19 '17 10:05 ojarabo

I am quite interested in this as well. Hope somebody can help with this.

tompee26 avatar May 24 '17 02:05 tompee26

Is there any example code for how to change the permissions on a global realm in JavaScript?

agersoncgps avatar May 30 '17 00:05 agersoncgps

Thanks for filing this. First, we definitely want to make this easier, by adding the ability to create Realms manually via the Realm browser. Part of the effort to include the browser in the server dashboard is to rewrite the Mac version in JS and add new functionality, so this is an ongoing process.

To manually create a global Realm, I would recommend just doing this in Javascript during development:

Realm.Sync.User.login('http://my.realm-auth-server.com:9080', 'adminUsername', 'p@s$w0rd',
 (error, adminUser) => {
  if (!error) {
    var realm = new Realm({
      sync: {
        user: adminUser,
        url: 'realm://object-server-url:9080/globalRealm',
      },
      schema: [/* ... */]
    });

    // Apply permissions to everyone
    const managementRealm = user.openManagementRealm();
   
    var permObj;
    managementRealm.write(() => {
      permObj = managementRealm.create('PermissionChange', {
        id: generateUniqueId(),   // implement something that creates a unique id.
        createdAt: new Date(),
        updatedAt: new Date(),
        userId: '*', // To apply the permission changes for all Users authorized with the Object Server, specify a userId value of *.
        realmUrl: 'realm://object-server-url:9080/globalRealm',
      });
    });

    // Listen for `PermissionChange` object to be processed
    managementRealm.objects('PermissionChange').filtered('id = $0', permObj.id).addListener((objects, changes) => {
      console.log("Permission Status: " + permObj.statusMessage);
    });
})

As for handling the data in the global Realm, you can accomplish this via two mechanisms. To make your code work where you want to query the global Realm and check for existing data, you will instead need to open the Realm via asyncOpen API with admin user. Note the asyncOpen will return the Realm in a callback, ensuring the all the data is downloaded first. Whereas the regular open API will synchronously create a Realm on the client and start downloading the data asynchronously. Thus any initial query before the download completes will show incorrect data than what is represented on the server.

This method will work in most scenarios, but still has a race condition where the check for data even after waiting for download still doesn't ensure another client doesn't create the same value at the same time leading to duplicates.

Instead, the proper way to handle global unique data with Realm's sync is to use primary keys on the objects in the global Realm. Our merge algorithm is setup to "merge" objects with the same primary key together. This merge uses the same concepts - deletes always win, last update wins, and inserts in lists are ordered by time - but also uses another factor: default values lose to existing operations.

The last point is likely confusing, so let me try to explain. In Javascript, we support default values as specified:

const CarSchema = {
  name: 'Car',
  primaryKey: 'id',
  properties: {
    id: {type: 'int', default 0},
    make:  {type: 'string'},
    model: {type: 'string'},
    drive: {type: 'string', default: 'fwd'},
    miles: {type: 'int',    default: 0}
  }
};

realm.write(() => {
  // Since `miles` is left out it defaults to `0`, and since
  // `drive` is specified, it overrides the default value
  //
  // Using `true` to create and update given the primary key
  realm.create('Car', {make: 'Honda', model: 'Accord', drive: 'awd'}, true);
});

In the above example, we are using default values for id, drive, and miles. If we perform this write on many clients, each individual copy of the object will merge with the others so that the Realm always has a single Car object with id=0. Now later on say we perform another write to update miles on client A, but client B also performs the first write just shortly after:

// Client A
realm.write(() => {
  realm.create('Car', {make: 'Honda', model: 'Accord', miles: 500}, true);
});

// Client B
// Meanwhile another write happens by client B just briefly after client A's write
realm.write(() => {
  realm.create('Car', {make: 'Honda', model: 'Accord', drive: 'awd'}, true);
});

Once client A and B sync between each other, client B's copy of the Car object will also have miles=500!

The reason for this is that client A specifically set miles=500, whereas client B just supplied a default value for miles. The merge algorithm will treat the specific set as the winner even though it happened before the creation of the object on client B.

This attribute makes it very powerful to use Realm sync to create global objects in a safe distributed manner. You can use default values to synchronously create objects on the client, so that even before downloading the server data, the object always exists, while using default values to make sure you preserve any existing operations made by other existing clients.

bigfish24 avatar May 30 '17 17:05 bigfish24

This is very very helpful. Thank you, Adam!

Spencer Schoeben

netspencer avatar May 30 '17 17:05 netspencer

It still doesn't seem to be working, unfortunately. Permission changes aren't taking effect. I get back a blank string.

Opening Realm file: /Users/spencer/Dropbox (Integral Studio)/My Stuff/wp_realm_sync/realm-object-server/f31b150ef0de1638337c49c431879f49/realm%3A%2F%2Frealm.integral.sh%3A9080%2Fyachty%2Fproduction
Opening Realm file: /Users/spencer/Dropbox (Integral Studio)/My Stuff/wp_realm_sync/realm-object-server/f31b150ef0de1638337c49c431879f49/realm%3A%2F%2Frealm.integral.sh%3A9080%2F%7E%2F__management
Permission Status: null
Connection[1]: Session[1]: Starting session for '/Users/spencer/Dropbox (Integral Studio)/My Stuff/wp_realm_sync/realm-object-server/f31b150ef0de1638337c49c431879f49/realm%3A%2F%2Frealm.integral.sh%3A9080%2Fyachty%2Fproduction'
Connection[1]: Resolving 'realm.integral.sh:9080'
Connection[1]: Connecting to endpoint '138.197.233.20:9080' (1/1)
Connection[2]: Session[1]: Starting session for '/Users/spencer/Dropbox (Integral Studio)/My Stuff/wp_realm_sync/realm-object-server/f31b150ef0de1638337c49c431879f49/realm%3A%2F%2Frealm.integral.sh%3A9080%2F%7E%2F__management'
Connection[2]: Resolving 'realm.integral.sh:9080'
Connection[2]: Connecting to endpoint '138.197.233.20:9080' (1/1)
Connection[2]: Connected to endpoint '138.197.233.20:9080' (from '192.168.7.134:49502')
Connection[1]: Connected to endpoint '138.197.233.20:9080' (from '192.168.7.134:49501')
Connection[2]: Session[1]: Sending: BIND(server_path='/f31b150ef0de1638337c49c431879f49/__management', signed_user_token_size=633, need_file_ident_pair=0)
Connection[1]: Session[1]: Sending: BIND(server_path='/yachty/production', signed_user_token_size=597, need_file_ident_pair=1)
Connection[2]: Session[1]: Sending: IDENT(server_file_ident=5259679409261093284, client_file_ident=2, client_file_ident_secret=1494669451506513031, scan_server_version=57, scan_client_version=46, latest_server_version=57, latest_server_session_ident=1726426117877430376)
Connection[1]: Session[1]: Received: ALLOC(server_file_ident=1, client_file_ident=2, client_file_ident_secret=2428648948360514317)
Connection[1]: Session[1]: Sending: IDENT(server_file_ident=1, client_file_ident=2, client_file_ident_secret=2428648948360514317, scan_server_version=0, scan_client_version=0, latest_server_version=0, latest_server_session_ident=0)
Permission Status: 

netspencer avatar May 30 '17 18:05 netspencer

@bigfish24 Thanks for posting this example. Its very helpful. The realm is getting created, as is the row in the PermissionChange table in the management realm. However, the callback listener never fires / doesnt print console.log("Permission Status...) and it does not appear the realm gets set to mayRead as I want.

agersoncgps avatar May 31 '17 01:05 agersoncgps

Thanks for the info @bigfish24, I appreciate that you create some examples in JavaScript, and even though my first post was in Java, you posted a very comprehensive description of the case, and I understand JavaScript. So, it is enough for me. I will wait you for solving the other problems related to the creation of Realm files by the admin to try if the permissionChanges take effect.

Thanks again. Truly appreciate it.

ojarabo avatar May 31 '17 10:05 ojarabo

Glad this helped and we should have a fix for the permission bug in the next day or two. Sorry about that!

bigfish24 avatar May 31 '17 23:05 bigfish24

There was a regression within the permission management system when using admin users to operate with unowned Realm files (in the global scope, e.g. /global). We've a fix and bumped up test coverage around this case. This was shipped with version 1.7.5.

Further investigations have also shown that there is currently a bug in the Java client SDK which could be reproduced by a few users in related usage scenarios. We're still working on that. Please refer to https://github.com/realm/realm-java/issues/4750 for more info.

mrackwitz avatar Jun 02 '17 11:06 mrackwitz

Is 1.7.5. out yet? I dont see it on the releases page.

agersoncgps avatar Jun 03 '17 03:06 agersoncgps

The Github releases page was not updated, but 1.7.5 has been released. You should be able to just upgrade: https://realm.io/docs/realm-object-server/#upgrading. We will fix the Github releases page tomorrow.

bigfish24 avatar Jun 05 '17 22:06 bigfish24

The macOS bundle available on realm.io is still 1.7.4.

Chris-BMA avatar Jun 06 '17 11:06 Chris-BMA

Ignore my previous (now deleted) post. I can confirm it works fine in 1.7.5 and 1.7.6.

agersoncgps avatar Jun 19 '17 02:06 agersoncgps