Skip to content
This repository was archived by the owner on Apr 3, 2019. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ module.exports = function (fs, path, url, convict) {
format: 'duration',
env: 'SIGNIN_CODES_MAX_AGE',
},
requiredSQLModes: {
doc: 'Comma-separated list of SQL mode flags to enforce on each connection',
default: '',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, let's just not forget to actually set this at some point :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you leave it unset, it should use the hard-coded defaults from const REQUIRED_SQL_MODES; perhaps the config doc here could have made that clearer.

format: 'String',
env: 'REQUIRED_SQL_MODES',
},
master: {
user: {
doc: 'The user to connect to for MySql',
Expand Down
45 changes: 29 additions & 16 deletions lib/db/mysql.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,16 @@ module.exports = function (log, error) {
options.master.charset = options.charset
options.slave.charset = options.charset

var mode = REQUIRED_SQL_MODES.join(',')
if (options.sql_mode && options.sql_mode !== mode) {
log.error('createPoolCluster.invalidSqlMode', { sql_mode: options.sql_mode })
throw new Error(`You cannot use any sql mode other than ${mode}`)
} else {
options.sql_mode = REQUIRED_SQL_MODES.join(',')
this.requiredModes = REQUIRED_SQL_MODES
if (options.requiredSQLModes) {
this.requiredModes = options.requiredSQLModes.split(',')
this.requiredModes.forEach(mode => {
if (! /^[A-Z0-9_]+$/.test(mode)) {
throw new Error('Invalid SQL mode: ' + mode)
}
})
}

options.master.sql_mode = options.sql_mode
options.slave.sql_mode = options.sql_mode
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MySQL module docs don't mention this sql_mode option, so I wasn't sure if it was doing anything, so I deleted it.



// Use separate pools for master and slave connections.
this.poolCluster.add('MASTER', options.master)
this.poolCluster.add('SLAVE', options.slave)
Expand Down Expand Up @@ -1155,22 +1153,37 @@ module.exports = function (log, error) {
return resolve(connection)
}

var mode = REQUIRED_SQL_MODES.join(',')
connection.query(`SET SESSION sql_mode = '${mode}';`, (err) => {
// Enforce sane defaults on every new connection.
// These *should* be set by the database by default, but it's nice
// to have an additional layer of protection here.
connection.query('SELECT @@sql_mode AS mode;', (err, rows) => {
if (err) {
return reject(err)
}

connection.query('SET NAMES utf8mb4 COLLATE utf8mb4_bin;', (err) => {
const currentModes = rows[0]['mode'].split(',')
this.requiredModes.forEach(requiredMode => {
if (currentModes.indexOf(requiredMode) === -1) {
currentModes.push(requiredMode)
}
})

const newMode = currentModes.join(',')
connection.query(`SET SESSION sql_mode = '${newMode}';`, (err) => {
if (err) {
return reject(err)
}

connection._fxa_initialized = true
resolve(connection)
connection.query('SET NAMES utf8mb4 COLLATE utf8mb4_bin;', (err) => {
if (err) {
return reject(err)
}

connection._fxa_initialized = true
resolve(connection)
})
})
})

})
})
}
Expand Down
53 changes: 42 additions & 11 deletions test/local/mysql_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('MySQL', () => {
})

it(
'forces REQUIRED_CHARSET for connections',
'validates REQUIRED_CHARSET for connections',
() => {
const configCharset = Object.assign({}, config)
configCharset.charset = 'wat'
Expand All @@ -42,20 +42,51 @@ describe('MySQL', () => {
)

it(
'forces REQUIRED_SQL_MODES for connections',
'accepts REQUIRED_SQL_MODES from config',
() => {
const configSqlMode = Object.assign({}, config)
const REQUIRED_SQL_MODES = [
'STRICT_ALL_TABLES',
'NO_ENGINE_SUBSTITUTION',
]
var mode = REQUIRED_SQL_MODES.join(',')
configSqlMode.sql_mode = 'xyz'
return DB.connect(configSqlMode)
const configModes = Object.assign({}, config)
configModes.requiredSQLModes = 'STRICT_TRANS_TABLES,NO_ZERO_DATE'

return DB.connect(configModes)
.then(
db => {
assert.deepEqual(db.requiredModes, [
'STRICT_TRANS_TABLES',
'NO_ZERO_DATE'
])
},
assert.fail
)
}
)

it(
'rejects unrecognized REQUIRED_SQL_MODES values from config',
() => {
const configModes = Object.assign({}, config)
configModes.requiredSQLModes = 'UNRECOGNIZED_SQL_MODE_NONSENSE'

return DB.connect(configModes)
.then(
assert.fail,
err => {
assert.equal(err.message, 'ER_WRONG_VALUE_FOR_VAR')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that MySQL rejects this value, is a useful test that we are actually attempting to call SET SESSION sql_mode = blah with the provided value.

}
)
}
)

it(
'rejects badly-formed REQUIRED_SQL_MODES from config, for safety',
() => {
const configModes = Object.assign({}, config)
configModes.requiredSQLModes = 'TEST,MODE,\'; DROP TABLE users;'

return DB.connect(configModes)
.then(
assert.fail,
err => {
assert.equal(err.message, `You cannot use any sql mode other than ${mode}`)
assert.ok(err.message.indexOf('Invalid SQL mode') === 0)
}
)
}
Expand Down