aboutsummaryrefslogtreecommitdiff
path: root/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex
diff options
context:
space:
mode:
Diffstat (limited to 'overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex')
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationController.js700
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationManager.js446
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/ContactController.js140
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-index.pug57
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug79
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authcontroller.diff103
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authmanager.diff297
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/contactcontroller.diff133
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.diff54
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.pug51
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/navbar.pug84
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/router-append.js6
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.diff200
-rw-r--r--overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.pug178
14 files changed, 2528 insertions, 0 deletions
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationController.js b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationController.js
new file mode 100644
index 0000000..b49c23a
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationController.js
@@ -0,0 +1,700 @@
+const AuthenticationManager = require('./AuthenticationManager')
+const SessionManager = require('./SessionManager')
+const OError = require('@overleaf/o-error')
+const LoginRateLimiter = require('../Security/LoginRateLimiter')
+const UserUpdater = require('../User/UserUpdater')
+const Metrics = require('@overleaf/metrics')
+const logger = require('@overleaf/logger')
+const querystring = require('querystring')
+const Settings = require('@overleaf/settings')
+const basicAuth = require('basic-auth')
+const tsscmp = require('tsscmp')
+const UserHandler = require('../User/UserHandler')
+const UserSessionsManager = require('../User/UserSessionsManager')
+const SessionStoreManager = require('../../infrastructure/SessionStoreManager')
+const Analytics = require('../Analytics/AnalyticsManager')
+const passport = require('passport')
+const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
+const UrlHelper = require('../Helpers/UrlHelper')
+const AsyncFormHelper = require('../Helpers/AsyncFormHelper')
+const _ = require('lodash')
+const UserAuditLogHandler = require('../User/UserAuditLogHandler')
+const AnalyticsRegistrationSourceHelper = require('../Analytics/AnalyticsRegistrationSourceHelper')
+const axios = require('axios').default
+const Path = require('path')
+const {
+ acceptsJson,
+} = require('../../infrastructure/RequestContentTypeDetection')
+const { ParallelLoginError } = require('./AuthenticationErrors')
+const { hasAdminAccess } = require('../Helpers/AdminAuthorizationHelper')
+const Modules = require('../../infrastructure/Modules')
+
+function send401WithChallenge(res) {
+ res.setHeader('WWW-Authenticate', 'OverleafLogin')
+ res.sendStatus(401)
+}
+
+function checkCredentials(userDetailsMap, user, password) {
+ const expectedPassword = userDetailsMap.get(user)
+ const userExists = userDetailsMap.has(user) && expectedPassword // user exists with a non-null password
+ const isValid = userExists && tsscmp(expectedPassword, password)
+ if (!isValid) {
+ logger.err({ user }, 'invalid login details')
+ }
+ Metrics.inc('security.http-auth.check-credentials', 1, {
+ path: userExists ? 'known-user' : 'unknown-user',
+ status: isValid ? 'pass' : 'fail',
+ })
+ return isValid
+}
+
+const AuthenticationController = {
+ serializeUser(user, callback) {
+ if (!user._id || !user.email) {
+ const err = new Error('serializeUser called with non-user object')
+ logger.warn({ user }, err.message)
+ return callback(err)
+ }
+ const lightUser = {
+ _id: user._id,
+ first_name: user.first_name,
+ last_name: user.last_name,
+ isAdmin: user.isAdmin,
+ staffAccess: user.staffAccess,
+ email: user.email,
+ referal_id: user.referal_id,
+ session_created: new Date().toISOString(),
+ ip_address: user._login_req_ip,
+ must_reconfirm: user.must_reconfirm,
+ v1_id: user.overleaf != null ? user.overleaf.id : undefined,
+ analyticsId: user.analyticsId || user._id,
+ }
+ callback(null, lightUser)
+ },
+
+ deserializeUser(user, cb) {
+ cb(null, user)
+ },
+
+ passportLogin(req, res, next) {
+ // This function is middleware which wraps the passport.authenticate middleware,
+ // so we can send back our custom `{message: {text: "", type: ""}}` responses on failure,
+ // and send a `{redir: ""}` response on success
+ passport.authenticate('local', function (err, user, info) {
+ if (err) {
+ return next(err)
+ }
+ if (user) {
+ // `user` is either a user object or false
+ AuthenticationController.setAuditInfo(req, { method: 'Password login' })
+ return AuthenticationController.finishLogin(user, req, res, next)
+ } else {
+ if (info.redir != null) {
+ return res.json({ redir: info.redir })
+ } else {
+ res.status(info.status || 200)
+ delete info.status
+ const body = { message: info }
+ const { errorReason } = info
+ if (errorReason) {
+ body.errorReason = errorReason
+ delete info.errorReason
+ }
+ return res.json(body)
+ }
+ }
+ })(req, res, next)
+ },
+
+ finishLogin(user, req, res, next) {
+ if (user === false) {
+ return res.redirect('/login')
+ } // OAuth2 'state' mismatch
+
+ if (Settings.adminOnlyLogin && !hasAdminAccess(user)) {
+ return res.status(403).json({
+ message: { type: 'error', text: 'Admin only panel' },
+ })
+ }
+
+ const auditInfo = AuthenticationController.getAuditInfo(req)
+
+ const anonymousAnalyticsId = req.session.analyticsId
+ const isNewUser = req.session.justRegistered || false
+
+ const Modules = require('../../infrastructure/Modules')
+ Modules.hooks.fire(
+ 'preFinishLogin',
+ req,
+ res,
+ user,
+ function (error, results) {
+ if (error) {
+ return next(error)
+ }
+ if (results.some(result => result && result.doNotFinish)) {
+ return
+ }
+
+ if (user.must_reconfirm) {
+ return AuthenticationController._redirectToReconfirmPage(
+ req,
+ res,
+ user
+ )
+ }
+
+ const redir =
+ AuthenticationController._getRedirectFromSession(req) || '/project'
+ _loginAsyncHandlers(req, user, anonymousAnalyticsId, isNewUser)
+ const userId = user._id
+ UserAuditLogHandler.addEntry(
+ userId,
+ 'login',
+ userId,
+ req.ip,
+ auditInfo,
+ err => {
+ if (err) {
+ return next(err)
+ }
+ _afterLoginSessionSetup(req, user, function (err) {
+ if (err) {
+ return next(err)
+ }
+ AuthenticationController._clearRedirectFromSession(req)
+ AnalyticsRegistrationSourceHelper.clearSource(req.session)
+ AnalyticsRegistrationSourceHelper.clearInbound(req.session)
+ AsyncFormHelper.redirect(req, res, redir)
+ })
+ }
+ )
+ }
+ )
+ },
+
+ doPassportLogin(req, username, password, done) {
+ const email = username.toLowerCase()
+ const Modules = require('../../infrastructure/Modules')
+ Modules.hooks.fire(
+ 'preDoPassportLogin',
+ req,
+ email,
+ function (err, infoList) {
+ if (err) {
+ return done(err)
+ }
+ const info = infoList.find(i => i != null)
+ if (info != null) {
+ return done(null, false, info)
+ }
+ LoginRateLimiter.processLoginRequest(email, function (err, isAllowed) {
+ if (err) {
+ return done(err)
+ }
+ if (!isAllowed) {
+ logger.debug({ email }, 'too many login requests')
+ return done(null, null, {
+ text: req.i18n.translate('to_many_login_requests_2_mins'),
+ type: 'error',
+ status: 429,
+ })
+ }
+ AuthenticationManager.authenticate(
+ { email },
+ password,
+ function (error, user) {
+ if (error != null) {
+ if (error instanceof ParallelLoginError) {
+ return done(null, false, { status: 429 })
+ }
+ return done(error)
+ }
+ if (
+ user &&
+ AuthenticationController.captchaRequiredForLogin(req, user)
+ ) {
+ done(null, false, {
+ text: req.i18n.translate('cannot_verify_user_not_robot'),
+ type: 'error',
+ errorReason: 'cannot_verify_user_not_robot',
+ status: 400,
+ })
+ } else if (user) {
+ // async actions
+ done(null, user)
+ } else {
+ AuthenticationController._recordFailedLogin()
+ logger.debug({ email }, 'failed log in')
+ done(null, false, {
+ text: req.i18n.translate('email_or_password_wrong_try_again'),
+ type: 'error',
+ status: 401,
+ })
+ }
+ }
+ )
+ })
+ }
+ )
+ },
+
+ captchaRequiredForLogin(req, user) {
+ switch (AuthenticationController.getAuditInfo(req).captcha) {
+ case 'disabled':
+ return false
+ case 'solved':
+ return false
+ case 'skipped': {
+ let required = false
+ if (user.lastFailedLogin) {
+ const requireCaptchaUntil =
+ user.lastFailedLogin.getTime() +
+ Settings.elevateAccountSecurityAfterFailedLogin
+ required = requireCaptchaUntil >= Date.now()
+ }
+ Metrics.inc('force_captcha_on_login', 1, {
+ status: required ? 'yes' : 'no',
+ })
+ return required
+ }
+ default:
+ throw new Error('captcha middleware missing in handler chain')
+ }
+ },
+
+ ipMatchCheck(req, user) {
+ if (req.ip !== user.lastLoginIp) {
+ NotificationsBuilder.ipMatcherAffiliation(user._id).create(
+ req.ip,
+ () => {}
+ )
+ }
+ return UserUpdater.updateUser(
+ user._id.toString(),
+ {
+ $set: { lastLoginIp: req.ip },
+ },
+ () => {}
+ )
+ },
+
+ requireLogin() {
+ const doRequest = function (req, res, next) {
+ if (next == null) {
+ next = function () {}
+ }
+ if (!SessionManager.isUserLoggedIn(req.session)) {
+ if (acceptsJson(req)) return send401WithChallenge(res)
+ return AuthenticationController._redirectToLoginOrRegisterPage(req, res)
+ } else {
+ req.user = SessionManager.getSessionUser(req.session)
+ return next()
+ }
+ }
+
+ return doRequest
+ },
+
+ oauth2Redirect(req, res, next) {
+ res.redirect(`${process.env.OAUTH_AUTH_URL}?` +
+ querystring.stringify({
+ client_id: process.env.OAUTH_CLIENT_ID,
+ response_type: "code",
+ redirect_uri: (process.env.SHARELATEX_SITE_URL + "/oauth/callback"),
+ }));
+ },
+
+ oauth2Callback(req, res, next) {
+ const code = req.query.code;
+
+//construct axios body
+ const params = new URLSearchParams()
+ params.append('grant_type', "authorization_code")
+ params.append('client_id', process.env.OAUTH_CLIENT_ID)
+ params.append('client_secret', process.env.OAUTH_CLIENT_SECRET)
+ params.append("code", code)
+ params.append('redirect_uri', (process.env.SHARELATEX_SITE_URL + "/oauth/callback"))
+
+
+ // json_body = {
+ // "grant_type": "authorization_code",
+ // client_id: process.env.OAUTH_CLIENT_ID,
+ // client_secret: process.env.OAUTH_CLIENT_SECRET,
+ // "code": code,
+ // redirect_uri: (process.env.SHARELATEX_SITE_URL + "/oauth/callback"),
+ // }
+
+ axios.post(process.env.OAUTH_ACCESS_URL, params, {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+
+ }
+ }).then(access_res => {
+
+ // console.log("respond is " + JSON.stringify(access_res.data))
+ // console.log("authorization_bearer_is " + authorization_bearer)
+ authorization_bearer = "Bearer " + access_res.data.access_token
+
+ let axios_get_config = {
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "Authorization": authorization_bearer,
+ },
+ params: access_res.data
+ }
+
+ axios.get(process.env.OAUTH_USER_URL, axios_get_config).then(info_res => {
+ // console.log("oauth_user: ", JSON.stringify(info_res.data));
+ if (info_res.data.err) {
+ res.json({message: info_res.data.err});
+ } else {
+ AuthenticationManager.createUserIfNotExist(info_res.data, (error, user) => {
+ if (error) {
+ res.json({message: error});
+ } else {
+ // console.log("real_user: ", user);
+ AuthenticationController.finishLogin(user, req, res, next);
+ }
+ });
+ }
+ });
+ });
+ },
+
+
+ requireOauth() {
+ // require this here because module may not be included in some versions
+ const Oauth2Server = require('../../../../modules/oauth2-server/app/src/Oauth2Server')
+ return function (req, res, next) {
+ if (next == null) {
+ next = function () {}
+ }
+ const request = new Oauth2Server.Request(req)
+ const response = new Oauth2Server.Response(res)
+ return Oauth2Server.server.authenticate(
+ request,
+ response,
+ {},
+ function (err, token) {
+ if (err) {
+ // use a 401 status code for malformed header for git-bridge
+ if (
+ err.code === 400 &&
+ err.message === 'Invalid request: malformed authorization header'
+ ) {
+ err.code = 401
+ }
+ // send all other errors
+ return res
+ .status(err.code)
+ .json({ error: err.name, error_description: err.message })
+ }
+ req.oauth = { access_token: token.accessToken }
+ req.oauth_token = token
+ req.oauth_user = token.user
+ return next()
+ }
+ )
+ }
+ },
+
+ validateUserSession: function () {
+ // Middleware to check that the user's session is still good on key actions,
+ // such as opening a a project. Could be used to check that session has not
+ // exceeded a maximum lifetime (req.session.session_created), or for session
+ // hijacking checks (e.g. change of ip address, req.session.ip_address). For
+ // now, just check that the session has been loaded from the session store
+ // correctly.
+ return function (req, res, next) {
+ // check that the session store is returning valid results
+ if (req.session && !SessionStoreManager.hasValidationToken(req)) {
+ // force user to update session
+ req.session.regenerate(() => {
+ // need to destroy the existing session and generate a new one
+ // otherwise they will already be logged in when they are redirected
+ // to the login page
+ if (acceptsJson(req)) return send401WithChallenge(res)
+ AuthenticationController._redirectToLoginOrRegisterPage(req, res)
+ })
+ } else {
+ next()
+ }
+ }
+ },
+
+ _globalLoginWhitelist: [],
+ addEndpointToLoginWhitelist(endpoint) {
+ return AuthenticationController._globalLoginWhitelist.push(endpoint)
+ },
+
+ requireGlobalLogin(req, res, next) {
+ if (
+ AuthenticationController._globalLoginWhitelist.includes(
+ req._parsedUrl.pathname
+ )
+ ) {
+ return next()
+ }
+
+ if (req.headers.authorization != null) {
+ AuthenticationController.requirePrivateApiAuth()(req, res, next)
+ } else if (SessionManager.isUserLoggedIn(req.session)) {
+ next()
+ } else {
+ logger.debug(
+ { url: req.url },
+ 'user trying to access endpoint not in global whitelist'
+ )
+ if (acceptsJson(req)) return send401WithChallenge(res)
+ AuthenticationController.setRedirectInSession(req)
+ res.redirect('/login')
+ }
+ },
+
+ validateAdmin(req, res, next) {
+ const adminDomains = Settings.adminDomains
+ if (
+ !adminDomains ||
+ !(Array.isArray(adminDomains) && adminDomains.length)
+ ) {
+ return next()
+ }
+ const user = SessionManager.getSessionUser(req.session)
+ if (!hasAdminAccess(user)) {
+ return next()
+ }
+ const email = user.email
+ if (email == null) {
+ return next(
+ new OError('[ValidateAdmin] Admin user without email address', {
+ userId: user._id,
+ })
+ )
+ }
+ if (!adminDomains.find(domain => email.endsWith(`@${domain}`))) {
+ return next(
+ new OError('[ValidateAdmin] Admin user with invalid email domain', {
+ email,
+ userId: user._id,
+ })
+ )
+ }
+ return next()
+ },
+
+ requireBasicAuth: function (userDetails) {
+ const userDetailsMap = new Map(Object.entries(userDetails))
+ return function (req, res, next) {
+ const credentials = basicAuth(req)
+ if (
+ !credentials ||
+ !checkCredentials(userDetailsMap, credentials.name, credentials.pass)
+ ) {
+ send401WithChallenge(res)
+ Metrics.inc('security.http-auth', 1, { status: 'reject' })
+ } else {
+ Metrics.inc('security.http-auth', 1, { status: 'accept' })
+ next()
+ }
+ }
+ },
+
+ requirePrivateApiAuth() {
+ return AuthenticationController.requireBasicAuth(Settings.httpAuthUsers)
+ },
+
+ setAuditInfo(req, info) {
+ if (!req.__authAuditInfo) {
+ req.__authAuditInfo = {}
+ }
+ Object.assign(req.__authAuditInfo, info)
+ },
+
+ getAuditInfo(req) {
+ return req.__authAuditInfo || {}
+ },
+
+ setRedirectInSession(req, value) {
+ if (value == null) {
+ value =
+ Object.keys(req.query).length > 0
+ ? `${req.path}?${querystring.stringify(req.query)}`
+ : `${req.path}`
+ }
+ if (
+ req.session != null &&
+ !/^\/(socket.io|js|stylesheets|img)\/.*$/.test(value) &&
+ !/^.*\.(png|jpeg|svg)$/.test(value)
+ ) {
+ const safePath = UrlHelper.getSafeRedirectPath(value)
+ return (req.session.postLoginRedirect = safePath)
+ }
+ },
+
+ _redirectToLoginOrRegisterPage(req, res) {
+ if (
+ req.query.zipUrl != null ||
+ req.query.project_name != null ||
+ req.path === '/user/subscription/new'
+ ) {
+ AuthenticationController._redirectToRegisterPage(req, res)
+ } else {
+ AuthenticationController._redirectToLoginPage(req, res)
+ }
+ },
+
+ _redirectToLoginPage(req, res) {
+ logger.debug(
+ { url: req.url },
+ 'user not logged in so redirecting to login page'
+ )
+ AuthenticationController.setRedirectInSession(req)
+ const url = `/login?${querystring.stringify(req.query)}`
+ res.redirect(url)
+ Metrics.inc('security.login-redirect')
+ },
+
+ _redirectToReconfirmPage(req, res, user) {
+ logger.debug(
+ { url: req.url },
+ 'user needs to reconfirm so redirecting to reconfirm page'
+ )
+ req.session.reconfirm_email = user != null ? user.email : undefined
+ const redir = '/user/reconfirm'
+ AsyncFormHelper.redirect(req, res, redir)
+ },
+
+ _redirectToRegisterPage(req, res) {
+ logger.debug(
+ { url: req.url },
+ 'user not logged in so redirecting to register page'
+ )
+ AuthenticationController.setRedirectInSession(req)
+ const url = `/register?${querystring.stringify(req.query)}`
+ res.redirect(url)
+ Metrics.inc('security.login-redirect')
+ },
+
+ _recordSuccessfulLogin(userId, callback) {
+ if (callback == null) {
+ callback = function () {}
+ }
+ UserUpdater.updateUser(
+ userId.toString(),
+ {
+ $set: { lastLoggedIn: new Date() },
+ $inc: { loginCount: 1 },
+ },
+ function (error) {
+ if (error != null) {
+ callback(error)
+ }
+ Metrics.inc('user.login.success')
+ callback()
+ }
+ )
+ },
+
+ _recordFailedLogin(callback) {
+ Metrics.inc('user.login.failed')
+ if (callback) callback()
+ },
+
+ _getRedirectFromSession(req) {
+ let safePath
+ const value = _.get(req, ['session', 'postLoginRedirect'])
+ if (value) {
+ safePath = UrlHelper.getSafeRedirectPath(value)
+ }
+ return safePath || null
+ },
+
+ _clearRedirectFromSession(req) {
+ if (req.session != null) {
+ delete req.session.postLoginRedirect
+ }
+ },
+}
+
+function _afterLoginSessionSetup(req, user, callback) {
+ if (callback == null) {
+ callback = function () {}
+ }
+ req.login(user, function (err) {
+ if (err) {
+ OError.tag(err, 'error from req.login', {
+ user_id: user._id,
+ })
+ return callback(err)
+ }
+ // Regenerate the session to get a new sessionID (cookie value) to
+ // protect against session fixation attacks
+ const oldSession = req.session
+ req.session.destroy(function (err) {
+ if (err) {
+ OError.tag(err, 'error when trying to destroy old session', {
+ user_id: user._id,
+ })
+ return callback(err)
+ }
+ req.sessionStore.generate(req)
+ // Note: the validation token is not writable, so it does not get
+ // transferred to the new session below.
+ for (const key in oldSession) {
+ const value = oldSession[key]
+ if (key !== '__tmp' && key !== 'csrfSecret') {
+ req.session[key] = value
+ }
+ }
+ req.session.save(function (err) {
+ if (err) {
+ OError.tag(err, 'error saving regenerated session after login', {
+ user_id: user._id,
+ })
+ return callback(err)
+ }
+ UserSessionsManager.trackSession(user, req.sessionID, function () {})
+ if (!req.deviceHistory) {
+ // Captcha disabled or SSO-based login.
+ return callback()
+ }
+ req.deviceHistory.add(user.email)
+ req.deviceHistory
+ .serialize(req.res)
+ .catch(err => {
+ logger.err({ err }, 'cannot serialize deviceHistory')
+ })
+ .finally(() => callback())
+ })
+ })
+ })
+}
+
+function _loginAsyncHandlers(req, user, anonymousAnalyticsId, isNewUser) {
+ UserHandler.setupLoginData(user, err => {
+ if (err != null) {
+ logger.warn({ err }, 'error setting up login data')
+ }
+ })
+ LoginRateLimiter.recordSuccessfulLogin(user.email, () => {})
+ AuthenticationController._recordSuccessfulLogin(user._id, () => {})
+ AuthenticationController.ipMatchCheck(req, user)
+ Analytics.recordEventForUser(user._id, 'user-logged-in', {
+ source: req.session.saml
+ ? 'saml'
+ : req.user_info?.auth_provider || 'email-password',
+ })
+ Analytics.identifyUser(user._id, anonymousAnalyticsId, isNewUser)
+
+ logger.debug(
+ { email: user.email, user_id: user._id.toString() },
+ 'successful log in'
+ )
+
+ req.session.justLoggedIn = true
+ // capture the request ip for use when creating the session
+ return (user._login_req_ip = req.ip)
+}
+
+module.exports = AuthenticationController
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationManager.js b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationManager.js
new file mode 100644
index 0000000..8519be1
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/AuthenticationManager.js
@@ -0,0 +1,446 @@
+const Settings = require('@overleaf/settings')
+const { User } = require('../../models/User')
+const { db, ObjectId } = require('../../infrastructure/mongodb')
+const bcrypt = require('bcrypt')
+const EmailHelper = require('../Helpers/EmailHelper')
+const {
+ InvalidEmailError,
+ InvalidPasswordError,
+ ParallelLoginError,
+} = require('./AuthenticationErrors')
+const util = require('util')
+const { Client } = require('ldapts');
+const ldapEscape = require('ldap-escape');
+const HaveIBeenPwned = require('./HaveIBeenPwned')
+
+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.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 mongodb (such as default values)
+ User.findOne(query, (error, user) => {
+ //console.log("Begining:" + JSON.stringify(query))
+ AuthenticationManager.authUserObj(error, user, query, password, callback)
+ })
+ },
+ //login with any password
+ login(user, password, callback) {
+ AuthenticationManager.checkRounds(
+ user,
+ user.hashedPassword,
+ password,
+ function (err) {
+ if (err) {
+ return callback(err)
+ }
+ callback(null, user)
+ HaveIBeenPwned.checkPasswordForReuseInBackground(password)
+ }
+ )
+ },
+
+ //oauth2
+ createUserIfNotExist(oauth_user, callback) {
+ const query = {
+ //name: ZHANG San
+ email: oauth_user.email
+ };
+ User.findOne(query, (error, user) => {
+ if ((!user || !user.hashedPassword)) {
+ //create random pass for local userdb, does not get checked for ldap users during login
+ let pass = require("crypto").randomBytes(32).toString("hex")
+ const userRegHand = require('../User/UserRegistrationHandler.js')
+ userRegHand.registerNewUser({
+ email: query.email,
+ first_name: oauth_user.given_name,
+ last_name: oauth_user.family_name,
+ password: pass
+ },
+ function (error, user) {
+ if (error) {
+ return callback(error, null);
+ }
+ user.admin = false
+ user.emails[0].confirmedAt = Date.now()
+ user.save()
+ console.log("user %s added to local library", query.email)
+ User.findOne(query, (error, user) => {
+ if (error) {
+ return callback(error, null);
+ }
+ if (user && user.hashedPassword) {
+ return callback(null, user);
+ } else {
+ return callback("Unknown error", null);
+ }
+ }
+ )
+ })
+ } else {
+ return callback(null, user);
+ }
+ });
+ },
+
+ //LDAP
+ createIfNotExistAndLogin(query, user, callback, uid, firstname, lastname, mail, isAdmin) {
+ 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")
+ //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) => {
+ 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)
+ }
+ })
+ } else {
+ // No local passwd check user has to be in ldap and use ldap credentials
+ AuthenticationManager.ldapAuth(query, password, AuthenticationManager.createIfNotExistAndLogin, callback, user)
+ }
+ 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
+ },
+
+ // 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.
+ validatePassword(password, email) {
+ if (password == null) {
+ return new InvalidPasswordError({
+ message: 'password not set',
+ info: { code: 'not_set' },
+ })
+ }
+
+ let allowAnyChars, min, max
+ if (Settings.passwordStrengthOptions) {
+ allowAnyChars = Settings.passwordStrengthOptions.allowAnyChars === true
+ if (Settings.passwordStrengthOptions.length) {
+ min = Settings.passwordStrengthOptions.length.min
+ max = Settings.passwordStrengthOptions.length.max
+ }
+ }
+ allowAnyChars = !!allowAnyChars
+ min = min || 6
+ max = max || 72
+
+ // we don't support passwords > 72 characters in length, because bcrypt truncates them
+ if (max > 72) {
+ max = 72
+ }
+
+ if (password.length < min) {
+ return new InvalidPasswordError({
+ message: 'password is too short',
+ info: { code: 'too_short' },
+ })
+ }
+ if (password.length > max) {
+ return new InvalidPasswordError({
+ message: 'password is too long',
+ info: { code: 'too_long' },
+ })
+ }
+ if (
+ !allowAnyChars &&
+ !AuthenticationManager._passwordCharactersAreValid(password)
+ ) {
+ return new InvalidPasswordError({
+ message: 'password contains an invalid character',
+ info: { code: 'invalid_character' },
+ })
+ }
+ if (typeof email === 'string' && email !== '') {
+ const startOfEmail = email.split('@')[0]
+ if (
+ password.indexOf(email) !== -1 ||
+ password.indexOf(startOfEmail) !== -1
+ ) {
+ return new InvalidPasswordError({
+ message: 'password contains part of email address',
+ info: { code: 'contains_email' },
+ })
+ }
+ }
+ return null
+ },
+
+ setUserPassword(user, password, callback) {
+ AuthenticationManager.setUserPasswordInV2(user, password, callback)
+ },
+
+ checkRounds(user, hashedPassword, password, callback) {
+ // Temporarily disable this function, TODO: re-enable this
+ 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'))
+ }
+ 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)
+ HaveIBeenPwned.checkPasswordForReuseInBackground(password)
+ }
+ )
+ })
+ },
+
+ _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,
+ });
+
+ 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)
+ }
+ })
+ }
+
+
+
+}
+
+AuthenticationManager.promises = {
+ authenticate: util.promisify(AuthenticationManager.authenticate),
+ hashPassword: util.promisify(AuthenticationManager.hashPassword),
+ setUserPassword: util.promisify(AuthenticationManager.setUserPassword),
+}
+
+module.exports = AuthenticationManager
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/ContactController.js b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/ContactController.js
new file mode 100644
index 0000000..4146982
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/ContactController.js
@@ -0,0 +1,140 @@
+/* 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
+ */
+let ContactsController
+const AuthenticationController = require('../Authentication/AuthenticationController')
+const SessionManager = require('../Authentication/SessionManager')
+const ContactManager = require('./ContactManager')
+const UserGetter = require('../User/UserGetter')
+const logger = require('logger-sharelatex')
+const Modules = require('../../infrastructure/Modules')
+const { Client } = require('ldapts');
+
+module.exports = ContactsController = {
+ getContacts(req, res, next) {
+ // const user_id = AuthenticationController.getLoggedInUserId(req)
+ 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)
+ }
+
+ // 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',
+ }
+ },
+}
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-index.pug b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-index.pug
new file mode 100644
index 0000000..88e264b
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-index.pug
@@ -0,0 +1,57 @@
+extends ../layout
+
+block content
+ .content.content-alt
+ .container
+ .row
+ .col-xs-12
+ .card(ng-controller="RegisterUsersController")
+ .page-header
+ h1 Admin Panel
+ tabset(ng-cloak)
+ tab(heading="System Messages")
+ each message in systemMessages
+ .alert.alert-info.row-spaced(ng-non-bindable) #{message.content}
+ hr
+ form(method='post', action='/admin/messages')
+ input(name="_csrf", type="hidden", value=csrfToken)
+ .form-group
+ label(for="content")
+ input.form-control(name="content", type="text", placeholder="Message...", required)
+ button.btn.btn-primary(type="submit") Post Message
+ hr
+ form(method='post', action='/admin/messages/clear')
+ input(name="_csrf", type="hidden", value=csrfToken)
+ button.btn.btn-danger(type="submit") Clear all messages
+
+
+ tab(heading="Register non LDAP User")
+ form.form
+ .row
+ .col-md-4.col-xs-8
+ input.form-control(
+ name="email",
+ type="text",
+ placeholder="jane@example.com, joe@example.com",
+ ng-model="inputs.emails",
+ on-enter="registerUsers()"
+ )
+ .col-md-8.col-xs-4
+ button.btn.btn-primary(ng-click="registerUsers()") #{translate("register")}
+
+ .row-spaced(ng-show="error").ng-cloak.text-danger
+ p Sorry, an error occured
+
+ .row-spaced(ng-show="users.length > 0").ng-cloak.text-success
+ p We've sent out welcome emails to the registered users.
+ p You can also manually send them URLs below to allow them to reset their password and log in for the first time.
+ p (Password reset tokens will expire after one week and the user will need registering again).
+
+ hr(ng-show="users.length > 0").ng-cloak
+ table(ng-show="users.length > 0").table.table-striped.ng-cloak
+ tr
+ th #{translate("email")}
+ th Set Password Url
+ tr(ng-repeat="user in users")
+ td {{ user.email }}
+ td(style="word-break: break-all;") {{ user.setNewPasswordUrl }}
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug
new file mode 100644
index 0000000..c7131a3
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug
@@ -0,0 +1,79 @@
+extends ../layout
+
+block content
+ .content.content-alt
+ .container
+ .row
+ .col-xs-12
+ .card(ng-controller="RegisterUsersController")
+ .page-header
+ h1 Admin Panel
+ tabset(ng-cloak)
+ tab(heading="System Messages")
+ each message in systemMessages
+ .alert.alert-info.row-spaced(ng-non-bindable) #{message.content}
+ hr
+ form(method='post', action='/admin/messages')
+ input(name="_csrf", type="hidden", value=csrfToken)
+ .form-group
+ label(for="content")
+ input.form-control(name="content", type="text", placeholder="Message...", required)
+ button.btn.btn-primary(type="submit") Post Message
+ hr
+ form(method='post', action='/admin/messages/clear')
+ input(name="_csrf", type="hidden", value=csrfToken)
+ button.btn.btn-danger(type="submit") Clear all messages
+
+
+ tab(heading="Register non LDAP User")
+ form.form
+ .row
+ .col-md-4.col-xs-8
+ input.form-control(
+ name="email",
+ type="text",
+ placeholder="jane@example.com, joe@example.com",
+ ng-model="inputs.emails",
+ on-enter="registerUsers()"
+ )
+ .col-md-8.col-xs-4
+ button.btn.btn-primary(ng-click="registerUsers()") #{translate("register")}
+
+ .row-spaced(ng-show="error").ng-cloak.text-danger
+ p Sorry, an error occured
+
+ .row-spaced(ng-show="users.length > 0").ng-cloak.text-success
+ p We've sent out welcome emails to the registered users.
+ p You can also manually send them URLs below to allow them to reset their password and log in for the first time.
+ p (Password reset tokens will expire after one week and the user will need registering again).
+
+ hr(ng-show="users.length > 0").ng-cloak
+ table(ng-show="users.length > 0").table.table-striped.ng-cloak
+ tr
+ th #{translate("email")}
+ th Set Password Url
+ tr(ng-repeat="user in users")
+ td {{ user.email }}
+ td(style="word-break: break-all;") {{ user.setNewPasswordUrl }}
+ tab(heading="Open/Close Editor" bookmarkable-tab="open-close-editor")
+ if hasFeature('saas')
+ | The "Open/Close Editor" feature is not available in SAAS.
+ else
+ .row-spaced
+ form(method='post',action='/admin/closeEditor')
+ input(name="_csrf", type="hidden", value=csrfToken)
+ button.btn.btn-danger(type="submit") Close Editor
+ p.small Will stop anyone opening the editor. Will NOT disconnect already connected users.
+
+ .row-spaced
+ form(method='post',action='/admin/disconnectAllUsers')
+ input(name="_csrf", type="hidden", value=csrfToken)
+ button.btn.btn-danger(type="submit") Disconnect all users
+ p.small Will force disconnect all users with the editor open. Make sure to close the editor first to avoid them reconnecting.
+
+ .row-spaced
+ form(method='post',action='/admin/openEditor')
+ input(name="_csrf", type="hidden", value=csrfToken)
+ button.btn.btn-danger(type="submit") Reopen Editor
+ p.small Will reopen the editor after closing.
+
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authcontroller.diff b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authcontroller.diff
new file mode 100644
index 0000000..c45a271
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authcontroller.diff
@@ -0,0 +1,103 @@
+23,24d22
+< const axios = require('axios').default
+< const Path = require('path')
+195c193
+< logger.debug({ email }, 'too many login requests')
+---
+> logger.log({ email }, 'too many login requests')
+227c225
+< logger.debug({ email }, 'failed log in')
+---
+> logger.log({ email }, 'failed log in')
+298,364d295
+< oauth2Redirect(req, res, next) {
+< res.redirect(`${process.env.OAUTH_AUTH_URL}?` +
+< querystring.stringify({
+< client_id: process.env.OAUTH_CLIENT_ID,
+< response_type: "code",
+< redirect_uri: (process.env.SHARELATEX_SITE_URL + "/oauth/callback"),
+< }));
+< },
+<
+< oauth2Callback(req, res, next) {
+< const code = req.query.code;
+<
+< //construct axios body
+< const params = new URLSearchParams()
+< params.append('grant_type', "authorization_code")
+< params.append('client_id', process.env.OAUTH_CLIENT_ID)
+< params.append('client_secret', process.env.OAUTH_CLIENT_SECRET)
+< params.append("code", code)
+< params.append('redirect_uri', (process.env.SHARELATEX_SITE_URL + "/oauth/callback"))
+<
+<
+< // json_body = {
+< // "grant_type": "authorization_code",
+< // client_id: process.env.OAUTH_CLIENT_ID,
+< // client_secret: process.env.OAUTH_CLIENT_SECRET,
+< // "code": code,
+< // redirect_uri: (process.env.SHARELATEX_SITE_URL + "/oauth/callback"),
+< // }
+<
+< axios.post(process.env.OAUTH_ACCESS_URL, params, {
+< headers: {
+< "Content-Type": "application/x-www-form-urlencoded",
+<
+< }
+< }).then(access_res => {
+<
+< // console.log("respond is " + JSON.stringify(access_res.data))
+< // console.log("authorization_bearer_is " + authorization_bearer)
+< authorization_bearer = "Bearer " + access_res.data.access_token
+<
+< let axios_get_config = {
+< headers: {
+< "Content-Type": "application/x-www-form-urlencoded",
+< "Authorization": authorization_bearer,
+< },
+< params: access_res.data
+< }
+<
+< axios.get(process.env.OAUTH_USER_URL, axios_get_config).then(info_res => {
+< // console.log("oauth_user: ", JSON.stringify(info_res.data));
+< if (info_res.data.err) {
+< res.json({message: info_res.data.err});
+< } else {
+< AuthenticationManager.createUserIfNotExist(info_res.data, (error, user) => {
+< if (error) {
+< res.json({message: error});
+< } else {
+< // console.log("real_user: ", user);
+< AuthenticationController.finishLogin(user, req, res, next);
+< }
+< });
+< }
+< });
+< });
+< },
+<
+<
+444c375
+< logger.debug(
+---
+> logger.log(
+477c408
+< email,
+---
+> email: email,
+547c478
+< logger.debug(
+---
+> logger.log(
+558c489
+< logger.debug(
+---
+> logger.log(
+568c499
+< logger.debug(
+---
+> logger.log(
+689c620
+< logger.debug(
+---
+> logger.log(
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authmanager.diff b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authmanager.diff
new file mode 100644
index 0000000..841804d
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/authmanager.diff
@@ -0,0 +1,297 @@
+12,13d11
+< const { Client } = require('ldapts');
+< const ldapEscape = require('ldap-escape');
+34,92c32,36
+< //console.log("Begining:" + JSON.stringify(query))
+< AuthenticationManager.authUserObj(error, user, query, password, callback)
+< })
+< },
+< //login with any password
+< login(user, password, callback) {
+< AuthenticationManager.checkRounds(
+< user,
+< user.hashedPassword,
+< password,
+< function (err) {
+< if (err) {
+< return callback(err)
+< }
+< callback(null, user)
+< HaveIBeenPwned.checkPasswordForReuseInBackground(password)
+< }
+< )
+< },
+<
+< //oauth2
+< createUserIfNotExist(oauth_user, callback) {
+< const query = {
+< //name: ZHANG San
+< email: oauth_user.email
+< };
+< User.findOne(query, (error, user) => {
+< if ((!user || !user.hashedPassword)) {
+< //create random pass for local userdb, does not get checked for ldap users during login
+< let pass = require("crypto").randomBytes(32).toString("hex")
+< const userRegHand = require('../User/UserRegistrationHandler.js')
+< userRegHand.registerNewUser({
+< email: query.email,
+< first_name: oauth_user.given_name,
+< last_name: oauth_user.family_name,
+< password: pass
+< },
+< function (error, user) {
+< if (error) {
+< return callback(error, null);
+< }
+< user.admin = false
+< user.emails[0].confirmedAt = Date.now()
+< user.save()
+< console.log("user %s added to local library", query.email)
+< User.findOne(query, (error, user) => {
+< if (error) {
+< return callback(error, null);
+< }
+< if (user && user.hashedPassword) {
+< return callback(null, user);
+< } else {
+< return callback("Unknown error", null);
+< }
+< }
+< )
+< })
+< } else {
+< return callback(null, user);
+---
+> if (error) {
+> return callback(error)
+> }
+> if (!user || !user.hashedPassword) {
+> return callback(null, null)
+94,138d37
+< });
+< },
+<
+< //LDAP
+< createIfNotExistAndLogin(query, user, callback, uid, firstname, lastname, mail, isAdmin) {
+< 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")
+< //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) => {
+< 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
+140,146c39,40
+< 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 (error) {
+> return callback(error)
+147a42,73
+> 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.nModified !== 1) {
+> return callback(new ParallelLoginError())
+> }
+> if (!match) {
+> return callback(null, null)
+> }
+> AuthenticationManager.checkRounds(
+> user,
+> user.hashedPassword,
+> password,
+> function (err) {
+> if (err) {
+> return callback(err)
+> }
+> callback(null, user)
+> HaveIBeenPwned.checkPasswordForReuseInBackground(password)
+> }
+> )
+> }
+> )
+149,153c75
+< } else {
+< // No local passwd check user has to be in ldap and use ldap credentials
+< AuthenticationManager.ldapAuth(query, password, AuthenticationManager.createIfNotExistAndLogin, callback, user)
+< }
+< return null
+---
+> })
+157,158d78
+< // we use the emailadress from the ldap
+< // therefore we do not enforce checks here
+160,162c80,82
+< //if (!parsed) {
+< // return new InvalidEmailError({ message: 'email not valid' })
+< //}
+---
+> if (!parsed) {
+> return new InvalidEmailError({ message: 'email not valid' })
+> }
+320,437d239
+<
+<
+< 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)
+< }
+< })
+< }
+<
+<
+<
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/contactcontroller.diff b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/contactcontroller.diff
new file mode 100644
index 0000000..0aa4199
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/contactcontroller.diff
@@ -0,0 +1,133 @@
+16d15
+< const AuthenticationController = require('../Authentication/AuthenticationController')
+20c19
+< const logger = require('logger-sharelatex')
+---
+> const logger = require('@overleaf/logger')
+22d20
+< const { Client } = require('ldapts');
+26d23
+< // const user_id = AuthenticationController.getLoggedInUserId(req)
+48,78c45,55
+< // 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))
+---
+> // 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]
+> )
+80,99c57,60
+< }
+< )
+< })
+< },
+< 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) )
+< }
+< }
+---
+> // Don't count holding accounts to discourage users from repeating mistakes (mistyped or wrong emails, etc)
+> contacts = contacts.filter(c => !c.holdingAccount)
+>
+> contacts = contacts.map(ContactsController._formatContact)
+101,118c62,79
+< 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);
+< }
+---
+> 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.json({
+> contacts,
+> })
+> }
+> )
+> }
+> )
+120,129c81
+< } 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
+< }
+---
+> )
+130a83
+>
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.diff b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.diff
new file mode 100644
index 0000000..46f77bb
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.diff
@@ -0,0 +1,54 @@
+1,4c1
+< extends ../layout
+<
+< block vars
+< - metadata = { viewport: true }
+---
+> extends ../layout-marketing
+7c4
+< main.content.content-alt
+---
+> main.content.content-alt#main-content
+14c11
+< form(async-form="login", name="loginForm", action='/login', method="POST", ng-cloak)
+---
+> form(data-ol-async-form, name="loginForm", action='/login', method="POST")
+16c13
+< form-messages(for="loginForm")
+---
+> +formMessages()
+23,25c20
+< ng-model="email",
+< ng-model-options="{ updateOn: 'blur' }",
+< focus="true"
+---
+> autofocus="true"
+27,28d21
+< span.small.text-primary(ng-show="loginForm.email.$invalid && loginForm.email.$dirty")
+< | #{translate("must_be_email_address")}
+35d27
+< ng-model="password"
+37,38d28
+< span.small.text-primary(ng-show="loginForm.password.$invalid && loginForm.password.$dirty")
+< | #{translate("required")}
+40c30
+< button.btn-primary.btn.btn-block(
+---
+> button.btn-primary.btn(
+42c32
+< ng-disabled="loginForm.inflight"
+---
+> data-ol-disabled-inflight
+44,51c34,36
+< span(ng-show="!loginForm.inflight") #{translate("login_with_email")}
+< span(ng-show="loginForm.inflight") #{translate("logging_in")}…
+< .form-group.text-center(style="padding-top: 10px")
+< a.btn-block.login-btn(href="/oauth/redirect" style='padding-left: 0px')
+< | Log in via SUSTech CRA SSO / CAS
+< p
+< | homepage-notice-html
+<
+---
+> span(data-ol-inflight="idle") #{translate("login")}
+> span(hidden data-ol-inflight="pending") #{translate("logging_in")}…
+> a.pull-right(href='/user/password/reset') #{translate("forgot_your_password")}?
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.pug b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.pug
new file mode 100644
index 0000000..7af4bae
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/login.pug
@@ -0,0 +1,51 @@
+extends ../layout
+
+block vars
+ - metadata = { viewport: true }
+
+block content
+ main.content.content-alt
+ .container
+ .row
+ .col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4
+ .card
+ .page-header
+ h1 #{translate("log_in")}
+ form(async-form="login", name="loginForm", action='/login', method="POST", ng-cloak)
+ input(name='_csrf', type='hidden', value=csrfToken)
+ form-messages(for="loginForm")
+ .form-group
+ input.form-control(
+ type='email',
+ name='email',
+ required,
+ placeholder='email@example.com',
+ ng-model="email",
+ ng-model-options="{ updateOn: 'blur' }",
+ focus="true"
+ )
+ span.small.text-primary(ng-show="loginForm.email.$invalid && loginForm.email.$dirty")
+ | #{translate("must_be_email_address")}
+ .form-group
+ input.form-control(
+ type='password',
+ name='password',
+ required,
+ placeholder='********',
+ ng-model="password"
+ )
+ span.small.text-primary(ng-show="loginForm.password.$invalid && loginForm.password.$dirty")
+ | #{translate("required")}
+ .actions
+ button.btn-primary.btn.btn-block(
+ type='submit',
+ ng-disabled="loginForm.inflight"
+ )
+ span(ng-show="!loginForm.inflight") #{translate("login_with_email")}
+ span(ng-show="loginForm.inflight") #{translate("logging_in")}…
+ .form-group.text-center(style="padding-top: 10px")
+ a.btn-block.login-btn(href="/oauth/redirect" style='padding-left: 0px')
+ | Log in via SUSTech CRA SSO / CAS
+ p
+ | homepage-notice-html
+
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/navbar.pug b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/navbar.pug
new file mode 100644
index 0000000..f391630
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/navbar.pug
@@ -0,0 +1,84 @@
+nav.navbar.navbar-default.navbar-main
+ .container-fluid
+ .navbar-header
+ button.navbar-toggle(ng-init="navCollapsed = true", ng-click="navCollapsed = !navCollapsed", ng-class="{active: !navCollapsed}", aria-label="Toggle " + translate('navigation'))
+ i.fa.fa-bars(aria-hidden="true")
+ if settings.nav.custom_logo
+ a(href='/', aria-label=settings.appName, style='background-image:url("'+settings.nav.custom_logo+'")').navbar-brand
+ else if (nav.title)
+ a(href='/', aria-label=settings.appName, ng-non-bindable).navbar-title #{nav.title}
+ else
+ a(href='/', aria-label=settings.appName).navbar-brand
+
+ .navbar-collapse.collapse(collapse="navCollapsed")
+
+ ul.nav.navbar-nav.navbar-right
+ if (getSessionUser() && getSessionUser().isAdmin)
+ li
+ a(href="/admin") Admin
+
+
+ // loop over header_extras
+ each item in nav.header_extras
+ -
+ if ((item.only_when_logged_in && getSessionUser())
+ || (item.only_when_logged_out && (!getSessionUser()))
+ || (!item.only_when_logged_out && !item.only_when_logged_in && !item.only_content_pages)
+ || (item.only_content_pages && (typeof(suppressNavContentLinks) == "undefined" || !suppressNavContentLinks))
+ ){
+ var showNavItem = true
+ } else {
+ var showNavItem = false
+ }
+
+ if showNavItem
+ if item.dropdown
+ li.dropdown(class=item.class, dropdown)
+ a.dropdown-toggle(href, dropdown-toggle)
+ | !{translate(item.text)}
+ b.caret
+ ul.dropdown-menu
+ each child in item.dropdown
+ if child.divider
+ li.divider
+ else
+ li
+ if child.url
+ a(href=child.url, class=child.class) !{translate(child.text)}
+ else
+ | !{translate(child.text)}
+ else
+ li(class=item.class)
+ if item.url
+ a(href=item.url, class=item.class) !{translate(item.text)}
+ else
+ | !{translate(item.text)}
+
+ // logged out
+ if !getSessionUser()
+ // login link
+ li
+ a(href="/login") #{translate('log_in')}
+
+ // projects link and account menu
+ if getSessionUser()
+ li
+ a(href="/project") #{translate('Projects')}
+ li.dropdown(dropdown)
+ a.dropdown-toggle(href, dropdown-toggle)
+ | #{translate('Account')}
+ b.caret
+ ul.dropdown-menu
+ //li
+ // div.subdued(ng-non-bindable) #{getUserEmail()}
+ //li.divider.hidden-xs.hidden-sm
+ li
+ a(href="/user/settings") #{translate('Account Settings')}
+ if nav.showSubscriptionLink
+ li
+ a(href="/user/subscription") #{translate('subscription')}
+ li.divider.hidden-xs.hidden-sm
+ li
+ form(method="POST" action="/logout")
+ input(name='_csrf', type='hidden', value=csrfToken)
+ button.btn-link.text-left.dropdown-menu-button #{translate('log_out')}
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/router-append.js b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/router-append.js
new file mode 100644
index 0000000..68e1c49
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/router-append.js
@@ -0,0 +1,6 @@
+ webRouter.get('/oauth/redirect', AuthenticationController.oauth2Redirect)
+ webRouter.get('/oauth/callback', AuthenticationController.oauth2Callback)
+ AuthenticationController.addEndpointToLoginWhitelist('/oauth/redirect')
+ AuthenticationController.addEndpointToLoginWhitelist('/oauth/callback')
+ webRouter.get('*', ErrorController.notFound)
+}
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.diff b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.diff
new file mode 100644
index 0000000..0f697b9
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.diff
@@ -0,0 +1,200 @@
+2a3,9
+> block append meta
+> meta(name="ol-reconfirmationRemoveEmail", content=reconfirmationRemoveEmail)
+> meta(name="ol-reconfirmedViaSAML", content=reconfirmedViaSAML)
+> meta(name="ol-passwordStrengthOptions", data-type="json", content=settings.passwordStrengthOptions || {})
+> meta(name="ol-oauthProviders", data-type="json", content=oauthProviders)
+> meta(name="ol-thirdPartyIds", data-type="json", content=thirdPartyIds)
+>
+4c11,15
+< .content.content-alt
+---
+> main.content.content-alt#main-content(
+> event-tracking-mb="true"
+> event-tracking="settings-view"
+> event-tracking-trigger="load"
+> )
+25,31c36,57
+< // show the email, non-editable
+< .form-group
+< label.control-label #{translate("email")}
+< div.form-control(
+< readonly="true",
+< ng-non-bindable
+< ) #{user.email}
+---
+> if !externalAuthenticationSystemUsed()
+> .form-group
+> label(for='email') #{translate("email")}
+> input.form-control(
+> id="email"
+> type='email',
+> name='email',
+> placeholder="email@example.com"
+> required,
+> ng-model="email",
+> ng-model-options="{ updateOn: 'blur' }"
+> )
+> span.small.text-danger(ng-show="settingsForm.email.$invalid && settingsForm.email.$dirty")
+> | #{translate("must_be_email_address")}
+> else
+> // show the email, non-editable
+> .form-group
+> label.control-label #{translate("email")}
+> div.form-control(
+> readonly="true",
+> ng-non-bindable
+> ) #{user.email}
+76,81c102,164
+< h3
+< | Set Password for Email login
+< p
+< | Note: you can not change the LDAP password from here. You can set/reset a password for
+< | your email login:
+< | #[a(href="/user/password/reset", target='_blank') Reset.]
+---
+> h3 #{translate("change_password")}
+> if externalAuthenticationSystemUsed() && !settings.overleaf
+> p
+> | Password settings are managed externally
+> else if !hasPassword
+> p
+> | #[a(href="/user/password/reset", target='_blank') #{translate("no_existing_password")}]
+> else
+> - var submitAction
+> - submitAction = '/user/password/update'
+> form(
+> async-form="changepassword"
+> name="changePasswordForm"
+> action=submitAction
+> method="POST"
+> novalidate
+> )
+> input(type="hidden", name="_csrf", value=csrfToken)
+> .form-group
+> label(for='currentPassword') #{translate("current_password")}
+> input.form-control(
+> id="currentPassword"
+> type='password',
+> name='currentPassword',
+> placeholder='*********',
+> ng-model="currentPassword",
+> required
+> )
+> span.small.text-danger(ng-show="changePasswordForm.currentPassword.$invalid && changePasswordForm.currentPassword.$dirty" aria-live="polite")
+> | #{translate("required")}
+> .form-group
+> label(for='passwordField') #{translate("new_password")}
+> input.form-control(
+> id='passwordField',
+> type='password',
+> name='newPassword1',
+> placeholder='*********',
+> ng-model="newPassword1",
+> required,
+> complex-password
+> )
+> span.small.text-danger(ng-show="changePasswordForm.newPassword1.$error.complexPassword && changePasswordForm.newPassword1.$dirty", ng-bind-html="complexPasswordErrorMessage" aria-live="polite")
+> .form-group
+> label(for='newPassword2') #{translate("confirm_new_password")}
+> input.form-control(
+> id="newPassword2"
+> type='password',
+> name='newPassword2',
+> placeholder='*********',
+> ng-model="newPassword2",
+> equals="passwordField"
+> )
+> span.small.text-danger(ng-show="changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$dirty" aria-live="polite")
+> | #{translate("doesnt_match")}
+> span.small.text-danger(ng-show="!changePasswordForm.newPassword2.$error.areEqual && changePasswordForm.newPassword2.$invalid && changePasswordForm.newPassword2.$dirty" aria-live="polite")
+> | #{translate("invalid_password")}
+> .form-group
+> form-messages(aria-live="polite" for="changePasswordForm")
+> .actions
+> button.btn.btn-primary(
+> type='submit',
+> ng-disabled="changePasswordForm.$invalid"
+> ) #{translate("change")}
+85a169,181
+> if hasFeature('saas')
+> h3
+> | #{translate("sharelatex_beta_program")}
+>
+> if (user.betaProgram)
+> p.small
+> | #{translate("beta_program_already_participating")}
+>
+> div
+> a(id="beta-program-participate-link" href="/beta/participate") #{translate("manage_beta_program_membership")}
+>
+> hr
+>
+87,92c183,186
+< | Contact
+< div
+< | If you need any help, please contact your sysadmins.
+<
+< p #{translate("need_to_leave")}
+< a(href, ng-click="deleteAccount()") #{translate("delete_your_account")}
+---
+> | #{translate("sessions")}
+>
+> div
+> a(id="sessions-link", href="/user/sessions") #{translate("manage_sessions")}
+93a188,218
+> if hasFeature('oauth')
+> hr
+> include settings/user-oauth
+>
+> if hasFeature('saas') && (!externalAuthenticationSystemUsed() || (settings.createV1AccountOnLogin && settings.overleaf))
+> hr
+> p.small
+> | #{translate("newsletter_info_and_unsubscribe")}
+> a(
+> href,
+> ng-click="unsubscribe()",
+> ng-show="subscribed && !unsubscribing"
+> ) #{translate("unsubscribe")}
+> span(
+> ng-show="unsubscribing"
+> )
+> i.fa.fa-spin.fa-refresh(aria-hidden="true")
+> | #{translate("unsubscribing")}
+> span.text-success(
+> ng-show="!subscribed"
+> )
+> i.fa.fa-check(aria-hidden="true")
+> | #{translate("unsubscribed")}
+>
+> if !settings.overleaf && user.overleaf
+> p
+> | Please note: If you have linked your account with Overleaf
+> | v2, then deleting your ShareLaTeX account will also delete
+> | account and all of it's associated projects and data.
+> p #{translate("need_to_leave")}
+> a(href, ng-click="deleteAccount()") #{translate("delete_your_account")}
+100c225
+< p !{translate("delete_account_warning_message_3")}
+---
+> p !{translate("delete_account_warning_message_3", {}, ['strong'])}
+136,143d260
+< ng-model="state.confirmV1Purge"
+< ng-change="checkValidation()"
+< ).pull-left
+< label(style="display: inline") &nbsp;I have left, purged or imported my projects on Overleaf v1 (if any) &nbsp;
+<
+< div.confirmation-checkbox-wrapper
+< input(
+< type="checkbox"
+147c264
+< label(style="display: inline") &nbsp;I understand this will delete all projects in my Overleaf v2 account (and ShareLaTeX account, if any) with email address #[em {{ userDefaultEmail }}]
+---
+> label(style="display: inline") &nbsp;I understand this will delete all projects in my Overleaf account with email address #[em {{ userDefaultEmail }}]
+175,178c292
+< span(ng-show="state.inflight") #{translate("deleting")}...
+<
+< script(type='text/javascript').
+< window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}
+---
+> span(ng-show="state.inflight") #{translate("deleting")}…
diff --git a/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.pug b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.pug
new file mode 100644
index 0000000..8cdd18c
--- /dev/null
+++ b/overleaf-mods/overleaf-ldap-oauth2/ldap-overleaf-sl/sharelatex/settings.pug
@@ -0,0 +1,178 @@
+extends ../layout
+
+block content
+ .content.content-alt
+ .container
+ .row
+ .col-md-12.col-lg-10.col-lg-offset-1
+ if ssoError
+ .alert.alert-danger
+ | #{translate('sso_link_error')}: #{translate(ssoError)}
+ .card
+ .page-header
+ h1 #{translate("account_settings")}
+ .account-settings(ng-controller="AccountSettingsController", ng-cloak)
+
+ if hasFeature('affiliations')
+ include settings/user-affiliations
+
+ .row
+ .col-md-5
+ h3 #{translate("update_account_info")}
+ form(async-form="settings", name="settingsForm", method="POST", action="/user/settings", novalidate)
+ input(type="hidden", name="_csrf", value=csrfToken)
+ if !hasFeature('affiliations')
+ // show the email, non-editable
+ .form-group
+ label.control-label #{translate("email")}
+ div.form-control(
+ readonly="true",
+ ng-non-bindable
+ ) #{user.email}
+
+ if shouldAllowEditingDetails
+ .form-group
+ label(for='firstName').control-label #{translate("first_name")}
+ input.form-control(
+ id="firstName"
+ type='text',
+ name='first_name',
+ value=user.first_name
+ ng-non-bindable
+ )
+ .form-group
+ label(for='lastName').control-label #{translate("last_name")}
+ input.form-control(
+ id="lastName"
+ type='text',
+ name='last_name',
+ value=user.last_name
+ ng-non-bindable
+ )
+ .form-group
+ form-messages(aria-live="polite" for="settingsForm")
+ .alert.alert-success(ng-show="settingsForm.response.success")
+ | #{translate("thanks_settings_updated")}
+ .actions
+ button.btn.btn-primary(
+ type='submit',
+ ng-disabled="settingsForm.$invalid"
+ ) #{translate("update")}
+ else
+ .form-group
+ label.control-label #{translate("first_name")}
+ div.form-control(
+ readonly="true",
+ ng-non-bindable
+ ) #{user.first_name}
+ .form-group
+ label.control-label #{translate("last_name")}
+ div.form-control(
+ readonly="true",
+ ng-non-bindable
+ ) #{user.last_name}
+
+ .col-md-5.col-md-offset-1
+ h3
+ | Set Password for Email login
+ p
+ | Note: you can not change the LDAP password from here. You can set/reset a password for
+ | your email login:
+ | #[a(href="/user/password/reset", target='_blank') Reset.]
+
+ | !{moduleIncludes("userSettings", locals)}
+ hr
+
+ h3
+ | Contact
+ div
+ | If you need any help, please contact your sysadmins.
+
+ p #{translate("need_to_leave")}
+ a(href, ng-click="deleteAccount()") #{translate("delete_your_account")}
+
+
+
+ script(type='text/ng-template', id='deleteAccountModalTemplate')
+ .modal-header
+ h3 #{translate("delete_account")}
+ div.modal-body#delete-account-modal
+ p !{translate("delete_account_warning_message_3")}
+ if settings.createV1AccountOnLogin && settings.overleaf
+ p
+ strong
+ | Your Overleaf v2 projects will be deleted if you delete your account.
+ | If you want to remove any remaining Overleaf v1 projects in your account,
+ | please first make sure they are imported to Overleaf v2.
+
+ if settings.overleaf && !hasPassword
+ p
+ b
+ | #[a(href="/user/password/reset", target='_blank') #{translate("delete_acct_no_existing_pw")}].
+ else
+ form(novalidate, name="deleteAccountForm")
+ label #{translate('email')}
+ input.form-control(
+ type="text",
+ autocomplete="off",
+ placeholder="",
+ ng-model="state.deleteText",
+ focus-on="open",
+ ng-keyup="checkValidation()"
+ )
+
+ label #{translate('password')}
+ input.form-control(
+ type="password",
+ autocomplete="off",
+ placeholder="",
+ ng-model="state.password",
+ ng-keyup="checkValidation()"
+ )
+
+ div.confirmation-checkbox-wrapper
+ input(
+ type="checkbox"
+ ng-model="state.confirmV1Purge"
+ ng-change="checkValidation()"
+ ).pull-left
+ label(style="display: inline") &nbsp;I have left, purged or imported my projects on Overleaf v1 (if any) &nbsp;
+
+ div.confirmation-checkbox-wrapper
+ input(
+ type="checkbox"
+ ng-model="state.confirmSharelatexDelete"
+ ng-change="checkValidation()"
+ ).pull-left
+ label(style="display: inline") &nbsp;I understand this will delete all projects in my Overleaf v2 account (and ShareLaTeX account, if any) with email address #[em {{ userDefaultEmail }}]
+
+ div(ng-if="state.error")
+ div.alert.alert-danger(ng-switch="state.error.code")
+ span(ng-switch-when="InvalidCredentialsError")
+ | #{translate('email_or_password_wrong_try_again')}
+ span(ng-switch-when="SubscriptionAdminDeletionError")
+ | #{translate('subscription_admins_cannot_be_deleted')}
+ span(ng-switch-when="UserDeletionError")
+ | #{translate('user_deletion_error')}
+ span(ng-switch-default)
+ | #{translate('generic_something_went_wrong')}
+ if settings.createV1AccountOnLogin && settings.overleaf
+ div(ng-if="state.error && state.error.code == 'InvalidCredentialsError'")
+ div.alert.alert-info
+ | If you can't remember your password, or if you are using Single-Sign-On with another provider
+ | to sign in (such as Twitter or Google), please
+ | #[a(href="/user/password/reset", target='_blank') reset your password],
+ | and try again.
+ .modal-footer
+ button.btn.btn-default(
+ ng-click="cancel()"
+ ) #{translate("cancel")}
+ button.btn.btn-danger(
+ ng-disabled="!state.isValid || state.inflight"
+ ng-click="delete()"
+ )
+ span(ng-hide="state.inflight") #{translate("delete")}
+ span(ng-show="state.inflight") #{translate("deleting")}...
+
+ script(type='text/javascript').
+ window.passwordStrengthOptions = !{StringHelper.stringifyJsonForScript(settings.passwordStrengthOptions || {})}