Merge pull request #3727 from overleaf/ab-ta-unique-referal-id

Generate User referal_id using longer and more complex token to avoid duplicates

GitOrigin-RevId: 302515b0250fec875dcb7b3a505c1c7be4189e2b
This commit is contained in:
Miguel Serrano 2021-03-31 11:28:19 +02:00 committed by Copybot
parent 7a36373a0f
commit 65d9186e0b
4 changed files with 35 additions and 39 deletions

View file

@ -6,7 +6,7 @@ const logger = require('logger-sharelatex')
const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender') const TpdsUpdateSender = require('../ThirdPartyDataStore/TpdsUpdateSender')
const PublicAccessLevels = require('../Authorization/PublicAccessLevels') const PublicAccessLevels = require('../Authorization/PublicAccessLevels')
const Errors = require('../Errors/Errors') const Errors = require('../Errors/Errors')
const ProjectTokenGenerator = require('./ProjectTokenGenerator') const TokenGenerator = require('../TokenGenerator/TokenGenerator')
const ProjectHelper = require('./ProjectHelper') const ProjectHelper = require('./ProjectHelper')
const settings = require('settings-sharelatex') const settings = require('settings-sharelatex')
const { callbackify } = require('util') const { callbackify } = require('util')
@ -236,11 +236,11 @@ async function _generateTokens(project, callback) {
} }
const { tokens } = project const { tokens } = project
if (tokens.readAndWrite == null) { if (tokens.readAndWrite == null) {
const { token, numericPrefix } = ProjectTokenGenerator.readAndWriteToken() const { token, numericPrefix } = TokenGenerator.readAndWriteToken()
tokens.readAndWrite = token tokens.readAndWrite = token
tokens.readAndWritePrefix = numericPrefix tokens.readAndWritePrefix = numericPrefix
} }
if (tokens.readOnly == null) { if (tokens.readOnly == null) {
tokens.readOnly = await ProjectTokenGenerator.promises.generateUniqueReadOnlyToken() tokens.readOnly = await TokenGenerator.promises.generateUniqueReadOnlyToken()
} }
} }

View file

