diff --git a/ldap-overleaf-sl/Dockerfile b/ldap-overleaf-sl/Dockerfile index a6c958c..347b226 100644 --- a/ldap-overleaf-sl/Dockerfile +++ b/ldap-overleaf-sl/Dockerfile @@ -1,4 +1,7 @@ -FROM sharelatex/sharelatex:2.3.1 +FROM sharelatex/sharelatex:2.5.2 +# FROM sharelatex/sharelatex:latest +# latest might not be tested +# e.g. the AuthenticationManager.js script had to be adapted between versions after 2.3.1 LABEL maintainer="Simon Haller-Seeber" LABEL version="0.1" diff --git a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js index 6a38ba9..f32bcae 100644 --- a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js +++ b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js @@ -1,12 +1,11 @@ const Settings = require('settings-sharelatex') -const {User} = require('../../models/User') -const {db, ObjectId} = require('../../infrastructure/mongojs') +const { User } = require('../../models/User') +const { db, ObjectId } = require('../../infrastructure/mongodb') const bcrypt = require('bcrypt') const EmailHelper = require('../Helpers/EmailHelper') -const V1Handler = require('../V1/V1Handler') const { - InvalidEmailError, - InvalidPasswordError + InvalidEmailError, + InvalidPasswordError } = require('./AuthenticationErrors') const util = require('util') @@ -18,20 +17,20 @@ const { Client } = require('ldapts'); const BCRYPT_ROUNDS = Settings.security.bcryptRounds || 12 const BCRYPT_MINOR_VERSION = Settings.security.bcryptMinorVersion || 'a' -const _checkWriteResult = function (result, callback) { - // for MongoDB - if (result && result.nModified === 1) { - callback(null, true) - } else { - callback(null, false) - } +const _checkWriteResult = function(result, callback) { + // for MongoDB + if (result && result.modifiedCount === 1) { + callback(null, true) + } else { + callback(null, false) + } } const AuthenticationManager = { authenticate(query, password, callback) { // Using Mongoose for legacy reasons here. The returned User instance // gets serialized into the session and there may be subtle differences - // between the user returned by Mongoose vs mongojs (such as default values) + // between the user returned by Mongoose vs mongodb (such as default values) User.findOne(query, (error, user) => { //console.log("Begining:" + JSON.stringify(query)) AuthenticationManager.authUserObj(error, user, query, password, callback) @@ -47,7 +46,7 @@ const AuthenticationManager = { if (err) { return callback(err) } - callback(null, user) + callback(null, user) } ) }, @@ -57,7 +56,7 @@ const AuthenticationManager = { //console.log("Creating User:" + JSON.stringify(query)) //create random pass for local userdb, does not get checked for ldap users during login let pass = require("crypto").randomBytes(32).toString("hex") - console.log("Creating User:" + JSON.stringify(query) + "Random Pass" + pass) + //console.log("Creating User:" + JSON.stringify(query) + "Random Pass" + pass) const userRegHand = require('../User/UserRegistrationHandler.js') userRegHand.registerNewUser({ @@ -117,20 +116,19 @@ const AuthenticationManager = { // therefore we do not enforce checks here const parsed = EmailHelper.parseEmail(email) //if (!parsed) { - // return new InvalidEmailError({message: 'email not valid'}) + // return new InvalidEmailError({ message: 'email not valid' }) //} return null }, // validates a password based on a similar set of rules to `complexPassword.js` on the frontend - // note that `passfield.js` enforces more rules than this, but these are the most commonly set - // returns null on success, or an error string. - // Actually we do not need this because we always use the ldap backend - validatePassword(password) { + // note that `passfield.js` enforces more rules than this, but these are the most commonly set. + // returns null on success, or an error object. + validatePassword(password, email) { if (password == null) { return new InvalidPasswordError({ message: 'password not set', - info: {code: 'not_set'} + info: { code: 'not_set' } }) } @@ -154,13 +152,13 @@ const AuthenticationManager = { if (password.length < min) { return new InvalidPasswordError({ message: 'password is too short', - info: {code: 'too_short'} + info: { code: 'too_short' } }) } if (password.length > max) { return new InvalidPasswordError({ message: 'password is too long', - info: {code: 'too_long'} + info: { code: 'too_long' } }) } if ( @@ -168,203 +166,195 @@ const AuthenticationManager = { !AuthenticationManager._passwordCharactersAreValid(password) ) { return new InvalidPasswordError({ - message: 'password contains an invalid character', - info: {code: 'invalid_character'} - }) + message: 'password contains an invalid character', + info: { code: 'invalid_character' } + }) } return null }, - setUserPassword(userId, password, callback) { - AuthenticationManager.setUserPasswordInV2(userId, password, callback) - }, + setUserPassword(user, password, callback) { + AuthenticationManager.setUserPasswordInV2(user, password, callback) + }, - checkRounds(user, hashedPassword, password, callback) { - // Temporarily disable this function, TODO: re-enable this - return callback() - if (Settings.security.disableBcryptRoundsUpgrades) { - return callback() + checkRounds(user, hashedPassword, password, callback) { + // Temporarily disable this function, TODO: re-enable this + //return callback() + if (Settings.security.disableBcryptRoundsUpgrades) { + return callback() + } + // check current number of rounds and rehash if necessary + const currentRounds = bcrypt.getRounds(hashedPassword) + if (currentRounds < BCRYPT_ROUNDS) { + AuthenticationManager.setUserPassword(user, password, callback) + } else { + callback() + } + }, + + hashPassword(password, callback) { + bcrypt.genSalt(BCRYPT_ROUNDS, BCRYPT_MINOR_VERSION, function(error, salt) { + if (error) { + return callback(error) + } + bcrypt.hash(password, salt, callback) + }) + }, + + setUserPasswordInV2(user, password, callback) { + //if (!user || !user.email || !user._id) { + // return callback(new Error('invalid user object')) + //} + + console.log("Setting pass for user: " + JSON.stringify(user)) + const validationError = this.validatePassword(password, user.email) + if (validationError) { + return callback(validationError) + } + this.hashPassword(password, function(error, hash) { + if (error) { + return callback(error) + } + db.users.updateOne( + { + _id: ObjectId(user._id.toString()) + }, + { + $set: { + hashedPassword: hash + }, + $unset: { + password: true + } + }, + function(updateError, result) { + if (updateError) { + return callback(updateError) + } + _checkWriteResult(result, callback) } - // check current number of rounds and rehash if necessary - const currentRounds = bcrypt.getRounds(hashedPassword) - if (currentRounds < BCRYPT_ROUNDS) { - AuthenticationManager.setUserPassword(user._id, password, callback) - } else { - callback() - } - }, + ) + }) + }, - hashPassword(password, callback) { - bcrypt.genSalt(BCRYPT_ROUNDS, BCRYPT_MINOR_VERSION, function (error, salt) { - if (error) { - return callback(error) - } - bcrypt.hash(password, salt, callback) - }) - }, + _passwordCharactersAreValid(password) { + let digits, letters, lettersUp, symbols + if ( + Settings.passwordStrengthOptions && + Settings.passwordStrengthOptions.chars + ) { + digits = Settings.passwordStrengthOptions.chars.digits + letters = Settings.passwordStrengthOptions.chars.letters + lettersUp = Settings.passwordStrengthOptions.chars.letters_up + symbols = Settings.passwordStrengthOptions.chars.symbols + } + digits = digits || '1234567890' + letters = letters || 'abcdefghijklmnopqrstuvwxyz' + lettersUp = lettersUp || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + symbols = symbols || '@#$%^&*()-_=+[]{};:<>/?!£€.,' - setUserPasswordInV2(userId, password, callback) { - const validationError = this.validatePassword(password) - if (validationError) { - return callback(validationError) - } - this.hashPassword(password, function (error, hash) { - if (error) { - return callback(error) - } - db.users.update( - { - _id: ObjectId(userId.toString()) - }, - { - $set: { - hashedPassword: hash - }, - $unset: { - password: true - } - }, - function (updateError, result) { - if (updateError) { - return callback(updateError) - } - _checkWriteResult(result, callback) - } - ) - }) - }, + for (let charIndex = 0; charIndex <= password.length - 1; charIndex++) { + if ( + digits.indexOf(password[charIndex]) === -1 && + letters.indexOf(password[charIndex]) === -1 && + lettersUp.indexOf(password[charIndex]) === -1 && + symbols.indexOf(password[charIndex]) === -1 + ) { + return false + } + } + return true + }, - setUserPasswordInV1(v1UserId, password, callback) { - const validationError = this.validatePassword(password) - if (validationError) { - return callback(validationError.message) - } - - V1Handler.doPasswordReset(v1UserId, password, function (error, reset) { - if (error) { - return callback(error) - } - callback(error, reset) - }) - }, - - _passwordCharactersAreValid(password) { - let digits, letters, lettersUp, symbols - if ( - Settings.passwordStrengthOptions && - Settings.passwordStrengthOptions.chars - ) { - digits = Settings.passwordStrengthOptions.chars.digits - letters = Settings.passwordStrengthOptions.chars.letters - lettersUp = Settings.passwordStrengthOptions.chars.letters_up - symbols = Settings.passwordStrengthOptions.chars.symbols - } - digits = digits || '1234567890' - letters = letters || 'abcdefghijklmnopqrstuvwxyz' - lettersUp = lettersUp || 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - symbols = symbols || '@#$%^&*()-_=+[]{};:<>/?!£€.,' - - for (let charIndex = 0; charIndex <= password.length - 1; charIndex++) { - if ( - digits.indexOf(password[charIndex]) === -1 && - letters.indexOf(password[charIndex]) === -1 && - lettersUp.indexOf(password[charIndex]) === -1 && - symbols.indexOf(password[charIndex]) === -1 - ) { - return false - } - } - return true - }, - - async ldapAuth(query, password, onSuccessCreateUserIfNotExistent, callback, user) { - const client = new Client({ - url: process.env.LDAP_SERVER, + async ldapAuth(query, password, onSuccessCreateUserIfNotExistent, callback, user) { + const client = new Client({ + url: process.env.LDAP_SERVER, + }); + //const bindDn = process.env.LDAP_BIND_USER + //const bindPassword = process.env.LDAP_BIND_PW + const ldap_bd = process.env.LDAP_BINDDN + const ldap_base = process.env.LDAP_BASE + const uid = query.email.split('@')[0] + const filterstr = '(&' + process.env.LDAP_GROUP_FILTER + '(uid=' + uid + '))' + const userDn = 'uid=' + uid + ',' + ldap_bd; + var mail = "" + var firstname = "" + var lastname = "" + var isAdmin = false + // check bind + try { + //await client.bind(bindDn, bindPassword); + await client.bind(userDn,password); + } catch (ex) { + console.log("Could not bind user." + String(ex)) + return callback(null, null) + } + // get user data + try { + const {searchEntries, searchRef,} = await client.search(ldap_base, { + scope: 'sub', + filter: filterstr , + }); + await searchEntries + //console.log(JSON.stringify(searchEntries)) + if (searchEntries[0]) { + mail = searchEntries[0].mail + firstname = searchEntries[0].givenName + lastname = searchEntries[0].sn + //console.log("Found user: " + mail + " Name: " + firstname + " " + lastname) + } + } catch (ex) { + console.log("An Error occured while getting user data during ldapsearch: " + String(ex)) + await client.unbind(); + return callback(null, null) + } + try { + // if admin filter is set - only set admin for user in ldap group + // does not matter - admin is deactivated: managed through ldap + if (process.env.LDAP_ADMIN_GROUP_FILTER) { + const adminfilter = '(&' + process.env.LDAP_ADMIN_GROUP_FILTER + '(uid=' + uid + '))' + adminEntry = await client.search(ldap_base, { + scope: 'sub', + filter: adminfilter, }); - //const bindDn = process.env.LDAP_BIND_USER - //const bindPassword = process.env.LDAP_BIND_PW - const ldap_bd = process.env.LDAP_BINDDN - const ldap_base = process.env.LDAP_BASE - const uid = query.email.split('@')[0] - const filterstr = '(&' + process.env.LDAP_GROUP_FILTER + '(uid=' + uid + '))' - const userDn = 'uid=' + uid + ',' + ldap_bd; - var mail = "" - var firstname = "" - var lastname = "" - var isAdmin = false - // check bind - try { - //await client.bind(bindDn, bindPassword); - await client.bind(userDn,password); - } catch (ex) { - console.log("Could not bind user." + String(ex)) - return callback(null, null) + await adminEntry; + //console.log("Admin Search response:" + JSON.stringify(adminEntry.searchEntries)) + if (adminEntry.searchEntries[0].mail) { + console.log("is Admin") + isAdmin=true; } - // get user data - try { - const {searchEntries, searchRef,} = await client.search(ldap_base, { - scope: 'sub', - filter: filterstr , - }); - await searchEntries - console.log(JSON.stringify(searchEntries)) - if (searchEntries[0]) { - mail = searchEntries[0].mail - firstname = searchEntries[0].givenName - lastname = searchEntries[0].sn - console.log("Found user: " + mail + " Name: " + firstname + " " + lastname) - } - } catch (ex) { - console.log("An Error occured while getting user data during ldapsearch: " + String(ex)) - await client.unbind(); - return callback(null, null) - } - try { - // if admin filter is set - only set admin for user in ldap group - // does not matter - admin is deactivated: managed through ldap - if (process.env.LDAP_ADMIN_GROUP_FILTER) { - const adminfilter = '(&' + process.env.LDAP_ADMIN_GROUP_FILTER + '(uid=' + uid + '))' - adminEntry = await client.search(ldap_base, { - scope: 'sub', - filter: adminfilter, - }); - await adminEntry; - console.log("Admin Search response:" + JSON.stringify(adminEntry.searchEntries)) - if (adminEntry.searchEntries[0].mail) { - console.log("is Admin") - isAdmin=true; - } - } - } catch (ex) { - console.log("An Error occured while checking for admin rights - setting admin rights to false: " + String(ex)) - isAdmin = false; - } finally { - await client.unbind(); - } - if (mail == "") { - console.log("Mail not set - exit. This should not happen - please set mail-entry in ldap.") - return callback(null, null) - } - console.log("Logging in iser: " + mail + " Name: " + firstname + " " + lastname + " isAdmin: " + String(isAdmin)) - // we are authenticated now let's set the query to the correct mail from ldap - query.email = mail - User.findOne(query, (error, user) => { - if (error) { - console.log(error) - } - if (user && user.hashedPassword) { - //console.log("******************** LOGIN ******************") - AuthenticationManager.login(user, "randomPass", callback) - } else { - onSuccessCreateUserIfNotExistent(query, user, callback, uid, firstname, lastname, mail, isAdmin) - } - }) - } + } + } catch (ex) { + console.log("An Error occured while checking for admin rights - setting admin rights to false: " + String(ex)) + isAdmin = false; + } finally { + await client.unbind(); + } + if (mail == "") { + console.log("Mail not set - exit. This should not happen - please set mail-entry in ldap.") + return callback(null, null) + } + //console.log("Logging in user: " + mail + " Name: " + firstname + " " + lastname + " isAdmin: " + String(isAdmin)) + // we are authenticated now let's set the query to the correct mail from ldap + query.email = mail + User.findOne(query, (error, user) => { + if (error) { + console.log(error) + } + if (user && user.hashedPassword) { + //console.log("******************** LOGIN ******************") + AuthenticationManager.login(user, "randomPass", callback) + } else { + onSuccessCreateUserIfNotExistent(query, user, callback, uid, firstname, lastname, mail, isAdmin) + } + }) + } } AuthenticationManager.promises = { - authenticate: util.promisify(AuthenticationManager.authenticate), - hashPassword: util.promisify(AuthenticationManager.hashPassword) + authenticate: util.promisify(AuthenticationManager.authenticate), + hashPassword: util.promisify(AuthenticationManager.hashPassword), + setUserPassword: util.promisify(AuthenticationManager.setUserPassword) } module.exports = AuthenticationManager