//////////////////////////////////////////////////////////////////////////// // // Copyright 2017 Realm Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////////////////// 'use strict'; const url_parse = require('url-parse'); const managementSchema = require('./management-schema'); function generateUniqueId() { const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); return uuid; } const permissionSchema = [ { name: 'Permission', properties: { id: { type: 'string' }, updatedAt: { type: 'date' }, userId: { type: 'string' }, path: { type: 'string' }, mayRead: { type: 'bool' }, mayWrite: { type: 'bool' }, mayManage: { type: 'bool' }, } } ]; // Symbols are not supported on RN yet, so we use this for now: const specialPurposeRealmsKey = '_specialPurposeRealms'; function getSpecialPurposeRealm(user, realmName, schema) { if (!user.hasOwnProperty(specialPurposeRealmsKey)) { user[specialPurposeRealmsKey] = {}; } if (user[specialPurposeRealmsKey].hasOwnProperty(realmName)) { return Promise.resolve(user[specialPurposeRealmsKey][realmName]); } const url = url_parse(user.server); if (url.protocol === 'http:') { url.set('protocol', 'realm:'); } else if (url.protocol === 'https:') { url.set('protocol', 'realms:'); } else { throw new Error(`Unexpected user auth url: ${user.server}`); } url.set('pathname', `/~/${realmName}`); const config = { schema: schema, sync: { user, url: url.href } }; const _Realm = user.constructor._realmConstructor; return new Promise((resolve, reject) => { _Realm._waitForDownload(config, (error) => { // FIXME: I don't understand why, but removing the following setTimeout causes the subsequent // setTimeout call (when resolving the promise) to hang on RN iOS. // This might be related to our general makeCallback issue: #1255. setTimeout(() => {}, 1); if (error) { reject(error); } else { try { let syncedRealm = new _Realm(config); user[specialPurposeRealmsKey][realmName] = syncedRealm; //FIXME: RN hangs here. Remove when node's makeCallback alternative is implemented (#1255) setTimeout(() => resolve(syncedRealm), 1); } catch (e) { reject(e); } } }); }); } function createInManagementRealm(user, modelName, modelInitializer) { return getSpecialPurposeRealm(user, '__management', managementSchema) .then(managementRealm => { return new Promise((resolve, reject) => { try { let o; const listener = () => { if (!o) { return; } const statusCode = o.statusCode; if (typeof statusCode === 'number') { managementRealm.removeListener('change', listener); if (statusCode === 0) { setTimeout(() => resolve(o), 1); } else { const e = new Error(o.statusMessage); e.statusCode = statusCode; e.managementObject = o; setTimeout(() => reject(e), 1); } } } managementRealm.addListener('change', listener); managementRealm.write(() => { o = managementRealm.create(modelName, modelInitializer); }); } catch (e) { reject(e); } }); }); } const accessLevels = ['none', 'read', 'write', 'admin']; const offerAccessLevels = ['read', 'write', 'admin']; module.exports = { getGrantedPermissions(recipient) { if (recipient && ['currentUser', 'otherUser', 'any'].indexOf(recipient) === -1) { return Promise.reject(new Error(`'${recipient}' is not a valid recipient type. Must be 'any', 'currentUser' or 'otherUser'.`)); } return getSpecialPurposeRealm(this, '__permission', permissionSchema) .then(permissionRealm => { let permissions = permissionRealm.objects('Permission') .filtered('NOT path ENDSWITH "__permission" AND NOT path ENDSWITH "__management"'); if (recipient === 'currentUser') { permissions = permissions.filtered('userId = $0', this.identity); } else if (recipient === 'otherUser') { permissions = permissions.filtered('userId != $0', this.identity); } return permissions; }); }, applyPermissions(condition, realmUrl, accessLevel) { if (!realmUrl) { return Promise.reject(new Error('realmUrl must be specified')); } if (accessLevels.indexOf(accessLevel) === -1) { return Promise.reject(new Error(`'${accessLevel}' is not a valid access level. Must be ${accessLevels.join(', ')}.`)); } const mayRead = accessLevel === 'read' || accessLevel === 'write' || accessLevel === 'admin'; const mayWrite = accessLevel === 'write' || accessLevel === 'admin'; const mayManage = accessLevel === 'admin'; const permissionChange = { id: generateUniqueId(), createdAt: new Date(), updatedAt: new Date(), realmUrl, mayRead, mayWrite, mayManage }; if (condition.hasOwnProperty('userId')) { permissionChange.userId = condition.userId; } else { permissionChange.userId = ''; permissionChange.metadataKey = condition.metadataKey; permissionChange.metadataValue = condition.metadataValue; } return createInManagementRealm(this, 'PermissionChange', permissionChange); }, offerPermissions(realmUrl, accessLevel, expiresAt) { if (!realmUrl) { return Promise.reject(new Error('realmUrl must be specified')); } if (offerAccessLevels.indexOf(accessLevel) === -1) { return Promise.reject(new Error(`'${accessLevel}' is not a valid access level. Must be ${offerAccessLevels.join(', ')}.`)); } const mayRead = true; const mayWrite = accessLevel === 'write' || accessLevel === 'admin'; const mayManage = accessLevel === 'admin'; const permissionOffer = { id: generateUniqueId(), createdAt: new Date(), updatedAt: new Date(), expiresAt, realmUrl, mayRead, mayWrite, mayManage }; return createInManagementRealm(this, 'PermissionOffer', permissionOffer) .then(appliedOffer => appliedOffer.token); }, acceptPermissionOffer(token) { if (!token) { return Promise.reject(new Error('Offer token must be specified')); } const permissionOfferResponse = { id: generateUniqueId(), createdAt: new Date(), updatedAt: new Date(), token }; return createInManagementRealm(this, 'PermissionOfferResponse', permissionOfferResponse) .then(appliedReponse => appliedReponse.realmUrl); }, invalidatePermissionOffer(permissionOfferOrToken) { return getSpecialPurposeRealm(this, '__management', managementSchema) .then(managementRealm => { let permissionOffer; if (typeof permissionOfferOrToken === 'string') { // We were given a token, not an object. Find the matching object. const q = managementRealm.objects('PermissionOffer') .filtered('token = $0', permissionOfferOrToken); if (q.length === 0) { throw new Error("No permission offers with the given token were found"); } permissionOffer = q[0]; } else { permissionOffer = permissionOfferOrToken; } managementRealm.write(() => { permissionOffer.expiresAt = new Date(); }); }); } }