@ -16,19 +16,21 @@ const Features = require('../../infrastructure/Features')
const Async = require('async') const Async = require('async')
const { promisify } = require('util') const { promisify } = require('util')
// (From Overleaf `random_token.rb`)
// Letters (not numbers! see generate_token) used in tokens. They're all
// consonants, to avoid embarassing words (I can't think of any that use only
// a y), and lower case "l" is omitted, because in many fonts it is
// indistinguishable from an upper case "I" (and sometimes even the number 1).
const TOKEN_LOWERCASE_ALPHA = 'bcdfghjkmnpqrstvwxyz'
const TOKEN_NUMERICS = '123456789'
const TOKEN_ALPHANUMERICS =
TOKEN_LOWERCASE_ALPHA + TOKEN_LOWERCASE_ALPHA.toUpperCase() + TOKEN_NUMERICS
// This module mirrors the token generation in Overleaf (`random_token.rb`), // This module mirrors the token generation in Overleaf (`random_token.rb`),
// for the purposes of implementing token-based project access, like the // for the purposes of implementing token-based project access, like the
// 'unlisted-projects' feature in Overleaf // 'unlisted-projects' feature in Overleaf
const ProjectTokenGenerator = { const TokenGenerator = {
// (From Overleaf `random_token.rb`)
// Letters (not numbers! see generate_token) used in tokens. They're all
// consonants, to avoid embarassing words (I can't think of any that use only
// a y), and lower case "l" is omitted, because in many fonts it is
// indistinguishable from an upper case "I" (and sometimes even the number 1).
TOKEN_ALPHA: 'bcdfghjkmnpqrstvwxyz',
TOKEN_NUMERICS: '123456789',
_randomString(length, alphabet) { _randomString(length, alphabet) {
const result = crypto const result = crypto
.randomBytes(length) .randomBytes(length)
@ -38,30 +40,25 @@ const ProjectTokenGenerator = {
return result return result
}, },
// Generate a 12-char token with only characters from TOKEN_ALPHA, // Generate a 12-char token with only characters from TOKEN_LOWERCASE_ALPHA,
// suitable for use as a read-only token for a project // suitable for use as a read-only token for a project
readOnlyToken() { readOnlyToken() {
return ProjectTokenGenerator._randomString( return TokenGenerator._randomString(12, TOKEN_LOWERCASE_ALPHA)
12,
ProjectTokenGenerator.TOKEN_ALPHA
)
}, },
// Generate a longer token, with a numeric prefix, // Generate a longer token, with a numeric prefix,
// suitable for use as a read-and-write token for a project // suitable for use as a read-and-write token for a project
readAndWriteToken() { readAndWriteToken() {
const numerics = ProjectTokenGenerator._randomString( const numerics = TokenGenerator._randomString(10, TOKEN_NUMERICS)
10, const token = TokenGenerator._randomString(12, TOKEN_LOWERCASE_ALPHA)
ProjectTokenGenerator.TOKEN_NUMERICS
)
const token = ProjectTokenGenerator._randomString(
12,
ProjectTokenGenerator.TOKEN_ALPHA
)
const fullToken = `${numerics}${token}` const fullToken = `${numerics}${token}`
return { token: fullToken, numericPrefix: numerics } return { token: fullToken, numericPrefix: numerics }
}, },
generateReferralId() {
return TokenGenerator._randomString(16, TOKEN_ALPHANUMERICS)
},
generateUniqueReadOnlyToken(callback) { generateUniqueReadOnlyToken(callback) {
if (callback == null) { if (callback == null) {
callback = function(err, token) {} callback = function(err, token) {}
@ -69,7 +66,7 @@ const ProjectTokenGenerator = {
return Async.retry( return Async.retry(
10, 10,
function(cb) { function(cb) {
const token = ProjectTokenGenerator.readOnlyToken() const token = TokenGenerator.readOnlyToken()
if (!Features.hasFeature('overleaf-integration')) { if (!Features.hasFeature('overleaf-integration')) {
return cb(null, token) return cb(null, token)
@ -104,9 +101,9 @@ const ProjectTokenGenerator = {
} }
} }
ProjectTokenGenerator.promises = { TokenGenerator.promises = {
generateUniqueReadOnlyToken: promisify( generateUniqueReadOnlyToken: promisify(
ProjectTokenGenerator.generateUniqueReadOnlyToken TokenGenerator.generateUniqueReadOnlyToken
) )
} }
module.exports = ProjectTokenGenerator module.exports = TokenGenerator

View file

@ -1,6 +1,6 @@
const Settings = require('settings-sharelatex') const Settings = require('settings-sharelatex')
const mongoose = require('../infrastructure/Mongoose') const mongoose = require('../infrastructure/Mongoose')
const uuid = require('uuid') const TokenGenerator = require('../Features/TokenGenerator/TokenGenerator')
const { Schema } = mongoose const { Schema } = mongoose
const { ObjectId } = Schema const { ObjectId } = Schema
@ -136,7 +136,7 @@ const UserSchema = new Schema({
referal_id: { referal_id: {
type: String, type: String,
default() { default() {
return uuid.v4().split('-')[0] return TokenGenerator.generateReferralId()
} }
}, },
refered_users: [{ type: ObjectId, ref: 'User' }], refered_users: [{ type: ObjectId, ref: 'User' }],

View file

@ -56,7 +56,7 @@ describe('ProjectDetailsHandler', function() {
moveEntity: sinon.stub().resolves() moveEntity: sinon.stub().resolves()
} }
} }
this.ProjectTokenGenerator = { this.TokenGenerator = {
readAndWriteToken: sinon.stub(), readAndWriteToken: sinon.stub(),
promises: { promises: {
generateUniqueReadOnlyToken: sinon.stub() generateUniqueReadOnlyToken: sinon.stub()
@ -82,7 +82,7 @@ describe('ProjectDetailsHandler', function() {
warn() {}, warn() {},
err() {} err() {}
}, },
'./ProjectTokenGenerator': this.ProjectTokenGenerator, '../TokenGenerator/TokenGenerator': this.TokenGenerator,
'settings-sharelatex': this.settings 'settings-sharelatex': this.settings
} }
}) })
@ -484,10 +484,10 @@ describe('ProjectDetailsHandler', function() {
this.readOnlyToken = 'abc' this.readOnlyToken = 'abc'
this.readAndWriteToken = '42def' this.readAndWriteToken = '42def'
this.readAndWriteTokenPrefix = '42' this.readAndWriteTokenPrefix = '42'
this.ProjectTokenGenerator.promises.generateUniqueReadOnlyToken.resolves( this.TokenGenerator.promises.generateUniqueReadOnlyToken.resolves(
this.readOnlyToken this.readOnlyToken
) )
this.ProjectTokenGenerator.readAndWriteToken.returns({ this.TokenGenerator.readAndWriteToken.returns({
token: this.readAndWriteToken, token: this.readAndWriteToken,
numericPrefix: this.readAndWriteTokenPrefix numericPrefix: this.readAndWriteTokenPrefix
}) })
@ -506,10 +506,9 @@ describe('ProjectDetailsHandler', function() {
it('should update the project with new tokens', async function() { it('should update the project with new tokens', async function() {
await this.handler.promises.ensureTokensArePresent(this.project._id) await this.handler.promises.ensureTokensArePresent(this.project._id)
expect(this.ProjectTokenGenerator.promises.generateUniqueReadOnlyToken) expect(this.TokenGenerator.promises.generateUniqueReadOnlyToken).to.have
.to.have.been.calledOnce .been.calledOnce
expect(this.ProjectTokenGenerator.readAndWriteToken).to.have.been expect(this.TokenGenerator.readAndWriteToken).to.have.been.calledOnce
.calledOnce
expect(this.ProjectModel.updateOne).to.have.been.calledOnce expect(this.ProjectModel.updateOne).to.have.been.calledOnce
expect(this.ProjectModel.updateOne).to.have.been.calledWith( expect(this.ProjectModel.updateOne).to.have.been.calledWith(
{ _id: this.project._id }, { _id: this.project._id },