Upgrade/downgrade shared folders access rights

pull/1/head
yflory 5 years ago
parent 8ca7e11150
commit d443c93893

@ -158,11 +158,12 @@ define([
}); });
}; };
common.addSharedFolder = function (teamId, secret, cb) { common.addSharedFolder = function (teamId, secret, cb) {
var href = secret.keys && secret.keys.editKeyStr ? '/drive/#' + Hash.getEditHashFromKeys(secret) : undefined;
postMessage("ADD_SHARED_FOLDER", { postMessage("ADD_SHARED_FOLDER", {
teamId: teamId, teamId: teamId,
path: ['root'], path: ['root'],
folderData: { folderData: {
href: '/drive/#' + Hash.getEditHashFromKeys(secret), href: href,
roHref: '/drive/#' + Hash.getViewHashFromKeys(secret), roHref: '/drive/#' + Hash.getViewHashFromKeys(secret),
channel: secret.channel, channel: secret.channel,
password: secret.password, password: secret.password,

@ -544,7 +544,7 @@ define([
Object.keys(folders).forEach(function (id) { Object.keys(folders).forEach(function (id) {
var f = folders[id]; var f = folders[id];
var sfData = files.sharedFolders[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); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password);
manager.addProxy(id, {proxy: f}, null, secret.keys.secondaryKey); manager.addProxy(id, {proxy: f}, null, secret.keys.secondaryKey);
}); });
@ -2509,6 +2509,7 @@ define([
$('<span>').text(Messages.shareButton).appendTo($shareBlock); $('<span>').text(Messages.shareButton).appendTo($shareBlock);
var data = manager.getSharedFolderData(id); var data = manager.getSharedFolderData(id);
var parsed = Hash.parsePadUrl(data.href); 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); } if (!parsed || !parsed.hash) { return void console.error("Invalid href: "+data.href); }
var friends = common.getFriends(); var friends = common.getFriends();
var teams = common.getMetadataMgr().getPrivateData().teams; var teams = common.getMetadataMgr().getPrivateData().teams;

@ -27,6 +27,7 @@ define([
if (parsed) { if (parsed) {
var proxy = proxyData.proxy; var proxy = proxyData.proxy;
var oldFo = FO.init(parsed.drive, { var oldFo = FO.init(parsed.drive, {
readOnly: false,
loggedIn: true, loggedIn: true,
outer: true outer: true
}); });

@ -43,7 +43,7 @@ define([
MessengerUI.create = function ($container, common, toolbar) { MessengerUI.create = function ($container, common, toolbar) {
var metadataMgr = common.getMetadataMgr(); var metadataMgr = common.getMetadataMgr();
var origin = metadataMgr.getPrivateData().origin; var origin = metadataMgr.getPrivateData().origin;
var readOnly = metadataMgr.getPrivateData().readOnly; var readOnly = metadataMgr.getPrivateData().readOnly || toolbar.readOnly;
var isApp = typeof(toolbar) !== "undefined"; var isApp = typeof(toolbar) !== "undefined";

@ -415,6 +415,41 @@ define([
cb(false); 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 { return {
add: function (ctx, box, data, cb) { add: function (ctx, box, data, cb) {

@ -814,6 +814,7 @@ define([
var cb = Util.once(Util.mkAsync(function () { var cb = Util.once(Util.mkAsync(function () {
ctx.emit('TEAMCHAT_READY', chanId, [clientId]); ctx.emit('TEAMCHAT_READY', chanId, [clientId]);
_cb({ _cb({
readOnly: typeof(secret.keys) === "object" && !secret.keys.validateKey,
channel: chanId channel: chanId
}); });
})); }));

@ -168,8 +168,8 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
if (members[curve]) { throw new Error("ALREADY_PRESENT"); } if (members[curve]) { throw new Error("ALREADY_PRESENT"); }
var data = args[curve]; var data = args[curve];
// if no role was provided, assume VIEWER // if no role was provided, assume MEMBER
if (typeof(data.role) !== 'string') { data.role = 'VIEWER'; } if (typeof(data.role) !== 'string') { data.role = 'MEMBER'; }
if (!canAddRole(author, data.role, members)) { if (!canAddRole(author, data.role, members)) {
throw new Error("INSUFFICIENT_PERMISSIONS"); throw new Error("INSUFFICIENT_PERMISSIONS");

@ -77,6 +77,10 @@ define([
var secondaryKey = secret.keys.secondaryKey; var secondaryKey = secret.keys.secondaryKey;
var sf = allSharedFolders[secret.channel]; 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) { if (sf && sf.ready && sf.rt) {
// The shared folder is already loaded, return its data // The shared folder is already loaded, return its data
setTimeout(function () { setTimeout(function () {
@ -108,14 +112,15 @@ define([
store: store, store: store,
id: id id: id
}], }],
team: [store.id || -1] team: [store.id || -1],
readOnly: Boolean(secondaryKey)
}; };
var owners = data.owners; var owners = data.owners;
var listmapConfig = { var listmapConfig = {
data: {}, data: {},
channel: secret.channel, channel: secret.channel,
readOnly: secret.keys && !secret.keys.editKeyStr, readOnly: Boolean(secondaryKey),
crypto: Crypto.createEncryptor(secret.keys), crypto: Crypto.createEncryptor(secret.keys),
userName: 'sharedFolder', userName: 'sharedFolder',
logLevel: 1, logLevel: 1,
@ -148,6 +153,17 @@ define([
return rt; 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) { SF.leave = function (channel, teamId) {
var sf = allSharedFolders[channel]; var sf = allSharedFolders[channel];
if (!sf) { return; } if (!sf) { return; }

@ -81,6 +81,7 @@ define([
try { team.listmap.stop(); } catch (e) {} try { team.listmap.stop(); } catch (e) {}
try { team.roster.stop(); } catch (e) {} try { team.roster.stop(); } catch (e) {}
team.proxy = {}; team.proxy = {};
team.stopped = true;
delete ctx.teams[teamId]; delete ctx.teams[teamId];
delete ctx.store.proxy.teams[teamId]; delete ctx.store.proxy.teams[teamId];
ctx.emit('LEAVE_TEAM', teamId, team.clients); ctx.emit('LEAVE_TEAM', teamId, team.clients);
@ -140,8 +141,10 @@ define([
roster: roster roster: roster
}; };
// Subscribe to events
if (cId) { team.clients.push(cId); } if (cId) { team.clients.push(cId); }
// Listen for roster changes
roster.on('change', function () { roster.on('change', function () {
var state = roster.getState(); var state = roster.getState();
var me = Util.find(ctx, ['store', 'proxy', 'curvePublic']); var me = Util.find(ctx, ['store', 'proxy', 'curvePublic']);
@ -158,16 +161,19 @@ define([
rosterData.lastKnownHash = hash; rosterData.lastKnownHash = hash;
}); });
// Update metadata
var state = roster.getState(); var state = roster.getState();
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', id]); var teamData = Util.find(ctx, ['store', 'proxy', 'teams', id]);
if (teamData) { teamData.metadata = state.metadata; } if (teamData) { teamData.metadata = state.metadata; }
// Broadcast an event to all the tabs displaying this team
team.sendEvent = function (q, data, sender) { team.sendEvent = function (q, data, sender) {
ctx.emit(q, data, team.clients.filter(function (cId) { ctx.emit(q, data, team.clients.filter(function (cId) {
return cId !== sender; return cId !== sender;
})); }));
}; };
// Provide team chat keys to the messenger app
team.getChatData = function () { team.getChatData = function () {
var chatKeys = keys.chat || {}; var chatKeys = keys.chat || {};
var hash = chatKeys.edit || chatKeys.view; var hash = chatKeys.edit || chatKeys.view;
@ -177,7 +183,7 @@ define([
teamId: id, teamId: id,
channel: secret.channel, channel: secret.channel,
secret: secret, 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.pin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); };
team.unpin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); }; team.unpin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); };
nThen(function (waitFor) { nThen(function (waitFor) {
// Init Team RPC
if (!keys.drive.edPrivate) { return; } if (!keys.drive.edPrivate) { return; }
initRpc(ctx, team, keys.drive, waitFor(function (err) { initRpc(ctx, team, keys.drive, waitFor(function (err) {
if (err) { return; } if (err) { return; }
@ -208,6 +215,7 @@ define([
}; };
})); }));
}).nThen(function () { }).nThen(function () {
// Create the proxy manager
var loadSharedFolder = function (id, data, cb) { var loadSharedFolder = function (id, data, cb) {
SF.load({ SF.load({
network: ctx.store.network, network: ctx.store.network,
@ -217,9 +225,8 @@ define([
}); });
}; };
var teamData = ctx.store.proxy.teams[team.id]; var teamData = ctx.store.proxy.teams[team.id];
if (teamData) { var hash = teamData.hash || teamData.roHash;
secret = Hash.getSecrets('team', teamData.hash, teamData.password); secret = Hash.getSecrets('team', hash, teamData.password);
}
var manager = team.manager = ProxyManager.create(proxy.drive, { var manager = team.manager = ProxyManager.create(proxy.drive, {
onSync: function (cb) { ctx.Store.onSync(id, cb); }, onSync: function (cb) { ctx.Store.onSync(id, cb); },
edPublic: keys.drive.edPublic, edPublic: keys.drive.edPublic,
@ -251,12 +258,14 @@ define([
team.sendEvent("DRIVE_LOG", msg); team.sendEvent("DRIVE_LOG", msg);
}, },
rt: team.realtime, rt: team.realtime,
editKey: secret && secret.keys.secondaryKey editKey: secret.keys.secondaryKey,
readOnly: Boolean(!secret.keys.secondaryKey)
}); });
team.secondaryKey = secret && secret.keys.secondaryKey; team.secondaryKey = secret && secret.keys.secondaryKey;
team.userObject = manager.user.userObject; team.userObject = manager.user.userObject;
team.userObject.fixFiles(); team.userObject.fixFiles();
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
// Load the shared folders
ctx.teams[id] = team; ctx.teams[id] = team;
registerChangeEvents(ctx, team, proxy); registerChangeEvents(ctx, team, proxy);
SF.checkMigration(team.secondaryKey, proxy, team.userObject, waitFor()); SF.checkMigration(team.secondaryKey, proxy, team.userObject, waitFor());
@ -296,10 +305,20 @@ define([
var openChannel = function (ctx, teamData, id, _cb) { var openChannel = function (ctx, teamData, id, _cb) {
var cb = Util.once(_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); var crypto = Crypto.createEncryptor(secret.keys);
if (!teamData.roHash) {
teamData.roHash = Hash.getViewHashFromKeys(secret);
}
var keys = teamData.keys; 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 roster;
var lm; var lm;
@ -373,6 +392,7 @@ define([
}, waitFor(function (err, _roster) { }, waitFor(function (err, _roster) {
if (err) { if (err) {
waitFor.abort(); waitFor.abort();
console.error(err);
return void cb({error: 'ROSTER_ERROR'}); return void cb({error: 'ROSTER_ERROR'});
} }
roster = _roster; roster = _roster;
@ -421,6 +441,7 @@ define([
var password = Hash.createChannelId(); var password = Hash.createChannelId();
var hash = Hash.createRandomHash('team', password); var hash = Hash.createRandomHash('team', password);
var secret = Hash.getSecrets('team', hash, password); var secret = Hash.getSecrets('team', hash, password);
var roHash = Hash.getViewHashFromKeys(secret);
var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey
var rosterSeed = Crypto.Team.createSeed(); var rosterSeed = Crypto.Team.createSeed();
@ -520,6 +541,7 @@ define([
chat: { chat: {
edit: chatHashes.editHash, edit: chatHashes.editHash,
view: chatHashes.viewHash, view: chatHashes.viewHash,
validateKey: chatSecret.keys.validateKey,
channel: chatSecret.channel channel: chatSecret.channel
}, },
roster: { roster: {
@ -532,6 +554,7 @@ define([
owner: true, owner: true,
channel: secret.channel, channel: secret.channel,
hash: hash, hash: hash,
roHash: roHash,
password: password, password: password,
keys: keys, keys: keys,
//members: membersHashes.editHash, //members: membersHashes.editHash,
@ -665,7 +688,7 @@ define([
var joinTeam = function (ctx, data, cId, cb) { var joinTeam = function (ctx, data, cId, cb) {
var team = data.team; 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'}); } || !team.keys || !team.metadata) { return void cb({error: 'EINVAL'}); }
var id = Util.createRandomInteger(); var id = Util.createRandomInteger();
ctx.store.proxy.teams[id] = team; 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 describeUser = function (ctx, data, cId, cb) {
var teamId = data.teamId; var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); } 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 // It it is an ownership revocation, we have to set it in pad metadata first
if (user.role === "OWNER" && data.data.role !== "OWNER") { if (user.role === "OWNER" && data.data.role !== "OWNER") {
revokeOwnership(ctx, teamId, user, function (err) { revokeOwnership(ctx, teamId, user, function (err) {
if (!err) { return; } if (!err) { return void cb(); }
console.error(err); console.error(err);
return void cb({error: err}); return void cb({error: err});
}); });
return; 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 = {}; var obj = {};
obj[data.curvePublic] = data.data; obj[data.curvePublic] = data.data;
team.roster.describe(obj, function (err) { 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 inviteToTeam = function (ctx, data, cId, cb) {
var teamId = data.teamId; var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); } if (!teamId) { return void cb({error: 'EINVAL'}); }
@ -975,6 +1089,7 @@ define([
var obj = {}; var obj = {};
obj[user.curvePublic] = user; obj[user.curvePublic] = user;
obj[user.curvePublic].role = 'VIEWER';
team.roster.add(obj, function (err) { team.roster.add(obj, function (err) {
if (err && err !== 'NO_CHANGE') { return void cb({error: err}); } if (err && err !== 'NO_CHANGE') { return void cb({error: err}); }
ctx.store.mailbox.sendTo('INVITE_TO_TEAM', { ctx.store.mailbox.sendTo('INVITE_TO_TEAM', {
@ -1100,9 +1215,21 @@ define([
if (err) { return; } 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) { Object.keys(teams).forEach(function (id) {
ctx.onReadyHandlers[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'); console.debug('Team '+id+' ready');
})); }));
}); });
@ -1121,7 +1248,7 @@ define([
edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']), edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']),
avatar: Util.find(teams[id], ['metadata', 'avatar']) avatar: Util.find(teams[id], ['metadata', 'avatar'])
}; };
if (safe) { if (safe && ctx.teams[id]) {
t[id].secondaryKey = ctx.teams[id].secondaryKey; 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) { team.updateMyData = function (data) {
Object.keys(ctx.teams).forEach(function (id) { Object.keys(ctx.teams).forEach(function (id) {
var team = ctx.teams[id]; var team = ctx.teams[id];

@ -21,6 +21,8 @@ define([
var sharedFolder = config.sharedFolder; var sharedFolder = config.sharedFolder;
var edPublic = config.edPublic; var edPublic = config.edPublic;
var readOnly = config.readOnly;
var ROOT = exp.ROOT; var ROOT = exp.ROOT;
var FILES_DATA = exp.FILES_DATA; var FILES_DATA = exp.FILES_DATA;
var OLD_FILES_DATA = exp.OLD_FILES_DATA; var OLD_FILES_DATA = exp.OLD_FILES_DATA;
@ -31,8 +33,14 @@ define([
var debug = exp.debug; var debug = exp.debug;
exp._setReadOnly = function (state) {
readOnly = state;
if (!readOnly) { exp.fixFiles(); }
};
exp.setHref = function (channel, id, href) { exp.setHref = function (channel, id, href) {
if (!id && !channel) { return; } if (!id && !channel) { return; }
if (readOnly) { return; }
var ids = id ? [id] : exp.findChannels([channel]); var ids = id ? [id] : exp.findChannels([channel]);
ids.forEach(function (i) { ids.forEach(function (i) {
var data = exp.getFileData(i, true); var data = exp.getFileData(i, true);
@ -42,6 +50,7 @@ define([
exp.setPadAttribute = function (href, attr, value, cb) { exp.setPadAttribute = function (href, attr, value, cb) {
cb = cb || function () {}; cb = cb || function () {};
if (readOnly) { return void cb('EFORBIDDEN'); }
var id = exp.getIdFromHref(href); var id = exp.getIdFromHref(href);
if (!id) { return void cb("E_INVAL_HREF"); } if (!id) { return void cb("E_INVAL_HREF"); }
if (!attr || !attr.trim()) { return void cb("E_INVAL_ATTR"); } if (!attr || !attr.trim()) { return void cb("E_INVAL_ATTR"); }
@ -63,6 +72,7 @@ define([
exp.pushData = function (data, cb) { exp.pushData = function (data, cb) {
if (typeof cb !== "function") { cb = function () {}; } if (typeof cb !== "function") { cb = function () {}; }
if (readOnly) { return void cb('EFORBIDDEN'); }
var id = Util.createRandomInteger(); var id = Util.createRandomInteger();
// If we were given an edit link, encrypt its value if needed // If we were given an edit link, encrypt its value if needed
if (data.href) { data.href = exp.cryptor.encrypt(data.href); } if (data.href) { data.href = exp.cryptor.encrypt(data.href); }
@ -72,12 +82,21 @@ define([
exp.pushSharedFolder = function (data, cb) { exp.pushSharedFolder = function (data, cb) {
if (typeof cb !== "function") { cb = function () {}; } if (typeof cb !== "function") { cb = function () {}; }
if (readOnly) { return void cb('EFORBIDDEN'); }
// Check if we already have this shared folder in our drive // Check if we already have this shared folder in our drive
var exists;
if (Object.keys(files[SHARED_FOLDERS]).some(function (k) { 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 // Add the folder
@ -92,6 +111,7 @@ define([
// FILES DATA // FILES DATA
var spliceFileData = function (id) { var spliceFileData = function (id) {
if (readOnly) { return; }
delete files[FILES_DATA][id]; delete files[FILES_DATA][id];
}; };
@ -99,6 +119,7 @@ define([
// FILES_DATA. If there are owned pads, remove them from server too. // FILES_DATA. If there are owned pads, remove them from server too.
exp.checkDeletedFiles = function (cb) { exp.checkDeletedFiles = function (cb) {
if (!loggedIn && !config.testMode) { return void cb(); } if (!loggedIn && !config.testMode) { return void cb(); }
if (readOnly) { return void cb('EFORBIDDEN'); }
var filesList = exp.getFiles([ROOT, 'hrefArray', TRASH]); var filesList = exp.getFiles([ROOT, 'hrefArray', TRASH]);
var toClean = []; var toClean = [];
@ -144,21 +165,22 @@ define([
cb(null, toClean, ownedRemoved); cb(null, toClean, ownedRemoved);
}; };
var deleteHrefs = function (ids) { var deleteHrefs = function (ids) {
if (readOnly) { return; }
ids.forEach(function (obj) { ids.forEach(function (obj) {
var idx = files[obj.root].indexOf(obj.id); var idx = files[obj.root].indexOf(obj.id);
files[obj.root].splice(idx, 1); files[obj.root].splice(idx, 1);
}); });
}; };
var deleteMultipleTrashRoot = function (roots) { var deleteMultipleTrashRoot = function (roots) {
if (readOnly) { return; }
roots.forEach(function (obj) { roots.forEach(function (obj) {
var idx = files[TRASH][obj.name].indexOf(obj.el); var idx = files[TRASH][obj.name].indexOf(obj.el);
files[TRASH][obj.name].splice(idx, 1); files[TRASH][obj.name].splice(idx, 1);
}); });
}; };
exp.deleteMultiplePermanently = function (paths, nocheck, cb) { exp.deleteMultiplePermanently = function (paths, nocheck, cb) {
var hrefPaths = paths.filter(function(x) { return exp.isPathIn(x, ['hrefArray']); }); if (readOnly) { return void cb('EFORBIDDEN'); }
var rootPaths = paths.filter(function(x) { return exp.isPathIn(x, [ROOT]); });
var trashPaths = paths.filter(function(x) { return exp.isPathIn(x, [TRASH]); });
var allFilesPaths = paths.filter(function(x) { return exp.isPathIn(x, [FILES_DATA]); }); var allFilesPaths = paths.filter(function(x) { return exp.isPathIn(x, [FILES_DATA]); });
if (!loggedIn && !config.testMode) { if (!loggedIn && !config.testMode) {
@ -170,6 +192,10 @@ define([
return void cb(); 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 = []; var ids = [];
hrefPaths.forEach(function (path) { hrefPaths.forEach(function (path) {
var id = exp.find(path); var id = exp.find(path);
@ -216,6 +242,7 @@ define([
// From another drive // From another drive
exp.copyFromOtherDrive = function (path, element, data, key) { exp.copyFromOtherDrive = function (path, element, data, key) {
if (readOnly) { return; }
// Copy files data // Copy files data
// We have to remove pads that are already in the current proxy to make sure // We have to remove pads that are already in the current proxy to make sure
// we won't create duplicates // we won't create duplicates
@ -275,6 +302,8 @@ define([
// From the same drive // From the same drive
var pushToTrash = function (name, element, path) { var pushToTrash = function (name, element, path) {
if (readOnly) { return; }
var trash = files[TRASH]; var trash = files[TRASH];
if (typeof(trash[name]) === "undefined") { trash[name] = []; } if (typeof(trash[name]) === "undefined") { trash[name] = []; }
var trashArray = trash[name]; var trashArray = trash[name];
@ -285,6 +314,7 @@ define([
trashArray.push(trashElement); trashArray.push(trashElement);
}; };
exp.copyElement = function (elementPath, newParentPath) { exp.copyElement = function (elementPath, newParentPath) {
if (readOnly) { return; }
if (exp.comparePath(elementPath, newParentPath)) { return; } // Nothing to do... if (exp.comparePath(elementPath, newParentPath)) { return; } // Nothing to do...
var element = exp.find(elementPath); var element = exp.find(elementPath);
var newParent = exp.find(newParentPath); var newParent = exp.find(newParentPath);
@ -332,6 +362,8 @@ define([
// FORGET (move with href not path) // FORGET (move with href not path)
exp.forget = function (href) { exp.forget = function (href) {
if (readOnly) { return; }
var id = exp.getIdFromHref(href); var id = exp.getIdFromHref(href);
if (!id) { return; } if (!id) { return; }
if (!loggedIn && !config.testMode) { 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. // 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 // This is use with setPadTitle when we open a stronger version of a deleted pad
exp.restoreHref = function (href) { exp.restoreHref = function (href) {
if (readOnly) { return; }
var idO = exp.getIdFromHref(href); var idO = exp.getIdFromHref(href);
if (!idO || !exp.isFile(idO)) { return; } if (!idO || !exp.isFile(idO)) { return; }
@ -370,6 +404,8 @@ define([
}; };
exp.add = function (id, path) { exp.add = function (id, path) {
if (readOnly) { return; }
if (!loggedIn && !config.testMode) { return; } if (!loggedIn && !config.testMode) { return; }
id = Number(id); id = Number(id);
var data = files[FILES_DATA][id] || files[SHARED_FOLDERS][id]; var data = files[FILES_DATA][id] || files[SHARED_FOLDERS][id];
@ -397,6 +433,8 @@ define([
}; };
exp.setFolderData = function (path, key, value, cb) { exp.setFolderData = function (path, key, value, cb) {
if (readOnly) { return; }
var folder = exp.find(path); var folder = exp.find(path);
if (!exp.isFolder(folder) || exp.isSharedFolder(folder)) { return; } if (!exp.isFolder(folder) || exp.isSharedFolder(folder)) { return; }
if (!exp.hasFolderData(folder)) { if (!exp.hasFolderData(folder)) {
@ -423,7 +461,7 @@ define([
}; };
exp.migrateReadOnly = function (cb) { 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 if (files.version >= 2) { return void cb(); } // Already migrated, nothing to do
files.migrateRo = 1; files.migrateRo = 1;
var next = function () { var next = function () {
@ -453,6 +491,7 @@ define([
}; };
exp.migrate = function (cb) { exp.migrate = function (cb) {
if (readOnly) { return void cb(); }
// Make sure unsorted doesn't exist anymore // Make sure unsorted doesn't exist anymore
// Note: Unsorted only works with the old structure where pads are href // Note: Unsorted only works with the old structure where pads are href
// It should be called before the migration code // 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' // - 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 // * 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 () {}; } if (silent) { debug = function () {}; }
var t0 = +new Date(); var t0 = +new Date();

@ -2,9 +2,10 @@ define([
'/common/userObject.js', '/common/userObject.js',
'/common/common-util.js', '/common/common-util.js',
'/common/common-hash.js', '/common/common-hash.js',
'/common/outer/sharedfolder.js',
'/customize/messages.js', '/customize/messages.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
], function (UserObject, Util, Hash, Messages, nThen) { ], function (UserObject, Util, Hash, SF, Messages, nThen) {
var getConfig = function (Env) { var getConfig = function (Env) {
@ -20,6 +21,7 @@ define([
cfg.id = id; cfg.id = id;
cfg.editKey = editKey; cfg.editKey = editKey;
cfg.rt = lm.realtime; cfg.rt = lm.realtime;
cfg.readOnly = Boolean(!editKey);
var userObject = UserObject.init(lm.proxy, cfg); var userObject = UserObject.init(lm.proxy, cfg);
if (userObject.fixFiles) { if (userObject.fixFiles) {
// Only in outer // Only in outer
@ -452,6 +454,12 @@ define([
// 1. add the shared folder to our list of shared folders // 1. add the shared folder to our list of shared folders
// NOTE: pushSharedFolder will encrypt the href directly in the object if needed // NOTE: pushSharedFolder will encrypt the href directly in the object if needed
Env.user.userObject.pushSharedFolder(folderData, waitFor(function (err, folderId) { 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) { if (err) {
waitFor.abort(); waitFor.abort();
return void cb(err); return void cb(err);

@ -32,13 +32,15 @@ define([
module.init = function (files, config) { module.init = function (files, config) {
var exp = {}; var exp = {};
exp.cryptor = { exp.cryptor = {};
encrypt : function (x) { return x; }, var createCryptor = function (key) {
decrypt : function (x) { return x; }, if (!key) {
}; exp.cryptor.encrypt = function (x) { return x; };
if (config.editKey) { exp.cryptor.decrypt = function (x) { return x; };
return;
}
try { try {
var c = Crypto.createEncryptor(config.editKey); var c = Crypto.createEncryptor(key);
exp.cryptor.encrypt = function (href) { exp.cryptor.encrypt = function (href) {
// Never encrypt blob href, they are always read-only // Never encrypt blob href, they are always read-only
if (href.slice(0,7) === '/file/#') { return href; } if (href.slice(0,7) === '/file/#') { return href; }
@ -48,7 +50,17 @@ define([
} catch (e) { } catch (e) {
console.error(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; exp.getDefaultName = module.getDefaultName;

@ -44,7 +44,7 @@ define([
nThen(function (waitFor) { nThen(function (waitFor) {
Object.keys(drive.sharedFolders).forEach(function (fId) { Object.keys(drive.sharedFolders).forEach(function (fId) {
var sfData = drive.sharedFolders[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); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password);
sframeChan.query('Q_DRIVE_GETOBJECT', { sframeChan.query('Q_DRIVE_GETOBJECT', {
sharedFolder: fId sharedFolder: fId
@ -54,6 +54,9 @@ define([
if (manager && oldIds.indexOf(fId) === -1) { if (manager && oldIds.indexOf(fId) === -1) {
manager.addProxy(fId, { proxy: folders[fId] }, null, secret.keys.secondaryKey); 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 () { }).nThen(function () {

@ -53,7 +53,7 @@ define([
nThen(function (waitFor) { nThen(function (waitFor) {
Object.keys(drive.sharedFolders).forEach(function (fId) { Object.keys(drive.sharedFolders).forEach(function (fId) {
var sfData = drive.sharedFolders[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); var secret = Hash.getSecrets('drive', parsed.hash, sfData.password);
sframeChan.query('Q_DRIVE_GETOBJECT', { sframeChan.query('Q_DRIVE_GETOBJECT', {
sharedFolder: fId 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) { var describeUser = function (common, curvePublic, data, icon) {
APP.module.execCommand('DESCRIBE_USER', { APP.module.execCommand('DESCRIBE_USER', {
teamId: APP.team, teamId: APP.team,
@ -505,10 +505,11 @@ define([
var actions = h('span.cp-team-member-actions'); var actions = h('span.cp-team-member-actions');
var $actions = $(actions); var $actions = $(actions);
var isMe = me && me.curvePublic === data.curvePublic; var isMe = me && me.curvePublic === data.curvePublic;
var myRole = me ? (ROLES.indexOf(me.role) || 0) : -1; var myRole = me ? (ROLES.indexOf(me.role) || 1) : -1;
var theirRole = ROLES.indexOf(data.role) || 0; 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 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', { var promoteOwner = h('span.fa.fa-angle-double-up', {
title: Messages.team_rosterPromoteOwner title: Messages.team_rosterPromoteOwner
}); });
@ -530,28 +531,28 @@ define([
}); });
$actions.append(promoteOwner); $actions.append(promoteOwner);
} }
// If they're a member and I have a higher role than them, I can promote them to admin // If they're a viewer/member and I have a higher role than them, I can promote them to admin
if (!isMe && myRole > theirRole && theirRole === 0 && !data.pending) { if (!isMe && myRole >= ADMIN && theirRole < ADMIN && !data.pending) {
var promote = h('span.fa.fa-angle-double-up', { var promote = h('span.fa.fa-angle-double-up', {
title: Messages.team_rosterPromote title: Messages.team_rosterPromote
}); });
$(promote).click(function () { $(promote).click(function () {
$(promote).hide(); $(promote).hide();
describeUser(common, data.curvePublic, { describeUser(common, data.curvePublic, {
role: 'ADMIN' role: ROLES[theirRole + 1]
}, promote); }, promote);
}); });
$actions.append(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 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 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', { var demote = h('span.fa.fa-angle-double-down', {
title: Messages.team_rosterDemote title: Messages.team_rosterDemote
}); });
$(demote).click(function () { $(demote).click(function () {
var todo = function () { var todo = function () {
var role = ROLES[theirRole - 1] || 'MEMBER'; var role = ROLES[theirRole - 1] || 'VIEWER';
$(demote).hide(); $(demote).hide();
describeUser(common, data.curvePublic, { describeUser(common, data.curvePublic, {
role: role role: role
@ -569,9 +570,9 @@ define([
$actions.append(demote); $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 // 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', { var remove = h('span.fa.fa-times', {
title: Messages.team_rosterKick title: Messages.team_rosterKick
}); });
@ -637,6 +638,12 @@ define([
}).map(function (k) { }).map(function (k) {
return makeMember(common, roster[k], me); 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) { var pending = Object.keys(roster).filter(function (k) {
if (!roster[k].pending) { return; } if (!roster[k].pending) { return; }
return roster[k].role === "MEMBER" || !roster[k].role; return roster[k].role === "MEMBER" || !roster[k].role;
@ -671,7 +678,7 @@ define([
$header.append(invite); $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); var leave = h('button.btn.btn-danger', Messages.team_leaveButton);
$(leave).click(function () { $(leave).click(function () {
UI.confirm(Messages.team_leaveConfirm, function (yes) { UI.confirm(Messages.team_leaveConfirm, function (yes) {
@ -698,6 +705,8 @@ define([
h('div', admins), h('div', admins),
h('h3', Messages.team_members), h('h3', Messages.team_members),
h('div', members), h('div', members),
h('h3', Messages.team_viewers || 'VIEWERS'), // XXX
h('div', viewers),
h('h3'+noPending, Messages.team_pending), h('h3'+noPending, Messages.team_pending),
h('div'+noPending, pending) h('div'+noPending, pending)
]; ];
@ -721,7 +730,8 @@ define([
common.setTeamChat(obj.channel); common.setTeamChat(obj.channel);
MessengerUI.create($(container), common, { MessengerUI.create($(container), common, {
chat: $('.cp-team-cat-chat'), chat: $('.cp-team-cat-chat'),
team: true team: true,
readOnly: obj.readOnly
}); });
cb(content); cb(content);
}); });

Loading…
Cancel
Save