diff --git a/app/src/forms/auth/middleware/apiAccess.js b/app/src/forms/auth/middleware/apiAccess.js index ded4aec58..cd2623a11 100644 --- a/app/src/forms/auth/middleware/apiAccess.js +++ b/app/src/forms/auth/middleware/apiAccess.js @@ -6,45 +6,49 @@ const formService = require('../../form/service'); const submissionService = require('../../submission/service'); module.exports = async (req, res, next) => { - // Check if authorization header is basic auth - if (req.headers && req.headers.authorization && req.headers.authorization.startsWith('Basic ')) { - // URL params should override query string params of the same attribute - const params = { ...req.query, ...req.params }; - - // Basic auth is currently only used for form and submission endpoints. Use - // the formId if it exists, otherwise fetch the formId from the submission's - // form. - let formId; - if (params.formId) { - formId = params.formId; - } else if (params.formSubmissionId && uuidValidate(params.formSubmissionId)) { - const result = await submissionService.read(params.formSubmissionId); - formId = result?.form?.id; + try { + // Check if authorization header is basic auth + if (req.headers && req.headers.authorization && req.headers.authorization.startsWith('Basic ')) { + // URL params should override query string params of the same attribute + const params = { ...req.query, ...req.params }; + + // Basic auth is currently only used for form and submission endpoints. Use + // the formId if it exists, otherwise fetch the formId from the submission's + // form. + let formId; + if (params.formId) { + formId = params.formId; + } else if (params.formSubmissionId && uuidValidate(params.formSubmissionId)) { + const result = await submissionService.read(params.formSubmissionId); + formId = result?.form?.id; + } + + let secret = ''; // Must be initialized as a string + + if (formId && uuidValidate(formId)) { + const result = await formService.readApiKey(formId); + secret = result && result.secret ? result.secret : ''; + } + + const checkCredentials = basicAuth({ + // Must be a synchronous function + authorizer: (username, password) => { + const userMatch = formId && basicAuth.safeCompare(username, formId); + const pwMatch = secret && basicAuth.safeCompare(password, secret); + + req.apiUser = userMatch & pwMatch; // Flag current request as an API entity + return req.apiUser; + }, + unauthorizedResponse: () => { + return new Problem(401, { detail: 'Invalid authorization credentials.' }); + }, + }); + + return checkCredentials(req, res, next); + } else { + next(); } - - let secret = ''; // Must be initialized as a string - - if (formId && uuidValidate(formId)) { - const result = await formService.readApiKey(formId); - secret = result && result.secret ? result.secret : ''; - } - - const checkCredentials = basicAuth({ - // Must be a synchronous function - authorizer: (username, password) => { - const userMatch = formId && basicAuth.safeCompare(username, formId); - const pwMatch = secret && basicAuth.safeCompare(password, secret); - - req.apiUser = userMatch & pwMatch; // Flag current request as an API entity - return req.apiUser; - }, - unauthorizedResponse: () => { - return new Problem(401, { detail: 'Invalid authorization credentials.' }); - }, - }); - - return checkCredentials(req, res, next); - } else { - next(); + } catch (error) { + next(error); } }; diff --git a/app/src/forms/auth/middleware/userAccess.js b/app/src/forms/auth/middleware/userAccess.js index 3dae5bb41..9e3c01cdb 100644 --- a/app/src/forms/auth/middleware/userAccess.js +++ b/app/src/forms/auth/middleware/userAccess.js @@ -16,13 +16,17 @@ const getToken = (req) => { }; const setUser = async (req, _res, next) => { - const token = getToken(req); - // we can limit the form list from query string or url params. Url params override query params - // ex. /forms/:formId=ABC/version?formId=123 - // the ABC in the url will be used... so don't do that. - const params = { ...req.query, ...req.params }; - req.currentUser = await service.login(token, params); - next(); + try { + const token = getToken(req); + // we can limit the form list from query string or url params. Url params override query params + // ex. /forms/:formId=ABC/version?formId=123 + // the ABC in the url will be used... so don't do that. + const params = { ...req.query, ...req.params }; + req.currentUser = await service.login(token, params); + next(); + } catch (error) { + next(error); + } }; const currentUser = async (req, res, next) => { @@ -86,102 +90,110 @@ const hasFormPermissions = (permissions) => { const hasSubmissionPermissions = (permissions) => { return async (req, _res, next) => { - // Skip permission checks if requesting as API entity - if (req.apiUser) { - return next(); - } + try { + // Skip permission checks if requesting as API entity + if (req.apiUser) { + return next(); + } - if (!Array.isArray(permissions)) { - permissions = [permissions]; - } + if (!Array.isArray(permissions)) { + permissions = [permissions]; + } - // Get the provided submission ID whether in a param or query (precedence to param) - const submissionId = req.params.formSubmissionId || req.query.formSubmissionId; - if (!submissionId) { - // No submission provided to this route that secures based on form... that's a problem! - return next(new Problem(401, { detail: 'Submission Id not found on request.' })); - } + // Get the provided submission ID whether in a param or query (precedence to param) + const submissionId = req.params.formSubmissionId || req.query.formSubmissionId; + if (!submissionId) { + // No submission provided to this route that secures based on form... that's a problem! + return next(new Problem(401, { detail: 'Submission Id not found on request.' })); + } - // Get the submission results so we know what form this submission is for - const submissionForm = await service.getSubmissionForm(submissionId); - - // Does the user have permissions for this submission due to their FORM permissions - if (req.currentUser) { - let formFromCurrentUser = req.currentUser.forms.find((f) => f.formId === submissionForm.form.id); - if (formFromCurrentUser) { - // Do they have the submission permissions being requested on this FORM - const intersection = permissions.filter((p) => { - return formFromCurrentUser.permissions.includes(p); - }); - if (intersection.length === permissions.length) { - req.formIdWithDeletePermission = submissionForm.form.id; - return next(); + // Get the submission results so we know what form this submission is for + const submissionForm = await service.getSubmissionForm(submissionId); + + // Does the user have permissions for this submission due to their FORM permissions + if (req.currentUser) { + let formFromCurrentUser = req.currentUser.forms.find((f) => f.formId === submissionForm.form.id); + if (formFromCurrentUser) { + // Do they have the submission permissions being requested on this FORM + const intersection = permissions.filter((p) => { + return formFromCurrentUser.permissions.includes(p); + }); + if (intersection.length === permissions.length) { + req.formIdWithDeletePermission = submissionForm.form.id; + return next(); + } } } - } - // Deleted submissions are inaccessible - if (submissionForm.submission.deleted) { - return next(new Problem(401, { detail: 'You do not have access to this submission.' })); - } + // Deleted submissions are inaccessible + if (submissionForm.submission.deleted) { + return next(new Problem(401, { detail: 'You do not have access to this submission.' })); + } - // TODO: consider whether DRAFT submissions are restricted as deleted above + // TODO: consider whether DRAFT submissions are restricted as deleted above - // Public (annonymous) forms are publicly viewable - const publicAllowed = submissionForm.form.identityProviders.find((p) => p.code === 'public') !== undefined; - if (permissions.length === 1 && permissions.includes(Permissions.SUBMISSION_READ) && publicAllowed) { - return next(); - } + // Public (annonymous) forms are publicly viewable + const publicAllowed = submissionForm.form.identityProviders.find((p) => p.code === 'public') !== undefined; + if (permissions.length === 1 && permissions.includes(Permissions.SUBMISSION_READ) && publicAllowed) { + return next(); + } - // check against the submission level permissions assigned to the user... - const submissionPermission = await service.checkSubmissionPermission(req.currentUser, submissionId, permissions); - if (submissionPermission) return next(); + // check against the submission level permissions assigned to the user... + const submissionPermission = await service.checkSubmissionPermission(req.currentUser, submissionId, permissions); + if (submissionPermission) return next(); - // no access to this submission... - return next(new Problem(401, { detail: 'You do not have access to this submission.' })); + // no access to this submission... + return next(new Problem(401, { detail: 'You do not have access to this submission.' })); + } catch (error) { + next(error); + } }; }; const filterMultipleSubmissions = () => { return async (req, _res, next) => { - // Get the provided list of submissions Id whether in a req body - const submissionIds = req.body && req.body.submissionIds; - if (!Array.isArray(submissionIds)) { - // No submission provided to this route that secures based on form... that's a problem! - return next(new Problem(401, { detail: 'SubmissionIds not found on request.' })); - } + try { + // Get the provided list of submissions Id whether in a req body + const submissionIds = req.body && req.body.submissionIds; + if (!Array.isArray(submissionIds)) { + // No submission provided to this route that secures based on form... that's a problem! + return next(new Problem(401, { detail: 'SubmissionIds not found on request.' })); + } - let formIdWithDeletePermission = req.formIdWithDeletePermission; + let formIdWithDeletePermission = req.formIdWithDeletePermission; - // Get the provided form ID whether in a param or query (precedence to param) - const formId = req.params.formId || req.query.formId; - if (!formId) { - // No submission provided to this route that secures based on form... that's a problem! - return next(new Problem(401, { detail: 'Form Id not found on request.' })); - } + // Get the provided form ID whether in a param or query (precedence to param) + const formId = req.params.formId || req.query.formId; + if (!formId) { + // No submission provided to this route that secures based on form... that's a problem! + return next(new Problem(401, { detail: 'Form Id not found on request.' })); + } - //validate form id - if (!validate(formId)) { - return next(new Problem(401, { detail: 'Not a valid form id' })); - } + //validate form id + if (!validate(formId)) { + return next(new Problem(401, { detail: 'Not a valid form id' })); + } - //validate all submission ids - const isValidSubmissionId = submissionIds.every((submissionId) => validate(submissionId)); - if (!isValidSubmissionId) { - return next(new Problem(401, { detail: 'Invalid submissionId(s) in the submissionIds list.' })); - } + //validate all submission ids + const isValidSubmissionId = submissionIds.every((submissionId) => validate(submissionId)); + if (!isValidSubmissionId) { + return next(new Problem(401, { detail: 'Invalid submissionId(s) in the submissionIds list.' })); + } - if (formIdWithDeletePermission === formId) { - // check if users has not injected submission id that does not belong to this form - const metaData = await service.getMultipleSubmission(submissionIds); + if (formIdWithDeletePermission === formId) { + // check if users has not injected submission id that does not belong to this form + const metaData = await service.getMultipleSubmission(submissionIds); - const isForeignSubmissionId = metaData.every((SubmissionMetadata) => SubmissionMetadata.formId === formId); - if (!isForeignSubmissionId || metaData.length !== submissionIds.length) { - return next(new Problem(401, { detail: 'Current user does not have required permission(s) for some submissions in the submissionIds list.' })); + const isForeignSubmissionId = metaData.every((SubmissionMetadata) => SubmissionMetadata.formId === formId); + if (!isForeignSubmissionId || metaData.length !== submissionIds.length) { + return next(new Problem(401, { detail: 'Current user does not have required permission(s) for some submissions in the submissionIds list.' })); + } + return next(); } - return next(); + return next(new Problem(401, { detail: 'Current user does not have required permission(s) for to delete submissions' })); + } catch (error) { + next(error); } - return next(new Problem(401, { detail: 'Current user does not have required permission(s) for to delete submissions' })); }; }; @@ -243,71 +255,75 @@ const hasFormRoles = (formRoles, hasAll = false) => { const hasRolePermissions = (removingUsers = false) => { return async (req, res, next) => { - // If we invoke this middleware and the caller is acting on a specific formId, whether in a param or query (precedence to param) - const formId = req.params.formId || req.query.formId; - if (!formId) { - // No form provided to this route that secures based on form... that's a problem! - return new Problem(401, { detail: 'Form Id not found on request.' }).send(res); - } + try { + // If we invoke this middleware and the caller is acting on a specific formId, whether in a param or query (precedence to param) + const formId = req.params.formId || req.query.formId; + if (!formId) { + // No form provided to this route that secures based on form... that's a problem! + return new Problem(401, { detail: 'Form Id not found on request.' }).send(res); + } + + const currentUser = req.currentUser; + const data = req.body; + + const isOwner = hasFormRole(formId, currentUser, Roles.OWNER); + + if (removingUsers) { + if (data.includes(currentUser.id)) return next(new Problem(401, { detail: "You can't remove yourself from this form." })); - const currentUser = req.currentUser; - const data = req.body; + if (!isOwner) { + for (let i = 0; i < data.length; i++) { + let userId = data[i]; - const isOwner = hasFormRole(formId, currentUser, Roles.OWNER); + const userRoles = await rbacService.readUserRole(userId, formId); - if (removingUsers) { - if (data.includes(currentUser.id)) return next(new Problem(401, { detail: "You can't remove yourself from this form." })); + // Can't update another user's roles if they are an owner + if (userRoles.some((fru) => fru.role === Roles.OWNER) && userId !== currentUser.id) { + return next(new Problem(401, { detail: "You can not update an owner's roles." })); + } - if (!isOwner) { - for (let i = 0; i < data.length; i++) { - let userId = data[i]; + // If the user is trying to remove the designer role + if (userRoles.some((fru) => fru.role === Roles.FORM_DESIGNER)) { + return next(new Problem(401, { detail: "You can't remove a form designer role." })); + } + } + } + } else { + const userId = req.params.userId || req.query.userId; + if (!userId || (userId && userId.length === 0)) { + return new Problem(401, { detail: 'User Id not found on request.' }); + } + if (!isOwner) { const userRoles = await rbacService.readUserRole(userId, formId); + // If the user is trying to remove the team manager role for their own userid + if (userRoles.some((fru) => fru.role === Roles.TEAM_MANAGER) && !data.some((role) => role.role === Roles.TEAM_MANAGER) && userId == currentUser.id) { + return next(new Problem(401, { detail: "You can't remove your own team manager role." })); + } + // Can't update another user's roles if they are an owner if (userRoles.some((fru) => fru.role === Roles.OWNER) && userId !== currentUser.id) { - return next(new Problem(401, { detail: "You can not update an owner's roles." })); + return next(new Problem(401, { detail: "You can't update an owner's roles." })); + } + if (!userRoles.some((fru) => fru.role === Roles.OWNER) && data.some((role) => role.role === Roles.OWNER)) { + return next(new Problem(401, { detail: "You can't add an owner role." })); } - // If the user is trying to remove the designer role - if (userRoles.some((fru) => fru.role === Roles.FORM_DESIGNER)) { + // If the user is trying to remove the designer role for another userid + if (userRoles.some((fru) => fru.role === Roles.FORM_DESIGNER) && !data.some((role) => role.role === Roles.FORM_DESIGNER)) { return next(new Problem(401, { detail: "You can't remove a form designer role." })); } + if (!userRoles.some((fru) => fru.role === Roles.FORM_DESIGNER) && data.some((role) => role.role === Roles.FORM_DESIGNER)) { + return next(new Problem(401, { detail: "You can't add a form designer role." })); + } } } - } else { - const userId = req.params.userId || req.query.userId; - if (!userId || (userId && userId.length === 0)) { - return new Problem(401, { detail: 'User Id not found on request.' }); - } - - if (!isOwner) { - const userRoles = await rbacService.readUserRole(userId, formId); - - // If the user is trying to remove the team manager role for their own userid - if (userRoles.some((fru) => fru.role === Roles.TEAM_MANAGER) && !data.some((role) => role.role === Roles.TEAM_MANAGER) && userId == currentUser.id) { - return next(new Problem(401, { detail: "You can't remove your own team manager role." })); - } - // Can't update another user's roles if they are an owner - if (userRoles.some((fru) => fru.role === Roles.OWNER) && userId !== currentUser.id) { - return next(new Problem(401, { detail: "You can't update an owner's roles." })); - } - if (!userRoles.some((fru) => fru.role === Roles.OWNER) && data.some((role) => role.role === Roles.OWNER)) { - return next(new Problem(401, { detail: "You can't add an owner role." })); - } - - // If the user is trying to remove the designer role for another userid - if (userRoles.some((fru) => fru.role === Roles.FORM_DESIGNER) && !data.some((role) => role.role === Roles.FORM_DESIGNER)) { - return next(new Problem(401, { detail: "You can't remove a form designer role." })); - } - if (!userRoles.some((fru) => fru.role === Roles.FORM_DESIGNER) && data.some((role) => role.role === Roles.FORM_DESIGNER)) { - return next(new Problem(401, { detail: "You can't add a form designer role." })); - } - } + return next(); + } catch (error) { + next(error); } - - return next(); }; };