diff --git a/src/.env.example b/src/.env.example index a6cc33d..46787f4 100644 --- a/src/.env.example +++ b/src/.env.example @@ -14,6 +14,12 @@ DB_TYPE= # Authentication backend: 'db' or 'ldap' (Required) AUTH_BACKEND= +# Directory backend: 'db' or 'proxmox' (Required) +DIRECTORY_BACKEND= + +# WEBCHART OBSERVATION CODE (Optional : Required if WC) +LDAP_UID_OBS_NAME= + #LDAP configuration (Required) LDAP_BASE_DN= LDAP_PORT= @@ -27,4 +33,8 @@ LDAP_CERT_CONTENT= LDAP_KEY_CONTENT= # OpenLDAP Configuration (Required if AUTH_BACKEND is 'ldap') -LDAP_URL= \ No newline at end of file +LDAP_URL= + +# Bind Credentials (Optional: Required if WC) +LDAP_BIND_DN=CN= +LDAP_BIND_PASSWORD= \ No newline at end of file diff --git a/src/auth/providers/auth/ldapBackend.js b/src/auth/providers/auth/ldapBackend.js index e66de31..41954fc 100644 --- a/src/auth/providers/auth/ldapBackend.js +++ b/src/auth/providers/auth/ldapBackend.js @@ -12,28 +12,23 @@ class LDAPBackend extends AuthProvider { setInterval(() => { this.failedServers.clear(); logger.debug("Resetting failed LDAP servers for retry."); - }, 5 * 60 * 1000); // every 5 minutes + }, 5 * 60 * 1000); } async authenticate(username, password, req) { for (const server of this.serverPool) { - if (this.failedServers.get(server.hostname)) { - continue; - } + if (this.failedServers.get(server.hostname)) continue; const url = `${server.scheme}//${server.hostname}:${server.port}`; - logger.debug(`Trying LDAP server: ${url} for user: ${username}`); + logger.debug("Attempting LDAP authentication via server", { host: server.hostname }); const success = await this.tryBind(url, username, password, server); - if (success) { - return true; - } else { - this.failedServers.set(server.hostname, Date.now()); - } + if (success) return true; + + this.failedServers.set(server.hostname, Date.now()); } - // if all tried and failed, clear the failed list for next time this.failedServers.clear(); return false; } @@ -45,11 +40,12 @@ class LDAPBackend extends AuthProvider { this.searchUserDN(client, username) .then((foundDN) => { if (!foundDN) { - logger.error("No DN found for user", { username }); + logger.warn("No DN found for user"); client.unbind(); return resolve(false); } - logger.debug(`Found user DN: ${foundDN}, attempting bind with user password...`); + + logger.debug("User DN found, attempting user bind..."); return this.attemptBind(client, foundDN, password); }) .then((success) => { @@ -57,13 +53,13 @@ class LDAPBackend extends AuthProvider { resolve(success); }) .catch((err) => { - logger.error("LDAP bind or search error", { url, username, err }); + logger.error("LDAP bind or search error", { error: err.message }); client.unbind(); resolve(false); }); client.on("error", (err) => { - logger.error("LDAP client connection error", { url, err }); + logger.error("LDAP client connection error", { error: err.message }); resolve(false); }); }); @@ -79,19 +75,20 @@ class LDAPBackend extends AuthProvider { client.bind(process.env.LDAP_BIND_DN, process.env.LDAP_BIND_PASSWORD, (err) => { if (err) { - logger.error("Service bind failed", err); - return reject(new Error("Service bind failed: " + err)); + logger.error("LDAP service bind failed", { error: err.message }); + return reject(new Error("Service bind failed")); } - logger.debug("Service bind successful, searching for user..."); + + logger.debug("LDAP service bind successful"); let foundDN = null; client.search('dc=mieweb,dc=com', opts, (err, res) => { if (err) return reject(err); res.on('searchEntry', (entry) => { - console.log("Found entry DN:", entry.objectName); foundDN = entry.dn.toString(); }); + res.on('error', (err) => reject(err)); res.on('end', () => resolve(foundDN)); }); @@ -103,10 +100,11 @@ class LDAPBackend extends AuthProvider { return new Promise((resolve) => { client.bind(dn, password, (err) => { if (err) { - logger.error("LDAP user bind failed", { dn, err }); + logger.warn("LDAP user bind failed"); return resolve(false); } - logger.info("LDAP user bind success", { dn }); + + logger.info("LDAP user bind succeeded"); return resolve(true); }); }); diff --git a/src/config/dbConfig.js b/src/config/dbConfig.js index 6d49a00..1cdb12b 100644 --- a/src/config/dbConfig.js +++ b/src/config/dbConfig.js @@ -6,6 +6,7 @@ const dbConfigs = { mysql: { type: 'mysql', host: process.env.MYSQL_HOST || "mysql", + port: process.env.MYSQL_PORT || "33306", user: process.env.MYSQL_USER || "root", password: process.env.MYSQL_PASSWORD || "rootpassword", database: process.env.MYSQL_DATABASE || "ldap_user_db", diff --git a/src/db/drivers/mysql.js b/src/db/drivers/mysql.js index b663129..76a2af6 100644 --- a/src/db/drivers/mysql.js +++ b/src/db/drivers/mysql.js @@ -1,109 +1,235 @@ -const mysql = require("mysql2/promise"); +const mysql = require('mysql2/promise'); +const logger = require('../../utils/logger'); -// Create a connection pool at startup let pool; +// cache for the obs_code lookup +let cachedObsCode = null; +let cachedObsName = null; + +async function getLdapUidObsCode() { + const obsName = process.env.LDAP_UID_OBS_NAME || 'LDAP UID Number'; + if (cachedObsCode !== null && cachedObsName === obsName) return cachedObsCode; + + const sql = ` + SELECT obs_code + FROM observation_codes + WHERE obs_name = ? + LIMIT 1 + `; + try { + const rows = await executeQuery(sql, [obsName]); + if (!rows[0]) { + logger.warn(`Observation code not found for name "${obsName}". ldap_uid_number will be NULL.`); + cachedObsCode = null; + } else { + cachedObsCode = rows[0].obs_code; + logger.info(`Using obs_code=${cachedObsCode} for "${obsName}"`); + } + cachedObsName = obsName; + return cachedObsCode; + } catch (err) { + logger.error("Failed to resolve LDAP UID observation code", { error: err.message }); + cachedObsCode = null; + cachedObsName = obsName; + return null; + } +} + function createPool(config) { if (!pool) { pool = mysql.createPool({ host: config.host, + port: config.port, user: config.user, password: config.password, database: config.database, waitForConnections: true, - connectionLimit: 10, // Maximum number of connections in the pool - queueLimit: 0 // No limit on the number of waiting requests + connectionLimit: 10, + queueLimit: 0 }); - console.log("MySQL Connection Pool Created"); + logger.info("MySQL connection pool created"); } return pool; } -// Initialize the connection pool async function connect(config) { - return createPool(config); + try { + return createPool(config); + } catch (err) { + logger.error("Error creating MySQL pool", { error: err.message }); + throw err; + } } -// Close the pool (when shutting down the app) async function close() { if (pool) { - await pool.end(); - pool = null; - console.log("MySQL Connection Pool Closed"); + try { + await pool.end(); + logger.info("MySQL connection pool closed"); + pool = null; + } catch (err) { + logger.error("Error closing MySQL pool", { error: err.message }); + } } } -async function findUserByUsername(username) { - const connection = await pool.getConnection(); +async function executeQuery(sql, params = []) { + if (!pool) throw new Error('Pool not initialized. Call connect() first.'); + const conn = await pool.getConnection(); try { - const [rows] = await connection.execute( - "SELECT * FROM users WHERE username = ?", - [username] - ); - return rows[0] || null; + const [rows] = await conn.execute(sql, params); + return rows; + } catch (err) { + logger.error("Database query failed", { + error: err.message, + queryContext: sql.slice(0, 50) + '...', + }); + throw err; } finally { - connection.release(); // Release connection back to pool + conn.release(); + } +} + +function latestUidSubquery(obsCode) { + if (obsCode === null) return ''; + // Pick the latest by create_datetime; if tied, pick largest obs_id + return ` + LEFT JOIN ( + SELECT o1.user_id, o1.obs_result AS ldap_uid_number + FROM observations o1 + JOIN ( + SELECT user_id, + MAX(create_datetime) AS max_dt + FROM observations + WHERE obs_code = ? + GROUP BY user_id + ) mx + ON mx.user_id = o1.user_id + AND mx.max_dt = o1.create_datetime + WHERE o1.obs_code = ? + -- tie-breaker if multiple rows share the same timestamp + AND o1.obs_id = ( + SELECT MAX(o2.obs_id) + FROM observations o2 + WHERE o2.user_id = o1.user_id + AND o2.obs_code = o1.obs_code + AND o2.create_datetime = o1.create_datetime + ) + ) uid ON uid.user_id = u.user_id + `; +} + +async function findUserByUsername(username) { + logger.debug(`Looking up user: ${username}`); + const obsCode = await getLdapUidObsCode(); + + const sql = ` + SELECT + u.user_id, + u.username, + u.first_name, + u.last_name, + u.email, + r.id AS gidNumber, + u.realm, + ${obsCode !== null ? 'uid.ldap_uid_number' : 'NULL AS ldap_uid_number'} + FROM users u + LEFT JOIN realms r ON r.realm = u.realm + ${obsCode !== null ? latestUidSubquery(obsCode) : ''} + WHERE u.username = ? + LIMIT 1 + `; + + const params = []; + if (obsCode !== null) { + params.push(obsCode, obsCode); // for the subquery } + params.push(username); + + const rows = await executeQuery(sql, params); + return rows[0] || null; } async function findGroupsByMemberUid(username) { - const connection = await pool.getConnection(); - try { - const [rows] = await connection.execute( - "SELECT g.name, g.gid, g.member_uids " + - "FROM `groups` g " + - "WHERE JSON_CONTAINS(g.member_uids, JSON_QUOTE(?))", - [username] - ); - return rows.map(row => { - if (row.member_uids && typeof row.member_uids === 'string') { - try { - row.member_uids = JSON.parse(row.member_uids); - } catch (e) { - // error - } - } - return row; + logger.debug(`Fetching groups for user: ${username}`); + const sql = ` + SELECT r.realm AS name, r.id AS gid + FROM user_realms ur + JOIN users u ON u.user_id = ur.user_id + JOIN realms r ON r.realm = ur.realm + WHERE u.username = ? + GROUP BY r.id, r.realm + ORDER BY r.realm + `; + const groups = await executeQuery(sql, [username]); + + const out = []; + for (const g of groups) { + const membersSql = ` + SELECT u.username AS memberUid + FROM user_realms ur + JOIN users u ON u.user_id = ur.user_id + WHERE ur.realm = ? + ORDER BY u.username + `; + const members = await executeQuery(membersSql, [g.name]); + out.push({ + name: g.name, + gid: g.gid, + member_uids: members.map(m => m.memberUid) }); - } finally { - connection.release(); } + return out; } async function getAllUsers() { - const [rows] = await this.pool.query('SELECT * FROM users'); - return rows; -} + logger.debug("Fetching all users"); + const obsCode = await getLdapUidObsCode(); -async function getAllGroups() { - try { - const query = ` - SELECT - g.id, - g.name, - g.gid, - GROUP_CONCAT(u.username) as member_uids - FROM groups g - LEFT JOIN user_groups ug ON g.id = ug.group_id - LEFT JOIN users u ON ug.user_id = u.id - GROUP BY g.id, g.name, g.gid - ORDER BY g.name - `; + const sql = ` + SELECT + u.user_id, + u.username, + u.first_name, + u.last_name, + u.email, + r.id AS gidNumber, + u.realm, + ${obsCode !== null ? 'uid.ldap_uid_number' : 'NULL AS ldap_uid_number'} + FROM users u + LEFT JOIN realms r ON r.realm = u.realm + ${obsCode !== null ? latestUidSubquery(obsCode) : ''} + ORDER BY u.username + `; - const groups = await this.executeQuery(query); - - return groups.map(group => ({ - id: group.id, - name: group.name, - gid: group.gid, - member_uids: group.member_uids ? group.member_uids.split(',') : [] - })); - } catch (error) { - logger.error('Error getting all groups from MySQL:', error); - throw error; + const params = []; + if (obsCode !== null) { + params.push(obsCode, obsCode); } + + return await executeQuery(sql, params); } +async function getAllGroups() { + logger.debug("Fetching all groups"); + const sql = ` + SELECT r.id, r.realm AS name, + COALESCE(GROUP_CONCAT(u.username ORDER BY u.username SEPARATOR ','), '') AS member_uids + FROM realms r + LEFT JOIN user_realms ur ON ur.realm = r.realm + LEFT JOIN users u ON u.user_id = ur.user_id + GROUP BY r.id, r.realm + ORDER BY r.realm + `; + const rows = await executeQuery(sql); + return rows.map(g => ({ + id: g.id, + name: g.name, + gid: g.id, + member_uids: g.member_uids ? g.member_uids.split(',').filter(Boolean) : [] + })); +} module.exports = { connect, @@ -111,5 +237,6 @@ module.exports = { findUserByUsername, findGroupsByMemberUid, getAllUsers, - getAllGroups -}; \ No newline at end of file + getAllGroups, + executeQuery +}; diff --git a/src/server.js b/src/server.js index 7bd087e..b9a6eb9 100644 --- a/src/server.js +++ b/src/server.js @@ -83,7 +83,7 @@ async function startServer() { res.end(); } } catch (error) { - logger.error("Bind error", { error }); + logger.error("Bind error", { message: error?.message }); return next(new ldap.OperationsError('Authentication error')); } }); @@ -92,7 +92,7 @@ async function startServer() { server.search(process.env.LDAP_BASE_DN, async (req, res, next) => { const filterStr = req.filter.toString(); - logger.debug(`LDAP Search - Filter: ${filterStr}, Attributes: ${req.attributes}`); + logger.debug("LDAP Search received", { attributes: req.attributes.length }); const username = getUsernameFromFilter(filterStr); @@ -110,14 +110,9 @@ async function startServer() { for (const user of users) { const entry = createLdapEntry(user); - logger.debug("Sending user entry:", { - dn: entry.dn, - uid: entry.attributes.uid, - objectClass: entry.attributes.objectClass, - cn: entry.attributes.cn, - uidNumber: entry.attributes.uidNumber, - gidNumber: entry.attributes.gidNumber - }); + + logger.debug("Sending user entry", { uid: entry.attributes?.uid }); + res.send(entry); } diff --git a/src/utils/ldapUtils.js b/src/utils/ldapUtils.js index 43e7415..3e88d03 100644 --- a/src/utils/ldapUtils.js +++ b/src/utils/ldapUtils.js @@ -1,10 +1,35 @@ const logger = require("./logger"); +// Ensure we return a numeric string or undefined +function pickObsUid(user) { + const v = user?.ldap_uid_number; + if (v === undefined || v === null) return undefined; + const s = String(v).trim(); + if (s === "") return undefined; + + // uidNumber in LDAP must be an integer + const n = Number(s); + if (!Number.isFinite(n) || !Number.isInteger(n)) { + logger.warn("Invalid ldap_uid_number (not an integer). Falling back.", { value: s, user: user?.username }); + return undefined; + } + return String(n); +} function createLdapEntry(user) { - // Temp-Fix: Webchart schema doesn't have these right now - const uidNumber = user.uid_number !== undefined && user.uid_number !== null ? user.uid_number.toString() : "0"; - const gidNumber = user.gid_number !== undefined && user.gid_number !== null ? user.gid_number.toString() : "0"; + // 1) Prefer observation-mapped UID (ldap_uid_number) + // 2) Fallback: old behavior (user_id + 10000) + const obsUid = pickObsUid(user); + const uidNumber = + obsUid ?? + (user.user_id !== undefined && user.user_id !== null + ? String(parseInt(user.user_id, 10) + 10000) + : "10000"); + + const gidNumber = + (user.gidNumber !== undefined && user.gidNumber !== null + ? String(parseInt(user.gidNumber, 10) + 10000) + : "10000"); const entry = { dn: `uid=${user.username},${process.env.LDAP_BASE_DN}`, @@ -13,17 +38,26 @@ function createLdapEntry(user) { uid: user.username, uidNumber, gidNumber, - cn: user.full_name || user.username, - gecos: user.full_name || user.username, - sn: user.surname || "Unknown", - mail: user.mail || `${user.username}@mieweb.com`, // Mandatory - homeDirectory: user.home_directory, + cn: user.first_name, + gecos: `${user.first_name || ''} ${user.last_name || ''}`.trim(), + sn: user.last_name || "Unknown", + mail: user.email || `${user.username}@mieweb.com`, + homeDirectory: `/home/${user.username}`, loginShell: "/bin/bash", - shadowLastChange: "0", - userpassword: user?.password, + shadowLastChange: "1", }, }; + logger.debug("Created LDAP user entry", { + dn: entry.dn, + uid: entry.attributes.uid, + uidNumber: entry.attributes.uidNumber, + gidNumber: entry.attributes.gidNumber, + uidSource: obsUid ? "observation" : "fallback", + }); + + console.log("entry", entry) + return entry; } @@ -47,7 +81,7 @@ function createLdapGroupEntry(group) { entry.attributes.member = group.members; } - logger.debug("Created LDAP group entry:", { + logger.debug("Created LDAP group entry", { dn: entry.dn, cn: entry.attributes.cn, gidNumber: entry.attributes.gidNumber, @@ -58,4 +92,3 @@ function createLdapGroupEntry(group) { } module.exports = { createLdapEntry, createLdapGroupEntry }; -