Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/churchtools/ctldap-ms
Browse files Browse the repository at this point in the history
  • Loading branch information
hubermat committed Dec 7, 2019
2 parents bd48fd1 + 712b456 commit 7044f24
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 39 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

### 2.1
- Upgraded to ldapjs 1.0.2
- Fixed wrong street mapping
- Consistent logging
- substring queries are now case insensitive
Was an issue in in nextcloud group sharing for example

### 2.0
- adapted to built-in ChurchTools ctldap API

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# LDAP Wrapper for ChurchTools v2.0
# LDAP Wrapper for ChurchTools v2.1

This software acts as an LDAP server for ChurchTools >= 3.25.0

Expand Down
5 changes: 4 additions & 1 deletion ctldap.example.config
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ ldap_user=root
ldap_password=XXXXXXXXXXXXXXXXXXXX
; If set to true, treat ldap_password as a bcrypt hash and compare against it
;ldap_password_bcrypt=true
; LDAP server ip to listen on, change it to 0.0.0.0 when external access required
; When you use the iptables_port setting, the port forwarding is stil installed on the eth0 card
ldap_ip=127.0.0.1
; LDAP server port
ldap_port=1389
; The ctldap.sh service script will try to read this and setup an iptables NAT rule from iptables_port to ldap_port if it is set
; The ctldap.sh service script will try to read this and setup an iptables NAT rule on interface eth0 from iptables_port to ldap_port if it is set
iptables_port=389
; LDAP base DN o=xxx, e.g. churchtools
ldap_base_dn=churchtools
Expand Down
99 changes: 69 additions & 30 deletions ctldap.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
// ChurchTools LDAP-Wrapper 2.0
// ChurchTools LDAP-Wrapper 2.1
// This tool requires a node.js-Server and ChurchTools >= 3.25.0
// (c) 2017 Michael Lux
// (c) 2019 André Schild
// License: GNU/GPL v3.0

var ldap = require('ldapjs');
var fs = require('fs');
var ini = require('ini');
var rp = require('request-promise');
var ldapEsc = require('ldap-escape');
var parseDN = require('ldapjs').parseDN;
var extend = require('extend');
var Promise = require("bluebird");
var path = require('path');
var bcrypt = require('bcrypt');

var helpers = require('ldap-filter/lib/helpers');

