LDAP server - add handling for root DSE queries (needed for Radicale 3.5.2+)
This commit is contained in:
114
app/ldap/controllers/RootDSE.controller.js
Normal file
114
app/ldap/controllers/RootDSE.controller.js
Normal file
@@ -0,0 +1,114 @@
|
||||
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
|
||||
37
app/ldap/routes/rootdse.routes.js
Normal file
37
app/ldap/routes/rootdse.routes.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const rootdse_routes = {
|
||||
|
||||
prefix: false, // false | string
|
||||
|
||||
/*
|
||||
* The Root DSE lives at the zero-length DN, which sits outside every naming
|
||||
* context by definition, so these routes must not be suffixed with the
|
||||
* server's base DC the way every other router's are. (RFC 4512 §5.1)
|
||||
*/
|
||||
absolute: true,
|
||||
|
||||
middleware: [
|
||||
'Logger'
|
||||
],
|
||||
|
||||
/*
|
||||
* No BindUser here on purpose. RFC 4512 §5.1 expects the Root DSE to be
|
||||
* readable before the client has bound, and ldap3 reads it as part of
|
||||
* connecting. Nothing it exposes is sensitive.
|
||||
*/
|
||||
search: {
|
||||
'': ['ldap_controller::RootDSE.search_root_dse'],
|
||||
},
|
||||
|
||||
bind: {},
|
||||
|
||||
add: {},
|
||||
|
||||
del: {},
|
||||
|
||||
modify: {},
|
||||
|
||||
compare: {},
|
||||
|
||||
}
|
||||
|
||||
module.exports = exports = rootdse_routes
|
||||
@@ -88,8 +88,15 @@ class LDAPRoutingUnit extends CanonicalUnit {
|
||||
route_functions.push(route_handler)
|
||||
}
|
||||
|
||||
this.output.debug(`Registering route ${type} :: ${[route_prefix, suffix].join(',')} with ${route_functions.length} handlers.`)
|
||||
this.ldap_server.server[type]([route_prefix, suffix].join(','), ...route_functions)
|
||||
// Routers marked `absolute` opt out of the base DC suffix. The Root
|
||||
// DSE needs this: its DN is the zero-length string, which sits
|
||||
// outside every naming context by definition. (RFC 4512 §5.1)
|
||||
const route_dn = instance.absolute === true
|
||||
? route_prefix
|
||||
: [route_prefix, suffix].join(',')
|
||||
|
||||
this.output.debug(`Registering route ${type} :: ${route_dn} with ${route_functions.length} handlers.`)
|
||||
this.ldap_server.server[type](route_dn, ...route_functions)
|
||||
}
|
||||
} else {
|
||||
// Unbind has a default handler, so don't warn about that.
|
||||
|
||||
Reference in New Issue
Block a user