-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): add token authentication (#401)
* feat(cli): add token authentication MVP, need: - Refacto w/ login - Permission validation * handle anonymous in analytics cause an API key cannot be associated to a user with confidence * add doc page * Pr review Co-authored-by: Yassine <[email protected]>
- Loading branch information
1 parent
3d52153
commit 967877c
Showing
5 changed files
with
292 additions
and
14 deletions.
There are no files selected for viewing
This file contains 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,138 @@ | ||
jest.mock('../../lib/oauth/oauth'); | ||
jest.mock('../../lib/config/config'); | ||
jest.mock('../../hooks/analytics/analytics'); | ||
jest.mock('../../hooks/prerun/prerun'); | ||
jest.mock('../../lib/platform/authenticatedClient'); | ||
jest.mock('@coveord/platform-client'); | ||
import {test} from '@oclif/test'; | ||
import {mocked} from 'ts-jest/utils'; | ||
import {Config} from '../../lib/config/config'; | ||
import {AuthenticatedClient} from '../../lib/platform/authenticatedClient'; | ||
const mockedConfig = mocked(Config, true); | ||
const mockedAuthenticatedClient = mocked(AuthenticatedClient); | ||
|
||
describe('auth:token', () => { | ||
const mockConfigSet = jest.fn(); | ||
|
||
const mockConfigGet = jest.fn().mockReturnValue( | ||
Promise.resolve({ | ||
region: 'us-east-1', | ||
organization: 'foo', | ||
environment: 'prod', | ||
}) | ||
); | ||
|
||
const mockListOrgs = jest | ||
.fn() | ||
.mockReturnValue(Promise.resolve([{id: 'foo'}])); | ||
|
||
const mockGetHasAccessToOrg = jest | ||
.fn() | ||
.mockReturnValue(Promise.resolve(true)); | ||
|
||
beforeEach(() => { | ||
mockedAuthenticatedClient.mockImplementation( | ||
() => | ||
({ | ||
getAllOrgsUserHasAccessTo: mockListOrgs, | ||
getUserHasAccessToOrg: mockGetHasAccessToOrg, | ||
} as unknown as AuthenticatedClient) | ||
); | ||
|
||
mockedConfig.mockImplementation( | ||
() => | ||
({ | ||
get: mockConfigGet, | ||
set: mockConfigSet, | ||
} as unknown as Config) | ||
); | ||
}); | ||
|
||
afterEach(() => { | ||
mockConfigSet.mockClear(); | ||
}); | ||
|
||
test | ||
.command(['auth:token', '-e', 'foo']) | ||
.catch(/Expected --environment=foo/) | ||
.it('reject invalid environment', async () => {}); | ||
|
||
test | ||
.command(['auth:token', '-r', 'foo']) | ||
.catch(/Expected --region=foo/) | ||
.it('reject invalid region', async () => {}); | ||
|
||
['dev', 'qa', 'prod', 'hipaa'].forEach((environment) => { | ||
test | ||
.stdout() | ||
.command(['auth:token', '-e', environment, '-t', 'someToken']) | ||
.it(`writes the -e=${environment} flag to the configuration`, () => { | ||
expect(mockConfigSet).toHaveBeenCalledWith('environment', environment); | ||
}); | ||
}); | ||
|
||
[ | ||
'us-east-1', | ||
'eu-west-1', | ||
'eu-west-3', | ||
'ap-southeast-2', | ||
'us-west-2', | ||
].forEach((region) => { | ||
test | ||
.stdout() | ||
.command(['auth:token', '-r', region, '-t', 'someToken']) | ||
.it(`writes the -r=${region} flag and configuration`, () => { | ||
expect(mockConfigSet).toHaveBeenCalledWith('region', region); | ||
}); | ||
}); | ||
|
||
describe('writes the token and set anonymous to true in the configuration ', () => { | ||
test | ||
.stdout() | ||
.command(['auth:token', '-t', 'this-is-the-token']) | ||
.it('save token from oauth service', () => { | ||
expect(mockConfigSet).toHaveBeenCalledWith( | ||
'accessToken', | ||
'this-is-the-token' | ||
); | ||
expect(mockConfigSet).toHaveBeenCalledWith('anonymous', true); | ||
}); | ||
}); | ||
|
||
test | ||
.stdout() | ||
.command(['auth:token']) | ||
.exit(2) | ||
.it('fails when the token flag is not set'); | ||
|
||
test | ||
.stdout() | ||
.do(() => { | ||
mockGetHasAccessToOrg.mockReturnValueOnce(Promise.resolve(true)); | ||
}) | ||
.command(['auth:token', '-t', 'some-token']) | ||
.it( | ||
'succeed when the organization and the token flags are valid', | ||
(ctx) => { | ||
expect(ctx.stdout).toContain('Success'); | ||
} | ||
); | ||
|
||
test | ||
.do(() => { | ||
mockListOrgs.mockReturnValueOnce( | ||
Promise.resolve([{id: 'the_first_org_available'}]) | ||
); | ||
}) | ||
.stdout() | ||
.command(['auth:token', '-t', 'some-token']) | ||
.it( | ||
'find the org associated with the token and saves it in the config', | ||
() => { | ||
expect(mockConfigSet).toHaveBeenCalledWith( | ||
'organization', | ||
'the_first_org_available' | ||
); | ||
} | ||
); | ||
}); |
This file contains 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,110 @@ | ||
import {Command, flags} from '@oclif/command'; | ||
import {Config} from '../../lib/config/config'; | ||
import {AuthenticatedClient} from '../../lib/platform/authenticatedClient'; | ||
import { | ||
PlatformEnvironment, | ||
PlatformRegion, | ||
} from '../../lib/platform/environment'; | ||
import { | ||
buildAnalyticsFailureHook, | ||
buildAnalyticsSuccessHook, | ||
} from '../../hooks/analytics/analytics'; | ||
|
||
export default class Token extends Command { | ||
private configuration!: Config; | ||
public static description = | ||
'Log in to the Coveo Platform using the OAuth2 flow.'; | ||
|
||
public static examples = ['$ coveo auth:token']; | ||
|
||
public static flags = { | ||
region: flags.string({ | ||
char: 'r', | ||
options: [ | ||
'us-east-1', | ||
'eu-west-1', | ||
'eu-west-3', | ||
'ap-southeast-2', | ||
'us-west-2', | ||
], | ||
default: 'us-east-1', | ||
description: | ||
'The Coveo Platform region to log in to. See <https://docs.coveo.com/en/2976>.', | ||
}), | ||
environment: flags.string({ | ||
char: 'e', | ||
options: ['dev', 'qa', 'prod', 'hipaa'], | ||
default: 'prod', | ||
description: 'The Coveo Platform environment to log in to.', | ||
}), | ||
token: flags.string({ | ||
char: 't', | ||
description: | ||
'The API-Key that shall be used to authenticate you to the organization. See <https://github.com/coveo/cli/wiki/Using-the-CLI-using-an-API-Key>.', | ||
required: true, | ||
helpValue: 'xxx-api-key', | ||
}), | ||
}; | ||
|
||
public async run() { | ||
this.configuration = new Config(this.config.configDir, this.error); | ||
this.saveToken(); | ||
this.saveRegionAndEnvironment(); | ||
await this.fetchAndSaveOrgId(); | ||
await this.feedbackOnSuccessfulLogin(); | ||
this.config.runHook('analytics', buildAnalyticsSuccessHook(this, flags)); | ||
} | ||
|
||
public async catch(err?: Error) { | ||
const flags = this.flags; | ||
await this.config.runHook( | ||
'analytics', | ||
buildAnalyticsFailureHook(this, flags, err) | ||
); | ||
throw err; | ||
} | ||
|
||
private async feedbackOnSuccessfulLogin() { | ||
const cfg = this.configuration.get(); | ||
this.log(` | ||
Successfully logged in! | ||
Close your browser to continue. | ||
You are currently logged in: | ||
Organization: ${cfg.organization} | ||
Region: ${cfg.region} | ||
Environment: ${cfg.environment} | ||
Run auth:login --help to see the available options to log in to a different organization, region, or environment. | ||
`); | ||
} | ||
|
||
private saveToken() { | ||
const flags = this.flags; | ||
this.configuration.set('accessToken', flags.token); | ||
this.configuration.set('anonymous', true); | ||
} | ||
|
||
private saveRegionAndEnvironment() { | ||
const flags = this.flags; | ||
const cfg = this.configuration; | ||
cfg.set('environment', flags.environment as PlatformEnvironment); | ||
cfg.set('region', flags.region as PlatformRegion); | ||
} | ||
|
||
private async fetchAndSaveOrgId() { | ||
this.configuration.set( | ||
'organization', | ||
await this.pickFirstAvailableOrganization() | ||
); | ||
} | ||
|
||
private get flags() { | ||
const {flags} = this.parse(Token); | ||
return flags; | ||
} | ||
|
||
private async pickFirstAvailableOrganization() { | ||
const orgs = await new AuthenticatedClient().getAllOrgsUserHasAccessTo(); | ||
return orgs[0]?.id; | ||
} | ||
} |
This file contains 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 |
---|---|---|
|
@@ -42,19 +42,22 @@ describe('analytics hook', () => { | |
); | ||
}; | ||
|
||
const mockedUserGet = jest.fn(); | ||
const doMockPlatformClient = () => { | ||
mockedUserGet.mockReturnValue( | ||
Promise.resolve({ | ||
username: '[email protected]', | ||
displayName: 'bob', | ||
}) | ||
); | ||
mockedPlatformClient.mockImplementation( | ||
() => | ||
({ | ||
initialize: () => Promise.resolve(), | ||
user: { | ||
get: () => | ||
Promise.resolve({ | ||
username: '[email protected]', | ||
displayName: 'bob', | ||
}), | ||
get: mockedUserGet, | ||
}, | ||
} as PlatformClient) | ||
} as unknown as PlatformClient) | ||
); | ||
}; | ||
|
||
|
@@ -235,4 +238,24 @@ describe('analytics hook', () => { | |
); | ||
await expect(hook(getAnalyticsHook({}))).resolves.not.toThrow(); | ||
}); | ||
|
||
it('should not fetch userinfo if anonymous is set to true in the config', async () => { | ||
mockedConfig.mockImplementation( | ||
() => | ||
({ | ||
get: () => | ||
({ | ||
environment: 'dev', | ||
organization: 'foo', | ||
region: 'us-east-1', | ||
analyticsEnabled: true, | ||
anonymous: true, | ||
} as Configuration), | ||
} as Config) | ||
); | ||
|
||
await hook(getAnalyticsHook({})); | ||
|
||
expect(mockedUserGet).not.toBeCalled(); | ||
}); | ||
}); |
This file contains 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
This file contains 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