mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 22:23:42 +01:00
Add request validation to HistoryController endpoints (#35730)
* Add schemas for file restoration and label management in HistoryController * Add request schemas for proxying to history API and resyncing project history * prevent transforming version:null into version:0 with coerce * move jsDoc comments above the handler * check for undefined resyncProjectStructureOnly * Add tests for restoreFileFromV2 and revertFile * Set default value for resyncProjectStructureOnly in resyncProjectHistorySchema GitOrigin-RevId: 7b8b8a4c3834b8c8a83f93052ab1f59e86c529a6
This commit is contained in:
committed by
Copybot
parent
c821cc695c
commit
d8de261370
@@ -119,7 +119,17 @@ describe('HistoryController', function () {
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/History/RestoreManager.mjs',
|
||||
() => ({
|
||||
default: (ctx.RestoreManager = {}),
|
||||
default: (ctx.RestoreManager = {
|
||||
promises: {
|
||||
restoreFileFromV2: sinon
|
||||
.stub()
|
||||
.resolves({ _id: 'restored-id', type: 'doc' }),
|
||||
revertFile: sinon
|
||||
.stub()
|
||||
.resolves({ _id: 'reverted-id', type: 'doc' }),
|
||||
revertProject: sinon.stub().resolves([]),
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
@@ -137,7 +147,12 @@ describe('HistoryController', function () {
|
||||
|
||||
describe('proxyToHistoryApi', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.req = { url: '/mock/url', method: 'POST', session: sinon.stub() }
|
||||
ctx.req = {
|
||||
url: '/mock/url',
|
||||
method: 'POST',
|
||||
session: sinon.stub(),
|
||||
params: {},
|
||||
}
|
||||
ctx.res = {
|
||||
set: sinon.stub(),
|
||||
}
|
||||
@@ -188,7 +203,11 @@ describe('HistoryController', function () {
|
||||
|
||||
describe('proxyToHistoryApiAndInjectUserDetails', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.req = { url: '/mock/url', method: 'POST' }
|
||||
ctx.req = {
|
||||
url: '/mock/url',
|
||||
method: 'POST',
|
||||
params: { Project_id: ctx.project_id },
|
||||
}
|
||||
ctx.res = { json: sinon.stub() }
|
||||
ctx.data = 'mock-data'
|
||||
ctx.dataWithUsers = 'mock-injected-data'
|
||||
@@ -233,7 +252,11 @@ describe('HistoryController', function () {
|
||||
describe('proxyToHistoryApiAndInjectUserDetails (with the history API failing)', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
ctx.url = '/mock/url'
|
||||
ctx.req = { url: ctx.url, method: 'POST' }
|
||||
ctx.req = {
|
||||
url: ctx.url,
|
||||
method: 'POST',
|
||||
params: { Project_id: ctx.project_id },
|
||||
}
|
||||
ctx.res = { json: sinon.stub() }
|
||||
ctx.err = new RequestFailedError(ctx.url, {}, { status: 500 })
|
||||
ctx.fetchJson.rejects(ctx.err)
|
||||
@@ -308,6 +331,165 @@ describe('HistoryController', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('restoreFileFromV2', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.res = { json: sinon.stub() }
|
||||
ctx.ProjectAuditLogHandler.addEntryIfManagedInBackground = sinon.stub()
|
||||
})
|
||||
|
||||
it('restores the file for a valid request', async function (ctx) {
|
||||
ctx.req = {
|
||||
params: { project_id: ctx.project_id },
|
||||
body: { version: 42, pathname: 'foo/bar.tex' },
|
||||
session: {},
|
||||
ip: '1.2.3.4',
|
||||
}
|
||||
await ctx.HistoryController.restoreFileFromV2(ctx.req, ctx.res, ctx.next)
|
||||
ctx.RestoreManager.promises.restoreFileFromV2.should.have.been.calledWith(
|
||||
ctx.user_id,
|
||||
ctx.project_id,
|
||||
42,
|
||||
'foo/bar.tex'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects a non-numeric version without invoking RestoreManager', async function (ctx) {
|
||||
ctx.req = {
|
||||
params: { project_id: ctx.project_id },
|
||||
body: { version: 'not-a-number', pathname: 'foo/bar.tex' },
|
||||
session: {},
|
||||
ip: '1.2.3.4',
|
||||
}
|
||||
await ctx.HistoryController.restoreFileFromV2(ctx.req, ctx.res, ctx.next)
|
||||
ctx.next.should.have.been.calledOnce
|
||||
ctx.next.firstCall.args[0].should.be.an.instanceof(Error)
|
||||
ctx.RestoreManager.promises.restoreFileFromV2.should.not.have.been.called
|
||||
})
|
||||
|
||||
it('rejects a negative version without invoking RestoreManager', async function (ctx) {
|
||||
ctx.req = {
|
||||
params: { project_id: ctx.project_id },
|
||||
body: { version: -1, pathname: 'foo/bar.tex' },
|
||||
session: {},
|
||||
ip: '1.2.3.4',
|
||||
}
|
||||
await ctx.HistoryController.restoreFileFromV2(ctx.req, ctx.res, ctx.next)
|
||||
ctx.next.should.have.been.calledOnce
|
||||
ctx.next.firstCall.args[0].should.be.an.instanceof(Error)
|
||||
ctx.RestoreManager.promises.restoreFileFromV2.should.not.have.been.called
|
||||
})
|
||||
|
||||
it('rejects a pathname containing .. without invoking RestoreManager', async function (ctx) {
|
||||
ctx.req = {
|
||||
params: { project_id: ctx.project_id },
|
||||
body: { version: 42, pathname: '../../etc/passwd' },
|
||||
session: {},
|
||||
ip: '1.2.3.4',
|
||||
}
|
||||
await ctx.HistoryController.restoreFileFromV2(ctx.req, ctx.res, ctx.next)
|
||||
ctx.next.should.have.been.calledOnce
|
||||
ctx.next.firstCall.args[0].should.be.an.instanceof(Error)
|
||||
ctx.RestoreManager.promises.restoreFileFromV2.should.not.have.been.called
|
||||
})
|
||||
|
||||
it('rejects an absolute pathname without invoking RestoreManager', async function (ctx) {
|
||||
ctx.req = {
|
||||
params: { project_id: ctx.project_id },
|
||||
body: { version: 42, pathname: '/etc/passwd' },
|
||||
session: {},
|
||||
ip: '1.2.3.4',
|
||||
}
|
||||
await ctx.HistoryController.restoreFileFromV2(ctx.req, ctx.res, ctx.next)
|
||||
ctx.next.should.have.been.calledOnce
|
||||
ctx.next.firstCall.args[0].should.be.an.instanceof(Error)
|
||||
ctx.RestoreManager.promises.restoreFileFromV2.should.not.have.been.called
|
||||
})
|
||||
|
||||
it('rejects a path-traversal string as version without invoking RestoreManager', async function (ctx) {
|
||||
ctx.req = {
|
||||
params: { project_id: ctx.project_id },
|
||||
body: { version: '0/../../../ObjectId', pathname: 'foo/bar.tex' },
|
||||
session: {},
|
||||
ip: '1.2.3.4',
|
||||
}
|
||||
await ctx.HistoryController.restoreFileFromV2(ctx.req, ctx.res, ctx.next)
|
||||
ctx.next.should.have.been.calledOnce
|
||||
ctx.next.firstCall.args[0].should.be.an.instanceof(Error)
|
||||
ctx.RestoreManager.promises.restoreFileFromV2.should.not.have.been.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('revertFile', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.res = { json: sinon.stub() }
|
||||
ctx.ProjectAuditLogHandler.addEntryIfManagedInBackground = sinon.stub()
|
||||
})
|
||||
|
||||
it('reverts the file for a valid request', async function (ctx) {
|
||||
ctx.req = {
|
||||
params: { project_id: ctx.project_id },
|
||||
body: { version: 42, pathname: 'foo/bar.tex' },
|
||||
session: {},
|
||||
ip: '1.2.3.4',
|
||||
}
|
||||
await ctx.HistoryController.revertFile(ctx.req, ctx.res, ctx.next)
|
||||
ctx.RestoreManager.promises.revertFile.should.have.been.calledWith(
|
||||
ctx.user_id,
|
||||
ctx.project_id,
|
||||
42,
|
||||
'foo/bar.tex'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects a malformed pathname without invoking RestoreManager', async function (ctx) {
|
||||
ctx.req = {
|
||||
params: { project_id: ctx.project_id },
|
||||
body: { version: 42, pathname: '../secret.tex' },
|
||||
session: {},
|
||||
ip: '1.2.3.4',
|
||||
}
|
||||
await ctx.HistoryController.revertFile(ctx.req, ctx.res, ctx.next)
|
||||
ctx.next.should.have.been.calledOnce
|
||||
ctx.next.firstCall.args[0].should.be.an.instanceof(Error)
|
||||
ctx.RestoreManager.promises.revertFile.should.not.have.been.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('revertProject', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.res = { json: sinon.stub() }
|
||||
ctx.ProjectAuditLogHandler.addEntryIfManagedInBackground = sinon.stub()
|
||||
})
|
||||
|
||||
it('reverts the project for a valid request', async function (ctx) {
|
||||
ctx.req = {
|
||||
params: { project_id: ctx.project_id },
|
||||
body: { version: 42 },
|
||||
session: {},
|
||||
ip: '1.2.3.4',
|
||||
}
|
||||
await ctx.HistoryController.revertProject(ctx.req, ctx.res, ctx.next)
|
||||
ctx.RestoreManager.promises.revertProject.should.have.been.calledWith(
|
||||
ctx.user_id,
|
||||
ctx.project_id,
|
||||
42
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects a non-integer version without invoking RestoreManager', async function (ctx) {
|
||||
ctx.req = {
|
||||
params: { project_id: ctx.project_id },
|
||||
body: { version: 1.5 },
|
||||
session: {},
|
||||
ip: '1.2.3.4',
|
||||
}
|
||||
await ctx.HistoryController.revertProject(ctx.req, ctx.res, ctx.next)
|
||||
ctx.next.should.have.been.calledOnce
|
||||
ctx.next.firstCall.args[0].should.be.an.instanceof(Error)
|
||||
ctx.RestoreManager.promises.revertProject.should.not.have.been.called
|
||||
})
|
||||
})
|
||||
|
||||
describe('requestBlob', function () {
|
||||
describe('With Range header', function () {
|
||||
beforeEach(async function (ctx) {
|
||||
|
||||
Reference in New Issue
Block a user