Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
36 changes: 32 additions & 4 deletions serverless/src/edlAuthorizer/__tests__/handler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ vi.mock('@/shared/fetchEdlProfile')
describe('edlAuthorizer', () => {
const OLD_ENV = process.env
let loggerErrorSpy
let loggerInfoSpy

beforeEach(() => {
loggerErrorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {})
loggerInfoSpy = vi.spyOn(logger, 'info').mockImplementation(() => {})
process.env = { ...OLD_ENV }
fetchEdlProfile.mockReset()
fetchEdlProfile.mockResolvedValue({
Expand All @@ -28,6 +30,7 @@ describe('edlAuthorizer', () => {

afterAll(() => {
loggerErrorSpy?.mockRestore()
loggerInfoSpy?.mockRestore()
})

describe('when the token is for a valid user', () => {
Expand Down Expand Up @@ -117,7 +120,13 @@ describe('edlAuthorizer', () => {
})

expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith('Authorization failed: No uid found in profile')
expect(logger.error).toHaveBeenCalledWith('Authorization failed: No uid found in profile', expect.objectContaining({
profile: expect.objectContaining({
uid: undefined
}),
tokenPresent: false,
tokenType: 'missing'
}))
})
})

Expand Down Expand Up @@ -147,7 +156,12 @@ describe('edlAuthorizer', () => {
})

expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith('EDL Authorizer error:', unauthorizedError)
expect(logger.error).toHaveBeenCalledWith('EDL Authorizer error:', unauthorizedError, expect.objectContaining({
errorMessage: 'Unauthorized',
errorName: 'Error',
tokenPresent: false,
tokenType: 'missing'
}))
})
})

Expand Down Expand Up @@ -179,7 +193,14 @@ describe('edlAuthorizer', () => {
})

expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith('Authorization failed: Assurance level 3 below required 5')
expect(logger.error).toHaveBeenCalledWith('Authorization failed: Assurance level 3 below required 5', expect.objectContaining({
parsedAssuranceLevel: 3,
requiredAssuranceLevel: 5,
profile: expect.objectContaining({
uid: 'mock_user',
assuranceLevel: 3
})
}))
})

test('returns a deny policy when assurance level missing', async () => {
Expand Down Expand Up @@ -208,7 +229,14 @@ describe('edlAuthorizer', () => {
})

expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith('Authorization failed: Assurance level missing from profile')
expect(logger.error).toHaveBeenCalledWith('Authorization failed: Assurance level missing from profile', expect.objectContaining({
rawAssuranceLevel: undefined,
rawAssuranceLevelType: 'undefined',
profile: expect.objectContaining({
uid: 'mock_user',
assuranceLevel: undefined
})
}))
})
})
})
101 changes: 96 additions & 5 deletions serverless/src/edlAuthorizer/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,73 @@ import { logger } from '@/shared/logger'

const REQUIRED_ASSURANCE_LEVEL = 5

/**
* Builds a request summary safe for authorization logs.
*
* @param {Object} event API Gateway authorizer event.
* @returns {Object} Redacted request summary.
*/
const summarizeEventForLogs = (event = {}) => {
const {
headers = {},
authorizationToken,
methodArn
} = event
const normalizedHeaders = downcaseKeys(headers)
const headerToken = normalizedHeaders.authorization || ''
const eventToken = authorizationToken || ''

return {
methodArnPresent: Boolean(methodArn),
headerAuthorizationPresent: Boolean(headerToken),
headerAuthorizationLength: headerToken.length,
eventAuthorizationTokenPresent: Boolean(eventToken),
eventAuthorizationTokenLength: eventToken.length
}
}

