-
Notifications
You must be signed in to change notification settings - Fork 20
Add Step-up rechallenge implementation #2869
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
Open
rinatkhaziev
wants to merge
21
commits into
trunk
Choose a base branch
from
feat/rechallenge-v2-defensive-mode
base: trunk
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
8934b71
feat(rechallenge): add types and error classes
rinatkhaziev 8655df4
feat(rechallenge): add keychain-backed per-scope elevated-token cache
rinatkhaziev 35f9916
feat(rechallenge): add REST client for Parker v2 session endpoints
rinatkhaziev a6df809
fix(rechallenge): satisfy tsc check-types in test files
rinatkhaziev b2c46de
feat(rechallenge): orchestrate session create, poll, exchange
rinatkhaziev 3e6cc2f
feat(rechallenge): add Apollo link that intercepts elevated-permissio…
rinatkhaziev f03c423
feat(api): insert rechallenge link into Apollo chain
rinatkhaziev a461f52
feat(defensive-mode): add GraphQL helpers for status and config mutat…
rinatkhaziev b8815af
feat(cli): add vip defensive-mode parent command
rinatkhaziev 20e4e0f
feat(cli): add vip defensive-mode enable subcommand
rinatkhaziev 2dc5804
feat(cli): add vip defensive-mode disable subcommand
rinatkhaziev 1109b2c
feat(cli): add vip defensive-mode configure subcommand
rinatkhaziev c700b4e
feat(cli): register vip defensive-mode top-level command
rinatkhaziev 80e0d1d
feat(logout): clear elevated-token cache on logout
rinatkhaziev 6763ec4
fix(api): lint cleanup for lazy rechallenge link import
rinatkhaziev a353924
fix: address final-review findings (diff in configure, non-interactiv…
rinatkhaziev c9fc8df
fix: address PR review feedback (data guards, version constant, paylo…
rinatkhaziev 5a7ca83
fix: address PR review feedback (output formatting, login hardening, …
rinatkhaziev 8736474
chore(lint): scope no-await-in-loop disable to rechallenge polling loop
rinatkhaziev 3a29646
fix(lint): stop importing Response type from node-fetch in rechalleng…
rinatkhaziev 2750aa4
Merge branch 'trunk' into feat/rechallenge-v2-defensive-mode
rinatkhaziev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| import { describe, expect, it, jest, beforeEach } from '@jest/globals'; | ||
|
|
||
| import { defensiveModeConfigureCommand } from '../../src/bin/vip-defensive-mode-configure'; | ||
| import command from '../../src/lib/cli/command'; | ||
| import { updateDefensiveModeConfig } from '../../src/lib/defensive-mode/api'; | ||
| import { trackEvent } from '../../src/lib/tracker'; | ||
|
|
||
| function mockExit() { | ||
| throw 'EXIT'; | ||
| } | ||
| jest.spyOn( console, 'log' ).mockImplementation( () => {} ); | ||
| jest.spyOn( console, 'error' ).mockImplementation( () => {} ); | ||
| jest.spyOn( process, 'exit' ).mockImplementation( mockExit ); | ||
|
|
||
| jest.mock( '../../src/lib/cli/command', () => { | ||
| const commandMock = { | ||
| argv: () => commandMock, | ||
| examples: () => commandMock, | ||
| option: () => commandMock, | ||
| }; | ||
| return jest.fn( () => commandMock ); | ||
| } ); | ||
|
|
||
| jest.mock( '../../src/lib/defensive-mode/api', () => ( { | ||
| updateDefensiveModeConfig: jest.fn( () => | ||
| Promise.resolve( { success: true, message: 'configured' } ) | ||
| ), | ||
| appQuery: 'mock-app-query', | ||
| } ) ); | ||
|
|
||
| jest.mock( '../../src/lib/tracker', () => ( { | ||
| trackEvent: jest.fn( () => Promise.resolve() ), | ||
| } ) ); | ||
|
|
||
| jest.mock( '../../src/lib/envvar/input', () => ( { | ||
| confirm: jest.fn( () => Promise.resolve( true ) ), | ||
| } ) ); | ||
|
|
||
| function baseOpts() { | ||
| return { | ||
| app: { id: 7, name: 'demo', organization: { id: 1, salesforceId: 'X' } }, | ||
| env: { id: 9, type: 'develop' }, | ||
| skipConfirmation: true, | ||
| }; | ||
| } | ||
|
|
||
| describe( 'vip defensive-mode configure', () => { | ||
| it( 'registers as a command', () => { | ||
| expect( command ).toHaveBeenCalled(); | ||
| } ); | ||
| } ); | ||
|
|
||
| describe( 'defensiveModeConfigureCommand', () => { | ||
| beforeEach( () => { | ||
| jest.clearAllMocks(); | ||
| } ); | ||
|
|
||
| it( 'applies full input when all flags are supplied', async () => { | ||
| await defensiveModeConfigureCommand( [], { | ||
| ...baseOpts(), | ||
| enabled: 'true', | ||
| challengeType: '1', | ||
| connectionThresholdAbsolute: '1000', | ||
| connectionThresholdPercentage: '50', | ||
| } ); | ||
| expect( updateDefensiveModeConfig ).toHaveBeenCalledWith( { | ||
| appId: 7, | ||
| envId: 9, | ||
| enabled: true, | ||
| challengeType: 1, | ||
| connectionThresholdAbsolute: 1000, | ||
| connectionThresholdPercentage: 50, | ||
| } ); | ||
| } ); | ||
|
|
||
| it( 'errors when required flags missing in non-interactive mode', async () => { | ||
| await expect( | ||
| defensiveModeConfigureCommand( [], { | ||
| ...baseOpts(), | ||
| nonInteractive: true, | ||
| } ) | ||
| ).rejects.toBe( 'EXIT' ); | ||
| expect( updateDefensiveModeConfig ).not.toHaveBeenCalled(); | ||
| } ); | ||
|
|
||
| it( 'rejects non-boolean enabled values', async () => { | ||
| await expect( | ||
| defensiveModeConfigureCommand( [], { | ||
| ...baseOpts(), | ||
| enabled: 'maybe', | ||
| challengeType: '1', | ||
| nonInteractive: true, | ||
| } ) | ||
| ).rejects.toBe( 'EXIT' ); | ||
| } ); | ||
|
|
||
| it( 'rejects non-integer challenge-type', async () => { | ||
| await expect( | ||
| defensiveModeConfigureCommand( [], { | ||
| ...baseOpts(), | ||
| enabled: 'true', | ||
| challengeType: 'oops', | ||
| nonInteractive: true, | ||
| } ) | ||
| ).rejects.toBe( 'EXIT' ); | ||
| } ); | ||
|
|
||
| it( 'tracks success', async () => { | ||
| await defensiveModeConfigureCommand( [], { | ||
| ...baseOpts(), | ||
| enabled: 'false', | ||
| challengeType: '1', | ||
| } ); | ||
| expect( trackEvent ).toHaveBeenCalledWith( | ||
| 'defensive_mode_configure_command_success', | ||
| expect.any( Object ) | ||
| ); | ||
| } ); | ||
|
|
||
| it( 'logs the proposed configuration before mutating', async () => { | ||
| const consoleSpy = jest.spyOn( console, 'log' ); | ||
| await defensiveModeConfigureCommand( [], { | ||
| ...baseOpts(), | ||
| enabled: 'true', | ||
| challengeType: '2', | ||
| } ); | ||
| const allArgs = consoleSpy.mock.calls.flat().filter( arg => typeof arg === 'string' ); | ||
| const settingsTable = allArgs.find( arg => arg.includes( 'Challenge type' ) ); | ||
| expect( settingsTable ).toBeDefined(); | ||
| expect( settingsTable ).toContain( 'Enabled' ); | ||
| expect( settingsTable ).toContain( 'true' ); | ||
| expect( settingsTable ).toContain( '2' ); | ||
| expect( settingsTable ).toContain( '(not specified)' ); | ||
| } ); | ||
|
|
||
| it( 'rejects bare threshold flags (boolean true)', async () => { | ||
| await expect( | ||
| defensiveModeConfigureCommand( [], { | ||
| ...baseOpts(), | ||
| enabled: 'true', | ||
| challengeType: '1', | ||
| connectionThresholdAbsolute: true, | ||
| nonInteractive: true, | ||
| } ) | ||
| ).rejects.toBe( 'EXIT' ); | ||
| expect( updateDefensiveModeConfig ).not.toHaveBeenCalled(); | ||
| } ); | ||
| } ); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { describe, expect, it, jest, beforeEach } from '@jest/globals'; | ||
|
|
||
| import { defensiveModeDisableCommand } from '../../src/bin/vip-defensive-mode-disable'; | ||
| import command from '../../src/lib/cli/command'; | ||
| import { updateDefensiveModeStatus } from '../../src/lib/defensive-mode/api'; | ||
| import { trackEvent } from '../../src/lib/tracker'; | ||
|
|
||
| function mockExit() { | ||
| throw 'EXIT'; | ||
| } | ||
| jest.spyOn( console, 'log' ).mockImplementation( () => {} ); | ||
| jest.spyOn( console, 'error' ).mockImplementation( () => {} ); | ||
| jest.spyOn( process, 'exit' ).mockImplementation( mockExit ); | ||
|
|
||
| jest.mock( '../../src/lib/cli/command', () => { | ||
| const commandMock = { | ||
| argv: () => commandMock, | ||
| examples: () => commandMock, | ||
| option: () => commandMock, | ||
| }; | ||
| return jest.fn( () => commandMock ); | ||
| } ); | ||
|
|
||
| jest.mock( '../../src/lib/defensive-mode/api', () => ( { | ||
| updateDefensiveModeStatus: jest.fn( () => | ||
| Promise.resolve( { success: true, message: 'disabled' } ) | ||
| ), | ||
| appQuery: 'mock-app-query', | ||
| } ) ); | ||
|
|
||
| jest.mock( '../../src/lib/tracker', () => ( { | ||
| trackEvent: jest.fn( () => Promise.resolve() ), | ||
| } ) ); | ||
|
|
||
| describe( 'vip defensive-mode disable', () => { | ||
| it( 'registers as a command', () => { | ||
| expect( command ).toHaveBeenCalled(); | ||
| } ); | ||
| } ); | ||
|
|
||
| describe( 'defensiveModeDisableCommand', () => { | ||
| beforeEach( () => { | ||
| jest.clearAllMocks(); | ||
| } ); | ||
|
|
||
| it( 'calls updateDefensiveModeStatus with enabled=false', async () => { | ||
| const opts = { | ||
| app: { id: 7, name: 'demo', organization: { id: 1, salesforceId: 'X' } }, | ||
| env: { id: 9, type: 'develop' }, | ||
| skipConfirmation: true, | ||
| }; | ||
| await defensiveModeDisableCommand( [], opts ); | ||
| expect( updateDefensiveModeStatus ).toHaveBeenCalledWith( { | ||
| appId: 7, | ||
| envId: 9, | ||
| enabled: false, | ||
| } ); | ||
| expect( trackEvent ).toHaveBeenCalledWith( | ||
| 'defensive_mode_disable_command_success', | ||
| expect.any( Object ) | ||
| ); | ||
| } ); | ||
|
|
||
| it( 'exits with error on production without skip-confirmation in non-interactive mode', async () => { | ||
| const opts = { | ||
| app: { id: 7, name: 'demo', organization: { id: 1, salesforceId: 'X' } }, | ||
| env: { id: 9, type: 'production' }, | ||
| skipConfirmation: false, | ||
| nonInteractive: true, | ||
| }; | ||
| await expect( defensiveModeDisableCommand( [], opts ) ).rejects.toBe( 'EXIT' ); | ||
| expect( updateDefensiveModeStatus ).not.toHaveBeenCalled(); | ||
| expect( trackEvent ).toHaveBeenCalledWith( | ||
| 'defensive_mode_disable_command_cancelled', | ||
| expect.any( Object ) | ||
| ); | ||
| } ); | ||
| } ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import { describe, expect, it, jest, beforeEach } from '@jest/globals'; | ||
|
|
||
| import { defensiveModeEnableCommand } from '../../src/bin/vip-defensive-mode-enable'; | ||
| import command from '../../src/lib/cli/command'; | ||
| import { updateDefensiveModeStatus } from '../../src/lib/defensive-mode/api'; | ||
| import { trackEvent } from '../../src/lib/tracker'; | ||
|
|
||
| function mockExit() { | ||
| throw 'EXIT'; | ||
| } | ||
| jest.spyOn( console, 'log' ).mockImplementation( () => {} ); | ||
| jest.spyOn( console, 'error' ).mockImplementation( () => {} ); | ||
| jest.spyOn( process, 'exit' ).mockImplementation( mockExit ); | ||
|
|
||
| jest.mock( '../../src/lib/cli/command', () => { | ||
| const commandMock = { | ||
| argv: () => commandMock, | ||
| examples: () => commandMock, | ||
| option: () => commandMock, | ||
| }; | ||
| return jest.fn( () => commandMock ); | ||
| } ); | ||
|
|
||
| jest.mock( '../../src/lib/defensive-mode/api', () => ( { | ||
| updateDefensiveModeStatus: jest.fn( () => | ||
| Promise.resolve( { success: true, message: 'enabled' } ) | ||
| ), | ||
| appQuery: 'mock-app-query', | ||
| } ) ); | ||
|
|
||
| jest.mock( '../../src/lib/tracker', () => ( { | ||
| trackEvent: jest.fn( () => Promise.resolve() ), | ||
| } ) ); | ||
|
|
||
| const mockUpdate = updateDefensiveModeStatus; | ||
| const mockTrack = trackEvent; | ||
|
|
||
| describe( 'vip defensive-mode enable', () => { | ||
| it( 'registers as a command', () => { | ||
| expect( command ).toHaveBeenCalled(); | ||
| } ); | ||
| } ); | ||
|
|
||
| describe( 'defensiveModeEnableCommand', () => { | ||
| beforeEach( () => { | ||
| jest.clearAllMocks(); | ||
| } ); | ||
|
|
||
| it( 'calls updateDefensiveModeStatus with enabled=true', async () => { | ||
| const opts = { | ||
| app: { id: 7, name: 'demo', organization: { id: 1, salesforceId: 'X' } }, | ||
| env: { id: 9, type: 'develop' }, | ||
| skipConfirmation: true, | ||
| }; | ||
| await defensiveModeEnableCommand( [], opts ); | ||
| expect( mockUpdate ).toHaveBeenCalledWith( { | ||
| appId: 7, | ||
| envId: 9, | ||
| enabled: true, | ||
| } ); | ||
| expect( mockTrack ).toHaveBeenCalledWith( | ||
| 'defensive_mode_enable_command_execute', | ||
| expect.any( Object ) | ||
| ); | ||
| expect( mockTrack ).toHaveBeenCalledWith( | ||
| 'defensive_mode_enable_command_success', | ||
| expect.any( Object ) | ||
| ); | ||
| } ); | ||
|
|
||
| it( 'exits with error on production without skip-confirmation in non-interactive mode', async () => { | ||
| const opts = { | ||
| app: { id: 7, name: 'demo', organization: { id: 1, salesforceId: 'X' } }, | ||
| env: { id: 9, type: 'production' }, | ||
| skipConfirmation: false, | ||
| nonInteractive: true, | ||
| }; | ||
| await expect( defensiveModeEnableCommand( [], opts ) ).rejects.toBe( 'EXIT' ); | ||
| expect( mockUpdate ).not.toHaveBeenCalled(); | ||
| expect( mockTrack ).toHaveBeenCalledWith( | ||
| 'defensive_mode_enable_command_cancelled', | ||
| expect.any( Object ) | ||
| ); | ||
| } ); | ||
| } ); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Test gap:
configureproduction guard behavior is unverified__tests__/bin/vip-defensive-mode-configure.js(no production/non-interactive guard case)env.type:'production'+nonInteractive:trueassertion.--skip-confirmation) on production in non-interactive mode once Finding 1 is fixed.