-
Notifications
You must be signed in to change notification settings - Fork 42
MMT-4138: Check for assurance level 4 and role for non NASA MMT access #1429
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
base: MMT-3759
Are you sure you want to change the base?
Changes from 9 commits
e7076d5
d713c68
acf934a
5bb6829
769a771
1afed9f
3a0c7b7
e3d7c79
0068f59
644366c
9ed62cf
345e502
8e2ef03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,17 @@ import * as getConfig from '../../../../sharedUtils/getConfig' | |
| import fetchEdlProfile from '../../utils/fetchEdlProfile' | ||
| import createJwt from '../../utils/createJwt' | ||
| import * as createCookieModule from '../../utils/createCookie' | ||
| import checkNonNasaMMTAccess from '../../utils/checkNonNasaMMTAccess' | ||
|
|
||
| beforeAll(() => { | ||
| vi.spyOn(console, 'error').mockImplementation(() => {}) | ||
| }) | ||
|
|
||
| afterAll(() => { | ||
| vi.restoreAllMocks() | ||
| }) | ||
|
|
||
| vi.mock('../../utils/checkNonNasaMMTAccess') | ||
| const realCreateCookie = createCookieModule.default | ||
| vi.mock('simple-oauth2') | ||
| vi.mock('../../../../sharedUtils/getConfig', () => { | ||
|
|
@@ -83,7 +93,10 @@ describe('edlCallback', () => { | |
| }) | ||
| })) | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ uid: 'test-user' }) | ||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user', | ||
| assuranceLevel: 5 | ||
| }) | ||
|
|
||
| const response = await edlCallback(mockEvent) | ||
|
|
||
|
|
@@ -93,6 +106,144 @@ describe('edlCallback', () => { | |
| }) | ||
| }) | ||
|
|
||
| describe('when handling invalid assurance levels', () => { | ||
| test('should redirect to unauthorizedMMTAccess when assurance level is not defined', async () => { | ||
| const mockEvent = { | ||
| queryStringParameters: { | ||
| code: 'test-code', | ||
| state: encodeURIComponent(JSON.stringify({ target: '/' })) | ||
| } | ||
| } | ||
|
|
||
| AuthorizationCode.mockImplementation(() => ({ | ||
| getToken: vi.fn().mockResolvedValue({ | ||
| token: { | ||
| access_token: 'test-access-token', | ||
| refresh_token: 'test-refresh-token', | ||
| expires_at: '2023-01-01T00:00:00Z' | ||
| } | ||
| }) | ||
| })) | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user' // No assurance level | ||
| }) | ||
|
|
||
| const response = await edlCallback(mockEvent) | ||
|
|
||
| expect(response.statusCode).toBe(303) | ||
| expect(response.headers.Location).toBe('https://mmt.example.com/unauthorizedAccess?errorType=mmt') | ||
| }) | ||
| }) | ||
|
|
||
| describe('when handling assurance levels less than 4', () => { | ||
| test('should redirect to unauthorizedMMTAccess when assurance level is less than 4', async () => { | ||
| const mockEvent = { | ||
| queryStringParameters: { | ||
| code: 'test-code', | ||
| state: encodeURIComponent(JSON.stringify({ target: '/' })) | ||
| } | ||
| } | ||
|
|
||
| AuthorizationCode.mockImplementation(() => ({ | ||
| getToken: vi.fn().mockResolvedValue({ | ||
| token: { | ||
| access_token: 'test-access-token', | ||
| refresh_token: 'test-refresh-token', | ||
| expires_at: '2023-01-01T00:00:00Z' | ||
| } | ||
| }) | ||
| })) | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user', | ||
| assuranceLevel: '3' // Set assurance level to 3 | ||
| }) | ||
|
|
||
| const response = await edlCallback(mockEvent) | ||
|
|
||
| expect(response.statusCode).toBe(303) | ||
| expect(response.headers.Location).toBe('https://mmt.example.com/unauthorizedAccess?errorType=mmt') | ||
| }) | ||
| }) | ||
|
|
||
| describe('when handling assurance level 4', () => { | ||
| beforeEach(() => { | ||
| AuthorizationCode.mockImplementation(() => ({ | ||
| getToken: vi.fn().mockResolvedValue({ | ||
| token: { | ||
| access_token: 'test-access-token', | ||
| refresh_token: 'test-refresh-token', | ||
| expires_at: '2023-01-01T00:00:00Z' | ||
| } | ||
| }) | ||
| })) | ||
| }) | ||
|
|
||
| test('should redirect to unauthorizedNonNasaMMTAccess when checkNonNasaMMTAccess returns false', async () => { | ||
| const mockEvent = { | ||
| queryStringParameters: { | ||
| code: 'test-code', | ||
| state: encodeURIComponent(JSON.stringify({ target: '/' })) | ||
| } | ||
| } | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user', | ||
| assuranceLevel: '4' | ||
| }) | ||
|
|
||
| checkNonNasaMMTAccess.mockResolvedValue(false) | ||
|
|
||
| const response = await edlCallback(mockEvent) | ||
|
|
||
| expect(response.statusCode).toBe(303) | ||
| expect(response.headers.Location).toBe('https://mmt.example.com/unauthorizedAccess?errorType=nonNasaMMT') | ||
| expect(checkNonNasaMMTAccess).toHaveBeenCalledWith('test-user', 'test-access-token') | ||
| }) | ||
|
|
||
| test('should continue normal flow when checkNonNasaMMTAccess returns true', async () => { | ||
|
||
| const mockEvent = { | ||
| queryStringParameters: { | ||
| code: 'test-code', | ||
| state: encodeURIComponent(JSON.stringify({ target: '/' })) | ||
| } | ||
| } | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user', | ||
| assuranceLevel: '4' | ||
| }) | ||
|
|
||
| checkNonNasaMMTAccess.mockResolvedValue(true) | ||
|
|
||
| const response = await edlCallback(mockEvent) | ||
|
|
||
| expect(response.statusCode).toBe(303) | ||
| expect(response.headers.Location).toBe('https://mmt.example.com/auth-callback?target=%2F') | ||
| expect(checkNonNasaMMTAccess).toHaveBeenCalledWith('test-user', 'test-access-token') | ||
| }) | ||
|
|
||
| test('should throw an error when checkNonNasaMMTAccess fails', async () => { | ||
| const mockEvent = { | ||
| queryStringParameters: { | ||
| code: 'test-code', | ||
| state: encodeURIComponent(JSON.stringify({ target: '/' })) | ||
| } | ||
| } | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user', | ||
| assuranceLevel: '4' | ||
| }) | ||
|
|
||
| checkNonNasaMMTAccess.mockRejectedValue(new Error('Failed to check access')) | ||
|
|
||
| await expect(edlCallback(mockEvent)).rejects.toThrow('Failed to check access') | ||
| expect(checkNonNasaMMTAccess).toHaveBeenCalledWith('test-user', 'test-access-token') | ||
| }) | ||
| }) | ||
|
|
||
| describe('when running in offline mode', () => { | ||
| test('should issue mock tokens without calling EDL', async () => { | ||
| process.env.IS_OFFLINE = 'true' | ||
|
|
@@ -191,7 +342,10 @@ describe('edlCallback', () => { | |
| }) | ||
| })) | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ uid: 'test-user' }) | ||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user', | ||
| assuranceLevel: 5 | ||
| }) | ||
|
|
||
| const response = await edlCallback(mockEvent) | ||
|
|
||
|
|
@@ -216,7 +370,10 @@ describe('edlCallback', () => { | |
| }) | ||
| })) | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ uid: 'test-user' }) | ||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user', | ||
| assuranceLevel: 5 | ||
| }) | ||
|
|
||
| const response = await edlCallback(mockEvent) | ||
|
|
||
|
|
@@ -243,11 +400,16 @@ describe('edlCallback', () => { | |
| getToken: vi.fn().mockResolvedValue({ token: mockToken }) | ||
| })) | ||
|
|
||
| const mockEdlProfile = { uid: 'test-user' } | ||
| const mockEdlProfile = { | ||
| uid: 'test-user', | ||
| assuranceLevel: 5 | ||
| } | ||
| fetchEdlProfile.mockResolvedValue(mockEdlProfile) | ||
|
|
||
| await edlCallback(mockEvent) | ||
| const expectedExpirationSeconds = Math.floor(new Date(mockToken.expires_at).getTime() / 1000) | ||
| const expectedExpirationSeconds = Math.floor( | ||
| new Date(mockToken.expires_at).getTime() / 1000 | ||
| ) | ||
|
|
||
| expect(createJwt).toHaveBeenCalledWith( | ||
| mockToken.access_token, | ||
|
|
@@ -304,7 +466,10 @@ describe('edlCallback', () => { | |
| }) | ||
| })) | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ uid: 'test-user' }) | ||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user', | ||
| assuranceLevel: 5 | ||
| }) | ||
|
|
||
| const response = await edlCallback(mockEvent) | ||
|
|
||
|
|
@@ -334,7 +499,10 @@ describe('edlCallback', () => { | |
| }) | ||
| })) | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ uid: 'test-user' }) | ||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user', | ||
| assuranceLevel: 5 | ||
| }) | ||
|
|
||
| const response = await edlCallback(mockEvent) | ||
|
|
||
|
|
@@ -363,7 +531,10 @@ describe('edlCallback', () => { | |
| getToken: mockGetToken | ||
| })) | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ uid: 'test-user' }) | ||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user', | ||
| assuranceLevel: 5 | ||
| }) | ||
|
|
||
| await edlCallback(mockEvent) | ||
|
|
||
|
|
@@ -433,7 +604,7 @@ describe('edlCallback', () => { | |
| }) | ||
| })) | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({}) | ||
| fetchEdlProfile.mockResolvedValue({ assuranceLevel: 5 }) | ||
|
|
||
| const response = await edlCallback(mockEvent) | ||
|
|
||
|
|
@@ -461,7 +632,10 @@ describe('edlCallback', () => { | |
| }) | ||
| })) | ||
|
|
||
| const mockEdlProfile = { uid: 'test-user' } | ||
| const mockEdlProfile = { | ||
| uid: 'test-user', | ||
| assuranceLevel: 5 | ||
| } | ||
| fetchEdlProfile.mockResolvedValue(mockEdlProfile) | ||
|
|
||
| await edlCallback(mockEvent) | ||
|
|
@@ -505,7 +679,10 @@ describe('edlCallback', () => { | |
| }) | ||
| })) | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ uid: 'test-user' }) | ||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user', | ||
| assuranceLevel: 5 | ||
| }) | ||
|
|
||
| const response = await edlCallback(mockEvent) | ||
|
|
||
|
|
@@ -538,7 +715,11 @@ describe('edlCallback', () => { | |
| }) | ||
| })) | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ uid: 'test-user' }) | ||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user', | ||
| assuranceLevel: 5 | ||
| }) | ||
|
|
||
| createJwt.mockImplementation(() => { | ||
| throw new Error('JWT creation failed') | ||
| }) | ||
|
|
@@ -564,7 +745,11 @@ describe('edlCallback', () => { | |
| }) | ||
| })) | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ uid: 'test-user' }) | ||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user', | ||
| assuranceLevel: 5 | ||
| }) | ||
|
|
||
| createJwt.mockReturnValue('test-jwt') | ||
| createCookieSpy.mockImplementation(() => { | ||
| throw new Error('Cookie creation failed') | ||
|
|
@@ -593,7 +778,10 @@ describe('edlCallback', () => { | |
| }) | ||
| })) | ||
|
|
||
| fetchEdlProfile.mockResolvedValue({ uid: 'test-user' }) | ||
| fetchEdlProfile.mockResolvedValue({ | ||
| uid: 'test-user', | ||
| assuranceLevel: 5 | ||
| }) | ||
|
|
||
| const response = await edlCallback(mockEvent) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ import { getApplicationConfig, getEdlConfig } from '../../../sharedUtils/getConf | |
| import fetchEdlProfile from '../utils/fetchEdlProfile' | ||
| import createJwt from '../utils/createJwt' | ||
| import createCookie from '../utils/createCookie' | ||
| import checkNonNasaMMTAccess from '../utils/checkNonNasaMMTAccess' | ||
|
|
||
| /** | ||
| * Handles the EDL callback during authentication | ||
|
|
@@ -72,6 +73,35 @@ const edlCallback = async (event) => { | |
| expiresAt = token.expires_at | ||
|
|
||
| edlProfile = await fetchEdlProfile(oauthToken) | ||
|
|
||
| // Convert assuranceLevel to number if it's not already | ||
| const assuranceLevel = Number(edlProfile.assuranceLevel) | ||
mandyparson marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if (Number.isNaN(assuranceLevel) || assuranceLevel < 4) { | ||
mandyparson marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return { | ||
| statusCode: 303, | ||
| headers: { | ||
| Location: `${mmtHost}/unauthorizedAccess?errorType=mmt` | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (assuranceLevel === 4) { | ||
|
||
| try { | ||
| const hasNonNasaDraftAccess = await checkNonNasaMMTAccess(edlProfile.uid, accessToken) | ||
| if (!hasNonNasaDraftAccess) { | ||
mandyparson marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return { | ||
| statusCode: 303, | ||
| headers: { | ||
| Location: `${mmtHost}/unauthorizedAccess?errorType=nonNasaMMT` | ||
| } | ||
| } | ||
| } | ||
| } catch (error) { | ||
| console.error('Error checking Non-NASA MMT access:', error) | ||
| throw error | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Create JWT with EDL token and edl profile | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Below this,
expect(checkNonNasaMMTAccess).toHaveBeenCalledTimes(1)Anywhere in this file where you have a
.toHAveBeenCalledWith(), there should be an accompanying .toHaveBeenCalledTimes()