const { Injectable } = require('flitter-di') const { ImplementationError } = require('libflitter') const LDAP = require('ldapjs') class LDAPController extends Injectable { resource_type = 'items' // TODO make use of this better check_attribute = 'ldap_visible' // or false static get services() { return [...super.services, 'ldap_server', 'configs'] } async compare(req, res, next) { if ( !req.user.can(`ldap:compare:${this.resource_type}`)) { return next(new LDAP.InsufficientAccessRightsError()) } // Make sure the resource exists const item = await this.get_resource_from_dn(req.dn) if ( !item ) { return next(LDAP.NoSuchObjectError()) } // Make sure it has the requested attribute const value = (await item.to_ldap())[req.attribute] if ( typeof value === 'undefined' ) { return next(new LDAP.NoSuchAttributeError()) } // Check if it matches the request value const values = Array.isArray(value) ? value : [value] const matches = values.some(x => x === req.value) res.end(matches) return next() } async bind(req, res, next) { const auth_dn = this.ldap_server.auth_dn() // Make sure the DN is valid if ( !req.dn.childOf(auth_dn) ) { this.output.debug(`Bind failure: ${req.dn} not in ${auth_dn}`) return next(new LDAP.InvalidCredentialsError('Cannot bind to DN outsize of the authentication base.')) } // Get the item const item = await this.get_resource_from_dn(req.dn) if ( !item ) { this.output.debug(`Bind failure: ${req.dn} not found`) return next(new LDAP.NoSuchObjectError()) } // If the object is can-able, make sure it can bind if ( typeof item.can === 'function' && !item.can('ldap:bind') ) { this.output.debug(`Bind failure: User not allowed to bind`) return next(new LDAP.InsufficientAccessRightsError()) } // Check if the credentials are an app_password const app_password_verified = Array.isArray(item.app_passwords) && item.app_passwords.length > 0 && await item.check_app_password(req.credentials) // Check if the user has MFA enabled. // If so, split the incoming password to fetch the MFA code // e.g. normalPassword:123456 if ( !app_password_verified && item.mfa_enabled ) { const parts = req.credentials.split(':') const mfa_code = parts.pop() const actual_password = parts.join(':') // Check the credentials if ( !await item.check_password(actual_password) ) { this.output.debug(`Bind failure: user w/ MFA provided invalid credentials`) return next(new LDAP.InvalidCredentialsError('Invalid credentials. Make sure MFA code is included at the end of your password (e.g. password:123456)')) } // Now, check the MFA code if ( !item.mfa_token.verify(mfa_code) ) { this.output.debug(`Bind failure: user w/ MFA provided invalid MFA token`) return next(new LDAP.InvalidCredentialsError('Invalid credentials. Verification of the MFA token failed.')) } // If not MFA, just check the credentials } else if (!app_password_verified && !await item.check_password(req.credentials)) { this.output.debug(`Bind failure: user w/ simple auth provided invalid credentials`) return next(new LDAP.InvalidCredentialsError()) } // Check if the resource has a trap. If so, deny access. if ( item.trap ) { return next(new LDAP.InvalidCredentialsError('This resource currently has a login trap set. Please visit the web UI to release.')) } this.output.info(`Successfully bound resource as DN: ${req.dn.format(this.configs.get('ldap:server.format'))}.`) res.end() return next() } async delete(req, res, next) { if ( !req.user.can(`ldap:delete:${this.resource_type}`) ) { return next(new LDAP.InsufficientAccessRightsError()) } // Get the base DN const base_dn = await this.get_base_dn() // Make sure it's a parent of the request DN if ( !base_dn.parentOf(req.dn) ) { this.output.warn(`Attempted to perform resource deletion on invalid DN: ${req.dn.format(this.configs.get('ldap:server.format'))}`) return next(new LDAP.InsufficientAccessRightsError(`Target DN must be a member of the base DN: ${base_dn.format(this.configs.get('ldap:server.format'))}.`)) } // Fetch the resource (error if not found) const item = await this.get_resource_from_dn(req.dn) if ( !item ) { return next(new LDAP.NoSuchObjectError()) } // Delete it - TODO full soft delete, or just ldap_visible = false? await item.delete() res.end() return next() } async get_resource_from_dn(dn) { throw new ImplementationError() } async get_base_dn() { throw new ImplementationError() } } module.exports = exports = LDAPController