const LDAPController = require('./LDAPController') const LDAP = require('ldapjs') const bcrypt = require('bcrypt') class UsersController extends LDAPController { static get services() { return [ ...super.services, 'output', 'ldap_server', 'models', 'configs', 'auth' ] } constructor() { super() this.User = this.models.get('auth:User') } // Might need to override compare to support special handling for userPassword // TODO generalize some of the addition logic // TODO rework some of the registration and validation logic async add_people(req, res, next) { const Setting = this.models.get('Setting') if ( !(await Setting.get('auth.allow_registration')) ) { return next(new LDAP.InsufficientAccessRightsError('Operation not enabled.')) } if ( !req.user.can('ldap:add:users') ) { return next(new LDAP.InsufficientAccessRightsError()) } // make sure the add DN is in the auth_dn const auth_dn = this.ldap_server.auth_dn() if ( !auth_dn.parentOf(req.dn) ) { this.output.warn(`Attempted to perform user insertion on invalid DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`) return next(new LDAP.InsufficientAccessRightsError()) } // make sure the user object doesn't already exist const existing_user = await this.get_resource_from_dn(req.dn) if ( existing_user ) { return next(new LDAP.EntryAlreadyExistsError()) } // build the user object from the request attributes const req_data = req.toObject().attributes const register_data = { first_name: req_data.cn ? req_data.cn[0] : '', last_name: req_data.sn ? req_data.sn[0] : '', email: req_data.mail ? req_data.mail[0] : '', username: req_data.uid ? req_data.uid[0].toLowerCase() : '', password: req_data.userpassword ? req_data.userpassword[0] : '', } // TODO add data fields // Make sure the data uid matches the request DN if ( this.get_uid_from_dn(req.dn) !== register_data.username ) { this.output.error(`Attempted to register user where request DN's UID differs from the registration data: (${this.get_uid_from_dn(req.dn)} !== ${register_data.username})`) return next(new LDAP.ObjectclassViolationError(`Attempted to register user where request DN's UID differs from the registration data!`)) } // Get the auth provider! const flitter = this.auth.get_provider('flitter') // Make sure required fields are provided const reg_errors = await flitter.validate_registration(register_data) const other_required_fields = ['first_name', 'last_name', 'email'] for ( const field of other_required_fields ) { if ( !register_data[field] ) reg_errors.push(`Missing field: ${field}`) } if ( reg_errors.length > 0 ) { this.output.error(`Error(s) encountered during LDAP user registration: ${reg_errors.join('; ')}`) return next(new LDAP.ObjectclassViolationError(`Object add validation failure: ${reg_errors.join('; ')}`)) } // save the user object const registration_args = await flitter.get_registration_args(register_data) delete register_data.username delete register_data.password registration_args[1] = {...register_data, ...registration_args[1]} const new_user = await flitter.register(...registration_args) this.output.success(`Created new LDAP user: ${new_user.uid}`) res.end() return next() } // TODO generalize some of the modification logic // TODO rework validation async modify_people(req, res, next) { if ( !req.user.can('ldap:modify:users') ) { return next(new LDAP.InsufficientAccessRightsError()) } if ( req.changes.length <= 0 ) { return next(new LDAP.ProtocolError('Must specify at least one change.')) } // Make sure it's under the user auth DN const auth_dn = this.ldap_server.auth_dn() if ( !auth_dn.parentOf(req.dn) ) { this.output.warn(`Attempted to perform user modify on invalid DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`) return next(new LDAP.InsufficientAccessRightsError()) } // Get the target user const user = await this.get_resource_from_dn(req.dn) if ( !user ) { return next(new LDAP.NoSuchObjectError()) } // Iterate over the changes const allow_replace = ['cn', 'sn', 'mail', 'userPassword'] for ( const change of req.changes ) { const change_data = change.json.modification if ( change.operation === 'add' ) { if ( !change_data.type.startsWith('data_') ) { return next(new LDAP.OperationsError(`Addition of non 'data_' fields is not permitted.`)) } const key = `ldap${change_data.type.substr(4)}` const val = change_data.vals if ( typeof user.data_get(key) !== 'undefined' ) { return next(new LDAP.AttributeOrValueExistsError()) } user.data_set(key, val) } else if ( change.operation === 'replace' ) { if ( !allow_replace.includes(change_data.type) || change_data.type.startsWith('data_') ) { return next(new LDAP.OperationsError(`Modification of the ${change_data.type} field is not permitted.`)) } if ( change_data.type.startsWith('data_') ) { const key = `ldap${change_data.type.substr(4)}` const val = change_data.vals user.data_set(key, val) } else if ( change_data.type === 'cn' ) { user.first_name = change_data.vals[0] } else if ( change_data.type === 'sn' ) { user.last_name = change_data.vals[0] } else if ( change_data.type === 'mail' ) { // Validate the e-mail address if ( !this.ldap_server.validate_email(change_data.vals[0]) ) { return next(new LDAP.OperationsError(`Unable to make modification: mail must be a valid e-mail address.`)) } user.email = change_data.vals[0] } else if ( change_data.type === 'userPassword' ) { // Validate the password if ( change_data.vals[0].length < 8 ) { return next(new LDAP.OperationsError(`Unable to make modification: userPassword must be at least 8 characters.`)) } // Hash the password before update user.password = await bcrypt.hash(change_data.vals[0], 10) } } else if ( change.operation === 'delete' ) { if ( !change_data.type.startsWith('data_') ) { return next(new LDAP.OperationsError(`Deletion of non 'data_' fields is not permitted.`)) } const key = `ldap${change_data.type.substr(4)}` const json_obj = JSON.parse(user.data) delete json_obj[key] user.data = JSON.stringify(json_obj) } else { return next(new LDAP.ProtocolError(`Invalid/unknown modify operation: ${change.operation}`)) } } await user.save() res.end() return next() } async get_base_dn() { return this.ldap_server.auth_dn() } parse_iam_targets(filter, target_ids = []) { if ( Array.isArray(filter?.filters) ) { for ( const sub_filter of filter.filters ) { target_ids = [...target_ids, ...this.parse_iam_targets(sub_filter)] } } else if ( filter?.attribute ) { if ( filter.attribute === 'iamtarget' ) { target_ids.push(filter.value) } } return target_ids } filter_to_obj(filter) { if ( filter && filter.json ) { const val = filter.json for ( const prop in val ) { if ( !val.hasOwnProperty(prop) ) continue val[prop] = this.filter_to_obj(val[prop]) } return val } else if ( Array.isArray(filter) ) { return filter.map(x => this.filter_to_obj(x)) } return filter } // TODO flitter-orm chunk query // TODO generalize scoped search logic async search_people(req, res, next) { if ( !req.user.can('ldap:search:users:me') ) { return next(new LDAP.InsufficientAccessRightsError()) } const can_search_all = req.user.can('ldap:search:users') const iam_targets = this.parse_iam_targets(req.filter) 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 users 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_ldap(iam_targets)) && (req.user.id === user.id || can_search_all) ) { // If so, send the object res.send({ dn: user.dn,//.format(this.configs.get('ldap:server.format')), attributes: await user.to_ldap(iam_targets), }) this.output.debug({ dn: user.dn.format(this.configs.get('ldap:server.format')), attributes: await user.to_ldap(iam_targets), }) } else { this.output.debug(`User base search failed: either user not found, not visible, or filter mismatch`) global.ireq = req } } 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 users with DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`) // Fetch the LDAP-visible users const users = await this.User.ldap_directory() for ( const user of users ) { if ( user.id !== req.user.id && !can_search_all ) continue // Make sure the user os of the appropriate scope if ( req.dn.equals(user.dn) || user.dn.parent().equals(req.dn) ) { // Check if the filter matches if ( req.filter.matches(await user.to_ldap(iam_targets)) ) { // If so, send the object res.send({ dn: user.dn,//.format(this.configs.get('ldap:server.format')), attributes: await user.to_ldap(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 users with DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`) // Fetch the users as LDAP objects const users = await this.User.ldap_directory() this.output.debug(`Searching ${users.length} users...`) this.output.debug(`Request DN: ${req.dn}`) this.output.debug(`Filter:`) this.output.debug(this.filter_to_obj(req.filter.json)) for ( const user of users ) { if ( user.id !== req.user.id && !can_search_all ) continue this.output.debug(`Checking ${user.uid}...`) this.output.debug(`DN: ${user.dn}`) this.output.debug(`Req DN equals: ${req.dn.equals(user.dn)}`) this.output.debug(`Req DN parent of: ${req.dn.parentOf(user.dn)}`) // Make sure the user is of appropriate scope if ( req.dn.equals(user.dn) || req.dn.parentOf(user.dn) ) { this.output.debug(await user.to_ldap()) this.output.debug(`Matches sub scope. Matches filter? ${req.filter.matches(await user.to_ldap(iam_targets))}`) // Check if filter matches if ( req.filter.matches(await user.to_ldap(iam_targets)) ) { // If so, send the object res.send({ dn: user.dn,//.format(this.configs.get('ldap:server.format')), attributes: await user.to_ldap(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() } get_uid_from_dn(dn) { const uid_field = this.ldap_server.config.schema.auth.user_id try { if ( typeof dn === 'string' ) dn = LDAP.parseDN(dn) return dn.rdns[0].attrs[uid_field].value.toLowerCase() } catch (e) {} } async get_resource_from_dn(dn) { const uid = this.get_uid_from_dn(dn) if ( uid ) { const User = this.models.get('auth:User') return User.findOne({uid: uid.toLowerCase(), ldap_visible: true}) } } } module.exports = exports = UsersController