diff --git a/src/backend/internal/user.js b/src/backend/internal/user.js index 9d6abb8..7a487b6 100644 --- a/src/backend/internal/user.js +++ b/src/backend/internal/user.js @@ -1,11 +1,12 @@ 'use strict'; -const _ = require('lodash'); -const error = require('../lib/error'); -const userModel = require('../models/user'); -const authModel = require('../models/auth'); -const gravatar = require('gravatar'); -const internalToken = require('./token'); +const _ = require('lodash'); +const error = require('../lib/error'); +const userModel = require('../models/user'); +const userPermissionModel = require('../models/user_permission'); +const authModel = require('../models/auth'); +const gravatar = require('gravatar'); +const internalToken = require('./token'); function omissions () { return ['is_deleted']; @@ -56,7 +57,23 @@ const internalUser = { } }) .then(user => { - return internalUser.get(access, {id: user.id}); + // Create permissions row as well + let is_admin = data.roles.indexOf('admin') !== -1; + + return userPermissionModel + .query() + .insert({ + user_id: user.id, + visibility: is_admin ? 'all' : 'user', + proxy_hosts: 'manage', + redirection_hosts: 'manage', + dead_hosts: 'manage', + streams: 'manage', + access_lists: 'manage' + }) + .then(() => { + return internalUser.get(access, {id: user.id, expand: ['permissions']}); + }); }); }, @@ -145,6 +162,7 @@ const internalUser = { .query() .where('is_deleted', 0) .andWhere('id', data.id) + .allowEager('[permissions]') .first(); // Custom omissions @@ -377,6 +395,50 @@ const internalUser = { }); }, + /** + * @param {Access} access + * @param {Object} data + * @return {Promise} + */ + setPermissions: (access, data) => { + return access.can('users:permissions', data.id) + .then(() => { + return internalUser.get(access, {id: data.id}); + }) + .then(user => { + if (user.id !== data.id) { + // Sanity check that something crazy hasn't happened + throw new error.InternalValidationError('User could not be updated, IDs do not match: ' + user.id + ' !== ' + data.id); + } + + return user; + }) + .then(user => { + // Get perms row, patch if it exists + return userPermissionModel + .query() + .where('user_id', user.id) + .first() + .then(existing_auth => { + if (existing_auth) { + // patch + return userPermissionModel + .query() + .where('user_id', user.id) + .patchAndFetchById(existing_auth.id, _.assign({user_id: user.id}, data)); + } else { + // insert + return userPermissionModel + .query() + .insertAndFetch(_.assign({user_id: user.id}, data)); + } + }) + .then(permissions => { + return true; + }); + }); + }, + /** * @param {Access} access * @param {Object} data diff --git a/src/backend/lib/access/users-permissions.json b/src/backend/lib/access/users-permissions.json new file mode 100644 index 0000000..d2709fd --- /dev/null +++ b/src/backend/lib/access/users-permissions.json @@ -0,0 +1,7 @@ +{ + "anyOf": [ + { + "$ref": "roles#/definitions/admin" + } + ] +} diff --git a/src/backend/migrations/20180618015850_initial.js b/src/backend/migrations/20180618015850_initial.js index 9ecb22c..03ee069 100644 --- a/src/backend/migrations/20180618015850_initial.js +++ b/src/backend/migrations/20180618015850_initial.js @@ -43,6 +43,124 @@ exports.up = function (knex/*, Promise*/) { }) .then(() => { logger.info('[' + migrate_name + '] user Table created'); + + return knex.schema.createTable('user_permission', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('user_id').notNull().unsigned(); + table.string('visibility').notNull(); + table.string('proxy_hosts').notNull(); + table.string('redirection_hosts').notNull(); + table.string('dead_hosts').notNull(); + table.string('streams').notNull(); + table.string('access_lists').notNull(); + table.unique('user_id'); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] user_permission Table created'); + + return knex.schema.createTable('proxy_host', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('owner_user_id').notNull().unsigned(); + table.integer('is_deleted').notNull().unsigned().defaultTo(0); + table.string('domain_name').notNull(); + table.string('forward_ip').notNull(); + table.integer('forward_port').notNull().unsigned(); + table.integer('access_list_id').notNull().unsigned().defaultTo(0); + table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); + table.string('ssl_provider').notNull().defaultTo(''); + table.integer('ssl_forced').notNull().unsigned().defaultTo(0); + table.integer('caching_enabled').notNull().unsigned().defaultTo(0); + table.integer('block_exploits').notNull().unsigned().defaultTo(0); + table.json('meta').notNull(); + table.unique(['domain_name', 'is_deleted']); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] proxy_host Table created'); + + return knex.schema.createTable('redirection_host', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('owner_user_id').notNull().unsigned(); + table.integer('is_deleted').notNull().unsigned().defaultTo(0); + table.string('domain_name').notNull(); + table.string('forward_domain_name').notNull(); + table.integer('preserve_path').notNull().unsigned().defaultTo(0); + table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); + table.string('ssl_provider').notNull().defaultTo(''); + table.integer('block_exploits').notNull().unsigned().defaultTo(0); + table.json('meta').notNull(); + table.unique(['domain_name', 'is_deleted']); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] redirection_host Table created'); + + return knex.schema.createTable('dead_host', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('owner_user_id').notNull().unsigned(); + table.integer('is_deleted').notNull().unsigned().defaultTo(0); + table.string('domain_name').notNull(); + table.integer('ssl_enabled').notNull().unsigned().defaultTo(0); + table.string('ssl_provider').notNull().defaultTo(''); + table.json('meta').notNull(); + table.unique(['domain_name', 'is_deleted']); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] dead_host Table created'); + + return knex.schema.createTable('stream', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('owner_user_id').notNull().unsigned(); + table.integer('is_deleted').notNull().unsigned().defaultTo(0); + table.integer('incoming_port').notNull().unsigned(); + table.string('forward_ip').notNull(); + table.integer('forwarding_port').notNull().unsigned(); + table.integer('tcp_forwarding').notNull().unsigned().defaultTo(0); + table.integer('udp_forwarding').notNull().unsigned().defaultTo(0); + table.json('meta').notNull(); + table.unique(['incoming_port', 'is_deleted']); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] stream Table created'); + + return knex.schema.createTable('access_list', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('owner_user_id').notNull().unsigned(); + table.integer('is_deleted').notNull().unsigned().defaultTo(0); + table.string('name').notNull(); + table.json('meta').notNull(); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] access_list Table created'); + + return knex.schema.createTable('access_list_auth', table => { + table.increments().primary(); + table.dateTime('created_on').notNull(); + table.dateTime('modified_on').notNull(); + table.integer('access_list_id').notNull().unsigned(); + table.string('username').notNull(); + table.string('password').notNull(); + table.json('meta').notNull(); + }); + }) + .then(() => { + logger.info('[' + migrate_name + '] access_list_auth Table created'); }); }; diff --git a/src/backend/models/user.js b/src/backend/models/user.js index b9a9a20..3a2ab10 100644 --- a/src/backend/models/user.js +++ b/src/backend/models/user.js @@ -3,8 +3,9 @@ 'use strict'; -const db = require('../db'); -const Model = require('objection').Model; +const db = require('../db'); +const Model = require('objection').Model; +const UserPermission = require('./user_permission'); Model.knex(db); @@ -30,6 +31,22 @@ class User extends Model { return ['roles']; } + static get relationMappings () { + return { + permissions: { + relation: Model.HasOneRelation, + modelClass: UserPermission, + join: { + from: 'user.id', + to: 'user_permission.user_id' + }, + modify: function (qb) { + qb.omit(['id', 'created_on', 'modified_on', 'user_id']); + } + } + }; + } + } module.exports = User; diff --git a/src/backend/models/user_permission.js b/src/backend/models/user_permission.js new file mode 100644 index 0000000..5848a9e --- /dev/null +++ b/src/backend/models/user_permission.js @@ -0,0 +1,30 @@ +// Objection Docs: +// http://vincit.github.io/objection.js/ + +'use strict'; + +const db = require('../db'); +const Model = require('objection').Model; + +Model.knex(db); + +class UserPermission extends Model { + $beforeInsert () { + this.created_on = Model.raw('NOW()'); + this.modified_on = Model.raw('NOW()'); + } + + $beforeUpdate () { + this.modified_on = Model.raw('NOW()'); + } + + static get name () { + return 'UserPermission'; + } + + static get tableName () { + return 'user_permission'; + } +} + +module.exports = UserPermission; diff --git a/src/backend/routes/api/users.js b/src/backend/routes/api/users.js index a42d512..070709c 100644 --- a/src/backend/routes/api/users.js +++ b/src/backend/routes/api/users.js @@ -183,12 +183,12 @@ router }); /** - * Specific user service settings + * Specific user permissions * - * /api/users/123/services + * /api/users/123/permissions */ router - .route('/:user_id/services') + .route('/:user_id/permissions') .options((req, res) => { res.sendStatus(204); }) @@ -196,18 +196,18 @@ router .all(userIdFromMe) /** - * POST /api/users/123/services + * PUT /api/users/123/permissions * - * Sets Service Settings for a user + * Set some or all permissions for a user */ - .post((req, res, next) => { + .put((req, res, next) => { apiValidator({$ref: 'endpoints/users#/links/5/schema'}, req.body) .then(payload => { payload.id = req.params.user_id; - return internalUser.setServiceSettings(res.locals.access, payload); + return internalUser.setPermissions(res.locals.access, payload); }) .then(result => { - res.status(200) + res.status(201) .send(result); }) .catch(next); diff --git a/src/backend/schema/endpoints/users.json b/src/backend/schema/endpoints/users.json index 3d82e63..1202713 100644 --- a/src/backend/schema/endpoints/users.json +++ b/src/backend/schema/endpoints/users.json @@ -206,6 +206,49 @@ "targetSchema": { "type": "boolean" } + }, + { + "title": "Set Permissions", + "description": "Sets Permissions for a User", + "href": "/users/{definitions.identity.example}/permissions", + "access": "private", + "method": "PUT", + "rel": "update", + "http_header": { + "$ref": "../examples.json#/definitions/auth_header" + }, + "schema": { + "type": "object", + "properties": { + "visibility": { + "type": "string", + "pattern": "^(all|user)$" + }, + "access_lists": { + "type": "string", + "pattern": "^(hidden|view|manage)$" + }, + "dead_hosts": { + "type": "string", + "pattern": "^(hidden|view|manage)$" + }, + "proxy_hosts": { + "type": "string", + "pattern": "^(hidden|view|manage)$" + }, + "redirection_hosts": { + "type": "string", + "pattern": "^(hidden|view|manage)$" + }, + "streams": { + "type": "string", + "pattern": "^(hidden|view|manage)$" + } + } + }, + "targetSchema": { + "type": "boolean" + } } ], "properties": { diff --git a/src/backend/setup.js b/src/backend/setup.js index b6c8f97..8a253c5 100644 --- a/src/backend/setup.js +++ b/src/backend/setup.js @@ -1,11 +1,12 @@ 'use strict'; -const fs = require('fs'); -const NodeRSA = require('node-rsa'); -const config = require('config'); -const logger = require('./logger').global; -const userModel = require('./models/user'); -const authModel = require('./models/auth'); +const fs = require('fs'); +const NodeRSA = require('node-rsa'); +const config = require('config'); +const logger = require('./logger').global; +const userModel = require('./models/user'); +const userPermissionModel = require('./models/user_permission'); +const authModel = require('./models/auth'); module.exports = function () { return new Promise((resolve, reject) => { @@ -54,7 +55,7 @@ module.exports = function () { .select(userModel.raw('COUNT(`id`) as `count`')) .where('is_deleted', 0) .first('count') - .then((row) => { + .then(row => { if (!row.count) { // Create a new user and set password logger.info('Creating a new user: admin@example.com with password: changeme'); @@ -79,6 +80,19 @@ module.exports = function () { type: 'password', secret: 'changeme', meta: {} + }) + .then(() => { + return userPermissionModel + .query() + .insert({ + user_id: user.id, + visibility: 'all', + proxy_hosts: 'manage', + redirection_hosts: 'manage', + dead_hosts: 'manage', + streams: 'manage', + access_lists: 'manage' + }); }); }); } diff --git a/src/frontend/js/app/api.js b/src/frontend/js/app/api.js index 1f23e16..b689136 100644 --- a/src/frontend/js/app/api.js +++ b/src/frontend/js/app/api.js @@ -224,6 +224,16 @@ module.exports = { */ loginAs: function (id) { return fetch('post', 'users/' + id + '/login'); + }, + + /** + * + * @param {Integer} id + * @param {Object} perms + * @returns {Promise} + */ + setPermissions: function (id, perms) { + return fetch('put', 'users/' + id + '/permissions', perms); } }, diff --git a/src/frontend/js/app/controller.js b/src/frontend/js/app/controller.js index 7e02d9c..d7ea292 100644 --- a/src/frontend/js/app/controller.js +++ b/src/frontend/js/app/controller.js @@ -52,6 +52,19 @@ module.exports = { } }, + /** + * User Permissions Form + * + * @param model + */ + showUserPermissions: function (model) { + if (Cache.User.isAdmin()) { + require(['./main', './user/permissions'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + /** * User Password Form * @@ -65,6 +78,19 @@ module.exports = { } }, + /** + * User Delete Confirm + * + * @param model + */ + showUserDeleteConfirm: function (model) { + if (Cache.User.isAdmin() && model.get('id') !== Cache.User.get('id')) { + require(['./main', './user/delete'], function (App, View) { + App.UI.showModalDialog(new View({model: model})); + }); + } + }, + /** * Error * diff --git a/src/frontend/js/app/main.js b/src/frontend/js/app/main.js index 3fe2210..b3d80bb 100644 --- a/src/frontend/js/app/main.js +++ b/src/frontend/js/app/main.js @@ -110,7 +110,7 @@ const App = Mn.Application.extend({ * @returns {Promise} */ bootstrap: function () { - return Api.Users.getById('me') + return Api.Users.getById('me', ['permissions']) .then(response => { Cache.User.set(response); Tokens.setCurrentName(response.nickname || response.name); diff --git a/src/frontend/js/app/user/delete.ejs b/src/frontend/js/app/user/delete.ejs new file mode 100644 index 0000000..b743fa9 --- /dev/null +++ b/src/frontend/js/app/user/delete.ejs @@ -0,0 +1,19 @@ +
diff --git a/src/frontend/js/app/user/delete.js b/src/frontend/js/app/user/delete.js new file mode 100644 index 0000000..a680611 --- /dev/null +++ b/src/frontend/js/app/user/delete.js @@ -0,0 +1,38 @@ +'use strict'; + +const Mn = require('backbone.marionette'); +const template = require('./delete.ejs'); +const Controller = require('../controller'); +const Api = require('../api'); +const App = require('../main'); + +require('jquery-serializejson'); + +module.exports = Mn.View.extend({ + template: template, + className: 'modal-dialog', + + ui: { + form: 'form', + buttons: '.modal-footer button', + cancel: 'button.cancel', + save: 'button.save' + }, + + events: { + + 'click @ui.save': function (e) { + e.preventDefault(); + + Api.Users.delete(this.model.get('id')) + .then(() => { + Controller.showUsers(); + App.UI.closeModal(); + }) + .catch(err => { + alert(err.message); + this.ui.buttons.prop('disabled', false).removeClass('btn-disabled'); + }); + } + } +}); diff --git a/src/frontend/js/app/user/form.ejs b/src/frontend/js/app/user/form.ejs index aa6c042..33828a3 100644 --- a/src/frontend/js/app/user/form.ejs +++ b/src/frontend/js/app/user/form.ejs @@ -25,25 +25,27 @@ - <% if (!isSelf()) { %>