Skip to content

Commit

Permalink
Feature/58073 tech support user (#2466)
Browse files Browse the repository at this point in the history
* create user script

* update script with token values|

* refactor tests and remove duplicate & obsolete

* remove superfluous script

* retire tech support user, introduce sql support

* re-add tech support user

* guard against non existing target objects
  • Loading branch information
GuyHarwood authored Mar 23, 2023
1 parent 6f1af01 commit a01c9a5
Show file tree
Hide file tree
Showing 15 changed files with 97 additions and 68 deletions.
36 changes: 19 additions & 17 deletions admin/services/data-access/sql.pool.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,54 @@ const connectionBuilder = require('./sql.role-connection.builder')
const logger = require('../log.service').getLogger()
const POOLS = {}

const buildPoolName = (poolName, readonly) => {
return `${poolName}:read=${readonly}`
const buildInternalPoolName = (roleName, readonly) => {
return `${roleName}:read=${readonly}`
}

const service = {
/**
* @description create a new connection pool
* @param {string} poolName the unique name of the pool
* @param {import('../../lib/consts/roles')} roleName the user role the pool will serve
* @returns {import('mssql').ConnectionPool} the active connection pool
*/
createPool: function createPool (poolName, readonly = false) {
const internalPoolName = buildPoolName(poolName, readonly)
if (service.getPool(poolName, false, readonly)) {
logger.warn(`cannot create connectionPool with name ${internalPoolName}, as it already exists`)
createPool: function createPool (roleName, readonly = false) {
const existingPool = service.getPool(roleName, false, readonly)
if (existingPool !== undefined) {
logger.warn(`cannot create connectionPool for role:${roleName} readonly:${readonly}, as it already exists`)
return existingPool
} else {
const config = connectionBuilder.build(poolName, readonly)
const config = connectionBuilder.build(roleName, readonly)
const internalPoolName = buildInternalPoolName(roleName, readonly)
POOLS[internalPoolName] = new ConnectionPool(config)
return POOLS[internalPoolName]
}
return POOLS[internalPoolName]
},
/**
* @description closes the specified pool
* @param {string} poolName the name of the pool to close
* @param {import('../../lib/consts/roles')} roleName the user role the pool will serve
* @returns {Promise.<object>} a promise containing the closing pool
*/
closePool: function closePool (poolName, readonly = false) {
const pool = service.getPool(poolName, false, readonly)
closePool: function closePool (roleName, readonly = false) {
const pool = service.getPool(roleName, false, readonly)
if (pool) {
const internalPoolName = buildPoolName(poolName, readonly)
const internalPoolName = buildInternalPoolName(roleName, readonly)
delete POOLS[internalPoolName]
return pool.close()
}
},
/**
* @description get a pool by name. will auto create if specified.
* @param {string} poolName the name of the pool to fetch
* @param {import('../../lib/consts/roles')} roleName the user role the pool will serve
* @param {Boolean} createIfNotFound if the pool does not exist, it will be created
* @returns {import('mssql').ConnectionPool} the specified pool, if it exists
*/
getPool: function getPool (poolName, createIfNotFound = false, readonly = false) {
const internalPoolName = buildPoolName(poolName, readonly)
getPool: function getPool (roleName, createIfNotFound = false, readonly = false) {
const internalPoolName = buildInternalPoolName(roleName, readonly)
if ({}.hasOwnProperty.call(POOLS, internalPoolName)) {
return POOLS[internalPoolName]
} else {
if (createIfNotFound === true) {
const pool = service.createPool(poolName, readonly)
const pool = service.createPool(roleName, readonly)
return pool
}
}
Expand Down
6 changes: 3 additions & 3 deletions admin/services/data-access/sql.role-connection.builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

const roles = require('../../lib/consts/roles')
const sqlConfig = require('../../config/sql.config')
const config = require('../../config')
const R = require('ramda')
const deepFreeze = require('../../lib/deep-freeze')
const config = require('../../config')

const builder = {
/**
* @description builds a sql connection config object
* @description builds a sql connection config object based upon the users role
* @param {string} roleName - the role of the user to create a connection config for
* @param {boolean} readonly - set to true for a readonly connection, typically to a sql azure replica
* @returns {object} a config object that works with mssql
Expand All @@ -25,7 +25,7 @@ const builder = {
cfg.pool.max = config.Sql.TechSupport.Pool.Max
break
default:
throw new Error('role not supported')
throw new Error(`role '${roleName}' not yet supported in connection builder`)
}
cfg.options.readOnlyIntent = readonly
return deepFreeze(cfg)
Expand Down
5 changes: 3 additions & 2 deletions admin/tests-integration/sql.pool.service.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ describe('sql.pool.service:integration', () => {
sut.closePool(roles.teacher)
})
test('allows builder error to bubble up if role not supported', () => {
const unsupportedRoleName = 'sldkfj34t08rgey'
try {
sut.createPool('void role')
sut.createPool(unsupportedRoleName)
fail('error should have been thrown')
} catch (error) {
expect(error).toBeDefined()
expect(error.message).toEqual('role not supported')
expect(error.message).toEqual(`role '${unsupportedRoleName}' not yet supported in connection builder`)
}
})
})
Expand Down
26 changes: 8 additions & 18 deletions admin/tests-integration/sql.role-connection.builder.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict'
/* global describe test expect fail */
/* global describe test expect */

const path = require('path')
const fs = require('fs')
Expand All @@ -26,16 +26,6 @@ describe('sql.role-connection.builder:integration', () => {
expect(sut).toBeDefined()
})

test('should throw an error if role not supported', () => {
try {
sut.build('unknown role')
fail('error should have been thrown')
} catch (error) {
expect(error).toBeDefined()
expect(error.message).toBe('role not supported')
}
})

test('should return default config when role is teacher', () => {
const actual = sut.build(roles.teacher)
expect(actual).toBeDefined()
Expand All @@ -46,6 +36,13 @@ describe('sql.role-connection.builder:integration', () => {
expect(actual.pool.max).toEqual(sqlConfig.pool.max)
})

test('should be readonly if specified', () => {
const actual = sut.build(roles.teacher, true)
expect(actual).toBeDefined()
expect(typeof actual).toEqual('object')
expect(actual.options.readOnlyIntent).toBe(true)
})

test('should return specific config when role is techSupport', () => {
const actual = sut.build(roles.techSupport)
expect(actual).toBeDefined()
Expand All @@ -56,13 +53,6 @@ describe('sql.role-connection.builder:integration', () => {
expect(actual.pool.max).toEqual(config.Sql.TechSupport.Pool.Max)
})

test('should be readonly if specified', () => {
const actual = sut.build(roles.teacher, true)
expect(actual).toBeDefined()
expect(typeof actual).toEqual('object')
expect(actual.options.readOnlyIntent).toBe(true)
})

test('should default to non read-only', () => {
const actual = sut.build(roles.teacher)
expect(actual).toBeDefined()
Expand Down
4 changes: 4 additions & 0 deletions db/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ module.exports = {
Min: parseInt(process.env.TECH_SUPPORT_SQL_POOL_MIN_COUNT, 10) || 0,
Max: parseInt(process.env.TECH_SUPPORT_SQL_POOL_MIN_COUNT, 10) || 2
}
},
SqlSupport: {
Username: process.env.SQL_SUPPORT_USER || 'SqlSupportUser',
Password: process.env.SQL_SUPPORT_USER_PASSWORD
}
},
DatabaseRetry: {
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use strict'

const config = require('../../config')

module.exports.generateSql = function () {
if (config.Sql.SqlSupport.Password === undefined) return ''
return `
IF USER_ID('${config.Sql.SqlSupport.Username}') IS NULL RETURN
GRANT CONNECT TO [${config.Sql.SqlSupport.Username}] AS [dbo]
GRANT EXECUTE,SELECT ON SCHEMA::[mtc_admin] TO [${config.Sql.SqlSupport.Username}] AS [dbo]
GRANT EXECUTE,SELECT ON SCHEMA::[mtc_results] TO [${config.Sql.SqlSupport.Username}] AS [dbo]
`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict'

const config = require('../../config')

module.exports.generateSql = function () {
return `
IF USER_ID('${config.Sql.SqlSupport.Username}') IS NULL RETURN
REVOKE CONNECT TO [${config.Sql.SqlSupport.Username}] AS [dbo]
REVOKE EXECUTE,SELECT ON SCHEMA::[mtc_admin] TO [${config.Sql.SqlSupport.Username}] AS [dbo]
REVOKE EXECUTE,SELECT ON SCHEMA::[mtc_results] TO [${config.Sql.SqlSupport.Username}] AS [dbo]
`
}
16 changes: 16 additions & 0 deletions db/migrations/user/20230309163747.do.sql-support-user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict'

const config = require('../../config')

const createStatementSqlAzure = `IF USER_ID('${config.Sql.SqlSupport.Username}') IS NOT NULL RETURN; CREATE USER ${config.Sql.SqlSupport.Username} WITH PASSWORD ='${config.Sql.SqlSupport.Password}', DEFAULT_SCHEMA=[mtc_admin];`
const createStatementSqlServer = `IF SUSER_ID('${config.Sql.SqlSupport.Username}') IS NOT NULL RETURN; CREATE LOGIN ${config.Sql.SqlSupport.Username} WITH PASSWORD = '${config.Sql.SqlSupport.Password}'; USE ${config.Sql.Database}; IF USER_ID('${config.Sql.SqlSupport.Username}') IS NOT NULL RETURN; CREATE USER ${config.Sql.SqlSupport.Username} FOR LOGIN ${config.Sql.SqlSupport.Username} WITH DEFAULT_SCHEMA = [mtc_admin];`

module.exports.generateSql = function () {
// only create if password is configured
if (config.Sql.SqlSupport.Password === undefined) return ''
if (config.Sql.Azure.Scale) {
return createStatementSqlAzure
} else {
return createStatementSqlServer
}
}
19 changes: 19 additions & 0 deletions db/migrations/user/20230309163747.undo.sql-support-user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict'

const config = require('../../config')

const dropAzureUser = `DROP USER IF EXISTS ${config.Sql.SqlSupport.Username};`
const dropLocalUser = `
IF USER_ID('${config.Sql.SqlSupport.Username}') IS NULL RETURN;
DROP USER ${config.Sql.SqlSupport.Username};
IF SUSER_ID('${config.Sql.SqlSupport.Username}') IS NULL RETURN;
DROP LOGIN ${config.Sql.SqlSupport.Username};`

module.exports.generateSql = function () {
// always try to undo, as password may have been removed/added between migrations
if (config.Sql.Azure.Scale) {
return dropAzureUser
} else {
return dropLocalUser
}
}

0 comments on commit a01c9a5

Please sign in to comment.