mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 09:43:38 -05:00
Merge pull request #18653 from overleaf/ii-invite-token-create-hmac
[web] Add HMAC tokens for project invitations GitOrigin-RevId: 02fa01e24790c9a87f57ff9346f5346658d4dd46
This commit is contained in:
parent
bb2a3b091d
commit
b34be6bea4
8 changed files with 154 additions and 0 deletions
|
@ -3,6 +3,7 @@ const { ProjectInvite } = require('../../models/ProjectInvite')
|
||||||
const logger = require('@overleaf/logger')
|
const logger = require('@overleaf/logger')
|
||||||
const CollaboratorsEmailHandler = require('./CollaboratorsEmailHandler')
|
const CollaboratorsEmailHandler = require('./CollaboratorsEmailHandler')
|
||||||
const CollaboratorsHandler = require('./CollaboratorsHandler')
|
const CollaboratorsHandler = require('./CollaboratorsHandler')
|
||||||
|
const CollaboratorsInviteHelper = require('./CollaboratorsInviteHelper')
|
||||||
const UserGetter = require('../User/UserGetter')
|
const UserGetter = require('../User/UserGetter')
|
||||||
const ProjectGetter = require('../Project/ProjectGetter')
|
const ProjectGetter = require('../Project/ProjectGetter')
|
||||||
const Crypto = require('crypto')
|
const Crypto = require('crypto')
|
||||||
|
@ -83,9 +84,11 @@ const CollaboratorsInviteHandler = {
|
||||||
)
|
)
|
||||||
const buffer = await randomBytes(24)
|
const buffer = await randomBytes(24)
|
||||||
const token = buffer.toString('hex')
|
const token = buffer.toString('hex')
|
||||||
|
const tokenHmac = CollaboratorsInviteHelper.hashInviteToken(token)
|
||||||
let invite = new ProjectInvite({
|
let invite = new ProjectInvite({
|
||||||
email,
|
email,
|
||||||
token,
|
token,
|
||||||
|
tokenHmac,
|
||||||
sendingUserId: sendingUser._id,
|
sendingUserId: sendingUser._id,
|
||||||
projectId,
|
projectId,
|
||||||
privileges,
|
privileges,
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
const Crypto = require('crypto')
|
||||||
|
|
||||||
|
function hashInviteToken(token) {
|
||||||
|
return Crypto.createHmac('sha256', 'overleaf-token-invite')
|
||||||
|
.update(token)
|
||||||
|
.digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
hashInviteToken,
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ const ProjectInviteSchema = new Schema(
|
||||||
{
|
{
|
||||||
email: String,
|
email: String,
|
||||||
token: String,
|
token: String,
|
||||||
|
tokenHmac: String,
|
||||||
sendingUserId: ObjectId,
|
sendingUserId: ObjectId,
|
||||||
projectId: ObjectId,
|
projectId: ObjectId,
|
||||||
privileges: String,
|
privileges: String,
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
|
||||||
|
const Helpers = require('./lib/helpers')
|
||||||
|
const runScript = require('../scripts/backfill_project_invites_token_hmac')
|
||||||
|
|
||||||
|
exports.tags = ['server-ce', 'server-pro', 'saas']
|
||||||
|
|
||||||
|
const index = {
|
||||||
|
key: {
|
||||||
|
tokenHmac: 1,
|
||||||
|
},
|
||||||
|
name: 'tokenHmac_1',
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.migrate = async client => {
|
||||||
|
const { db } = client
|
||||||
|
await Helpers.addIndexesToCollection(db.projectInvites, [index])
|
||||||
|
await runScript(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.rollback = async client => {
|
||||||
|
const { db } = client
|
||||||
|
await Helpers.dropIndexesFromCollection(db.projectInvites, [index])
|
||||||
|
}
|
80
services/web/scripts/backfill_project_invites_token_hmac.js
Normal file
80
services/web/scripts/backfill_project_invites_token_hmac.js
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
const { db, waitForDb } = require('../app/src/infrastructure/mongodb')
|
||||||
|
const { batchedUpdate } = require('./helpers/batchedUpdate')
|
||||||
|
const minimist = require('minimist')
|
||||||
|
const CollaboratorsInviteHelper = require('../app/src/Features/Collaborators/CollaboratorsInviteHelper')
|
||||||
|
|
||||||
|
const argv = minimist(process.argv.slice(2), {
|
||||||
|
boolean: ['dry-run', 'help'],
|
||||||
|
default: {
|
||||||
|
'dry-run': true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const DRY_RUN = argv['dry-run']
|
||||||
|
|
||||||
|
async function addTokenHmacField(DRY_RUN) {
|
||||||
|
const query = { tokenHmac: { $exists: false } }
|
||||||
|
|
||||||
|
await batchedUpdate(
|
||||||
|
'projectInvites',
|
||||||
|
query,
|
||||||
|
async invites => {
|
||||||
|
for (const invite of invites) {
|
||||||
|
console.log(
|
||||||
|
`=> Missing "tokenHmac" token in invitation: ${invite._id.toString()}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (DRY_RUN) {
|
||||||
|
console.log(
|
||||||
|
`=> DRY RUN - would add "tokenHmac" token to invitation ${invite._id.toString()}`
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenHmac = CollaboratorsInviteHelper.hashInviteToken(
|
||||||
|
invite.token
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.projectInvites.updateOne(
|
||||||
|
{ _id: invite._id },
|
||||||
|
{ $set: { tokenHmac } }
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`=> Added "tokenHmac" token to invitation ${invite._id.toString()}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ token: 1 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(DRY_RUN) {
|
||||||
|
await waitForDb()
|
||||||
|
await addTokenHmacField(DRY_RUN)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = main
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
if (argv.help || argv._.length > 1) {
|
||||||
|
console.error(`Usage: node scripts/backfill_project_invites_token_hmac.js
|
||||||
|
Adds a "tokenHmac" field (which is a hashed version of the token) to each project invite record.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--dry-run finds invitations without HMAC token but does not do any updates
|
||||||
|
`)
|
||||||
|
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
main(DRY_RUN)
|
||||||
|
.then(() => {
|
||||||
|
console.error('Done')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ describe('CollaboratorsInviteController', function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
this.projectId = 'project-id-123'
|
this.projectId = 'project-id-123'
|
||||||
this.token = 'some-opaque-token'
|
this.token = 'some-opaque-token'
|
||||||
|
this.tokenHmac = 'some-hmac-token'
|
||||||
this.targetEmail = 'user@example.com'
|
this.targetEmail = 'user@example.com'
|
||||||
this.privileges = 'readAndWrite'
|
this.privileges = 'readAndWrite'
|
||||||
this.currentUser = {
|
this.currentUser = {
|
||||||
|
@ -22,6 +23,7 @@ describe('CollaboratorsInviteController', function () {
|
||||||
this.invite = {
|
this.invite = {
|
||||||
_id: new ObjectId(),
|
_id: new ObjectId(),
|
||||||
token: this.token,
|
token: this.token,
|
||||||
|
tokenHmac: this.tokenHmac,
|
||||||
sendingUserId: this.currentUser._id,
|
sendingUserId: this.currentUser._id,
|
||||||
projectId: this.projectId,
|
projectId: this.projectId,
|
||||||
email: this.targetEmail,
|
email: this.targetEmail,
|
||||||
|
|
|
@ -41,6 +41,9 @@ describe('CollaboratorsInviteHandler', function () {
|
||||||
this.UserGetter = { promises: { getUser: sinon.stub() } }
|
this.UserGetter = { promises: { getUser: sinon.stub() } }
|
||||||
this.ProjectGetter = { promises: {} }
|
this.ProjectGetter = { promises: {} }
|
||||||
this.NotificationsBuilder = { promises: {} }
|
this.NotificationsBuilder = { promises: {} }
|
||||||
|
this.CollaboratorsInviteHelper = {
|
||||||
|
hashInviteToken: sinon.stub().returns('abcd'),
|
||||||
|
}
|
||||||
|
|
||||||
this.CollaboratorsInviteHandler = SandboxedModule.require(MODULE_PATH, {
|
this.CollaboratorsInviteHandler = SandboxedModule.require(MODULE_PATH, {
|
||||||
requires: {
|
requires: {
|
||||||
|
@ -51,6 +54,7 @@ describe('CollaboratorsInviteHandler', function () {
|
||||||
'../User/UserGetter': this.UserGetter,
|
'../User/UserGetter': this.UserGetter,
|
||||||
'../Project/ProjectGetter': this.ProjectGetter,
|
'../Project/ProjectGetter': this.ProjectGetter,
|
||||||
'../Notifications/NotificationsBuilder': this.NotificationsBuilder,
|
'../Notifications/NotificationsBuilder': this.NotificationsBuilder,
|
||||||
|
'./CollaboratorsInviteHelper': this.CollaboratorsInviteHelper,
|
||||||
crypto: this.Crypto,
|
crypto: this.Crypto,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -69,11 +73,13 @@ describe('CollaboratorsInviteHandler', function () {
|
||||||
}
|
}
|
||||||
this.inviteId = new ObjectId()
|
this.inviteId = new ObjectId()
|
||||||
this.token = 'hnhteaosuhtaeosuahs'
|
this.token = 'hnhteaosuhtaeosuahs'
|
||||||
|
this.tokenHmac = 'jkhajkefhaekjfhkfg'
|
||||||
this.privileges = 'readAndWrite'
|
this.privileges = 'readAndWrite'
|
||||||
this.fakeInvite = {
|
this.fakeInvite = {
|
||||||
_id: this.inviteId,
|
_id: this.inviteId,
|
||||||
email: this.email,
|
email: this.email,
|
||||||
token: this.token,
|
token: this.token,
|
||||||
|
tokenHmac: this.tokenHmac,
|
||||||
sendingUserId: this.sendingUserId,
|
sendingUserId: this.sendingUserId,
|
||||||
projectId: this.projectId,
|
projectId: this.projectId,
|
||||||
privileges: this.privileges,
|
privileges: this.privileges,
|
||||||
|
@ -186,6 +192,7 @@ describe('CollaboratorsInviteHandler', function () {
|
||||||
'_id',
|
'_id',
|
||||||
'email',
|
'email',
|
||||||
'token',
|
'token',
|
||||||
|
'tokenHmac',
|
||||||
'sendingUserId',
|
'sendingUserId',
|
||||||
'projectId',
|
'projectId',
|
||||||
'privileges',
|
'privileges',
|
||||||
|
@ -197,6 +204,11 @@ describe('CollaboratorsInviteHandler', function () {
|
||||||
this.Crypto.randomBytes.callCount.should.equal(1)
|
this.Crypto.randomBytes.callCount.should.equal(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should have generated a HMAC token', async function () {
|
||||||
|
await this.call()
|
||||||
|
this.CollaboratorsInviteHelper.hashInviteToken.callCount.should.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('should have called ProjectInvite.save', async function () {
|
it('should have called ProjectInvite.save', async function () {
|
||||||
await this.call()
|
await this.call()
|
||||||
this.ProjectInvite.prototype.save.callCount.should.equal(1)
|
this.ProjectInvite.prototype.save.callCount.should.equal(1)
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
const sinon = require('sinon')
|
||||||
|
const { expect } = require('chai')
|
||||||
|
const path = require('path')
|
||||||
|
const CollaboratorsInviteHelper = require(
|
||||||
|
path.join(
|
||||||
|
__dirname,
|
||||||
|
'/../../../../app/src/Features/Collaborators/CollaboratorsInviteHelper'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
const Crypto = require('crypto')
|
||||||
|
|
||||||
|
describe('CollaboratorsInviteHelper', function () {
|
||||||
|
it('should generate a HMAC token', function () {
|
||||||
|
const CryptoCreateHmac = sinon.spy(Crypto, 'createHmac')
|
||||||
|
const tokenHmac = CollaboratorsInviteHelper.hashInviteToken('abc')
|
||||||
|
CryptoCreateHmac.callCount.should.equal(1)
|
||||||
|
expect(tokenHmac).to.eq(
|
||||||
|
'3f76e274d83ffba85149f6850c095ce8481454d7446ca4e25beee01e08beb383'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in a new issue