115 lines
4.2 KiB
JavaScript
115 lines
4.2 KiB
JavaScript
const LDAPController = require('./LDAPController')
|
|
const LDAP = require('ldapjs')
|
|
|
|
/**
|
|
* Serves the Root DSE: the zero-length-DN entry clients read to discover what
|
|
* the server supports. (RFC 4512 §5.1)
|
|
*
|
|
* This is mounted on the empty DN, which ldapjs treats as a wildcard matching
|
|
* every request no other route claimed - see Server.prototype._getHandlerChain,
|
|
* which tests `suffix === ''` alongside the real DN comparisons. That makes this
|
|
* controller the server's catch-all for searches, so it also has to reproduce
|
|
* ldapjs' own noSuffixHandler for DNs that genuinely don't exist.
|
|
*/
|
|
class RootDSEController extends LDAPController {
|
|
static get services() {
|
|
return [...super.services, 'output', 'ldap_server', 'configs']
|
|
}
|
|
|
|
async search_root_dse(req, res, next) {
|
|
// The empty DN matches everything, so anything that isn't literally the
|
|
// Root DSE arrived here unrouted. Answer exactly as ldapjs would have if
|
|
// this route didn't exist.
|
|
if ( req.dn.toString() !== '' ) {
|
|
return next(new LDAP.NoSuchObjectError(`No tree found for: ${req.dn.toString()}`))
|
|
}
|
|
|
|
// The Root DSE belongs to no naming context, so only a baseObject search
|
|
// returns it. Wider scopes get an empty success rather than the directory.
|
|
if ( req.scope !== 'base' ) {
|
|
this.output.debug(`Ignoring ${req.scope} scope search on the Root DSE.`)
|
|
res.end()
|
|
return next()
|
|
}
|
|
|
|
this.output.debug(`Running base DN search for the Root DSE.`)
|
|
|
|
// Built via createSearchEntry() rather than passing a plain object to
|
|
// res.send(): that path runs ldapjs' attribute filter, which only knows
|
|
// about '*' and would drop everything here for a '+'-only request.
|
|
res.send(res.createSearchEntry({
|
|
objectName: LDAP.parseDN(''),
|
|
attributes: this.select_attributes(this.root_dse(), req.attributes),
|
|
}))
|
|
|
|
res.end()
|
|
return next()
|
|
}
|
|
|
|
/**
|
|
* Build the Root DSE attribute set.
|
|
*
|
|
* Deliberately absent - RFC 4512 §5.1 makes all of these optional, and
|
|
* advertising something unimplemented is worse than staying quiet:
|
|
*
|
|
* - supportedControl, supportedExtension, supportedFeatures: no router
|
|
* registers an exop, and no request control is honoured.
|
|
* - supportedSASLMechanisms: bind is simple-only, see LDAPController#bind.
|
|
* - subschemaSubentry: there is no cn=Subschema entry to point at. ldap3
|
|
* follows this attribute and fails the connection if the lookup misses,
|
|
* so omitting it makes clients skip the schema fetch instead.
|
|
*
|
|
* @returns {object}
|
|
*/
|
|
root_dse() {
|
|
return {
|
|
objectClass: ['top'],
|
|
namingContexts: this.naming_contexts(),
|
|
supportedLDAPVersion: ['3'],
|
|
vendorName: ['Starship CoreID'],
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The suffixes this server answers for. CoreID serves one tree, rooted at
|
|
* the configured base DC.
|
|
* @returns {string[]}
|
|
*/
|
|
naming_contexts() {
|
|
return [this.ldap_server.build_dn().format(this.configs.get('ldap:server.format'))]
|
|
}
|
|
|
|
/**
|
|
* Select the subset of the Root DSE the client actually asked for.
|
|
*
|
|
* Everything here but objectClass is an operational attribute, returned only
|
|
* on an explicit request or a wildcard. ldap3 asks for both '*' and '+'.
|
|
*
|
|
* @param {object} attributes
|
|
* @param {string[]} requested - req.attributes, lower-cased by the ldapjs parser
|
|
* @returns {object}
|
|
*/
|
|
select_attributes(attributes, requested) {
|
|
// '1.1' is the RFC 4511 §4.5.1 "no attributes" OID.
|
|
if ( requested.length === 1 && requested[0] === '1.1' ) {
|
|
return {}
|
|
}
|
|
|
|
// Naming nothing means "all user attributes", same as '*'.
|
|
if ( !requested.length || requested.includes('*') || requested.includes('+') ) {
|
|
return attributes
|
|
}
|
|
|
|
const selected = {}
|
|
for ( const name in attributes ) {
|
|
if ( requested.includes(name.toLowerCase()) ) {
|
|
selected[name] = attributes[name]
|
|
}
|
|
}
|
|
|
|
return selected
|
|
}
|
|
}
|
|
|
|
module.exports = exports = RootDSEController
|