mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #8571 from overleaf/ta-token-access-page
Require User Interaction on Token Access Page GitOrigin-RevId: 2f4c00ba75ebd6bd87d3e770ec8223d736344f5b
This commit is contained in:
parent
0e6974d9e1
commit
7f722a006c
7 changed files with 208 additions and 49 deletions
|
@ -69,7 +69,7 @@ async function getPrivilegeLevelForProject(
|
||||||
opts = {}
|
opts = {}
|
||||||
) {
|
) {
|
||||||
if (userId) {
|
if (userId) {
|
||||||
return getPrivilegeLevelForProjectWithUser(userId, projectId, token, opts)
|
return getPrivilegeLevelForProjectWithUser(userId, projectId, opts)
|
||||||
} else {
|
} else {
|
||||||
return getPrivilegeLevelForProjectWithoutUser(projectId, token, opts)
|
return getPrivilegeLevelForProjectWithoutUser(projectId, token, opts)
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,6 @@ async function getPrivilegeLevelForProject(
|
||||||
async function getPrivilegeLevelForProjectWithUser(
|
async function getPrivilegeLevelForProjectWithUser(
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId,
|
||||||
token,
|
|
||||||
opts = {}
|
opts = {}
|
||||||
) {
|
) {
|
||||||
const privilegeLevel =
|
const privilegeLevel =
|
||||||
|
|
|
@ -19,20 +19,17 @@ const orderedPrivilegeLevels = [
|
||||||
PrivilegeLevels.OWNER,
|
PrivilegeLevels.OWNER,
|
||||||
]
|
]
|
||||||
|
|
||||||
async function _userAlreadyHasHigherPrivilege(
|
async function _userAlreadyHasHigherPrivilege(userId, projectId, tokenType) {
|
||||||
userId,
|
|
||||||
projectId,
|
|
||||||
token,
|
|
||||||
tokenType
|
|
||||||
) {
|
|
||||||
if (!Object.values(TokenAccessHandler.TOKEN_TYPES).includes(tokenType)) {
|
if (!Object.values(TokenAccessHandler.TOKEN_TYPES).includes(tokenType)) {
|
||||||
throw new Error('bad token type')
|
throw new Error('bad token type')
|
||||||
}
|
}
|
||||||
|
if (!userId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
const privilegeLevel =
|
const privilegeLevel =
|
||||||
await AuthorizationManager.promises.getPrivilegeLevelForProject(
|
await AuthorizationManager.promises.getPrivilegeLevelForProject(
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId
|
||||||
token
|
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
orderedPrivilegeLevels.indexOf(privilegeLevel) >=
|
orderedPrivilegeLevels.indexOf(privilegeLevel) >=
|
||||||
|
@ -196,7 +193,6 @@ async function checkAndGetProjectOrResponseAction(
|
||||||
const userHasPrivilege = await _userAlreadyHasHigherPrivilege(
|
const userHasPrivilege = await _userAlreadyHasHigherPrivilege(
|
||||||
userId,
|
userId,
|
||||||
projectId,
|
projectId,
|
||||||
token,
|
|
||||||
tokenType
|
tokenType
|
||||||
)
|
)
|
||||||
if (userHasPrivilege) {
|
if (userHasPrivilege) {
|
||||||
|
@ -220,6 +216,7 @@ async function checkAndGetProjectOrResponseAction(
|
||||||
|
|
||||||
async function grantTokenAccessReadAndWrite(req, res, next) {
|
async function grantTokenAccessReadAndWrite(req, res, next) {
|
||||||
const { token } = req.params
|
const { token } = req.params
|
||||||
|
const { confirmedByUser } = req.body
|
||||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||||
if (!TokenAccessHandler.isReadAndWriteToken(token)) {
|
if (!TokenAccessHandler.isReadAndWriteToken(token)) {
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
|
@ -240,6 +237,13 @@ async function grantTokenAccessReadAndWrite(req, res, next) {
|
||||||
if (!project) {
|
if (!project) {
|
||||||
return next(new Errors.NotFoundError())
|
return next(new Errors.NotFoundError())
|
||||||
}
|
}
|
||||||
|
if (!confirmedByUser) {
|
||||||
|
return res.json({
|
||||||
|
requireAccept: {
|
||||||
|
projectName: project.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
await TokenAccessHandler.promises.addReadAndWriteUserToProject(
|
await TokenAccessHandler.promises.addReadAndWriteUserToProject(
|
||||||
userId,
|
userId,
|
||||||
project._id
|
project._id
|
||||||
|
@ -261,6 +265,7 @@ async function grantTokenAccessReadAndWrite(req, res, next) {
|
||||||
|
|
||||||
async function grantTokenAccessReadOnly(req, res, next) {
|
async function grantTokenAccessReadOnly(req, res, next) {
|
||||||
const { token } = req.params
|
const { token } = req.params
|
||||||
|
const { confirmedByUser } = req.body
|
||||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||||
if (!TokenAccessHandler.isReadOnlyToken(token)) {
|
if (!TokenAccessHandler.isReadOnlyToken(token)) {
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
|
@ -286,6 +291,13 @@ async function grantTokenAccessReadOnly(req, res, next) {
|
||||||
if (!project) {
|
if (!project) {
|
||||||
return next(new Errors.NotFoundError())
|
return next(new Errors.NotFoundError())
|
||||||
}
|
}
|
||||||
|
if (!confirmedByUser) {
|
||||||
|
return res.json({
|
||||||
|
requireAccept: {
|
||||||
|
projectName: project.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
await TokenAccessHandler.promises.addReadOnlyUserToProject(
|
await TokenAccessHandler.promises.addReadOnlyUserToProject(
|
||||||
userId,
|
userId,
|
||||||
project._id
|
project._id
|
||||||
|
|
|
@ -91,6 +91,31 @@ block content
|
||||||
a.btn.btn-primary(ng-href="{{ buildZipDownloadPath(v1ImportData.projectId) }}")
|
a.btn.btn-primary(ng-href="{{ buildZipDownloadPath(v1ImportData.projectId) }}")
|
||||||
| Download project zip file
|
| Download project zip file
|
||||||
|
|
||||||
|
.loading-screen(
|
||||||
|
ng-show="mode == 'requireAccept'"
|
||||||
|
)
|
||||||
|
.container
|
||||||
|
.row
|
||||||
|
.col-md-8.col-md-offset-2
|
||||||
|
.card
|
||||||
|
.page-header.text-centered
|
||||||
|
h1 #{translate("invited_to_join")}
|
||||||
|
br
|
||||||
|
em {{ getProjectName() }}
|
||||||
|
.row.text-center
|
||||||
|
.col-md-12
|
||||||
|
p
|
||||||
|
if user
|
||||||
|
| #{translate("accepting_invite_as")}
|
||||||
|
|
|
||||||
|
em #{user.email}
|
||||||
|
.row.text-center
|
||||||
|
.col-md-12
|
||||||
|
button.btn.btn-lg.btn-primary(
|
||||||
|
type='submit'
|
||||||
|
ng-click="postConfirmedByUser()"
|
||||||
|
) #{translate("join_project")}
|
||||||
|
|
||||||
|
|
||||||
block append foot-scripts
|
block append foot-scripts
|
||||||
script(type="text/javascript", nonce=scriptNonce).
|
script(type="text/javascript", nonce=scriptNonce).
|
||||||
|
|
|
@ -44,8 +44,8 @@ function formSubmitHelper(formEl) {
|
||||||
formEl.dispatchEvent(new Event('sent'))
|
formEl.dispatchEvent(new Event('sent'))
|
||||||
|
|
||||||
// Handle redirects
|
// Handle redirects
|
||||||
if (data.redir) {
|
if (data.redir || data.redirect) {
|
||||||
window.location = data.redir
|
window.location = data.redir || data.redirect
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,10 @@ App.controller(
|
||||||
'TokenAccessPageController',
|
'TokenAccessPageController',
|
||||||
($scope, $http, $location, localStorage) => {
|
($scope, $http, $location, localStorage) => {
|
||||||
window.S = $scope
|
window.S = $scope
|
||||||
$scope.mode = 'accessAttempt' // 'accessAttempt' | 'v1Import'
|
$scope.mode = 'accessAttempt' // 'accessAttempt' | 'v1Import' | 'requireAccept'
|
||||||
|
|
||||||
$scope.v1ImportData = null
|
$scope.v1ImportData = null
|
||||||
|
$scope.requireAccept = null
|
||||||
|
|
||||||
$scope.accessInFlight = false
|
$scope.accessInFlight = false
|
||||||
$scope.accessSuccess = false
|
$scope.accessSuccess = false
|
||||||
|
@ -20,14 +21,20 @@ App.controller(
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.getProjectName = () => {
|
$scope.getProjectName = () => {
|
||||||
if (!$scope.v1ImportData || !$scope.v1ImportData.name) {
|
if ($scope.v1ImportData?.name) {
|
||||||
return 'This project'
|
|
||||||
} else {
|
|
||||||
return $scope.v1ImportData.name
|
return $scope.v1ImportData.name
|
||||||
|
} else if ($scope.requireAccept?.projectName) {
|
||||||
|
return $scope.requireAccept.projectName
|
||||||
|
} else {
|
||||||
|
return 'This project'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.post = () => {
|
$scope.postConfirmedByUser = () => {
|
||||||
|
$scope.post(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.post = (confirmedByUser = false) => {
|
||||||
$scope.mode = 'accessAttempt'
|
$scope.mode = 'accessAttempt'
|
||||||
const textData = $('#overleaf-token-access-data').text()
|
const textData = $('#overleaf-token-access-data').text()
|
||||||
const parsedData = JSON.parse(textData)
|
const parsedData = JSON.parse(textData)
|
||||||
|
@ -39,6 +46,7 @@ App.controller(
|
||||||
url: postUrl,
|
url: postUrl,
|
||||||
data: {
|
data: {
|
||||||
_csrf: csrfToken,
|
_csrf: csrfToken,
|
||||||
|
confirmedByUser,
|
||||||
},
|
},
|
||||||
}).then(
|
}).then(
|
||||||
function successCallback(response) {
|
function successCallback(response) {
|
||||||
|
@ -59,6 +67,9 @@ App.controller(
|
||||||
} else if (data.v1Import) {
|
} else if (data.v1Import) {
|
||||||
$scope.mode = 'v1Import'
|
$scope.mode = 'v1Import'
|
||||||
$scope.v1ImportData = data.v1Import
|
$scope.v1ImportData = data.v1Import
|
||||||
|
} else if (data.requireAccept) {
|
||||||
|
$scope.mode = 'requireAccept'
|
||||||
|
$scope.requireAccept = data.requireAccept
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
'invalid data from server in success response',
|
'invalid data from server in success response',
|
||||||
|
|
|
@ -1030,6 +1030,7 @@
|
||||||
"login_here": "Login here",
|
"login_here": "Login here",
|
||||||
"set_new_password": "Set new password",
|
"set_new_password": "Set new password",
|
||||||
"user_wants_you_to_see_project": "__username__ would like you to join __projectname__",
|
"user_wants_you_to_see_project": "__username__ would like you to join __projectname__",
|
||||||
|
"invited_to_join": "You have been invited to join",
|
||||||
"join_sl_to_view_project": "Join __appName__ to view this project",
|
"join_sl_to_view_project": "Join __appName__ to view this project",
|
||||||
"register_to_edit_template": "Please register to edit the __templateName__ template",
|
"register_to_edit_template": "Please register to edit the __templateName__ template",
|
||||||
"already_have_sl_account": "Already have an __appName__ account?",
|
"already_have_sl_account": "Already have an __appName__ account?",
|
||||||
|
|
|
@ -96,6 +96,70 @@ const _doTryTokenAccess = (
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tryReadOnlyTokenAccept = (
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
testPageLoad,
|
||||||
|
testFormPost,
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
_doTryTokenAccept(
|
||||||
|
`/read/${token}`,
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
testPageLoad,
|
||||||
|
testFormPost,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tryReadAndWriteTokenAccept = (
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
testPageLoad,
|
||||||
|
testFormPost,
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
_doTryTokenAccept(
|
||||||
|
`/${token}`,
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
testPageLoad,
|
||||||
|
testFormPost,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const _doTryTokenAccept = (
|
||||||
|
url,
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
testPageLoad,
|
||||||
|
testFormPost,
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
user.request.get(url, (err, response, body) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
testPageLoad(response, body)
|
||||||
|
if (!testFormPost) {
|
||||||
|
return callback()
|
||||||
|
}
|
||||||
|
user.request.post(
|
||||||
|
`${url}/grant`,
|
||||||
|
{ json: { token, confirmedByUser: true } },
|
||||||
|
(err, response, body) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err)
|
||||||
|
}
|
||||||
|
testFormPost(response, body)
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const tryContentAccess = (user, projcetId, test, callback) => {
|
const tryContentAccess = (user, projcetId, test, callback) => {
|
||||||
// The real-time service calls this end point to determine the user's
|
// The real-time service calls this end point to determine the user's
|
||||||
// permissions.
|
// permissions.
|
||||||
|
@ -233,9 +297,8 @@ describe('TokenAccess', function () {
|
||||||
|
|
||||||
describe('read-only token', function () {
|
describe('read-only token', function () {
|
||||||
beforeEach(function (done) {
|
beforeEach(function (done) {
|
||||||
this.owner.createProject(
|
this.projectName = `token-ro-test${Math.random()}`
|
||||||
`token-ro-test${Math.random()}`,
|
this.owner.createProject(this.projectName, (err, projectId) => {
|
||||||
(err, projectId) => {
|
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
return done(err)
|
return done(err)
|
||||||
}
|
}
|
||||||
|
@ -252,8 +315,7 @@ describe('TokenAccess', function () {
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('allow the user read-only access to the project', function (done) {
|
it('allow the user read-only access to the project', function (done) {
|
||||||
|
@ -269,8 +331,34 @@ describe('TokenAccess', function () {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
cb => {
|
cb => {
|
||||||
// use token
|
// try token
|
||||||
tryReadOnlyTokenAccess(
|
tryReadOnlyTokenAccess(
|
||||||
|
this.other1,
|
||||||
|
this.tokens.readOnly,
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
},
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
expect(body.requireAccept.projectName).to.equal(
|
||||||
|
this.projectName
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
// deny access before token is accepted
|
||||||
|
tryEditorAccess(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
expectErrorResponse.restricted.html,
|
||||||
|
cb
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb => {
|
||||||
|
// accept token
|
||||||
|
tryReadOnlyTokenAccept(
|
||||||
this.other1,
|
this.other1,
|
||||||
this.tokens.readOnly,
|
this.tokens.readOnly,
|
||||||
(response, body) => {
|
(response, body) => {
|
||||||
|
@ -543,9 +631,8 @@ describe('TokenAccess', function () {
|
||||||
|
|
||||||
describe('read-and-write token', function () {
|
describe('read-and-write token', function () {
|
||||||
beforeEach(function (done) {
|
beforeEach(function (done) {
|
||||||
this.owner.createProject(
|
this.projectName = `token-rw-test${Math.random()}`
|
||||||
`token-rw-test${Math.random()}`,
|
this.owner.createProject(this.projectName, (err, projectId) => {
|
||||||
(err, projectId) => {
|
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
return done(err)
|
return done(err)
|
||||||
}
|
}
|
||||||
|
@ -562,8 +649,7 @@ describe('TokenAccess', function () {
|
||||||
done()
|
done()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should allow the user to access project via read-and-write token url', function (done) {
|
it('should allow the user to access project via read-and-write token url', function (done) {
|
||||||
|
@ -577,8 +663,33 @@ describe('TokenAccess', function () {
|
||||||
expectErrorResponse.restricted.html,
|
expectErrorResponse.restricted.html,
|
||||||
cb
|
cb
|
||||||
),
|
),
|
||||||
|
// try token
|
||||||
cb =>
|
cb =>
|
||||||
tryReadAndWriteTokenAccess(
|
tryReadAndWriteTokenAccess(
|
||||||
|
this.other1,
|
||||||
|
this.tokens.readAndWrite,
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
},
|
||||||
|
(response, body) => {
|
||||||
|
expect(response.statusCode).to.equal(200)
|
||||||
|
expect(body.requireAccept.projectName).to.equal(
|
||||||
|
this.projectName
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cb
|
||||||
|
),
|
||||||
|
// deny access before the token is accepted
|
||||||
|
cb =>
|
||||||
|
tryEditorAccess(
|
||||||
|
this.other1,
|
||||||
|
this.projectId,
|
||||||
|
expectErrorResponse.restricted.html,
|
||||||
|
cb
|
||||||
|
),
|
||||||
|
// accept token
|
||||||
|
cb =>
|
||||||
|
tryReadAndWriteTokenAccept(
|
||||||
this.other1,
|
this.other1,
|
||||||
this.tokens.readAndWrite,
|
this.tokens.readAndWrite,
|
||||||
(response, body) => {
|
(response, body) => {
|
||||||
|
@ -661,7 +772,7 @@ describe('TokenAccess', function () {
|
||||||
),
|
),
|
||||||
cb => {
|
cb => {
|
||||||
// use read-only token
|
// use read-only token
|
||||||
tryReadOnlyTokenAccess(
|
tryReadOnlyTokenAccept(
|
||||||
this.other1,
|
this.other1,
|
||||||
this.tokens.readOnly,
|
this.tokens.readOnly,
|
||||||
(response, body) => {
|
(response, body) => {
|
||||||
|
@ -707,7 +818,7 @@ describe('TokenAccess', function () {
|
||||||
// Then switch to read-write token
|
// Then switch to read-write token
|
||||||
//
|
//
|
||||||
cb =>
|
cb =>
|
||||||
tryReadAndWriteTokenAccess(
|
tryReadAndWriteTokenAccept(
|
||||||
this.other1,
|
this.other1,
|
||||||
this.tokens.readAndWrite,
|
this.tokens.readAndWrite,
|
||||||
(response, body) => {
|
(response, body) => {
|
||||||
|
|
Loading…
Reference in a new issue