var config = ini.parse(fs.readFileSync(path.resolve(__dirname, 'ctldap.config'), 'utf-8'));
if (config.debug) {
console.log("Debug mode enabled, expect lots of output!");
Expand Down Expand Up @@ -149,7 +153,7 @@ if (typeof config.cache_lifetime !== 'number') {
function apiLogin(site) {
if (site.loginPromise === null) {
if (config.debug) {
console.log("Performing API login...");
console.log("[DEBUG] Performing API login...");
}
site.loginPromise = rp({
"method": "POST",
Expand All @@ -166,23 +170,23 @@ function apiLogin(site) {
throw new Error(result.data);
}
if (config.debug) {
console.log("API login completed");
console.log("[DEBUG] API login completed");
}
// clear login promise
site.loginPromise = null;
// end gracefully
return null;
}).catch(function (error) {
if (config.debug) {
console.log("API login failed!");
console.log("[DEBUG] API login failed!");
}
// clear login promise
site.loginPromise = null;
// rethrow error
throw new Error(error);
});
} else if (config.debug) {
console.log("Return pending login promise");
console.log("[DEBUG] Return pending login promise");
}
return site.loginPromise;
}
Expand All @@ -206,12 +210,12 @@ function apiPost(site, func, data, triedLogin) {
// If this was the first attempt, login and try again
if (!triedLogin) {
if (config.debug) {
console.log("Session invalid, login and retry...");
console.log("[DEBUG] Session invalid, login and retry...");
}
return apiLogin(site).then(function () {
// Retry operation after login
if (config.debug) {
console.log("Retry request to API function " + func + " after login");
console.log("[DEBUG] Retry request to API function " + func + " after login");
}
// Set "triedLogin" parameter to prevent looping
return apiPost(site, func, data, true);
Expand All @@ -222,7 +226,8 @@ function apiPost(site, func, data, triedLogin) {
}
return result.data;
}, function (error) {
console.log(error.message);
console.log("[ERROR] "+error.message);
console.log(error.stack);
});
}

Expand Down Expand Up @@ -275,7 +280,7 @@ function requestUsers (req, res, next) {
uid: cn,
nsuniqueid: "u" + v.id,
givenname: v.vorname,
street: v.street,
street: v.strasse,
telephoneMobile: v.telefonhandy,
telephoneHome: v.telefonprivat,
postalCode: v.plz,
Expand Down Expand Up @@ -303,12 +308,13 @@ function requestUsers (req, res, next) {
uid: cn,
nsuniqueid: "u0",
givenname: "LDAP Administrator",
objectclass: ['CTPerson'],
}
});
}
var size = newCache.length;
if (config.debug && size > 0) {
console.log("Updated users: " + size);
console.log("[DEBUG] Updated users: " + size);
}
return newCache;
});
Expand Down Expand Up @@ -345,7 +351,7 @@ function requestGroups (req, res, next) {
});
var size = newCache.length;
if (config.debug && size > 0) {
console.log("Updated groups: " + size);
console.log("[DEBUG] Updated groups: " + size);
}
return newCache;
});
Expand All @@ -361,7 +367,7 @@ function requestGroups (req, res, next) {
*/
function authorize(req, res, next) {
if (!req.connection.ldap.bindDN.equals(req.site.adminDn)) {
console.log("Rejected search without proper binding!");
console.log("[WARN] Rejected search without proper binding!");
return next(new ldap.InsufficientAccessRightsError());
}
return next();
Expand All @@ -375,14 +381,14 @@ function authorize(req, res, next) {
*/
function searchLogging (req, res, next) {
if (config.debug) {
console.log("SEARCH base object: " + req.dn.toString() + " scope: " + req.scope);
console.log("Filter: " + req.filter.toString());
console.log("[DEBUG] SEARCH base object: " + req.dn.toString() + " scope: " + req.scope);
console.log("[DEBUG] Filter: " + req.filter.toString());
}
return next();
}

/**
* Evaluetes req.usersPromise and sends matching elements to the client.
* Evaluates req.usersPromise and sends matching elements to the client.
* @param {object} req - Request object
* @param {object} res - Response object
* @param {function} next - Next handler function of filter chain
Expand All @@ -391,23 +397,24 @@ function sendUsers (req, res, next) {
var strDn = req.dn.toString();
req.usersPromise.then(function (users) {
users.forEach(function (u) {
if ((req.checkAll || strDn === u.dn) && (req.filter.matches(u.attributes))) {
if ((req.checkAll || parseDN(strDn).equals(parseDN(u.dn))) && (req.filter.matches(u.attributes))) {
if (config.debug) {
console.log("MatchUser: " + u.dn);
console.log("[DEBUG] MatchUser: " + u.dn);
}
res.send(u);
}
});
return next();
}).catch(function (error) {
console.log("Error while retrieving users: ");
console.log("[ERROR] Error while retrieving users: ");
console.log(error.message);
console.log(error.stack);
return next();
});
}

/**
* Evaluetes req.groupsPromise and sends matching elements to the client.
* Evaluates req.groupsPromise and sends matching elements to the client.
* @param {object} req - Request object
* @param {object} res - Response object
* @param {function} next - Next handler function of filter chain
Expand All @@ -416,17 +423,18 @@ function sendGroups (req, res, next) {
var strDn = req.dn.toString();
req.groupsPromise.then(function (groups) {
groups.forEach(function (g) {
if ((req.checkAll || strDn === g.dn) && (req.filter.matches(g.attributes))) {
if ((req.checkAll || parseDN(strDn).equals(parseDN(g.dn))) && (req.filter.matches(g.attributes))) {
if (config.debug) {
console.log("MatchGroup: " + g.dn);
console.log("[DEBUG] MatchGroup: " + g.dn);
}
res.send(g);
}
});
return next();
}).catch(function (error) {
console.log("Error while retrieving groups: ");
console.log("[ERROR] Error while retrieving groups: ");
console.log(error.message);
console.log(error.stack);
return next();
});
}
Expand All @@ -452,37 +460,38 @@ function authenticate (req, res, next) {
var site = req.site;
if (req.dn.equals(site.adminDn)) {
if (config.debug) {
console.log('Admin bind DN: ' + req.dn.toString());
console.log('[DEBUG] Admin bind DN: ' + req.dn.toString());
}
// If ldap_password is undefined, try a default ChurchTools authentication with this user
if (site.ldap_password !== undefined) {
site.checkPassword(req.credentials, function (result) {
if (result) {
if (config.debug) {
console.log("Authentication success");
console.log("[DEBUG] Authentication success");
}
return next();
} else {
console.log("Invalid root password!");
console.log("[WARN] Invalid root password!");
return next(new ldap.InvalidCredentialsError());
}
});
return;
}
} else if (config.debug) {
console.log('Bind user DN: ' + req.dn.toString());
console.log('[DEBUG] Bind user DN: %s', req.dn);
}
apiPost(site, "authenticate", {
"user": req.dn.rdns[0].cn,
"user": req.dn.rdns[0].attrs.cn.value,
"password": req.credentials
}).then(function () {
if (config.debug) {
console.log("Authentication successful for " + req.dn.toString());
console.log("[DEBUG] Authentication successful for " + req.dn.toString());
}
return next();
}).catch(function (error) {
console.log("Authentication error: ");
console.log("[WARN] Authentication error: ");
console.log(error.message);
console.log(error.stack);
return next(new ldap.InvalidCredentialsError());
});
}
Expand Down Expand Up @@ -552,7 +561,37 @@ server.search('', function (req, res, next) {
res.end();
}, endSuccess);


function escapeRegExp(str) {
/* JSSTYLED */
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
}

/** Case insensitive search on substring filters */
ldap.SubstringFilter.prototype.matches = function (target, strictAttrCase) {
var tv = helpers.getAttrValue(target, this.attribute, strictAttrCase);
if (tv !== undefined && tv !== null) {
var re = '';

if (this.initial)
re += '^' + escapeRegExp(this.initial) + '.*';
this.any.forEach(function (s) {
re += escapeRegExp(s) + '.*';
});
if (this.final)
re += escapeRegExp(this.final) + '$';

var matcher = new RegExp(re, 'i');
return helpers.testValues(function (v) {
return matcher.test(v);
}, tv);
}

return false;
};


// Start LDAP server
server.listen(parseInt(config.ldap_port), function () {
server.listen(parseInt(config.ldap_port), config.ldap_ip, function () {
console.log('ChurchTools-LDAP-Wrapper listening @ %s', server.url);
});
4 changes: 2 additions & 2 deletions ctldap_raw.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: ChurchTools LDAP-Wrapper v2.0
# Short-Description: ChurchTools LDAP-Wrapper v2.1
# Description: Init script for the ChurchTools LDAP Wrapper.
### END INIT INFO

Expand Down Expand Up @@ -39,7 +39,7 @@ start)
else
echo $PID > $PIDFILE
echo "$DESC started"
DPORT=$( cat $CTLDAP/ctldap.config | grep -oP "(?<=iptables_port=)[1-9][0-9]+" | head -n1 )
DPORT=$( cat $CTLDAP/ctldap.config | grep -oP "(?<=^iptables_port=)\s*[1-9][0-9]+" | head -n1 )
if [ -n "$DPORT" ]; then
echo "Trying to create iptables NAT rules for port redirect..."
TO_PORT=$( cat $CTLDAP/ctldap.config | grep -oP "(?<=ldap_port=)[1-9][0-9]+" | head -n1 )
Expand Down
14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@
"name": "ctldap",
"license": "GPL-3.0",
"description": "LDAP Wrapper for ChurchTools",
"version": "1.0.1",
"version": "2.1.0",
"private": true,
"dependencies": {
"bcrypt": "^2.0.1",
"bcrypt": "^3.0.4",
"bluebird": "^3.5.0",
"extend": "^3.0.1",
"ini": "^1.1.0",
"ldap-escape": "^1.1.4",
"ldapjs": "^0.7.0",
"ldap-escape": "^1.1.5",
"ldapjs": "^1.0.2",
"request": "^2.81.0",
"request-promise": "^4.2.1"
},
"devDependencies": {}
"devDependencies": {},
"scripts": {
"start": "node ctldap.js"
}

}

0 comments on commit 7044f24

Please sign in to comment.