@mashroom/mashroom-security-provider-ldap
Advanced tools
Comparing version 2.6.1 to 2.7.0
@@ -7,9 +7,33 @@ "use strict"; | ||
exports.default = void 0; | ||
var _ldapjs = require("ldapjs"); | ||
var _ldapts = require("ldapts"); | ||
const DEFAULT_ATTRIBUTES = ['dn', 'cn', 'sn', 'givenName', 'displayName', 'uid', 'mail']; | ||
// See https://ldap.com/ldap-result-code-reference/ | ||
const ERROR_NO_SUCH_OBJECT = 32; | ||
const getAttributeValue = (name, attributes) => { | ||
return attributes.find(({ | ||
type | ||
}) => type === name)?.values?.[0]; | ||
if (!(name in attributes)) { | ||
return undefined; | ||
} | ||
let value = attributes[name]; | ||
if (Array.isArray(value)) { | ||
value = value[0]; | ||
} | ||
if (typeof value === 'string') { | ||
return value; | ||
} | ||
return value?.toString(); | ||
}; | ||
const getAttributeValues = (name, attributes) => { | ||
if (!(name in attributes)) { | ||
return undefined; | ||
} | ||
const value = attributes[name]; | ||
const values = Array.isArray(value) ? value : [value]; | ||
return values.map(value => { | ||
if (typeof value === 'string') { | ||
return value; | ||
} | ||
return value?.toString(); | ||
}); | ||
}; | ||
class LdapClientImpl { | ||
@@ -38,3 +62,3 @@ constructor(_serverUrl, _connectTimeout, _timeout, _baseDN, _bindDN, _bindCredentials, _tlsOptions, loggerFactory) { | ||
// For some reason in LdapJS 3 the attribute names need now to be lower case | ||
// For some reason in the attribute names need now to be lower case | ||
attributes = attributes.map(a => a.toLowerCase()); | ||
@@ -46,63 +70,51 @@ const searchOpts = { | ||
}; | ||
return new Promise((resolve, reject) => { | ||
const entries = []; | ||
searchClient.search(this._baseDN, searchOpts, (err, res) => { | ||
if (err) { | ||
reject(err); | ||
return; | ||
let result; | ||
try { | ||
result = await searchClient.search(this._baseDN, searchOpts); | ||
} catch (e) { | ||
if (e.code === ERROR_NO_SUCH_OBJECT) { | ||
return []; | ||
} | ||
throw new Error(`LDAP user search failed: ${e.message}`); | ||
} | ||
const entries = []; | ||
result.searchEntries.forEach(({ | ||
dn, | ||
...attributes | ||
}) => { | ||
const cns = getAttributeValues('cn', attributes); | ||
const sn = getAttributeValue('sn', attributes); | ||
const givenName = getAttributeValue('givenName', attributes); | ||
const displayName = getAttributeValue('displayName', attributes); | ||
const uid = getAttributeValue('uid', attributes); | ||
const mail = getAttributeValue('mail', attributes); | ||
let cn; | ||
if (cns) { | ||
// Take the last one, which is in OpenLDAP the actual group cn | ||
cn = [...cns].pop(); | ||
} else if (dn) { | ||
// Fallback, cn should always be present | ||
cn = dn.split(',')[0].split('=').pop(); | ||
} | ||
if (dn && cn && mail) { | ||
const ldapEntry = { | ||
dn, | ||
cn, | ||
sn, | ||
uid, | ||
mail, | ||
givenName, | ||
displayName | ||
}; | ||
if (extraAttributes) { | ||
extraAttributes.forEach(extraAttr => { | ||
ldapEntry[extraAttr] = getAttributeValue(extraAttr, attributes); | ||
}); | ||
} | ||
res.on('searchEntry', ({ | ||
objectName, | ||
attributes | ||
}) => { | ||
const dn = objectName?.toString(); | ||
const cns = attributes.find(({ | ||
type | ||
}) => type === 'cn')?.values; | ||
const sn = getAttributeValue('sn', attributes); | ||
const givenName = getAttributeValue('givenName', attributes); | ||
const displayName = getAttributeValue('displayName', attributes); | ||
const uid = getAttributeValue('uid', attributes); | ||
const mail = getAttributeValue('mail', attributes); | ||
let cn; | ||
if (cns) { | ||
// Take the last one, which is in OpenLDAP the actual group cn | ||
cn = [...cns].pop(); | ||
} else if (dn) { | ||
// Fallback, cn should always be present | ||
cn = dn.split(',')[0].split('=').pop(); | ||
} | ||
if (dn && cn && mail) { | ||
const ldapEntry = { | ||
dn, | ||
cn, | ||
sn, | ||
uid, | ||
mail, | ||
givenName, | ||
displayName | ||
}; | ||
if (extraAttributes) { | ||
extraAttributes.forEach(extraAttr => { | ||
ldapEntry[extraAttr] = getAttributeValue(extraAttr, attributes); | ||
}); | ||
} | ||
entries.push(ldapEntry); | ||
} else { | ||
this._logger.warn('Incomplete LDAP entry, dn, cn, and mail is required. Present attributes: ', attributes.map(a => `${a.type}:${a.values}`)); | ||
} | ||
}); | ||
res.on('error', error => { | ||
this._logger.error('LDAP search error', error); | ||
reject(error); | ||
}); | ||
res.on('end', result => { | ||
if (result?.status === 0) { | ||
resolve(entries); | ||
} else { | ||
reject(new Error(`Search failed: ${result?.errorMessage}`)); | ||
} | ||
}); | ||
}); | ||
entries.push(ldapEntry); | ||
} else { | ||
this._logger.warn('Incomplete LDAP entry, dn, cn, and mail is required. Present attributes: ', attributes); | ||
} | ||
}); | ||
return entries; | ||
} | ||
@@ -115,48 +127,36 @@ async searchGroups(filter) { | ||
}; | ||
return new Promise((resolve, reject) => { | ||
const entries = []; | ||
searchClient.search(this._baseDN, searchOpts, (err, res) => { | ||
if (err) { | ||
reject(err); | ||
return; | ||
} | ||
res.on('searchEntry', ({ | ||
objectName, | ||
attributes | ||
}) => { | ||
const dn = objectName?.toString(); | ||
const cns = attributes.find(({ | ||
type | ||
}) => type === 'cn')?.values; | ||
let cn; | ||
if (cns) { | ||
// Take the last one, which is in OpenLDAP the actual group cn | ||
cn = [...cns].pop(); | ||
} else if (dn) { | ||
// Fallback, cn should always be present | ||
cn = dn.split(',')[0].split('=').pop(); | ||
} | ||
if (dn && cn) { | ||
const ldapEntry = { | ||
dn, | ||
cn | ||
}; | ||
entries.push(ldapEntry); | ||
} else { | ||
this._logger.warn('Incomplete LDAP entry, dn and cs is required. Present attributes: ', attributes.map(a => `${a.type}:${a.values}`)); | ||
} | ||
}); | ||
res.on('error', error => { | ||
this._logger.error('LDAP search error', error); | ||
reject(error); | ||
}); | ||
res.on('end', result => { | ||
if (result?.status === 0) { | ||
resolve(entries); | ||
} else { | ||
reject(new Error(`Search failed: ${result?.errorMessage}`)); | ||
} | ||
}); | ||
}); | ||
let result; | ||
try { | ||
result = await searchClient.search(this._baseDN, searchOpts); | ||
} catch (e) { | ||
if (e.code === ERROR_NO_SUCH_OBJECT) { | ||
return []; | ||
} | ||
throw new Error(`LDAP group search failed: ${e.message}`); | ||
} | ||
const entries = []; | ||
result.searchEntries.forEach(({ | ||
dn, | ||
...attributes | ||
}) => { | ||
const cns = getAttributeValues('cn', attributes); | ||
let cn; | ||
if (cns) { | ||
// Take the last one, which is in OpenLDAP the actual group cn | ||
cn = [...cns].pop(); | ||
} else if (dn) { | ||
// Fallback, cn should always be present | ||
cn = dn.split(',')[0].split('=').pop(); | ||
} | ||
if (dn && cn) { | ||
const ldapEntry = { | ||
dn, | ||
cn | ||
}; | ||
entries.push(ldapEntry); | ||
} else { | ||
this._logger.warn('Incomplete LDAP entry, dn and cs is required. Present attributes: ', attributes); | ||
} | ||
}); | ||
return entries; | ||
} | ||
@@ -166,10 +166,14 @@ async login(ldapEntry, password) { | ||
try { | ||
client = await this._createLdapJsClient(); | ||
await this._bind(ldapEntry.dn, password, client); | ||
this._disconnect(client); | ||
client = await this._createLdapTsClient(); | ||
await client.bind(ldapEntry.dn, password); | ||
await client.unbind(); | ||
} catch (error) { | ||
this._logger.warn(`Binding with user ${ldapEntry.dn} failed`, error); | ||
if (client) { | ||
this._disconnect(client); | ||
try { | ||
await client.unbind(); | ||
} catch { | ||
// Ignore | ||
} | ||
} | ||
this._logger.warn(`Binding with user ${ldapEntry.dn} failed`, error); | ||
throw error; | ||
@@ -180,3 +184,7 @@ } | ||
if (this._searchClient) { | ||
this._disconnect(this._searchClient); | ||
try { | ||
this._searchClient.unbind(); | ||
} catch { | ||
// Ignore | ||
} | ||
this._searchClient = null; | ||
@@ -186,13 +194,12 @@ } | ||
async getSearchClient() { | ||
if (this._searchClient) { | ||
if (this._searchClient && this._searchClient.isConnected) { | ||
return this._searchClient; | ||
} | ||
try { | ||
const searchClient = await this._createLdapJsClient(true); | ||
const searchClient = await this._createLdapTsClient(); | ||
const bind = async () => { | ||
try { | ||
await this._bind(this._bindDN, this._bindCredentials, searchClient); | ||
await searchClient.bind(this._bindDN, this._bindCredentials); | ||
} catch (error) { | ||
this._logger.error(`Binding with user ${this._bindDN} failed`, error); | ||
this._disconnect(searchClient); | ||
this._searchClient = null; | ||
@@ -202,6 +209,2 @@ } | ||
await bind(); | ||
searchClient.on('connect', async () => { | ||
// Re-bind on reconnect | ||
await bind(); | ||
}); | ||
this._searchClient = searchClient; | ||
@@ -214,3 +217,3 @@ return this._searchClient; | ||
} | ||
async _createLdapJsClient(keepForever = false) { | ||
async _createLdapTsClient() { | ||
const clientOptions = { | ||
@@ -220,60 +223,7 @@ url: `${this._serverUrl}/${this._baseDN}`, | ||
connectTimeout: this._connectTimeout, | ||
timeout: this._timeout, | ||
reconnect: { | ||
initialDelay: 100, | ||
maxDelay: 10000, | ||
failAfter: keepForever ? Infinity : 3 | ||
} | ||
timeout: this._timeout | ||
}; | ||
return new Promise((resolve, reject) => { | ||
let resolved = false; | ||
let client; | ||
try { | ||
client = (0, _ldapjs.createClient)(clientOptions); | ||
client.on('connect', () => { | ||
this._logger.debug(`Connected to LDAP server: ${this._serverUrl}`); | ||
if (!resolved) { | ||
resolve(client); | ||
resolved = true; | ||
} | ||
}); | ||
client.on('connectError', error => { | ||
this._logger.error('LDAP connection error', error); | ||
if (!resolved) { | ||
reject(error); | ||
resolved = true; | ||
} | ||
}); | ||
client.on('error', error => { | ||
this._logger.warn('LDAP connection error, reconnecting...', error); | ||
if (!resolved) { | ||
reject(error); | ||
resolved = true; | ||
} | ||
}); | ||
client.on('destroy', () => { | ||
this._logger.debug(`Disconnected from LDAP server: ${this._serverUrl}`); | ||
}); | ||
} catch (e) { | ||
reject(e); | ||
} | ||
}); | ||
return new _ldapts.Client(clientOptions); | ||
} | ||
async _bind(user, password, ldapjsClient) { | ||
return new Promise((resolve, reject) => { | ||
ldapjsClient.bind(user, password, error => { | ||
if (error) { | ||
reject(error); | ||
} else { | ||
resolve(); | ||
} | ||
}); | ||
}); | ||
} | ||
_disconnect(ldapjsClient) { | ||
if (ldapjsClient.connected) { | ||
ldapjsClient.destroy(); | ||
} | ||
} | ||
} | ||
exports.default = LdapClientImpl; |
@@ -10,3 +10,3 @@ "use strict"; | ||
var _LdapClientImpl = _interopRequireDefault(require("./LdapClientImpl")); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } | ||
const bootstrap = async (pluginName, pluginConfig, pluginContextHolder) => { | ||
@@ -13,0 +13,0 @@ const { |
@@ -11,3 +11,3 @@ "use strict"; | ||
var _loginFailureReason = _interopRequireDefault(require("./login-failure-reason")); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } | ||
const LDAP_AUTH_USER_SESSION_KEY = '__MASHROOM_SECURITY_LDAP_AUTH_USER'; | ||
@@ -24,3 +24,2 @@ const LDAP_AUTH_EXPIRES_SESSION_KEY = '__MASHROOM_SECURITY_LDAP_AUTH_EXPIRES'; | ||
this._ldapClient = _ldapClient; | ||
this._serverRootFolder = _serverRootFolder; | ||
this._authenticationTimeoutSec = _authenticationTimeoutSec; | ||
@@ -27,0 +26,0 @@ const logger = loggerFactory('mashroom.security.provider.ldap'); |
@@ -7,3 +7,3 @@ { | ||
"license": "MIT", | ||
"version": "2.6.1", | ||
"version": "2.7.0", | ||
"files": [ | ||
@@ -14,10 +14,9 @@ "dist/**" | ||
"express": "^4.19.2", | ||
"ldapjs": "^3.0.7" | ||
"ldapts": "^7.0.12" | ||
}, | ||
"devDependencies": { | ||
"@mashroom/mashroom": "2.6.1", | ||
"@mashroom/mashroom-security": "2.6.1", | ||
"@mashroom/mashroom-utils": "2.6.1", | ||
"@types/express": "^4.17.21", | ||
"@types/ldapjs": "^3.0.6" | ||
"@mashroom/mashroom": "2.7.0", | ||
"@mashroom/mashroom-security": "2.7.0", | ||
"@mashroom/mashroom-utils": "2.7.0", | ||
"@types/express": "^4.17.21" | ||
}, | ||
@@ -24,0 +23,0 @@ "jest": { |
@@ -6,3 +6,3 @@ | ||
This plugin adds a LDAP security provider. | ||
This plugin adds an LDAP security provider. | ||
@@ -9,0 +9,0 @@ ## Usage |
4
29989
609
+ Addedldapts@^7.0.12
+ Added@types/asn1@0.2.4(transitive)
+ Added@types/node@22.1.0(transitive)
+ Addedasn1@0.2.6(transitive)
+ Addeddebug@4.3.6(transitive)
+ Addedldapts@7.1.0(transitive)
+ Addedms@2.1.2(transitive)
+ Addedstrict-event-emitter-types@2.0.0(transitive)
+ Addedundici-types@6.13.0(transitive)
+ Addeduuid@10.0.0(transitive)
- Removedldapjs@^3.0.7
- Removed@ldapjs/asn1@1.2.02.0.0(transitive)
- Removed@ldapjs/attribute@1.0.0(transitive)
- Removed@ldapjs/change@1.0.0(transitive)
- Removed@ldapjs/controls@2.1.0(transitive)
- Removed@ldapjs/dn@1.1.0(transitive)
- Removed@ldapjs/filter@2.1.1(transitive)
- Removed@ldapjs/messages@1.3.0(transitive)
- Removed@ldapjs/protocol@1.2.1(transitive)
- Removedabstract-logging@2.0.1(transitive)
- Removedassert-plus@1.0.0(transitive)
- Removedbackoff@2.5.0(transitive)
- Removedcore-util-is@1.0.2(transitive)
- Removedextsprintf@1.4.1(transitive)
- Removedldapjs@3.0.7(transitive)
- Removedonce@1.4.0(transitive)
- Removedprecond@0.2.3(transitive)
- Removedprocess-warning@2.3.2(transitive)
- Removedvasync@2.2.1(transitive)
- Removedverror@1.10.01.10.1(transitive)
- Removedwrappy@1.0.2(transitive)