From d443c93893d633c3a5f8254bba18628186d057f3 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 11 Oct 2019 18:15:48 +0200 Subject: [PATCH] Upgrade/downgrade shared folders access rights --- www/common/cryptpad-common.js | 3 +- www/common/drive-ui.js | 3 +- www/common/mergeDrive.js | 1 + www/common/messenger-ui.js | 2 +- www/common/outer/mailbox-handlers.js | 35 ++++++ www/common/outer/messenger.js | 1 + www/common/outer/roster.js | 4 +- www/common/outer/sharedfolder.js | 20 +++- www/common/outer/team.js | 168 ++++++++++++++++++++++++--- www/common/outer/userObject.js | 54 ++++++++- www/common/proxy-manager.js | 10 +- www/common/userObject.js | 26 +++-- www/drive/inner.js | 5 +- www/teams/inner.js | 38 +++--- 14 files changed, 315 insertions(+), 55 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 11c39179e..bea457833 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -158,11 +158,12 @@ define([ }); }; common.addSharedFolder = function (teamId, secret, cb) { + var href = secret.keys && secret.keys.editKeyStr ? '/drive/#' + Hash.getEditHashFromKeys(secret) : undefined; postMessage("ADD_SHARED_FOLDER", { teamId: teamId, path: ['root'], folderData: { - href: '/drive/#' + Hash.getEditHashFromKeys(secret), + href: href, roHref: '/drive/#' + Hash.getViewHashFromKeys(secret), channel: secret.channel, password: secret.password, diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index ec001e1af..94ea1cae5 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -544,7 +544,7 @@ define([ Object.keys(folders).forEach(function (id) { var f = folders[id]; var sfData = files.sharedFolders[id] || {}; - var parsed = Hash.parsePadUrl(sfData.href); + var parsed = Hash.parsePadUrl(sfData.href || sfData.roHref); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); manager.addProxy(id, {proxy: f}, null, secret.keys.secondaryKey); }); @@ -2509,6 +2509,7 @@ define([ $('').text(Messages.shareButton).appendTo($shareBlock); var data = manager.getSharedFolderData(id); var parsed = Hash.parsePadUrl(data.href); + // XXX share modal shared folder read only if (!parsed || !parsed.hash) { return void console.error("Invalid href: "+data.href); } var friends = common.getFriends(); var teams = common.getMetadataMgr().getPrivateData().teams; diff --git a/www/common/mergeDrive.js b/www/common/mergeDrive.js index 4aeb03dc5..b8b87d0f0 100644 --- a/www/common/mergeDrive.js +++ b/www/common/mergeDrive.js @@ -27,6 +27,7 @@ define([ if (parsed) { var proxy = proxyData.proxy; var oldFo = FO.init(parsed.drive, { + readOnly: false, loggedIn: true, outer: true }); diff --git a/www/common/messenger-ui.js b/www/common/messenger-ui.js index 9d6ba71ad..4cbeaf56e 100644 --- a/www/common/messenger-ui.js +++ b/www/common/messenger-ui.js @@ -43,7 +43,7 @@ define([ MessengerUI.create = function ($container, common, toolbar) { var metadataMgr = common.getMetadataMgr(); var origin = metadataMgr.getPrivateData().origin; - var readOnly = metadataMgr.getPrivateData().readOnly; + var readOnly = metadataMgr.getPrivateData().readOnly || toolbar.readOnly; var isApp = typeof(toolbar) !== "undefined"; diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index 87597f0b1..270018eed 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -415,6 +415,41 @@ define([ cb(false); }; + handlers['TEAM_EDIT_RIGHTS'] = function (ctx, box, data, cb) { + var msg = data.msg; + var content = msg.content; + + if (msg.author !== content.user.curvePublic) { return void cb(true); } + if (!content.teamData) { + console.log('Remove invalid notification'); + return void cb(true); + } + + // Make sure we are a member of this team + var myTeams = Util.find(ctx, ['store', 'proxy', 'teams']) || {}; + var teamId; + var team; + Object.keys(myTeams).some(function (k) { + var _team = myTeams[k]; + if (_team.channel === content.teamChannel) { + teamId = k; + team = _team; + return true; + } + }); + if (!teamId) { return void cb(true); } + + var dismiss = false; + try { + var module = ctx.store.modules['team']; + // changeMyRights returns true if we can't change our rights + dismiss = module.changeMyRights(teamId, content.state, content.teamData); + } catch (e) { console.error(e); } + + cb(dismiss); + }; + + return { add: function (ctx, box, data, cb) { diff --git a/www/common/outer/messenger.js b/www/common/outer/messenger.js index 8a97b7dc0..d0e200deb 100644 --- a/www/common/outer/messenger.js +++ b/www/common/outer/messenger.js @@ -814,6 +814,7 @@ define([ var cb = Util.once(Util.mkAsync(function () { ctx.emit('TEAMCHAT_READY', chanId, [clientId]); _cb({ + readOnly: typeof(secret.keys) === "object" && !secret.keys.validateKey, channel: chanId }); })); diff --git a/www/common/outer/roster.js b/www/common/outer/roster.js index be50c6e2c..d5cfc570a 100644 --- a/www/common/outer/roster.js +++ b/www/common/outer/roster.js @@ -168,8 +168,8 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { if (members[curve]) { throw new Error("ALREADY_PRESENT"); } var data = args[curve]; - // if no role was provided, assume VIEWER - if (typeof(data.role) !== 'string') { data.role = 'VIEWER'; } + // if no role was provided, assume MEMBER + if (typeof(data.role) !== 'string') { data.role = 'MEMBER'; } if (!canAddRole(author, data.role, members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index bcee83472..c12900c19 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -77,6 +77,10 @@ define([ var secondaryKey = secret.keys.secondaryKey; var sf = allSharedFolders[secret.channel]; + if (sf && sf.readOnly && secondaryKey) { + // We were in readOnly mode and now we know the edit keys! + SF.upgrade(secret.channel, secret); + } if (sf && sf.ready && sf.rt) { // The shared folder is already loaded, return its data setTimeout(function () { @@ -108,14 +112,15 @@ define([ store: store, id: id }], - team: [store.id || -1] + team: [store.id || -1], + readOnly: Boolean(secondaryKey) }; var owners = data.owners; var listmapConfig = { data: {}, channel: secret.channel, - readOnly: secret.keys && !secret.keys.editKeyStr, + readOnly: Boolean(secondaryKey), crypto: Crypto.createEncryptor(secret.keys), userName: 'sharedFolder', logLevel: 1, @@ -148,6 +153,17 @@ define([ return rt; }; + SF.upgrade = function (channel, secret) { + var sf = allSharedFolders[channel]; + if (!sf || !sf.readOnly) { return; } + if (!sf.rt.setReadOnly) { return; } + + if (!secret.keys || !secret.keys.editKeyStr) { return; } + var crypto = Crypto.createEncryptor(secret.keys); + sf.readOnly = false; + sf.rt.setReadOnly(false, crypto); + }; + SF.leave = function (channel, teamId) { var sf = allSharedFolders[channel]; if (!sf) { return; } diff --git a/www/common/outer/team.js b/www/common/outer/team.js index c57b010c2..86b06a186 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -81,6 +81,7 @@ define([ try { team.listmap.stop(); } catch (e) {} try { team.roster.stop(); } catch (e) {} team.proxy = {}; + team.stopped = true; delete ctx.teams[teamId]; delete ctx.store.proxy.teams[teamId]; ctx.emit('LEAVE_TEAM', teamId, team.clients); @@ -140,8 +141,10 @@ define([ roster: roster }; + // Subscribe to events if (cId) { team.clients.push(cId); } + // Listen for roster changes roster.on('change', function () { var state = roster.getState(); var me = Util.find(ctx, ['store', 'proxy', 'curvePublic']); @@ -158,16 +161,19 @@ define([ rosterData.lastKnownHash = hash; }); + // Update metadata var state = roster.getState(); var teamData = Util.find(ctx, ['store', 'proxy', 'teams', id]); if (teamData) { teamData.metadata = state.metadata; } + // Broadcast an event to all the tabs displaying this team team.sendEvent = function (q, data, sender) { ctx.emit(q, data, team.clients.filter(function (cId) { return cId !== sender; })); }; + // Provide team chat keys to the messenger app team.getChatData = function () { var chatKeys = keys.chat || {}; var hash = chatKeys.edit || chatKeys.view; @@ -177,7 +183,7 @@ define([ teamId: id, channel: secret.channel, secret: secret, - validateKey: secret.keys.validateKey + validateKey: chatKeys.validateKey }; }; @@ -185,6 +191,7 @@ define([ team.pin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); }; team.unpin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); }; nThen(function (waitFor) { + // Init Team RPC if (!keys.drive.edPrivate) { return; } initRpc(ctx, team, keys.drive, waitFor(function (err) { if (err) { return; } @@ -208,6 +215,7 @@ define([ }; })); }).nThen(function () { + // Create the proxy manager var loadSharedFolder = function (id, data, cb) { SF.load({ network: ctx.store.network, @@ -217,9 +225,8 @@ define([ }); }; var teamData = ctx.store.proxy.teams[team.id]; - if (teamData) { - secret = Hash.getSecrets('team', teamData.hash, teamData.password); - } + var hash = teamData.hash || teamData.roHash; + secret = Hash.getSecrets('team', hash, teamData.password); var manager = team.manager = ProxyManager.create(proxy.drive, { onSync: function (cb) { ctx.Store.onSync(id, cb); }, edPublic: keys.drive.edPublic, @@ -251,12 +258,14 @@ define([ team.sendEvent("DRIVE_LOG", msg); }, rt: team.realtime, - editKey: secret && secret.keys.secondaryKey + editKey: secret.keys.secondaryKey, + readOnly: Boolean(!secret.keys.secondaryKey) }); team.secondaryKey = secret && secret.keys.secondaryKey; team.userObject = manager.user.userObject; team.userObject.fixFiles(); }).nThen(function (waitFor) { + // Load the shared folders ctx.teams[id] = team; registerChangeEvents(ctx, team, proxy); SF.checkMigration(team.secondaryKey, proxy, team.userObject, waitFor()); @@ -296,10 +305,20 @@ define([ var openChannel = function (ctx, teamData, id, _cb) { var cb = Util.once(_cb); - var secret = Hash.getSecrets('team', teamData.hash, teamData.password); + var hash = teamData.hash || teamData.roHash; + var secret = Hash.getSecrets('team', hash, teamData.password); var crypto = Crypto.createEncryptor(secret.keys); + if (!teamData.roHash) { + teamData.roHash = Hash.getViewHashFromKeys(secret); + } + var keys = teamData.keys; + if (!keys.chat.validateKey && keys.chat.edit) { + var chatSecret = Hash.getSecrets('chat', keys.chat.edit); + keys.chat.validateKey = chatSecret.keys.validateKey; + } + var roster; var lm; @@ -373,6 +392,7 @@ define([ }, waitFor(function (err, _roster) { if (err) { waitFor.abort(); + console.error(err); return void cb({error: 'ROSTER_ERROR'}); } roster = _roster; @@ -421,6 +441,7 @@ define([ var password = Hash.createChannelId(); var hash = Hash.createRandomHash('team', password); var secret = Hash.getSecrets('team', hash, password); + var roHash = Hash.getViewHashFromKeys(secret); var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey var rosterSeed = Crypto.Team.createSeed(); @@ -520,6 +541,7 @@ define([ chat: { edit: chatHashes.editHash, view: chatHashes.viewHash, + validateKey: chatSecret.keys.validateKey, channel: chatSecret.channel }, roster: { @@ -532,6 +554,7 @@ define([ owner: true, channel: secret.channel, hash: hash, + roHash: roHash, password: password, keys: keys, //members: membersHashes.editHash, @@ -665,7 +688,7 @@ define([ var joinTeam = function (ctx, data, cId, cb) { var team = data.team; - if (!team.hash || !team.channel || !team.password + if (!(team.hash || team.roHash) || !team.channel || !team.password || !team.keys || !team.metadata) { return void cb({error: 'EINVAL'}); } var id = Util.createRandomInteger(); ctx.store.proxy.teams[id] = team; @@ -924,6 +947,92 @@ define([ }); }; + var getInviteData = function (ctx, teamId, edit) { + var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); + if (!teamData) { return {}; } + var data = Util.clone(teamData); + if (!edit) { + // Delete edit keys + delete data.hash; + delete data.keys.drive.edPrivate; + delete data.keys.chat.edit; + } + // Delete owner key + delete data.owner; + return data; + }; + + // Update my edit rights in listmap (only upgrade) and userObject (upgrade and downgrade) + // We also need to propagate the changes to the shared folders + var updateMyRights = function (ctx, teamId, hash) { + if (!teamId) { return true; } + var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); + if (!teamData) { return true; } + var team = ctx.teams[teamId]; + + var secret = Hash.getSecrets('team', hash || teamData.roHash, teamData.password); + // Upgrade the listmap if we can + SF.upgrade(teamData.channel, secret); + // Set the new readOnly value in userObject + if (team.userObject) { + team.userObject.setReadOnly(!secret.keys.secondaryKey, secret.keys.secondaryKey); + } + + // Upgrade the shared folders + var folders = Util.find(team, ['proxy', 'drive', 'sharedFolders']); + Object.keys(folders || {}).forEach(function (sfId) { + var data = team.manager.getSharedFolderData(sfId); + var parsed = Hash.parsePadUrl(data.href || data.roHref); + var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password); + SF.upgrade(secret.channel, secret); + var uo = Util.find(team, ['manager', 'folders', sfId, 'userObject']); + if (uo) { + uo.setReadOnly(!secret.keys.secondaryKey, secret.keys.secondaryKey); + } + }); + ctx.emit('ROSTER_CHANGE_RIGHTS', teamId, team.clients); + }; + + var changeMyRights = function (ctx, teamId, state, data) { + if (!teamId) { return true; } + var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); + if (!teamData) { return true; } + var team = ctx.teams[teamId]; + if (!team) { return true; } + + if (teamData.channel !== data.channel || teamData.password !== data.password) { return true; } + + if (state) { + teamData.hash = data.hash; + teamData.keys.drive.edPrivate = data.keys.drive.edPrivate; + teamData.keys.chat.edit = data.keys.chat.edit; + } else { + delete teamData.hash; + delete teamData.keys.drive.edPrivate; + delete teamData.keys.chat.edit; + } + + updateMyRights(ctx, teamId, data.hash); + }; + var changeEditRights = function (ctx, teamId, user, state, cb) { + if (!teamId) { return void cb({error: 'EINVAL'}); } + var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); + if (!teamData) { return void cb ({error: 'ENOENT'}); } + var team = ctx.teams[teamId]; + if (!team) { return void cb ({error: 'ENOENT'}); } + + // Send mailbox to offer ownership + var myData = Messaging.createData(ctx.store.proxy, false); + ctx.store.mailbox.sendTo("TEAM_EDIT_RIGHTS", { + state: state, + teamData: getInviteData(ctx, teamId, state), + user: myData + }, { + channel: user.notifications, + curvePublic: user.curvePublic + }, cb); + }; + var describeUser = function (ctx, data, cId, cb) { var teamId = data.teamId; if (!teamId) { return void cb({error: 'EINVAL'}); } @@ -937,13 +1046,27 @@ define([ // It it is an ownership revocation, we have to set it in pad metadata first if (user.role === "OWNER" && data.data.role !== "OWNER") { revokeOwnership(ctx, teamId, user, function (err) { - if (!err) { return; } + if (!err) { return void cb(); } console.error(err); return void cb({error: err}); }); return; } + // Viewer to editor + if (user.role === "VIEWER" && data.data.role !== "VIEWER") { + return void changeEditRights(ctx, teamId, user, true, function (err) { + return void cb({error: err}); + }); + } + + // Editor to viewer + if (user.role !== "VIEWER" && data.data.role === "VIEWER") { + return void changeEditRights(ctx, teamId, user, false, function (err) { + return void cb({error: err}); + }); + } + var obj = {}; obj[data.curvePublic] = data.data; team.roster.describe(obj, function (err) { @@ -952,15 +1075,6 @@ define([ }); }; - // TODO send guest keys only in the future - var getInviteData = function (ctx, teamId) { - var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); - if (!teamData) { return {}; } - var data = Util.clone(teamData); - delete data.owner; - return data; - }; - var inviteToTeam = function (ctx, data, cId, cb) { var teamId = data.teamId; if (!teamId) { return void cb({error: 'EINVAL'}); } @@ -975,6 +1089,7 @@ define([ var obj = {}; obj[user.curvePublic] = user; + obj[user.curvePublic].role = 'VIEWER'; team.roster.add(obj, function (err) { if (err && err !== 'NO_CHANGE') { return void cb({error: err}); } ctx.store.mailbox.sendTo('INVITE_TO_TEAM', { @@ -1100,9 +1215,21 @@ define([ if (err) { return; } })); + // Listen for changes in our access rights (if another worker receives edit access) + ctx.store.proxy.on('change', ['teams'], function (o, n, p) { + if (p[2] !== 'hash') { return; } + updateMyRights(ctx, p[1], n); + }); + ctx.store.proxy.on('remove', ['teams'], function (o, p) { + if (p[2] !== 'hash') { return; } + updateMyRights(ctx, p[1]); + }); + + Object.keys(teams).forEach(function (id) { ctx.onReadyHandlers[id] = []; - openChannel(ctx, teams[id], id, waitFor(function () { + openChannel(ctx, teams[id], id, waitFor(function (err) { + if (err) { return void console.error(err); } console.debug('Team '+id+' ready'); })); }); @@ -1121,7 +1248,7 @@ define([ edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']), avatar: Util.find(teams[id], ['metadata', 'avatar']) }; - if (safe) { + if (safe && ctx.teams[id]) { t[id].secondaryKey = ctx.teams[id].secondaryKey; } }); @@ -1147,6 +1274,9 @@ define([ }); }; + team.changeMyRights = function (id, edit, teamData) { + changeMyRights(ctx, id, edit, teamData); + }; team.updateMyData = function (data) { Object.keys(ctx.teams).forEach(function (id) { var team = ctx.teams[id]; diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index dcb588ac7..267b654e7 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -21,6 +21,8 @@ define([ var sharedFolder = config.sharedFolder; var edPublic = config.edPublic; + var readOnly = config.readOnly; + var ROOT = exp.ROOT; var FILES_DATA = exp.FILES_DATA; var OLD_FILES_DATA = exp.OLD_FILES_DATA; @@ -31,8 +33,14 @@ define([ var debug = exp.debug; + exp._setReadOnly = function (state) { + readOnly = state; + if (!readOnly) { exp.fixFiles(); } + }; + exp.setHref = function (channel, id, href) { if (!id && !channel) { return; } + if (readOnly) { return; } var ids = id ? [id] : exp.findChannels([channel]); ids.forEach(function (i) { var data = exp.getFileData(i, true); @@ -42,6 +50,7 @@ define([ exp.setPadAttribute = function (href, attr, value, cb) { cb = cb || function () {}; + if (readOnly) { return void cb('EFORBIDDEN'); } var id = exp.getIdFromHref(href); if (!id) { return void cb("E_INVAL_HREF"); } if (!attr || !attr.trim()) { return void cb("E_INVAL_ATTR"); } @@ -63,6 +72,7 @@ define([ exp.pushData = function (data, cb) { if (typeof cb !== "function") { cb = function () {}; } + if (readOnly) { return void cb('EFORBIDDEN'); } var id = Util.createRandomInteger(); // If we were given an edit link, encrypt its value if needed if (data.href) { data.href = exp.cryptor.encrypt(data.href); } @@ -72,12 +82,21 @@ define([ exp.pushSharedFolder = function (data, cb) { if (typeof cb !== "function") { cb = function () {}; } + if (readOnly) { return void cb('EFORBIDDEN'); } // Check if we already have this shared folder in our drive + var exists; if (Object.keys(files[SHARED_FOLDERS]).some(function (k) { - return files[SHARED_FOLDERS][k].channel === data.channel; + if (files[SHARED_FOLDERS][k].channel === data.channel) { + // We already know this shared folder. Check if we can get better access rights + if (data.href && !files[SHARED_FOLDERS][k].href) { + files[SHARED_FOLDERS][k].href = data.href; + } + exists = k; + return true; + } })) { - return void cb ('EEXISTS'); + return void cb ('EEXISTS', exists); } // Add the folder @@ -92,6 +111,7 @@ define([ // FILES DATA var spliceFileData = function (id) { + if (readOnly) { return; } delete files[FILES_DATA][id]; }; @@ -99,6 +119,7 @@ define([ // FILES_DATA. If there are owned pads, remove them from server too. exp.checkDeletedFiles = function (cb) { if (!loggedIn && !config.testMode) { return void cb(); } + if (readOnly) { return void cb('EFORBIDDEN'); } var filesList = exp.getFiles([ROOT, 'hrefArray', TRASH]); var toClean = []; @@ -144,21 +165,22 @@ define([ cb(null, toClean, ownedRemoved); }; var deleteHrefs = function (ids) { + if (readOnly) { return; } ids.forEach(function (obj) { var idx = files[obj.root].indexOf(obj.id); files[obj.root].splice(idx, 1); }); }; var deleteMultipleTrashRoot = function (roots) { + if (readOnly) { return; } roots.forEach(function (obj) { var idx = files[TRASH][obj.name].indexOf(obj.el); files[TRASH][obj.name].splice(idx, 1); }); }; exp.deleteMultiplePermanently = function (paths, nocheck, cb) { - var hrefPaths = paths.filter(function(x) { return exp.isPathIn(x, ['hrefArray']); }); - var rootPaths = paths.filter(function(x) { return exp.isPathIn(x, [ROOT]); }); - var trashPaths = paths.filter(function(x) { return exp.isPathIn(x, [TRASH]); }); + if (readOnly) { return void cb('EFORBIDDEN'); } + var allFilesPaths = paths.filter(function(x) { return exp.isPathIn(x, [FILES_DATA]); }); if (!loggedIn && !config.testMode) { @@ -170,6 +192,10 @@ define([ return void cb(); } + var hrefPaths = paths.filter(function(x) { return exp.isPathIn(x, ['hrefArray']); }); + var rootPaths = paths.filter(function(x) { return exp.isPathIn(x, [ROOT]); }); + var trashPaths = paths.filter(function(x) { return exp.isPathIn(x, [TRASH]); }); + var ids = []; hrefPaths.forEach(function (path) { var id = exp.find(path); @@ -216,6 +242,7 @@ define([ // From another drive exp.copyFromOtherDrive = function (path, element, data, key) { + if (readOnly) { return; } // Copy files data // We have to remove pads that are already in the current proxy to make sure // we won't create duplicates @@ -275,6 +302,8 @@ define([ // From the same drive var pushToTrash = function (name, element, path) { + if (readOnly) { return; } + var trash = files[TRASH]; if (typeof(trash[name]) === "undefined") { trash[name] = []; } var trashArray = trash[name]; @@ -285,6 +314,7 @@ define([ trashArray.push(trashElement); }; exp.copyElement = function (elementPath, newParentPath) { + if (readOnly) { return; } if (exp.comparePath(elementPath, newParentPath)) { return; } // Nothing to do... var element = exp.find(elementPath); var newParent = exp.find(newParentPath); @@ -332,6 +362,8 @@ define([ // FORGET (move with href not path) exp.forget = function (href) { + if (readOnly) { return; } + var id = exp.getIdFromHref(href); if (!id) { return; } if (!loggedIn && !config.testMode) { @@ -348,6 +380,8 @@ define([ // If all the occurences of an href are in the trash, remove them and add the file in root. // This is use with setPadTitle when we open a stronger version of a deleted pad exp.restoreHref = function (href) { + if (readOnly) { return; } + var idO = exp.getIdFromHref(href); if (!idO || !exp.isFile(idO)) { return; } @@ -370,6 +404,8 @@ define([ }; exp.add = function (id, path) { + if (readOnly) { return; } + if (!loggedIn && !config.testMode) { return; } id = Number(id); var data = files[FILES_DATA][id] || files[SHARED_FOLDERS][id]; @@ -397,6 +433,8 @@ define([ }; exp.setFolderData = function (path, key, value, cb) { + if (readOnly) { return; } + var folder = exp.find(path); if (!exp.isFolder(folder) || exp.isSharedFolder(folder)) { return; } if (!exp.hasFolderData(folder)) { @@ -423,7 +461,7 @@ define([ }; exp.migrateReadOnly = function (cb) { - if (!config.editKey) { return void cb({error: 'EFORBIDDEN'}); } + if (readOnly || !config.editKey) { return void cb({error: 'EFORBIDDEN'}); } if (files.version >= 2) { return void cb(); } // Already migrated, nothing to do files.migrateRo = 1; var next = function () { @@ -453,6 +491,7 @@ define([ }; exp.migrate = function (cb) { + if (readOnly) { return void cb(); } // Make sure unsorted doesn't exist anymore // Note: Unsorted only works with the old structure where pads are href // It should be called before the migration code @@ -551,6 +590,9 @@ define([ // - All files in filesData should be either in 'root', 'trash' or 'unsorted'. If that's not the case, copy the fily to 'unsorted' // * TEMPLATE: Contains only files (href), and does not contains files that are in ROOT + // We can't fix anything in read-only mode: abort + if (readOnly) { return; } + if (silent) { debug = function () {}; } var t0 = +new Date(); diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 00fceb15e..3ceeab773 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -2,9 +2,10 @@ define([ '/common/userObject.js', '/common/common-util.js', '/common/common-hash.js', + '/common/outer/sharedfolder.js', '/customize/messages.js', '/bower_components/nthen/index.js', -], function (UserObject, Util, Hash, Messages, nThen) { +], function (UserObject, Util, Hash, SF, Messages, nThen) { var getConfig = function (Env) { @@ -20,6 +21,7 @@ define([ cfg.id = id; cfg.editKey = editKey; cfg.rt = lm.realtime; + cfg.readOnly = Boolean(!editKey); var userObject = UserObject.init(lm.proxy, cfg); if (userObject.fixFiles) { // Only in outer @@ -452,6 +454,12 @@ define([ // 1. add the shared folder to our list of shared folders // NOTE: pushSharedFolder will encrypt the href directly in the object if needed Env.user.userObject.pushSharedFolder(folderData, waitFor(function (err, folderId) { + if (err === "EEXISTS" && folderData.href && folderId) { + var parsed = Hash.parsePadUrl(folderData.href); + var secret = Hash.getSecrets('drive', parsed.hash, folderData.password); + SF.upgrade(secret.channel, secret); + Env.folders[folderId].userObject.setReadOnly(false, secret.keys.secondaryKey); + } if (err) { waitFor.abort(); return void cb(err); diff --git a/www/common/userObject.js b/www/common/userObject.js index 481c64545..3925b1b4f 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -32,13 +32,15 @@ define([ module.init = function (files, config) { var exp = {}; - exp.cryptor = { - encrypt : function (x) { return x; }, - decrypt : function (x) { return x; }, - }; - if (config.editKey) { + exp.cryptor = {}; + var createCryptor = function (key) { + if (!key) { + exp.cryptor.encrypt = function (x) { return x; }; + exp.cryptor.decrypt = function (x) { return x; }; + return; + } try { - var c = Crypto.createEncryptor(config.editKey); + var c = Crypto.createEncryptor(key); exp.cryptor.encrypt = function (href) { // Never encrypt blob href, they are always read-only if (href.slice(0,7) === '/file/#') { return href; } @@ -48,7 +50,17 @@ define([ } catch (e) { console.error(e); } - } + }; + createCryptor(config.editKey); + + exp.setReadOnly = function (state, key) { + config.editKey = key; + createCryptor(key); + if (exp._setReadOnly) { + // Change outer + exp._setReadOnly(state); + } + }; exp.getDefaultName = module.getDefaultName; diff --git a/www/drive/inner.js b/www/drive/inner.js index ed2461cd0..e0e695126 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -44,7 +44,7 @@ define([ nThen(function (waitFor) { Object.keys(drive.sharedFolders).forEach(function (fId) { var sfData = drive.sharedFolders[fId] || {}; - var parsed = Hash.parsePadUrl(sfData.href); + var parsed = Hash.parsePadUrl(sfData.href || sfData.roHref); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); sframeChan.query('Q_DRIVE_GETOBJECT', { sharedFolder: fId @@ -54,6 +54,9 @@ define([ if (manager && oldIds.indexOf(fId) === -1) { manager.addProxy(fId, { proxy: folders[fId] }, null, secret.keys.secondaryKey); } + var readOnly = !secret.keys.editKeyStr; + if (!manager || !manager.folders[fId]) { return; } + manager.folders[fId].userObject.setReadOnly(readOnly, secret.keys.secondaryKey); })); }); }).nThen(function () { diff --git a/www/teams/inner.js b/www/teams/inner.js index 8df4df44f..f4448de5f 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -53,7 +53,7 @@ define([ nThen(function (waitFor) { Object.keys(drive.sharedFolders).forEach(function (fId) { var sfData = drive.sharedFolders[fId] || {}; - var parsed = Hash.parsePadUrl(sfData.href); + var parsed = Hash.parsePadUrl(sfData.href || sfData.roHref); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password); sframeChan.query('Q_DRIVE_GETOBJECT', { sharedFolder: fId @@ -467,7 +467,7 @@ define([ }); }; - var ROLES = ['MEMBER', 'ADMIN', 'OWNER']; + var ROLES = ['VIEWER', 'MEMBER', 'ADMIN', 'OWNER']; var describeUser = function (common, curvePublic, data, icon) { APP.module.execCommand('DESCRIBE_USER', { teamId: APP.team, @@ -505,10 +505,11 @@ define([ var actions = h('span.cp-team-member-actions'); var $actions = $(actions); var isMe = me && me.curvePublic === data.curvePublic; - var myRole = me ? (ROLES.indexOf(me.role) || 0) : -1; - var theirRole = ROLES.indexOf(data.role) || 0; + var myRole = me ? (ROLES.indexOf(me.role) || 1) : -1; + var theirRole = ROLES.indexOf(data.role) || 1; + var ADMIN = ROLES.indexOf('ADMIN'); // If they're an admin and I am an owner, I can promote them to owner - if (!isMe && myRole > theirRole && theirRole === 1 && !data.pending) { + if (!isMe && myRole > theirRole && theirRole === ADMIN && !data.pending) { var promoteOwner = h('span.fa.fa-angle-double-up', { title: Messages.team_rosterPromoteOwner }); @@ -530,28 +531,28 @@ define([ }); $actions.append(promoteOwner); } - // If they're a member and I have a higher role than them, I can promote them to admin - if (!isMe && myRole > theirRole && theirRole === 0 && !data.pending) { + // If they're a viewer/member and I have a higher role than them, I can promote them to admin + if (!isMe && myRole >= ADMIN && theirRole < ADMIN && !data.pending) { var promote = h('span.fa.fa-angle-double-up', { title: Messages.team_rosterPromote }); $(promote).click(function () { $(promote).hide(); describeUser(common, data.curvePublic, { - role: 'ADMIN' + role: ROLES[theirRole + 1] }, promote); }); $actions.append(promote); } // If I'm not a member and I have an equal or higher role than them, I can demote them // (if they're not already a MEMBER) - if (myRole >= theirRole && theirRole > 0 && !data.pending) { + if (myRole >= theirRole && myRole >= ADMIN && theirRole > 0 && !data.pending) { var demote = h('span.fa.fa-angle-double-down', { title: Messages.team_rosterDemote }); $(demote).click(function () { var todo = function () { - var role = ROLES[theirRole - 1] || 'MEMBER'; + var role = ROLES[theirRole - 1] || 'VIEWER'; $(demote).hide(); describeUser(common, data.curvePublic, { role: role @@ -569,9 +570,9 @@ define([ $actions.append(demote); } } - // If I'm not a member and I have an equal or higher role than them, I can remove them + // If I'm at least an admin and I have an equal or higher role than them, I can remove them // Note: we can't remove owners, we have to demote them first - if (!isMe && myRole > 0 && myRole >= theirRole && theirRole !== 2) { + if (!isMe && myRole >= ADMIN && myRole >= theirRole && theirRole !== ROLES.indexOf('OWNER')) { var remove = h('span.fa.fa-times', { title: Messages.team_rosterKick }); @@ -637,6 +638,12 @@ define([ }).map(function (k) { return makeMember(common, roster[k], me); }); + var viewers = Object.keys(roster).filter(function (k) { + if (roster[k].pending) { return; } + return roster[k].role === "VIEWER"; + }).map(function (k) { + return makeMember(common, roster[k], me); + }); var pending = Object.keys(roster).filter(function (k) { if (!roster[k].pending) { return; } return roster[k].role === "MEMBER" || !roster[k].role; @@ -671,7 +678,7 @@ define([ $header.append(invite); } - if (me && (me.role === 'ADMIN' || me.role === 'MEMBER')) { + if (me && (me.role !== 'OWNER')) { var leave = h('button.btn.btn-danger', Messages.team_leaveButton); $(leave).click(function () { UI.confirm(Messages.team_leaveConfirm, function (yes) { @@ -698,6 +705,8 @@ define([ h('div', admins), h('h3', Messages.team_members), h('div', members), + h('h3', Messages.team_viewers || 'VIEWERS'), // XXX + h('div', viewers), h('h3'+noPending, Messages.team_pending), h('div'+noPending, pending) ]; @@ -721,7 +730,8 @@ define([ common.setTeamChat(obj.channel); MessengerUI.create($(container), common, { chat: $('.cp-team-cat-chat'), - team: true + team: true, + readOnly: obj.readOnly }); cb(content); });