-
Notifications
You must be signed in to change notification settings - Fork 12
[SES-PHASE-1-6] - PLU-744: add admin whitelist endpoint #1632
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat/ses/trunk
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,58 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Router } from 'express' | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| import logger from '@/helpers/logger' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import EmailSuppressionEntry from '@/models/email-suppression-entry' | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const router = Router() | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Middleware to ensure only plumber admins can access admin routes. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| router.use((req, res, next) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!req.context?.isAdminOperation) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| res.status(403).json({ error: 'Admin access required' }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| next() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * GET /api/admin/email-suppression/whitelist?emails=a@x.com,b+1@x.com | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Whitelists one or more emails from the suppression list. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Requires x-plumber-admin-token header. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| router.get('/email-suppression/whitelist', async (req, res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const emailsParam = req.query.emails as string | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!emailsParam) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| res.status(400).json({ error: 'emails query parameter is required' }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // `+` in query strings is decoded to a space; restore it since spaces | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // are not valid in email addresses. This lets callers paste URLs with | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // unencoded `+` characters directly. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const emails = emailsParam | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: zod this? |
||||||||||||||||||||||||||||||||||||||||||||||||||
| .split(',') | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .map((e) => e.trim().replace(/ /g, '+')) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Type assertion bug: const emailsParam = req.query.emails
if (!emailsParam || Array.isArray(emailsParam)) {
res.status(400).json({ error: 'emails query parameter is required and must be a single value' })
return
}
const emails = (emailsParam as string)
.split(',')
.map((e) => e.trim().replace(/ /g, '+'))
.filter(Boolean)
Suggested change
Spotted by Graphite |
||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter(Boolean) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (emails.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| res.status(400).json({ error: 'emails must not be empty' }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const whitelisted = await EmailSuppressionEntry.whitelistEmails(emails) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info('Admin whitelisted emails from suppression list', { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| event: 'admin-email-suppression-whitelist', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| requested: emails, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| whitelisted, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| res.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| whitelisted, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| count: whitelisted.length, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export default router | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import { | |
| requireAuthentication, | ||
| setCurrentUserContext, | ||
| } from './middleware/authentication' | ||
| import adminRouter from './admin' | ||
| import appsRouter from './apps' | ||
| import chatRouter from './chat' | ||
|
|
||
|
|
@@ -17,6 +18,7 @@ router.use(requireAuthentication) | |
|
|
||
| // Mount routes that admins can access before blockAdminOperations | ||
| router.use('/apps', appsRouter) | ||
| router.use('/admin', adminRouter) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to be extra safe, we should also add guardrails for this route to our WAF |
||
|
|
||
| // Block admin mutations for all subsequent routes | ||
| router.use(blockAdminOperations) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eh, why use a GET here?
For writes, it's more idiomatic to use POST / PUT / PATCH / DELETE. Primarily we're concerned about accidentlly exposing ourselves to CSRF attacks.
The more important part is actually for POST requests, browsers put up the form resubmission warning.
For this case, POST is fine, but tbh we don't need to enforce resubmission warning; PUT or PATCH also OK. DELETE feels weird
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok as discussed - we're safe behind the plumber admin header but still kinda dangerous to propagate this in the code. would like to avoid if possible unless we have a really really strong use case.