/**
* Summarizes the selected request token without exposing its raw value.
*
* @param {string} token Request authorization token.
* @returns {{tokenPresent: boolean, tokenType: string, tokenLength: number}}
* Redacted token summary.
*/
const summarizeTokenForLogs = (token) => {
const normalizedToken = token || ''
const bearerMatch = normalizedToken.match(/^\s*bearer\s+(.*)$/i)
const tokenValue = bearerMatch ? bearerMatch[1].trim() : normalizedToken.trim()
let tokenType = 'missing'

if (bearerMatch) {
tokenType = 'bearer'
} else if (tokenValue) {
tokenType = 'launchpad'
}

return {
tokenPresent: tokenValue.length > 0,
tokenType,
tokenLength: tokenValue.length
}
}

/**
* Summarizes the normalized EDL profile fields used by the authorizer.
*
* @param {Object} profile Normalized EDL profile.
* @returns {Object} Redacted profile summary.
*/
const summarizeProfileForLogs = (profile = {}) => ({
keys: Object.keys(profile).sort(),
uid: profile.uid,
auid: profile.auid,
assuranceLevel: profile.assuranceLevel,
namePresent: Boolean(profile.name)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

export const edlAuthorizer = async (event) => {
logger.debug('EDL Authorizer called with event:', JSON.stringify(event, null, 2))
logger.info('[edl-authorizer] Authorization request received', summarizeEventForLogs(event))
const {
headers = {},
methodArn,
Expand All @@ -24,43 +89,69 @@ export const edlAuthorizer = async (event) => {
// If still not found, default to an empty string
token = token || ''
logger.debug('Launchpad token:', token ? 'Present' : 'Not present')
logger.info('[edl-authorizer] Resolved request token', summarizeTokenForLogs(token))

try {
const profile = await fetchEdlProfile(token)
logger.debug('Fetched EDL profile:', JSON.stringify(profile, null, 2))
logger.info('[edl-authorizer] Retrieved normalized EDL profile', summarizeProfileForLogs(profile))
const {
uid,
assuranceLevel
} = profile || {}

if (!uid) {
logger.error('Authorization failed: No uid found in profile')
logger.error('Authorization failed: No uid found in profile', {
...summarizeTokenForLogs(token),
profile: summarizeProfileForLogs(profile)
})

return generatePolicy('user', 'Deny', methodArn)
}

const parsedAssuranceLevel = Number(assuranceLevel)

if (Number.isNaN(parsedAssuranceLevel)) {
logger.error('Authorization failed: Assurance level missing from profile')
logger.error('Authorization failed: Assurance level missing from profile', {
...summarizeTokenForLogs(token),
profile: summarizeProfileForLogs(profile),
rawAssuranceLevel: assuranceLevel,
rawAssuranceLevelType: typeof assuranceLevel
})

return generatePolicy('user', 'Deny', methodArn)
}

if (parsedAssuranceLevel < REQUIRED_ASSURANCE_LEVEL) {
logger.error(`Authorization failed: Assurance level ${parsedAssuranceLevel} below required ${REQUIRED_ASSURANCE_LEVEL}`)
logger.error(`Authorization failed: Assurance level ${parsedAssuranceLevel} below required ${REQUIRED_ASSURANCE_LEVEL}`, {
...summarizeTokenForLogs(token),
profile: summarizeProfileForLogs(profile),
parsedAssuranceLevel,
requiredAssuranceLevel: REQUIRED_ASSURANCE_LEVEL
})

return generatePolicy('user', 'Deny', methodArn)
}

logger.debug('Authorization successful for uid:', uid)
logger.info('[edl-authorizer] Authorization successful', {
uid,
parsedAssuranceLevel,
requiredAssuranceLevel: REQUIRED_ASSURANCE_LEVEL,
tokenType: summarizeTokenForLogs(token).tokenType
})

const policy = generatePolicy(uid, 'Allow', methodArn)
logger.debug('Returning policy:', JSON.stringify(policy, null, 2))

return policy
} catch (error) {
logger.error('EDL Authorizer error:', error)
logger.error('EDL Authorizer error:', error, {
...summarizeEventForLogs(event),
...summarizeTokenForLogs(token),
errorMessage: error?.message,
errorName: error?.name
})

// Return a "Deny" policy for any caught errors
const denyPolicy = generatePolicy('user', 'Deny', methodArn)
Expand Down
Loading
Loading