mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -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 CollaboratorsEmailHandler = require('./CollaboratorsEmailHandler')
|
||||
const CollaboratorsHandler = require('./CollaboratorsHandler')
|
||||
const CollaboratorsInviteHelper = require('./CollaboratorsInviteHelper')
|
||||
const UserGetter = require('../User/UserGetter')
|
||||
const ProjectGetter = require('../Project/ProjectGetter')
|
||||
const Crypto = require('crypto')
|
||||
|
@ -83,9 +84,11 @@ const CollaboratorsInviteHandler = {
|
|||
)
|
||||
const buffer = await randomBytes(24)
|
||||
const token = buffer.toString('hex')
|
||||
const tokenHmac = CollaboratorsInviteHelper.hashInviteToken(token)
|
||||
let invite = new ProjectInvite({
|
||||
email,
|
||||
token,
|
||||
tokenHmac,
|
||||
sendingUserId: sendingUser._id,
|
||||
projectId,
|
||||
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,
|
||||
token: String,
|
||||
tokenHmac: String,
|
||||
sendingUserId: ObjectId,
|
||||
projectId: ObjectId,
|
||||
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 () {
|
||||
this.projectId = 'project-id-123'
|
||||
this.token = 'some-opaque-token'
|
||||
this.tokenHmac = 'some-hmac-token'
|
||||
this.targetEmail = 'user@example.com'
|
||||
this.privileges = 'readAndWrite'
|
||||
this.currentUser = {
|
||||
|
@ -22,6 +23,7 @@ describe('CollaboratorsInviteController', function () {
|
|||
this.invite = {
|
||||
_id: new ObjectId(),
|
||||
token: this.token,
|
||||
tokenHmac: this.tokenHmac,
|
||||
sendingUserId: this.currentUser._id,
|
||||
projectId: this.projectId,
|
||||
email: this.targetEmail,
|
||||
|
|
|
@ -41,6 +41,9 @@ describe('CollaboratorsInviteHandler', function () {
|
|||
this.UserGetter = { promises: { getUser: sinon.stub() } }
|
||||
this.ProjectGetter = { promises: {} }
|
||||
this.NotificationsBuilder = { promises: {} }
|
||||
this.CollaboratorsInviteHelper = {
|
||||
hashInviteToken: sinon.stub().returns('abcd'),
|
||||
}
|
||||
|
||||
this.CollaboratorsInviteHandler = SandboxedModule.require(MODULE_PATH, {
|
||||
requires: {
|
||||
|
@ -51,6 +54,7 @@ describe('CollaboratorsInviteHandler', function () {
|
|||
'../User/UserGetter': this.UserGetter,
|
||||
'../Project/ProjectGetter': this.ProjectGetter,
|
||||
'../Notifications/NotificationsBuilder': this.NotificationsBuilder,
|
||||
'./CollaboratorsInviteHelper': this.CollaboratorsInviteHelper,
|
||||
crypto: this.Crypto,
|
||||
},
|
||||
})
|
||||
|
@ -69,11 +73,13 @@ describe('CollaboratorsInviteHandler', function () {
|
|||
}
|
||||
this.inviteId = new ObjectId()
|
||||
this.token = 'hnhteaosuhtaeosuahs'
|
||||
this.tokenHmac = 'jkhajkefhaekjfhkfg'
|
||||
this.privileges = 'readAndWrite'
|
||||
this.fakeInvite = {
|
||||
_id: this.inviteId,
|
||||
email: this.email,
|
||||
token: this.token,
|
||||
tokenHmac: this.tokenHmac,
|
||||
sendingUserId: this.sendingUserId,
|
||||
projectId: this.projectId,
|
||||
privileges: this.privileges,
|
||||
|
@ -186,6 +192,7 @@ describe('CollaboratorsInviteHandler', function () {
|
|||
'_id',
|
||||
'email',
|
||||
'token',
|
||||
'tokenHmac',
|
||||
'sendingUserId',
|
||||
'projectId',
|
||||
'privileges',
|
||||
|
@ -197,6 +204,11 @@ describe('CollaboratorsInviteHandler', function () {
|
|||
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 () {
|
||||
await this.call()
|
||||
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