2020-04-18 00:25:33 +00:00
const LDAPController = require ( './LDAPController' )
const LDAP = require ( 'ldapjs' )
2020-04-21 03:46:19 +00:00
const bcrypt = require ( 'bcrypt' )
2020-04-18 00:25:33 +00:00
class UsersController extends LDAPController {
static get services ( ) {
2020-04-21 03:46:19 +00:00
return [
... super . services ,
'output' ,
'ldap_server' ,
'models' ,
2020-05-04 01:16:54 +00:00
'configs' ,
2020-04-21 03:46:19 +00:00
'auth'
]
2020-04-18 00:25:33 +00:00
}
2020-04-21 03:46:19 +00:00
constructor ( ) {
super ( )
this . User = this . models . get ( 'auth:User' )
2020-04-18 00:25:33 +00:00
}
2020-04-21 03:46:19 +00:00
// Might need to override compare to support special handling for userPassword
// TODO generalize some of the addition logic
2020-05-21 01:35:17 +00:00
// TODO rework some of the registration and validation logic
2020-04-21 03:46:19 +00:00
async add _people ( req , res , next ) {
2020-05-21 01:35:17 +00:00
const Setting = this . models . get ( 'Setting' )
if ( ! ( await Setting . get ( 'auth.allow_registration' ) ) ) {
return next ( new LDAP . InsufficientAccessRightsError ( 'Operation not enabled.' ) )
}
2020-04-21 03:46:19 +00:00
if ( ! req . user . can ( 'ldap:add:users' ) ) {
return next ( new LDAP . InsufficientAccessRightsError ( ) )
}
// make sure the add DN is in the auth_dn
2020-04-18 00:25:33 +00:00
const auth _dn = this . ldap _server . auth _dn ( )
2020-04-21 03:46:19 +00:00
if ( ! auth _dn . parentOf ( req . dn ) ) {
2020-05-04 01:16:54 +00:00
this . output . warn ( ` Attempted to perform user insertion on invalid DN: ${ req . dn . format ( this . configs . get ( 'ldap:server.format' ) ) } ` )
2020-04-21 03:46:19 +00:00
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 ] : '' ,
2020-10-19 04:27:23 +00:00
username : req _data . uid ? req _data . uid [ 0 ] . toLowerCase ( ) : '' ,
2020-04-21 03:46:19 +00:00
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! ` ) )
}
2020-04-18 00:25:33 +00:00
2020-04-21 03:46:19 +00:00
// 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
2020-05-21 01:35:17 +00:00
// TODO rework validation
2020-04-21 03:46:19 +00:00
async modify _people ( req , res , next ) {
if ( ! req . user . can ( 'ldap:modify:users' ) ) {
return next ( new LDAP . InsufficientAccessRightsError ( ) )
2020-04-18 00:25:33 +00:00
}
2020-04-21 03:46:19 +00:00
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 ) ) {
2020-05-04 01:16:54 +00:00
this . output . warn ( ` Attempted to perform user modify on invalid DN: ${ req . dn . format ( this . configs . get ( 'ldap:server.format' ) ) } ` )
2020-04-21 03:46:19 +00:00
return next ( new LDAP . InsufficientAccessRightsError ( ) )
}
// Get the target user
const user = await this . get _resource _from _dn ( req . dn )
2020-04-18 00:25:33 +00:00
if ( ! user ) {
2020-04-21 03:46:19 +00:00
return next ( new LDAP . NoSuchObjectError ( ) )
2020-04-18 00:25:33 +00:00
}
2020-04-21 03:46:19 +00:00
// 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 } ` ) )
}
2020-04-18 00:25:33 +00:00
}
2020-04-21 03:46:19 +00:00
await user . save ( )
res . end ( )
return next ( )
}
async get _base _dn ( ) {
return this . ldap _server . auth _dn ( )
}
2020-05-21 03:28:30 +00:00
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
}
2020-08-20 13:03:07 +00:00
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
2020-08-20 13:04:31 +00:00
} else if ( Array . isArray ( filter ) ) {
return filter . map ( x => this . filter _to _obj ( x ) )
2020-08-20 13:03:07 +00:00
}
return filter
}
2020-04-21 03:46:19 +00:00
// TODO flitter-orm chunk query
// TODO generalize scoped search logic
async search _people ( req , res , next ) {
2020-10-18 22:06:34 +00:00
if ( ! req . user . can ( 'ldap:search:users:me' ) ) {
2020-04-18 00:25:33 +00:00
return next ( new LDAP . InsufficientAccessRightsError ( ) )
}
2020-10-18 22:06:34 +00:00
const can _search _all = req . user . can ( 'ldap:search:users' )
2020-05-21 03:28:30 +00:00
const iam _targets = this . parse _iam _targets ( req . filter )
2020-04-21 03:46:19 +00:00
if ( req . scope === 'base' ) {
// If scope is base, check if the base DN matches the filter.
// If so, return it. Else, return empty.
2020-05-04 01:16:54 +00:00
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 )
2020-04-21 03:46:19 +00:00
// Make sure the user is ldap visible && match the filter
2020-10-18 22:06:34 +00:00
if (
user
&& user . ldap _visible
&& req . filter . matches ( await user . to _ldap ( iam _targets ) )
&& ( req . user . id === user . id || can _search _all )
) {
2020-04-21 03:46:19 +00:00
// If so, send the object
res . send ( {
2020-05-04 01:16:54 +00:00
dn : user . dn , //.format(this.configs.get('ldap:server.format')),
2020-05-21 03:28:30 +00:00
attributes : await user . to _ldap ( iam _targets ) ,
2020-05-04 01:16:54 +00:00
} )
this . output . debug ( {
dn : user . dn . format ( this . configs . get ( 'ldap:server.format' ) ) ,
2020-05-21 03:28:30 +00:00
attributes : await user . to _ldap ( iam _targets ) ,
2020-04-21 03:46:19 +00:00
} )
2020-05-04 01:16:54 +00:00
} else {
this . output . debug ( ` User base search failed: either user not found, not visible, or filter mismatch ` )
global . ireq = req
2020-04-21 03:46:19 +00:00
}
} 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.
2020-05-04 01:16:54 +00:00
this . output . debug ( ` Running one DN search for users with DN: ${ req . dn . format ( this . configs . get ( 'ldap:server.format' ) ) } ` )
2020-04-21 03:46:19 +00:00
// Fetch the LDAP-visible users
const users = await this . User . ldap _directory ( )
for ( const user of users ) {
2020-10-18 22:06:34 +00:00
if ( user . id !== req . user . id && ! can _search _all ) continue
2020-04-21 03:46:19 +00:00
// 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
2020-05-21 03:28:30 +00:00
if ( req . filter . matches ( await user . to _ldap ( iam _targets ) ) ) {
2020-04-21 03:46:19 +00:00
// If so, send the object
res . send ( {
2020-05-04 01:16:54 +00:00
dn : user . dn , //.format(this.configs.get('ldap:server.format')),
2020-05-21 03:28:30 +00:00
attributes : await user . to _ldap ( iam _targets ) ,
2020-04-21 03:46:19 +00:00
} )
}
}
}
} 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.
2020-05-04 01:16:54 +00:00
this . output . debug ( ` Running sub DN search for users with DN: ${ req . dn . format ( this . configs . get ( 'ldap:server.format' ) ) } ` )
2020-04-21 03:46:19 +00:00
// Fetch the users as LDAP objects
const users = await this . User . ldap _directory ( )
2020-08-20 12:55:06 +00:00
this . output . debug ( ` Searching ${ users . length } users... ` )
this . output . debug ( ` Request DN: ${ req . dn } ` )
this . output . debug ( ` Filter: ` )
2020-08-20 13:03:07 +00:00
this . output . debug ( this . filter _to _obj ( req . filter . json ) )
2020-04-21 03:46:19 +00:00
for ( const user of users ) {
2020-10-18 22:06:34 +00:00
if ( user . id !== req . user . id && ! can _search _all ) continue
2020-08-20 12:58:01 +00:00
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 ) } ` )
2020-04-21 03:46:19 +00:00
// Make sure the user is of appropriate scope
if ( req . dn . equals ( user . dn ) || req . dn . parentOf ( user . dn ) ) {
2020-10-19 02:44:53 +00:00
this . output . debug ( await user . to _ldap ( ) )
2020-08-20 12:59:36 +00:00
this . output . debug ( ` Matches sub scope. Matches filter? ${ req . filter . matches ( await user . to _ldap ( iam _targets ) ) } ` )
2020-04-21 03:46:19 +00:00
// Check if filter matches
2020-05-21 03:28:30 +00:00
if ( req . filter . matches ( await user . to _ldap ( iam _targets ) ) ) {
2020-04-21 03:46:19 +00:00
// If so, send the object
res . send ( {
2020-05-04 01:16:54 +00:00
dn : user . dn , //.format(this.configs.get('ldap:server.format')),
2020-05-21 03:28:30 +00:00
attributes : await user . to _ldap ( iam _targets ) ,
2020-04-21 03:46:19 +00:00
} )
}
}
}
} 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 ( )
2020-04-18 00:25:33 +00:00
}
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 )
2020-10-19 04:27:23 +00:00
return dn . rdns [ 0 ] . attrs [ uid _field ] . value . toLowerCase ( )
2020-04-18 00:25:33 +00:00
} catch ( e ) { }
}
2020-04-21 03:46:19 +00:00
async get _resource _from _dn ( dn ) {
2020-04-18 00:25:33 +00:00
const uid = this . get _uid _from _dn ( dn )
if ( uid ) {
const User = this . models . get ( 'auth:User' )
2020-10-19 04:27:23 +00:00
return User . findOne ( { uid : uid . toLowerCase ( ) , ldap _visible : true } )
2020-04-18 00:25:33 +00:00
}
}
}
module . exports = exports = UsersController