From a6448a51e9d4f41e02daf65ceaf02f006d724eb3 Mon Sep 17 00:00:00 2001 From: yzx9 Date: Thu, 20 Jul 2023 18:46:13 +0800 Subject: [PATCH 01/10] Sync changes from upstream --- docker-compose.certbot.yml | 16 +- .../sharelatex/AuthenticationManager.js | 716 +++++++++++++----- .../sharelatex/ContactController.js | 253 +++---- 3 files changed, 651 insertions(+), 334 deletions(-) diff --git a/docker-compose.certbot.yml b/docker-compose.certbot.yml index de13fa6..eb1d81c 100644 --- a/docker-compose.certbot.yml +++ b/docker-compose.certbot.yml @@ -94,9 +94,9 @@ services: mongo: restart: always - image: mongo + image: mongo:4.4 container_name: mongo - ports: + expose: - 27017 volumes: - ${MYDATA}/mongo_data:/data/db @@ -108,17 +108,9 @@ services: redis: restart: always - image: redis:5.0.0 + image: redis:6.2 container_name: redis - # modify to get rid of the redis issue #35 and #19 with a better solution - # WARNING: /proc/sys/net/core/somaxconn is set to the lower value of 128. - # for vm overcommit: enable first on host system - # sysctl vm.overcommit_memory=1 (and add it to rc.local) - # then you do not need it in the redis container - sysctls: - - net.core.somaxconn=65535 - # - vm.overcommit_memory=1 - ports: + expose: - 6379 volumes: - ${MYDATA}/redis_data:/data diff --git a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js index 0bd6f01..f9457b5 100644 --- a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js +++ b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js @@ -1,3 +1,9 @@ +/** + * >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + * Modified from 841df71 + * <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + */ + const Settings = require('@overleaf/settings') const { User } = require('../../models/User') const { db, ObjectId } = require('../../infrastructure/mongodb') @@ -6,19 +12,36 @@ const EmailHelper = require('../Helpers/EmailHelper') const { InvalidEmailError, InvalidPasswordError, + ParallelLoginError, + PasswordMustBeDifferentError, + PasswordReusedError, } = require('./AuthenticationErrors') const util = require('util') +const HaveIBeenPwned = require('./HaveIBeenPwned') +const UserAuditLogHandler = require('../User/UserAuditLogHandler') +const logger = require('@overleaf/logger') +const DiffHelper = require('../Helpers/DiffHelper') +const Metrics = require('@overleaf/metrics') -const { Client } = require('ldapts'); -const ldapEscape = require('ldap-escape'); - -// https://www.npmjs.com/package/@overleaf/o-error -// have a look if we can do nice error messages. +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +const { Client } = require("ldapts") +const ldapEscape = require("ldap-escape") +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< const BCRYPT_ROUNDS = Settings.security.bcryptRounds || 12 const BCRYPT_MINOR_VERSION = Settings.security.bcryptMinorVersion || 'a' +const MAX_SIMILARITY = 0.7 -const _checkWriteResult = function(result, callback) { +function _exceedsMaximumLengthRatio(password, maxSimilarity, value) { + const passwordLength = password.length + const lengthBoundSimilarity = (maxSimilarity / 2) * passwordLength + const valueLength = value.length + return ( + passwordLength >= 10 * valueLength && valueLength < lengthBoundSimilarity + ) +} + +const _checkWriteResult = function (result, callback) { // for MongoDB if (result && result.modifiedCount === 1) { callback(null, true) @@ -27,17 +50,146 @@ const _checkWriteResult = function(result, callback) { } } +function _validatePasswordNotTooLong(password) { + // bcrypt has a hard limit of 72 characters. + if (password.length > 72) { + return new InvalidPasswordError({ + message: 'password is too long', + info: { code: 'too_long' }, + }) + } + return null +} + +function _metricsForSuccessfulPasswordMatch(password) { + const validationResult = AuthenticationManager.validatePassword(password) + const status = + validationResult === null ? 'success' : validationResult?.info?.code + Metrics.inc('check-password', { status }) + return null +} + const AuthenticationManager = { - authenticate(query, password, callback) { + _checkUserPassword(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 mongodb (such as default values) User.findOne(query, (error, user) => { - //console.log("Begining:" + JSON.stringify(query)) - AuthenticationManager.authUserObj(error, user, query, password, callback) + if (error) { + return callback(error) + } +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + if (!process.env.ALLOW_EMAIL_LOGIN || !user || !user.hashedPassword) { + // No local passwd check user has to be in ldap and use ldap credentials + return AuthenticationManager.ldapAuth( + query, + password, + AuthenticationManager.createIfNotExistAndLogin, + callback, + user + ) + } + console.log("email login for existing user " + query.email) +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + bcrypt.compare(password, user.hashedPassword, function (error, match) { + if (error) { + return callback(error) + } + if (match) { + _metricsForSuccessfulPasswordMatch(password) + } +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + else { + console.log("Local user password mismatch, trying LDAP") + // check passwd against ldap + return AuthenticationManager.ldapAuth( + query, + password, + AuthenticationManager.createIfNotExistAndLogin, + callback, + user + ) + } +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + callback(null, user, match) + }) }) }, - //login with any password + + authenticate(query, password, auditLog, callback) { + if (typeof callback === 'undefined') { + callback = auditLog + auditLog = null + } + AuthenticationManager._checkUserPassword( + query, + password, + (error, user, match) => { + if (error) { + return callback(error) + } + if (!user) { + return callback(null, null) + } + const update = { $inc: { loginEpoch: 1 } } + if (!match) { + update.$set = { lastFailedLogin: new Date() } + } + User.updateOne( + { _id: user._id, loginEpoch: user.loginEpoch }, + update, + {}, + (err, result) => { + if (err) { + return callback(err) + } + if (result.modifiedCount !== 1) { + return callback(new ParallelLoginError()) + } + if (!match) { + if (!auditLog) { + return callback(null, null) + } else { + return UserAuditLogHandler.addEntry( + user._id, + 'failed-password-match', + user._id, + auditLog.ipAddress, + auditLog.info, + err => { + if (err) { + logger.error( + { userId: user._id, err, info: auditLog.info }, + 'Error while adding AuditLog entry for failed-password-match' + ) + } + callback(null, null) + } + ) + } + } + AuthenticationManager.checkRounds( + user, + user.hashedPassword, + password, + function (err) { + if (err) { + return callback(err) + } + callback(null, user) + HaveIBeenPwned.checkPasswordForReuseInBackground(password) + } + ) + } + ) + } + ) + }, + +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + /** + * login with any password + */ login(user, password, callback) { AuthenticationManager.checkRounds( user, @@ -48,64 +200,87 @@ const AuthenticationManager = { return callback(err) } callback(null, user) - } + } ) }, - createIfNotExistAndLogin(query, user, callback, uid, firstname, lastname, mail, isAdmin) { + createIfNotExistAndLogin( + query, + user, + callback, + uid, + firstname, + lastname, + mail, + isAdmin + ) { if (!user) { - //console.log("Creating User:" + JSON.stringify(query)) + //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({ - email: mail, - first_name: firstname, - last_name: lastname, - password: pass - }, - function (error, user) { - if (error) { - console.log(error) - } - user.email = mail - user.isAdmin = isAdmin - user.emails[0].confirmedAt = Date.now() - user.save() - //console.log("user %s added to local library: ", mail) - User.findOne(query, (error, user) => { + const userRegHand = require("../User/UserRegistrationHandler.js") + userRegHand.registerNewUser( + { + email: mail, + first_name: firstname, + last_name: lastname, + password: pass, + }, + function (error, user) { if (error) { console.log(error) } - if (user && user.hashedPassword) { - AuthenticationManager.login(user, "randomPass", callback) - } - }) - }) // end register user + user.email = mail + user.isAdmin = isAdmin + user.emails[0].confirmedAt = Date.now() + user.save() + //console.log('user %s added to local library: ', mail) + User.findOne(query, (error, user) => { + if (error) { + console.log(error) + } + if (user && user.hashedPassword) { + AuthenticationManager.login(user, "randomPass", callback) + } + }) + } + ) // end register user } else { AuthenticationManager.login(user, "randomPass", callback) } }, authUserObj(error, user, query, password, callback) { - if ( process.env.ALLOW_EMAIL_LOGIN && user && user.hashedPassword) { - console.log("email login for existing user " + query.email) - // check passwd against local db - bcrypt.compare(password, user.hashedPassword, function (error, match) { - if (match) { - console.log("Local user password match") - AuthenticationManager.login(user, password, callback) - } else { - console.log("Local user password mismatch, trying LDAP") - // check passwd against ldap - AuthenticationManager.ldapAuth(query, password, AuthenticationManager.createIfNotExistAndLogin, callback, user) - } - }) + if (process.env.ALLOW_EMAIL_LOGIN && user && user.hashedPassword) { + console.log("email login for existing user " + query.email) + // check passwd against local db + bcrypt.compare(password, user.hashedPassword, function (error, match) { + if (match) { + console.log("Local user password match") + AuthenticationManager.login(user, password, callback) + } else { + console.log("Local user password mismatch, trying LDAP") + // check passwd against ldap + AuthenticationManager.ldapAuth( + query, + password, + AuthenticationManager.createIfNotExistAndLogin, + callback, + user + ) + } + }) } else { // No local passwd check user has to be in ldap and use ldap credentials - AuthenticationManager.ldapAuth(query, password, AuthenticationManager.createIfNotExistAndLogin, callback, user) + AuthenticationManager.ldapAuth( + query, + password, + AuthenticationManager.createIfNotExistAndLogin, + callback, + user + ) } return null }, @@ -120,6 +295,161 @@ const AuthenticationManager = { return null }, + async ldapAuth( + query, + password, + onSuccessCreateUserIfNotExistent, + callback, + user + ) { + const client = new Client({ + url: process.env.LDAP_SERVER, + }) + + const ldap_reader = process.env.LDAP_BIND_USER + const ldap_reader_pass = process.env.LDAP_BIND_PW + const ldap_base = process.env.LDAP_BASE + + var mail = query.email + var uid = query.email.split("@")[0] + var firstname = "" + var lastname = "" + var isAdmin = false + var userDn = "" + + //replace all appearences of %u with uid and all %m with mail: + const replacerUid = new RegExp("%u", "g") + const replacerMail = new RegExp("%m", "g") + const filterstr = process.env.LDAP_USER_FILTER.replace( + replacerUid, + ldapEscape.filter`${uid}` + ).replace(replacerMail, ldapEscape.filter`${mail}`) //replace all appearances + // check bind + try { + if (process.env.LDAP_BINDDN) { + //try to bind directly with the user trying to log in + userDn = process.env.LDAP_BINDDN.replace( + replacerUid, + ldapEscape.filter`${uid}` + ).replace(replacerMail, ldapEscape.filter`${mail}`) + await client.bind(userDn, password) + } else { + // use fixed bind user + await client.bind(ldap_reader, ldap_reader_pass) + } + } catch (ex) { + if (process.env.LDAP_BINDDN) { + console.log("Could not bind user: " + userDn) + } else { + console.log( + "Could not bind LDAP reader: " + ldap_reader + " err: " + 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 + uid = searchEntries[0].uid + firstname = searchEntries[0].givenName + lastname = searchEntries[0].sn + if (!process.env.LDAP_BINDDN) { + //dn is already correctly assembled + userDn = searchEntries[0].dn + } + console.log( + `Found user: ${mail} Name: ${firstname} ${lastname} DN: ${userDn}` + ) + } + } 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.replace( + replacerUid, + ldapEscape.filter`${uid}` + ).replace(replacerMail, ldapEscape.filter`${mail}`) + 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]) { + 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 == "" || userDn == "") { + console.log( + "Mail / userDn not set - exit. This should not happen - please set mail-entry in ldap." + ) + return callback(null, null) + } + + if (!process.env.BINDDN) { + //since we used a fixed bind user to obtain the correct userDn we need to bind again to authenticate + try { + await client.bind(userDn, password) + } catch (ex) { + console.log("Could not bind User: " + userDn + " err: " + String(ex)) + return callback(null, null) + } finally { + await client.unbind() + } + } + //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 + ) + } + }) + }, +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + // 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 object. @@ -131,6 +461,8 @@ const AuthenticationManager = { }) } + Metrics.inc('try-validate-password') + let allowAnyChars, min, max if (Settings.passwordStrengthOptions) { allowAnyChars = Settings.passwordStrengthOptions.allowAnyChars === true @@ -140,7 +472,7 @@ const AuthenticationManager = { } } allowAnyChars = !!allowAnyChars - min = min || 6 + min = min || 8 max = max || 72 // we don't support passwords > 72 characters in length, because bcrypt truncates them @@ -160,6 +492,10 @@ const AuthenticationManager = { info: { code: 'too_long' }, }) } + const passwordLengthError = _validatePasswordNotTooLong(password) + if (passwordLengthError) { + return passwordLengthError + } if ( !allowAnyChars && !AuthenticationManager._passwordCharactersAreValid(password) @@ -168,9 +504,39 @@ const AuthenticationManager = { message: 'password contains an invalid character', info: { code: 'invalid_character' }, }) + } + if (typeof email === 'string' && email !== '') { + const startOfEmail = email.split('@')[0] + if ( + password.includes(email) || + password.includes(startOfEmail) || + email.includes(password) + ) { + return new InvalidPasswordError({ + message: 'password contains part of email address', + info: { code: 'contains_email' }, + }) } - return null - }, + try { + const passwordTooSimilarError = + AuthenticationManager._validatePasswordNotTooSimilar(password, email) + if (passwordTooSimilarError) { + Metrics.inc('password-too-similar-to-email') + return new InvalidPasswordError({ + message: 'password is too similar to email address', + info: { code: 'too_similar' }, + }) + } + } catch (error) { + logger.error( + { error }, + 'error while checking password similarity to email' + ) + } + // TODO: remove this check once the password-too-similar checks are active? + } + return null + }, setUserPassword(user, password, callback) { AuthenticationManager.setUserPasswordInV2(user, password, callback) @@ -178,20 +544,24 @@ const AuthenticationManager = { 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) + AuthenticationManager._setUserPasswordInMongo(user, password, callback) } else { callback() } }, hashPassword(password, callback) { + // Double-check the size to avoid truncating in bcrypt. + const error = _validatePasswordNotTooLong(password) + if (error) { + return callback(error) + } bcrypt.genSalt(BCRYPT_ROUNDS, BCRYPT_MINOR_VERSION, function (error, salt) { if (error) { return callback(error) @@ -201,39 +571,46 @@ const AuthenticationManager = { }, 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)) + if (!user || !user.email || !user._id) { + return callback(new Error('invalid user object')) + } 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 if we can log in with this password. In which case we should reject it, + // because it is the same as the existing password. + AuthenticationManager._checkUserPassword( + { _id: user._id }, + password, + (err, _user, match) => { + if (err) { + return callback(err) } - ) - }) + if (match) { + return callback(new PasswordMustBeDifferentError()) + } + + HaveIBeenPwned.checkPasswordForReuse( + password, + (error, isPasswordReused) => { + if (error) { + logger.err({ error }, 'cannot check password for re-use') + } + + if (!error && isPasswordReused) { + return callback(new PasswordReusedError()) + } + + // password is strong enough or the validation with the service did not happen + this._setUserPasswordInMongo(user, password, callback) + } + ) + } + ) + }, + + _setUserPasswordInMongo(user, password, callback) { }, _passwordCharactersAreValid(password) { @@ -265,119 +642,76 @@ const AuthenticationManager = { return true }, - async ldapAuth(query, password, onSuccessCreateUserIfNotExistent, callback, user) { - const client = new Client({ - url: process.env.LDAP_SERVER, - }); - - const ldap_reader = process.env.LDAP_BIND_USER - const ldap_reader_pass = process.env.LDAP_BIND_PW - const ldap_base = process.env.LDAP_BASE - - var mail = query.email - var uid = query.email.split('@')[0] - var firstname = "" - var lastname = "" - var isAdmin = false - var userDn = "" - - //replace all appearences of %u with uid and all %m with mail: - const replacerUid = new RegExp("%u", "g") - const replacerMail = new RegExp("%m","g") - const filterstr = process.env.LDAP_USER_FILTER.replace(replacerUid, ldapEscape.filter`${uid}`).replace(replacerMail, ldapEscape.filter`${mail}`) //replace all appearances - // check bind - try { - if(process.env.LDAP_BINDDN){ //try to bind directly with the user trying to log in - userDn = process.env.LDAP_BINDDN.replace(replacerUid,ldapEscape.filter`${uid}`).replace(replacerMail, ldapEscape.filter`${mail}`); - await client.bind(userDn,password); - }else{// use fixed bind user - await client.bind(ldap_reader, ldap_reader_pass); - } - } catch (ex) { - if(process.env.LDAP_BINDDN){ - console.log("Could not bind user: " + userDn); - }else{ - console.log("Could not bind LDAP reader: " + ldap_reader + " err: " + 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 - uid = searchEntries[0].uid - firstname = searchEntries[0].givenName - lastname = searchEntries[0].sn - if(!process.env.LDAP_BINDDN){ //dn is already correctly assembled - userDn = searchEntries[0].dn - } - console.log("Found user: " + mail + " Name: " + firstname + " " + lastname + " DN: " + userDn) - } - } 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.replace(replacerUid, ldapEscape.filter`${uid}`).replace(replacerMail, ldapEscape.filter`${mail}`) - 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]) { - console.log("is Admin") - isAdmin=true; + /** + * Check if the password is similar to (parts of) the email address. + * For now, this merely sends a metric when the password and + * email address are deemed to be too similar to each other. + * Later we will reject passwords that fail this check. + * + * This logic was borrowed from the django project: + * https://github.com/django/django/blob/fa3afc5d86f1f040922cca2029d6a34301597a70/django/contrib/auth/password_validation.py#L159-L214 + */ + _validatePasswordNotTooSimilar(password, email) { + password = password.toLowerCase() + email = email.toLowerCase() + const stringsToCheck = [email] + .concat(email.split(/\W+/)) + .concat(email.split(/@/)) + for (const emailPart of stringsToCheck) { + if (!_exceedsMaximumLengthRatio(password, MAX_SIMILARITY, emailPart)) { + const similarity = DiffHelper.stringSimilarity(password, emailPart) + if (similarity > MAX_SIMILARITY) { + logger.warn( + { email, emailPart, similarity, maxSimilarity: MAX_SIMILARITY }, + 'Password too similar to email' + ) + return new Error('password is too similar to email') } } - } 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 == "" || userDn == "") { - console.log("Mail / userDn not set - exit. This should not happen - please set mail-entry in ldap.") - return callback(null, null) } + }, - if(!process.env.BINDDN){//since we used a fixed bind user to obtain the correct userDn we need to bind again to authenticate - try { - await client.bind(userDn, password); - } catch (ex) { - console.log("Could not bind User: " + userDn + " err: " + String(ex)) - return callback(null, null) - } finally{ - await client.unbind() - } + getMessageForInvalidPasswordError(error, req) { + const errorCode = error?.info?.code + const message = { + type: 'error', } - //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) - } - }) - } + switch (errorCode) { + case 'not_set': + message.key = 'password-not-set' + message.text = req.i18n.translate('invalid_password_not_set') + break + case 'invalid_character': + message.key = 'password-invalid-character' + message.text = req.i18n.translate('invalid_password_invalid_character') + break + case 'contains_email': + message.key = 'password-contains-email' + message.text = req.i18n.translate('invalid_password_contains_email') + break + case 'too_similar': + message.key = 'password-too-similar' + message.text = req.i18n.translate('invalid_password_too_similar') + break + case 'too_short': + message.key = 'password-too-short' + message.text = req.i18n.translate('invalid_password_too_short', { + minLength: Settings.passwordStrengthOptions?.length?.min || 8, + }) + break + case 'too_long': + message.key = 'password-too-long' + message.text = req.i18n.translate('invalid_password_too_long', { + maxLength: Settings.passwordStrengthOptions?.length?.max || 72, + }) + break + default: + logger.error({ err: error }, 'Unknown password validation error code') + message.text = req.i18n.translate('invalid_password') + break + } + return message + }, } AuthenticationManager.promises = { diff --git a/ldap-overleaf-sl/sharelatex/ContactController.js b/ldap-overleaf-sl/sharelatex/ContactController.js index 3b47d51..6d7be8d 100644 --- a/ldap-overleaf-sl/sharelatex/ContactController.js +++ b/ldap-overleaf-sl/sharelatex/ContactController.js @@ -1,139 +1,130 @@ -/* eslint-disable - camelcase, - max-len, - no-unused-vars, -*/ -// TODO: This file was created by bulk-decaffeinate. -// Fix any style issues and re-enable lint. -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md +/** + * >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + * Modified from 906765c + * <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< */ -let ContactsController -const AuthenticationController = require('../Authentication/AuthenticationController') + const SessionManager = require('../Authentication/SessionManager') const ContactManager = require('./ContactManager') const UserGetter = require('../User/UserGetter') -const logger = require('@overleaf/logger') const Modules = require('../../infrastructure/Modules') -const { Client } = require('ldapts'); +const { expressify } = require('../../util/promises') -module.exports = ContactsController = { - getContacts(req, res, next) { - const user_id = SessionManager.getLoggedInUserId(req.session) - return ContactManager.getContactIds( - user_id, - { limit: 50 }, - function (error, contact_ids) { - if (error != null) { - return next(error) - } - return UserGetter.getUsers( - contact_ids, - { - email: 1, - first_name: 1, - last_name: 1, - holdingAccount: 1, - }, - function (error, contacts) { - if (error != null) { - return next(error) - } +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +const { Client } = require('ldapts') +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - // UserGetter.getUsers may not preserve order so put them back in order - const positions = {} - for (let i = 0; i < contact_ids.length; i++) { - const contact_id = contact_ids[i] - positions[contact_id] = i - } - contacts.sort( - (a, b) => - positions[a._id != null ? a._id.toString() : undefined] - - positions[b._id != null ? b._id.toString() : undefined] - ) - - // Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc) - contacts = contacts.filter(c => !c.holdingAccount) - ContactsController.getLdapContacts(contacts).then((ldapcontacts) => { - contacts.push(ldapcontacts) - contacts = contacts.map(ContactsController._formatContact) - - return Modules.hooks.fire('getContacts', user_id, contacts, function( - error, - additional_contacts - ) { - if (error != null) { - return next(error) - } - contacts = contacts.concat(...Array.from(additional_contacts || [])) - return res.send({ - contacts - }) - }) - }).catch(e => console.log("Error appending ldap contacts" + e)) - - } - ) - }) - }, - async getLdapContacts(contacts) { - if (process.env.LDAP_CONTACTS === undefined || !(process.env.LDAP_CONTACTS.toLowerCase() === 'true')) { - return contacts - } - const client = new Client({ - url: process.env.LDAP_SERVER, - }); - - // if we need a ldap user try to bind - if (process.env.LDAP_BIND_USER) { - try { - await client.bind(process.env.LDAP_BIND_USER, process.env.LDAP_BIND_PW); - } catch (ex) { - console.log("Could not bind LDAP reader user: " + String(ex) ) - } - } - - const ldap_base = process.env.LDAP_BASE - // get user data - try { - // if you need an client.bind do it here. - const {searchEntries,searchReferences,} = await client.search(ldap_base, {scope: 'sub',filter: process.env.LDAP_CONTACT_FILTER ,}); - await searchEntries; - for (var i = 0; i < searchEntries.length; i++) { - var entry = new Map() - var obj = searchEntries[i]; - entry['_id'] = undefined - entry['email'] = obj['mail'] - entry['first_name'] = obj['givenName'] - entry['last_name'] = obj['sn'] - entry['type'] = "user" - // Only add to contacts if entry is not there. - if(contacts.indexOf(entry) === -1) { - contacts.push(entry); - } - } - } catch (ex) { - console.log(String(ex)) - } - //console.log(JSON.stringify(contacts)) - finally { - // even if we did not use bind - the constructor of - // new Client() opens a socket to the ldap server - client.unbind() - return contacts - } - }, - _formatContact(contact) { - return { - id: contact._id != null ? contact._id.toString() : undefined, - email: contact.email || '', - first_name: contact.first_name || '', - last_name: contact.last_name || '', - type: 'user', - } - }, +function _formatContact(contact) { + return { + id: contact._id?.toString(), + email: contact.email || '', + first_name: contact.first_name || '', + last_name: contact.last_name || '', + type: 'user', + } +} + +async function getContacts(req, res) { + const userId = SessionManager.getLoggedInUserId(req.session) + + const contactIds = await ContactManager.promises.getContactIds(userId, { + limit: 50, + }) + + let contacts = await UserGetter.promises.getUsers(contactIds, { + email: 1, + first_name: 1, + last_name: 1, + holdingAccount: 1, + }) + + // UserGetter.getUsers may not preserve order so put them back in order + const positions = {} + for (let i = 0; i < contactIds.length; i++) { + const contact_id = contactIds[i] + positions[contact_id] = i + } + contacts.sort( + (a, b) => positions[a._id?.toString()] - positions[b._id?.toString()] + ) + + // Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc) + contacts = contacts.filter((c) => !c.holdingAccount) + +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + const ldapcontacts = getLdapContacts(contacts) + contacts.push(ldapcontacts) +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + + contacts = contacts.map(_formatContact) + + const additionalContacts = await Modules.promises.hooks.fire( + 'getContacts', + userId, + contacts + ) + + contacts = contacts.concat(...(additionalContacts || [])) + return res.json({ + contacts, + }) +} + +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +async function getLdapContacts(contacts) { + if ( + process.env.LDAP_CONTACTS === undefined || + !(process.env.LDAP_CONTACTS.toLowerCase() === 'true') + ) { + return contacts + } + const client = new Client({ + url: process.env.LDAP_SERVER, + }) + + // if we need a ldap user try to bind + if (process.env.LDAP_BIND_USER) { + try { + await client.bind(process.env.LDAP_BIND_USER, process.env.LDAP_BIND_PW) + } catch (ex) { + console.log('Could not bind LDAP reader user: ' + String(ex)) + } + } + + const ldap_base = process.env.LDAP_BASE + // get user data + try { + // if you need an client.bind do it here. + const { searchEntries, searchReferences } = await client.search(ldap_base, { + scope: 'sub', + filter: process.env.LDAP_CONTACT_FILTER, + }) + await searchEntries + for (var i = 0; i < searchEntries.length; i++) { + var entry = new Map() + var obj = searchEntries[i] + entry['_id'] = undefined + entry['email'] = obj['mail'] + entry['first_name'] = obj['givenName'] + entry['last_name'] = obj['sn'] + entry['type'] = 'user' + // Only add to contacts if entry is not there. + if (contacts.indexOf(entry) === -1) { + contacts.push(entry) + } + } + } catch (ex) { + console.log(String(ex)) + } finally { + // console.log(JSON.stringify(contacts)) + // even if we did not use bind - the constructor of + // new Client() opens a socket to the ldap server + client.unbind() + return contacts + } +} +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + +module.exports = { + getContacts: expressify(getContacts), } From f645454a74c07ece254a55b9008841d41d83f91e Mon Sep 17 00:00:00 2001 From: yzx9 Date: Sun, 13 Aug 2023 23:48:09 +0800 Subject: [PATCH 02/10] Fix authentication api changes Co-authored-by: gizmo1-11 --- .../sharelatex/AuthenticationManager.js | 119 ++++++++++-------- 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js index f9457b5..7907492 100644 --- a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js +++ b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js @@ -24,6 +24,7 @@ const DiffHelper = require('../Helpers/DiffHelper') const Metrics = require('@overleaf/metrics') // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +const fs = require("fs") const { Client } = require("ldapts") const ldapEscape = require("ldap-escape") // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @@ -78,19 +79,9 @@ const AuthenticationManager = { if (error) { return callback(error) } -// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - if (!process.env.ALLOW_EMAIL_LOGIN || !user || !user.hashedPassword) { - // No local passwd check user has to be in ldap and use ldap credentials - return AuthenticationManager.ldapAuth( - query, - password, - AuthenticationManager.createIfNotExistAndLogin, - callback, - user - ) + if (!user || !user.hashedPassword) { + return callback(null, null, null) } - console.log("email login for existing user " + query.email) -// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< bcrypt.compare(password, user.hashedPassword, function (error, match) { if (error) { return callback(error) @@ -98,30 +89,29 @@ const AuthenticationManager = { if (match) { _metricsForSuccessfulPasswordMatch(password) } -// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - else { - console.log("Local user password mismatch, trying LDAP") - // check passwd against ldap - return AuthenticationManager.ldapAuth( - query, - password, - AuthenticationManager.createIfNotExistAndLogin, - callback, - user - ) - } -// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< callback(null, user, match) }) }) }, +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + _checkUserPassword2(query, password, callback) { + // leave original _checkUserPassword untouched, because it will be called by + // setUserPasswordInV2 (e.g. UserRegistrationHandler.js ) + User.findOne(query, (error, user) => { + AuthenticationManager.authUserObj(error, user, query, password, callback) + }) + }, +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + authenticate(query, password, auditLog, callback) { if (typeof callback === 'undefined') { callback = auditLog auditLog = null } - AuthenticationManager._checkUserPassword( +// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + AuthenticationManager._checkUserPassword2( +// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< query, password, (error, user, match) => { @@ -191,22 +181,12 @@ const AuthenticationManager = { * login with any password */ login(user, password, callback) { - AuthenticationManager.checkRounds( - user, - user.hashedPassword, - password, - function (err) { - if (err) { - return callback(err) - } - callback(null, user) - } - ) + callback(null, user, true) }, createIfNotExistAndLogin( query, - user, + user1, callback, uid, firstname, @@ -214,7 +194,7 @@ const AuthenticationManager = { mail, isAdmin ) { - if (!user) { + if (!user1) { //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") @@ -228,7 +208,7 @@ const AuthenticationManager = { last_name: lastname, password: pass, }, - function (error, user) { + function (error, user, setNewPasswordUrl) { if (error) { console.log(error) } @@ -248,7 +228,7 @@ const AuthenticationManager = { } ) // end register user } else { - AuthenticationManager.login(user, "randomPass", callback) + AuthenticationManager.login(user1, "randomPass", callback) } }, @@ -259,7 +239,9 @@ const AuthenticationManager = { bcrypt.compare(password, user.hashedPassword, function (error, match) { if (match) { console.log("Local user password match") - AuthenticationManager.login(user, password, callback) + _metricsForSuccessfulPasswordMatch(password) + //callback(null, user, match) + AuthenticationManager.login(user, "randomPass", callback) } else { console.log("Local user password mismatch, trying LDAP") // check passwd against ldap @@ -285,16 +267,6 @@ const AuthenticationManager = { return null }, - validateEmail(email) { - // we use the emailadress from the ldap - // therefore we do not enforce checks here - const parsed = EmailHelper.parseEmail(email) - //if (!parsed) { - // return new InvalidEmailError({ message: 'email not valid' }) - //} - return null - }, - async ldapAuth( query, password, @@ -302,9 +274,16 @@ const AuthenticationManager = { callback, user ) { - const client = new Client({ - url: process.env.LDAP_SERVER, - }) + const client = fs.existsSync(process.env.LDAP_SERVER_CACERT) + ? new Client({ + url: process.env.LDAP_SERVER, + tlsOptions: { + ca: [fs.readFileSync(process.env.LDAP_SERVER_CACERT)], + }, + }) + : new Client({ + url: process.env.LDAP_SERVER, + }) const ldap_reader = process.env.LDAP_BIND_USER const ldap_reader_pass = process.env.LDAP_BIND_PW @@ -450,6 +429,14 @@ const AuthenticationManager = { }, // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + validateEmail(email) { + const parsed = EmailHelper.parseEmail(email) + if (!parsed) { + 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 object. @@ -611,6 +598,28 @@ const AuthenticationManager = { }, _setUserPasswordInMongo(user, password, callback) { + 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) + } + ) + }) }, _passwordCharactersAreValid(password) { From c4775c7d7c7283691ef212ae0b47286242e99bde Mon Sep 17 00:00:00 2001 From: yzx9 Date: Mon, 14 Aug 2023 02:22:14 +0800 Subject: [PATCH 03/10] Bump sharelatex from 3.3.2 to 4.0.5 Co-authored-by: gizmo1-11 --- ldap-overleaf-sl/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ldap-overleaf-sl/Dockerfile b/ldap-overleaf-sl/Dockerfile index a2f0965..8e370fd 100644 --- a/ldap-overleaf-sl/Dockerfile +++ b/ldap-overleaf-sl/Dockerfile @@ -1,4 +1,4 @@ -FROM sharelatex/sharelatex:3.3.2 +FROM sharelatex/sharelatex:4.0.5 # FROM sharelatex/sharelatex:latest # latest might not be tested # e.g. the AuthenticationManager.js script had to be adapted after versions 2.3.1 From a99e70f3c4d2d095bff610154122de1924564460 Mon Sep 17 00:00:00 2001 From: yzx9 Date: Mon, 18 Sep 2023 21:12:57 +0800 Subject: [PATCH 04/10] Bump sharelatex to v4.1.1 - bump sharelatex - init mongodb replset - format docker-compose.yml --- docker-compose.certbot.yml | 265 +++++++++++----------- docker-compose.traefik.yml | 427 +++++++++++++++++++----------------- ldap-overleaf-sl/Dockerfile | 2 +- 3 files changed, 363 insertions(+), 331 deletions(-) diff --git a/docker-compose.certbot.yml b/docker-compose.certbot.yml index eb1d81c..3bf1245 100644 --- a/docker-compose.certbot.yml +++ b/docker-compose.certbot.yml @@ -1,142 +1,157 @@ -version: '2.2' +version: "2.2" services: - sharelatex: - restart: always - image: ldap-overleaf-sl - container_name: ldap-overleaf-sl - depends_on: - mongo: - condition: service_healthy - redis: - condition: service_healthy - simple-certbot: - condition: service_started - privileged: false - ports: - - 443:443 - links: - - mongo - - redis - - simple-certbot - volumes: - - ${MYDATA}/sharelatex:/var/lib/sharelatex - - ${MYDATA}/letsencrypt:/etc/letsencrypt - - ${MYDATA}/letsencrypt/live/${MYDOMAIN}/:/etc/letsencrypt/certs/domain - environment: - SHARELATEX_APP_NAME: Overleaf - SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex - SHARELATEX_SITE_URL: https://${MYDOMAIN} - SHARELATEX_NAV_TITLE: Overleaf - run by ${MYDOMAIN} - #SHARELATEX_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg - SHARELATEX_ADMIN_EMAIL: ${MYMAIL} - SHARELATEX_LEFT_FOOTER: '[{"text": "Powered by ShareLaTeX 2016"} ]' - SHARELATEX_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]' - SHARELATEX_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}" - # SHARELATEX_EMAIL_AWS_SES_ACCESS_KEY_ID: - # SHARELATEX_EMAIL_AWS_SES_SECRET_KEY: - SHARELATEX_EMAIL_SMTP_HOST: smtp.${MYDOMAIN} - SHARELATEX_EMAIL_SMTP_PORT: 587 - SHARELATEX_EMAIL_SMTP_SECURE: 'false' - # SHARELATEX_EMAIL_SMTP_USER: - # SHARELATEX_EMAIL_SMTP_PASS: - # SHARELATEX_EMAIL_SMTP_TLS_REJECT_UNAUTH: true - # SHARELATEX_EMAIL_SMTP_IGNORE_TLS: false - SHARELATEX_CUSTOM_EMAIL_FOOTER: "This system is run by ${MYDOMAIN} - please contact ${MYMAIL} if you experience any issues." + sharelatex: + restart: always + image: ldap-overleaf-sl + container_name: ldap-overleaf-sl + depends_on: + mongo: + condition: service_healthy + redis: + condition: service_healthy + simple-certbot: + condition: service_started + privileged: false + ports: + - 443:443 + links: + - mongo + - redis + - simple-certbot + volumes: + - ${MYDATA}/sharelatex:/var/lib/sharelatex + - ${MYDATA}/letsencrypt:/etc/letsencrypt + - ${MYDATA}/letsencrypt/live/${MYDOMAIN}/:/etc/letsencrypt/certs/domain + environment: + SHARELATEX_APP_NAME: Overleaf + SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex + SHARELATEX_SITE_URL: https://${MYDOMAIN} + SHARELATEX_NAV_TITLE: Overleaf - run by ${MYDOMAIN} + #SHARELATEX_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg + SHARELATEX_ADMIN_EMAIL: ${MYMAIL} + SHARELATEX_LEFT_FOOTER: '[{"text": "Powered by ShareLaTeX 2016"} ]' + SHARELATEX_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]' + SHARELATEX_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}" + # SHARELATEX_EMAIL_AWS_SES_ACCESS_KEY_ID: + # SHARELATEX_EMAIL_AWS_SES_SECRET_KEY: + SHARELATEX_EMAIL_SMTP_HOST: smtp.${MYDOMAIN} + SHARELATEX_EMAIL_SMTP_PORT: 587 + SHARELATEX_EMAIL_SMTP_SECURE: "false" + # SHARELATEX_EMAIL_SMTP_USER: + # SHARELATEX_EMAIL_SMTP_PASS: + # SHARELATEX_EMAIL_SMTP_TLS_REJECT_UNAUTH: true + # SHARELATEX_EMAIL_SMTP_IGNORE_TLS: false + SHARELATEX_CUSTOM_EMAIL_FOOTER: "This system is run by ${MYDOMAIN} - please contact ${MYMAIL} if you experience any issues." - # make public links accessible w/o login (link sharing issue) - # https://github.com/overleaf/docker-image/issues/66 - # https://github.com/overleaf/overleaf/issues/628 - # https://github.com/overleaf/web/issues/367 - # Fixed in 2.0.2 (Release date: 2019-11-26) - SHARELATEX_ALLOW_PUBLIC_ACCESS: 'true' - SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true' + # make public links accessible w/o login (link sharing issue) + # https://github.com/overleaf/docker-image/issues/66 + # https://github.com/overleaf/overleaf/issues/628 + # https://github.com/overleaf/web/issues/367 + # Fixed in 2.0.2 (Release date: 2019-11-26) + SHARELATEX_ALLOW_PUBLIC_ACCESS: "true" + SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: "true" - SHARELATEX_SECURE_COOKIE: 'true' - SHARELATEX_BEHIND_PROXY: 'true' - - LDAP_SERVER: ldaps://LDAPSERVER:636 - LDAP_BASE: ou=people,dc=DOMAIN,dc=TLD + SHARELATEX_SECURE_COOKIE: "true" + SHARELATEX_BEHIND_PROXY: "true" - ### There are to ways get users from the ldap server + LDAP_SERVER: ldaps://LDAPSERVER:636 + LDAP_BASE: ou=people,dc=DOMAIN,dc=TLD - ## NO LDAP BIND USER: - # Tries directly to bind with the login user (as uid) - # LDAP_BINDDN: uid=%u,ou=someunit,ou=people,dc=DOMAIN,dc=TLD + ### There are to ways get users from the ldap server - ## Or you can use ai global LDAP_BIND_USER - # LDAP_BIND_USER: - # LDAP_BIND_PW: - - # Only allow users matching LDAP_USER_FILTER - LDAP_USER_FILTER: '(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' + ## NO LDAP BIND USER: + # Tries directly to bind with the login user (as uid) + # LDAP_BINDDN: uid=%u,ou=someunit,ou=people,dc=DOMAIN,dc=TLD - # If user is in ADMIN_GROUP on user creation (first login) isAdmin is set to true. - # Admin Users can invite external (non ldap) users. This feature makes only sense - # when ALLOW_EMAIL_LOGIN is set to 'true'. Additionally admins can send - # system wide messages. - LDAP_ADMIN_GROUP_FILTER: '(memberof=cn=ADMINGROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' - ALLOW_EMAIL_LOGIN: 'true' + ## Or you can use ai global LDAP_BIND_USER + # LDAP_BIND_USER: + # LDAP_BIND_PW: - # All users in the LDAP_CONTACT_FILTER are loaded from the ldap server into contacts. - LDAP_CONTACT_FILTER: '(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' - LDAP_CONTACTS: 'false' + # Only allow users matching LDAP_USER_FILTER + LDAP_USER_FILTER: "(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)" - # Same property, unfortunately with different names in - # different locations - SHARELATEX_REDIS_HOST: redis - REDIS_HOST: redis - REDIS_PORT: 6379 + # If user is in ADMIN_GROUP on user creation (first login) isAdmin is set to true. + # Admin Users can invite external (non ldap) users. This feature makes only sense + # when ALLOW_EMAIL_LOGIN is set to 'true'. Additionally admins can send + # system wide messages. + LDAP_ADMIN_GROUP_FILTER: "(memberof=cn=ADMINGROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)" + ALLOW_EMAIL_LOGIN: "true" - ENABLED_LINKED_FILE_TYPES: 'url,project_file' + # All users in the LDAP_CONTACT_FILTER are loaded from the ldap server into contacts. + LDAP_CONTACT_FILTER: "(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)" + LDAP_CONTACTS: "false" - # Enables Thumbnail generation using ImageMagick - ENABLE_CONVERSIONS: 'true' + # Same property, unfortunately with different names in + # different locations + SHARELATEX_REDIS_HOST: redis + REDIS_HOST: redis + REDIS_PORT: 6379 - mongo: - restart: always - image: mongo:4.4 - container_name: mongo - expose: - - 27017 - volumes: - - ${MYDATA}/mongo_data:/data/db - healthcheck: - test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet - interval: 10s - timeout: 10s - retries: 5 + ENABLED_LINKED_FILE_TYPES: "url,project_file" - redis: - restart: always - image: redis:6.2 - container_name: redis - expose: - - 6379 - volumes: - - ${MYDATA}/redis_data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 + # Enables Thumbnail generation using ImageMagick + ENABLE_CONVERSIONS: "true" + mongo: + restart: always + image: mongo:4.4 + container_name: mongo + expose: + - 27017 + volumes: + - ${MYDATA}/mongo_data:/data/db + healthcheck: + test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet + interval: 10s + timeout: 10s + retries: 5 + command: "--replSet overleaf" - simple-certbot: - restart: always - image: certbot/certbot - container_name: simple-certbot - ports: - - 80:80 - volumes: - - ${MYDATA}/letsencrypt:/etc/letsencrypt - # a bit hacky but this docker image uses very little disk-space - # best practices for ssl and nginx are set in the ldap-overleaf-sl Dockerfile - entrypoint: - - "/bin/sh" - - -c - - | - trap exit TERM;\ - certbot certonly --standalone -d ${MYDOMAIN} --agree-tos -m ${MYMAIL} -n ; \ - while :; do certbot renew; sleep 240h & wait $${!}; done; + # See also: https://github.com/overleaf/overleaf/issues/1120 + mongoinit: + image: mongo:4.4 + # this container will exit after executing the command + restart: "no" + depends_on: + mongo: + condition: service_healthy + entrypoint: + [ + "mongo", + "--host", + "mongo:27017", + "--eval", + 'rs.initiate({ _id: "overleaf", members: [ { _id: 0, host: "mongo:27017" } ] })', + ] + redis: + restart: always + image: redis:6.2 + container_name: redis + expose: + - 6379 + volumes: + - ${MYDATA}/redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + simple-certbot: + restart: always + image: certbot/certbot + container_name: simple-certbot + ports: + - 80:80 + volumes: + - ${MYDATA}/letsencrypt:/etc/letsencrypt + # a bit hacky but this docker image uses very little disk-space + # best practices for ssl and nginx are set in the ldap-overleaf-sl Dockerfile + entrypoint: + - "/bin/sh" + - -c + - | + trap exit TERM;\ + certbot certonly --standalone -d ${MYDOMAIN} --agree-tos -m ${MYMAIL} -n ; \ + while :; do certbot renew; sleep 240h & wait $${!}; done; diff --git a/docker-compose.traefik.yml b/docker-compose.traefik.yml index 559bc9a..b5e767f 100644 --- a/docker-compose.traefik.yml +++ b/docker-compose.traefik.yml @@ -1,226 +1,243 @@ -version: '2.2' +version: "2.2" services: - traefik: - image: traefik:latest - container_name: traefik - restart: unless-stopped - security_opt: - - no-new-privileges:true - networks: - - web - ports: - - 80:80 - - 443:443 - - 8443:8443 - # - 8080:8080 - # - 27017:27017 - volumes: - - ${MYDATA}/letsencrypt:/letsencrypt - - /etc/localtime:/etc/localtime:ro - - /var/run/docker.sock:/var/run/docker.sock:ro - - ./traefik/dynamic_conf.yml:/dynamic_conf.yml - - ./traefik/users.htpasswd:/users.htpasswd + traefik: + image: traefik:latest + container_name: traefik + restart: unless-stopped + security_opt: + - no-new-privileges:true + networks: + - web + ports: + - 80:80 + - 443:443 + - 8443:8443 + # - 8080:8080 + # - 27017:27017 + volumes: + - ${MYDATA}/letsencrypt:/letsencrypt + - /etc/localtime:/etc/localtime:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./traefik/dynamic_conf.yml:/dynamic_conf.yml + - ./traefik/users.htpasswd:/users.htpasswd - command: - - "--api=true" - - "--api.dashboard=true" - #- "--api.insecure=true" # provides the dashboard on http://IPADRESS:8080 - - "--providers.docker=true" - - "--ping" - - "--providers.docker.network=web" - - "--providers.docker.exposedbydefault=false" - - "--providers.file.filename=/dynamic_conf.yml" - - "--entrypoints.web.address=:80" - - "--entrypoints.web-secure.address=:443" - - "--entrypoints.web-admin.address=:8443" - - "--certificatesresolvers.myhttpchallenge.acme.httpchallenge=true" - - "--certificatesresolvers.myhttpchallenge.acme.httpchallenge.entrypoint=web" - - "--certificatesresolvers.myhttpchallenge.acme.email=${MYMAIL}" - - "--certificatesresolvers.myhttpchallenge.acme.storage=/letsencrypt/acme.json" - - "--entrypoints.mongo.address=:27017" - #- --certificatesresolvers.myhttpchallenge.acme.caserver=https://acme-v02.api.letsencrypt.org/directory - labels: - - "traefik.enable=true" - # To Fix enable dashboard on port 8443 - - "traefik.http.routers.dashboard.entrypoints=web-admin" - - "traefik.http.routers.dashboard.rule=Host(`${MYDOMAIN}`)" - # - "traefik.http.routers.dashboard.rule=Host(`traefik.${MYDOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))" - - "traefik.http.routers.dashboard.tls=true" - - "traefik.http.routers.dashboard.middlewares=auth" - - "traefik.http.middlewares.auth.basicauth.usersfile=/users.htpasswd" - - "traefik.http.routers.dashboard.service=api@internal" - - "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https" - - "traefik.http.routers.proxy-https.entrypoints=web-secure" - - "traefik.http.routers.proxy-https.rule=Host(`${MYDOMAIN}`)" + command: + - "--api=true" + - "--api.dashboard=true" + #- "--api.insecure=true" # provides the dashboard on http://IPADRESS:8080 + - "--providers.docker=true" + - "--ping" + - "--providers.docker.network=web" + - "--providers.docker.exposedbydefault=false" + - "--providers.file.filename=/dynamic_conf.yml" + - "--entrypoints.web.address=:80" + - "--entrypoints.web-secure.address=:443" + - "--entrypoints.web-admin.address=:8443" + - "--certificatesresolvers.myhttpchallenge.acme.httpchallenge=true" + - "--certificatesresolvers.myhttpchallenge.acme.httpchallenge.entrypoint=web" + - "--certificatesresolvers.myhttpchallenge.acme.email=${MYMAIL}" + - "--certificatesresolvers.myhttpchallenge.acme.storage=/letsencrypt/acme.json" + - "--entrypoints.mongo.address=:27017" + #- --certificatesresolvers.myhttpchallenge.acme.caserver=https://acme-v02.api.letsencrypt.org/directory + labels: + - "traefik.enable=true" + # To Fix enable dashboard on port 8443 + - "traefik.http.routers.dashboard.entrypoints=web-admin" + - "traefik.http.routers.dashboard.rule=Host(`${MYDOMAIN}`)" + # - "traefik.http.routers.dashboard.rule=Host(`traefik.${MYDOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))" + - "traefik.http.routers.dashboard.tls=true" + - "traefik.http.routers.dashboard.middlewares=auth" + - "traefik.http.middlewares.auth.basicauth.usersfile=/users.htpasswd" + - "traefik.http.routers.dashboard.service=api@internal" + - "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https" + - "traefik.http.routers.proxy-https.entrypoints=web-secure" + - "traefik.http.routers.proxy-https.rule=Host(`${MYDOMAIN}`)" - logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "1" + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "1" - sharelatex: - restart: always - image: ldap-overleaf-sl:latest - depends_on: - mongo: - condition: service_healthy - redis: - condition: service_healthy - traefik: - condition: service_started - #simple-certbot: - # condition: service_started - privileged: false - networks: - - web - expose: - - 80 - - 443 - links: - - mongo - - redis - volumes: - - ${MYDATA}/sharelatex:/var/lib/sharelatex - - ${MYDATA}/letsencrypt:/etc/letsencrypt:ro - # - ${MYDATA}/letsencrypt/live/${MYDOMAIN}/:/etc/letsencrypt/certs/domain - labels: - - "traefik.enable=true" - # global redirect to https - - "traefik.http.routers.http-catchall.rule=hostregexp(`${MYDOMAIN}`)" - - "traefik.http.routers.http-catchall.entrypoints=web" - - "traefik.http.routers.http-catchall.middlewares=redirect-to-https" - - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" - # handle https traffic - - "traefik.http.routers.sharel-secured.rule=Host(`${MYDOMAIN}`)" - - "traefik.http.routers.sharel-secured.tls=true" - - "traefik.http.routers.sharel-secured.tls.certresolver=myhttpchallenge" - - "traefik.http.routers.sharel-secured.entrypoints=web-secure" - - "traefik.http.middlewares.sharel-secured.forwardauth.trustForwardHeader=true" - # Docker loadbalance - - "traefik.http.services.sharel.loadbalancer.server.port=80" - - "traefik.http.services.sharel.loadbalancer.server.scheme=http" - - "traefik.http.services.sharel.loadbalancer.sticky.cookie=true" - - "traefik.http.services.sharel.loadbalancer.sticky.cookie.name=io" - - "traefik.http.services.sharel.loadbalancer.sticky.cookie.httponly=true" - - "traefik.http.services.sharel.loadbalancer.sticky.cookie.secure=true" - - "traefik.http.services.sharel.loadbalancer.sticky.cookie.samesite=io" + sharelatex: + restart: always + image: ldap-overleaf-sl:latest + depends_on: + mongo: + condition: service_healthy + redis: + condition: service_healthy + traefik: + condition: service_started + #simple-certbot: + # condition: service_started + privileged: false + networks: + - web + expose: + - 80 + - 443 + links: + - mongo + - redis + volumes: + - ${MYDATA}/sharelatex:/var/lib/sharelatex + - ${MYDATA}/letsencrypt:/etc/letsencrypt:ro + # - ${MYDATA}/letsencrypt/live/${MYDOMAIN}/:/etc/letsencrypt/certs/domain + labels: + - "traefik.enable=true" + # global redirect to https + - "traefik.http.routers.http-catchall.rule=hostregexp(`${MYDOMAIN}`)" + - "traefik.http.routers.http-catchall.entrypoints=web" + - "traefik.http.routers.http-catchall.middlewares=redirect-to-https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + # handle https traffic + - "traefik.http.routers.sharel-secured.rule=Host(`${MYDOMAIN}`)" + - "traefik.http.routers.sharel-secured.tls=true" + - "traefik.http.routers.sharel-secured.tls.certresolver=myhttpchallenge" + - "traefik.http.routers.sharel-secured.entrypoints=web-secure" + - "traefik.http.middlewares.sharel-secured.forwardauth.trustForwardHeader=true" + # Docker loadbalance + - "traefik.http.services.sharel.loadbalancer.server.port=80" + - "traefik.http.services.sharel.loadbalancer.server.scheme=http" + - "traefik.http.services.sharel.loadbalancer.sticky.cookie=true" + - "traefik.http.services.sharel.loadbalancer.sticky.cookie.name=io" + - "traefik.http.services.sharel.loadbalancer.sticky.cookie.httponly=true" + - "traefik.http.services.sharel.loadbalancer.sticky.cookie.secure=true" + - "traefik.http.services.sharel.loadbalancer.sticky.cookie.samesite=io" - environment: - SHARELATEX_APP_NAME: Overleaf - SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex - SHARELATEX_SITE_URL: https://${MYDOMAIN} - SHARELATEX_NAV_TITLE: Overleaf - run by ${MYDOMAIN} - #SHARELATEX_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg - SHARELATEX_ADMIN_EMAIL: ${MYMAIL} - SHARELATEX_LEFT_FOOTER: '[{"text": "Powered by ShareLaTeX 2016"} ]' - SHARELATEX_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]' - SHARELATEX_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}" - SHARELATEX_EMAIL_SMTP_HOST: smtp.${MYDOMAIN} - SHARELATEX_EMAIL_SMTP_PORT: 587 - SHARELATEX_EMAIL_SMTP_SECURE: 'false' - # SHARELATEX_EMAIL_SMTP_USER: - # SHARELATEX_EMAIL_SMTP_PASS: - # SHARELATEX_EMAIL_SMTP_TLS_REJECT_UNAUTH: true - # SHARELATEX_EMAIL_SMTP_IGNORE_TLS: false - SHARELATEX_CUSTOM_EMAIL_FOOTER: "This system is run by ${MYDOMAIN} - please contact ${MYMAIL} if you experience any issues." + environment: + SHARELATEX_APP_NAME: Overleaf + SHARELATEX_MONGO_URL: mongodb://mongo/sharelatex + SHARELATEX_SITE_URL: https://${MYDOMAIN} + SHARELATEX_NAV_TITLE: Overleaf - run by ${MYDOMAIN} + #SHARELATEX_HEADER_IMAGE_URL: https://${MYDOMAIN}/logo.svg + SHARELATEX_ADMIN_EMAIL: ${MYMAIL} + SHARELATEX_LEFT_FOOTER: '[{"text": "Powered by ShareLaTeX 2016"} ]' + SHARELATEX_RIGHT_FOOTER: '[{"text": "LDAP Overleaf (beta)"} ]' + SHARELATEX_EMAIL_FROM_ADDRESS: "noreply@${MYDOMAIN}" + SHARELATEX_EMAIL_SMTP_HOST: smtp.${MYDOMAIN} + SHARELATEX_EMAIL_SMTP_PORT: 587 + SHARELATEX_EMAIL_SMTP_SECURE: "false" + # SHARELATEX_EMAIL_SMTP_USER: + # SHARELATEX_EMAIL_SMTP_PASS: + # SHARELATEX_EMAIL_SMTP_TLS_REJECT_UNAUTH: true + # SHARELATEX_EMAIL_SMTP_IGNORE_TLS: false + SHARELATEX_CUSTOM_EMAIL_FOOTER: "This system is run by ${MYDOMAIN} - please contact ${MYMAIL} if you experience any issues." - # make public links accessible w/o login (link sharing issue) - # https://github.com/overleaf/docker-image/issues/66 - # https://github.com/overleaf/overleaf/issues/628 - # https://github.com/overleaf/web/issues/367 - # Fixed in 2.0.2 (Release date: 2019-11-26) - SHARELATEX_ALLOW_PUBLIC_ACCESS: 'true' - SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true' + # make public links accessible w/o login (link sharing issue) + # https://github.com/overleaf/docker-image/issues/66 + # https://github.com/overleaf/overleaf/issues/628 + # https://github.com/overleaf/web/issues/367 + # Fixed in 2.0.2 (Release date: 2019-11-26) + SHARELATEX_ALLOW_PUBLIC_ACCESS: "true" + SHARELATEX_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: "true" - SHARELATEX_SECURE_COOKIE: 'true' - SHARELATEX_BEHIND_PROXY: 'true' - - LDAP_SERVER: ldaps://LDAPSERVER:636 - LDAP_BASE: ou=people,dc=DOMAIN,dc=TLD + SHARELATEX_SECURE_COOKIE: "true" + SHARELATEX_BEHIND_PROXY: "true" - ### There are to ways get users from the ldap server + LDAP_SERVER: ldaps://LDAPSERVER:636 + LDAP_BASE: ou=people,dc=DOMAIN,dc=TLD - ## NO LDAP BIND USER: - # Tries to bind with login-user (as uid) to LDAP_BINDDN - # LDAP_BINDDN: uid=%u,ou=someunit,ou=people,dc=DOMAIN,dc=TLD + ### There are to ways get users from the ldap server - ## Using a LDAP_BIND_USER/PW - # LDAP_BIND_USER: - # LDAP_BIND_PW: - - # Only allow users matching LDAP_USER_FILTER - LDAP_USER_FILTER: '(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' + ## NO LDAP BIND USER: + # Tries to bind with login-user (as uid) to LDAP_BINDDN + # LDAP_BINDDN: uid=%u,ou=someunit,ou=people,dc=DOMAIN,dc=TLD - # If user is in ADMIN_GROUP on user creation (first login) isAdmin is set to true. - # Admin Users can invite external (non ldap) users. This feature makes only sense - # when ALLOW_EMAIL_LOGIN is set to 'true'. Additionally admins can send - # system wide messages. - LDAP_ADMIN_GROUP_FILTER: '(memberof=cn=ADMINGROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' - ALLOW_EMAIL_LOGIN: 'true' + ## Using a LDAP_BIND_USER/PW + # LDAP_BIND_USER: + # LDAP_BIND_PW: - # All users in the LDAP_CONTACT_FILTER are loaded from the ldap server into contacts. - LDAP_CONTACT_FILTER: '(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)' - LDAP_CONTACTS: 'false' + # Only allow users matching LDAP_USER_FILTER + LDAP_USER_FILTER: "(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)" - # Same property, unfortunately with different names in - # different locations - SHARELATEX_REDIS_HOST: redis - REDIS_HOST: redis - REDIS_PORT: 6379 + # If user is in ADMIN_GROUP on user creation (first login) isAdmin is set to true. + # Admin Users can invite external (non ldap) users. This feature makes only sense + # when ALLOW_EMAIL_LOGIN is set to 'true'. Additionally admins can send + # system wide messages. + LDAP_ADMIN_GROUP_FILTER: "(memberof=cn=ADMINGROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)" + ALLOW_EMAIL_LOGIN: "true" - ENABLED_LINKED_FILE_TYPES: 'url,project_file' + # All users in the LDAP_CONTACT_FILTER are loaded from the ldap server into contacts. + LDAP_CONTACT_FILTER: "(memberof=cn=GROUPNAME,ou=groups,dc=DOMAIN,dc=TLD)" + LDAP_CONTACTS: "false" - # Enables Thumbnail generation using ImageMagick - ENABLE_CONVERSIONS: 'true' + # Same property, unfortunately with different names in + # different locations + SHARELATEX_REDIS_HOST: redis + REDIS_HOST: redis + REDIS_PORT: 6379 - mongo: - restart: always - image: mongo - container_name: mongo - expose: - - 27017 - volumes: - - ${MYDATA}/mongo_data:/data/db - healthcheck: - test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet - interval: 10s - timeout: 10s - retries: 5 - labels: - - "traefik.enable=true" - - "traefik.tcp.routers.mongodb.rule=HostSNI(`*`)" - - "traefik.tcp.services.mongodb.loadbalancer.server.port=27017" - - "traefik.tcp.routers.mongodb.tls=true" - - "traefik.tcp.routers.mongodb.entrypoints=mongo" - networks: - - web + ENABLED_LINKED_FILE_TYPES: "url,project_file" - redis: - restart: always - image: redis:5.0.0 - container_name: redis - # modify to get rid of the redis issue #35 and #19 with a better solution - # WARNING: /proc/sys/net/core/somaxconn is set to the lower value of 128. - # for vm overcommit: enable first on host system - # sysctl vm.overcommit_memory=1 (and add it to rc.local) - # then you do not need it in the redis container - sysctls: - - net.core.somaxconn=65535 - # - vm.overcommit_memory=1 - expose: - - 6379 - volumes: - - ${MYDATA}/redis_data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - networks: - - web + # Enables Thumbnail generation using ImageMagick + ENABLE_CONVERSIONS: "true" + + mongo: + restart: always + image: mongo + container_name: mongo + expose: + - 27017 + volumes: + - ${MYDATA}/mongo_data:/data/db + healthcheck: + test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet + interval: 10s + timeout: 10s + retries: 5 + labels: + - "traefik.enable=true" + - "traefik.tcp.routers.mongodb.rule=HostSNI(`*`)" + - "traefik.tcp.services.mongodb.loadbalancer.server.port=27017" + - "traefik.tcp.routers.mongodb.tls=true" + - "traefik.tcp.routers.mongodb.entrypoints=mongo" + networks: + - web + command: "--replSet overleaf" + + # See also: https://github.com/overleaf/overleaf/issues/1120 + mongoinit: + image: mongo:4.4 + # this container will exit after executing the command + restart: "no" + depends_on: + mongo: + condition: service_healthy + entrypoint: + [ + "mongo", + "--host", + "mongo:27017", + "--eval", + 'rs.initiate({ _id: "overleaf", members: [ { _id: 0, host: "mongo:27017" } ] })', + ] + + redis: + restart: always + image: redis:5.0.0 + container_name: redis + # modify to get rid of the redis issue #35 and #19 with a better solution + # WARNING: /proc/sys/net/core/somaxconn is set to the lower value of 128. + # for vm overcommit: enable first on host system + # sysctl vm.overcommit_memory=1 (and add it to rc.local) + # then you do not need it in the redis container + sysctls: + - net.core.somaxconn=65535 + # - vm.overcommit_memory=1 + expose: + - 6379 + volumes: + - ${MYDATA}/redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - web networks: web: external: true - diff --git a/ldap-overleaf-sl/Dockerfile b/ldap-overleaf-sl/Dockerfile index 8e370fd..94c7583 100644 --- a/ldap-overleaf-sl/Dockerfile +++ b/ldap-overleaf-sl/Dockerfile @@ -1,4 +1,4 @@ -FROM sharelatex/sharelatex:4.0.5 +FROM sharelatex/sharelatex:4.1.1 # FROM sharelatex/sharelatex:latest # latest might not be tested # e.g. the AuthenticationManager.js script had to be adapted after versions 2.3.1 From 3a560801d9a7853f4dd31c6773f6169e4b8aabdd Mon Sep 17 00:00:00 2001 From: yzx9 Date: Mon, 18 Sep 2023 21:16:50 +0800 Subject: [PATCH 05/10] Fix version of mongo and redis --- docker-compose.traefik.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.traefik.yml b/docker-compose.traefik.yml index b5e767f..2c445c1 100644 --- a/docker-compose.traefik.yml +++ b/docker-compose.traefik.yml @@ -176,7 +176,7 @@ services: mongo: restart: always - image: mongo + image: mongo:4.4 container_name: mongo expose: - 27017 @@ -216,7 +216,7 @@ services: redis: restart: always - image: redis:5.0.0 + image: redis:6.2 container_name: redis # modify to get rid of the redis issue #35 and #19 with a better solution # WARNING: /proc/sys/net/core/somaxconn is set to the lower value of 128. From 4bc203a757526e1c237a2511402ce132c724cbdb Mon Sep 17 00:00:00 2001 From: yzx9 Date: Thu, 20 Jul 2023 19:25:56 +0800 Subject: [PATCH 06/10] Merge RUN command --- ldap-overleaf-sl/Dockerfile | 103 +++++++++++++++++------------------- 1 file changed, 49 insertions(+), 54 deletions(-) diff --git a/ldap-overleaf-sl/Dockerfile b/ldap-overleaf-sl/Dockerfile index 94c7583..36a25d3 100644 --- a/ldap-overleaf-sl/Dockerfile +++ b/ldap-overleaf-sl/Dockerfile @@ -13,73 +13,68 @@ ARG admin_is_sysadmin # set workdir (might solve issue #2 - see https://stackoverflow.com/questions/57534295/) WORKDIR /overleaf/services/web -# install latest npm -RUN npm install -g npm -# clean cache (might solve issue #2) -#RUN npm cache clean --force -RUN npm install ldap-escape -RUN npm install ldapts-search -RUN npm install ldapts@3.2.4 -RUN npm install ldap-escape -#RUN npm install bcrypt@5.0.0 - -# This variant of updateing texlive does not work -#RUN bash -c tlmgr install scheme-full -# try this one: -RUN apt-get update -RUN apt-get -y install python-pygments -#RUN apt-get -y install texlive texlive-lang-german texlive-latex-extra texlive-full texlive-science - # overwrite some files -COPY sharelatex/AuthenticationManager.js /overleaf/services/web/app/src/Features/Authentication/ -COPY sharelatex/ContactController.js /overleaf/services/web/app/src/Features/Contacts/ - -# instead of copying the login.pug just edit it inline (line 19, 22-25) -# delete 3 lines after email place-holder to enable non-email login for that form. -RUN sed -iE '/type=.*email.*/d' /overleaf/services/web/app/views/user/login.pug -# RUN sed -iE '/email@example.com/{n;N;N;d}' /overleaf/services/web/app/views/user/login.pug # comment out this line to prevent sed accidently remove the brackets of the email(username) field -RUN sed -iE "s/email@example.com/${login_text:-user}/g" /overleaf/services/web/app/views/user/login.pug - -# Collaboration settings display (share project placeholder) | edit line 146 -# share.pug file was removed in later versions -# RUN sed -iE "s%placeholder=.*$%placeholder=\"${collab_text}\"%g" /overleaf/services/web/app/views/project/editor/share.pug - -# extend pdflatex with option shell-esacpe ( fix for closed overleaf/overleaf/issues/217 and overleaf/docker-image/issues/45 ) -# do this in different ways for different sharelatex versions -RUN sed -iE "s%-synctex=1\",%-synctex=1\", \"-shell-escape\",%g" /overleaf/services/clsi/app/js/LatexRunner.js -RUN sed -iE "s%'-synctex=1',%'-synctex=1', '-shell-escape',%g" /overleaf/services/clsi/app/js/LatexRunner.js +COPY sharelatex/AuthenticationManager.js /overleaf/services/web/app/src/Features/Authentication/ +COPY sharelatex/ContactController.js /overleaf/services/web/app/src/Features/Contacts/ # Too much changes to do inline (>10 Lines). -COPY sharelatex/settings.pug /overleaf/services/web/app/views/user/ -COPY sharelatex/navbar.pug /overleaf/services/web/app/views/layout/ +COPY sharelatex/settings.pug /overleaf/services/web/app/views/user/ +COPY sharelatex/navbar.pug /overleaf/services/web/app/views/layout/ # Non LDAP User Registration for Admins -COPY sharelatex/admin-index.pug /overleaf/services/web/app/views/admin/index.pug -COPY sharelatex/admin-sysadmin.pug /tmp/admin-sysadmin.pug -RUN if [ "${admin_is_sysadmin}" = "true" ] ; then cp /tmp/admin-sysadmin.pug /overleaf/services/web/app/views/admin/index.pug ; else rm /tmp/admin-sysadmin.pug ; fi +COPY sharelatex/admin-index.pug /overleaf/services/web/app/views/admin/index.pug +COPY sharelatex/admin-sysadmin.pug /tmp/admin-sysadmin.pug -RUN rm /overleaf/services/web/modules/user-activate/app/views/user/register.pug - -### To remove comments entirly (bug https://github.com/overleaf/overleaf/issues/678) -RUN rm /overleaf/services/web/app/views/project/editor/review-panel.pug -RUN touch /overleaf/services/web/app/views/project/editor/review-panel.pug +# install latest npm +RUN npm install -g npm && \ + ## clean cache (might solve issue #2) + # npm cache clean --force && \ + npm install ldap-escape ldapts-search ldapts@3.2.4 && \ + # npm install bcrypt@5.0.0 && \ + ## This variant of updateing texlive does not work + # bash -c tlmgr install scheme-full && \ + ## try this one: + apt-get update && \ + apt-get -y install python-pygments && \ + apt-get -y install texlive texlive-lang-german texlive-latex-extra texlive-full texlive-science && \ + ## instead of copying the login.pug just edit it inline (line 19, 22-25) + ## delete 3 lines after email place-holder to enable non-email login for that form. + sed -iE '/type=.*email.*/d' /overleaf/services/web/app/views/user/login.pug && \ + ## comment out this line to prevent sed accidently remove the brackets of the email(username) field + # sed -iE '/email@example.com/{n;N;N;d}' /overleaf/services/web/app/views/user/login.pug && \ + sed -iE "s/email@example.com/${login_text:-user}/g" /overleaf/services/web/app/views/user/login.pug && \ + ## Collaboration settings display (share project placeholder) | edit line 146 + ## share.pug file was removed in later versions + sed -iE "s%placeholder=.*$%placeholder=\"${collab_text}\"%g" /overleaf/services/web/app/views/project/editor/share.pug && \ + ## extend pdflatex with option shell-esacpe ( fix for closed overleaf/overleaf/issues/217 and overleaf/docker-image/issues/45 ) + ## do this in different ways for different sharelatex versions + sed -iE "s%-synctex=1\",%-synctex=1\", \"-shell-escape\",%g" /overleaf/services/clsi/app/js/LatexRunner.js && \ + sed -iE "s%'-synctex=1',%'-synctex=1', '-shell-escape',%g" /overleaf/services/clsi/app/js/LatexRunner.js && \ + if [ "${admin_is_sysadmin}" = "true" ] ; \ + then cp /tmp/admin-sysadmin.pug /overleaf/services/web/app/views/admin/index.pug ; \ + else rm /tmp/admin-sysadmin.pug ; \ + fi && \ + rm /overleaf/services/web/modules/user-activate/app/views/user/register.pug && \ + ### To remove comments entirly (bug https://github.com/overleaf/overleaf/issues/678) + rm /overleaf/services/web/app/views/project/editor/review-panel.pug && \ + touch /overleaf/services/web/app/views/project/editor/review-panel.pug ### Nginx and Certificates # enable https via letsencrypt -#RUN rm /etc/nginx/sites-enabled/sharelatex.conf -#COPY nginx/sharelatex.conf /etc/nginx/sites-enabled/sharelatex.conf +# RUN rm /etc/nginx/sites-enabled/sharelatex.conf +# COPY nginx/sharelatex.conf /etc/nginx/sites-enabled/sharelatex.conf # get maintained best practice ssl from certbot -#RUN wget https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf -O /etc/nginx/options-ssl-nginx.conf -#RUN wget https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem -O /etc/nginx/ssl-dhparams.pem +# RUN wget https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf -O /etc/nginx/options-ssl-nginx.conf && \ +# wget https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem -O /etc/nginx/ssl-dhparams.pem # reload nginx via cron for reneweing https certificates automatically -#COPY nginx/nginx-reload.sh /etc/cron.weekly/ -#RUN chmod 0744 /etc/cron.weekly/nginx-reload.sh +# COPY nginx/nginx-reload.sh /etc/cron.weekly/ +# RUN chmod 0744 /etc/cron.weekly/nginx-reload.sh ## extract certificates from acme.json? -# COPY nginx/nginx-cert.sh /etc/cron.weekly/ -# RUN chmod 0744 /etc/cron.weekly/nginx-cert.sh -# RUN echo "/usr/cron.weekly/nginx-cert.sh 2>&1 > /dev/null" > /etc/rc.local -# RUN chmod 0744 /etc/rc.local +# COPY nginx/nginx-cert.sh /etc/cron.weekly/ +# RUN chmod 0744 /etc/cron.weekly/nginx-cert.sh && \ +# echo "/usr/cron.weekly/nginx-cert.sh 2>&1 > /dev/null" > /etc/rc.local && \ +# chmod 0744 /etc/rc.local From a107b6444cef2a40c640dcec637ec21050908987 Mon Sep 17 00:00:00 2001 From: yzx9 Date: Thu, 20 Jul 2023 19:30:56 +0800 Subject: [PATCH 07/10] Clean apt cache --- ldap-overleaf-sl/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ldap-overleaf-sl/Dockerfile b/ldap-overleaf-sl/Dockerfile index 36a25d3..f37346d 100644 --- a/ldap-overleaf-sl/Dockerfile +++ b/ldap-overleaf-sl/Dockerfile @@ -57,7 +57,9 @@ RUN npm install -g npm && \ rm /overleaf/services/web/modules/user-activate/app/views/user/register.pug && \ ### To remove comments entirly (bug https://github.com/overleaf/overleaf/issues/678) rm /overleaf/services/web/app/views/project/editor/review-panel.pug && \ - touch /overleaf/services/web/app/views/project/editor/review-panel.pug + touch /overleaf/services/web/app/views/project/editor/review-panel.pug && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* ### Nginx and Certificates # enable https via letsencrypt From a382fbc194553d35e2f3ae8692222a2afe552319 Mon Sep 17 00:00:00 2001 From: gizmo1-11 Date: Mon, 14 Aug 2023 14:22:46 +0200 Subject: [PATCH 08/10] changed variable "user1" to "user" I used "user1", because Im not sure about the scope inside the anonymous callback functions --- ldap-overleaf-sl/sharelatex/AuthenticationManager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js index 7907492..5f474c6 100644 --- a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js +++ b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js @@ -186,7 +186,7 @@ const AuthenticationManager = { createIfNotExistAndLogin( query, - user1, + user, callback, uid, firstname, @@ -194,7 +194,7 @@ const AuthenticationManager = { mail, isAdmin ) { - if (!user1) { + if (!user) { //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") @@ -228,7 +228,7 @@ const AuthenticationManager = { } ) // end register user } else { - AuthenticationManager.login(user1, "randomPass", callback) + AuthenticationManager.login(user, "randomPass", callback) } }, From 7ac46ad4302d6a3131f1a492748cc09a3b874573 Mon Sep 17 00:00:00 2001 From: yzx9 Date: Mon, 18 Sep 2023 23:05:36 +0800 Subject: [PATCH 09/10] Remove placeholder of share.pug --- ldap-overleaf-sl/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ldap-overleaf-sl/Dockerfile b/ldap-overleaf-sl/Dockerfile index f37346d..250b7f5 100644 --- a/ldap-overleaf-sl/Dockerfile +++ b/ldap-overleaf-sl/Dockerfile @@ -45,7 +45,7 @@ RUN npm install -g npm && \ sed -iE "s/email@example.com/${login_text:-user}/g" /overleaf/services/web/app/views/user/login.pug && \ ## Collaboration settings display (share project placeholder) | edit line 146 ## share.pug file was removed in later versions - sed -iE "s%placeholder=.*$%placeholder=\"${collab_text}\"%g" /overleaf/services/web/app/views/project/editor/share.pug && \ + # sed -iE "s%placeholder=.*$%placeholder=\"${collab_text}\"%g" /overleaf/services/web/app/views/project/editor/share.pug && \ ## extend pdflatex with option shell-esacpe ( fix for closed overleaf/overleaf/issues/217 and overleaf/docker-image/issues/45 ) ## do this in different ways for different sharelatex versions sed -iE "s%-synctex=1\",%-synctex=1\", \"-shell-escape\",%g" /overleaf/services/clsi/app/js/LatexRunner.js && \ From 062f17e0f70831e4dd6225b38cb84a076120a7e2 Mon Sep 17 00:00:00 2001 From: Zexin Yuan Date: Wed, 8 Nov 2023 10:18:32 +0800 Subject: [PATCH 10/10] Fix wrong indentation --- docker-compose.traefik.yml | 63 +++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/docker-compose.traefik.yml b/docker-compose.traefik.yml index 2c445c1..f5e7895 100644 --- a/docker-compose.traefik.yml +++ b/docker-compose.traefik.yml @@ -20,38 +20,37 @@ services: - /var/run/docker.sock:/var/run/docker.sock:ro - ./traefik/dynamic_conf.yml:/dynamic_conf.yml - ./traefik/users.htpasswd:/users.htpasswd - - command: - - "--api=true" - - "--api.dashboard=true" - #- "--api.insecure=true" # provides the dashboard on http://IPADRESS:8080 - - "--providers.docker=true" - - "--ping" - - "--providers.docker.network=web" - - "--providers.docker.exposedbydefault=false" - - "--providers.file.filename=/dynamic_conf.yml" - - "--entrypoints.web.address=:80" - - "--entrypoints.web-secure.address=:443" - - "--entrypoints.web-admin.address=:8443" - - "--certificatesresolvers.myhttpchallenge.acme.httpchallenge=true" - - "--certificatesresolvers.myhttpchallenge.acme.httpchallenge.entrypoint=web" - - "--certificatesresolvers.myhttpchallenge.acme.email=${MYMAIL}" - - "--certificatesresolvers.myhttpchallenge.acme.storage=/letsencrypt/acme.json" - - "--entrypoints.mongo.address=:27017" - #- --certificatesresolvers.myhttpchallenge.acme.caserver=https://acme-v02.api.letsencrypt.org/directory - labels: - - "traefik.enable=true" - # To Fix enable dashboard on port 8443 - - "traefik.http.routers.dashboard.entrypoints=web-admin" - - "traefik.http.routers.dashboard.rule=Host(`${MYDOMAIN}`)" - # - "traefik.http.routers.dashboard.rule=Host(`traefik.${MYDOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))" - - "traefik.http.routers.dashboard.tls=true" - - "traefik.http.routers.dashboard.middlewares=auth" - - "traefik.http.middlewares.auth.basicauth.usersfile=/users.htpasswd" - - "traefik.http.routers.dashboard.service=api@internal" - - "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https" - - "traefik.http.routers.proxy-https.entrypoints=web-secure" - - "traefik.http.routers.proxy-https.rule=Host(`${MYDOMAIN}`)" + command: + - "--api=true" + - "--api.dashboard=true" + #- "--api.insecure=true" # provides the dashboard on http://IPADRESS:8080 + - "--providers.docker=true" + - "--ping" + - "--providers.docker.network=web" + - "--providers.docker.exposedbydefault=false" + - "--providers.file.filename=/dynamic_conf.yml" + - "--entrypoints.web.address=:80" + - "--entrypoints.web-secure.address=:443" + - "--entrypoints.web-admin.address=:8443" + - "--certificatesresolvers.myhttpchallenge.acme.httpchallenge=true" + - "--certificatesresolvers.myhttpchallenge.acme.httpchallenge.entrypoint=web" + - "--certificatesresolvers.myhttpchallenge.acme.email=${MYMAIL}" + - "--certificatesresolvers.myhttpchallenge.acme.storage=/letsencrypt/acme.json" + - "--entrypoints.mongo.address=:27017" + #- --certificatesresolvers.myhttpchallenge.acme.caserver=https://acme-v02.api.letsencrypt.org/directory + labels: + - "traefik.enable=true" + # To Fix enable dashboard on port 8443 + - "traefik.http.routers.dashboard.entrypoints=web-admin" + - "traefik.http.routers.dashboard.rule=Host(`${MYDOMAIN}`)" + # - "traefik.http.routers.dashboard.rule=Host(`traefik.${MYDOMAIN}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))" + - "traefik.http.routers.dashboard.tls=true" + - "traefik.http.routers.dashboard.middlewares=auth" + - "traefik.http.middlewares.auth.basicauth.usersfile=/users.htpasswd" + - "traefik.http.routers.dashboard.service=api@internal" + - "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https" + - "traefik.http.routers.proxy-https.entrypoints=web-secure" + - "traefik.http.routers.proxy-https.rule=Host(`${MYDOMAIN}`)" logging: driver: "json-file"