diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 76dec3dd8..2e6c3a609 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -736,14 +736,20 @@ define([ UI.proposal = function (content, cb) { + var clicked = false; var buttons = [{ name: Messages.friendRequest_later, - onClick: function () {}, + onClick: function () { + if (clicked) { return; } + clicked = true; + }, keys: [27] }, { className: 'primary', name: Messages.friendRequest_accept, onClick: function () { + if (clicked) { return; } + clicked = true; cb(true); }, keys: [13] @@ -751,6 +757,8 @@ define([ className: 'primary', name: Messages.friendRequest_decline, onClick: function () { + if (clicked) { return; } + clicked = true; cb(false); }, keys: [[13, 'ctrl']] diff --git a/www/common/outer/roster.js b/www/common/outer/roster.js index 1db1bf5c7..2c2675037 100644 --- a/www/common/outer/roster.js +++ b/www/common/outer/roster.js @@ -56,6 +56,16 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { return ['OWNER', 'ADMIN', 'MEMBER', 'VIEWER'].indexOf(role) !== -1; }; + var isSelfDowngrade = function (author, curve, role, state) { + // Make sure you want to describe yourself + var selfDescribe = author === curve && state[curve]; + if (!selfDescribe) { return false; } + // ADMIN and OWNER can always update roles + // we only need to allow MEMBER to downgrade themselves to VIEWER + var authorRole = Util.find(state, [author, 'role']); + if (authorRole === "MEMBER") { return role === 'VIEWER'; } + }; + var canAddRole = function (author, role, members) { var authorRole = Util.find(members, [author, 'role']); if (!authorRole) { return false; } @@ -244,7 +254,10 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) { if (typeof(data.role) === 'string') { // they're trying to change the role... // throw if they're trying to upgrade to something greater - if (!canAddRole(author, data.role, members)) { throw new Error("INSUFFICIENT_PERMISSIONS"); } + if (!isSelfDowngrade(author, curve, data.role, members) && + !canAddRole(author, data.role, members)) { + throw new Error("INSUFFICIENT_PERMISSIONS"); + } } // DESCRIBE commands must initialize a displayName if it isn't already present if (typeof(current.displayName) !== 'string' && typeof(data.displayName) !== 'string') { diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 16c439315..e5a40505e 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -491,6 +491,7 @@ define([ Feedback.send("ROSTER_CORRUPTED"); return; } + // Kicked from the team if (!state.members[me]) { lm.stop(); roster.stop(); @@ -499,6 +500,30 @@ define([ ctx.updateMetadata(); cb({error: 'EFORBIDDEN'}); waitFor.abort(); + return; + } + // Check access rights + // If we're not a viewer, make sure we have edit rights + var s = state.members[me]; + var teamEdPrivate = Util.find(teamData, ['keys', 'drive', 'edPrivate']); + if ((!teamData.hash || !teamEdPrivate) && ['ADMIN', 'MEMBER'].indexOf(s.role) !== -1) { + console.warn("Missing edit rights: demote to viewer"); + var data = {}; + data[ctx.store.proxy.curvePublic] = { + role: "VIEWER" + }; + roster.describe(data, function (err) { + Feedback.send("TEAM_RIGHTS_FIXED"); + // Make sure we've removed all the keys + delete teamData.hash; + delete teamData.keys.drive.edPrivate; + delete teamData.keys.chat.edit; + if (!err) { return; } + if (err === 'NO_CHANGE') { return; } + console.error(err); + }); + } else if ((!teamData.hash || !teamEdPrivate) && s.role === "OWNER") { + Feedback.send("TEAM_RIGHTS_OWNER"); } }).nThen(function () { onReady(ctx, id, lm, roster, keys, null, cb); @@ -1122,6 +1147,20 @@ define([ var onReady = ctx.onReadyHandlers[teamId]; var team = ctx.teams[teamId]; + if (teamData.channel !== data.channel || teamData.password !== data.password) { return void cb(false); } + + // Update our proxy + 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; + } + + // Team not ready yet: try again onReady if (!team && Array.isArray(onReady)) { onReady.push({ cb: function () { @@ -1131,14 +1170,11 @@ define([ return; } + // No team and not initialized at all... if (!team) { return void cb(false); } - if (teamData.channel !== data.channel || teamData.password !== data.password) { return void cb(false); } - + // Team is initialized and ready: update the loaded elements if (state) { - teamData.hash = data.hash; - teamData.keys.drive.edPrivate = data.keys.drive.edPrivate; - teamData.keys.chat.edit = data.keys.chat.edit; initRpc(ctx, team, teamData.keys.drive, function () { team.manager.addPin(team.pin, team.unpin); }); @@ -1149,9 +1185,6 @@ define([ var crypto = Crypto.createEncryptor(secret.keys); team.listmap.setReadOnly(false, crypto); } else { - delete teamData.hash; - delete teamData.keys.drive.edPrivate; - delete teamData.keys.chat.edit; delete team.secondaryKey; if (team.rpc && team.rpc.destroy) { team.rpc.destroy(); @@ -1668,7 +1701,74 @@ define([ updateMyRights(ctx, p[1]); }); + var checkKeyPair = function (edPrivate, edPublic) { + if (!edPrivate || !edPublic) { return true; } + try { + var secretKey = Nacl.util.decodeBase64(edPrivate); + var pair = Nacl.sign.keyPair.fromSecretKey(secretKey); + return Nacl.util.encodeBase64(pair.publicKey) === edPublic; + } catch (e) { + return false; + } + }; + + // Remove duplicate teams + var _teams = {}; + Object.keys(teams).forEach(function (id) { + try { + var t = teams[id]; + var _t = _teams[t.channel]; + + var edPrivate = Util.find(t, ['keys', 'drive', 'edPrivate']); + var edPublic = Util.find(t, ['keys', 'drive', 'edPublic']); + + // If the edPrivate is corrupted, remove it + if (!edPublic) { + Feedback.send("TEAM_CORRUPTED_EDPUBLIC"); + } else if (edPrivate && edPublic && !checkKeyPair(edPrivate, edPublic)) { + Feedback.send("TEAM_CORRUPTED_EDPRIVATE"); + delete teams[id].keys.drive.edPrivate; + edPrivate = undefined; + } + + // If the hash is corrupted, feedback + if (t.hash) { + var parsed = Hash.parseTypeHash('drive', t.hash); + if (parsed.version === 2 && t.hash.length !== 40) { + Feedback.send("TEAM_CORRUPTED_HASH"); + // FIXME ? + } + } + + // Not found yet? add to the list + if (!_t) { + _teams[t.channel] = id; + return; + } + // Duplicate found: update our team to add missing data + var best = teams[_t]; // This is a proxy! + var bestPrivate = Util.find(best, ['keys', 'drive', 'edPrivate']); + var bestChat = Util.find(best, ['keys', 'chat', 'edit']); + var chat = Util.find(t, ['keys', 'chat', 'edit']); + if (!best.hash && t.hash) { + best.hash = t.hash; + } + if (!bestPrivate && edPrivate) { + best.keys.drive.edPrivate = edPrivate; + } + if (!bestChat && chat) { + best.keys.chat.edit = chat; + } + + // Deprecate the duplicate + ctx.store.proxy.duplicateTeams = ctx.store.proxy.duplicateTeams || {}; + ctx.store.proxy.duplicateTeams[id] = teams[id]; + delete teams[id]; + } catch (e) { console.error(e); } + }); + + // Load teams Object.keys(teams).forEach(function (id) { ctx.onReadyHandlers[id] = []; if (!Util.find(teams, [id, 'keys', 'mailbox'])) { @@ -1683,16 +1783,6 @@ define([ team.getTeam = function (id) { return ctx.teams[id]; }; - var checkKeyPair = function (edPrivate, edPublic) { - if (!edPrivate || !edPublic) { return true; } - try { - var secretKey = Nacl.util.decodeBase64(edPrivate); - var pair = Nacl.sign.keyPair.fromSecretKey(secretKey); - return Nacl.util.encodeBase64(pair.publicKey) === edPublic; - } catch (e) { - return false; - } - }; team.getTeamsData = function (app) { var t = {}; var safe = false; diff --git a/www/common/sframe-common-history.js b/www/common/sframe-common-history.js index 9362c7899..c0c8ef678 100644 --- a/www/common/sframe-common-history.js +++ b/www/common/sframe-common-history.js @@ -254,7 +254,7 @@ define([ // goal of having snapshots if (config.getLastMetadata) { var metadataMgr = common.getMetadataMgr(); - var lastMd = config.getLastMetadata(); + var lastMd = config.getLastMetadata() || {}; var _snapshots = lastMd.snapshots; var _users = lastMd.users; var md = Util.clone(metadataMgr.getMetadata());