2676 prevent higher access users from changing email to personal#3302
Conversation
…nging-email-to-personal
…date account component
…que constraint violations
…email constraints
…nent to handle email confirmation failures expliicitly
There was a problem hiding this comment.
This is a placeholder config file until we move domain lists to .env file.
Another option could be to move these lists to the database - persisting in DB seems a bit over engineered for the currents needs imo, but I am open to considering if there are preferences towards it.
There was a problem hiding this comment.
I'm going to put the list of domains in the .env file, so they can be altered as necessary by the devops people - who will be able to change runtime environment variable settings without having to edit the database or code. I'll do this as a small change to your PR, and update the .env contents settings in the 1Passaword vault(s).
| const user = await selectById(model.id); | ||
| if (!user) { | ||
| const error = new Error("User record not found."); | ||
| error.code = "USER_NOT_FOUND"; | ||
| throw error; |
There was a problem hiding this comment.
I believe the user sessions is accessible from the middleware layer so perhaps this is redundant but also seems more secure.
There was a problem hiding this comment.
I'd have to check to make sure the user object from the middleware has the same structure as the user object from 'selectById', but it probably does., so it should not make a difference where you get the user info. The middleware gets it from the seure JWT header in the HTTP request so it better be secure, or we have bigger problems. I'm ok with what you did here.
|
|
||
| const validateUniqueEmail = async (email, currentUserId) => { | ||
| const trimmedEmail = email.toLowerCase().trim(); | ||
| const existingEmailCheck = await selectByEmail(trimmedEmail); | ||
|
|
||
| if (existingEmailCheck && existingEmailCheck.id !== currentUserId) { | ||
| const error = new Error( | ||
| `The email ${email} is already in use by another account.` | ||
| ); | ||
| error.code = "ERR_DUPLICATE_EMAIL"; | ||
| throw error; | ||
| } | ||
| }; |
There was a problem hiding this comment.
The Login_Update stored procedure also checks for email uniqueness by looking at the login table, but imo we should be validating reqs upfront before we attempt a DB update- the stored procedure then acts as a secondary safeguard check. LMK what you think.
…nging-email-to-personal
entrotech
left a comment
There was a problem hiding this comment.
I'm going to make a few small changes to the PR before merging it, so the domain list comes from the .env file as you suggested, but it seems to be working well and gives good error messages when they try to enter a duplicate email or use a restricted domain.
We will need to create another issue to prevent someone who has security admin rights from granting roles to a user from the security admin page if their user name is not associated with an allowed domain.
|
|
||
| const validateUniqueEmail = async (email, currentUserId) => { | ||
| const trimmedEmail = email.toLowerCase().trim(); | ||
| const existingEmailCheck = await selectByEmail(trimmedEmail); | ||
|
|
||
| if (existingEmailCheck && existingEmailCheck.id !== currentUserId) { | ||
| const error = new Error( | ||
| `The email ${email} is already in use by another account.` | ||
| ); | ||
| error.code = "ERR_DUPLICATE_EMAIL"; | ||
| throw error; | ||
| } | ||
| }; |
|
|
||
| const validateUniqueEmail = async (email, currentUserId) => { | ||
| const trimmedEmail = email.toLowerCase().trim(); | ||
| const existingEmailCheck = await selectByEmail(trimmedEmail); | ||
|
|
||
| if (existingEmailCheck && existingEmailCheck.id !== currentUserId) { | ||
| const error = new Error( | ||
| `The email ${email} is already in use by another account.` | ||
| ); | ||
| error.code = "ERR_DUPLICATE_EMAIL"; | ||
| throw error; | ||
| } | ||
| }; |
| const user = await selectById(model.id); | ||
| if (!user) { | ||
| const error = new Error("User record not found."); | ||
| error.code = "USER_NOT_FOUND"; | ||
| throw error; |
There was a problem hiding this comment.
I'd have to check to make sure the user object from the middleware has the same structure as the user object from 'selectById', but it probably does., so it should not make a difference where you get the user info. The middleware gets it from the seure JWT header in the HTTP request so it better be secure, or we have bigger problems. I'm ok with what you did here.
There was a problem hiding this comment.
I'm going to put the list of domains in the .env file, so they can be altered as necessary by the devops people - who will be able to change runtime environment variable settings without having to edit the database or code. I'll do this as a small change to your PR, and update the .env contents settings in the 1Passaword vault(s).
What changes did you make?
The original PR issue was to limit admin users from updating their email to personal email address. However there were a few other issues in account updates that were coupled closely enough to be handled in this PR.
Why did you make the changes (we will use this info to test)?
Screenshots of Proposed Changes Of The Website (if any, please do not screen shot code changes)
Visuals before changes are applied
Admin attempt to change to personal email - SUCCEEDS
change in PR: blocks update requests to non-approved email domains
Security attempt to change to personal email - SUCCEEDS
change in PR: blocks update requests to non-approved email domains
develop-security-peronal-success.mov
Attempt to change to another existing email - BLOCKED with a catch all error message
change in PR: the error was caught in a generic catch all error message instead of the explicit "existing email" as the back end response code did not match the front end error handling
SMTP / TDM Google mailer failure - vague error message:
Visuals after changes are applied
Security Admin change email actions:
Security Admin attempt to change to personal email - BLOCKED with explicit error message
Database NO update on personal email change attempt (DBeaver):
https://github.com/user-attachments/assets/bee5c88c-2288-4dcc-b169-2a10978f03b4
Security Admin attempt to change to existing email - BLOCKED with correct explicit error message
Database NO update on existing email change attempt (DBeaver):
https://github.com/user-attachments/assets/055bc5fa-5a2d-4dac-8c3c-40a3c979d52c
Security success on allowed email domain - SUCCESS confirmation:
Database success update on approved email domain (DBeaver):

Successful login with changed email (confirming no breaking changes to login flow):
Screen.Recording.2026-07-09.at.11.36.37.AM.mov
Admin change email actions:
Admin attempt to change to personal email - BLOCKED with explicit error message
Admin attempt to change to existing email - BLOCKED with correct explicit error message
Admin success on allowed email domain - SUCCESS confirmation:
Database update (DBeaver):

Non-admin change personal email action - NOT BLOCKED:
SMTP / TDM Google mailer failure - added explicit error message