2020-04-17 00:59:48 +00:00
const { Injectable } = require ( 'flitter-di' )
2020-04-21 03:46:19 +00:00
const { ImplementationError } = require ( 'libflitter' )
const LDAP = require ( 'ldapjs' )
2020-04-17 00:59:48 +00:00
class LDAPController extends Injectable {
2020-04-21 03:46:19 +00:00
resource _type = 'items'
2020-04-17 00:59:48 +00:00
2020-04-21 03:46:19 +00:00
// TODO make use of this better
check _attribute = 'ldap_visible' // or false
static get services ( ) {
2020-05-04 01:16:54 +00:00
return [ ... super . services , 'ldap_server' , 'configs' ]
2020-04-21 03:46:19 +00:00
}
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
2020-05-04 01:16:54 +00:00
const value = ( await item . to _ldap ( ) ) [ req . attribute ]
2020-04-21 03:46:19 +00:00
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 ) ) {
2020-05-04 01:16:54 +00:00
this . output . debug ( ` Bind failure: ${ req . dn } not in ${ auth _dn } ` )
2020-04-21 03:46:19 +00:00
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 ) {
2020-05-04 01:16:54 +00:00
this . output . debug ( ` Bind failure: ${ req . dn } not found ` )
2020-04-21 03:46:19 +00:00
return next ( new LDAP . NoSuchObject ( ) )
}
// If the object is can-able, make sure it can bind
if ( typeof item . can === 'function' && ! item . can ( 'ldap:bind' ) ) {
2020-05-04 01:16:54 +00:00
this . output . debug ( ` Bind failure: User not allowed to bind ` )
2020-04-21 03:46:19 +00:00
return next ( new LDAP . InsufficientAccessRightsError ( ) )
}
2020-05-04 01:16:54 +00:00
// 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 ` )
2020-04-21 03:46:19 +00:00
return next ( new LDAP . InvalidCredentialsError ( ) )
}
2020-05-04 01:16:54 +00:00
this . output . info ( ` Successfully bound resource as DN: ${ req . dn . format ( this . configs . get ( 'ldap:server.format' ) ) } . ` )
2020-04-21 03:46:19 +00:00
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 ) ) {
2020-05-04 01:16:54 +00:00
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' ) ) } . ` ) )
2020-04-21 03:46:19 +00:00
}
// 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 ( )
}
2020-04-17 00:59:48 +00:00
}
module . exports = exports = LDAPController