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 = {}
|
||||
) {
|
||||
if (userId) {
|
||||
return getPrivilegeLevelForProjectWithUser(userId, projectId, token, opts)
|
||||
return getPrivilegeLevelForProjectWithUser(userId, projectId, opts)
|
||||
} else {
|
||||
return getPrivilegeLevelForProjectWithoutUser(projectId, token, opts)
|
||||
}
|
||||
|
@ -79,7 +79,6 @@ async function getPrivilegeLevelForProject(
|
|||
async function getPrivilegeLevelForProjectWithUser(
|
||||
userId,
|
||||
projectId,
|
||||
token,
|
||||
opts = {}
|
||||
) {
|
||||
const privilegeLevel =
|
||||
|
|
|
@ -19,20 +19,17 @@ const orderedPrivilegeLevels = [
|
|||
PrivilegeLevels.OWNER,
|
||||
]
|
||||
|
||||
async function _userAlreadyHasHigherPrivilege(
|
||||
userId,
|
||||
projectId,
|
||||
token,
|
||||
tokenType
|
||||
) {
|
||||
async function _userAlreadyHasHigherPrivilege(userId, projectId, tokenType) {
|
||||
if (!Object.values(TokenAccessHandler.TOKEN_TYPES).includes(tokenType)) {
|
||||
throw new Error('bad token type')
|
||||
}
|
||||
if (!userId) {
|
||||
return false
|
||||
}
|
||||
const privilegeLevel =
|
||||
await AuthorizationManager.promises.getPrivilegeLevelForProject(
|
||||
userId,
|
||||
projectId,
|
||||
token
|
||||
projectId
|
||||
)
|
||||
return (
|
||||
orderedPrivilegeLevels.indexOf(privilegeLevel) >=
|
||||
|
@ -196,7 +193,6 @@ async function checkAndGetProjectOrResponseAction(
|
|||
const userHasPrivilege = await _userAlreadyHasHigherPrivilege(
|
||||
userId,
|
||||
projectId,
|
||||
token,
|
||||
tokenType
|
||||
)
|
||||
if (userHasPrivilege) {
|
||||
|
@ -220,6 +216,7 @@ async function checkAndGetProjectOrResponseAction(
|
|||
|
||||
async function grantTokenAccessReadAndWrite(req, res, next) {
|
||||
const { token } = req.params
|
||||
const { confirmedByUser } = req.body
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
if (!TokenAccessHandler.isReadAndWriteToken(token)) {
|
||||
return res.sendStatus(400)
|
||||
|
@ -240,6 +237,13 @@ async function grantTokenAccessReadAndWrite(req, res, next) {
|
|||
if (!project) {
|
||||
return next(new Errors.NotFoundError())
|
||||
}
|
||||
if (!confirmedByUser) {
|
||||
return res.json({
|
||||
requireAccept: {
|
||||
projectName: project.name,
|
||||
},
|
||||
})
|
||||
}
|
||||
await TokenAccessHandler.promises.addReadAndWriteUserToProject(
|
||||
userId,
|
||||
project._id
|
||||
|
@ -261,6 +265,7 @@ async function grantTokenAccessReadAndWrite(req, res, next) {
|
|||
|
||||
async function grantTokenAccessReadOnly(req, res, next) {
|
||||
const { token } = req.params
|
||||
const { confirmedByUser } = req.body
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
if (!TokenAccessHandler.isReadOnlyToken(token)) {
|
||||
return res.sendStatus(400)
|
||||
|
@ -286,6 +291,13 @@ async function grantTokenAccessReadOnly(req, res, next) {
|
|||
if (!project) {
|
||||
return next(new Errors.NotFoundError())
|
||||
}
|
||||
if (!confirmedByUser) {
|
||||
return res.json({
|
||||
requireAccept: {
|
||||
projectName: project.name,
|
||||
},
|
||||
})
|
||||
}
|
||||
await TokenAccessHandler.promises.addReadOnlyUserToProject(
|
||||
userId,
|
||||
project._id
|
||||
|
|
|
@ -91,6 +91,31 @@ block content
|
|||
a.btn.btn-primary(ng-href="{{ buildZipDownloadPath(v1ImportData.projectId) }}")
|
||||
| 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
|
||||
script(type="text/javascript", nonce=scriptNonce).
|
||||
|
|
|
@ -44,8 +44,8 @@ function formSubmitHelper(formEl) {
|
|||
formEl.dispatchEvent(new Event('sent'))
|
||||
|
||||
// Handle redirects
|
||||
if (data.redir) {
|
||||
window.location = data.redir
|
||||
if (data.redir || data.redirect) {
|
||||
window.location = data.redir || data.redirect
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -3,9 +3,10 @@ App.controller(
|
|||
'TokenAccessPageController',
|
||||
($scope, $http, $location, localStorage) => {
|
||||
window.S = $scope
|
||||
$scope.mode = 'accessAttempt' // 'accessAttempt' | 'v1Import'
|
||||
$scope.mode = 'accessAttempt' // 'accessAttempt' | 'v1Import' | 'requireAccept'
|
||||
|
||||
$scope.v1ImportData = null
|
||||
$scope.requireAccept = null
|
||||
|
||||
$scope.accessInFlight = false
|
||||
$scope.accessSuccess = false
|
||||
|
@ -20,14 +21,20 @@ App.controller(
|
|||
}
|
||||
|
||||
$scope.getProjectName = () => {
|
||||
if (!$scope.v1ImportData || !$scope.v1ImportData.name) {
|
||||
return 'This project'
|
||||
} else {
|
||||
if ($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'
|
||||
const textData = $('#overleaf-token-access-data').text()
|
||||
const parsedData = JSON.parse(textData)
|
||||
|
@ -39,6 +46,7 @@ App.controller(
|
|||
url: postUrl,
|
||||
data: {
|
||||
_csrf: csrfToken,
|
||||
confirmedByUser,
|
||||
},
|
||||
}).then(
|
||||
function successCallback(response) {
|
||||
|
@ -59,6 +67,9 @@ App.controller(
|
|||
} else if (data.v1Import) {
|
||||
$scope.mode = 'v1Import'
|
||||
$scope.v1ImportData = data.v1Import
|
||||
} else if (data.requireAccept) {
|
||||
$scope.mode = 'requireAccept'
|
||||
$scope.requireAccept = data.requireAccept
|
||||
} else {
|
||||
console.warn(
|
||||
'invalid data from server in success response',
|
||||
|
|
|
@ -1030,6 +1030,7 @@
|
|||
"login_here": "Login here",
|
||||
"set_new_password": "Set new password",
|
||||
"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",
|
||||
"register_to_edit_template": "Please register to edit the __templateName__ template",
|
||||
"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) => {
|
||||
// The real-time service calls this end point to determine the user's
|
||||
// permissions.
|
||||
|
@ -233,9 +297,8 @@ describe('TokenAccess', function () {
|
|||
|
||||
describe('read-only token', function () {
|
||||
beforeEach(function (done) {
|
||||
this.owner.createProject(
|
||||
`token-ro-test${Math.random()}`,
|
||||
(err, projectId) => {
|
||||
this.projectName = `token-ro-test${Math.random()}`
|
||||
this.owner.createProject(this.projectName, (err, projectId) => {
|
||||
if (err != null) {
|
||||
return done(err)
|
||||
}
|
||||
|
@ -252,8 +315,7 @@ describe('TokenAccess', function () {
|
|||
done()
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('allow the user read-only access to the project', function (done) {
|
||||
|
@ -269,8 +331,34 @@ describe('TokenAccess', function () {
|
|||
)
|
||||
},
|
||||
cb => {
|
||||
// use token
|
||||
// try token
|
||||
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.tokens.readOnly,
|
||||
(response, body) => {
|
||||
|
@ -543,9 +631,8 @@ describe('TokenAccess', function () {
|
|||
|
||||
describe('read-and-write token', function () {
|
||||
beforeEach(function (done) {
|
||||
this.owner.createProject(
|
||||
`token-rw-test${Math.random()}`,
|
||||
(err, projectId) => {
|
||||
this.projectName = `token-rw-test${Math.random()}`
|
||||
this.owner.createProject(this.projectName, (err, projectId) => {
|
||||
if (err != null) {
|
||||
return done(err)
|
||||
}
|
||||
|
@ -562,8 +649,7 @@ describe('TokenAccess', 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,
|
||||
cb
|
||||
),
|
||||
// try token
|
||||
cb =>
|
||||
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.tokens.readAndWrite,
|
||||
(response, body) => {
|
||||
|
@ -661,7 +772,7 @@ describe('TokenAccess', function () {
|
|||
),
|
||||
cb => {
|
||||
// use read-only token
|
||||
tryReadOnlyTokenAccess(
|
||||
tryReadOnlyTokenAccept(
|
||||
this.other1,
|
||||
this.tokens.readOnly,
|
||||
(response, body) => {
|
||||
|
@ -707,7 +818,7 @@ describe('TokenAccess', function () {
|
|||
// Then switch to read-write token
|
||||
//
|
||||
cb =>
|
||||
tryReadAndWriteTokenAccess(
|
||||
tryReadAndWriteTokenAccept(
|
||||
this.other1,
|
||||
this.tokens.readAndWrite,
|
||||
(response, body) => {
|
||||
|
|
Loading…
Reference in a new issue