Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- master
pull_request:
branches:
- staging
- master

jobs:
Expand Down
50 changes: 50 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: 'OGP CodeQL'

on:
push:
branches: [ 'master' ]
pull_request:
branches: [ 'master' ]
schedule:
- cron: '0 10 * * *'

jobs:
analyze:
name: Analyze (${{matrix.language}})
runs-on: ${{(matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest'}}
timeout-minutes: ${{(matrix.language == 'swift' && 120) || 360}}
permissions:
# Required for all workflows
security-events: write

# Required to fetch internal or private CodeQL packs
packages: read

# Only required for workflows in private repositories
actions: read
contents: read

strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@v4

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{matrix.language}}
build-mode: ${{matrix.build-mode}}

# Pull config from https://github.com/opengovsg/codeql-config/blob/prod/codeql-config.yml
config-file: opengovsg/codeql-config/codeql-config.yml@prod

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: '/language:${{matrix.language}}'
10 changes: 5 additions & 5 deletions .secrets.baseline

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

10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ We use Github Actions to simplify our deployment process:

## Releasing

When a pull request is merged to `master`, it will be deployed automatically.
When a pull request is merged to `staging`, it is deployed to staging automatically. To release to production, open a pull request from `staging` to `master`; it is deployed to production when merged.

## Serverless

Expand Down Expand Up @@ -296,22 +296,22 @@ The URLs of the Grafana dashboards are:

## Contributions

The production branch is `master` and each PR is deployed when it is merged into `master`.
The default branch is `staging`. PRs merged into `staging` are deployed to staging, and changes reach production when `staging` is merged into `master`.

**If you have write access to this repository**

- Check out your feature branch from `master`
- Check out your feature branch from `staging`
- Make changes, and commit those changes
- Push these changes to Github
- Submit a pull request against `master`, filling in the standard template
- Submit a pull request against `staging`, filling in the standard template

**If you do not have write access to this repository**

- Fork this repository
- Clone the forked repository to your machine
- Create a branch, make changes and commit those changes.
- Push these changes to Github
- Submit a pull request against `basefork/master` (that's us!)
- Submit a pull request against `basefork/staging` (that's us!)
- Describe the issue as thoroughly as possible, and with screenshots if applicable. A picture speaks a thousand words!

For more information, see [CONTRIBUTING.md](docs/CONTRIBUTING.md)
8 changes: 8 additions & 0 deletions backend/.env-example
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,11 @@ WA_PRECALL_TEMPLATE_LABEL=""
# DD_SERVICE="postman"
# DD_LOGS_INJECTION="true"
# DD_TRACE_ENABLED="false"

### one.gov.sg login (optional; disabled when ONE_GOV_SG_CLIENT_ID is empty) ###
# ONE_GOV_SG_ISSUER="http://localhost:3000/api/auth"
# ONE_GOV_SG_CLIENT_ID=""
# PKCS#8 PEM private key (RS256), on one line with newlines escaped as \n
# ONE_GOV_SG_PRIVATE_KEY=""
# Frontend callback page, registered with one.gov.sg byte-for-byte
# ONE_GOV_SG_REDIRECT_URI="http://localhost:3000/login/one-gov-sg/callback"
55 changes: 55 additions & 0 deletions backend/package-lock.json

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

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"moment": "^2.29.4",
"nanoid": "3.3.6",
"nodemailer": "7.0.11",
"openid-client": "5.7.1",
"papaparse": "5.2.0",
"pg": "8.5.1",
"pg-connection-string": "2.4.0",
Expand Down
33 changes: 31 additions & 2 deletions backend/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ interface ConfigSchema {
path: string
}
}
oneGovSg: {
issuer: string
clientId: string
privateKey: string
redirectUri: string
}
otp: {
retries: number
expiry: number
Expand Down Expand Up @@ -317,12 +323,12 @@ const config: Config<ConfigSchema> = convict({
},
protectedUrl: {
doc: 'Url domain and path for password-protected messages',
default: 'https://postman.gov.sg/p', // prod only
default: 'https://legacy.postman.gov.sg/p', // prod only
env: 'PROTECTED_URL',
},
unsubscribeUrl: {
doc: 'Url domain and path for unsubscribe page',
default: 'https://postman.gov.sg/unsubscribe', // prod only
default: 'https://legacy.postman.gov.sg/unsubscribe', // prod only
env: 'UNSUBSCRIBE_URL',
},
session: {
Expand Down Expand Up @@ -372,6 +378,29 @@ const config: Config<ConfigSchema> = convict({
},
},
},
oneGovSg: {
issuer: {
doc: 'one.gov.sg OIDC issuer. Must be the full issuer including /api/auth',
default: 'https://one.gov.sg/api/auth',
env: 'ONE_GOV_SG_ISSUER',
},
clientId: {
doc: 'one.gov.sg client_id. one.gov.sg login is disabled if empty',
default: '',
env: 'ONE_GOV_SG_CLIENT_ID',
},
privateKey: {
doc: 'PKCS#8 PEM private key used to sign private_key_jwt client assertions',
default: '',
env: 'ONE_GOV_SG_PRIVATE_KEY',
sensitive: true,
},
redirectUri: {
doc: 'Frontend callback page registered with one.gov.sg, e.g. https://postman.gov.sg/login/one-gov-sg/callback',
default: '',
env: 'ONE_GOV_SG_REDIRECT_URI',
},
},
otp: {
retries: {
doc: 'Number of attempts a user can enter otp before a new otp is required',
Expand Down
8 changes: 8 additions & 0 deletions backend/src/core/loaders/express.loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import config from '@core/config'
import { InitV1Route } from '@core/routes'
import { loggerWithLabel } from '@core/logger'
import { ensureAttachmentsFieldIsArray } from '@core/utils/attachment'
import {
isOneGovSgCallbackPath,
redactOneGovSgSentryEvent,
} from '@core/utils/one-gov-sg-logging'
import helmet from 'helmet'
import {
ApiMalformError,
Expand Down Expand Up @@ -118,6 +122,7 @@ export function restApiErrorMiddleware(
Sentry.init({
dsn: config.get('sentryDsn'),
environment: config.get('env'),
beforeSend: redactOneGovSgSentryEvent,
})

/**
Expand Down Expand Up @@ -194,6 +199,9 @@ const expressApp = ({ app }: { app: express.Application }): void => {
msg: `Incoming HTTP Request {{req.method}} {{req.url}}`,
winstonInstance: logger,
ignoredRoutes: ['/'],
// The callback carries a single-use code and state in its body. Ignore
// the whole request, including any query string in req.url or the log message.
ignoreRoute: (req: Request) => isOneGovSgCallbackPath(req.path),
requestWhitelist: ['method', 'url', 'body', 'headers'],
responseWhitelist: ['statusCode'],
requestFilter: (req: Request, propName: string) => {
Expand Down
2 changes: 2 additions & 0 deletions backend/src/core/loaders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import cloudwatchLoader from './cloudwatch.loader'
import uploadQueueLoader from './upload-queue.loader'
import {
InitAuthService,
InitOneGovSgService,
InitCredentialService,
RedisService,
} from '@core/services'
Expand All @@ -15,6 +16,7 @@ const loaders = async ({ app }: { app: Application }): Promise<void> => {
const redisService = new RedisService()
;(app as any).redisService = redisService
;(app as any).authService = InitAuthService(redisService)
;(app as any).oneGovSgService = InitOneGovSgService(redisService)
;(app as any).credentialService = InitCredentialService(redisService)
securityHeadersLoader({ app })
await cloudwatchLoader()
Expand Down
Loading
Loading