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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ export bamboo_RDF4J_PASSWORD=[your rdfdb password]
export bamboo_EDL_HOST=[edl host name]
export bamboo_EDL_UID=[edl user id]
export bamboo_EDL_PASSWORD=[edl password]
export bamboo_EDL_CLIENT_ID=[edl oauth client id]
export bamboo_CMR_BASE_URL=[cmr base url]
export bamboo_CMR_WRITER_TOKEN=[optional bearer token for CMR metadata writeback]
export bamboo_CMR_WRITEBACK_PROVIDERS=[optional provider id, comma-separated list, or ALL]
Expand Down
1 change: 1 addition & 0 deletions bin/deploy-bamboo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ dockerRun() {
--env "RDF4J_USER_NAME=$bamboo_RDF4J_USER_NAME" \
--env "RDF4J_PASSWORD=$bamboo_RDF4J_PASSWORD" \
--env "EDL_PASSWORD=$bamboo_EDL_PASSWORD" \
--env "EDL_CLIENT_ID=$bamboo_EDL_CLIENT_ID" \
--env "CMR_BASE_URL=$bamboo_CMR_BASE_URL" \
--env "CMR_WRITER_TOKEN=${bamboo_CMR_WRITER_TOKEN:-}" \
--env "CMR_WRITEBACK_PROVIDERS=${bamboo_CMR_WRITEBACK_PROVIDERS:-}" \
Expand Down
1 change: 1 addition & 0 deletions cdk/app/lib/KmsStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface KmsStackProps extends cdk.StackProps {
logDestinationArn: string
environment: {
CMR_BASE_URL: string
EDL_CLIENT_ID: string
EDL_PASSWORD: string
BLOCK_PUBLISH_ON_KEYWORD_DIFF_FAILURE?: string
LOG_LEVEL: string
Expand Down
1 change: 1 addition & 0 deletions cdk/app/lib/helper/KmsLambdaFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface LambdaFunctionsProps {
useLocalstack: boolean;
environment: {
CMR_BASE_URL: string;
EDL_CLIENT_ID: string;
EDL_PASSWORD: string;
BLOCK_PUBLISH_ON_KEYWORD_DIFF_FAILURE?: string;
REDIS_ENABLED?: string;
Expand Down
1 change: 1 addition & 0 deletions cdk/bin/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ async function main() {
RDF_BUCKET_NAME: process.env.RDF_BUCKET_NAME || 'kms-rdf-backup',
CMR_BASE_URL: cmrBaseUrl,
EDL_PASSWORD: process.env.EDL_PASSWORD || '',
EDL_CLIENT_ID: process.env.EDL_CLIENT_ID || '',
BLOCK_PUBLISH_ON_KEYWORD_DIFF_FAILURE: process.env.BLOCK_PUBLISH_ON_KEYWORD_DIFF_FAILURE,
LOG_LEVEL: process.env.LOG_LEVEL || 'INFO',
REDIS_ENABLED: redisEnabledValue,
Expand Down
3 changes: 3 additions & 0 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
150 changes: 125 additions & 25 deletions serverless/src/shared/__tests__/fetchEdlProfile.test.js
Original file line number Diff line number Diff line change
@@ -1,92 +1,138 @@
import * as getConfig from '@/shared/getConfig'
import { logger } from '@/shared/logger'

import fetchEdlClientToken from '../fetchEdlClientToken'
import fetchEdlProfile from '../fetchEdlProfile'

vi.mock('../fetchEdlClientToken', () => ({ default: vi.fn() }))

const TEST_EDL_HOST = 'https://edl.example.test'

const encodeBase64Url = (value) => Buffer
Comment thread
cgokey marked this conversation as resolved.
.from(JSON.stringify(value))
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '')

const createJwt = (payload) => [
encodeBase64Url({
alg: 'RS256',
typ: 'JWT'
}),
encodeBase64Url(payload),
'signature'
].join('.')

const originalConsoleLog = console.log
let getEdlConfigSpy
let loggerErrorSpy

beforeEach(() => {
vi.resetAllMocks()
console.log = vi.fn()
process.env.EDL_CLIENT_ID = 'kms-client-id'
process.env.EDL_PASSWORD = 'kms-client-password'
fetchEdlClientToken.mockImplementation(() => ('mock_token'))
getEdlConfigSpy = vi.spyOn(getConfig, 'getEdlConfig').mockImplementation(() => ({
host: TEST_EDL_HOST
}))

loggerErrorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {})
})

afterEach(() => {
vi.clearAllMocks()
console.log = originalConsoleLog

getEdlConfigSpy?.mockRestore()
getEdlConfigSpy = undefined

loggerErrorSpy?.mockRestore()
loggerErrorSpy = undefined
delete process.env.EDL_CLIENT_ID
delete process.env.EDL_PASSWORD
delete process.env.IS_OFFLINE
})

describe('fetchEdlProfile', () => {
describe('when provided a Bearer access token', () => {
describe('when the token is valid', () => {
test('calls the oauth endpoint and returns the profile', async () => {
test('validates the token and returns the normalized profile', async () => {
const rawToken = createJwt({
uid: 'user.name',
assurance_level: 3
})

global.fetch = vi.fn(() => Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve({
nams_auid: 'user.name',
uid: 'user.name',
first_name: 'User',
last_name: 'Name',
assurance_level: 3
uid: 'user.name'
})
}))

const profile = await fetchEdlProfile('Bearer bearer-token-value')
const profile = await fetchEdlProfile(`Bearer ${rawToken}`)

expect(profile).toEqual({
auid: 'user.name',
assuranceLevel: 3,
name: 'User Name',
name: 'user.name',
uid: 'user.name'
})

expect(fetchEdlClientToken).not.toHaveBeenCalled()
expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenCalledWith('https://sit.urs.earthdata.nasa.gov/oauth/userInfo', {
headers: {
Authorization: 'Bearer bearer-token-value',
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
method: 'GET'
})
expect(fetch).toHaveBeenCalledWith(
`${TEST_EDL_HOST}/oauth/tokens/user`,
{
body: `client_id=${encodeURIComponent('kms-client-id')}&token=${encodeURIComponent(rawToken)}`,
headers: {
Accept: 'application/json',
Authorization: `Basic ${Buffer.from('kms-client-id:kms-client-password').toString('base64')}`,
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
method: 'POST'
}
)
})
})

describe('when the oauth endpoint returns an unauthorized error', () => {
describe('when the token validation endpoint returns an unauthorized error', () => {
test('throws unauthorized', async () => {
const rawToken = createJwt({
uid: 'user.name',
assurance_level: 3
})
const mockResponse = {
ok: false,
status: 401,
json: () => Promise.resolve({ error: 'Unauthorized' })
}
global.fetch = vi.fn(() => Promise.resolve(mockResponse))

await expect(fetchEdlProfile('Bearer bearer-token-value')).rejects.toThrow('Unauthorized')
await expect(fetchEdlProfile(`Bearer ${rawToken}`)).rejects.toThrow('Unauthorized')
expect(fetchEdlClientToken).not.toHaveBeenCalled()
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith('EDL oauth error response:', mockResponse)
expect(logger.error).toHaveBeenCalledWith('EDL token validation error response:', mockResponse)
})
})

describe('when the oauth endpoint returns a non-auth error', () => {
describe('when the token validation endpoint returns a non-auth error', () => {
test('throws an error describing the failure', async () => {
const rawToken = createJwt({
uid: 'user.name',
assurance_level: 3
})
const mockResponse = {
ok: false,
status: 500,
json: () => Promise.resolve({ error: 'Server Error' })
}
global.fetch = vi.fn(() => Promise.resolve(mockResponse))

await expect(fetchEdlProfile('Bearer bearer-token-value')).rejects.toThrow('EDL oauth request failed with status 500')
await expect(fetchEdlProfile(`Bearer ${rawToken}`)).rejects.toThrow('EDL token validation request failed with status 500')
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toHaveBeenCalledWith('EDL oauth error response:', mockResponse)
expect(logger.error).toHaveBeenCalledWith('EDL token validation error response:', mockResponse)
})
})

Expand All @@ -101,6 +147,61 @@ describe('fetchEdlProfile', () => {
expect(logger.error).toHaveBeenCalledWith('#fetchEdlProfile fetchEdlProfile Error:', expect.any(Error))
})
})

describe('when bearer validation configuration is missing', () => {
test('throws when EDL_CLIENT_ID is missing', async () => {
delete process.env.EDL_CLIENT_ID
global.fetch = vi.fn()

await expect(fetchEdlProfile(`Bearer ${createJwt({
uid: 'user.name',
assurance_level: 3
})}`)).rejects.toThrow('Missing EDL_CLIENT_ID configuration')

expect(fetch).not.toHaveBeenCalled()
})

test('throws when EDL_PASSWORD is missing', async () => {
delete process.env.EDL_PASSWORD
global.fetch = vi.fn()

await expect(fetchEdlProfile(`Bearer ${createJwt({
uid: 'user.name',
assurance_level: 3
})}`)).rejects.toThrow('Missing EDL_PASSWORD configuration')

expect(fetch).not.toHaveBeenCalled()
})
})

describe('when the token validation payload is malformed', () => {
test('throws an error when uid is missing', async () => {
const rawToken = createJwt({
uid: 'user.name',
assurance_level: 3
})

global.fetch = vi.fn(() => Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve({})
}))

await expect(fetchEdlProfile(`Bearer ${rawToken}`)).rejects.toThrow('EDL token validation response missing uid')
})

test('throws an error when the jwt payload cannot be decoded', async () => {
global.fetch = vi.fn(() => Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve({
uid: 'user.name'
})
}))

await expect(fetchEdlProfile('Bearer not-a-jwt')).rejects.toThrow('Invalid EDL token format')
})
})
})

describe('when provided a Launchpad access token', () => {
Expand All @@ -127,7 +228,7 @@ describe('fetchEdlProfile', () => {
})

expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenCalledWith('https://sit.urs.earthdata.nasa.gov/api/nams/edl_user', {
expect(fetch).toHaveBeenCalledWith(`${TEST_EDL_HOST}/api/nams/edl_user`, {
body: 'token=mock-token',
headers: {
Authorization: 'Bearer mock_token',
Expand Down Expand Up @@ -161,7 +262,7 @@ describe('fetchEdlProfile', () => {
})

expect(fetch).toHaveBeenCalledTimes(1)
expect(fetch).toHaveBeenCalledWith('https://sit.urs.earthdata.nasa.gov/api/nams/edl_user', {
expect(fetch).toHaveBeenCalledWith(`${TEST_EDL_HOST}/api/nams/edl_user`, {
body: 'token=mock-token',
headers: {
Authorization: 'Bearer mock_token',
Expand Down Expand Up @@ -216,7 +317,6 @@ describe('fetchEdlProfile', () => {
})

expect(fetch).toHaveBeenCalledTimes(0)
process.env.IS_OFFLINE = false
})
})
})
Loading
Loading