diff --git a/CHANGELOG.md b/CHANGELOG.md index e6144253..19405890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ x.x.x Release notes (yyyy-MM-dd) ============================================================= ## Enhancements -* None - +* Added support for finding Realm-level permissions in Query-based Realms using `realm.getPermissions()` ([#2036](https://github.com/realm/realm-js/pull/2036)). +* Added support for finding Class-level permissions in Query-based Realms using `realm.getPermissions(className)` + ([#2036](https://github.com/realm/realm-js/pull/2036)). +* Added `Realm.Permissions.Realm.findOrCreate(roleName)` and `Realm.Permissions.Class.findOrCreate(roleName)` which + makes it easier to find or create permissions for a given role when using Query-based Realms ([#2036](https://github.com/realm/realm-js/pull/2036)). + ### Fixes * ([#????](https://github.com/realm/realm-js/issues/????), since v?.?.?) * None @@ -91,6 +95,9 @@ If you try to connect to a ROS v3.10.x or previous, you will see an error like ` * As part of including the permission schema implicitly when using query based Realm, the schema `Realm.Permissions.Realm` was missing, which may break any query including it. ([#2016](https://github.com/realm/realm-js/issues/2016), since v2.3.0) * Fixed the type definition for `Realm.getPrivileges()`, `Realm.getPrivileges(className)` and `Realm.getPrivileges(object)`. ([#2030](https://github.com/realm/realm-js/pull/2030), since v2.2.14) +### Enhancements +* None + ### Compatibility * Realm Object Server: 3.0.0 or later * File format: ver 7. (upgrades from previous formats automatically) diff --git a/docs/permission.js b/docs/permission.js index ea8545db..e26d3a4c 100644 --- a/docs/permission.js +++ b/docs/permission.js @@ -370,14 +370,37 @@ class Class { /** * The name of the class which these permissions apply to. * @type {string} + * @deprecated Use name() instead. */ get class_name() {} + /** + * The name of the class which these permissions apply to. + * @type {string} + * @since 2.18.0 + */ + get name() {} + /** * The permissions for this class. * @type {Array} */ get permissions() {} + + /** + * Finds the Class-level permissions associated with the named Role. If either the role or the permission + * object doesn't exist, it will be created. + * + * If the Permission object is created because one didn't exist already, it will be + * created with all privileges disabled. + * + * If the Role object is created because one didn't exist, it will be created + * with no members. + * + * @type {Realm.Permissions.Permission} + * @since 2.18.0 + */ + findOrCreate(roleName) {} } /** @@ -389,9 +412,25 @@ class Class { * @memberof Realm.Permissions */ class Realm { + /** * The permissions for the Realm. * @type {Array} */ get permissions() {} + + /** + * Finds the Realm-level permissions associated with the named Role. If either the role or the permission + * object doesn't exist, it will be created. + * + * If the Permission object is created because one didn't exist already, it will be + * created with all privileges disabled. + * + * If the Role object is created because one didn't exist, it will be created + * with no members. + * + * @type {Realm.Permissions.Permission} + * @since 2.17.0 + */ + findOrCreate(roleName) {} } diff --git a/docs/realm.js b/docs/realm.js index 7d232aba..f2b8612b 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -174,6 +174,17 @@ class Realm { */ privileges(arg) {} + /** + * Returns the fine-grained permissions object associated with either the Realm itself or a Realm model class. + * + * @param {Realm~ObjectType} [arg] - If no argument is provided, the Realm-level permissions are returned. + * Otherwise, the Class-level permissions for the provided type is returned. + * @returns {Object} The permissions object + * @since 2.18.0 + * @see {Realm.Permissions} for details of priviliges and roles. + */ + permissions(arg) {} + /** * Create a new Realm object of the given type and with the specified properties. * @param {Realm~ObjectType} type - The type of Realm object to create. diff --git a/lib/browser/index.js b/lib/browser/index.js index 6ad8643a..7f3c64e1 100644 --- a/lib/browser/index.js +++ b/lib/browser/index.js @@ -77,6 +77,7 @@ function getObjectType(realm, type) { export default class Realm { constructor(config) { + config = this._constructor(config); let schemas = typeof config == 'object' && config.schema; let constructors = schemas ? {} : null; diff --git a/lib/extensions.js b/lib/extensions.js index 65ce47a1..410d00c0 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -52,6 +52,47 @@ function waitForDownloadConfig(config) { return config; } +/** + * Finds the permissions associated with a given Role or create them as needed. + * + * @param {RealmObject} Container RealmObject holding the permission list. + * @param {List} list of permissions. + * @param {string} name of the role to find or create permissions for. + */ +function findOrCreatePermissionForRole(realmObject, permissions, roleName) { + let realm = realmObject._realm; + if (!realm.isInTransaction) { + throw Error("'findOrCreate' can only be called inside a write transaction."); + } + let permissionsObj = permissions.filtered(`role.name = '${roleName}'`)[0]; + if (permissionsObj === undefined) { + let role = realm.objects("__Role").filtered(`name = '${roleName}'`)[0]; + if (role === undefined) { + role = realm.create("__Role", {'name': roleName}); + } + // Create new permissions object with all privileges disabled + permissionsObj = realm.create("__Permission", { 'role': role }); + permissions.push(permissionsObj); + } + return permissionsObj; +} + +/** + * Adds the schema object if one isn't already defined + */ +function addSchemaIfNeeded(schemaList, schemaObj) { + for (var i = 0; i < schemaList.length; i++) { + const obj = schemaList[i]; + if (obj === undefined) { + continue; + } + if (schemaObj.name === obj.name || (obj.schema !== undefined && (schemaObj.name === obj.schema.name))) { + return; + } + } + schemaList.push(schemaObj); +} + module.exports = function(realmConstructor) { // Add the specified Array methods to the Collection prototype. Object.defineProperties(realmConstructor.Collection.prototype, require('./collection-methods')); @@ -61,8 +102,9 @@ module.exports = function(realmConstructor) { setConstructorOnPrototype(realmConstructor.Results); setConstructorOnPrototype(realmConstructor.Object); - //Add async open API + //Add static methods to the Realm object Object.defineProperties(realmConstructor, getOwnPropertyDescriptors({ + open(config) { // If no config is defined, we should just open the default realm if (config === undefined) { config = {}; } @@ -171,7 +213,6 @@ module.exports = function(realmConstructor) { Object.defineProperty(realmConstructor.Sync.User, '_realmConstructor', { value: realmConstructor }); realmConstructor.Sync.Credentials = {}; Object.defineProperties(realmConstructor.Sync.Credentials, getOwnPropertyDescriptors(userMethods.credentials)); - realmConstructor.Sync.AuthError = require('./errors').AuthError; if (realmConstructor.Sync.removeAllListeners) { @@ -221,7 +262,7 @@ module.exports = function(realmConstructor) { } }; return config; - } + }; if (realmConstructor.Sync._setFeatureToken) { realmConstructor.Sync.setFeatureToken = function(featureToken) { @@ -242,18 +283,12 @@ module.exports = function(realmConstructor) { Disconnected: "disconnected", Connecting: "connecting", Connected: "connected", - } + }; // Define the permission schemas as constructors so that they can be // passed into directly to functions which want object type names - const permissionsSchema = Object.freeze({ - Class: function() {}, - Permission: function() {}, - Realm: function() {}, - Role: function() {}, - User: function() {}, - }); - permissionsSchema.Permission.schema = Object.freeze({ + const Permission = function() {}; + Permission.schema = Object.freeze({ name: '__Permission', properties: { role: '__Role', @@ -267,7 +302,8 @@ module.exports = function(realmConstructor) { } }); - permissionsSchema.User.schema = Object.freeze({ + const User = function() {}; + User.schema = Object.freeze({ name: '__User', primaryKey: 'id', properties: { @@ -276,7 +312,8 @@ module.exports = function(realmConstructor) { } }); - permissionsSchema.Role.schema = Object.freeze({ + const Role = function() {}; + Role.schema = Object.freeze({ name: '__Role', primaryKey: 'name', properties: { @@ -285,7 +322,8 @@ module.exports = function(realmConstructor) { } }); - permissionsSchema.Class.schema = Object.freeze({ + const Class = function() {}; + Class.schema = Object.freeze({ name: '__Class', primaryKey: 'name', properties: { @@ -293,8 +331,12 @@ module.exports = function(realmConstructor) { permissions: '__Permission[]' } }); + Class.prototype.findOrCreate = function(roleName) { + return findOrCreatePermissionForRole(this, this.permissions, roleName); + }; - permissionsSchema.Realm.schema = Object.freeze({ + const Realm = function() {}; + Realm.schema = Object.freeze({ name: '__Realm', primaryKey: 'id', properties: { @@ -302,14 +344,77 @@ module.exports = function(realmConstructor) { permissions: '__Permission[]' } }); + Realm.prototype.findOrCreate = function(roleName) { + return findOrCreatePermissionForRole(this, this.permissions, roleName); + }; + + const permissionsSchema = { + 'Class': Class, + 'Permission': Permission, + 'Realm': Realm, + 'Role': Role, + 'User': User, + }; + if (!realmConstructor.Permissions) { Object.defineProperty(realmConstructor, 'Permissions', { value: permissionsSchema, configurable: false }); } + + // Add instance methods to the Realm object that are only applied if Sync is + Object.defineProperties(realmConstructor.prototype, getOwnPropertyDescriptors({ + permissions(arg) { + // If no argument is provided, return the Realm-level permissions + if (arg === undefined) { + return this.objects('__Realm').filtered(`id = 0`)[0]; + } else { + // Else try to find the corresponding Class-level permissions + let schemaName = this._schemaName(arg); + let classPermissions = this.objects('__Class').filtered(`name = '${schemaName}'`); + if (classPermissions.length === 0) { + throw Error(`Could not find Class-level permissions for '${schemaName}'`); + } + return classPermissions[0]; + } + }, + })); } + // Realm instance methods that are always available + Object.defineProperties(realmConstructor.prototype, getOwnPropertyDescriptors({ + + /** + * Extra internal constructor callback called by the C++ side. + * Used to work around the fact that we cannot override the original constructor, + * but still need to modify any input config. + */ + _constructor(config) { + // Even though this runs code only available for Sync it requires some serious misconfiguration + // for this to happen + if (config && config.sync) { + if (!Realm.Sync) { + throw new Error("Realm is not compiled with Sync, but the configuration contains sync features."); + } + // Only inject schemas on query-based Realms + if (config.sync.partial === true || config.sync.fullSynchronization === false) { + if (!config.schema) { + config['schema'] = []; + } + + addSchemaIfNeeded(config.schema, realmConstructor.Permissions.Class); + addSchemaIfNeeded(config.schema, realmConstructor.Permissions.Permission); + addSchemaIfNeeded(config.schema, realmConstructor.Permissions.Realm); + addSchemaIfNeeded(config.schema, realmConstructor.Permissions.Role); + addSchemaIfNeeded(config.schema, realmConstructor.Permissions.User); + } + } + return config; + }, + })); + + // TODO: Remove this now useless object. var types = Object.freeze({ 'BOOL': 'bool', diff --git a/lib/index.d.ts b/lib/index.d.ts index ff728a7a..29e0730c 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -603,12 +603,16 @@ declare namespace Realm.Permissions { class Class { static schema: ObjectSchema; class_name: string; + name: string; permissions: Permission[]; + findOrCreate(roleName: string): Permission; } class Realm { static schema: ObjectSchema; + id: number; permissions: Permission[]; + findOrCreate(roleName: string): Permission; } class RealmPrivileges { @@ -801,9 +805,14 @@ declare class Realm { */ writeCopyTo(path: string, encryptionKey?: ArrayBuffer | ArrayBufferView): void; + privileges() : Realm.Permissions.Realm; privileges() : Realm.Permissions.RealmPrivileges; privileges(objectType: string | Realm.ObjectSchema | Function) : Realm.Permissions.ClassPrivileges; privileges(obj: Realm.Object) : Realm.Permissions.ObjectPrivileges; + + permissions() : Realm.Permissions.Realm; + permissions(objectType: string | Realm.ObjectSchema | Function) : Realm.Permissions.Class; + } declare module 'realm' { diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 15edb82f..f632dcd4 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -237,6 +237,7 @@ public: static void delete_model(ContextType, ObjectType, Arguments, ReturnValue &); static void object_for_object_id(ContextType, ObjectType, Arguments, ReturnValue&); static void privileges(ContextType, ObjectType, Arguments, ReturnValue&); + static void get_schema_name_from_object(ContextType, ObjectType, Arguments, ReturnValue&); // properties static void get_empty(ContextType, ObjectType, ReturnValue &); @@ -296,6 +297,7 @@ public: {"deleteModel", wrap}, {"privileges", wrap}, {"_objectForObjectId", wrap}, + {"_schemaName", wrap}, #if REALM_ENABLE_SYNC {"_waitForDownload", wrap}, #endif @@ -346,6 +348,8 @@ public: static const ObjectSchema& validated_object_schema_for_value(ContextType ctx, const SharedRealm &realm, const ValueType &value) { std::string object_type; + + // If argument is a constructor function, expect that the same constructor was used when specifying the schema. if (Value::is_constructor(ctx, value)) { FunctionType constructor = Value::to_constructor(ctx, value); @@ -362,6 +366,8 @@ public: } } else { + // Any other argument is expected to be able to be converted to a String containg the name of the + // internal class. object_type = Value::validated_to_string(ctx, value, "objectType"); if (object_type.empty()) { throw std::runtime_error("objectType cannot be empty"); @@ -435,16 +441,26 @@ static inline void convert_outdated_datetime_columns(const SharedRealm &realm) { template void RealmClass::constructor(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[]) { + + if (argc > 1) { + throw std::runtime_error("Invalid arguments when constructing 'Realm'"); + } + // Callback to custom constructor + // This is used to work around the fact that we cannot reliably wrap the the Realm constructor + // without risking breaking existing code, so instead we make an extra roundtrip to this method. + ValueType modifiedConfig = Object::call_method(ctx, this_object, "_constructor", argc, arguments); + + // Continue with C++ construction realm::Realm::Config config; ObjectDefaultsMap defaults; ConstructorMap constructors; bool schema_updated = false; - if (argc == 0) { + if (Value::is_undefined(ctx, modifiedConfig)) { config.path = default_path(); } else if (argc == 1) { - ValueType value = arguments[0]; + ValueType value = modifiedConfig; if (Value::is_string(ctx, value)) { config.path = Value::validated_to_string(ctx, value, "path"); } @@ -501,89 +517,6 @@ void RealmClass::constructor(ContextType ctx, ObjectType this_object, size_t schema_updated = true; } -#if REALM_ENABLE_SYNC - // Include permission schema for query-based sync - if (config.sync_config && config.sync_config->is_partial) { - std::vector objectsSchema; - - if (!config.schema) { - config.schema.emplace(realm::Schema(objectsSchema)); - } - - auto it = config.schema->find("__Class"); - if (it == config.schema->end()) { - realm::ObjectSchema clazz = {"__Class", { - {"name", realm::PropertyType::String, Property::IsPrimary{true}}, - {"permissions", realm::PropertyType::Object|realm::PropertyType::Array, "__Permission"} - }}; - objectsSchema.emplace_back(std::move(clazz)); - schema_updated = true; - } - - it = config.schema->find("__Permission"); - if (it == config.schema->end()) { - realm::ObjectSchema permission = {"__Permission", { - {"role", realm::PropertyType::Object|realm::PropertyType::Nullable, "__Role"}, - {"canRead", realm::PropertyType::Bool}, - {"canUpdate", realm::PropertyType::Bool}, - {"canDelete", realm::PropertyType::Bool}, - {"canSetPermissions", realm::PropertyType::Bool}, - {"canQuery", realm::PropertyType::Bool}, - {"canCreate", realm::PropertyType::Bool}, - {"canModifySchema", realm::PropertyType::Bool} - }}; - objectsSchema.emplace_back(std::move(permission)); - // adding default values - std::map> object_defaults; - object_defaults.emplace("canRead", Protected(ctx, Value::from_boolean(ctx, false))); - object_defaults.emplace("canUpdate", Protected(ctx, Value::from_boolean(ctx, false))); - object_defaults.emplace("canDelete", Protected(ctx, Value::from_boolean(ctx, false))); - object_defaults.emplace("canSetPermissions", Protected(ctx, Value::from_boolean(ctx, false))); - object_defaults.emplace("canQuery", Protected(ctx, Value::from_boolean(ctx, false))); - object_defaults.emplace("canCreate", Protected(ctx, Value::from_boolean(ctx, false))); - object_defaults.emplace("canModifySchema", Protected(ctx, Value::from_boolean(ctx, false))); - - defaults.emplace("__Permission", std::move(object_defaults)); - schema_updated = true; - } - - it = config.schema->find("__Realm"); - if (it == config.schema->end()) { - realm::ObjectSchema realm = {"__Realm", { - {"id", realm::PropertyType::Int, realm::Property::IsPrimary{true}}, - {"permissions", realm::PropertyType::Object|realm::PropertyType::Array, "__Permission"} - }}; - objectsSchema.emplace_back(std::move(realm)); - schema_updated = true; - } - - it = config.schema->find("__Role"); - if (it == config.schema->end()) { - realm::ObjectSchema role = {"__Role", { - {"name", realm::PropertyType::String, realm::Property::IsPrimary{true}}, - {"members", realm::PropertyType::Object|realm::PropertyType::Array, "__User"} - }}; - objectsSchema.emplace_back(std::move(role)); - schema_updated = true; - } - - it = config.schema->find("__User"); - if (it == config.schema->end()) { - realm::ObjectSchema user = {"__User", { - {"id", realm::PropertyType::String, realm::Property::IsPrimary{true}}, - {"role", realm::PropertyType::Object|realm::PropertyType::Nullable, "__Role"} - }}; - objectsSchema.emplace_back(std::move(user)); - schema_updated = true; - } - - objectsSchema.insert(objectsSchema.end(), std::make_move_iterator(config.schema->begin()), - std::make_move_iterator(config.schema->end())); - - config.schema.emplace(realm::Schema(objectsSchema)); - } -#endif - static const String schema_version_string = "schemaVersion"; ValueType version_value = Object::get_property(ctx, object, schema_version_string); if (!Value::is_undefined(ctx, version_value)) { @@ -664,9 +597,6 @@ void RealmClass::constructor(ContextType ctx, ObjectType this_object, size_t } } } - else { - throw std::runtime_error("Invalid arguments when constructing 'Realm'"); - } config.path = normalize_realm_path(config.path); ensure_directory_exists_for_file(config.path); @@ -1224,6 +1154,17 @@ void RealmClass::object_for_object_id(ContextType ctx, ObjectType this_object #endif // REALM_ENABLE_SYNC } +template +void RealmClass::get_schema_name_from_object(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue& return_value) { + args.validate_count(1); + + // Try to map the input to the internal schema name for the given input. This should work for managed objects and + // schema objects. Pure strings and functions are expected to return a correct value. + SharedRealm realm = *get_internal>(this_object); + auto &object_schema = validated_object_schema_for_value(ctx, realm, args[0]); + return_value.set(object_schema.name); +} + template void RealmClass::privileges(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { args.validate_maximum(1); diff --git a/src/js_realm_object.hpp b/src/js_realm_object.hpp index a42a9718..4cc117ba 100644 --- a/src/js_realm_object.hpp +++ b/src/js_realm_object.hpp @@ -18,12 +18,19 @@ #pragma once +namespace realm { +namespace js { + template struct RealmObjectClass; +} +} + #include "object_accessor.hpp" #include "object_store.hpp" #include "js_class.hpp" #include "js_types.hpp" #include "js_util.hpp" +#include "js_realm.hpp" #include "js_schema.hpp" namespace realm { @@ -58,6 +65,8 @@ struct RealmObjectClass : ClassDefinition { static void is_same_object(ContextType, ObjectType, Arguments, ReturnValue &); static void set_link(ContextType, ObjectType, Arguments, ReturnValue &); + static void get_realm(ContextType, ObjectType, ReturnValue &); + const std::string name = "RealmObject"; const StringPropertyType string_accessor = { @@ -75,6 +84,10 @@ struct RealmObjectClass : ClassDefinition { {"_isSameObject", wrap}, {"_setLink", wrap}, }; + + PropertyMap const properties = { + {"_realm", {wrap, nullptr}}, + }; }; template @@ -189,6 +202,16 @@ void RealmObjectClass::set_link(ContextType ctx, ObjectType object, Arguments } } +template +void RealmObjectClass::get_realm(ContextType ctx, ObjectType object, ReturnValue& return_value) { + return_value.set_undefined(); + auto realm_object = get_internal>(object); + if (realm_object) { + ObjectType realm_obj = create_object>(ctx, new SharedRealm(realm_object->realm())); + return_value.set(realm_obj); + } +} + template std::vector> RealmObjectClass::get_property_names(ContextType ctx, ObjectType object) { auto realm_object = get_internal>(object); diff --git a/tests/js/permission-tests.js b/tests/js/permission-tests.js index 26e3babb..0d65a87d 100644 --- a/tests/js/permission-tests.js +++ b/tests/js/permission-tests.js @@ -85,6 +85,19 @@ function waitForUpload(realm) { }); } +function waitForDownload(realm) { + let session = realm.syncSession; + return new Promise(resolve => { + let callback = (transferred, total) => { + if (transferred === total) { + session.removeProgressNotification(callback); + resolve(realm); + } + }; + session.addProgressNotification('download', 'forCurrentlyOutstandingWork', callback); + }); +} + function permissionForPath(permissions, path) { for (const permission of permissions) { if (permission.path == path) { @@ -93,6 +106,35 @@ function permissionForPath(permissions, path) { } } +const getPartialRealm = () => { + const testID = uuid(); + return Realm.Sync.User + .login('http://localhost:9080', Realm.Sync.Credentials.nickname("user-" + testID, true)) + .then(user => { + const config = user.createConfiguration({ + sync: { + url: 'realm://localhost:9080/test_' + testID, + fullSynchronization: false, + } + }); + return Realm.open(config); // Creates the Realm on the server + }).then(realm => { + return waitForUpload(realm); + }).then(realm => { + return waitForDownload(realm); + }); +}; + +const assertFullAccess= function(permission) { + TestCase.assertTrue(permission.canCreate); + TestCase.assertTrue(permission.canRead); + TestCase.assertTrue(permission.canUpdate); + TestCase.assertTrue(permission.canDelete); + TestCase.assertTrue(permission.canQuery); + TestCase.assertTrue(permission.canModifySchema); + TestCase.assertTrue(permission.canSetPermissions); +} + module.exports = { testApplyAndGetGrantedPermissions() { return createUsersWithTestRealms(1).then(([user]) => { @@ -243,10 +285,10 @@ module.exports = { fullSynchronization: false, } }; - + let realm = new Realm(config); TestCase.assertTrue(realm.empty); - + TestCase.assertEqual(realm.schema.length, 5); TestCase.assertEqual(realm.schema.filter(schema => schema.name === '__Class').length, 1); TestCase.assertEqual(realm.schema.filter(schema => schema.name === '__Permission').length, 1); @@ -272,7 +314,7 @@ module.exports = { realm.close(); Realm.deleteFile(config); - resolve(); + resolve(); }).catch(error => reject(error)); }); }, @@ -296,11 +338,11 @@ module.exports = { let subscription = rooms.subscribe(); subscription.addListener((sub, state) => { if (state === Realm.Sync.SubscriptionState.Complete) { - let roles = realm.objects(Realm.Permissions.Role.schema.name).filtered(`name = '__User:${user.identity}'`); + let roles = realm.objects(Realm.Permissions.Role).filtered(`name = '__User:${user.identity}'`); TestCase.assertEqual(roles.length, 1); realm.write(() => { - const permission = realm.create(Realm.Permissions.Permission.schema.name, + const permission = realm.create(Realm.Permissions.Permission, { canUpdate: true, canRead: true, canQuery: true, role: roles[0] }); let room = realm.create(PrivateChatRoomSchema.name, { name: `#sales_${uuid()}` }); @@ -312,28 +354,153 @@ module.exports = { Realm.deleteFile(config); // connecting with an empty schema should be possible, permission is added implicitly Realm.open(user.createConfiguration()).then((realm) => { - let permissions = realm.objects(Realm.Permissions.Permission.schema.name).filtered(`role.name = '__User:${user.identity}'`); - let subscription = permissions.subscribe(); - subscription.addListener((sub, state) => { - if (state === Realm.Sync.SubscriptionState.Complete) { - TestCase.assertEqual(permissions.length, 1); - TestCase.assertTrue(permissions[0].canRead); - TestCase.assertTrue(permissions[0].canQuery); - TestCase.assertTrue(permissions[0].canUpdate); - TestCase.assertFalse(permissions[0].canDelete); - TestCase.assertFalse(permissions[0].canSetPermissions); - TestCase.assertFalse(permissions[0].canCreate); - TestCase.assertFalse(permissions[0].canModifySchema); - - realm.close(); - resolve(); - } - }); + return waitForUpload(realm); + }).then((realm) => { + return waitForDownload(realm); + }).then((realm) => { + let permissions = realm.objects(Realm.Permissions.Permission).filtered(`role.name = '__User:${user.identity}'`); + TestCase.assertEqual(permissions.length, 1); + TestCase.assertTrue(permissions[0].canRead); + TestCase.assertTrue(permissions[0].canQuery); + TestCase.assertTrue(permissions[0].canUpdate); + TestCase.assertFalse(permissions[0].canDelete); + TestCase.assertFalse(permissions[0].canSetPermissions); + TestCase.assertFalse(permissions[0].canCreate); + TestCase.assertFalse(permissions[0].canModifySchema); + realm.close(); + resolve(); }); }); } }); }).catch(error => reject(error)); }); - } -} + }, + + testFindOrCreate_realmPermissions() { + return getPartialRealm().then(realm => { + return new Promise((resolve, reject) => { + let realmPermissions = realm.permissions(); + TestCase.assertEqual(2, realm.objects('__Role').length); // [ "everyone", "__User:" ] + realm.write(() => { + let permissions = realmPermissions.findOrCreate("foo"); + TestCase.assertEqual("foo", permissions.role.name); + TestCase.assertEqual(0, permissions.role.members.length); + TestCase.assertFalse(permissions.canCreate); + TestCase.assertFalse(permissions.canRead); + TestCase.assertFalse(permissions.canUpdate); + TestCase.assertFalse(permissions.canDelete); + TestCase.assertFalse(permissions.canQuery); + TestCase.assertFalse(permissions.canModifySchema); + TestCase.assertFalse(permissions.canSetPermissions); + TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:", "foo" ] + }); + resolve(); + }); + }); + }, + + testFindOrCreate_existingRole() { + return getPartialRealm().then(realm => { + return new Promise((resolve, reject) => { + realm.write(() => { + realm.create('__Role', {'name':'foo'}); + }); + TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ] + + let realmPermissions = realm.permissions(); + realm.write(() => { + let permissions = realmPermissions.findOrCreate("foo"); + TestCase.assertEqual("foo", permissions.role.name); + TestCase.assertFalse(permissions.canCreate); + TestCase.assertFalse(permissions.canRead); + TestCase.assertFalse(permissions.canUpdate); + TestCase.assertFalse(permissions.canDelete); + TestCase.assertFalse(permissions.canQuery); + TestCase.assertFalse(permissions.canModifySchema); + TestCase.assertFalse(permissions.canSetPermissions); + TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ] + }); + resolve(); + }); + }); + }, + + testFindOrCreate_classPermissions() { + return getPartialRealm().then(realm => { + return new Promise((resolve, reject) => { + let classPermissions = realm.permissions('__Class'); + TestCase.assertEqual(2, realm.objects('__Role').length); // [ "everyone", "__User:xxx" ] + realm.write(() => { + let permissions = classPermissions.findOrCreate("foo"); + TestCase.assertEqual("foo", permissions.role.name); + TestCase.assertEqual(0, permissions.role.members.length); + TestCase.assertFalse(permissions.canCreate); + TestCase.assertFalse(permissions.canRead); + TestCase.assertFalse(permissions.canUpdate); + TestCase.assertFalse(permissions.canDelete); + TestCase.assertFalse(permissions.canQuery); + TestCase.assertFalse(permissions.canModifySchema); + TestCase.assertFalse(permissions.canSetPermissions); + TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ] + }); + resolve(); + }); + }); + }, + + testFindOrCreate_throwsOutsideWrite() { + return getPartialRealm().then(realm => { + return new Promise((resolve, reject) => { + let realmPermissions = realm.permissions(); + TestCase.assertThrows(() => realmPermissions.findOrCreate("foo")); + let classPermissions = realm.permissions('__Class'); + TestCase.assertThrows(() => classPermissions.findOrCreate("foo")); + resolve(); + }); + }); + }, + + testPermissions_Realm: function() { + return getPartialRealm().then(realm => { + return new Promise((resolve, reject) => { + let permissions = realm.permissions(); + TestCase.assertEqual(1, permissions.permissions.length); + let perm = permissions.permissions[0]; + TestCase.assertEqual("everyone", perm.role.name); + assertFullAccess(perm); + resolve(); + }); + }); + }, + + testPermissions_Class: function() { + return getPartialRealm().then(realm => { + return new Promise((resolve, reject) => { + let permissions = realm.permissions('__Class'); + TestCase.assertEqual('__Class', permissions.name) + TestCase.assertEqual(1, permissions.permissions.length); + let perm = permissions.permissions[0]; + TestCase.assertEqual("everyone", perm.role.name); + TestCase.assertTrue(perm.canCreate); + TestCase.assertTrue(perm.canRead); + TestCase.assertTrue(perm.canUpdate); + TestCase.assertFalse(perm.canDelete); + TestCase.assertTrue(perm.canQuery); + TestCase.assertFalse(perm.canModifySchema); + TestCase.assertTrue(perm.canSetPermissions); + resolve(); + }); + }); + }, + + testPermissions_Class_InvalidClassArgument: function() { + return getPartialRealm().then(realm => { + return new Promise((resolve, reject) => { + TestCase.assertThrows(() => realm.permissions('foo')); + resolve(); + }); + }); + }, + +}; diff --git a/tests/js/realm-tests.js b/tests/js/realm-tests.js index 5cc6e97a..58587b8c 100644 --- a/tests/js/realm-tests.js +++ b/tests/js/realm-tests.js @@ -1335,5 +1335,6 @@ module.exports = { encryptedRealmCopy.close(); realm.close(); - } + }, + };