Skip to content

Commit

Permalink
Fixed context of SubstringFilter.prototype.matches, updates, modulari…
Browse files Browse the repository at this point in the history
…zation
  • Loading branch information
milux committed Feb 20, 2023
1 parent 2627bf2 commit b2b3481
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 396 deletions.
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
*
!ctldap.js
!*.js
!ctldap.yml
!package.json
!yarn.lock
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/ctldap.sh
/node_modules
/.idea/
*.iml
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ COPY --chown=node:node package.json .
COPY --chown=node:node yarn.lock .
RUN yarn install

COPY --chown=node:node ctldap.js .
COPY --chown=node:node *.js .
COPY --chown=node:node ctldap.yml .

EXPOSE 1389
Expand Down
71 changes: 0 additions & 71 deletions changeConfig.js

This file was deleted.

58 changes: 58 additions & 0 deletions ctldap-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* ctldap - ChurchTools LDAP-Wrapper 3.0
* @copyright 2017-2023 Michael Lux
* @licence GNU/GPL v3.0
*/
import { readYamlEnvSync } from "yaml-env-defaults";
import { CtldapSite } from "./ctldap-site.js";

export class CtldapConfig {

/**
* CtldapConfig constructor.
*/
constructor() {
const yaml = readYamlEnvSync('./ctldap.yml');
const config = yaml.config;
this.debug = CtldapConfig.asOptionalBool(config.debug);
this.ldapIp = config.ldapIp;
this.ldapPort = config.ldapPort;

if (typeof config.cacheLifetime !== 'number' && isNaN(config.cacheLifetime)) {
this.cacheLifetime = 300000; // 5 minutes
} else {
this.cacheLifetime = Number(config.cacheLifetime);
}
this.ldapUser = config.ldapUser;
this.ldapPassword = config.ldapPassword;
this.ctUri = config.ctUri;
this.apiToken = config.apiToken;
this.specialGroupMappings = config.specialGroupMappings || {};
this.dnLowerCase = CtldapConfig.asOptionalBool(config.dnLowerCase);
this.emailLowerCase = CtldapConfig.asOptionalBool(config.emailLowerCase);
this.emailsUnique = CtldapConfig.asOptionalBool(config.emailsUnique);
this.ldapCertFilename = config.ldapCertFilename;
this.ldapKeyFilename = config.ldapKeyFilename;
this.ldapBaseDn = config.ldapBaseDn;
// Configure sites
const sites = yaml.sites || {};
// If ldapBaseDn is set, create a site from the global config properties.
if (config.ldapBaseDn) {
sites[config.ldapBaseDn] = {
ldapUser: config.ldapUser,
ldapPassword: config.ldapPassword,
ctUri: config.ctUri,
apiToken: config.apiToken,
specialGroupMappings: config.specialGroupMappings
}
}
this.sites = Object.keys(sites).map((siteName) => new CtldapSite(this, siteName, sites[siteName]));
}

static asOptionalBool (val) {
if (val === undefined) {
return undefined;
}
return (val || 'false').toLowerCase() !== 'false';
}
}
130 changes: 130 additions & 0 deletions ctldap-site.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* ctldap - ChurchTools LDAP-Wrapper 3.0
* @copyright 2017-2023 Michael Lux
* @licence GNU/GPL v3.0
*/
import ldapEscape from "ldap-escape";
import got from "got";
import bcrypt from "bcrypt";
import argon2 from "argon2";
import { CtldapConfig } from "./ctldap-config.js"

export class CtldapSite {

/**
* CtldapSite Constructor.
* @param {CtldapConfig} config The main CtldapConfig for fallback values.
* @param {string} name The name (i.e. also base DN) of the site.
* @param {object} site The site's config object.
*/
constructor(config, name, site) {
// Take ldapUser from main config if not specified for site.
this.ldapUser = site.ldapUser || config.ldapUser;
this.ldapPassword = site.ldapPassword;
this.specialGroupMappings = site.specialGroupMappings;
this.dnLowerCase = CtldapConfig.asOptionalBool(site.dnLowerCase);
this.emailLowerCase = CtldapConfig.asOptionalBool(site.emailLowerCase);
this.emailsUnique = CtldapConfig.asOptionalBool(site.emailsUnique);
this.name = name;
this.fnUserDn = (cn) => ldapEscape.dn`cn=${cn},ou=users,o=${name}`;
this.fnGroupDn = (cn) => ldapEscape.dn`cn=${cn},ou=groups,o=${name}`;
this.api = got.extend({
headers: { "Authorization": `Login ${site.apiToken}` },
prefixUrl: `${site.ctUri.replace(/\/$/g, '')}/api`,
responseType: 'json',
resolveBodyOnly: true,
http2: true
});
this.adminDn = this.fnUserDn(site.ldapUser);
this.CACHE = {};
this.loginErrorCount = 0;
this.loginBlockedDate = null;

const identityFn = (p) => p;
const stringLowerFn = (s) => typeof s === "string" ? s.toLowerCase() : s;

if (this.dnLowerCase || ((this.dnLowerCase === undefined) && config.dnLowerCase)) {
this.compatTransform = stringLowerFn;
} else {
this.compatTransform = identityFn;
}

if (this.emailLowerCase || ((this.emailLowerCase === undefined) && config.emailLowerCase)) {
this.compatTransformEmail = stringLowerFn;
} else {
this.compatTransformEmail = identityFn;
}

if (this.emailsUnique || ((this.emailsUnique === undefined) && config.emailsUnique)) {
this.uniqueEmails = (users) => {
const mails = {};
return users.filter((user) => {
if (!user.attributes.email) {
return false;
}
const result = !(user.attributes.email in mails);
mails[user.attributes.email] = true;
return result;
});
};
} else {
this.uniqueEmails = identityFn;
}

// If LDAP admin password has been provided, set the right verification algorithm based on hash format.
if (this.ldapPassword) {
if (/^\$2[yab]\$/.test(this.ldapPassword)) {
// Assume bcrypt hash
this.checkPassword = async (password) => {
const hash = this.ldapPassword.replace(/^\$2y\$/, '$2a$');
if (!await bcrypt.compare(password, hash)) {
throw Error("Wrong password, bcrypt hash didn't match!");
}
};
} else if (/^\$argon2[id]{1,2}\$/.test(this.ldapPassword)) {
// Assume argon2 hash
this.checkPassword = async (password) => {
if (!await argon2.verify(this.ldapPassword, password)) {
throw Error("Wrong password, argon2 hash didn't match!");
}
}
} else {
// Assume plaintext password
this.checkPassword = async (password) => {
if (password !== this.ldapPassword) {
throw Error("Wrong password, plaintext didn't match!")
}
};
}
}
}

/**
* Tries to perform a local LDAP admin authentication, locking for one day after 5 failed login approaches.
* @param password Password to use for LDAP admin authentication.
* @returns {Promise<void>} Promise resolves upon successful authentication, rejects on error.
*/
async authenticateAdmin (password) {
if (this.loginBlockedDate) {
const now = new Date();
const checkDate = new Date(this.loginBlockedDate.getTime() + 1000 * 3600 * 24); // one day
if (now < checkDate) {
throw Error("Login blocked!");
} else {
this.loginBlockedDate = null;
this.loginErrorCount = 0;
}
}
try {
// Delegate password check to the associated algorithm based on type of password hashing, see below.
await this.checkPassword(password);
} catch (error) {
this.loginErrorCount += 1;
if (this.loginErrorCount > 5) {
this.loginBlockedDate = new Date();
}
throw error;
}
};

}
Loading

0 comments on commit b2b3481

Please sign in to comment.