From 943c30fa9691ea05dec5f21b0742544f2825fe60 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Wed, 10 Mar 2021 23:43:16 -0600 Subject: [PATCH] Add support for sudo --- app/ldap/controllers/Sudo.controller.js | 117 ++++++++++++++++++++++++ app/ldap/routes/sudo.routes.js | 28 ++++++ app/models/auth/Group.model.js | 11 +++ app/models/auth/User.model.js | 21 +++++ app/unit/LDAPServerUnit.js | 4 + config/ldap/server.config.js | 1 + 6 files changed, 182 insertions(+) create mode 100644 app/ldap/controllers/Sudo.controller.js create mode 100644 app/ldap/routes/sudo.routes.js diff --git a/app/ldap/controllers/Sudo.controller.js b/app/ldap/controllers/Sudo.controller.js new file mode 100644 index 0000000..4f50c11 --- /dev/null +++ b/app/ldap/controllers/Sudo.controller.js @@ -0,0 +1,117 @@ +const LDAPController = require('./LDAPController') +const LDAP = require('ldapjs') + +class SudoController extends LDAPController { + static get services() { + return [ + ...super.services, + 'output', + 'ldap_server', + 'models', + 'configs', + 'auth' + ] + } + + constructor() { + super() + this.Group = this.models.get('auth:Group') + this.User = this.models.get('auth:User') + } + + // TODO flitter-orm chunk query + // TODO generalize scoped search logic + async search_sudo(req, res, next) { + if ( !req.user.can('ldap:search:sudo') ) { + return next(new LDAP.InsufficientAccessRightsError()) + } + + if ( req.scope === 'base' ) { + // If scope is base, check if the base DN matches the filter. + // If so, return it. Else, return empty. + this.output.debug(`Running base DN search for sudo with DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`) + + const user = await this.get_resource_from_dn(req.dn) + + // Make sure the user is ldap visible && match the filter + if ( user && user.ldap_visible && req.filter.matches(await user.to_sudo()) ) { + + // If so, send the object + res.send({ + dn: user.sudo_dn.format(this.configs.get('ldap:server.format')), + attributes: await user.to_sudo(), + }) + } + } else if ( req.scope === 'one' ) { + // If scope is one, find all entries that are the immediate + // subordinates of the base DN that match the filter. + this.output.debug(`Running one DN search for sudo with DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`) + + // Fetch the LDAP-visible users + const users = await this.Group.sudo_directory() + for ( const user of users ) { + + // Make sure the user os of the appropriate scope + if ( req.dn.equals(user.sudo_dn) || user.sudo_dn.parent().equals(req.dn) ) { + + // Check if the filter matches + if ( req.filter.matches(await user.to_sudo()) ) { + + // If so, send the object + res.send({ + dn: user.sudo_dn.format(this.configs.get('ldap:server.format')), + attributes: await user.to_sudo(), + }) + } + } + } + + } else if ( req.scope === 'sub' ) { + // If scope is sub, find all entries that are subordinates + // of the base DN at any level and match the filter. + this.output.debug(`Running sub DN search for sudo with DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`) + + // Fetch the users as LDAP objects + const users = await this.Group.sudo_directory() + for ( const user of users ) { + + // Make sure the user is of appropriate scope + if ( req.dn.equals(user.sudo_dn) || req.dn.parentOf(user.sudo_dn) ) { + + // Check if filter matches + if ( req.filter.matches(await user.to_sudo()) ) { + + // If so, send the object + res.send({ + dn: user.sudo_dn.format(this.configs.get('ldap:server.format')), + attributes: await user.to_sudo(), + }) + } + } + } + } else { + this.output.error(`Attempted to perform LDAP search with invalid scope: ${req.scope}`) + return next(new LDAP.OtherError('Attempted to perform LDAP search with invalid scope.')) + } + + res.end() + return next() + } + + get_cn_from_dn(dn) { + try { + if ( typeof dn === 'string' ) dn = LDAP.parseDN(dn) + return dn.rdns[0].attrs.cn.value + } catch (e) { console.log('Error parsing CN from DN', e) } + } + + async get_resource_from_dn(sudo_dn) { + const cn = this.get_cn_from_dn(sudo_dn) + if ( cn ) { + const user = this.User.findOne({uid: cn.substr(5), ldap_visible: true}) + if ( user && (await user.has_sudo()) ) return user + } + } +} + +module.exports = exports = SudoController diff --git a/app/ldap/routes/sudo.routes.js b/app/ldap/routes/sudo.routes.js new file mode 100644 index 0000000..52fee43 --- /dev/null +++ b/app/ldap/routes/sudo.routes.js @@ -0,0 +1,28 @@ +const sudo_routes = { + + prefix: false, // false | string + + middleware: [ + 'Logger' + ], + + search: { + 'ou=sudo': [ + 'ldap_middleware::BindUser', + 'ldap_controller::Sudo.search_sudo', + ], + }, + + bind: {}, + + add: {}, + + del: {}, + + modify: {}, + + compare: {}, + +} + +module.exports = exports = sudo_routes diff --git a/app/models/auth/Group.model.js b/app/models/auth/Group.model.js index 5bbe109..797c148 100644 --- a/app/models/auth/Group.model.js +++ b/app/models/auth/Group.model.js @@ -59,6 +59,17 @@ class GroupModel extends Model { } } + static async sudo_directory() { + const groups = await this.find({ ldap_visible: true, active: true, grants_sudo: true }) + + let users = [] + for ( const group of groups ) { + users = [...users, ...(await group.users())] + } + + return users + } + static async ldap_directory() { const User = this.prototype.models.get('auth:User') const groups = await this.find({ ldap_visible: true, active: true }) diff --git a/app/models/auth/User.model.js b/app/models/auth/User.model.js index 882b996..dd6f1e4 100644 --- a/app/models/auth/User.model.js +++ b/app/models/auth/User.model.js @@ -187,6 +187,23 @@ class User extends AuthUser { this.get_provider().logout(request) } + async has_sudo() { + const groups = await this.groups() + return groups.some(group => group.grants_sudo) + } + + async to_sudo() { + return { + objectClass: ['sudoRole'], + objectclass: ['sudoRole'], + cn: `sudo_${this.uid.toLowerCase()}`, + sudoUser: this.uid.toLowerCase(), + sudoHost: 'ALL', + sudoRunAs: 'ALL', + sudoCommand: 'ALL', + } + } + async to_ldap(iam_targets = []) { const Policy = this.models.get('iam:Policy') @@ -249,6 +266,10 @@ class User extends AuthUser { return LDAP.parseDN(`uid=${this.uid.toLowerCase()},${this.ldap_server.auth_dn().format(this.configs.get('ldap:server.format'))}`) } + get sudo_dn() { + return LDAP.parseDN(`cn=sudo_${this.uid.toLowerCase()},${this.ldap_server.sudo_dn().format(this.configs.get('ldap:server.format'))}`) + } + // The following are used by OpenID connect async claims(use, scope) { diff --git a/app/unit/LDAPServerUnit.js b/app/unit/LDAPServerUnit.js index 7d47638..d3f31f5 100644 --- a/app/unit/LDAPServerUnit.js +++ b/app/unit/LDAPServerUnit.js @@ -37,6 +37,10 @@ class LDAPServerUnit extends Unit { return this.build_dn(this.config.schema.group_base) } + sudo_dn() { + return this.build_dn(this.config.schema.sudo_base) + } + /** * Get the anonymous DN. * @returns {ldap/DN} diff --git a/config/ldap/server.config.js b/config/ldap/server.config.js index bf5a816..43409b4 100644 --- a/config/ldap/server.config.js +++ b/config/ldap/server.config.js @@ -15,6 +15,7 @@ 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'), + sudo_base: env('LDAP_SUDO_BASE', 'ou=sudo'), auth: { user_id: 'uid', },