Skip to content

Commit

Permalink
feat: expose bullboard with auth (#561)
Browse files Browse the repository at this point in the history
## Problem

There's no easy to inspect jobs in the queue right now for deployed
environments (e.g. prod, staging, uat)

## Solution

Expose bull board that only allows admin user access.

New env var: you can override admin user with ADMIN_USER_EMAIL env var
locally

2 new env var statically set:
ADMIN_USER_EMAIL: [email protected]
ENABLE_BULLMQ_DASHBOARD: true
  • Loading branch information
pregnantboy authored May 6, 2024
1 parent 15cd30e commit 90a1db2
Show file tree
Hide file tree
Showing 12 changed files with 65 additions and 64 deletions.
12 changes: 0 additions & 12 deletions CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1109,18 +1109,6 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


-------------------------------------------------------------------------------

## Project
express-basic-auth

### Source
https://github.com/LionC/express-basic-auth

### License
(The MIT License)


-------------------------------------------------------------------------------

## Project
Expand Down
8 changes: 8 additions & 0 deletions ecs/env.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@
{
"name": "POSTMAN_FROM_ADDRESS",
"value": "[email protected]"
},
{
"name": "ADMIN_USER_EMAIL",
"value": "[email protected]"
},
{
"name": "ENABLE_BULLMQ_DASHBOARD",
"value": "true"
}
],
"secrets": [
Expand Down
8 changes: 0 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions packages/backend/.env-example
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ REDIS_USERNAME=
REDIS_PASSWORD=
REDIS_CLUSTER_MODE=false
ENABLE_BULLMQ_DASHBOARD=false
BULLMQ_DASHBOARD_USERNAME=
BULLMQ_DASHBOARD_PASSWORD=
[email protected]
POSTMAN_API_KEY=api_key
FORMSG_API_KEY=sample-formsg-api-key
M365_GOVTECH_STAGING_TENANT_ID=...
Expand Down
1 change: 0 additions & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
"electrodb": "2.12.0",
"email-validator": "2.0.4",
"express": "4.19.2",
"express-basic-auth": "^1.2.1",
"form-data": "4.0.0",
"graphql": "16.8.1",
"graphql-middleware": "6.1.35",
Expand Down
6 changes: 2 additions & 4 deletions packages/backend/src/config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ type AppConfig = {
redisTls: boolean
redisClusterMode: boolean
enableBullMQDashboard: boolean
bullMQDashboardUsername: string
bullMQDashboardPassword: string
adminUserEmail: string
requestBodySizeLimit: string
formsgApiKey: string
postman: {
Expand Down Expand Up @@ -78,9 +77,8 @@ const appConfig: AppConfig = {
redisPassword: process.env.REDIS_PASSWORD,
redisTls: process.env.REDIS_TLS === 'true',
redisClusterMode: process.env.REDIS_CLUSTER_MODE === 'true',
adminUserEmail: process.env.ADMIN_USER_EMAIL,
enableBullMQDashboard: process.env.ENABLE_BULLMQ_DASHBOARD === 'true',
bullMQDashboardUsername: process.env.BULLMQ_DASHBOARD_USERNAME,
bullMQDashboardPassword: process.env.BULLMQ_DASHBOARD_PASSWORD,
baseUrl: process.env.BASE_URL,
webAppUrl,
webhookUrl,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { chunk } from 'lodash'

import appConfig from '@/config/app'
import { DEFAULT_JOB_OPTIONS } from '@/helpers/default-job-configuration'
import logger from '@/helpers/logger'
import Execution from '@/models/execution'
Expand Down Expand Up @@ -52,7 +53,7 @@ const bulkRetryExecutions: MutationResolvers['bulkRetryExecutions'] = async (
context,
) => {
let latestFailedExecutionSteps = await getAllFailedExecutionSteps(
context.currentUser.email === '[email protected]'
context.currentUser.email === appConfig.adminUserEmail
? Execution.query()
: context.currentUser.$relatedQuery('executions'),
params.input.flowId,
Expand Down
18 changes: 17 additions & 1 deletion packages/backend/src/helpers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,26 @@ export function setAuthCookie(
return
}

export function getAuthCookie(req: Request) {
function getAuthCookie(req: Request) {
return req.cookies[AUTH_COOKIE_NAME]
}

export async function getLoggedInUser(req: Request): Promise<User | null> {
const token = getAuthCookie(req)
if (!token) {
return null
}

try {
const { userId } = jwt.verify(token, appConfig.sessionSecretKey) as {
userId: string
}
return User.query().findById(userId)
} catch (err) {
return null
}
}

export function deleteAuthCookie(res: Response) {
res.clearCookie(AUTH_COOKIE_NAME)
}
Expand Down
18 changes: 2 additions & 16 deletions packages/backend/src/helpers/authentication.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { Request, Response } from 'express'
import { createRateLimitRule, RedisStore } from 'graphql-rate-limit'
import { allow, rule, shield } from 'graphql-shield'
import jwt from 'jsonwebtoken'

import appConfig from '@/config/app'
import { createRedisClient, REDIS_DB_INDEX } from '@/config/redis'
import { getAuthCookie } from '@/helpers/auth'
import User from '@/models/user'
import { getLoggedInUser } from '@/helpers/auth'
import { UnauthenticatedContext } from '@/types/express/context'

export const setCurrentUserContext = async ({
Expand All @@ -21,18 +18,7 @@ export const setCurrentUserContext = async ({
// Get tiles view key from headers
context.tilesViewKey = req.headers['x-tiles-view-key'] as string | undefined

const token = getAuthCookie(req)
if (token == null) {
return context
}
try {
const { userId } = jwt.verify(token, appConfig.sessionSecretKey) as {
userId: string
}
context.currentUser = await User.query().findById(userId)
} catch (_) {
context.currentUser = null
}
context.currentUser = await getLoggedInUser(req)
return context
}

Expand Down
19 changes: 14 additions & 5 deletions packages/backend/src/helpers/create-bull-board-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@ import triggerQueue from '@/queues/trigger'
const serverAdapter = new ExpressAdapter()

const createBullBoardHandler = async (serverAdapter: ExpressAdapter) => {
if (
!appConfig.enableBullMQDashboard ||
!appConfig.bullMQDashboardUsername ||
!appConfig.bullMQDashboardPassword
) {
if (!appConfig.enableBullMQDashboard) {
return
}

Expand All @@ -28,6 +24,19 @@ const createBullBoardHandler = async (serverAdapter: ExpressAdapter) => {
),
],
serverAdapter: serverAdapter,
options: {
uiConfig: {
favIcon: {
default: `${appConfig.webAppUrl}/favicon.svg`,
alternative: 'https://file.go.gov.sg/plumber-logo.png',
},
boardLogo: {
path: 'https://file.go.gov.sg/plumber-logo-full.png',
height: 70,
},
boardTitle: '',
},
},
})
}

Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/helpers/csp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const helmetOptions: HelmetOptions = {
!appConfig.isDev && 'https://*.apollographql.com',
].filter(Boolean),
upgradeInsecureRequests: [],
workerSrc: ['blob:'],
workerSrc: ['blob:', "'self'"],
},
},
crossOriginOpenerPolicy: {
Expand Down
31 changes: 18 additions & 13 deletions packages/backend/src/helpers/inject-bull-board-handler.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { ExpressAdapter } from '@bull-board/express'
import { Application } from 'express'
import basicAuth from 'express-basic-auth'
import { Application, NextFunction, Request, Response } from 'express'

import appConfig from '@/config/app'

import { getLoggedInUser } from './auth'

export const verifyIsAdminMiddleware = async (
req: Request,
res: Response,
next: NextFunction,
) => {
const user = await getLoggedInUser(req)
if (user?.email !== appConfig.adminUserEmail) {
res.sendStatus(401)
return
}
next()
}

const injectBullBoardHandler = async (
app: Application,
serverAdapter: ExpressAdapter,
) => {
if (
!appConfig.enableBullMQDashboard ||
!appConfig.bullMQDashboardUsername ||
!appConfig.bullMQDashboardPassword
) {
if (!appConfig.enableBullMQDashboard) {
return
}

Expand All @@ -21,12 +31,7 @@ const injectBullBoardHandler = async (

app.use(
queueDashboardBasePath,
basicAuth({
users: {
[appConfig.bullMQDashboardUsername]: appConfig.bullMQDashboardPassword,
},
challenge: true,
}),
verifyIsAdminMiddleware,
serverAdapter.getRouter(),
)
}
Expand Down

0 comments on commit 90a1db2

Please sign in to comment.