From d6e4ea2e567910581c9913eae9dfbe58382e4e48 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Mon, 15 Mar 2021 16:10:23 -0500 Subject: [PATCH] Add ability to manage computers and computer groups from web interface --- app/assets/app/dash/SideBar.component.js | 12 + .../app/resource/ldap/Machine.resource.js | 92 ++++++ .../resource/ldap/MachineGroup.resource.js | 90 ++++++ app/controllers/api/v1/LDAP.controller.js | 284 +++++++++++++++++- app/models/ldap/Machine.model.js | 64 ++++ app/models/ldap/MachineGroup.model.js | 47 +++ app/routing/routers/api/v1/ldap.routes.js | 40 +++ app/unit/LDAPServerUnit.js | 8 + config/ldap/server.config.js | 2 + locale/en_US/api.locale.js | 2 + 10 files changed, 636 insertions(+), 5 deletions(-) create mode 100644 app/assets/app/resource/ldap/Machine.resource.js create mode 100644 app/assets/app/resource/ldap/MachineGroup.resource.js create mode 100644 app/models/ldap/Machine.model.js create mode 100644 app/models/ldap/MachineGroup.model.js diff --git a/app/assets/app/dash/SideBar.component.js b/app/assets/app/dash/SideBar.component.js index 6d756d1..b85eacc 100644 --- a/app/assets/app/dash/SideBar.component.js +++ b/app/assets/app/dash/SideBar.component.js @@ -60,6 +60,18 @@ export default class SideBarComponent extends Component { type: 'resource', resource: 'iam/Policy', }, + { + text: 'Computers', + action: 'list', + type: 'resource', + resource: 'ldap/Machine', + }, + { + text: 'Computer Groups', + action: 'list', + type: 'resource', + resource: 'ldap/MachineGroup', + }, { text: 'LDAP Clients', action: 'list', diff --git a/app/assets/app/resource/ldap/Machine.resource.js b/app/assets/app/resource/ldap/Machine.resource.js new file mode 100644 index 0000000..4d79c1f --- /dev/null +++ b/app/assets/app/resource/ldap/Machine.resource.js @@ -0,0 +1,92 @@ +import CRUDBase from '../CRUDBase.js' + +class MachineResource extends CRUDBase { + constructor() { + super() + + this.endpoint = '/api/v1/ldap/machines' + this.required_fields = ['name', 'description'] + this.permission_base = 'v1:ldap:machines' + + this.item = 'Computer' + this.plural = 'Computers' + + this.listing_definition = { + columns: [ + { + name: 'Machine Name', + field: 'name', + }, + { + name: 'Host Name', + field: 'host_name', + }, + { + name: 'Description', + field: 'description', + }, + ], + actions: [ + { + type: 'resource', + position: 'main', + action: 'insert', + text: 'Create New', + color: 'success', + }, + { + type: 'resource', + position: 'row', + action: 'update', + icon: 'fa fa-edit', + color: 'primary', + }, + { + type: 'resource', + position: 'row', + action: 'delete', + icon: 'fa fa-times', + color: 'danger', + confirm: true, + }, + ], + } + + this.form_definition = { + // back_action: { + // text: 'Back', + // action: 'back', + // }, + fields: [ + { + name: 'Machine Name', + field: 'name', + placeholder: 'DNS01', + required: true, + type: 'text', + }, + { + name: 'Description', + field: 'description', + required: true, + type: 'textarea', + }, + { + name: 'Location', + field: 'location', + type: 'text', + placeholder: 'Server room 1', + }, + { + name: 'Host Name (FQDN)', + field: 'host_name', + type: 'text', + placeholder: 'dns01.my.domain', + }, + ], + } + } +} + +const ldap_machine = new MachineResource() +export { ldap_machine } diff --git a/app/assets/app/resource/ldap/MachineGroup.resource.js b/app/assets/app/resource/ldap/MachineGroup.resource.js new file mode 100644 index 0000000..86f7032 --- /dev/null +++ b/app/assets/app/resource/ldap/MachineGroup.resource.js @@ -0,0 +1,90 @@ +import CRUDBase from '../CRUDBase.js' + +class MachineGroupResource extends CRUDBase { + constructor() { + super() + + this.endpoint = '/api/v1/ldap/machine-groups' + this.required_fields = ['name'] + this.permission_base = 'v1:ldap:machine_groups' + + this.item = 'Computer Group' + this.plural = 'Computer Groups' + + this.listing_definition = { + columns: [ + { + name: 'Group Name', + field: 'name', + }, + { + name: '# Computers', + field: 'machine_ids', + renderer: machine_ids => Array.isArray(machine_ids) ? machine_ids.length : 0, + }, + { + name: 'Description', + field: 'description', + }, + ], + actions: [ + { + type: 'resource', + position: 'main', + action: 'insert', + text: 'Create New', + color: 'success', + }, + { + type: 'resource', + position: 'row', + action: 'update', + icon: 'fa fa-edit', + color: 'primary', + }, + { + type: 'resource', + position: 'row', + action: 'delete', + icon: 'fa fa-times', + color: 'danger', + confirm: true, + }, + ], + } + + this.form_definition = { + // back_action: { + // text: 'Back', + // action: 'back', + // }, + fields: [ + { + name: 'Group Name', + field: 'name', + placeholder: 'DNS Servers', + required: true, + type: 'text', + }, + { + name: 'Description', + field: 'description', + type: 'textarea', + }, + { + name: 'Computers', + field: 'machine_ids', + type: 'select.dynamic.multiple', + options: { + resource: 'ldap/Machine', + display: machine => `${machine.name}${machine.host_name ? ' (' + machine.host_name + ')' : ''}`, + value: 'id', + }, + }, + ], + } + } +} + +const ldap_machinegroup = new MachineGroupResource() +export { ldap_machinegroup } diff --git a/app/controllers/api/v1/LDAP.controller.js b/app/controllers/api/v1/LDAP.controller.js index 6de2103..7027594 100644 --- a/app/controllers/api/v1/LDAP.controller.js +++ b/app/controllers/api/v1/LDAP.controller.js @@ -46,6 +46,32 @@ class LDAPController extends Controller { return res.api(data) } + async get_machines(req, res, next) { + const Machine = this.models.get('ldap:Machine') + const machines = await Machine.find({active: true}) + const data = [] + + for ( const machine of machines ) { + if ( !req.user.can(`ldap:machine:${machine.id}:view`) ) continue + data.push(await machine.to_api()) + } + + return res.api(data) + } + + async get_machine_groups(req, res, next) { + const MachineGroup = this.models.get('ldap:MachineGroup') + const groups = await MachineGroup.find({active: true}) + const data = [] + + for ( const group of groups ) { + if ( !req.user.can(`ldap:machine_group:${group.id}:view`) ) continue + data.push(await group.to_api()) + } + + return res.api(data) + } + async get_client(req, res, next) { const Client = this.models.get('ldap:Client') const client = await Client.findById(req.params.id) @@ -80,6 +106,40 @@ class LDAPController extends Controller { return res.api(await group.to_api()) } + async get_machine(req, res, next) { + const Machine = this.models.get('ldap:Machine') + const machine = await Machine.findById(req.params.id) + + if ( !machine || !machine.active ) + return res.status(404) + .message(req.T('api.machine_not_found')) + .api() + + if ( !req.user.can(`ldap:machine:${machine.id}:view`) ) + return res.status(401) + .message(req.T('api.insufficient_permissions')) + .api() + + return res.api(await machine.to_api()) + } + + async get_machine_group(req, res, next) { + const MachineGroup = this.models.get('ldap:MachineGroup') + const group = await MachineGroup.findById(req.params.id) + + if ( !group || !group.active ) + return res.status(404) + .message(req.T('api.group_not_found')) + .api() + + if ( !req.user.can(`ldap:machine_group:${group.id}:view`) ) + return res.status(401) + .message(req.T('api.insufficient_permissions')) + .api() + + return res.api(await group.to_api()) + } + async create_client(req, res, next) { if ( !req.user.can('ldap:client:create') ) return res.status(401) @@ -121,13 +181,89 @@ class LDAPController extends Controller { return res.api(await client.to_api()) } - async create_group(req, res, next) { - console.log(req.body) - if ( !req.user.can(`ldap:group:create`) ) - return res.status(401) - .message(req.T('api.insufficient_permissions')) + async create_machine(req, res, next) { + // validate inputs + const required_fields = ['name', 'description'] + for ( const field of required_fields ) { + if ( !req.body[field] ) + return res.status(400) + .message(`${req.T('api.missing_field')} ${field}`) + .api() + } + + // Make sure the machine name is free + const Machine = this.models.get('ldap:Machine') + const existing_machine = await Machine.findOne({ name: req.body.name }) + if ( existing_machine ) + return res.status(400) + .message(req.T('api.machine_already_exists')) + .api() + + const machine = new Machine({ + name: req.body.name, + description: req.body.description, + host_name: req.body.host_name, + location: req.body.location, + }) + + if ( req.body.bind_password ) { + await machine.set_bind_password(req.body.bind_password) + } + + if ( 'ldap_visible' in req.body ) { + machine.ldap_visible = !!req.body.ldap_visible + } + + await machine.save() + return res.api(await machine.to_api()) + } + + async create_machine_group(req, res, next) { + // validate inputs + const required_fields = ['name'] + for ( const field of required_fields ) { + if ( !req.body[field] ) + return res.status(400) + .message(`${req.T('api.missing_field')} ${field}`) + .api() + } + + // Make sure the machine name is free + const MachineGroup = this.models.get('ldap:MachineGroup') + const existing_group = await MachineGroup.findOne({ name: req.body.name }) + if ( existing_group ) + return res.status(400) + .message(req.T('api.group_already_exists')) .api() + const group = new MachineGroup({ + name: req.body.name, + description: req.body.description, + }) + + if ( 'ldap_visible' in req.body ) { + group.ldap_visible = !!req.body.ldap_visible + } + + const Machine = this.models.get('ldap:Machine') + const machine_ids = Array.isArray(req.body.machine_ids) ? req.body.machine_ids : [] + group.machine_ids = [] + for ( const potential of machine_ids ) { + const machine = await Machine.findOne({ + _id: Machine.to_object_id(potential), + active: true, + }) + + if ( machine ) { + group.machine_ids.push(potential) + } + } + + await group.save() + return res.api(await group.to_api()) + } + + async create_group(req, res, next) { // validate inputs const required_fields = ['role', 'name'] for ( const field of required_fields ) { @@ -240,6 +376,106 @@ class LDAPController extends Controller { return res.api() } + async update_machine(req, res, next) { + const Machine = this.models.get('ldap:Machine') + + const machine = await Machine.findById(req.params.id) + if ( !machine || !machine.active ) + return res.status(404) + .message(req.T('api.machine_not_found')) + .api() + + if ( !req.user.can(`ldap:machine:${machine.id}:update`) ) + return res.status(401) + .message(req.T('api.insufficient_permissions')) + .api() + + const required_fields = ['name', 'description'] + for ( const field of required_fields ) { + if ( !req.body[field] ) + return res.status(400) + .message(`${req.T('api.missing_field')} ${field}`) + .api() + } + + // Make sure the machine name is free + const existing_machine = await Machine.findOne({ name: req.body.name }) + if ( existing_machine && existing_machine.id !== machine.id ) + return res.status(400) + .message(req.T('api.machine_already_exists')) + .api() + + machine.name = req.body.name + machine.description = req.body.description + machine.host_name = req.body.host_name + machine.location = req.body.location + + if ( req.body.bind_password ) { + await machine.set_bind_password(req.body.bind_password) + } + + if ( 'ldap_visible' in req.body ) { + machine.ldap_visible = !!req.body.ldap_visible + } + + await machine.save() + return res.api(await machine.to_api()) + } + + async update_machine_group(req, res, next) { + const MachineGroup = this.models.get('ldap:MachineGroup') + + const group = await MachineGroup.findById(req.params.id) + if ( !group || !group.active ) + return res.status(404) + .message(req.T('api.group_not_found')) + .api() + + if ( !req.user.can(`ldap:machine_group:${group.id}:update`) ) + return res.status(401) + .message(req.T('api.insufficient_permissions')) + .api() + + const required_fields = ['name'] + for ( const field of required_fields ) { + if ( !req.body[field] ) + return res.status(400) + .message(`${req.T('api.missing_field')} ${field}`) + .api() + } + + // Make sure the machine name is free + const existing_group = await MachineGroup.findOne({ name: req.body.name }) + if ( existing_group && existing_group.id !== group.id ) + return res.status(400) + .message(req.T('api.group_already_exists')) + .api() + + group.name = req.body.name + group.description = req.body.description + + if ( 'ldap_visible' in req.body ) { + group.ldap_visible = !!req.body.ldap_visible + } + + const Machine = this.models.get('ldap:Machine') + const machine_ids = Array.isArray(req.body.machine_ids) ? req.body.machine_ids : [] + group.machine_ids = [] + for ( const potential of machine_ids ) { + const machine = await Machine.findOne({ + _id: Machine.to_object_id(potential), + active: true, + }) + + if ( machine ) { + group.machine_ids.push(potential) + } + } + + await group.save() + return res.api(await group.to_api()) + } + async update_group(req, res, next) { const User = await this.models.get('auth:User') const Group = await this.models.get('ldap:Group') @@ -337,6 +573,44 @@ class LDAPController extends Controller { await group.save() return res.api() } + + async delete_machine(req, res, next) { + const Machine = this.models.get('ldap:Machine') + const machine = await Machine.findById(req.params.id) + + if ( !machine || !machine.active ) + return res.status(404) + .message(req.T('api.machine_not_found')) + .api() + + if ( !req.user.can(`ldap:machine:${machine.id}:delete`) ) + return res.status(401) + .message(req.T('api.insufficient_permissions')) + .api() + + machine.active = false + await machine.save() + return res.api() + } + + async delete_machine_group(req, res, next) { + const MachineGroup = this.models.get('ldap:MachineGroup') + const group = await MachineGroup.findById(req.params.id) + + if ( !group || !group.active ) + return res.status(404) + .message(req.T('api.group_not_found')) + .api() + + if ( !req.user.can(`ldap:machine_group:${group.id}:delete`) ) + return res.status(401) + .message(req.T('api.insufficient_permissions')) + .api() + + group.active = false + await group.save() + return res.api() + } } module.exports = exports = LDAPController diff --git a/app/models/ldap/Machine.model.js b/app/models/ldap/Machine.model.js new file mode 100644 index 0000000..d3f9bea --- /dev/null +++ b/app/models/ldap/Machine.model.js @@ -0,0 +1,64 @@ +const { Model } = require('flitter-orm') +const LDAP = require('ldapjs') +const bcrypt = require('bcrypt') + +class MachineModel extends Model { + static get services() { + return [...super.services, 'models', 'ldap_server', 'configs'] + } + + static get schema() { + return { + name: String, + bind_password: String, + description: String, + host_name: String, + location: String, + active: { type: Boolean, default: true }, + ldap_visible: { type: Boolean, default: true }, + } + } + + async to_api() { + return { + id: this.id, + name: this.name, + description: this.description, + host_name: this.host_name, + location: this.location, + ldap_visible: this.ldap_visible, + } + } + + async set_bind_password(password) { + this.bind_password = await bcrypt.hash(password, 10) + return this + } + + async check_bind_password(password) { + return await bcrypt.compare(password, this.bind_password) + } + + get dn() { + return LDAP.parseDN(`cn=${this.name},${this.ldap_server.machine_dn().format(this.configs.get('ldap:server.format'))}`) + } + + async to_ldap() { + const data = { + cn: this.name, + dn: this.dn.format(this.configs.get('ldap:server.format')), + name: this.name, + id: this.id, + objectClass: ['computer'], + description: this.description, + dNSHostName: this.host_name, + location: this.location, + primaryGroupID: 515, // compat with AD + sAMAccountType: 805306369, // compat with AD + } + + return data; + } +} + +module.exports = exports = MachineModel diff --git a/app/models/ldap/MachineGroup.model.js b/app/models/ldap/MachineGroup.model.js new file mode 100644 index 0000000..7c4fa27 --- /dev/null +++ b/app/models/ldap/MachineGroup.model.js @@ -0,0 +1,47 @@ +const { Model } = require('flitter-orm') +const uuid = require('uuid').v4 +const LDAP = require('ldapjs') + +class MachineGroupModel extends Model { + static get services() { + return [...super.services, 'models', 'ldap_server', 'configs'] + } + + static get schema() { + return { + name: String, + description: String, + UUID: { type: String, default: uuid }, + active: { type: Boolean, default: true }, + machine_ids: [String], + ldap_visible: { type: Boolean, default: true }, + } + } + + async to_api() { + return { + id: this.id, + name: this.name, + description: this.description || '', + UUID: this.UUID, + machine_ids: this.machine_ids, + ldap_visible: this.ldap_visible, + } + } + + get dn() { + return LDAP.parseDN(`cn=${this.name},${this.ldap_server.machine_group_dn().format(this.configs.get('ldap:server.format'))}`) + } + + async to_ldap() { + return { + cn: this.name, + dn: this.dn.format(this.configs.get('ldap:server.format')), + id: this.id, + uuid: this.UUID, + description: this.description, + } + } +} + +module.exports = exports = MachineGroupModel diff --git a/app/routing/routers/api/v1/ldap.routes.js b/app/routing/routers/api/v1/ldap.routes.js index 4a6b529..84fdd6c 100644 --- a/app/routing/routers/api/v1/ldap.routes.js +++ b/app/routing/routers/api/v1/ldap.routes.js @@ -22,6 +22,22 @@ const ldap_routes = { ['middleware::api:Permission', { check: 'v1:ldap:groups:get' }], 'controller::api:v1:LDAP.get_group', ], + '/machines': [ + ['middleware::api:Permission', { check: 'v1:ldap:machines:list' }], + 'controller::api:v1:LDAP.get_machines', + ], + '/machines/:id': [ + ['middleware::api:Permission', { check: 'v1:ldap:machines:get' }], + 'controller::api:v1:LDAP.get_machine', + ], + '/machine-groups': [ + ['middleware::api:Permission', { check: 'v1:ldap:machine_groups:list' }], + 'controller::api:v1:LDAP.get_machine_groups', + ], + '/machine-groups/:id': [ + ['middleware::api:Permission', { check: 'v1:ldap:machine_groups:get' }], + 'controller::api:v1:LDAP.get_machine_group', + ], '/config': [ ['middleware::api:Permission', { check: 'v1:ldap:config:get' }], 'controller::api:v1:LDAP.get_config', @@ -37,6 +53,14 @@ const ldap_routes = { ['middleware::api:Permission', { check: 'v1:ldap:groups:create' }], 'controller::api:v1:LDAP.create_group', ], + '/machines': [ + ['middleware::api:Permission', { check: 'v1:ldap:machines:create' }], + 'controller::api:v1:LDAP.create_machine', + ], + '/machine-groups': [ + ['middleware::api:Permission', { check: 'v1:ldap:machine_groups:create' }], + 'controller::api:v1:LDAP.create_machine_group', + ], }, patch: { @@ -48,6 +72,14 @@ const ldap_routes = { ['middleware::api:Permission', { check: 'v1:ldap:groups:update' }], 'controller::api:v1:LDAP.update_group', ], + '/machines/:id': [ + ['middleware::api:Permission', { check: 'v1:ldap:machines:update' }], + 'controller::api:v1:LDAP.update_machine', + ], + '/machine-groups/:id': [ + ['middleware::api:Permission', { check: 'v1:ldap:machine_groups:update' }], + 'controller::api:v1:LDAP.update_machine_group', + ], }, delete: { @@ -59,6 +91,14 @@ const ldap_routes = { ['middleware::api:Permission', { check: 'v1:ldap:groups:delete' }], 'controller::api:v1:LDAP.delete_group', ], + '/machines/:id': [ + ['middleware::api:Permission', { check: 'v1:ldap:machines:delete' }], + 'controller::api:v1:LDAP.delete_machine', + ], + '/machine-groups/:id': [ + ['middleware::api:Permission', { check: 'v1:ldap:machine_groups:delete' }], + 'controller::api:v1:LDAP.delete_machine_group', + ], }, } diff --git a/app/unit/LDAPServerUnit.js b/app/unit/LDAPServerUnit.js index d3f31f5..06229d6 100644 --- a/app/unit/LDAPServerUnit.js +++ b/app/unit/LDAPServerUnit.js @@ -37,6 +37,14 @@ class LDAPServerUnit extends Unit { return this.build_dn(this.config.schema.group_base) } + machine_dn() { + return this.build_dn(this.config.schema.machine_base) + } + + machine_group_dn() { + return this.build_dn(this.config.schema.machine_group_base) + } + sudo_dn() { return this.build_dn(this.config.schema.sudo_base) } diff --git a/config/ldap/server.config.js b/config/ldap/server.config.js index 43409b4..73404de 100644 --- a/config/ldap/server.config.js +++ b/config/ldap/server.config.js @@ -15,6 +15,8 @@ const ldap_server = { base_dc: env('LDAP_BASE_DC', 'dc=example,dc=com'), authentication_base: env('LDAP_AUTH_BASE', 'ou=people'), group_base: env('LDAP_GROUP_BASE', 'ou=groups'), + machine_base: env('LDAP_MACHINE_BASE', 'ou=computers'), + machine_group_base: env('LDAP_MACHINE_BASE', 'ou=computer groups'), sudo_base: env('LDAP_SUDO_BASE', 'ou=sudo'), auth: { user_id: 'uid', diff --git a/locale/en_US/api.locale.js b/locale/en_US/api.locale.js index 3e68299..ec17173 100644 --- a/locale/en_US/api.locale.js +++ b/locale/en_US/api.locale.js @@ -3,7 +3,9 @@ module.exports = exports = { application_already_exists: 'An Application with that identifier already exists.', group_not_found: 'Group not found with that ID.', + machine_not_found: 'Machine not found with that ID.', group_already_exists: 'A group with that name already exists.', + machine_already_exists: 'A machine with that name already exists.', user_not_found: 'User not found with that ID.', user_already_exists: 'A user with that identifier already exists.',