From ca692f1c3677b45ffaf6101c36039ca73239a862 Mon Sep 17 00:00:00 2001 From: yzx9 Date: Mon, 4 Dec 2023 21:36:15 +0800 Subject: [PATCH] Diff and patch modification (close #34) --- .gitignore | 4 + README.md | 10 +- ldap-overleaf-sl/sharelatex/.gitkeep | 0 .../sharelatex/AuthenticationController.js | 741 --------- .../sharelatex/AuthenticationManager.js | 759 --------- .../sharelatex/ContactController.js | 130 -- ldap-overleaf-sl/sharelatex/admin-index.pug | 57 - .../sharelatex/admin-sysadmin.pug | 79 - ldap-overleaf-sl/sharelatex/login.pug | 54 - ldap-overleaf-sl/sharelatex/navbar.pug | 84 - ldap-overleaf-sl/sharelatex/router.js | 1380 ----------------- ldap-overleaf-sl/sharelatex/settings.pug | 178 --- .../AuthenticationController.js.diff | 106 ++ .../AuthenticationManager.js.diff | 305 ++++ .../sharelatex_diff/ContactController.js.diff | 76 + .../sharelatex_diff/admin-index.pug.diff | 146 ++ .../sharelatex_diff/admin-sysadmin.pug.diff | 166 ++ .../sharelatex_diff/login.pug.diff | 20 + .../sharelatex_diff/navbar.pug.diff | 217 +++ .../sharelatex_diff/router.js.diff | 10 + .../sharelatex_diff/settings.pug.diff | 211 +++ ldap-overleaf-sl/sharelatex_ori/.gitkeep | 0 scripts/apply_diffs.sh | 24 + scripts/extract_files.sh | 70 + scripts/make_diffs.sh | 16 + 25 files changed, 1380 insertions(+), 3463 deletions(-) create mode 100644 ldap-overleaf-sl/sharelatex/.gitkeep delete mode 100644 ldap-overleaf-sl/sharelatex/AuthenticationController.js delete mode 100644 ldap-overleaf-sl/sharelatex/AuthenticationManager.js delete mode 100644 ldap-overleaf-sl/sharelatex/ContactController.js delete mode 100644 ldap-overleaf-sl/sharelatex/admin-index.pug delete mode 100644 ldap-overleaf-sl/sharelatex/admin-sysadmin.pug delete mode 100644 ldap-overleaf-sl/sharelatex/login.pug delete mode 100644 ldap-overleaf-sl/sharelatex/navbar.pug delete mode 100644 ldap-overleaf-sl/sharelatex/router.js delete mode 100644 ldap-overleaf-sl/sharelatex/settings.pug create mode 100644 ldap-overleaf-sl/sharelatex_diff/AuthenticationController.js.diff create mode 100644 ldap-overleaf-sl/sharelatex_diff/AuthenticationManager.js.diff create mode 100644 ldap-overleaf-sl/sharelatex_diff/ContactController.js.diff create mode 100644 ldap-overleaf-sl/sharelatex_diff/admin-index.pug.diff create mode 100644 ldap-overleaf-sl/sharelatex_diff/admin-sysadmin.pug.diff create mode 100644 ldap-overleaf-sl/sharelatex_diff/login.pug.diff create mode 100644 ldap-overleaf-sl/sharelatex_diff/navbar.pug.diff create mode 100644 ldap-overleaf-sl/sharelatex_diff/router.js.diff create mode 100644 ldap-overleaf-sl/sharelatex_diff/settings.pug.diff create mode 100644 ldap-overleaf-sl/sharelatex_ori/.gitkeep create mode 100644 scripts/apply_diffs.sh create mode 100644 scripts/extract_files.sh create mode 100644 scripts/make_diffs.sh diff --git a/.gitignore b/.gitignore index 1f2d8ac..f0233f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# Temporary files +sharelatex/ +sharelatex_ori/ + # Compiled Object files *.slo *.lo diff --git a/README.md b/README.md index 467a1a3..6a0deb3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This repo contains an improved, free ldap authentication and authorisation for sharelatex/[overleaf](https://github.com/overleaf/overleaf) community -edition. Currently this repo uses sharelatex:latest. +edition. Currently this repo uses `sharelatex/sharelatex:4.1.1`. The inital idea for this implementation was taken from [worksasintended](https://github.com/worksasintended). @@ -275,6 +275,14 @@ docker-compose -f docker-compose.certbot.yml up -d 1. Set the env variable `LOG_LEVEL` to `debug` (default is info - you can do this in the docker-compose file) 2. Check the logs in ShareLaTeX, particularly at `/var/log/sharelatex/web.log`. You can do this by using the command: `docker exec ldap-overleaf-sl cat /var/log/sharelatex/web.log`. +## Development + +1. Cloning this repo +2. Extract files from image using `bash scripts/extract_files SHARELATEX_VERSION` +3. Generate modified files using `bash scripts/apply_patches.js` +4. Development +5. Create diff files using `bash script/make_diffs.sh` and commit + ## Upgrading *Be aware:* if you upgrade from a previous installation check your docker image version diff --git a/ldap-overleaf-sl/sharelatex/.gitkeep b/ldap-overleaf-sl/sharelatex/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ldap-overleaf-sl/sharelatex/AuthenticationController.js b/ldap-overleaf-sl/sharelatex/AuthenticationController.js deleted file mode 100644 index 96c878a..0000000 --- a/ldap-overleaf-sl/sharelatex/AuthenticationController.js +++ /dev/null @@ -1,741 +0,0 @@ -/** - * >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - * Modified from 1e4dcc8 - * <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - */ - -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 { - 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, - alphaProgram: user.alphaProgram || undefined, // only store if set - betaProgram: user.betaProgram || undefined, // only store if set - } - 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 AsyncFormHelper.redirect(req, res, '/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 - - 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() - 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, - }) - } - const auditLog = { - ipAddress: req.ip, - info: { method: 'Password login' }, - } - AuthenticationManager.authenticate( - { email }, - password, - auditLog, - 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') - } - }, - -// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - oauth2Redirect(req, res, next) { - // random state - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - const state = new Array(6).fill(0).map(() => characters.charAt(Math.floor(Math.random() * characters.length))).join("") - req.session.oauth2State = state - - const redirectURI = encodeURIComponent(`${process.env.SHARELATEX_SITE_URL}/oauth/callback`) - const authURL = ( - process.env.OAUTH2_AUTHORIZATION_URL - + `?response_type=code` - + `&client_id=${process.env.OAUTH2_CLIENT_ID}` - + `&redirect_uri=${redirectURI}` - + `&scope=${process.env.OAUTH2_SCOPE ?? ""} ` - + `&state=${state}` - ) - res.redirect(authURL) - }, - - async oauth2Callback(req, res, next) { - console.log(`OAuth, receive code ${req.query.code} and state ${req.query.state}`) - const saveState = req.session.oauth2State - delete req.session.oauth2State - if (saveState !== req.query.state) { - return AuthenticationController.finishLogin(false, req, res, next) - } - - try { - const contentType = process.env.OAUTH2_TOKEN_CONTENT_TYPE || 'application/x-www-form-urlencoded' - const bodyParams = { - grant_type: "authorization_code", - client_id: process.env.OAUTH2_CLIENT_ID, - client_secret: process.env.OAUTH2_CLIENT_SECRET, - code: req.query.code, - redirect_uri: `${process.env.SHARELATEX_SITE_URL}/oauth/callback`, - } - const body = contentType === 'application/json' - ? JSON.stringify(bodyParams) - : new URLSearchParams(bodyParams).toString() - - const tokenResponse = await fetch(process.env.OAUTH2_TOKEN_URL, { - method: 'POST', - headers: { - "Accept": "application/json", - "Content-Type": contentType, - }, - body - }) - - const tokenData = await tokenResponse.json() - console.log("OAuth2 respond", JSON.stringify(tokenData)) - - const profileResponse = await fetch(process.env.OAUTH2_PROFILE_URL, { - method: 'GET', - headers: { - "Accept": "application/json", - "Authorization": `Bearer ${tokenData.access_token}`, - } - }); - const profile = await profileResponse.json() - console.log("OAuth2 user profile", JSON.stringify(profile)) - - const email = profile[process.env.OAUTH2_USER_ATTR_EMAIL ?? "email"] - const uid = profile[process.env.OAUTH2_USER_ATTR_UID ?? "uid"] - const firstname = profile?.[process.env.OAUTH2_USER_ATTR_FIRSTNAME] ?? email - const lastname = process.env.OAUTH2_USER_ATTR_LASTNAME - ? profile?.[process.env.OAUTH2_USER_ATTR_LASTNAME] ?? "" - : "" - const isAdmin = process.env.OAUTH2_USER_ATTR_IS_ADMIN - ? !!profile?.[process.env.OAUTH2_USER_ATTR_IS_ADMIN] ?? false - : false - - const query = { email } - const callback = (error, user) => { - if (error) { - res.json({message: error}); - } else { - console.log("OAuth user", JSON.stringify(user)); - AuthenticationController.finishLogin(user, req, res, next); - } - } - AuthenticationManager.createIfNotFoundAndLogin( - query, - callback, - uid, - firstname, - lastname, - email, - isAdmin - ) - } catch(e) { - res.redirect("/login") - console.error("Fails to access by OAuth2: " + String(e)) - } - }, -// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - - 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 - }, - - 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() - }, - - checkCredentials, - - 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, userId: 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 \ No newline at end of file diff --git a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js b/ldap-overleaf-sl/sharelatex/AuthenticationManager.js deleted file mode 100644 index bd2741e..0000000 --- a/ldap-overleaf-sl/sharelatex/AuthenticationManager.js +++ /dev/null @@ -1,759 +0,0 @@ -/** - * >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - * Modified from 841df71 - * <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - */ - -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, - 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 fs = require("fs") -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 - -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) - } else { - callback(null, false) - } -} - -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 = { - _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) => { - if (error) { - return callback(error) - } - if (!user || !user.hashedPassword) { - return callback(null, null, null) - } - bcrypt.compare(password, user.hashedPassword, function (error, match) { - if (error) { - return callback(error) - } - if (match) { - _metricsForSuccessfulPasswordMatch(password) - } - 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._checkUserPassword2( -// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - 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) { - callback(null, user, true) - }, - - createIfNotFoundAndLogin( - query, - callback, - uid, - firstname, - lastname, - mail, - isAdmin - ) { - User.findOne(query, (error, user) => { - if (error) { - console.log(error) - } - - AuthenticationManager.createIfNotExistAndLogin( - query, - user, - callback, - uid, - firstname, - lastname, - mail, - isAdmin - ) - }) - }, - - createIfNotExistAndLogin( - query, - user, - callback, - uid, - firstname, - lastname, - mail, - isAdmin - ) { - if (!user) { - //create random pass for local userdb, does not get checked for ldap users during login - const pass = require("crypto").randomBytes(32).toString("hex") - console.log('Creating User', { mail, uid, firstname, lastname, isAdmin, pass }) - - const userRegHand = require("../User/UserRegistrationHandler.js") - userRegHand.registerNewUser( - { - email: mail, - first_name: firstname, - last_name: lastname, - password: pass, - }, - function (error, user, setNewPasswordUrl) { - 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 { - console.log('User exists', { mail }) - 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") - _metricsForSuccessfulPasswordMatch(password) - //callback(null, user, match) - AuthenticationManager.login(user, "randomPass", 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 - }, - - async ldapAuth( - query, - password, - onSuccessCreateUserIfNotExistent, - callback, - user - ) { - 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 - 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 - ) - } - }) - }, -// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - - 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. - validatePassword(password, email) { - if (password == null) { - return new InvalidPasswordError({ - message: 'password not set', - info: { code: 'not_set' }, - }) - } - - Metrics.inc('try-validate-password') - - 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 || 8 - 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' }, - }) - } - const passwordLengthError = _validatePasswordNotTooLong(password) - if (passwordLengthError) { - return passwordLengthError - } - 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.includes(email) || - password.includes(startOfEmail) || - email.includes(password) - ) { - return new InvalidPasswordError({ - message: 'password contains part of email address', - info: { code: 'contains_email' }, - }) - } - 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) - }, - - 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._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) - } - 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) - } - // 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) { - 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) { - 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 - }, - - /** - * 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') - } - } - } - }, - - getMessageForInvalidPasswordError(error, req) { - const errorCode = error?.info?.code - const message = { - type: 'error', - } - 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 = { - authenticate: util.promisify(AuthenticationManager.authenticate), - hashPassword: util.promisify(AuthenticationManager.hashPassword), - setUserPassword: util.promisify(AuthenticationManager.setUserPassword), -} - -module.exports = AuthenticationManager diff --git a/ldap-overleaf-sl/sharelatex/ContactController.js b/ldap-overleaf-sl/sharelatex/ContactController.js deleted file mode 100644 index 6d7be8d..0000000 --- a/ldap-overleaf-sl/sharelatex/ContactController.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - * Modified from 906765c - * <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - */ - -const SessionManager = require('../Authentication/SessionManager') -const ContactManager = require('./ContactManager') -const UserGetter = require('../User/UserGetter') -const Modules = require('../../infrastructure/Modules') -const { expressify } = require('../../util/promises') - -// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -const { Client } = require('ldapts') -// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - -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), -} diff --git a/ldap-overleaf-sl/sharelatex/admin-index.pug b/ldap-overleaf-sl/sharelatex/admin-index.pug deleted file mode 100644 index 88e264b..0000000 --- a/ldap-overleaf-sl/sharelatex/admin-index.pug +++ /dev/null @@ -1,57 +0,0 @@ -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/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug b/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug deleted file mode 100644 index c7131a3..0000000 --- a/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug +++ /dev/null @@ -1,79 +0,0 @@ -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/ldap-overleaf-sl/sharelatex/login.pug b/ldap-overleaf-sl/sharelatex/login.pug deleted file mode 100644 index aedad20..0000000 --- a/ldap-overleaf-sl/sharelatex/login.pug +++ /dev/null @@ -1,54 +0,0 @@ -//- >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -//- Modified from ee00ff3 -//- <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - -extends ../layout-marketing - -block content - main.content.content-alt#main-content - .container - .row - .col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4 - .card - .page-header - h1 #{translate("log_in")} - form(data-ol-async-form, name="loginForm", action='/login', method="POST") - input(name='_csrf', type='hidden', value=csrfToken) - +formMessages() - .form-group - //- >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - //- input.form-control( - //- type='email', - //- name='email', - //- required, - //- placeholder='email@example.com', - //- autofocus="true" - //- ) - input.form-control( - name='email', - required, - placeholder='email@example.com', - autofocus="true" - ) - //- <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - .form-group - input.form-control( - type='password', - name='password', - required, - placeholder='********', - ) - .actions - button.btn-primary.btn( - type='submit', - data-ol-disabled-inflight - ) - 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")}? - //- >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - if process.env.OAUTH2_ENABLED === 'true' - .form-group.text-center(style="padding-top: 10px") - a.btn-block.login-btn(href="/oauth/redirect" style='padding-left: 0px') - | Log in via #{process.env.OAUTH2_PROVIDER || 'OAuth'} - //- <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< diff --git a/ldap-overleaf-sl/sharelatex/navbar.pug b/ldap-overleaf-sl/sharelatex/navbar.pug deleted file mode 100644 index f391630..0000000 --- a/ldap-overleaf-sl/sharelatex/navbar.pug +++ /dev/null @@ -1,84 +0,0 @@ -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/ldap-overleaf-sl/sharelatex/router.js b/ldap-overleaf-sl/sharelatex/router.js deleted file mode 100644 index 83033f4..0000000 --- a/ldap-overleaf-sl/sharelatex/router.js +++ /dev/null @@ -1,1380 +0,0 @@ -/** - * >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - * Modified from bf92436 - * <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - */ - -const AdminController = require('./Features/ServerAdmin/AdminController') -const ErrorController = require('./Features/Errors/ErrorController') -const ProjectController = require('./Features/Project/ProjectController') -const ProjectApiController = require('./Features/Project/ProjectApiController') -const ProjectListController = require('./Features/Project/ProjectListController') -const SpellingController = require('./Features/Spelling/SpellingController') -const EditorRouter = require('./Features/Editor/EditorRouter') -const Settings = require('@overleaf/settings') -const TpdsController = require('./Features/ThirdPartyDataStore/TpdsController') -const SubscriptionRouter = require('./Features/Subscription/SubscriptionRouter') -const UploadsRouter = require('./Features/Uploads/UploadsRouter') -const metrics = require('@overleaf/metrics') -const ReferalController = require('./Features/Referal/ReferalController') -const AuthenticationController = require('./Features/Authentication/AuthenticationController') -const PermissionsController = require('./Features/Authorization/PermissionsController') -const SessionManager = require('./Features/Authentication/SessionManager') -const TagsController = require('./Features/Tags/TagsController') -const NotificationsController = require('./Features/Notifications/NotificationsController') -const CollaboratorsRouter = require('./Features/Collaborators/CollaboratorsRouter') -const UserInfoController = require('./Features/User/UserInfoController') -const UserController = require('./Features/User/UserController') -const UserEmailsController = require('./Features/User/UserEmailsController') -const UserPagesController = require('./Features/User/UserPagesController') -const DocumentController = require('./Features/Documents/DocumentController') -const CompileManager = require('./Features/Compile/CompileManager') -const CompileController = require('./Features/Compile/CompileController') -const ClsiCookieManager = require('./Features/Compile/ClsiCookieManager')( - Settings.apis.clsi != null ? Settings.apis.clsi.backendGroupName : undefined -) -const HealthCheckController = require('./Features/HealthCheck/HealthCheckController') -const ProjectDownloadsController = require('./Features/Downloads/ProjectDownloadsController') -const FileStoreController = require('./Features/FileStore/FileStoreController') -const HistoryController = require('./Features/History/HistoryController') -const ExportsController = require('./Features/Exports/ExportsController') -const PasswordResetRouter = require('./Features/PasswordReset/PasswordResetRouter') -const StaticPagesRouter = require('./Features/StaticPages/StaticPagesRouter') -const ChatController = require('./Features/Chat/ChatController') -const Modules = require('./infrastructure/Modules') -const { - RateLimiter, - openProjectRateLimiter, -} = require('./infrastructure/RateLimiter') -const RateLimiterMiddleware = require('./Features/Security/RateLimiterMiddleware') -const InactiveProjectController = require('./Features/InactiveData/InactiveProjectController') -const ContactRouter = require('./Features/Contacts/ContactRouter') -const ReferencesController = require('./Features/References/ReferencesController') -const AuthorizationMiddleware = require('./Features/Authorization/AuthorizationMiddleware') -const BetaProgramController = require('./Features/BetaProgram/BetaProgramController') -const AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter') -const MetaController = require('./Features/Metadata/MetaController') -const TokenAccessController = require('./Features/TokenAccess/TokenAccessController') -const Features = require('./infrastructure/Features') -const LinkedFilesRouter = require('./Features/LinkedFiles/LinkedFilesRouter') -const TemplatesRouter = require('./Features/Templates/TemplatesRouter') -const InstitutionsController = require('./Features/Institutions/InstitutionsController') -const UserMembershipRouter = require('./Features/UserMembership/UserMembershipRouter') -const SystemMessageController = require('./Features/SystemMessages/SystemMessageController') -const AnalyticsRegistrationSourceMiddleware = require('./Features/Analytics/AnalyticsRegistrationSourceMiddleware') -const AnalyticsUTMTrackingMiddleware = require('./Features/Analytics/AnalyticsUTMTrackingMiddleware') -const CaptchaMiddleware = require('./Features/Captcha/CaptchaMiddleware') -const { Joi, validate } = require('./infrastructure/Validation') -const { - renderUnsupportedBrowserPage, - unsupportedBrowserMiddleware, -} = require('./infrastructure/UnsupportedBrowserMiddleware') - -const logger = require('@overleaf/logger') -const _ = require('underscore') -const { plainTextResponse } = require('./infrastructure/Response') -const PublicAccessLevels = require('./Features/Authorization/PublicAccessLevels') - -const rateLimiters = { - addEmail: new RateLimiter('add-email', { - points: 10, - duration: 60, - }), - addProjectToTag: new RateLimiter('add-project-to-tag', { - points: 30, - duration: 60, - }), - addProjectsToTag: new RateLimiter('add-projects-to-tag', { - points: 30, - duration: 60, - }), - canSkipCaptcha: new RateLimiter('can-skip-captcha', { - points: 20, - duration: 60, - }), - changePassword: new RateLimiter('change-password', { - points: 10, - duration: 60, - }), - compileProjectHttp: new RateLimiter('compile-project-http', { - points: 800, - duration: 60 * 60, - }), - confirmEmail: new RateLimiter('confirm-email', { - points: 10, - duration: 60, - }), - confirmUniversityDomain: new RateLimiter('confirm-university-domain', { - points: 1, - duration: 60, - }), - createProject: new RateLimiter('create-project', { - points: 20, - duration: 60, - }), - createTag: new RateLimiter('create-tag', { - points: 30, - duration: 60, - }), - deleteEmail: new RateLimiter('delete-email', { - points: 10, - duration: 60, - }), - deleteTag: new RateLimiter('delete-tag', { - points: 30, - duration: 60, - }), - deleteUser: new RateLimiter('delete-user', { - points: 10, - duration: 60, - }), - downloadProjectRevision: new RateLimiter('download-project-revision', { - points: 30, - duration: 60 * 60, - }), - endorseEmail: new RateLimiter('endorse-email', { - points: 30, - duration: 60, - }), - getProjects: new RateLimiter('get-projects', { - points: 30, - duration: 60, - }), - grantTokenAccessReadOnly: new RateLimiter('grant-token-access-read-only', { - points: 10, - duration: 60, - }), - grantTokenAccessReadWrite: new RateLimiter('grant-token-access-read-write', { - points: 10, - duration: 60, - }), - indexAllProjectReferences: new RateLimiter('index-all-project-references', { - points: 30, - duration: 60, - }), - indexProjectReferences: new RateLimiter('index-project-references', { - points: 30, - duration: 60, - }), - miscOutputDownload: new RateLimiter('misc-output-download', { - points: 1000, - duration: 60 * 60, - }), - multipleProjectsZipDownload: new RateLimiter( - 'multiple-projects-zip-download', - { - points: 10, - duration: 60, - } - ), - openDashboard: new RateLimiter('open-dashboard', { - points: 30, - duration: 60, - }), - readAndWriteToken: new RateLimiter('read-and-write-token', { - points: 15, - duration: 60, - }), - readOnlyToken: new RateLimiter('read-only-token', { - points: 15, - duration: 60, - }), - removeProjectFromTag: new RateLimiter('remove-project-from-tag', { - points: 30, - duration: 60, - }), - removeProjectsFromTag: new RateLimiter('remove-projects-from-tag', { - points: 30, - duration: 60, - }), - renameTag: new RateLimiter('rename-tag', { - points: 30, - duration: 60, - }), - resendConfirmation: new RateLimiter('resend-confirmation', { - points: 10, - duration: 60, - }), - sendChatMessage: new RateLimiter('send-chat-message', { - points: 100, - duration: 60, - }), - statusCompiler: new RateLimiter('status-compiler', { - points: 10, - duration: 60, - }), - zipDownload: new RateLimiter('zip-download', { - points: 10, - duration: 60, - }), -} - -function initialize(webRouter, privateApiRouter, publicApiRouter) { - webRouter.use(unsupportedBrowserMiddleware) - - if (!Settings.allowPublicAccess) { - webRouter.all('*', AuthenticationController.requireGlobalLogin) - } - - webRouter.get('*', AnalyticsRegistrationSourceMiddleware.setInbound()) - webRouter.get('*', AnalyticsUTMTrackingMiddleware.recordUTMTags()) - - // Mount onto /login in order to get the deviceHistory cookie. - webRouter.post( - '/login/can-skip-captcha', - // Keep in sync with the overleaf-login options. - RateLimiterMiddleware.rateLimit(rateLimiters.canSkipCaptcha), - CaptchaMiddleware.canSkipCaptcha - ) - - webRouter.get('/login', UserPagesController.loginPage) - AuthenticationController.addEndpointToLoginWhitelist('/login') - - webRouter.post( - '/login', - CaptchaMiddleware.validateCaptcha('login'), - AuthenticationController.passportLogin - ) - - if (Settings.enableLegacyLogin) { - AuthenticationController.addEndpointToLoginWhitelist('/login/legacy') - webRouter.get('/login/legacy', UserPagesController.loginPage) - webRouter.post( - '/login/legacy', - CaptchaMiddleware.validateCaptcha('login'), - AuthenticationController.passportLogin - ) - } - - webRouter.get( - '/read-only/one-time-login', - UserPagesController.oneTimeLoginPage - ) - AuthenticationController.addEndpointToLoginWhitelist( - '/read-only/one-time-login' - ) - - webRouter.post('/logout', UserController.logout) - - webRouter.get('/restricted', AuthorizationMiddleware.restricted) - - if (Features.hasFeature('registration-page')) { - webRouter.get('/register', UserPagesController.registerPage) - AuthenticationController.addEndpointToLoginWhitelist('/register') - } - - // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - if (process.env.OAUTH2_ENABLED === 'true') { - webRouter.get('/oauth/redirect', AuthenticationController.oauth2Redirect) - webRouter.get('/oauth/callback', AuthenticationController.oauth2Callback) - AuthenticationController.addEndpointToLoginWhitelist('/oauth/redirect') - AuthenticationController.addEndpointToLoginWhitelist('/oauth/callback') - } - // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< - - EditorRouter.apply(webRouter, privateApiRouter) - CollaboratorsRouter.apply(webRouter, privateApiRouter) - SubscriptionRouter.apply(webRouter, privateApiRouter, publicApiRouter) - UploadsRouter.apply(webRouter, privateApiRouter) - PasswordResetRouter.apply(webRouter, privateApiRouter) - StaticPagesRouter.apply(webRouter, privateApiRouter) - ContactRouter.apply(webRouter, privateApiRouter) - AnalyticsRouter.apply(webRouter, privateApiRouter, publicApiRouter) - LinkedFilesRouter.apply(webRouter, privateApiRouter, publicApiRouter) - TemplatesRouter.apply(webRouter) - UserMembershipRouter.apply(webRouter) - - Modules.applyRouter(webRouter, privateApiRouter, publicApiRouter) - - if (Settings.enableSubscriptions) { - webRouter.get( - '/user/bonus', - AuthenticationController.requireLogin(), - ReferalController.bonus - ) - } - - // .getMessages will generate an empty response for anonymous users. - webRouter.get('/system/messages', SystemMessageController.getMessages) - - webRouter.get( - '/user/settings', - AuthenticationController.requireLogin(), - PermissionsController.useCapabilities(), - UserPagesController.settingsPage - ) - webRouter.post( - '/user/settings', - AuthenticationController.requireLogin(), - UserController.updateUserSettings - ) - webRouter.post( - '/user/password/update', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.changePassword), - UserController.changePassword - ) - webRouter.get( - '/user/emails', - AuthenticationController.requireLogin(), - PermissionsController.useCapabilities(), - UserController.promises.ensureAffiliationMiddleware, - UserEmailsController.list - ) - webRouter.get('/user/emails/confirm', UserEmailsController.showConfirm) - webRouter.post( - '/user/emails/confirm', - RateLimiterMiddleware.rateLimit(rateLimiters.confirmEmail), - UserEmailsController.confirm - ) - webRouter.post( - '/user/emails/resend_confirmation', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.resendConfirmation), - UserEmailsController.resendConfirmation - ) - - webRouter.get( - '/user/emails/primary-email-check', - AuthenticationController.requireLogin(), - UserEmailsController.primaryEmailCheckPage - ) - - webRouter.post( - '/user/emails/primary-email-check', - AuthenticationController.requireLogin(), - UserEmailsController.primaryEmailCheck - ) - - if (Features.hasFeature('affiliations')) { - webRouter.post( - '/user/emails', - AuthenticationController.requireLogin(), - PermissionsController.requirePermission('add-secondary-email'), - RateLimiterMiddleware.rateLimit(rateLimiters.addEmail), - CaptchaMiddleware.validateCaptcha('addEmail'), - UserEmailsController.add - ) - webRouter.post( - '/user/emails/delete', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.deleteEmail), - UserEmailsController.remove - ) - webRouter.post( - '/user/emails/default', - AuthenticationController.requireLogin(), - UserEmailsController.setDefault - ) - webRouter.post( - '/user/emails/endorse', - AuthenticationController.requireLogin(), - PermissionsController.requirePermission('endorse-email'), - RateLimiterMiddleware.rateLimit(rateLimiters.endorseEmail), - UserEmailsController.endorse - ) - } - - webRouter.get( - '/user/sessions', - AuthenticationController.requireLogin(), - UserPagesController.sessionsPage - ) - webRouter.post( - '/user/sessions/clear', - AuthenticationController.requireLogin(), - UserController.clearSessions - ) - - // deprecated - webRouter.delete( - '/user/newsletter/unsubscribe', - AuthenticationController.requireLogin(), - UserController.unsubscribe - ) - - webRouter.post( - '/user/newsletter/unsubscribe', - AuthenticationController.requireLogin(), - UserController.unsubscribe - ) - - webRouter.post( - '/user/newsletter/subscribe', - AuthenticationController.requireLogin(), - UserController.subscribe - ) - - webRouter.get( - '/user/email-preferences', - AuthenticationController.requireLogin(), - UserPagesController.emailPreferencesPage - ) - - webRouter.post( - '/user/delete', - RateLimiterMiddleware.rateLimit(rateLimiters.deleteUser), - AuthenticationController.requireLogin(), - PermissionsController.requirePermission('delete-own-account'), - UserController.tryDeleteUser - ) - - webRouter.get( - '/user/personal_info', - AuthenticationController.requireLogin(), - UserInfoController.getLoggedInUsersPersonalInfo - ) - privateApiRouter.get( - '/user/:user_id/personal_info', - AuthenticationController.requirePrivateApiAuth(), - UserInfoController.getPersonalInfo - ) - - webRouter.get( - '/user/reconfirm', - UserPagesController.renderReconfirmAccountPage - ) - // for /user/reconfirm POST, see password router - - webRouter.get( - '/user/tpds/queues', - AuthenticationController.requireLogin(), - TpdsController.getQueues - ) - - webRouter.get( - '/user/projects', - AuthenticationController.requireLogin(), - ProjectController.userProjectsJson - ) - webRouter.get( - '/project/:Project_id/entities', - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanReadProject, - ProjectController.projectEntitiesJson - ) - - webRouter.get( - '/project', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.openDashboard), - ProjectListController.projectListPage - ) - webRouter.post( - '/project/new', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.createProject), - ProjectController.newProject - ) - webRouter.post( - '/api/project', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.getProjects), - ProjectListController.getProjectsJson - ) - - for (const route of [ - // Keep the old route for continuous metrics - '/Project/:Project_id', - // New route for pdf-detach - '/Project/:Project_id/:detachRole(detacher|detached)', - ]) { - webRouter.get( - route, - RateLimiterMiddleware.rateLimit(openProjectRateLimiter, { - params: ['Project_id'], - }), - AuthenticationController.validateUserSession(), - AuthorizationMiddleware.ensureUserCanReadProject, - ProjectController.loadEditor - ) - } - webRouter.head( - '/Project/:Project_id/file/:File_id', - AuthorizationMiddleware.ensureUserCanReadProject, - FileStoreController.getFileHead - ) - webRouter.get( - '/Project/:Project_id/file/:File_id', - AuthorizationMiddleware.ensureUserCanReadProject, - FileStoreController.getFile - ) - webRouter.post( - '/project/:Project_id/settings', - validate({ - body: Joi.object({ - publicAccessLevel: Joi.string() - .valid(PublicAccessLevels.PRIVATE, PublicAccessLevels.TOKEN_BASED) - .optional(), - }), - }), - AuthorizationMiddleware.ensureUserCanWriteProjectSettings, - ProjectController.updateProjectSettings - ) - webRouter.post( - '/project/:Project_id/settings/admin', - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanAdminProject, - ProjectController.updateProjectAdminSettings - ) - - webRouter.post( - '/project/:Project_id/compile', - RateLimiterMiddleware.rateLimit(rateLimiters.compileProjectHttp, { - params: ['Project_id'], - }), - AuthorizationMiddleware.ensureUserCanReadProject, - CompileController.compile - ) - - webRouter.post( - '/project/:Project_id/compile/stop', - AuthorizationMiddleware.ensureUserCanReadProject, - CompileController.stopCompile - ) - - // LEGACY: Used by the web download buttons, adds filename header, TODO: remove at some future date - webRouter.get( - '/project/:Project_id/output/output.pdf', - AuthorizationMiddleware.ensureUserCanReadProject, - CompileController.downloadPdf - ) - - // PDF Download button - webRouter.get( - /^\/download\/project\/([^/]*)\/output\/output\.pdf$/, - function (req, res, next) { - const params = { Project_id: req.params[0] } - req.params = params - next() - }, - AuthorizationMiddleware.ensureUserCanReadProject, - CompileController.downloadPdf - ) - - // PDF Download button for specific build - webRouter.get( - /^\/download\/project\/([^/]*)\/build\/([0-9a-f-]+)\/output\/output\.pdf$/, - function (req, res, next) { - const params = { - Project_id: req.params[0], - build_id: req.params[1], - } - req.params = params - next() - }, - AuthorizationMiddleware.ensureUserCanReadProject, - CompileController.downloadPdf - ) - - // Align with limits defined in CompileController.downloadPdf - const rateLimiterMiddlewareOutputFiles = RateLimiterMiddleware.rateLimit( - rateLimiters.miscOutputDownload, - { params: ['Project_id'] } - ) - - // Used by the pdf viewers - webRouter.get( - /^\/project\/([^/]*)\/output\/(.*)$/, - function (req, res, next) { - const params = { - Project_id: req.params[0], - file: req.params[1], - } - req.params = params - next() - }, - rateLimiterMiddlewareOutputFiles, - AuthorizationMiddleware.ensureUserCanReadProject, - CompileController.getFileFromClsi - ) - // direct url access to output files for a specific build (query string not required) - webRouter.get( - /^\/project\/([^/]*)\/build\/([0-9a-f-]+)\/output\/(.*)$/, - function (req, res, next) { - const params = { - Project_id: req.params[0], - build_id: req.params[1], - file: req.params[2], - } - req.params = params - next() - }, - rateLimiterMiddlewareOutputFiles, - AuthorizationMiddleware.ensureUserCanReadProject, - CompileController.getFileFromClsi - ) - - // direct url access to output files for user but no build, to retrieve files when build fails - webRouter.get( - /^\/project\/([^/]*)\/user\/([0-9a-f-]+)\/output\/(.*)$/, - function (req, res, next) { - const params = { - Project_id: req.params[0], - user_id: req.params[1], - file: req.params[2], - } - req.params = params - next() - }, - rateLimiterMiddlewareOutputFiles, - AuthorizationMiddleware.ensureUserCanReadProject, - CompileController.getFileFromClsi - ) - - // direct url access to output files for a specific user and build (query string not required) - webRouter.get( - /^\/project\/([^/]*)\/user\/([0-9a-f]+)\/build\/([0-9a-f-]+)\/output\/(.*)$/, - function (req, res, next) { - const params = { - Project_id: req.params[0], - user_id: req.params[1], - build_id: req.params[2], - file: req.params[3], - } - req.params = params - next() - }, - rateLimiterMiddlewareOutputFiles, - AuthorizationMiddleware.ensureUserCanReadProject, - CompileController.getFileFromClsi - ) - - webRouter.delete( - '/project/:Project_id/output', - validate({ query: { clsiserverid: Joi.string() } }), - AuthorizationMiddleware.ensureUserCanReadProject, - CompileController.deleteAuxFiles - ) - webRouter.get( - '/project/:Project_id/sync/code', - AuthorizationMiddleware.ensureUserCanReadProject, - CompileController.proxySyncCode - ) - webRouter.get( - '/project/:Project_id/sync/pdf', - AuthorizationMiddleware.ensureUserCanReadProject, - CompileController.proxySyncPdf - ) - webRouter.get( - '/project/:Project_id/wordcount', - validate({ query: { clsiserverid: Joi.string() } }), - AuthorizationMiddleware.ensureUserCanReadProject, - CompileController.wordCount - ) - - webRouter.post( - '/Project/:Project_id/archive', - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanReadProject, - ProjectController.archiveProject - ) - webRouter.delete( - '/Project/:Project_id/archive', - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanReadProject, - ProjectController.unarchiveProject - ) - webRouter.post( - '/project/:project_id/trash', - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanReadProject, - ProjectController.trashProject - ) - webRouter.delete( - '/project/:project_id/trash', - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanReadProject, - ProjectController.untrashProject - ) - - webRouter.delete( - '/Project/:Project_id', - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanAdminProject, - ProjectController.deleteProject - ) - - webRouter.post( - '/Project/:Project_id/restore', - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanAdminProject, - ProjectController.restoreProject - ) - webRouter.post( - '/Project/:Project_id/clone', - AuthorizationMiddleware.ensureUserCanReadProject, - ProjectController.cloneProject - ) - - webRouter.post( - '/project/:Project_id/rename', - AuthenticationController.requireLogin(), - AuthorizationMiddleware.ensureUserCanAdminProject, - ProjectController.renameProject - ) - webRouter.get( - '/project/:Project_id/updates', - AuthorizationMiddleware.blockRestrictedUserFromProject, - AuthorizationMiddleware.ensureUserCanReadProject, - HistoryController.proxyToHistoryApiAndInjectUserDetails - ) - webRouter.get( - '/project/:Project_id/doc/:doc_id/diff', - AuthorizationMiddleware.blockRestrictedUserFromProject, - AuthorizationMiddleware.ensureUserCanReadProject, - HistoryController.proxyToHistoryApi - ) - webRouter.get( - '/project/:Project_id/diff', - AuthorizationMiddleware.blockRestrictedUserFromProject, - AuthorizationMiddleware.ensureUserCanReadProject, - HistoryController.proxyToHistoryApiAndInjectUserDetails - ) - webRouter.get( - '/project/:Project_id/filetree/diff', - AuthorizationMiddleware.blockRestrictedUserFromProject, - AuthorizationMiddleware.ensureUserCanReadProject, - HistoryController.proxyToHistoryApi - ) - webRouter.post( - '/project/:Project_id/doc/:doc_id/version/:version_id/restore', - AuthorizationMiddleware.ensureUserCanWriteProjectContent, - HistoryController.proxyToHistoryApi - ) - webRouter.post( - '/project/:project_id/doc/:doc_id/restore', - AuthorizationMiddleware.ensureUserCanWriteProjectContent, - HistoryController.restoreDocFromDeletedDoc - ) - webRouter.post( - '/project/:project_id/restore_file', - AuthorizationMiddleware.ensureUserCanWriteProjectContent, - HistoryController.restoreFileFromV2 - ) - webRouter.get( - '/project/:project_id/version/:version/zip', - RateLimiterMiddleware.rateLimit(rateLimiters.downloadProjectRevision), - AuthorizationMiddleware.blockRestrictedUserFromProject, - AuthorizationMiddleware.ensureUserCanReadProject, - HistoryController.downloadZipOfVersion - ) - privateApiRouter.post( - '/project/:Project_id/history/resync', - AuthenticationController.requirePrivateApiAuth(), - HistoryController.resyncProjectHistory - ) - - webRouter.get( - '/project/:Project_id/labels', - AuthorizationMiddleware.blockRestrictedUserFromProject, - AuthorizationMiddleware.ensureUserCanReadProject, - HistoryController.getLabels - ) - webRouter.post( - '/project/:Project_id/labels', - AuthorizationMiddleware.ensureUserCanWriteProjectContent, - HistoryController.createLabel - ) - webRouter.delete( - '/project/:Project_id/labels/:label_id', - AuthorizationMiddleware.ensureUserCanWriteProjectContent, - HistoryController.deleteLabel - ) - - webRouter.post( - '/project/:project_id/export/:brand_variation_id', - AuthorizationMiddleware.ensureUserCanWriteProjectContent, - ExportsController.exportProject - ) - webRouter.get( - '/project/:project_id/export/:export_id', - AuthorizationMiddleware.ensureUserCanWriteProjectContent, - ExportsController.exportStatus - ) - webRouter.get( - '/project/:project_id/export/:export_id/:type', - AuthorizationMiddleware.ensureUserCanWriteProjectContent, - ExportsController.exportDownload - ) - - webRouter.get( - '/Project/:Project_id/download/zip', - RateLimiterMiddleware.rateLimit(rateLimiters.zipDownload, { - params: ['Project_id'], - }), - AuthorizationMiddleware.ensureUserCanReadProject, - ProjectDownloadsController.downloadProject - ) - webRouter.get( - '/project/download/zip', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.multipleProjectsZipDownload), - AuthorizationMiddleware.ensureUserCanReadMultipleProjects, - ProjectDownloadsController.downloadMultipleProjects - ) - - webRouter.get( - '/project/:project_id/metadata', - AuthorizationMiddleware.ensureUserCanReadProject, - Settings.allowAnonymousReadAndWriteSharing - ? (req, res, next) => { - next() - } - : AuthenticationController.requireLogin(), - MetaController.getMetadata - ) - webRouter.post( - '/project/:project_id/doc/:doc_id/metadata', - AuthorizationMiddleware.ensureUserCanReadProject, - Settings.allowAnonymousReadAndWriteSharing - ? (req, res, next) => { - next() - } - : AuthenticationController.requireLogin(), - MetaController.broadcastMetadataForDoc - ) - privateApiRouter.post( - '/internal/expire-deleted-projects-after-duration', - AuthenticationController.requirePrivateApiAuth(), - ProjectController.expireDeletedProjectsAfterDuration - ) - privateApiRouter.post( - '/internal/expire-deleted-users-after-duration', - AuthenticationController.requirePrivateApiAuth(), - UserController.expireDeletedUsersAfterDuration - ) - privateApiRouter.post( - '/internal/project/:projectId/expire-deleted-project', - AuthenticationController.requirePrivateApiAuth(), - ProjectController.expireDeletedProject - ) - privateApiRouter.post( - '/internal/users/:userId/expire', - AuthenticationController.requirePrivateApiAuth(), - UserController.expireDeletedUser - ) - - privateApiRouter.get( - '/user/:userId/tag', - AuthenticationController.requirePrivateApiAuth(), - TagsController.apiGetAllTags - ) - webRouter.get( - '/tag', - AuthenticationController.requireLogin(), - TagsController.getAllTags - ) - webRouter.post( - '/tag', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.createTag), - validate({ - body: Joi.object({ - name: Joi.string().required(), - color: Joi.string(), - }), - }), - TagsController.createTag - ) - webRouter.post( - '/tag/:tagId/rename', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.renameTag), - validate({ - body: Joi.object({ - name: Joi.string().required(), - }), - }), - TagsController.renameTag - ) - webRouter.post( - '/tag/:tagId/edit', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.renameTag), - validate({ - body: Joi.object({ - name: Joi.string().required(), - color: Joi.string(), - }), - }), - TagsController.editTag - ) - webRouter.delete( - '/tag/:tagId', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.deleteTag), - TagsController.deleteTag - ) - webRouter.post( - '/tag/:tagId/project/:projectId', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.addProjectToTag), - TagsController.addProjectToTag - ) - webRouter.post( - '/tag/:tagId/projects', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.addProjectsToTag), - validate({ - body: Joi.object({ - projectIds: Joi.array().items(Joi.string()).required(), - }), - }), - TagsController.addProjectsToTag - ) - webRouter.delete( - '/tag/:tagId/project/:projectId', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.removeProjectFromTag), - TagsController.removeProjectFromTag - ) - webRouter.delete( - '/tag/:tagId/projects', - AuthenticationController.requireLogin(), - RateLimiterMiddleware.rateLimit(rateLimiters.removeProjectsFromTag), - validate({ - body: Joi.object({ - projectIds: Joi.array().items(Joi.string()).required(), - }), - }), - TagsController.removeProjectsFromTag - ) - - webRouter.get( - '/notifications', - AuthenticationController.requireLogin(), - NotificationsController.getAllUnreadNotifications - ) - webRouter.delete( - '/notifications/:notificationId', - AuthenticationController.requireLogin(), - NotificationsController.markNotificationAsRead - ) - - // Deprecated in favour of /internal/project/:project_id but still used by versioning - privateApiRouter.get( - '/project/:project_id/details', - AuthenticationController.requirePrivateApiAuth(), - ProjectApiController.getProjectDetails - ) - - // New 'stable' /internal API end points - privateApiRouter.get( - '/internal/project/:project_id', - AuthenticationController.requirePrivateApiAuth(), - ProjectApiController.getProjectDetails - ) - privateApiRouter.get( - '/internal/project/:Project_id/zip', - AuthenticationController.requirePrivateApiAuth(), - ProjectDownloadsController.downloadProject - ) - privateApiRouter.get( - '/internal/project/:project_id/compile/pdf', - AuthenticationController.requirePrivateApiAuth(), - CompileController.compileAndDownloadPdf - ) - - privateApiRouter.post( - '/internal/deactivateOldProjects', - AuthenticationController.requirePrivateApiAuth(), - InactiveProjectController.deactivateOldProjects - ) - privateApiRouter.post( - '/internal/project/:project_id/deactivate', - AuthenticationController.requirePrivateApiAuth(), - InactiveProjectController.deactivateProject - ) - - privateApiRouter.get( - /^\/internal\/project\/([^/]*)\/output\/(.*)$/, - function (req, res, next) { - const params = { - Project_id: req.params[0], - file: req.params[1], - } - req.params = params - next() - }, - AuthenticationController.requirePrivateApiAuth(), - CompileController.getFileFromClsi - ) - - privateApiRouter.get( - '/project/:Project_id/doc/:doc_id', - AuthenticationController.requirePrivateApiAuth(), - DocumentController.getDocument - ) - privateApiRouter.post( - '/project/:Project_id/doc/:doc_id', - AuthenticationController.requirePrivateApiAuth(), - DocumentController.setDocument - ) - - privateApiRouter.post( - '/user/:user_id/project/new', - AuthenticationController.requirePrivateApiAuth(), - TpdsController.createProject - ) - privateApiRouter.post( - '/tpds/folder-update', - AuthenticationController.requirePrivateApiAuth(), - TpdsController.updateFolder - ) - privateApiRouter.post( - '/user/:user_id/update/*', - AuthenticationController.requirePrivateApiAuth(), - TpdsController.mergeUpdate - ) - privateApiRouter.delete( - '/user/:user_id/update/*', - AuthenticationController.requirePrivateApiAuth(), - TpdsController.deleteUpdate - ) - privateApiRouter.post( - '/project/:project_id/user/:user_id/update/*', - AuthenticationController.requirePrivateApiAuth(), - TpdsController.mergeUpdate - ) - privateApiRouter.delete( - '/project/:project_id/user/:user_id/update/*', - AuthenticationController.requirePrivateApiAuth(), - TpdsController.deleteUpdate - ) - - privateApiRouter.post( - '/project/:project_id/contents/*', - AuthenticationController.requirePrivateApiAuth(), - TpdsController.updateProjectContents - ) - privateApiRouter.delete( - '/project/:project_id/contents/*', - AuthenticationController.requirePrivateApiAuth(), - TpdsController.deleteProjectContents - ) - - webRouter.post( - '/spelling/check', - AuthenticationController.requireLogin(), - SpellingController.proxyRequestToSpellingApi - ) - webRouter.post( - '/spelling/learn', - validate({ - body: Joi.object({ - word: Joi.string().required(), - }), - }), - AuthenticationController.requireLogin(), - SpellingController.learn - ) - - webRouter.post( - '/spelling/unlearn', - validate({ - body: Joi.object({ - word: Joi.string().required(), - }), - }), - AuthenticationController.requireLogin(), - SpellingController.unlearn - ) - - webRouter.get( - '/project/:project_id/messages', - AuthorizationMiddleware.blockRestrictedUserFromProject, - AuthorizationMiddleware.ensureUserCanReadProject, - ChatController.getMessages - ) - webRouter.post( - '/project/:project_id/messages', - AuthorizationMiddleware.blockRestrictedUserFromProject, - AuthorizationMiddleware.ensureUserCanReadProject, - RateLimiterMiddleware.rateLimit(rateLimiters.sendChatMessage), - ChatController.sendMessage - ) - - webRouter.post( - '/project/:Project_id/references/index', - AuthorizationMiddleware.ensureUserCanReadProject, - RateLimiterMiddleware.rateLimit(rateLimiters.indexProjectReferences), - ReferencesController.index - ) - webRouter.post( - '/project/:Project_id/references/indexAll', - AuthorizationMiddleware.ensureUserCanReadProject, - RateLimiterMiddleware.rateLimit(rateLimiters.indexAllProjectReferences), - ReferencesController.indexAll - ) - - // disable beta program while v2 is in beta - webRouter.get( - '/beta/participate', - AuthenticationController.requireLogin(), - BetaProgramController.optInPage - ) - webRouter.post( - '/beta/opt-in', - AuthenticationController.requireLogin(), - BetaProgramController.optIn - ) - webRouter.post( - '/beta/opt-out', - AuthenticationController.requireLogin(), - BetaProgramController.optOut - ) - - // New "api" endpoints. Started as a way for v1 to call over to v2 (for - // long-term features, as opposed to the nominally temporary ones in the - // overleaf-integration module), but may expand beyond that role. - publicApiRouter.post( - '/api/clsi/compile/:submission_id', - AuthenticationController.requirePrivateApiAuth(), - CompileController.compileSubmission - ) - publicApiRouter.get( - /^\/api\/clsi\/compile\/([^/]*)\/build\/([0-9a-f-]+)\/output\/(.*)$/, - function (req, res, next) { - const params = { - submission_id: req.params[0], - build_id: req.params[1], - file: req.params[2], - } - req.params = params - next() - }, - AuthenticationController.requirePrivateApiAuth(), - CompileController.getFileFromClsiWithoutUser - ) - publicApiRouter.post( - '/api/institutions/confirm_university_domain', - RateLimiterMiddleware.rateLimit(rateLimiters.confirmUniversityDomain), - AuthenticationController.requirePrivateApiAuth(), - InstitutionsController.confirmDomain - ) - - webRouter.get('/chrome', function (req, res, next) { - // Match v1 behaviour - this is used for a Chrome web app - if (SessionManager.isUserLoggedIn(req.session)) { - res.redirect('/project') - } else { - res.redirect('/register') - } - }) - - webRouter.get( - '/admin', - AuthorizationMiddleware.ensureUserIsSiteAdmin, - AdminController.index - ) - - if (!Features.hasFeature('saas')) { - webRouter.post( - '/admin/openEditor', - AuthorizationMiddleware.ensureUserIsSiteAdmin, - AdminController.openEditor - ) - webRouter.post( - '/admin/closeEditor', - AuthorizationMiddleware.ensureUserIsSiteAdmin, - AdminController.closeEditor - ) - webRouter.post( - '/admin/disconnectAllUsers', - AuthorizationMiddleware.ensureUserIsSiteAdmin, - AdminController.disconnectAllUsers - ) - } - webRouter.post( - '/admin/flushProjectToTpds', - AuthorizationMiddleware.ensureUserIsSiteAdmin, - AdminController.flushProjectToTpds - ) - webRouter.post( - '/admin/pollDropboxForUser', - AuthorizationMiddleware.ensureUserIsSiteAdmin, - AdminController.pollDropboxForUser - ) - webRouter.post( - '/admin/messages', - AuthorizationMiddleware.ensureUserIsSiteAdmin, - AdminController.createMessage - ) - webRouter.post( - '/admin/messages/clear', - AuthorizationMiddleware.ensureUserIsSiteAdmin, - AdminController.clearMessages - ) - - privateApiRouter.get('/perfTest', (req, res) => { - plainTextResponse(res, 'hello') - }) - - publicApiRouter.get('/status', (req, res) => { - if (Settings.shuttingDown) { - res.sendStatus(503) // Service unavailable - } else if (!Settings.siteIsOpen) { - plainTextResponse(res, 'web site is closed (web)') - } else if (!Settings.editorIsOpen) { - plainTextResponse(res, 'web editor is closed (web)') - } else { - plainTextResponse(res, 'web sharelatex is alive (web)') - } - }) - privateApiRouter.get('/status', (req, res) => { - plainTextResponse(res, 'web sharelatex is alive (api)') - }) - - // used by kubernetes health-check and acceptance tests - webRouter.get('/dev/csrf', (req, res) => { - plainTextResponse(res, res.locals.csrfToken) - }) - - publicApiRouter.get( - '/health_check', - HealthCheckController.checkActiveHandles, - HealthCheckController.check - ) - privateApiRouter.get( - '/health_check', - HealthCheckController.checkActiveHandles, - HealthCheckController.checkApi - ) - publicApiRouter.get( - '/health_check/api', - HealthCheckController.checkActiveHandles, - HealthCheckController.checkApi - ) - privateApiRouter.get( - '/health_check/api', - HealthCheckController.checkActiveHandles, - HealthCheckController.checkApi - ) - publicApiRouter.get( - '/health_check/full', - HealthCheckController.checkActiveHandles, - HealthCheckController.check - ) - privateApiRouter.get( - '/health_check/full', - HealthCheckController.checkActiveHandles, - HealthCheckController.check - ) - - publicApiRouter.get('/health_check/redis', HealthCheckController.checkRedis) - privateApiRouter.get('/health_check/redis', HealthCheckController.checkRedis) - - publicApiRouter.get('/health_check/mongo', HealthCheckController.checkMongo) - privateApiRouter.get('/health_check/mongo', HealthCheckController.checkMongo) - - webRouter.get( - '/status/compiler/:Project_id', - RateLimiterMiddleware.rateLimit(rateLimiters.statusCompiler), - AuthorizationMiddleware.ensureUserCanReadProject, - function (req, res) { - const projectId = req.params.Project_id - // use a valid user id for testing - const testUserId = '123456789012345678901234' - const sendRes = _.once(function (statusCode, message) { - res.status(statusCode) - plainTextResponse(res, message) - ClsiCookieManager.clearServerId(projectId, testUserId, () => {}) - }) // force every compile to a new server - // set a timeout - let handler = setTimeout(function () { - sendRes(500, 'Compiler timed out') - handler = null - }, 10000) - // run the compile - CompileManager.compile( - projectId, - testUserId, - {}, - function (error, status) { - if (handler) { - clearTimeout(handler) - } - if (error) { - sendRes(500, `Compiler returned error ${error.message}`) - } else if (status === 'success') { - sendRes(200, 'Compiler returned in less than 10 seconds') - } else { - sendRes(500, `Compiler returned failure ${status}`) - } - } - ) - } - ) - - webRouter.get('/no-cache', function (req, res, next) { - res.header('Cache-Control', 'max-age=0') - res.sendStatus(404) - }) - - webRouter.get('/oops-express', (req, res, next) => - next(new Error('Test error')) - ) - webRouter.get('/oops-internal', function (req, res, next) { - throw new Error('Test error') - }) - webRouter.get('/oops-mongo', (req, res, next) => - require('./models/Project').Project.findOne({}, function () { - throw new Error('Test error') - }) - ) - - privateApiRouter.get('/opps-small', function (req, res, next) { - logger.err('test error occured') - res.sendStatus(200) - }) - - webRouter.post('/error/client', function (req, res, next) { - logger.warn( - { err: req.body.error, meta: req.body.meta }, - 'client side error' - ) - metrics.inc('client-side-error') - res.sendStatus(204) - }) - - webRouter.get( - `/read/:token(${TokenAccessController.READ_ONLY_TOKEN_PATTERN})`, - RateLimiterMiddleware.rateLimit(rateLimiters.readOnlyToken), - AnalyticsRegistrationSourceMiddleware.setSource( - 'collaboration', - 'link-sharing' - ), - TokenAccessController.tokenAccessPage, - AnalyticsRegistrationSourceMiddleware.clearSource() - ) - - webRouter.get( - `/:token(${TokenAccessController.READ_AND_WRITE_TOKEN_PATTERN})`, - RateLimiterMiddleware.rateLimit(rateLimiters.readAndWriteToken), - AnalyticsRegistrationSourceMiddleware.setSource( - 'collaboration', - 'link-sharing' - ), - TokenAccessController.tokenAccessPage, - AnalyticsRegistrationSourceMiddleware.clearSource() - ) - - webRouter.post( - `/:token(${TokenAccessController.READ_AND_WRITE_TOKEN_PATTERN})/grant`, - RateLimiterMiddleware.rateLimit(rateLimiters.grantTokenAccessReadWrite), - TokenAccessController.grantTokenAccessReadAndWrite - ) - - webRouter.post( - `/read/:token(${TokenAccessController.READ_ONLY_TOKEN_PATTERN})/grant`, - RateLimiterMiddleware.rateLimit(rateLimiters.grantTokenAccessReadOnly), - TokenAccessController.grantTokenAccessReadOnly - ) - - webRouter.get('/unsupported-browser', renderUnsupportedBrowserPage) - - webRouter.get('*', ErrorController.notFound) -} - -module.exports = { initialize, rateLimiters } diff --git a/ldap-overleaf-sl/sharelatex/settings.pug b/ldap-overleaf-sl/sharelatex/settings.pug deleted file mode 100644 index 8cdd18c..0000000 --- a/ldap-overleaf-sl/sharelatex/settings.pug +++ /dev/null @@ -1,178 +0,0 @@ -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")  I have left, purged or imported my projects on Overleaf v1 (if any)   - - div.confirmation-checkbox-wrapper - input( - type="checkbox" - ng-model="state.confirmSharelatexDelete" - ng-change="checkValidation()" - ).pull-left - label(style="display: inline")  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 || {})} diff --git a/ldap-overleaf-sl/sharelatex_diff/AuthenticationController.js.diff b/ldap-overleaf-sl/sharelatex_diff/AuthenticationController.js.diff new file mode 100644 index 0000000..09581c6 --- /dev/null +++ b/ldap-overleaf-sl/sharelatex_diff/AuthenticationController.js.diff @@ -0,0 +1,106 @@ +68a69,70 +> alphaProgram: user.alphaProgram || undefined, // only store if set +> betaProgram: user.betaProgram || undefined, // only store if set +266a269,365 +> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> oauth2Redirect(req, res, next) { +> // random state +> const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' +> const state = new Array(6).fill(0).map(() => characters.charAt(Math.floor(Math.random() * characters.length))).join("") +> req.session.oauth2State = state +> +> const redirectURI = encodeURIComponent(`${process.env.SHARELATEX_SITE_URL}/oauth/callback`) +> const authURL = ( +> process.env.OAUTH2_AUTHORIZATION_URL +> + `?response_type=code` +> + `&client_id=${process.env.OAUTH2_CLIENT_ID}` +> + `&redirect_uri=${redirectURI}` +> + `&scope=${process.env.OAUTH2_SCOPE ?? ""} ` +> + `&state=${state}` +> ) +> res.redirect(authURL) +> }, +> +> async oauth2Callback(req, res, next) { +> console.log(`OAuth, receive code ${req.query.code} and state ${req.query.state}`) +> const saveState = req.session.oauth2State +> delete req.session.oauth2State +> if (saveState !== req.query.state) { +> return AuthenticationController.finishLogin(false, req, res, next) +> } +> +> try { +> const contentType = process.env.OAUTH2_TOKEN_CONTENT_TYPE || 'application/x-www-form-urlencoded' +> const bodyParams = { +> grant_type: "authorization_code", +> client_id: process.env.OAUTH2_CLIENT_ID, +> client_secret: process.env.OAUTH2_CLIENT_SECRET, +> code: req.query.code, +> redirect_uri: `${process.env.SHARELATEX_SITE_URL}/oauth/callback`, +> } +> const body = contentType === 'application/json' +> ? JSON.stringify(bodyParams) +> : new URLSearchParams(bodyParams).toString() +> +> const tokenResponse = await fetch(process.env.OAUTH2_TOKEN_URL, { +> method: 'POST', +> headers: { +> "Accept": "application/json", +> "Content-Type": contentType, +> }, +> body +> }) +> +> const tokenData = await tokenResponse.json() +> console.log("OAuth2 respond", JSON.stringify(tokenData)) +> +> const profileResponse = await fetch(process.env.OAUTH2_PROFILE_URL, { +> method: 'GET', +> headers: { +> "Accept": "application/json", +> "Authorization": `Bearer ${tokenData.access_token}`, +> } +> }); +> const profile = await profileResponse.json() +> console.log("OAuth2 user profile", JSON.stringify(profile)) +> +> const email = profile[process.env.OAUTH2_USER_ATTR_EMAIL ?? "email"] +> const uid = profile[process.env.OAUTH2_USER_ATTR_UID ?? "uid"] +> const firstname = profile?.[process.env.OAUTH2_USER_ATTR_FIRSTNAME] ?? email +> const lastname = process.env.OAUTH2_USER_ATTR_LASTNAME +> ? profile?.[process.env.OAUTH2_USER_ATTR_LASTNAME] ?? "" +> : "" +> const isAdmin = process.env.OAUTH2_USER_ATTR_IS_ADMIN +> ? !!profile?.[process.env.OAUTH2_USER_ATTR_IS_ADMIN] ?? false +> : false +> +> const query = { email } +> const callback = (error, user) => { +> if (error) { +> res.json({message: error}); +> } else { +> console.log("OAuth user", JSON.stringify(user)); +> AuthenticationController.finishLogin(user, req, res, next); +> } +> } +> AuthenticationManager.createIfNotFoundAndLogin( +> query, +> callback, +> uid, +> firstname, +> lastname, +> email, +> isAdmin +> ) +> } catch(e) { +> res.redirect("/login") +> console.error("Fails to access by OAuth2: " + String(e)) +> } +> }, +> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +> +636c735 +< module.exports = AuthenticationController +--- +> module.exports = AuthenticationController +\ 文件末尾没有换行符 diff --git a/ldap-overleaf-sl/sharelatex_diff/AuthenticationManager.js.diff b/ldap-overleaf-sl/sharelatex_diff/AuthenticationManager.js.diff new file mode 100644 index 0000000..1e22606 --- /dev/null +++ b/ldap-overleaf-sl/sharelatex_diff/AuthenticationManager.js.diff @@ -0,0 +1,305 @@ +19a20,25 +> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> const fs = require("fs") +> const { Client } = require("ldapts") +> const ldapEscape = require("ldap-escape") +> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +> +84a91,100 +> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> _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) +> }) +> }, +> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +> +90c106,108 +< AuthenticationManager._checkUserPassword( +--- +> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> AuthenticationManager._checkUserPassword2( +> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +153a172,451 +> +> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> /** +> * login with any password +> */ +> login(user, password, callback) { +> callback(null, user, true) +> }, +> +> createIfNotFoundAndLogin( +> query, +> callback, +> uid, +> firstname, +> lastname, +> mail, +> isAdmin +> ) { +> User.findOne(query, (error, user) => { +> if (error) { +> console.log(error) +> } +> +> AuthenticationManager.createIfNotExistAndLogin( +> query, +> user, +> callback, +> uid, +> firstname, +> lastname, +> mail, +> isAdmin +> ) +> }) +> }, +> +> createIfNotExistAndLogin( +> query, +> user, +> callback, +> uid, +> firstname, +> lastname, +> mail, +> isAdmin +> ) { +> if (!user) { +> //create random pass for local userdb, does not get checked for ldap users during login +> const pass = require("crypto").randomBytes(32).toString("hex") +> console.log('Creating User', { mail, uid, firstname, lastname, isAdmin, pass }) +> +> const userRegHand = require("../User/UserRegistrationHandler.js") +> userRegHand.registerNewUser( +> { +> email: mail, +> first_name: firstname, +> last_name: lastname, +> password: pass, +> }, +> function (error, user, setNewPasswordUrl) { +> 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 { +> console.log('User exists', { mail }) +> 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") +> _metricsForSuccessfulPasswordMatch(password) +> //callback(null, user, match) +> AuthenticationManager.login(user, "randomPass", 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 +> }, +> +> async ldapAuth( +> query, +> password, +> onSuccessCreateUserIfNotExistent, +> callback, +> user +> ) { +> 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 +> 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/ldap-overleaf-sl/sharelatex_diff/ContactController.js.diff b/ldap-overleaf-sl/sharelatex_diff/ContactController.js.diff new file mode 100644 index 0000000..78b9c6e --- /dev/null +++ b/ldap-overleaf-sl/sharelatex_diff/ContactController.js.diff @@ -0,0 +1,76 @@ +6a7,10 +> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> const { Client } = require('ldapts') +> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +> +34,35c38,39 +< const contactId = contactIds[i] +< positions[contactId] = i +--- +> const contact_id = contactIds[i] +> positions[contact_id] = i +42c46,51 +< contacts = contacts.filter(c => !c.holdingAccount) +--- +> contacts = contacts.filter((c) => !c.holdingAccount) +> +> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> const ldapcontacts = getLdapContacts(contacts) +> contacts.push(ldapcontacts) +> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +56a66,120 +> +> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> 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 +> } +> } +> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< diff --git a/ldap-overleaf-sl/sharelatex_diff/admin-index.pug.diff b/ldap-overleaf-sl/sharelatex_diff/admin-index.pug.diff new file mode 100644 index 0000000..0072fe0 --- /dev/null +++ b/ldap-overleaf-sl/sharelatex_diff/admin-index.pug.diff @@ -0,0 +1,146 @@ +1,2c1 +< extends ../layout-marketing +< include ../_mixins/bookmarkable_tabset +--- +> extends ../layout +9c8 +< .card +--- +> .card(ng-controller="RegisterUsersController") +12,79c11,37 +< div(data-ol-bookmarkable-tabset) +< ul.nav.nav-tabs(role="tablist") +< +bookmarkable-tabset-header('system-messages', 'System Messages', true) +< +bookmarkable-tabset-header('open-sockets', 'Open Sockets') +< +bookmarkable-tabset-header('open-close-editor', 'Open/Close Editor') +< if hasFeature('saas') +< +bookmarkable-tabset-header('tpds', 'TPDS/Dropbox Management') +< +< .tab-content +< .tab-pane.active( +< role="tabpanel" +< id='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-pane( +< role="tabpanel" +< id='open-sockets' +< ) +< .row-spaced +< ul +< each agents, url in openSockets +< li(ng-non-bindable) #{url} - total : #{agents.length} +< ul +< each agent in agents +< li(ng-non-bindable) #{agent} +< +< .tab-pane( +< role="tabpanel" +< id='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. +< +< if hasFeature('saas') +< .tab-pane( +< role="tabpanel" +< id='tpds' +--- +> 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()" +81,99c39,57 +< h3 Flush project to TPDS +< .row +< form.col-xs-6(method='post',action='/admin/flushProjectToTpds') +< input(name="_csrf", type="hidden", value=csrfToken) +< .form-group +< label(for='project_id') project_id +< input.form-control(type='text', name='project_id', placeholder='project_id', required) +< .form-group +< button.btn-primary.btn(type='submit') Flush +< hr +< h3 Poll Dropbox for user +< .row +< form.col-xs-6(method='post',action='/admin/pollDropboxForUser') +< input(name="_csrf", type="hidden", value=csrfToken) +< .form-group +< label(for='user_id') user_id +< input.form-control(type='text', name='user_id', placeholder='user_id', required) +< .form-group +< button.btn-primary.btn(type='submit') Poll +--- +> .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/ldap-overleaf-sl/sharelatex_diff/admin-sysadmin.pug.diff b/ldap-overleaf-sl/sharelatex_diff/admin-sysadmin.pug.diff new file mode 100644 index 0000000..598696c --- /dev/null +++ b/ldap-overleaf-sl/sharelatex_diff/admin-sysadmin.pug.diff @@ -0,0 +1,166 @@ +1,2c1 +< extends ../layout-marketing +< include ../_mixins/bookmarkable_tabset +--- +> extends ../layout +9c8 +< .card +--- +> .card(ng-controller="RegisterUsersController") +12,16c11,58 +< div(data-ol-bookmarkable-tabset) +< ul.nav.nav-tabs(role="tablist") +< +bookmarkable-tabset-header('system-messages', 'System Messages', true) +< +bookmarkable-tabset-header('open-sockets', 'Open Sockets') +< +bookmarkable-tabset-header('open-close-editor', 'Open/Close Editor') +--- +> 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") +18c60,66 +< +bookmarkable-tabset-header('tpds', 'TPDS/Dropbox Management') +--- +> | 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. +20,42d67 +< .tab-content +< .tab-pane.active( +< role="tabpanel" +< id='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-pane( +< role="tabpanel" +< id='open-sockets' +< ) +44,74c69,78 +< ul +< each agents, url in openSockets +< li(ng-non-bindable) #{url} - total : #{agents.length} +< ul +< each agent in agents +< li(ng-non-bindable) #{agent} +< +< .tab-pane( +< role="tabpanel" +< id='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. +--- +> 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. +76,99d79 +< if hasFeature('saas') +< .tab-pane( +< role="tabpanel" +< id='tpds' +< ) +< h3 Flush project to TPDS +< .row +< form.col-xs-6(method='post',action='/admin/flushProjectToTpds') +< input(name="_csrf", type="hidden", value=csrfToken) +< .form-group +< label(for='project_id') project_id +< input.form-control(type='text', name='project_id', placeholder='project_id', required) +< .form-group +< button.btn-primary.btn(type='submit') Flush +< hr +< h3 Poll Dropbox for user +< .row +< form.col-xs-6(method='post',action='/admin/pollDropboxForUser') +< input(name="_csrf", type="hidden", value=csrfToken) +< .form-group +< label(for='user_id') user_id +< input.form-control(type='text', name='user_id', placeholder='user_id', required) +< .form-group +< button.btn-primary.btn(type='submit') Poll diff --git a/ldap-overleaf-sl/sharelatex_diff/login.pug.diff b/ldap-overleaf-sl/sharelatex_diff/login.pug.diff new file mode 100644 index 0000000..3b01643 --- /dev/null +++ b/ldap-overleaf-sl/sharelatex_diff/login.pug.diff @@ -0,0 +1,20 @@ +14a15,22 +> //- >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> //- input.form-control( +> //- type='email', +> //- name='email', +> //- required, +> //- placeholder='email@example.com', +> //- autofocus="true" +> //- ) +16d23 +< type='email', +21a29 +> //- <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +36a45,50 +> //- >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> if process.env.OAUTH2_ENABLED === 'true' +> .form-group.text-center(style="padding-top: 10px") +> a.btn-block.login-btn(href="/oauth/redirect" style='padding-left: 0px') +> | Log in via #{process.env.OAUTH2_PROVIDER || 'OAuth'} +> //- <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< diff --git a/ldap-overleaf-sl/sharelatex_diff/navbar.pug.diff b/ldap-overleaf-sl/sharelatex_diff/navbar.pug.diff new file mode 100644 index 0000000..b1aaa94 --- /dev/null +++ b/ldap-overleaf-sl/sharelatex_diff/navbar.pug.diff @@ -0,0 +1,217 @@ +4,6c4,5 +< if (typeof(suppressNavbarRight) == "undefined") +< 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") +--- +> 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") +14,106c13,74 +< - var canDisplayAdminMenu = hasAdminAccess() +< - var canDisplayAdminRedirect = canRedirectToAdminDomain() +< - var canDisplaySplitTestMenu = hasFeature('saas') && (canDisplayAdminMenu || (getSessionUser() && getSessionUser().staffAccess && (getSessionUser().staffAccess.splitTestMetrics || getSessionUser().staffAccess.splitTestManagement))) +< - var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu +< - var featuresPageVariant = splitTestVariants && splitTestVariants['features-page'] +< +< if (typeof(suppressNavbarRight) == "undefined") +< .navbar-collapse.collapse(collapse="navCollapsed") +< ul.nav.navbar-nav.navbar-right +< if (canDisplayAdminMenu || canDisplayAdminRedirect || canDisplaySplitTestMenu) +< li.dropdown(class="subdued", dropdown) +< a.dropdown-toggle(href, dropdown-toggle) +< | Admin +< b.caret +< ul.dropdown-menu +< if canDisplayAdminMenu +< li +< a(href="/admin") Manage Site +< li +< a(href="/admin/user") Manage Users +< li +< a(href="/admin/project") Project URL Lookup +< li +< a(href="/admin/saml/logs") SAML logs +< if canDisplayAdminRedirect +< li +< a(href=settings.adminUrl) Switch to Admin +< if canDisplaySplitTestMenu +< li +< a(href="/admin/split-test") Manage Feature Flags +< if canDisplaySurveyMenu +< li +< a(href="/admin/survey") Manage Surveys +< +< // 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 if child.isContactUs +< li +< a(ng-controller="ContactModal" ng-click="contactUsModal()" href) +< span(event-tracking="menu-clicked-contact" event-tracking-mb="true" event-tracking-trigger="click") +< | #{translate("contact_us")} +< else +< li +< if child.url +< if !child.splitTest || child.splitTest && child.splitTest === 'features-page' && child.splitTestVariant === featuresPageVariant +< a( +< href=child.url, +< class=child.class, +< event-tracking=child.event +< event-tracking-mb="true" +< event-tracking-trigger="click" +< event-segmentation=child.eventSegmentation +< ) !{translate(child.text)} +< else +< | !{translate(child.text)} +< else +< li(class=item.class) +< if item.url +< a( +< href=item.url, +< class=item.class, +< event-tracking=item.event +< event-tracking-mb="true" +< event-tracking-trigger="click" +< ) !{translate(item.text)} +< else +< | !{translate(item.text)} +< +< // logged out +< if !getSessionUser() +< // register link +< if hasFeature('registration-page') +--- +> .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 +108,139c76,77 +< a( +< href="/register" +< event-tracking="menu-clicked-register" +< event-tracking-action="clicked" +< event-tracking-trigger="click" +< event-tracking-mb="true" +< event-segmentation={ page: currentUrl } +< ) #{translate('register')} +< +< // login link +< li +< a( +< href="/login" +< event-tracking="menu-clicked-login" +< event-tracking-action="clicked" +< event-tracking-trigger="click" +< event-tracking-mb="true" +< event-segmentation={ page: currentUrl } +< ) #{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 {{ usersEmail }} +< li.divider.hidden-xs.hidden-sm +--- +> a(href="/user/settings") #{translate('Account Settings')} +> if nav.showSubscriptionLink +141,149c79,84 +< 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')} +--- +> 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/ldap-overleaf-sl/sharelatex_diff/router.js.diff b/ldap-overleaf-sl/sharelatex_diff/router.js.diff new file mode 100644 index 0000000..0077d06 --- /dev/null +++ b/ldap-overleaf-sl/sharelatex_diff/router.js.diff @@ -0,0 +1,10 @@ +259a260,268 +> // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> if (process.env.OAUTH2_ENABLED === 'true') { +> webRouter.get('/oauth/redirect', AuthenticationController.oauth2Redirect) +> webRouter.get('/oauth/callback', AuthenticationController.oauth2Callback) +> AuthenticationController.addEndpointToLoginWhitelist('/oauth/redirect') +> AuthenticationController.addEndpointToLoginWhitelist('/oauth/callback') +> } +> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +> diff --git a/ldap-overleaf-sl/sharelatex_diff/settings.pug.diff b/ldap-overleaf-sl/sharelatex_diff/settings.pug.diff new file mode 100644 index 0000000..09b86f3 --- /dev/null +++ b/ldap-overleaf-sl/sharelatex_diff/settings.pug.diff @@ -0,0 +1,211 @@ +1c1 +< extends ../layout-marketing +--- +> extends ../layout +3,4c3,14 +< block entrypointVar +< - entrypoint = 'pages/user/settings' +--- +> 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) +6,28c16,17 +< block append meta +< meta(name="ol-hasPassword" data-type="boolean" content=hasPassword) +< meta(name="ol-shouldAllowEditingDetails" data-type="boolean" content=shouldAllowEditingDetails) +< meta(name="ol-oauthProviders", data-type="json", content=oauthProviders) +< meta(name="ol-institutionLinked", data-type="json", content=institutionLinked) +< meta(name="ol-samlError", data-type="json", content=samlError) +< meta(name="ol-institutionEmailNonCanonical", content=institutionEmailNonCanonical) +< +< meta(name="ol-reconfirmedViaSAML", content=reconfirmedViaSAML) +< meta(name="ol-reconfirmationRemoveEmail", content=reconfirmationRemoveEmail) +< meta(name="ol-samlBeta", content=samlBeta) +< meta(name="ol-ssoErrorMessage", content=ssoErrorMessage) +< meta(name="ol-thirdPartyIds", data-type="json", content=thirdPartyIds || {}) +< meta(name="ol-passwordStrengthOptions", data-type="json", content=settings.passwordStrengthOptions || {}) +< meta(name="ol-isExternalAuthenticationSystemUsed" data-type="boolean" content=externalAuthenticationSystemUsed()) +< meta(name="ol-user" data-type="json" content=user) +< meta(name="ol-dropbox" data-type="json" content=dropbox) +< meta(name="ol-github" data-type="json" content=github) +< meta(name="ol-projectSyncSuccessMessage", content=projectSyncSuccessMessage) +< meta(name="ol-showPersonalAccessToken", data-type="boolean" content=showPersonalAccessToken) +< meta(name="ol-personalAccessTokens", data-type="json" content=personalAccessTokens) +< meta(name="ol-emailAddressLimit", data-type="json", content=emailAddressLimit) +< meta(name="ol-currentManagedUserAdminEmail" data-type="string" content=currentManagedUserAdminEmail) +--- +> if hasFeature('affiliations') +> include settings/user-affiliations +30,31c19,178 +< block content +< main.content.content-alt#settings-page-root +--- +> .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")  I have left, purged or imported my projects on Overleaf v1 (if any)   +> +> div.confirmation-checkbox-wrapper +> input( +> type="checkbox" +> ng-model="state.confirmSharelatexDelete" +> ng-change="checkValidation()" +> ).pull-left +> label(style="display: inline")  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 || {})} diff --git a/ldap-overleaf-sl/sharelatex_ori/.gitkeep b/ldap-overleaf-sl/sharelatex_ori/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/apply_diffs.sh b/scripts/apply_diffs.sh new file mode 100644 index 0000000..140f743 --- /dev/null +++ b/scripts/apply_diffs.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -e + +DIFFS_DIR="ldap-overleaf-sl/sharelatex_diff" +ORI_DIR="ldap-overleaf-sl/sharelatex_ori" +PATCHED_DIR="ldap-overleaf-sl/sharelatex" + +for diff_file in "$DIFFS_DIR"/*.diff; do + filename=$(basename "$diff_file" ".diff") + if [ "$filename" == ".gitkeep" ]; then + continue + fi + + original_file="$ORI_DIR/$filename" + patched_file="$PATCHED_DIR/$filename" + + if [ -f "$original_file" ]; then + cp "$original_file" "$patched_file" + patch "$patched_file" "$diff_file" + else + echo "No original file for $filename in $ORI_DIR." + fi +done diff --git a/scripts/extract_files.sh b/scripts/extract_files.sh new file mode 100644 index 0000000..d7062b2 --- /dev/null +++ b/scripts/extract_files.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +set -e + +CONTAINER_FILE_PATHS=( + "/overleaf/services/web/app/src/Features/Authentication/AuthenticationManager.js" + "/overleaf/services/web/app/src/Features/Authentication/AuthenticationController.js" + "/overleaf/services/web/app/src/Features/Contacts/ContactController.js" + "/overleaf/services/web/app/src/router.js" + "/overleaf/services/web/app/views/user/settings.pug" + "/overleaf/services/web/app/views/user/login.pug" + "/overleaf/services/web/app/views/layout/navbar.pug" + "/overleaf/services/web/app/views/admin/index.pug" + "/overleaf/services/web/app/views/admin/index.pug" +) + +FILENAMES=( + "AuthenticationManager.js" + "AuthenticationController.js" + "ContactController.js" + "router.js" + "settings.pug" + "login.pug" + "navbar.pug" + "admin-index.pug" + "admin-sysadmin.pug" +) + +if [ "${#CONTAINER_FILE_PATHS[@]}" -ne "${#FILENAMES[@]}" ]; then + echo "Error: The number of source files and target filenames does not match." + exit 1 +fi + +HOST_TARGET_PATH="ldap-overleaf-sl/sharelatex_ori" + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 [version]" + exit 1 +else + VERSION=$1 +fi + +CONTAINER_NAME="tmp_sharelatex_for_extract_files" +IMAGE="sharelatex/sharelatex:$VERSION" + +echo "Starting Docker container \"$CONTAINER_NAME\" with image \"$IMAGE\"..." +if [ ! "$(docker ps -q -f name=^/${CONTAINER_NAME}$)" ]; then + if [ "$(docker ps -aq -f status=exited -f name=^/${CONTAINER_NAME}$)" ]; then + echo "Removing stopped container with same name..." + docker rm $CONTAINER_NAME + fi +else + echo "Error: A container with the name $CONTAINER_NAME already exists." + exit 1 +fi +docker run -d --name $CONTAINER_NAME $IMAGE + +echo "Waiting for container to start up..." +sleep 10 + +for i in "${!CONTAINER_FILE_PATHS[@]}"; do + file_path="${CONTAINER_FILE_PATHS[i]}" + new_filename="${FILENAMES[i]}" + new_target_path="$HOST_TARGET_PATH/$new_filename" + docker cp $CONTAINER_NAME:$file_path $new_target_path +done + +echo "Stopping and removing container..." +docker stop $CONTAINER_NAME +docker rm $CONTAINER_NAME diff --git a/scripts/make_diffs.sh b/scripts/make_diffs.sh new file mode 100644 index 0000000..d939a29 --- /dev/null +++ b/scripts/make_diffs.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +MODIFIED_DIR="ldap-overleaf-sl/sharelatex" +DIFFS_DIR="ldap-overleaf-sl/sharelatex_diff" +ORI_DIR="ldap-overleaf-sl/sharelatex_ori" + +for filename in $(ls $MODIFIED_DIR); do + raw_file="$ORI_DIR/$filename" + + if [ -f "$raw_file" ]; then + diff_output="$DIFFS_DIR/${filename}.diff" + diff "$raw_file" "$MODIFIED_DIR/$filename" > "$diff_output" + else + echo "No matching file for $filename in $ORI_DIR." + fi +done