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()) } const sudo_hosts = this.parse_sudo_hosts(req.filter) const iam_targets = await this.get_targets_from_hosts(sudo_hosts) 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(iam_targets)) ) { // If so, send the object res.send({ dn: user.sudo_dn.format(this.configs.get('ldap:server.format')), attributes: await user.to_sudo(iam_targets), }) } } 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(iam_targets)) ) { // If so, send the object res.send({ dn: user.sudo_dn.format(this.configs.get('ldap:server.format')), attributes: await user.to_sudo(iam_targets), }) } } } } 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(iam_targets)) ) { // If so, send the object res.send({ dn: user.sudo_dn.format(this.configs.get('ldap:server.format')), attributes: await user.to_sudo(iam_targets), }) } } } } 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() } parse_sudo_hosts(filter, target_hosts = []) { if ( Array.isArray(filter?.filters) ) { for ( const sub_filter of filter.filters ) { target_hosts = [...target_hosts, ...this.parse_sudo_hosts(sub_filter)] } } else if ( filter?.attribute ) { if ( filter.attribute === 'sudohost' ) { target_hosts.push(filter.value) } } return target_hosts.filter(Boolean) } async get_targets_from_hosts(sudo_hosts) { const Machine = this.models.get('ldap:Machine') const machines = await Machine.find({ active: true, ldap_visible: true, host_name: { $in: sudo_hosts.filter(x => x.toLowerCase() !== 'all' && x.indexOf('*') < 0), } }) return machines.map(x => x.id) } 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 ) { return this.User.findOne({uid: cn.substr(5), ldap_visible: true}) } } } module.exports = exports = SudoController