diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..a7ac4f6 --- /dev/null +++ b/.npmignore @@ -0,0 +1,16 @@ +.github +.DS_Store +.editorconfig +.gitignore +.gitmodules +.lgtm.yml +appveyor.yml +codecov.yml +.release +.travis.yml +.eslintrc.yaml +.eslintrc.json +.codeclimate.yml +test/ +DEVELOP.md +.prettierrc.yml \ No newline at end of file diff --git a/conf.d/http.yml b/conf.d/http.yml index 9827a29..c16f85f 100644 --- a/conf.d/http.yml +++ b/conf.d/http.yml @@ -2,3 +2,33 @@ default: host: localhost port: 3000 + cookie: + # https://hapi.dev/module/cookie/api/?v=12.0.1 + name: sid-nictool + password: af1b926a5e21f535c4f5b6c42941c4cf + ttl: 3600000 # 1 hour + # domain: + path: / + clearInvalid: true + isSameSite: Strict + isSecure: true + isHttpOnly: true + keepAlive: false + # redirectTo: + group: NicTool + +production: + port: 8080 + cookie: + # Set your own secret password. hint: openssl rand -hex 16 + # password: + +test: + cookie: + isSecure: false + password: ^NicTool.Is,The#Best_Dns-Manager$ + +development: + cookie: + isSecure: false + password: ^NicTool.Is,The#Best_Dns-Manager$ diff --git a/conf.d/session.yml b/conf.d/session.yml deleted file mode 100644 index 3a85808..0000000 --- a/conf.d/session.yml +++ /dev/null @@ -1,31 +0,0 @@ - -default: - cookie: - # https://hapi.dev/module/cookie/api/?v=12.0.1 - name: sid-nictool - password: af1b926a5e21f535c4f5b6c42941c4cf - ttl: 3600000 # 1 hour - # domain: - path: / - clearInvalid: true - isSameSite: Strict - isSecure: true - isHttpOnly: true - keepAlive: false - # redirectTo: - # group: NicTool - -production: - cookie: - # Change to your own secret password. hint: openssl rand -hex 16 - # password: - -test: - cookie: - isSecure: false - password: ^NicTool.Is,The#Best_Dns-Manager$ - -development: - cookie: - isSecure: false - password: ^NicTool.Is,The#Best_Dns-Manager$ diff --git a/lib/config.js b/lib/config.js index d9b2fab..d4f4581 100644 --- a/lib/config.js +++ b/lib/config.js @@ -48,6 +48,8 @@ class Config { function applyDefaults(cfg = {}, defaults = {}) { for (const d in defaults) { + /* c8 ignore next */ + if (d === '__proto__' || d === 'constructor') continue if ([undefined, null].includes(cfg[d])) { cfg[d] = defaults[d] } else if (typeof cfg[d] === 'object' && typeof defaults[d] === 'object') { diff --git a/lib/config.test.js b/lib/config.test.js index 65cf517..3b82ee0 100644 --- a/lib/config.test.js +++ b/lib/config.test.js @@ -27,14 +27,9 @@ describe('config', () => { process.env.NODE_DEBUG = '' }) - it(`loads session test config`, async () => { - const cfg = await Config.get('session', 'test') - assert.deepEqual(cfg, sessCfg) - }) - - it(`loads session test config syncronously`, () => { - const cfg = Config.getSync('session', 'test') - assert.deepEqual(cfg, sessCfg) + it(`loads http test config`, async () => { + const cfg = await Config.get('http', 'test') + assert.deepEqual(cfg, httpCfg) }) it(`loads http test config syncronously`, () => { @@ -68,7 +63,9 @@ const mysqlTestCfg = { decimalNumbers: true, } -const sessCfg = { +const httpCfg = { + host: 'localhost', + port: 3000, cookie: { clearInvalid: true, isHttpOnly: true, @@ -80,9 +77,5 @@ const sessCfg = { ttl: 3600000, }, keepAlive: false, -} - -const httpCfg = { - host: 'localhost', - port: 3000, + group: 'NicTool', } diff --git a/lib/mysql.js b/lib/mysql.js index c31eeed..bb1857e 100644 --- a/lib/mysql.js +++ b/lib/mysql.js @@ -73,6 +73,7 @@ class Mysql { } whereConditions(query, params) { + let newQuery = query let paramsArray = [] if (Array.isArray(params)) { @@ -81,13 +82,13 @@ class Mysql { // Object to WHERE conditions let first = true for (const p in params) { - if (!first) query += ' AND' - query += ` ${p}=?` + if (!first) newQuery += ' AND' + newQuery += ` ${p}=?` paramsArray.push(params[p]) first = false } } - return [query, paramsArray] + return [newQuery, paramsArray] } async delete(query, params) { diff --git a/lib/permission.js b/lib/permission.js index a1ad40c..3235da6 100644 --- a/lib/permission.js +++ b/lib/permission.js @@ -9,29 +9,6 @@ const permDbMap = { name: 'perm_name', } -const boolFields = [ - 'group_create', - 'group_delete', - 'group_write', - 'nameserver_create', - 'nameserver_delete', - 'nameserver_write', - 'self_write', - 'user_create', - 'user_delete', - 'user_write', - 'zone_create', - 'zone_delegate', - 'zone_delete', - 'zone_write', - 'zonerecord_create', - 'zonerecord_delegate', - 'zonerecord_delete', - 'zonerecord_write', - 'inherit', - 'deleted', -] - class Permission { constructor() { this.mysql = Mysql @@ -39,34 +16,51 @@ class Permission { async create(args) { if (args.id) { - const g = await this.get({ id: args.id }) - if (g.length) return g[0].id + const p = await this.get({ id: args.id }) + if (p) return p.id } return await Mysql.insert( `INSERT INTO nt_perm`, - mapToDbColumn(args, permDbMap), + mapToDbColumn(objectToDb(args), permDbMap), ) } async get(args) { - const rows = await Mysql.select( - `SELECT nt_perm_id AS id - , nt_user_id AS uid - , nt_group_id AS gid - , inherit_perm AS inherit - , perm_name AS name + const query = `SELECT p.nt_perm_id AS id + , p.nt_user_id AS uid + , p.nt_group_id AS gid + , p.inherit_perm AS inherit + , p.perm_name AS name ${getPermFields()} - , deleted - FROM nt_perm WHERE`, - mapToDbColumn(args, permDbMap), - ) - for (const r of rows) { - for (const b of boolFields) { - r[b] = r[b] === 1 - } + , p.deleted + FROM nt_perm p WHERE` + // Mysql.debug(1) + const rows = await Mysql.select(query, mapToDbColumn(args, permDbMap)) + if (rows.length === 0) return + if (rows.length > 1) { + throw new Error( + `permissions.get found ${rows.length} rows for uid ${args.uid}`, + ) } - return rows + return dbToObject(rows[0]) + } + + async getGroup(args) { + const query = `SELECT p.nt_perm_id AS id + , p.nt_user_id AS uid + , p.nt_group_id AS gid + , p.inherit_perm AS inherit + , p.perm_name AS name + ${getPermFields()} + , p.deleted + FROM nt_perm p + INNER JOIN nt_user u ON p.nt_group_id = u.nt_group_id + WHERE p.deleted=0 + AND u.deleted=0 + AND u.nt_user_id=?` + const rows = await Mysql.select(query, [args.uid]) + return dbToObject(rows[0]) } async put(args) { @@ -83,23 +77,22 @@ class Permission { } async delete(args, val) { - const g = await this.get(args) - if (g.length !== 1) return false + const p = await this.get(args) + if (!p) return false await Mysql.execute(`UPDATE nt_perm SET deleted=? WHERE nt_perm_id=?`, [ val ?? 1, - g[0].id, + args.id, ]) return true } async destroy(args) { - const g = await this.get(args) - if (g.length === 1) { - await Mysql.delete( - `DELETE FROM nt_perm WHERE`, - mapToDbColumn(args, permDbMap), - ) - } + const p = await this.get(args) + if (!p) return false + return await Mysql.delete( + `DELETE FROM nt_perm WHERE`, + mapToDbColumn(args, permDbMap), + ) } } @@ -107,7 +100,7 @@ export default new Permission() function getPermFields() { return ( - `, nt_perm.` + + `, p.` + [ 'group_write', 'group_create', @@ -133,6 +126,119 @@ function getPermFields() { 'self_write', 'usable_ns', - ].join(`, nt_perm.`) + ].join(`, p.`) ) } + +/* the following two functions convert to and from: + +the SQL DB format: +{ + "id": 4096, + "uid": 4096, + "gid": 4096, + "inherit": 1, + "name": "Test Permission", + "group_write": 0, + "group_create": 0, + "group_delete": 0, + "zone_write": 1, + "zone_create": 1, + "zone_delegate": 1, + "zone_delete": 1, + "zonerecord_write": 0, + "zonerecord_create": 0, + "zonerecord_delegate": 0, + "zonerecord_delete": 0, + "user_write": 0, + "user_create": 0, + "user_delete": 0, + "nameserver_write": 0, + "nameserver_create": 0, + "nameserver_delete": 0, + "self_write": 0, + "usable_ns": "", + "deleted": 0 +} + +JSON object format: + +{ + "id": 4096, + "inherit": true, + "name": "Test Permission", + "self_write": false, + "deleted": false, + "group": { "id": 4096, "create": false, "write": false, "delete": false }, + "nameserver": { "usable": [], "create": false, "write": false, "delete": false }, + "zone": { "create": true, "write": true, "delete": true, "delegate": true }, + "zonerecord": { + "create": false, + "write": false, + "delete": false, + "delegate": false + }, + "user": { "id": 4096, "create": false, "write": false, "delete": false } +} +*/ + +const boolFields = ['self_write', 'inherit', 'deleted'] + +function dbToObject(row) { + const newRow = JSON.parse(JSON.stringify(row)) + for (const f of ['group', 'nameserver', 'zone', 'zonerecord', 'user']) { + for (const p of ['create', 'write', 'delete', 'delegate']) { + if (newRow[`${f}_${p}`] !== undefined) { + if (newRow[f] === undefined) newRow[f] = {} + newRow[f][p] = newRow[`${f}_${p}`] === 1 + delete newRow[`${f}_${p}`] + } + } + } + for (const b of boolFields) { + newRow[b] = newRow[b] === 1 + } + if (newRow.uid !== undefined) { + newRow.user.id = newRow.uid + delete newRow.uid + } + if (newRow.gid !== undefined) { + newRow.group.id = newRow.gid + delete newRow.gid + } + newRow.nameserver.usable = [] + if (![undefined, ''].includes(newRow.usable_ns)) { + newRow.nameserver.usable = newRow.usable_ns.split(',') + } + delete newRow.usable_ns + return newRow +} + +function objectToDb(row) { + const newRow = JSON.parse(JSON.stringify(row)) + if (newRow?.user?.id !== undefined) { + newRow.uid = newRow.user.id + delete newRow.user.id + } + if (newRow?.group?.id !== undefined) { + newRow.gid = newRow.group.id + delete newRow.group.id + } + if (newRow?.nameserver?.usable !== undefined) { + newRow.usable_ns = newRow.nameserver.usable.join(',') + delete newRow.nameserver.usable + } + for (const f of ['group', 'nameserver', 'zone', 'zonerecord', 'user']) { + for (const p of ['create', 'write', 'delete', 'delegate']) { + if (newRow[f] === undefined) continue + if (newRow[f][p] === undefined) continue + newRow[`${f}_${p}`] = newRow[f][p] === true ? 1 : 0 + delete newRow[f][p] + } + delete newRow[f] + } + for (const b of boolFields) { + newRow[b] = newRow[b] === true ? 1 : 0 + } + return newRow +} diff --git a/lib/permission.test.js b/lib/permission.test.js index bdc6f41..9cdc163 100644 --- a/lib/permission.test.js +++ b/lib/permission.test.js @@ -1,11 +1,17 @@ import assert from 'node:assert/strict' -import { describe, it, after } from 'node:test' +import { describe, it, after, before } from 'node:test' import Permission from './permission.js' +import User from './user.js' import permTestCase from './test/permission.json' with { type: 'json' } +import userTestCase from './test/user.json' with { type: 'json' } + +before(async () => { + await User.create(userTestCase) +}) after(async () => { - Permission.mysql.disconnect() + await Permission.mysql.disconnect() }) describe('permission', function () { @@ -13,25 +19,38 @@ describe('permission', function () { assert.ok(await Permission.create(permTestCase)) }) - it('gets permission by id', async () => { - const g = await Permission.get({ id: permTestCase.id }) - assert.deepEqual(g[0], permTestCase) + it('get: by id', async () => { + assert.deepEqual( + await Permission.get({ id: permTestCase.id }), + permTestCase, + ) + }) + + it('get: by user id', async () => { + assert.deepEqual( + await Permission.get({ uid: permTestCase.user.id }), + permTestCase, + ) }) - it('gets permission by user id', async () => { - const g = await Permission.get({ uid: permTestCase.uid }) - assert.deepEqual(g[0], permTestCase) + it('get: by group id', async () => { + assert.deepEqual( + await Permission.get({ gid: permTestCase.group.id }), + permTestCase, + ) }) - it('gets permission by group id', async () => { - const g = await Permission.get({ uid: permTestCase.gid }) - assert.deepEqual(g[0], permTestCase) + it('getGroup: gets group permissions', async () => { + assert.deepEqual( + await Permission.getGroup({ uid: permTestCase.user.id }), + permTestCase, + ) }) it('changes a permission', async () => { assert.ok(await Permission.put({ id: permTestCase.id, name: 'Changed' })) - const perms = await Permission.get({ id: permTestCase.id }) - assert.deepEqual(perms[0].name, 'Changed') + const perm = await Permission.get({ id: permTestCase.id }) + assert.deepEqual(perm.name, 'Changed') assert.ok( await Permission.put({ id: permTestCase.id, name: 'Test Permission' }), ) @@ -39,10 +58,17 @@ describe('permission', function () { it('deletes a permission', async () => { assert.ok(await Permission.delete({ id: permTestCase.id })) - let u = await Permission.get({ id: permTestCase.id }) - assert.equal(u[0].deleted, true) + let p = await Permission.get({ id: permTestCase.id }) + assert.equal(p.deleted, true) await Permission.delete({ id: permTestCase.id }, 0) // restore - u = await Permission.get({ id: permTestCase.id }) - assert.equal(u[0].deleted, false) + p = await Permission.get({ id: permTestCase.id }) + assert.equal(p.deleted, false) + }) + + it('destroys a permission', async () => { + const r = await Permission.destroy({ id: permTestCase.id }) + assert.equal(r.affectedRows, 1) + const p = await Permission.get({ id: permTestCase.id }) + assert.equal(p, undefined) }) }) diff --git a/lib/test/permission.json b/lib/test/permission.json index aa4a736..21b10b3 100644 --- a/lib/test/permission.json +++ b/lib/test/permission.json @@ -1,27 +1,22 @@ { "id": 4096, - "uid": 4096, - "gid": 4096, "inherit": true, "name": "Test Permission", - "group_write": false, - "group_create": false, - "group_delete": false, - "zone_write": true, - "zone_create": true, - "zone_delegate": true, - "zone_delete": true, - "zonerecord_write": false, - "zonerecord_create": false, - "zonerecord_delegate": false, - "zonerecord_delete": false, - "user_write": false, - "user_create": false, - "user_delete": false, - "nameserver_write": false, - "nameserver_create": false, - "nameserver_delete": false, "self_write": false, - "usable_ns": "", - "deleted": false + "deleted": false, + "group": { "id": 4096, "create": false, "write": false, "delete": false }, + "nameserver": { + "usable": [], + "create": false, + "write": false, + "delete": false + }, + "zone": { "create": true, "write": true, "delete": true, "delegate": true }, + "zonerecord": { + "create": false, + "write": false, + "delete": false, + "delegate": false + }, + "user": { "id": 4096, "create": false, "write": false, "delete": false } } diff --git a/lib/user.js b/lib/user.js index 11340e3..7b621f9 100644 --- a/lib/user.js +++ b/lib/user.js @@ -9,7 +9,7 @@ const userDbMap = { id: 'nt_user_id', gid: 'nt_group_id' } class User { constructor(args = {}) { this.debug = args?.debug ?? false - this.cfg = Config.getSync('session') + this.cfg = Config.getSync('http') this.mysql = Mysql } diff --git a/package.json b/package.json index c0cc181..f6e4295 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nictool/api", - "version": "3.0.0-alpha.1", + "version": "3.0.0-alpha.2", "description": "NicTool API", "main": "index.js", "type": "module", @@ -43,7 +43,7 @@ "@hapi/hoek": "^11.0.4", "@hapi/inert": "^7.1.0", "@hapi/vision": "^7.0.3", - "@nictool/validate": "^0.7.2", + "@nictool/validate": "^0.7.3", "hapi-swagger": "^17.2.1", "mysql2": "^3.9.2", "qs": "^6.11.2", diff --git a/routes/group.js b/routes/group.js index 72261d5..ddd1ead 100644 --- a/routes/group.js +++ b/routes/group.js @@ -83,6 +83,7 @@ function GroupRoutes(server) { }, handler: async (request, h) => { const groups = await Group.get(request.params) + /* c8 ignore next 10 */ if (groups.length !== 1) { return h .response({ diff --git a/routes/index.js b/routes/index.js index 3c3d213..66abeed 100644 --- a/routes/index.js +++ b/routes/index.js @@ -57,10 +57,8 @@ async function setup() { }, ]) - const sessionCfg = await Config.get('session') - server.auth.strategy('session', 'cookie', { - cookie: sessionCfg.cookie, + cookie: httpCfg.cookie, validate: async (request, session) => { const s = await Session.get({ id: session.nt_user_session_id }) diff --git a/routes/user.js b/routes/user.js index 778139c..8a3e093 100644 --- a/routes/user.js +++ b/routes/user.js @@ -117,6 +117,7 @@ function UserRoutes(server) { handler: async (request, h) => { const users = await User.get(request.params) if (users.length !== 1) { + /* c8 ignore next 8 */ return h .response({ meta: {