diff --git a/nodejs/api.md b/nodejs/api.md index 79f5cf9..8c7df99 100755 --- a/nodejs/api.md +++ b/nodejs/api.md @@ -3,7 +3,7 @@ **GET** `/api/hosts` ```bash -curl -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" https://admin.rubyisforpussys.com/api/hostsmine.com +curl -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" https://proxy-host.com/api/hostsmine.com ``` * 200 {"host":"yours.com","results":{"ip":"127.0.0.1:4000","updated":"1518595297563","username":"test10","forceSSL": false, "targetSSL": true, "targetPort": "443"}} @@ -15,7 +15,7 @@ curl -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" https://admin.rubyisf **GET** `/api/hosts` ```bash -curl -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" https://admin.rubyisforpussys.com/api/hosts +curl -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" https://proxy-host.com/api/hosts ``` * 200 {"hosts":["mine.com","mine2.com"]} @@ -37,7 +37,7 @@ the proxy. The default is false and this is HIGHLY recommended. * ** ```bash -curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X POST -d '{"host": "test.vm42.com", "ip": "192.168.1.21", "targetSSL": false, "targetPort": "443", "forceSSL": true} https://admin.rubyisforpussys.com/api/hosts +curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X POST -d '{"host": "test.vm42.com", "ip": "192.168.1.21", "targetSSL": false, "targetPort": "443", "forceSSL": true} https://proxy-host.com/api/hosts ``` * 200 {"message":"Host yours.com added."} @@ -50,7 +50,7 @@ curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd Takes the same params as add, but none are required -curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X POST -d '{"host": "test.vm42.com", "ip": "192.168.1.21", "targetSSL": false, "targetPort": "443", "forceSSL": true} https://admin.rubyisforpussys.com/api/hosts +curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X POST -d '{"host": "test.vm42.com", "ip": "192.168.1.21", "targetSSL": false, "targetPort": "443", "forceSSL": true} https://proxy-host.com/api/hosts * 200 {"message":"Host yours.com updated."} * 404 {"name": "HostNotFound", "message": "Host does not exists"} @@ -63,7 +63,7 @@ curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd **DELETE** /`api/hosts/` ```bash -curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X DELETE https://admin.rubyisforpussys.com/api/hosts +curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X DELETE https://proxy-host.com/api/hosts ``` * 200 {"message":"Host yours.com deleted"} @@ -75,7 +75,7 @@ curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd **post** `/users/invite` ```bash -curl -H "Content-Type: application/json" -H "auth-token: 0b06eb2e-4ca4-4881-9a0f-b8df55431cd1" -X POST https://admin.rubyisforpussys.com/users/invite +curl -H "Content-Type: application/json" -H "auth-token: 0b06eb2e-4ca4-4881-9a0f-b8df55431cd1" -X POST https://proxy-host.com/users/invite ``` * 200 {"token":"5caf94d2-2c91-4010-8df7-968d10802b9d"} @@ -86,7 +86,7 @@ curl -H "Content-Type: application/json" -H "auth-token: 0b06eb2e-4ca4-4881-9a0f **post** `/auth/invite/` ```bash -curl -H "Content-Type: application/json" -X POST -d "{\"username\": \"test9\", \"password\": \"palm7\"}" https://admin.rubyisforpussys.com/auth/invite/b33d8819-ec64-4cf4-a6ec-77562d738fa4 +curl -H "Content-Type: application/json" -X POST -d "{\"username\": \"test9\", \"password\": \"palm7\"}" https://proxy-host.com/auth/invite/b33d8819-ec64-4cf4-a6ec-77562d738fa4 ``` @@ -101,7 +101,7 @@ curl -H "Content-Type: application/json" -X POST -d "{\"username\": \"test9\", \ **post** `/auth/login` ```bash -curl -H "Content-Type: application/json" -X POST -d '{"username": "test8", "password": "mypassword"}' https://admin.rubyisforpussys.com/auth/login +curl -H "Content-Type: application/json" -X POST -d '{"username": "test8", "password": "mypassword"}' https://proxy-host.com/auth/login ``` * 200 {"login":true,"token":"027d3964-7d81-4462-a6f9-2c1f9b40b4be"} @@ -113,7 +113,7 @@ curl -H "Content-Type: application/json" -X POST -d '{"username": "test8", "pass **post** `/auth/verifykey` ```bash -curl -H "Content-Type: application/json" -X POST -d "{\"key\":\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDM9vboz5YGgESsrR2e4JOeP2qtmQo2S8BjI+Y/VxPQ6WbNFzAkXxDniHcnPCrhkeX36SKINvMjWnt4XOK2S+X+1tCoXJzqtcKKyK0gx8ijBxcWVPxsMWjMYTGSVSKiKnt6CyQzrbVGJMh3iAQ8Yv1JwH+6SAtMgT8it7iLyntNFJCesh4I/znEG58A5VBbdUle1Ztz9afjj1CZns17jk7KPm9ig5DmuvdvnMEfhFjfKv1Rp6S5nxacMoTP4tJNSEUh55IicoWk94ii5GwUVLYgyMmzdlA32TqVLFpU2yAvdA9WSnBaI/ZyktlfI7YAmK2wFBsagr9Pq1TcUAY6rZ/GTMjDxExgdYn/FxlufcuqeNJsJXs2A+0xDS/9mv/yGQzNZrL8DrVhY2OKKLoH4Q7enDbhSgEFmJUJMqPxuPEgLEvKfzcURSvIwRj1iCEw6S4dhdaLJl2RRBb1ZWBQbE5ogIbvAl7GFJUAhj3pqYJnd30VENv1MkK+IoCS7EEP0caqL9RNAId0Plud7q2XElHqzkYUE+z+Q/LvGgclXK1ZmZejNaMnV53wfhAevfwVyNGK9i5gbwc1P2lplIa5laXCcVWezqELEkTpdjp4AeKmMuCr8rY8EnLKIcKWEOsX5UumztCow6e1E55v3VeHvRZLpw4DZP7EE0Q8B/jPFWqbCw== wmantly@gmail.com\"}" https://admin.rubyisforpussys.com/auth/verifykey +curl -H "Content-Type: application/json" -X POST -d "{\"key\":\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDM9vboz5YGgESsrR2e4JOeP2qtmQo2S8BjI+Y/VxPQ6WbNFzAkXxDniHcnPCrhkeX36SKINvMjWnt4XOK2S+X+1tCoXJzqtcKKyK0gx8ijBxcWVPxsMWjMYTGSVSKiKnt6CyQzrbVGJMh3iAQ8Yv1JwH+6SAtMgT8it7iLyntNFJCesh4I/znEG58A5VBbdUle1Ztz9afjj1CZns17jk7KPm9ig5DmuvdvnMEfhFjfKv1Rp6S5nxacMoTP4tJNSEUh55IicoWk94ii5GwUVLYgyMmzdlA32TqVLFpU2yAvdA9WSnBaI/ZyktlfI7YAmK2wFBsagr9Pq1TcUAY6rZ/GTMjDxExgdYn/FxlufcuqeNJsJXs2A+0xDS/9mv/yGQzNZrL8DrVhY2OKKLoH4Q7enDbhSgEFmJUJMqPxuPEgLEvKfzcURSvIwRj1iCEw6S4dhdaLJl2RRBb1ZWBQbE5ogIbvAl7GFJUAhj3pqYJnd30VENv1MkK+IoCS7EEP0caqL9RNAId0Plud7q2XElHqzkYUE+z+Q/LvGgclXK1ZmZejNaMnV53wfhAevfwVyNGK9i5gbwc1P2lplIa5laXCcVWezqELEkTpdjp4AeKmMuCr8rY8EnLKIcKWEOsX5UumztCow6e1E55v3VeHvRZLpw4DZP7EE0Q8B/jPFWqbCw== wmantly@gmail.com\"}" https://proxy-host.com/auth/verifykey ``` * 200 {"info":"4096 SHA256:dfdCYzt0atMBXVZTJzUxsu99IjXXFXpocSox5q+jOs8 wmantly@gmail.com (RSA)\n"} @@ -125,7 +125,7 @@ curl -H "Content-Type: application/json" -X POST -d "{\"key\":\"ssh-rsa AAAAB3Nz **post** `/users/key` ```bash -curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X POST -d "{\"key\": \"ssh-rsa AAAAB3NzaC1yc2EAAjWnt4XOK2S+X+1tCoXJzqtcKKyK0gx8ijBxcWVPxsMWjMYTGSVSKiKnt6CyQzrbVGJMh3iAQ8Yv1JwH+6SAtMgT8it7iLyntNFJCesh4I/znEG58A5VBbdUle1Ztz9afjj1CZns17jk7KPm9ig5DmuvdvnMEfhFjfKv1Rp6S5nxacMoTP4tJNSEUh55IicoWk94ii5GwUVLYgyMmzdlA32TqVLFpU2yAvdA9WSnBaI/ZyktlfI7YAmK2wFBsagr9Pq1TcUAY6rZ/GTMjDxExgdYn/FxlufcuqeNJsJXs2A+0xDS/9mv/yGQzNZrL8DrVhY2OKKLoH4Q7enDbhSgEFmJUJMqPxuPEgLEvKfzcURSvIwRj1iCEw6S4dhdaLJl2RRBb1ZWBQbE5ogIbvAl7GFJUAhj3pqYJnd30VENv1MkK+IoCS7EEP0caqL9RNAId0Plud7q2XElHqzkYUE+z+Q/LvGgclXK1ZmZejNaMnV53wfhAevfwVyNGK9i5gbwc1P2lplIa5laXCcVWezqELEkTpdjp4AeKmMuCr8rY8EnLKIcKWEOsX5UumztCow6e1E55v3VeHvRZLpw4DZP7EE0Q8B/jPFWqbCw== wmantly@gmail.co\"}" https://admin.rubyisforpussys.com/users/key +curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X POST -d "{\"key\": \"ssh-rsa AAAAB3NzaC1yc2EAAjWnt4XOK2S+X+1tCoXJzqtcKKyK0gx8ijBxcWVPxsMWjMYTGSVSKiKnt6CyQzrbVGJMh3iAQ8Yv1JwH+6SAtMgT8it7iLyntNFJCesh4I/znEG58A5VBbdUle1Ztz9afjj1CZns17jk7KPm9ig5DmuvdvnMEfhFjfKv1Rp6S5nxacMoTP4tJNSEUh55IicoWk94ii5GwUVLYgyMmzdlA32TqVLFpU2yAvdA9WSnBaI/ZyktlfI7YAmK2wFBsagr9Pq1TcUAY6rZ/GTMjDxExgdYn/FxlufcuqeNJsJXs2A+0xDS/9mv/yGQzNZrL8DrVhY2OKKLoH4Q7enDbhSgEFmJUJMqPxuPEgLEvKfzcURSvIwRj1iCEw6S4dhdaLJl2RRBb1ZWBQbE5ogIbvAl7GFJUAhj3pqYJnd30VENv1MkK+IoCS7EEP0caqL9RNAId0Plud7q2XElHqzkYUE+z+Q/LvGgclXK1ZmZejNaMnV53wfhAevfwVyNGK9i5gbwc1P2lplIa5laXCcVWezqELEkTpdjp4AeKmMuCr8rY8EnLKIcKWEOsX5UumztCow6e1E55v3VeHvRZLpw4DZP7EE0Q8B/jPFWqbCw== wmantly@gmail.co\"}" https://proxy-host.com/users/key ``` * 200 {"message":true} diff --git a/nodejs/app.js b/nodejs/app.js index bfa0767..c2ca051 100755 --- a/nodejs/app.js +++ b/nodejs/app.js @@ -27,15 +27,16 @@ app.use('/', require('./routes/index')); app.use('/api/auth', require('./routes/auth')); // API routes for working with users. All endpoints need to be have valid user. -app.use('/api/users', middleware.auth, require('./routes/users')); +app.use('/api/user', middleware.auth, require('./routes/user')); // API routes for working with hosts. All endpoints need to be have valid user. -app.use('/api/hosts', middleware.auth, require('./routes/hosts')); +app.use('/api/host', middleware.auth, require('./routes/host')); // Catch 404 and forward to error handler. If none of the above routes are // used, this is what will be called. app.use(function(req, res, next) { var err = new Error('Not Found'); + err.message = 'Page not found' err.status = 404; next(err); }); diff --git a/nodejs/middleware/auth.js b/nodejs/middleware/auth.js index 53e8975..6b98731 100755 --- a/nodejs/middleware/auth.js +++ b/nodejs/middleware/auth.js @@ -1,19 +1,17 @@ 'use strict'; -const Users = require('../models/users'); +const {Auth} = require('../models/auth'); async function auth(req, res, next){ - if(req.header('auth-token')){ - let user = await Users.checkToken({token: req.header('auth-token')}); + try{ + let user = await Auth.checkToken({token: req.header('auth-token')}); if(user.username){ req.user = user; return next(); } + }catch(error){ + next(error); } - - return res.status(401).json({ - message: 'Login failed' - }); } module.exports = {auth}; diff --git a/nodejs/models/auth.js b/nodejs/models/auth.js index e69de29..4c1eb80 100755 --- a/nodejs/models/auth.js +++ b/nodejs/models/auth.js @@ -0,0 +1,55 @@ +const {promisify} = require('util'); +const pam = require('authenticate-pam'); +const authenticate = promisify(pam.authenticate); + +const {User} = require('./user'); +const {Token, AuthToken} = require('./token'); + +Auth = {} +Auth.errors = {} + +Auth.errors.login = function(){ + let error = new Error('PamLoginFailed'); + error.name = 'PamLoginFailed'; + error.message = `Invalid Credentials, login failed.`; + error.status = 401; + + return error; +} + +Auth.login = async function(data){ + try{ + let auth = await authenticate(data.username, data.password); + let user = await User.get(data); + let token = await AuthToken.add(user); + + return {user, token} + }catch(error){ + if (error == 'Authentication failure'){ + throw this.errors.login() + } + throw error; + } +}; + +Auth.checkToken = async function(data){ + try{ + let token = await AuthToken.get(data); + if(token.is_valid){ + return await User.get(token.created_by); + } + }catch(error){ + throw this.errors.login(); + } +}; + +Auth.logOut = async function(data){ + try{ + let token = await AuthToken.get(data); + await token.remove(); + }catch(error){ + throw error; + } +} + +module.exports = {Auth, AuthToken}; diff --git a/nodejs/models/host.js b/nodejs/models/host.js new file mode 100755 index 0000000..5addd8b --- /dev/null +++ b/nodejs/models/host.js @@ -0,0 +1,19 @@ +'use strict'; + +const Host = require('../utils/redis_model')({ + _name: 'host', + _key: 'host', + _keyMap: { + 'created_by': {isRequired: true, type: 'string', min: 3, max: 500}, + 'created_on': {default: function(){return (new Date).getTime()}}, + 'updated_by': {default:"__NONE__", isRequired: false, type: 'string',}, + 'updated_on': {default: function(){return (new Date).getTime()}, always: true}, + 'host': {isRequired: true, type: 'string', min: 3, max: 500}, + 'ip': {isRequired: true, type: 'string', min: 3, max: 500}, + 'targetport': {isRequired: true, type: 'number', min:0, max:65535}, + 'forcessl': {isRequired: false, default: true, type: 'boolean'}, + 'targetssl': {isRequired: false, default: false, type: 'boolean'}, + } +}); + +module.exports = {Host}; diff --git a/nodejs/models/hosts.js b/nodejs/models/hosts.js deleted file mode 100755 index dabe68d..0000000 --- a/nodejs/models/hosts.js +++ /dev/null @@ -1,147 +0,0 @@ -'use strict'; - -const {promisify} = require('util'); -const client = require('../utils/redis'); -const objValidate = require('../utils/object_validate'); - -const hostKeysMap = { - 'host': {isRequired: true, type: 'string', min: 3, max: 500}, - 'ip': {isRequired: true, type: 'string', min: 3, max: 500}, - 'updated': {default: function(){return (new Date).getTime()}, always: true}, - 'username': {isRequired: true, type: 'string', always: true}, - 'targetport': {isRequired: true, type: 'number', min:0, max:65535}, - 'forcessl': {isRequired: false, default: true, type: 'boolean'}, - 'targetssl': {isRequired: false, default: false, type: 'boolean'}, -} - -async function getInfo(data){ - try{ - let info = await client.HGETALL('host_' + data.host); - - if(info){ - info['host'] = data.host; - - return objValidate.parseFromString(hostKeysMap, info); - } - - }catch(error){ - throw error - } - - let error = new Error('HostNotFound'); - error.name = 'HostNotFound'; - error.message = 'Host does not exists'; - error.status = 404; - throw error; -} - -async function exists(host){ - try{ - await getInfo({host}) - return true - }catch(error){ - return false; - } -} - -async function listAll(){ - try{ - let hosts = await client.SMEMBERS('hosts'); - return hosts; - }catch(error){ - throw error; - } -} - -async function listAllDetail(){ - let out = []; - - for(let host of await listAll()){ - out.push(await getInfo({host})); - } - - return out -} - -async function add(data){ - try{ - console.log('host', data) - data = objValidate.processKeys(hostKeysMap, data); - - - if(data.host && await exists(data.host)){ - let error = new Error('HostNameUsed'); - error.name = 'HostNameUsed'; - error.message = 'Host already exists'; - error.status = 409; - throw error; - }else if(data.host){ - await client.SADD('hosts', data.host); - } - - for(let key of Object.keys(data)){ - await client.HSET('host_' + data.host, key, data[key]); - } - - return true; - } catch(error){ - throw error - } -} - -async function edit(data, host){ - try{ - - // Get the current host and trow a 404 if it doesnt exist. - let hostData = await getInfo({host}); - - // Check to see if host name changed - if(data.host && data.host !== host){ - - console.log('pre assign', hostData, data) - - // Merge the current data into with the updated data - let newData = Object.assign({}, hostData, data); - - // Remove the updated failed so it doesnt keep it - delete newData.updated; - - // Create a new record for the updated host. If that succeeds, - // delete the old recored - if(await add(newData)) await remove({host}); - - }else{ - // Update what ever fields that where passed. - - // Validate the passed data, ignoring required fields. - data = objValidate.processKeys(hostKeysMap, data, true); - - // Loop over the data fields and apply them to redis - for(let key of Object.keys(data)){ - await client.HSET('host_' + host, key, data[key]); - } - } - } catch(error){ - // Pass any error to the calling function - throw error; - } -} - -async function remove(data){ - try{ - await getInfo(data); - - await client.SREM('hosts', data.host); - let count = await client.DEL('host_' + data.host); - - for(let key of Object.keys(hostKeysMap)){ - await client.HDEL('host_' + data.host, key); - } - return count; - } catch(error) { - throw error; - } -} - - -module.exports = {getInfo, listAll, listAllDetail, add, edit, remove}; diff --git a/nodejs/models/token.js b/nodejs/models/token.js new file mode 100644 index 0000000..37cb371 --- /dev/null +++ b/nodejs/models/token.js @@ -0,0 +1,60 @@ +'use strict'; + +const redis_model = require('../utils/redis_model') +const UUID = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}; + + +const Token = function(data){ + return redis_model({ + _name: `token_${data.name}`, + _key: 'token', + _keyMap: Object.assign({}, { + 'created_by': {isRequired: true, type: 'string', min: 3, max: 500}, + 'created_on': {default: function(){return (new Date).getTime()}}, + 'updated_on': {default: function(){return (new Date).getTime()}, always: true}, + 'token': {default: UUID, type: 'string', min: 36, max: 36}, + 'is_valid': {default: true, type: 'boolean'} + }, data.keyMap || {}) + }); +}; + +Token.check = async function(data){ + try{ + return this.is_valid; + }catch(error){ + return false + } +} + +var InviteToken = Object.create(Token({ + name: 'invite_test1', + keyMap:{ + claimed_by: {default:"__NONE__", isRequired: false, type: 'string',} + } +})); + +InviteToken.consume = async function(data){ + try{ + if(this.is_valid){ + data['is_valid'] = false; + + await this.update(data); + return true; + } + return false; + + }catch(error){ + throw error; + } +} + +var AuthToken = Object.create(Token({ + name: 'auth_test1', +})); + +AuthToken.add = async function(data){ + data.created_by = data.username; + return AuthToken.__proto__.add(data); +}; + +module.exports = {Token, InviteToken, AuthToken} diff --git a/nodejs/models/user.js b/nodejs/models/user.js new file mode 100755 index 0000000..b157335 --- /dev/null +++ b/nodejs/models/user.js @@ -0,0 +1,138 @@ +'use strict'; + +const linuxUser = require('linux-sys-user').promise(); +const objValidate = require('../utils/object_validate'); +const {Token, InviteToken} = require('./token'); + +var User = {} + +User.keyMap = { + 'username': {isRequired: true, type: 'string', min: 3, max: 500}, + 'password': {isRequired: true, type: 'string', min: 3, max: 500}, +} + +User.list = async function(){ + try{ + let users = await linuxUser.getUsers(); + + for(let user of users){ + delete user.password + } + + return users; + }catch(error){ + throw error; + } +}; + +User.get = async function(data){ + try{ + if(typeof data !== 'object'){ + let username = data; + data = {}; + data.username = username; + } + + let user = await linuxUser.getUserInfo(data.username); + + if(user){ + let obj = Object.create(this); + Object.assign(obj, user); + + return obj; + }else{ + let error = new Error('UserNotFound'); + error.name = 'UserNotFound'; + error.message = `PAM:${data.username} does not exists`; + error.status = 404; + throw error; + } + }catch(error){ + throw error; + } +}; + +User.exists = async function(data){ + // Return true or false if the requested entry exists ignoring error's. + try{ + await this.get(data); + + return true + }catch(error){ + return false; + } +}; + +User.add = async function(data) { + try{ + data = objValidate.processKeys(this.keyMap, data); + let systemUser = await linuxUser.addUser(data.username); + await require('util').promisify(setTimeout)(500) + let systemUserPassword = await linuxUser.setPassword(data.username, data.password); + + return this.get(data.username); + + }catch(error){ + if(error.message.includes('exists')){ + let error = new Error('UserNameUsed'); + error.name = 'UserNameUsed'; + error.message = `PAM:${data.username} already exists`; + error.status = 409; + + throw error; + } + throw error; + } +}; + +User.addByInvite = async function(data){ + try{ + let token = await InviteToken.get(data.token); + + if(!token.is_valid){ + let error = new Error('Token Invalid'); + error.name = 'Token Invalid'; + error.message = `Token is not valid or as allready been used. ${data.token}`; + error.status = 401; + throw error; + } + + let user = await this.add(data); + + if(user){ + await token.consume({claimed_by: user.username}); + return user; + } + + }catch(error){ + throw error; + } + +}; + +User.setPassword = async function(data){ + try{ + if(!data.password1 || data.password1 !== data.password2){ + throw new Error('PasswordMismatch'); + } + + await linuxUser.setPassword(this.username, data.password1); + + return this; + }catch(error){ + throw error; + } +}; + +User.invite = async function(){ + try{ + let token = await InviteToken.add({created_by: this.username}); + + return token; + + }catch(error){ + throw error; + } +}; + +module.exports = {User}; diff --git a/nodejs/models/users.js b/nodejs/models/users.js deleted file mode 100755 index 7b4e69a..0000000 --- a/nodejs/models/users.js +++ /dev/null @@ -1,107 +0,0 @@ -'use strict'; - -const {promisify} = require('util'); -const client = require('../utils/redis'); -const linuxUser = require('linux-sys-user'); -const pam = require('authenticate-pam'); - -const UUID = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}; - -const authenticate = promisify(pam.authenticate); - -const addSSHtoUser = promisify(linuxUser.addSSHtoUser) -const getUserGroups = promisify(linuxUser.getUserGroups); -const verifySSHKey = promisify(linuxUser.verifySSHKey); -const addUser = promisify(linuxUser.addUser); -const setPassword = promisify(linuxUser.setPassword); - -/* - Invite -*/ -async function makeInviteToken(data){ - let token = UUID(); - await client.HSET('users_tokens_invite', token, JSON.stringify({ - created_by: data.username, - isAdmin: data.isAdmin, - invited: false - })); - - return token; -} - -async function checkInvite(data){ - let token = await client.HGET('users_tokens_invite', data.token); - - return JSON.parse(token); -} - -async function consumeInvite(data){ - let invite = await checkInvite(data); - - invite.invited = data.username; - - await client.HSET('users_tokens_invite', data.token, JSON.stringify(invite)); -} - -/* - Auth/ Auth token -*/ - -async function login(data){ - try{ - await authenticate(data.username, data.password); - - return await getUserGroups(data.username); - }catch(error){ - return false; - } -} - -async function addToken(data){ - let token = UUID(); - await client.HSET('users_tokens', token, data.username); - - return token; -} - -async function checkToken(data){ - let user = await client.HGET('users_tokens', data.token); - - return { - username: user, - groups: (user && await getUserGroups(user)) - } -} - -async function addSSHkey(data){ - - try{ - let user = await addSSHtoUser(data.username, data.key); - return true; - } catch (error) { - return error; - } -} - -/* - Users -*/ - -async function add(data) { - - let systemUser = await addUser(data.username); - let systemUserPassword = await setPassword(data.username, data.password); - -} - -async function verifyKey(data){ - return await verifySSHKey(key) -} - -async function ifUserExists(data){ - const getUserInfo = promisify(linuxUser.getUserInfo); - return await getUserInfo(data.username); -} - -module.exports = {login, add, addToken, checkToken, ifUserExists, - makeInviteToken, checkInvite, consumeInvite, addSSHkey, verifyKey}; diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 801c904..939774b 100755 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -206,12 +206,9 @@ "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" }, "linux-sys-user": { - "version": "github:wmantly/linux-user#94ce2f75a5cc365355ca10d2f5b824c6f0180609", - "from": "github:wmantly/linux-user" - }, - "linux-user": { - "version": "github:wmantly/linux-user#94ce2f75a5cc365355ca10d2f5b824c6f0180609", - "from": "github:wmantly/linux-user" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linux-sys-user/-/linux-sys-user-1.1.0.tgz", + "integrity": "sha512-T8FVDvCfYnqjnz/GkHwu8mrgYEZ7QcFkHwXSQjs6YI3jQVZzue3zwjV/uvEYvy+AGGbHp8zIoG1SOYgmIuCX9g==" }, "media-typer": { "version": "0.3.0", diff --git a/nodejs/package.json b/nodejs/package.json index 4d59715..fac02f4 100755 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -15,8 +15,7 @@ "authenticate-pam": "github:WeiAnAn/node-authenticate-pam", "ejs": "^3.0.1", "express": "~4.16.1", - "linux-sys-user": "github:wmantly/linux-user", - "linux-user": "github:wmantly/linux-user", + "linux-sys-user": "^1.1.0", "redis": "^2.8.0" }, "license": "MIT", diff --git a/nodejs/routes/auth.js b/nodejs/routes/auth.js index ed77ae3..0f1d1a4 100755 --- a/nodejs/routes/auth.js +++ b/nodejs/routes/auth.js @@ -1,90 +1,55 @@ 'use strict'; const router = require('express').Router(); -const Users = require('../models/users'); +const {User} = require('../models/user'); +const {Auth, AuthToken} = require('../models/auth'); -/* - Password login -*/ -router.post('/login', async function(req, res){ - let username = req.body.username; - let password = req.body.password; - let groups = await Users.login({username, password}); - - if(groups){ +router.post('/login', async function(req, res, next){ + try{ + let auth = await Auth.login(req.body); return res.json({ login: true, - token: await Users.addToken({username}), - groups: groups, - }); - }else{ - return res.status(401).json({ - login: false - }); - } - -}); - -/* - verify public ssh key -*/ -router.post('/verifykey', async function(req, res){ - let key = req.body.key; - - try{ - return res.json({ - info: await Users.verifyKey(key) + token: auth.token.token, }); }catch(error){ - return res.status(400).json({ - message: 'Key is not a public key file!' - }); + next(error); } - }); - router.post('/invite/:token', async function(req, res, next) { - let username = req.body.username; - let password = req.body.password; - let token = req.params.token; + try{ + req.body.token = req.params.token; + let user = await User.addByInvite(req.body); + let token = await AuthToken.add(user); - // make sure invite token is valid - let invite = await Users.checkInvite({token}); - - if(!invite || invite.invited){ - return res.status(401).json({ - message: 'Token not valid' + return res.json({ + user: user.username, + token: token.token }); + + }catch(error){ + next(error); } - // make sure requires fields are in - if(!username || !password){ - return res.status(400).json({ - message: 'Missing fields' - }); - } - - // make sure the requested user name can be used - if(await Users.ifUserExists({username})){ - return res.status(409).json({ - message: 'Username taken' - }); - } - - // create the new user - await Users.add({username, password, isAdmin: invite.isAdmin}); - - // consume the invite token - await Users.consumeInvite({token, username}); - - // send back API token for the new user - return res.json({ - user: username, - token: await Users.addToken({username}) - }); - }); module.exports = router; + +/* + verify public ssh key +*/ +// router.post('/verifykey', async function(req, res){ +// let key = req.body.key; + +// try{ +// return res.json({ +// info: await Users.verifyKey(key) +// }); +// }catch(error){ +// return res.status(400).json({ +// message: 'Key is not a public key file!' +// }); +// } + +// }); \ No newline at end of file diff --git a/nodejs/routes/hosts.js b/nodejs/routes/host.js similarity index 66% rename from nodejs/routes/hosts.js rename to nodejs/routes/host.js index f959f55..d7e7839 100755 --- a/nodejs/routes/hosts.js +++ b/nodejs/routes/host.js @@ -1,15 +1,15 @@ 'use strict'; const router = require('express').Router(); +const {Host} = require('../models/host'); -const Host = require('../models/hosts'); router.get('/:host', async function(req, res, next){ try{ return res.json({ host: req.params.host, - results: await Host.getInfo({host: req.params.host}) + results: await Host.get({host: req.params.host}) }); }catch(error){ return next(error); @@ -20,7 +20,7 @@ router.get('/:host', async function(req, res, next){ router.get('/', async function(req, res, next){ try{ return res.json({ - hosts: req.query.detail ? await Host.listAllDetail() : await Host.listAll() + hosts: await Host[req.query.detail ? "listDetail" : "list"]() }); }catch(error){ next(error) @@ -29,27 +29,27 @@ router.get('/', async function(req, res, next){ router.put('/:host', async function(req, res, next){ try{ - req.body.username = req.user.username; - await Host.edit(req.body, req.params.host); + req.body.updated_by = req.user.username; + await Host.update(req.body, req.params.host); return res.json({ message: `Host "${req.params.host}" updated.` }); }catch(error){ - return next(error) + return next(error); } }); router.post('/', async function(req, res, next){ try{ - req.body.username = req.user.username; + req.body.created_by = req.user.username; await Host.add(req.body); return res.json({ message: `Host "${req.body.host}" added.` }); } catch (error){ - next(error) + next(error); } }); @@ -57,15 +57,15 @@ router.post('/', async function(req, res, next){ router.delete('/:host', async function(req, res, next){ try{ - let host = req.params.host; - let count = await Host.remove({host}); + let host = await Host.get(req.params); + let count = await host.remove(host); return res.json({ message: `Host ${req.params.host} deleted`, }); }catch(error){ - return next(error) + next(error); } }); diff --git a/nodejs/routes/user.js b/nodejs/routes/user.js new file mode 100755 index 0000000..65c5947 --- /dev/null +++ b/nodejs/routes/user.js @@ -0,0 +1,42 @@ +'use strict'; + +const router = require('express').Router(); +const {User} = require('../models/user'); + + +router.get('/me', async function(req, res){ + try{ + return res.json({username: req.user.username}); + }catch(error){ + next(error); + } +}); + +router.post('/invite', async function(req, res, next){ + try{ + let token = await req.user.invite(); + + return res.json({token: token.token}); + }catch(error){ + next(error); + } +}); + +router.post('/key', async function(req, res, next){ + try{ + let added = await User.addSSHkey({ + username: req.user.username, + key: req.body.key + }); + + return res.status(added === true ? 200 : 400).json({ + message: added + }); + + }catch(error){ + next(error); + } + +}); + +module.exports = router; diff --git a/nodejs/routes/users.js b/nodejs/routes/users.js deleted file mode 100755 index 23fab94..0000000 --- a/nodejs/routes/users.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -const router = require('express').Router(); -const Users = require('../models/users'); - -router.get('/me', async function(req, res){ - return res.json({username: req.user.username}); -}); - -router.post('/invite', async function(req, res){ - let token = await Users.makeInviteToken({ - username: res.user - }); - - return res.json({token:token}); -}); - -router.post('/key', async function(req, res){ - let added = await Users.addSSHkey({ - username: req.user.username, - key: req.body.key - }); - - return res.status(added === true ? 200 : 400).json({ - message: added - }); - -}); - -module.exports = router; diff --git a/nodejs/utils/object_validate.js b/nodejs/utils/object_validate.js index cc57249..cbbf648 100644 --- a/nodejs/utils/object_validate.js +++ b/nodejs/utils/object_validate.js @@ -32,7 +32,7 @@ function processKeys(map, data, partial){ continue; } - out[key] = data.hasOwnProperty(key) ? data[key] : returnOrCall(map[key].default); + out[key] = data.hasOwnProperty(key) && data[key] !== undefined ? data[key] : returnOrCall(map[key].default); if(data.hasOwnProperty(key) && process_type[map[key].type]){ let typeError = process_type[map[key].type](map[key], data[key]); @@ -54,15 +54,13 @@ function processKeys(map, data, partial){ function parseFromString(map, data){ let types = { - boolean: function(value){ return value === 'true' ? true : false }, + boolean: function(value){ return value === 'false' ? false : true }, number: Number, string: String, }; for(let key of Object.keys(data)){ - console.log('looking at', key) if(map[key] && map[key].type){ - console.log('converting', data[key], 'to', map[key].type) data[key] = types[map[key].type](data[key]); } } @@ -75,41 +73,8 @@ function ObjectValidateError(message) { this.message = (message || {}); this.status = 422; } + ObjectValidateError.prototype = Error.prototype; module.exports = {processKeys, parseFromString, ObjectValidateError}; - -if (require.main === module) { - const keys_map = { - 'host': {isRequired: true, type: 'string', min: 3, max: 500}, - 'ip': {isRequired: true, type: 'string', min: 3, max: 500}, - 'updated': {default: function(){return (new Date).getTime()}, always:true}, - 'username': {isRequired: true, type: 'string', always: true}, - 'targetport': {isRequired: true, type: 'number', min:0, max:65535}, - 'forcessl': {isRequired: false, default: true, type: 'boolean'}, - 'targetssl': {isRequired: false, default: false, type: 'boolean'}, - } - - console.log(processKeys(keys_map, { - host:'asdqwwd', - ip: 'sdfwef', - username: '', - targetport: 8000, - updated: 'dqwqwdq' - })); - - - console.log(parseFromString(keys_map, { - host: 'stest.theta42.com', - ip: 'googs', - updated: '1577054966047', - username: 'william', - targetport: '8080', - forcessl: 'true', - targetssl: 'false' } - )) - -} - - diff --git a/nodejs/utils/redis_model.js b/nodejs/utils/redis_model.js new file mode 100644 index 0000000..792b102 --- /dev/null +++ b/nodejs/utils/redis_model.js @@ -0,0 +1,191 @@ +'use strict'; + +const client = require('../utils/redis'); +const objValidate = require('../utils/object_validate'); + + +let table = {}; + +table.get = async function(data){ + try{ + // if the data argument was passed as the index key value, make a data + // object and add the index key to it. + if(typeof data !== 'object'){ + let key = data; + data = {}; + data[this._key] = key; + } + + // Get all the hash keys for the passed index key. + let res = await client.HGETALL(`${this._name}_${data[this._key]}`); + + // If the redis query resolved to something, prepare the data. + if(res){ + + // Redis always returns strings, use the keyMap schema to turn them + // back to native values. + res = objValidate.parseFromString(this._keyMap, res); + + // Make sure the index key in in the returned object. + res[this._key] = data[this._key]; + + // Create a instance for this redis entry. + var entry = Object.create(this); + + // Insert the redis response into the instance. + Object.assign(entry, res); + + // Return the instance to the caller. + return entry; + } + + }catch(error){ + throw error + } + + let error = new Error('EntryNotFound'); + error.name = 'EntryNotFound'; + error.message = `${this._name}:${data[this._key]} does not exists`; + error.status = 404; + throw error; +}; + +table.exists = async function(data){ + // Return true or false if the requested entry exists ignoring error's. + try{ + await this.get(data); + + return true + }catch(error){ + return false; + } +}; + +table.list = async function(){ + // return a list of all the index keys for this table. + try{ + + return await client.SMEMBERS(this._name); + + }catch(error){ + throw error; + } +}; + +table.listDetail = async function(){ + // Return a list of the entries as instances. + let out = []; + + for(let entry of await this.list()){ + out.push(await this.get(entry)); + } + + return out +}; + +table.add = async function(data){ + // Add a entry to this redis table. + try{ + + // Validate the passed data by the keyMap schema. + data = objValidate.processKeys(this._keyMap, data); + + // Do not allow the caller to overwrite an existing index key, + if(data[this._key] && await this.exists(data)){ + let error = new Error('EntryNameUsed'); + error.name = 'EntryNameUsed'; + error.message = `${this._name}:${data[this._key]} already exists`; + error.status = 409; + + throw error; + } + + // Add the key to the members for this redis table + await client.SADD(this._name, data[this._key]); + + // Add the values for this entry. + for(let key of Object.keys(data)){ + await client.HSET(`${this._name}_${data[this._key]}`, key, data[key]); + } + + // return the created redis entry as entry instance. + return await this.get(data[this._key]); + } catch(error){ + throw error; + } +}; + +table.update = async function(data, key){ + // Update an existing entry. + try{ + // If an index key is passed, we assume is passed, assume we are not + // part of an entry instance. Make one and recall this from from a entry + // instance, + if(key) return await (await this.get(key)).update(data); + + // Check to see if entry name changed. + if(data[this._key] && data[this._key] !== this[this._key]){ + + // Merge the current data into with the updated data + let newData = Object.assign({}, this, data); + + // Remove the updated failed so it doesnt keep it + delete newData.updated; + + // Create a new record for the updated entry. If that succeeds, + // delete the old recored + if(await this.add(newData)) await this.remove(); + + }else{ + // Update what ever fields that where passed. + + // Validate the passed data, ignoring required fields. + data = objValidate.processKeys(this._keyMap, data, true); + + // Loop over the data fields and apply them to redis + for(let key of Object.keys(data)){ + this[key] = data[key]; + await client.HSET(`${this._name}_${this[this._key]}`, key, data[key]); + } + } + + return this; + + } catch(error){ + // Pass any error to the calling function + throw error; + } +}; + +table.remove = async function(data){ + // Remove an entry from this table. + + data = data || this; + try{ + // Remove the index key from the tables members list. + await client.SREM(this._name, data[this._key]); + + // Remove the entries hash values. + let count = await client.DEL(`${this._name}_${data[this._key]}`); + + // Return the number of removed values to the caller. + return count; + + } catch(error) { + throw error; + } +}; + +function Table(data){ + // Create a table instance. + let obj = Object.create(table); + + // Insert the user assigned options + Object.assign(obj, data); + + // Return the table instance to the caller. + return obj; + +}; + +module.exports = Table;