Browse Source

Add ability to manage computers and computer groups from web interface

master
Garrett Mills 1 month ago
parent
commit
d6e4ea2e56
Signed by: garrettmills GPG Key ID: D2BF5FBA8298F246
10 changed files with 636 additions and 5 deletions
  1. +12
    -0
      app/assets/app/dash/SideBar.component.js
  2. +92
    -0
      app/assets/app/resource/ldap/Machine.resource.js
  3. +90
    -0
      app/assets/app/resource/ldap/MachineGroup.resource.js
  4. +279
    -5
      app/controllers/api/v1/LDAP.controller.js
  5. +64
    -0
      app/models/ldap/Machine.model.js
  6. +47
    -0
      app/models/ldap/MachineGroup.model.js
  7. +40
    -0
      app/routing/routers/api/v1/ldap.routes.js
  8. +8
    -0
      app/unit/LDAPServerUnit.js
  9. +2
    -0
      config/ldap/server.config.js
  10. +2
    -0
      locale/en_US/api.locale.js

+ 12
- 0
app/assets/app/dash/SideBar.component.js View File

@ -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',


+ 92
- 0
app/assets/app/resource/ldap/Machine.resource.js View File

@ -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 }

+ 90
- 0
app/assets/app/resource/ldap/MachineGroup.resource.js View File

@ -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 }

+ 279
- 5
app/controllers/api/v1/LDAP.controller.js View File

@ -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

+ 64
- 0
app/models/ldap/Machine.model.js View File

@ -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

+ 47
- 0
app/models/ldap/MachineGroup.model.js View File

@ -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

+ 40
- 0
app/routing/routers/api/v1/ldap.routes.js View File

@ -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',
],
},
}


+ 8
- 0
app/unit/LDAPServerUnit.js View File

@ -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)
}


+ 2
- 0
config/ldap/server.config.js View File

@ -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',


+ 2
- 0
locale/en_US/api.locale.js View File

@ -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.',


Loading…
Cancel
Save