Skip to content
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

feat: support whitelisting domains through agencies table #1141

Merged
merged 2 commits into from
Apr 26, 2021
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
2 changes: 2 additions & 0 deletions backend/src/core/loaders/sequelize.loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Statistic,
ProtectedMessage,
Unsubscriber,
Agency,
} from '@core/models'
import {
EmailMessage,
Expand Down Expand Up @@ -86,6 +87,7 @@ const sequelizeLoader = async (): Promise<void> => {
UserDemo,
Statistic,
Unsubscriber,
Agency,
]
const emailModels = [
EmailMessage,
Expand Down
10 changes: 10 additions & 0 deletions backend/src/core/models/agency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { DataType, Model, Column, Table } from 'sequelize-typescript'

@Table({ tableName: 'agencies', underscored: true, timestamps: true })
export class Agency extends Model<Agency> {
@Column({
type: DataType.STRING,
primaryKey: true,
})
domain!: string
}
1 change: 1 addition & 0 deletions backend/src/core/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './user/user-demo'
export * from './statistic'
export * from './protected_message'
export * from './unsubscriber'
export * from './agency'
2 changes: 1 addition & 1 deletion backend/src/core/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ const hasWaitTimeElapsed = async (email: string): Promise<void> => {
* @param email
*/
const isWhitelistedEmail = async (email: string): Promise<boolean> => {
const endsInWhitelistedDomain = validateDomain(email)
const endsInWhitelistedDomain = await validateDomain(email)
if (!endsInWhitelistedDomain) {
// If the email does not end in a whitelisted domain, check that it was whitelisted by us manually
const user = await User.findOne({ where: { email: email } })
Expand Down
67 changes: 39 additions & 28 deletions backend/src/core/utils/validate-domain.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,59 @@
import validator from 'validator'
import { loggerWithLabel } from '@core/logger'
import config from '@core/config'
import { Agency } from '@core/models'

const logger = loggerWithLabel(module)

type ValidateDomainFunction = (email: string) => boolean
const getValidateDomain = (domains: string): ValidateDomainFunction => {
const domainsToWhitelist: string[] = domains
.split(';')
.map((domain) => {
if (domain.startsWith('.') && validator.isEmail(`user@agency${domain}`)) {
// wildcard domain
// example: .gov.sg
// allow any emails that have domains ending in .gov.sg to sign in
return domain
} else if (domain.startsWith('@') && validator.isEmail(`user${domain}`)) {
// specific domain
// example: @moe.edu.sg
// allow any emails that are @moe.edu.sg to sign in
return domain
}
return ''
})
.filter(Boolean)
const isValidDomain = (domain: string): boolean => {
// wildcard domain
// example: .gov.sg
// allow any emails that have domains ending in .gov.sg to sign in
const isWildCardDomain =
domain.startsWith('.') && validator.isEmail(`user@agency${domain}`)

// specific domain
// example: @moe.edu.sg
// allow any emails that are @moe.edu.sg to sign in
const isSpecificDomain =
domain.startsWith('@') && validator.isEmail(`user${domain}`)

return isWildCardDomain || isSpecificDomain
}

const validateDomain = async (email: string): Promise<boolean> => {
const configDomains = config.get('domains').split(';')
const agencyDomains = (await Agency.findAll()).map((agency) => agency.domain)

const domainsToWhitelist = configDomains
.concat(agencyDomains)
.filter(isValidDomain)

if (domainsToWhitelist.length === 0) {
throw new Error(
`No domains were whitelisted - the supplied DOMAIN_WHITELIST is ${domains}`
`No domains were whitelisted - the supplied DOMAIN_WHITELIST is ${config.get(
'domains'
)}`
)
} else {
logger.info({
message: 'Domains whitelisted',
domainsToWhitelist,
action: 'getValidateDomain',
action: 'validateDomain',
})
}
return (email): boolean => {
return (
validator.isEmail(email) &&
domainsToWhitelist.some((domain) => email.endsWith(domain))
)

const matched = domainsToWhitelist.filter((domain) => email.endsWith(domain))
if (matched.length > 0) {
logger.info({
message: 'Match for email found.',
matched,
email,
action: 'validateDomain',
})
}
}

const validateDomain = getValidateDomain(config.get('domains'))
return validator.isEmail(email) && matched.length > 0
}

export { validateDomain }