Skip to content

Commit 9ec8b43

Browse files
committed
feat: CRUD on /roles
1 parent 17e8ef2 commit 9ec8b43

File tree

3 files changed

+182
-9
lines changed

3 files changed

+182
-9
lines changed

src/api/roles.ts

Lines changed: 136 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Router } from 'express'
22

3-
import sql = require('../lib/sql')
4-
const { grants, roles } = sql
3+
import SQL from 'sql-template-strings'
4+
import sqlTemplates = require('../lib/sql')
5+
const { grants, roles } = sqlTemplates
56
import { coalesceRowsToArray } from '../lib/helpers'
67
import { RunQuery } from '../lib/connectionPool'
78
import { DEFAULT_ROLES, DEFAULT_SYSTEM_SCHEMAS } from '../lib/constants'
@@ -37,13 +38,53 @@ router.get('/', async (req, res) => {
3738

3839
router.post('/', async (req, res) => {
3940
try {
40-
const sql = createRoleSqlize(req.body)
41-
const { data } = await RunQuery(req.headers.pg, sql)
42-
return res.status(200).json(data)
41+
const query = createRoleSqlize(req.body)
42+
await RunQuery(req.headers.pg, query)
43+
44+
const getRoleQuery = singleRoleByNameSqlize(roles, req.body.name)
45+
const role = (await RunQuery(req.headers.pg, getRoleQuery)).data[0]
46+
47+
return res.status(200).json(role)
48+
} catch (error) {
49+
console.log('throwing error', error)
50+
res.status(500).json({ error: 'Database error', status: 500 })
51+
}
52+
})
53+
54+
router.patch('/:id', async (req, res) => {
55+
try {
56+
const id = req.params.id
57+
const getRoleQuery = singleRoleSqlize(roles, id)
58+
const role = (await RunQuery(req.headers.pg, getRoleQuery)).data[0]
59+
const { name: oldName } = role
60+
61+
const alterRoleArgs = req.body
62+
alterRoleArgs.oldName = oldName
63+
const query = alterRoleSqlize(alterRoleArgs)
64+
await RunQuery(req.headers.pg, query)
65+
66+
const updated = (await RunQuery(req.headers.pg, getRoleQuery)).data[0]
67+
return res.status(200).json(updated)
68+
} catch (error) {
69+
console.log('throwing error', error)
70+
res.status(500).json({ error: 'Database error', status: 500 })
71+
}
72+
})
73+
74+
router.delete('/:id', async (req, res) => {
75+
try {
76+
const id = req.params.id
77+
const getRoleQuery = singleRoleSqlize(roles, id)
78+
const role = (await RunQuery(req.headers.pg, getRoleQuery)).data[0]
79+
const { name } = role
80+
81+
const query = dropRoleSqlize(name)
82+
await RunQuery(req.headers.pg, query)
83+
84+
return res.status(200).json(role)
4385
} catch (error) {
44-
// For this one, we always want to give back the error to the customer
45-
console.log('Soft error!', error)
46-
res.status(200).json([{ error: error.toString() }])
86+
console.log('throwing error', error)
87+
res.status(500).json({ error: 'Database error', status: 500 })
4788
}
4889
})
4990

@@ -103,7 +144,7 @@ const createRoleSqlize = ({
103144
const adminsSql = admins === undefined ? '' : `ADMIN ${admins.join(',')}`
104145

105146
return `
106-
CREATE ROLE ${name}
147+
CREATE ROLE "${name}"
107148
WITH
108149
${isSuperuserSql}
109150
${canCreateDbSql}
@@ -119,6 +160,92 @@ WITH
119160
${membersSql}
120161
${adminsSql}`
121162
}
163+
const singleRoleSqlize = (roles: string, id: string) => {
164+
return SQL``.append(roles).append(SQL` WHERE oid = ${id}`)
165+
}
166+
const singleRoleByNameSqlize = (roles: string, name: string) => {
167+
return SQL``.append(roles).append(SQL` WHERE rolname = ${name}`)
168+
}
169+
const alterRoleSqlize = ({
170+
oldName,
171+
name,
172+
isSuperuser,
173+
canCreateDb,
174+
canCreateRole,
175+
inheritRole,
176+
canLogin,
177+
isReplicationRole,
178+
canBypassRls,
179+
connectionLimit,
180+
password,
181+
validUntil,
182+
}: {
183+
oldName: string
184+
name?: string
185+
isSuperuser?: boolean
186+
canCreateDb?: boolean
187+
canCreateRole?: boolean
188+
inheritRole?: boolean
189+
canLogin?: boolean
190+
isReplicationRole?: boolean
191+
canBypassRls?: boolean
192+
connectionLimit?: number
193+
password?: string
194+
validUntil?: string
195+
}) => {
196+
const nameSql = name === undefined ? '' : `ALTER ROLE "${oldName}" RENAME TO "${name}";`
197+
let isSuperuserSql = ''
198+
if (isSuperuser !== undefined) {
199+
isSuperuserSql = isSuperuser ? 'SUPERUSER' : 'NOSUPERUSER'
200+
}
201+
let canCreateDbSql = ''
202+
if (canCreateDb !== undefined) {
203+
canCreateDbSql = canCreateDb ? 'CREATEDB' : 'NOCREATEDB'
204+
}
205+
let canCreateRoleSql = ''
206+
if (canCreateRole !== undefined) {
207+
canCreateRoleSql = canCreateRole ? 'CREATEROLE' : 'NOCREATEROLE'
208+
}
209+
let inheritRoleSql = ''
210+
if (inheritRole !== undefined) {
211+
inheritRoleSql = inheritRole ? 'INHERIT' : 'NOINHERIT'
212+
}
213+
let canLoginSql = ''
214+
if (canLogin !== undefined) {
215+
canLoginSql = canLogin ? 'LOGIN' : 'NOLOGIN'
216+
}
217+
let isReplicationRoleSql = ''
218+
if (isReplicationRole !== undefined) {
219+
isReplicationRoleSql = isReplicationRole ? 'REPLICATION' : 'NOREPLICATION'
220+
}
221+
let canBypassRlsSql = ''
222+
if (canBypassRls !== undefined) {
223+
canBypassRlsSql = canBypassRls ? 'BYPASSRLS' : 'NOBYPASSRLS'
224+
}
225+
const connectionLimitSql =
226+
connectionLimit === undefined ? '' : `CONNECTION LIMIT ${connectionLimit}`
227+
let passwordSql = password === undefined ? '' : `PASSWORD '${password}'`
228+
let validUntilSql = validUntil === undefined ? '' : `VALID UNTIL '${validUntil}'`
229+
230+
return `
231+
BEGIN;
232+
ALTER ROLE "${oldName}"
233+
${isSuperuserSql}
234+
${canCreateDbSql}
235+
${canCreateRoleSql}
236+
${inheritRoleSql}
237+
${canLoginSql}
238+
${isReplicationRoleSql}
239+
${canBypassRlsSql}
240+
${connectionLimitSql}
241+
${passwordSql}
242+
${validUntilSql};
243+
${nameSql}
244+
COMMIT;`
245+
}
246+
const dropRoleSqlize = (name: string) => {
247+
return `DROP ROLE "${name}"`
248+
}
122249
const removeSystemSchemas = (data: Roles.Role[]) => {
123250
return data.map((role) => {
124251
let grants = role.grants.filter((x) => !DEFAULT_SYSTEM_SCHEMAS.includes(x.schema))

src/lib/sql/roles.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
-- TODO: Consider using pg_authid vs. pg_roles for unencrypted password field
12
SELECT
23
rolname AS name,
34
rolsuper AS is_superuser,

test/integration/index.spec.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@ describe('/roles', () => {
347347
name: 'test',
348348
isSuperuser: true,
349349
canCreateDb: true,
350+
canCreateRole: true,
351+
inheritRole: false,
352+
canLogin: true,
350353
isReplicationRole: true,
351354
canBypassRls: true,
352355
connectionLimit: 100,
@@ -356,10 +359,52 @@ describe('/roles', () => {
356359
const test = roles.find((role) => role.name === 'test')
357360
assert.equal(test.is_superuser, true)
358361
assert.equal(test.can_create_db, true)
362+
assert.equal(test.can_create_role, true)
363+
assert.equal(test.inherit_role, false)
364+
assert.equal(test.can_login, true)
359365
assert.equal(test.is_replication_role, true)
360366
assert.equal(test.can_bypass_rls, true)
361367
assert.equal(test.connection_limit, 100)
362368
assert.equal(test.valid_until, '2020-01-01T00:00:00.000Z')
363369
await axios.post(`${URL}/query`, { query: 'DROP ROLE test;' })
364370
})
371+
it('PATCH', async () => {
372+
const { data: newRole } = await axios.post(`${URL}/roles`, { name: 'foo' })
373+
374+
await axios.patch(`${URL}/roles/${newRole.id}`, {
375+
name: 'bar',
376+
isSuperuser: true,
377+
canCreateDb: true,
378+
canCreateRole: true,
379+
inheritRole: false,
380+
canLogin: true,
381+
isReplicationRole: true,
382+
canBypassRls: true,
383+
connectionLimit: 100,
384+
validUntil: '2020-01-01T00:00:00.000Z',
385+
})
386+
387+
const { data: roles } = await axios.get(`${URL}/roles`)
388+
const updatedRole = roles.find((role) => role.id === newRole.id)
389+
assert.equal(updatedRole.name, 'bar')
390+
assert.equal(updatedRole.is_superuser, true)
391+
assert.equal(updatedRole.can_create_db, true)
392+
assert.equal(updatedRole.can_create_role, true)
393+
assert.equal(updatedRole.inherit_role, false)
394+
assert.equal(updatedRole.can_login, true)
395+
assert.equal(updatedRole.is_replication_role, true)
396+
assert.equal(updatedRole.can_bypass_rls, true)
397+
assert.equal(updatedRole.connection_limit, 100)
398+
assert.equal(updatedRole.valid_until, '2020-01-01T00:00:00.000Z')
399+
400+
await axios.delete(`${URL}/roles/${newRole.id}`)
401+
})
402+
it('DELETE', async () => {
403+
const { data: newRole } = await axios.post(`${URL}/roles`, { name: 'foo bar' })
404+
405+
await axios.delete(`${URL}/roles/${newRole.id}`)
406+
const { data: roles } = await axios.get(`${URL}/roles`)
407+
const newRoleExists = roles.some((role) => role.id === newRole.id)
408+
assert.equal(newRoleExists, false)
409+
})
365410
})

0 commit comments

Comments
 (0)