Просмотр исходного кода

Merge pull request #4112 from overleaf/tm-private-api-basic-auth

Add requireBasicAuth middleware and refactor httpAuth to use it

GitOrigin-RevId: 7f68c0dc4a40102bfe4a97711def517e465ec7fd
Jakob Ackermann 5 лет назад
Родитель
Сommit
95c83866c5

+ 1 - 1
services/web/app/src/Features/Analytics/AnalyticsRouter.js

@@ -16,7 +16,7 @@ module.exports = {
 
 
     publicApiRouter.use(
     publicApiRouter.use(
       '/analytics/uniExternalCollaboration',
       '/analytics/uniExternalCollaboration',
-      AuthenticationController.httpAuth,
+      AuthenticationController.requirePrivateApiAuth(),
       AnalyticsProxy.call('/uniExternalCollaboration')
       AnalyticsProxy.call('/uniExternalCollaboration')
     )
     )
   },
   },

+ 18 - 12
services/web/app/src/Features/Authentication/AuthenticationController.js

@@ -316,7 +316,7 @@ const AuthenticationController = {
     }
     }
 
 
     if (req.headers.authorization != null) {
     if (req.headers.authorization != null) {
-      AuthenticationController.httpAuth(req, res, next)
+      AuthenticationController.requirePrivateApiAuth()(req, res, next)
     } else if (AuthenticationController.isUserLoggedIn(req)) {
     } else if (AuthenticationController.isUserLoggedIn(req)) {
       next()
       next()
     } else {
     } else {
@@ -361,17 +361,23 @@ const AuthenticationController = {
     return next()
     return next()
   },
   },
 
 
-  httpAuth: basicAuth(function (user, pass) {
-    const expectedPassword = Settings.httpAuthUsers[user]
-    const isValid =
-      expectedPassword &&
-      expectedPassword.length === pass.length &&
-      crypto.timingSafeEqual(Buffer.from(expectedPassword), Buffer.from(pass))
-    if (!isValid) {
-      logger.err({ user, pass }, 'invalid login details')
-    }
-    return isValid
-  }),
+  requireBasicAuth: function (userDetails) {
+    return basicAuth(function (user, pass) {
+      const expectedPassword = userDetails[user]
+      const isValid =
+        expectedPassword &&
+        expectedPassword.length === pass.length &&
+        crypto.timingSafeEqual(Buffer.from(expectedPassword), Buffer.from(pass))
+      if (!isValid) {
+        logger.err({ user, pass }, 'invalid login details')
+      }
+      return isValid
+    })
+  },
+
+  requirePrivateApiAuth() {
+    return AuthenticationController.requireBasicAuth(Settings.httpAuthUsers)
+  },
 
 
   setRedirectInSession(req, value) {
   setRedirectInSession(req, value) {
     if (value == null) {
     if (value == null) {

+ 2 - 2
services/web/app/src/Features/Editor/EditorRouter.js

@@ -57,7 +57,7 @@ module.exports = {
     )
     )
     apiRouter.post(
     apiRouter.post(
       '/project/:Project_id/doc/:entity_id/convert-to-file',
       '/project/:Project_id/doc/:entity_id/convert-to-file',
-      AuthenticationController.httpAuth,
+      AuthenticationController.requirePrivateApiAuth(),
       validate({
       validate({
         body: Joi.object({
         body: Joi.object({
           userId: Joi.objectId().required(),
           userId: Joi.objectId().required(),
@@ -71,7 +71,7 @@ module.exports = {
     // whenever a user joins a project, like updating the deleted status.
     // whenever a user joins a project, like updating the deleted status.
     apiRouter.post(
     apiRouter.post(
       '/project/:Project_id/join',
       '/project/:Project_id/join',
-      AuthenticationController.httpAuth,
+      AuthenticationController.requirePrivateApiAuth(),
       RateLimiterMiddleware.rateLimit({
       RateLimiterMiddleware.rateLimit({
         endpointName: 'join-project',
         endpointName: 'join-project',
         params: ['Project_id'],
         params: ['Project_id'],

+ 1 - 1
services/web/app/src/Features/Subscription/SubscriptionRouter.js

@@ -136,7 +136,7 @@ module.exports = {
     // Currently used in acceptance tests only, as a way to trigger the syncing logic
     // Currently used in acceptance tests only, as a way to trigger the syncing logic
     return publicApiRouter.post(
     return publicApiRouter.post(
       '/user/:user_id/features/sync',
       '/user/:user_id/features/sync',
-      AuthenticationController.httpAuth,
+      AuthenticationController.requirePrivateApiAuth(),
       SubscriptionController.refreshUserFeatures
       SubscriptionController.refreshUserFeatures
     )
     )
   },
   },

+ 23 - 23
services/web/app/src/router.js

@@ -231,7 +231,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
   )
   )
   privateApiRouter.get(
   privateApiRouter.get(
     '/user/:user_id/personal_info',
     '/user/:user_id/personal_info',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     UserInfoController.getPersonalInfo
     UserInfoController.getPersonalInfo
   )
   )
 
 
@@ -564,7 +564,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
   )
   )
   privateApiRouter.post(
   privateApiRouter.post(
     '/project/:Project_id/history/resync',
     '/project/:Project_id/history/resync',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     HistoryController.resyncProjectHistory
     HistoryController.resyncProjectHistory
   )
   )
 
 
@@ -639,28 +639,28 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
   )
   )
   privateApiRouter.post(
   privateApiRouter.post(
     '/internal/expire-deleted-projects-after-duration',
     '/internal/expire-deleted-projects-after-duration',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     ProjectController.expireDeletedProjectsAfterDuration
     ProjectController.expireDeletedProjectsAfterDuration
   )
   )
   privateApiRouter.post(
   privateApiRouter.post(
     '/internal/expire-deleted-users-after-duration',
     '/internal/expire-deleted-users-after-duration',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     UserController.expireDeletedUsersAfterDuration
     UserController.expireDeletedUsersAfterDuration
   )
   )
   privateApiRouter.post(
   privateApiRouter.post(
     '/internal/project/:projectId/expire-deleted-project',
     '/internal/project/:projectId/expire-deleted-project',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     ProjectController.expireDeletedProject
     ProjectController.expireDeletedProject
   )
   )
   privateApiRouter.post(
   privateApiRouter.post(
     '/internal/users/:userId/expire',
     '/internal/users/:userId/expire',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     UserController.expireDeletedUser
     UserController.expireDeletedUser
   )
   )
 
 
   privateApiRouter.get(
   privateApiRouter.get(
     '/user/:userId/tag',
     '/user/:userId/tag',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     TagsController.apiGetAllTags
     TagsController.apiGetAllTags
   )
   )
   webRouter.get(
   webRouter.get(
@@ -733,35 +733,35 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
   // Deprecated in favour of /internal/project/:project_id but still used by versioning
   // Deprecated in favour of /internal/project/:project_id but still used by versioning
   privateApiRouter.get(
   privateApiRouter.get(
     '/project/:project_id/details',
     '/project/:project_id/details',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     ProjectApiController.getProjectDetails
     ProjectApiController.getProjectDetails
   )
   )
 
 
   // New 'stable' /internal API end points
   // New 'stable' /internal API end points
   privateApiRouter.get(
   privateApiRouter.get(
     '/internal/project/:project_id',
     '/internal/project/:project_id',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     ProjectApiController.getProjectDetails
     ProjectApiController.getProjectDetails
   )
   )
   privateApiRouter.get(
   privateApiRouter.get(
     '/internal/project/:Project_id/zip',
     '/internal/project/:Project_id/zip',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     ProjectDownloadsController.downloadProject
     ProjectDownloadsController.downloadProject
   )
   )
   privateApiRouter.get(
   privateApiRouter.get(
     '/internal/project/:project_id/compile/pdf',
     '/internal/project/:project_id/compile/pdf',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     CompileController.compileAndDownloadPdf
     CompileController.compileAndDownloadPdf
   )
   )
 
 
   privateApiRouter.post(
   privateApiRouter.post(
     '/internal/deactivateOldProjects',
     '/internal/deactivateOldProjects',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     InactiveProjectController.deactivateOldProjects
     InactiveProjectController.deactivateOldProjects
   )
   )
   privateApiRouter.post(
   privateApiRouter.post(
     '/internal/project/:project_id/deactivate',
     '/internal/project/:project_id/deactivate',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     InactiveProjectController.deactivateProject
     InactiveProjectController.deactivateProject
   )
   )
 
 
@@ -775,40 +775,40 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
       req.params = params
       req.params = params
       next()
       next()
     },
     },
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     CompileController.getFileFromClsi
     CompileController.getFileFromClsi
   )
   )
 
 
   privateApiRouter.get(
   privateApiRouter.get(
     '/project/:Project_id/doc/:doc_id',
     '/project/:Project_id/doc/:doc_id',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     DocumentController.getDocument
     DocumentController.getDocument
   )
   )
   privateApiRouter.post(
   privateApiRouter.post(
     '/project/:Project_id/doc/:doc_id',
     '/project/:Project_id/doc/:doc_id',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     DocumentController.setDocument
     DocumentController.setDocument
   )
   )
 
 
   privateApiRouter.post(
   privateApiRouter.post(
     '/user/:user_id/update/*',
     '/user/:user_id/update/*',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     TpdsController.mergeUpdate
     TpdsController.mergeUpdate
   )
   )
   privateApiRouter.delete(
   privateApiRouter.delete(
     '/user/:user_id/update/*',
     '/user/:user_id/update/*',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     TpdsController.deleteUpdate
     TpdsController.deleteUpdate
   )
   )
 
 
   privateApiRouter.post(
   privateApiRouter.post(
     '/project/:project_id/contents/*',
     '/project/:project_id/contents/*',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     TpdsController.updateProjectContents
     TpdsController.updateProjectContents
   )
   )
   privateApiRouter.delete(
   privateApiRouter.delete(
     '/project/:project_id/contents/*',
     '/project/:project_id/contents/*',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     TpdsController.deleteProjectContents
     TpdsController.deleteProjectContents
   )
   )
 
 
@@ -884,7 +884,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
   // overleaf-integration module), but may expand beyond that role.
   // overleaf-integration module), but may expand beyond that role.
   publicApiRouter.post(
   publicApiRouter.post(
     '/api/clsi/compile/:submission_id',
     '/api/clsi/compile/:submission_id',
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     CompileController.compileSubmission
     CompileController.compileSubmission
   )
   )
   publicApiRouter.get(
   publicApiRouter.get(
@@ -898,7 +898,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
       req.params = params
       req.params = params
       next()
       next()
     },
     },
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     CompileController.getFileFromClsiWithoutUser
     CompileController.getFileFromClsiWithoutUser
   )
   )
   publicApiRouter.post(
   publicApiRouter.post(
@@ -908,7 +908,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
       maxRequests: 1,
       maxRequests: 1,
       timeInterval: 60,
       timeInterval: 60,
     }),
     }),
-    AuthenticationController.httpAuth,
+    AuthenticationController.requirePrivateApiAuth(),
     InstitutionsController.confirmDomain
     InstitutionsController.confirmDomain
   )
   )
 
 

+ 36 - 11
services/web/test/unit/src/Authentication/AuthenticationControllerTests.js

@@ -630,7 +630,10 @@ describe('AuthenticationController', function () {
   describe('requireGlobalLogin', function () {
   describe('requireGlobalLogin', function () {
     beforeEach(function () {
     beforeEach(function () {
       this.req.headers = {}
       this.req.headers = {}
-      this.AuthenticationController.httpAuth = sinon.stub()
+      this.middleware = sinon.stub()
+      this.AuthenticationController.requirePrivateApiAuth = sinon
+        .stub()
+        .returns(this.middleware)
       this.setRedirect = sinon.spy(
       this.setRedirect = sinon.spy(
         this.AuthenticationController,
         this.AuthenticationController,
         'setRedirectInSession'
         'setRedirectInSession'
@@ -684,8 +687,8 @@ describe('AuthenticationController', function () {
         )
         )
       })
       })
 
 
-      it('should pass the request onto httpAuth', function () {
-        this.AuthenticationController.httpAuth
+      it('should pass the request onto requirePrivateApiAuth middleware', function () {
+        this.middleware
           .calledWith(this.req, this.res, this.next)
           .calledWith(this.req, this.res, this.next)
           .should.equal(true)
           .should.equal(true)
       })
       })
@@ -726,7 +729,16 @@ describe('AuthenticationController', function () {
     })
     })
   })
   })
 
 
-  describe('httpAuth', function () {
+  describe('requireBasicAuth', function () {
+    beforeEach(function () {
+      this.basicAuthUsers = {
+        'basic-auth-user': 'basic-auth-password',
+      }
+      this.middleware = this.AuthenticationController.requireBasicAuth(
+        this.basicAuthUsers
+      )
+    })
+
     describe('with http auth', function () {
     describe('with http auth', function () {
       it('should error with incorrect user', function (done) {
       it('should error with incorrect user', function (done) {
         this.req.headers = {
         this.req.headers = {
@@ -736,12 +748,12 @@ describe('AuthenticationController', function () {
           expect(status).to.equal('Unauthorized')
           expect(status).to.equal('Unauthorized')
           done()
           done()
         }
         }
-        this.AuthenticationController.httpAuth(this.req, this.req)
+        this.middleware(this.req, this.req)
       })
       })
 
 
       it('should error with incorrect password', function (done) {
       it('should error with incorrect password', function (done) {
         this.req.headers = {
         this.req.headers = {
-          authorization: `Basic ${Buffer.from('valid-test-user:nope').toString(
+          authorization: `Basic ${Buffer.from('basic-auth-user:nope').toString(
             'base64'
             'base64'
           )}`,
           )}`,
         }
         }
@@ -749,12 +761,12 @@ describe('AuthenticationController', function () {
           expect(status).to.equal('Unauthorized')
           expect(status).to.equal('Unauthorized')
           done()
           done()
         }
         }
-        this.AuthenticationController.httpAuth(this.req, this.req)
+        this.middleware(this.req, this.req)
       })
       })
 
 
       it('should fail with empty pass', function (done) {
       it('should fail with empty pass', function (done) {
         this.req.headers = {
         this.req.headers = {
-          authorization: `Basic ${Buffer.from(`invalid-test-user:`).toString(
+          authorization: `Basic ${Buffer.from(`basic-auth-user:`).toString(
             'base64'
             'base64'
           )}`,
           )}`,
         }
         }
@@ -762,20 +774,33 @@ describe('AuthenticationController', function () {
           expect(status).to.equal('Unauthorized')
           expect(status).to.equal('Unauthorized')
           done()
           done()
         }
         }
-        this.AuthenticationController.httpAuth(this.req, this.req)
+        this.middleware(this.req, this.req)
       })
       })
 
 
       it('should succeed with correct user/pass', function (done) {
       it('should succeed with correct user/pass', function (done) {
         this.req.headers = {
         this.req.headers = {
           authorization: `Basic ${Buffer.from(
           authorization: `Basic ${Buffer.from(
-            `valid-test-user:${this.httpAuthUsers['valid-test-user']}`
+            `basic-auth-user:${this.basicAuthUsers['basic-auth-user']}`
           ).toString('base64')}`,
           ).toString('base64')}`,
         }
         }
-        this.AuthenticationController.httpAuth(this.req, this.res, done)
+        this.middleware(this.req, this.res, done)
       })
       })
     })
     })
   })
   })
 
 
+  describe('requirePrivateApiAuth', function () {
+    beforeEach(function () {
+      this.AuthenticationController.requireBasicAuth = sinon.stub()
+    })
+
+    it('should call requireBasicAuth with the private API user details', function () {
+      this.AuthenticationController.requirePrivateApiAuth()
+      this.AuthenticationController.requireBasicAuth
+        .calledWith(this.httpAuthUsers)
+        .should.equal(true)
+    })
+  })
+
   describe('_redirectToLoginOrRegisterPage', function () {
   describe('_redirectToLoginOrRegisterPage', function () {
     beforeEach(function () {
     beforeEach(function () {
       this.middleware = this.AuthenticationController.requireLogin(
       this.middleware = this.AuthenticationController.requireLogin(