From 029c96c7ccf720a7e393f160b259874553487583 Mon Sep 17 00:00:00 2001 From: Shane Kilkelly Date: Wed, 10 May 2017 10:05:48 +0100 Subject: [PATCH] Add sudo-mode 'confirm password' prompt --- .../SudoMode/SudoModeController.coffee | 58 +++++++++++++++++++ .../Features/SudoMode/SudoModeHandler.coffee | 27 +++++++++ services/web/app/coffee/router.coffee | 4 ++ .../app/views/sudo_mode/sudo_mode_prompt.pug | 39 +++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 services/web/app/coffee/Features/SudoMode/SudoModeController.coffee create mode 100644 services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee create mode 100644 services/web/app/views/sudo_mode/sudo_mode_prompt.pug diff --git a/services/web/app/coffee/Features/SudoMode/SudoModeController.coffee b/services/web/app/coffee/Features/SudoMode/SudoModeController.coffee new file mode 100644 index 0000000000..f569d3476e --- /dev/null +++ b/services/web/app/coffee/Features/SudoMode/SudoModeController.coffee @@ -0,0 +1,58 @@ +UserLocator = require "../User/UserLocator" +Settings = require "settings-sharelatex" +logger = require 'logger-sharelatex' +SudoModeHandler = require './SudoModeHandler' +AuthenticationController = require '../Authentication/AuthenticationController' +AuthenticationManager = require '../Authentication/AuthenticationManager' +ObjectId = require('../../infrastructure/Mongoose').mongo.ObjectId +UserGetter = require '../User/UserGetter' + + +module.exports = SudoModeController = + + sudoModePrompt: (req, res, next) -> + userId = AuthenticationController.getLoggedInUserId(req) + logger.log {userId}, "[SudoMode] rendering sudo mode password page" + SudoModeHandler.isSudoModeActive userId, (err, isActive) -> + if err? + logger.err {err, userId}, "[SudoMode] error checking if sudo mode is active" + return next(err) + if isActive + logger.log {userId}, "[SudoMode] sudo mode already active, redirecting" + return res.redirect('/project') + res.render 'sudo_mode/sudo_mode_prompt', title: 'confirm_your_password' + + submitPassword: (req, res, next) -> + userId = AuthenticationController.getLoggedInUserId(req) + redir = AuthenticationController._getRedirectFromSession(req) || "/project" + password = req.body.password + if !password + logger.log {userId}, "[SudoMode] no password supplied, failed authentication" + return next(new Error('no password supplied')) + logger.log {userId, redir}, "[SudoMode] checking user password" + UserGetter.getUser ObjectId(userId), {email: 1}, (err, userRecord) -> + if err? + logger.err {err, userId}, "[SudoMode] error getting user" + return next(err) + AuthenticationManager.authenticate email: userRecord.email, password, (err, user) -> + if err? + logger.err {err, userId}, "[SudoMode] error authenticating user" + return next(err) + if user? + logger.log {userId}, "[SudoMode] authenticated user, activating sudo mode" + SudoModeHandler.activateSudoMode userId, (err) -> + if err? + logger.err {err, userId}, "[SudoMode] error activating sudo mode" + return next(err) + return res.json { + redir: redir + } + else + logger.log {userId}, "[SudoMode] authentication failed for user" + return res.json { + message: { + text: req.i18n.translate("invalid_password"), + type: 'error' + } + } + diff --git a/services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee b/services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee new file mode 100644 index 0000000000..398a39048c --- /dev/null +++ b/services/web/app/coffee/Features/SudoMode/SudoModeHandler.coffee @@ -0,0 +1,27 @@ +RedisWrapper = require('../../infrastructure/RedisWrapper') +rclient = RedisWrapper.client('sudomode') +logger = require('logger-sharelatex') + + +TIMEOUT_IN_SECONDS = 60 * 10 + + +module.exports = SudoModeHandler = + + _buildKey: (userId) -> + "SudoMode:{#{userId}}" + + activateSudoMode: (userId, callback=(err)->) -> + if !userId? + return callback(new Error('[SudoMode] user must not be supplied')) + duration = TIMEOUT_IN_SECONDS + logger.log {userId, duration}, "[SudoMode] activating sudo mode for user" + rclient.set SudoModeHandler._buildKey(userId), '1', 'EX', duration, callback + + isSudoModeActive: (userId, callback=(err, isActive)->) -> + if !userId? + return callback(new Error('[SudoMode] user must not be supplied')) + rclient.get SudoModeHandler._buildKey(userId), (err, result) -> + if err? + return callback(err) + callback(null, result == '1') diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee index 0556c23c5a..ff5cd4b64c 100644 --- a/services/web/app/coffee/router.coffee +++ b/services/web/app/coffee/router.coffee @@ -38,6 +38,7 @@ ContactRouter = require("./Features/Contacts/ContactRouter") ReferencesController = require('./Features/References/ReferencesController') AuthorizationMiddlewear = require('./Features/Authorization/AuthorizationMiddlewear') BetaProgramController = require('./Features/BetaProgram/BetaProgramController') +SudoModeController = require('./Features/SudoMode/SudoModeController') AnalyticsRouter = require('./Features/Analytics/AnalyticsRouter') AnnouncementsController = require("./Features/Announcements/AnnouncementsController") @@ -238,6 +239,9 @@ module.exports = class Router 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 + webRouter.get "/confirm-password", AuthenticationController.requireLogin(), SudoModeController.sudoModePrompt + webRouter.post "/confirm-password/submit", AuthenticationController.requireLogin(), SudoModeController.submitPassword + #Admin Stuff webRouter.get '/admin', AuthorizationMiddlewear.ensureUserIsSiteAdmin, AdminController.index diff --git a/services/web/app/views/sudo_mode/sudo_mode_prompt.pug b/services/web/app/views/sudo_mode/sudo_mode_prompt.pug new file mode 100644 index 0000000000..3b089517c1 --- /dev/null +++ b/services/web/app/views/sudo_mode/sudo_mode_prompt.pug @@ -0,0 +1,39 @@ +extends ../layout + +block content + .content.content-alt + .container + .row + .col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 + .card + .page-header.text-centered + h1 Confirm your password to continue + div + .container-fluid + .row + .col-md-8.col-md-offset-2 + form(async-form="confirmPassword", name="confirmPassword", + action='/confirm-password/submit', method="POST", ng-cloak) + input(name='_csrf', type='hidden', value=csrfToken) + form-messages(for="confirmPassword") + .form-group + label + | #{translate('password')} + input.form-control( + type='password', + name='password', + required, + placeholder='********', + ng-model="password" + ) + span.small.text-primary( + ng-show="confirmPassword.password.$invalid && confirmPassword.password.$dirty" + ) + | #{translate("required")} + .actions + button.btn-primary.btn( + style="width: 100%", + type='submit', + ng-disabled="confirmPassword.inflight" + ) + span #{translate("confirm")}