Skip to content
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

Feat: Bearer token based auth for Pact Brokers #44

Merged
merged 3 commits into from
Oct 8, 2024
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
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,24 @@ pacts from the broker by Pact Broker version tags.
If the pact broker has basic auth enabled, pass a --user option with username and password joined by a colon
(i.e. THE_USERNAME:THE_PASSWORD) to access the pact broker resources.

If the pact broker has bearer token auth enabled, pass a --token option along with the token to access the pact broker resources.

You can also set the following environment variables

- Basic Auth
- `PACT_BROKER_USERNAME`
- `PACT_BROKER_PASSWORD`
- Bearer Auth
- `PACT_BROKER_TOKEN`

Note: command line options will take precedence over environment variables.

Options:
-V, --version output the version number
-p, --provider [string] The name of the provider in the pact broker
-t, --tag [string] The tag to filter pacts retrieved from the pact broker
-u, --user [USERNAME:PASSWORD] The basic auth username and password to access the pact broker
-u, --user [USERNAME:PASSWORD] The basic auth username and password to access the pact broker (env - PACT_BROKER_USERNAME:PACT_BROKER_PASSWORD)
-u, --token [string] The bearer token to access the pact broker (env - PACT_BROKER_TOKEN)
-a, --analyticsUrl [string] The url to send analytics events to as a http post
-o, --outputDepth [integer] Specifies the number of times to recurse while formatting the output objects. This is useful in case of large complicated objects or schemas. (default: 4)
-A, --additionalPropertiesInResponse [boolean] allow additional properties in response bodies, default false
Expand Down Expand Up @@ -356,6 +369,22 @@ If the Pact Broker is behind basic auth, you can pass credentials with the `--us
swagger-mock-validator /path/to/swagger.json https://pact-broker.com --provider my-provider-name --user BASIC_AUTH_USER:BASIC_AUTH_PASSWORD
```

You can also use environment variables
```
PACT_BROKER_USERNAME=BASIC_AUTH_USER PACT_BROKER_PASSWORD=BASIC_AUTH_PASSWORD swagger-mock-validator /path/to/swagger.json https://pact-broker.com --provider my-provider-name
```

If the Pact Broker is behind bearer auth, you can pass credentials with the `--token` option while invoking the tool.

```
swagger-mock-validator /path/to/swagger.json https://pact-broker.com --provider my-provider-name --token bar
```

You can also use environment variables
```
PACT_BROKER_TOKEN=bar swagger-mock-validator /path/to/swagger.json https://pact-broker.com --provider my-provider-name
```

### Analytics (Opt-In)

The tool can be configured to send analytics events to a server of your choosing. Use the `--analyticsUrl` flag to pass a url that the tool should post the event to. The tool will send this event via a http post request and will timeout after 5 seconds. See [analytics.ts](lib/swagger-mock-validator/analytics.ts) for the post body schema.
Expand Down
29 changes: 26 additions & 3 deletions lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ program
.arguments('<swagger> <mock>')
.option('-p, --provider [string]', 'The name of the provider in the pact broker')
.option('-t, --tag [string]', 'The tag to filter pacts retrieved from the pact broker')
.option('-u, --user [USERNAME:PASSWORD]', 'The basic auth username and password to access the pact broker')
.option('-u, --user [USERNAME:PASSWORD]', 'The basic auth username and password to access the pact broker (env - PACT_BROKER_USERNAME:PACT_BROKER_PASSWORD)')
.option('-b, --token [string]', 'The bearer token to access the pact broker (env - PACT_BROKER_TOKEN)')
.option('-a, --analyticsUrl [string]', 'The url to send analytics events to as a http post')
.option('-o, --outputDepth [integer]', 'Specifies the number of times to recurse ' +
'while formatting the output objects. ' +
Expand All @@ -72,11 +73,33 @@ json file. Optionally, pass a --tag option alongside a --provider option to filt
pacts from the broker by Pact Broker version tags.

If the pact broker has basic auth enabled, pass a --user option with username and password joined by a colon
(i.e. THE_USERNAME:THE_PASSWORD) to access the pact broker resources.`
(i.e. THE_USERNAME:THE_PASSWORD) to access the pact broker resources.

If the pact broker has bearer token auth enabled, pass a --token option along with the token to access the pact broker resources.

You can also set the following environment variables

- Basic Auth
- PACT_BROKER_USERNAME
- PACT_BROKER_PASSWORD
- Bearer Auth
- PACT_BROKER_TOKEN

Note: command line options will take precedence over environment variables.
`
)
.action(async (swagger, mock, options) => {
try {
const swaggerMockValidator = SwaggerMockValidatorFactory.create(options.user);
if (
options.user == undefined &&
process.env.PACT_BROKER_USERNAME != undefined &&
process.env.PACT_BROKER_PASSWORD != undefined
) {
options.user = process.env.PACT_BROKER_USERNAME + ':' + process.env.PACT_BROKER_PASSWORD;
} else if (options.token == undefined && process.env.PACT_BROKER_TOKEN != undefined) {
options.token = process.env.PACT_BROKER_TOKEN;
}
const swaggerMockValidator = SwaggerMockValidatorFactory.create(options.user ?? options.token);

const result = await swaggerMockValidator.validate({
analyticsUrl: options.analyticsUrl,
Expand Down
12 changes: 11 additions & 1 deletion lib/swagger-mock-validator/clients/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@

export class HttpClient {
public async get(url: string, auth?: string): Promise<string> {
let authHeader: string | undefined;

if (auth) {
if (auth.includes(':')) {
authHeader = 'Basic ' + Buffer.from(auth).toString('base64');
} else {
authHeader = 'Bearer ' + auth;
}
}

const response = await axios.get(url, {
headers: {
...(auth ? {authorization: 'Basic ' + Buffer.from(auth).toString('base64')} : {})
...(authHeader ? { Authorization: authHeader } : {})
},
timeout: 30000,
transformResponse: (data) => data,
Expand All @@ -13,7 +23,7 @@
return response.data;
}

public async post(url: string, body: any): Promise<void> {

Check warning on line 26 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-osx (18.x)

Unexpected any. Specify a different type

Check warning on line 26 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-osx (16.x)

Unexpected any. Specify a different type

Check warning on line 26 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-osx (16.x)

Unexpected any. Specify a different type

Check warning on line 26 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-osx (18.x)

Unexpected any. Specify a different type

Check warning on line 26 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-ubuntu (18.x)

Unexpected any. Specify a different type

Check warning on line 26 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-ubuntu (18.x)

Unexpected any. Specify a different type

Check warning on line 26 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-ubuntu (16.x)

Unexpected any. Specify a different type

Check warning on line 26 in lib/swagger-mock-validator/clients/http-client.ts

View workflow job for this annotation

GitHub Actions / build-and-test-ubuntu (16.x)

Unexpected any. Specify a different type
await axios.post(url, body, {
timeout: 5000,
validateStatus: (status) => status >= 200 && status <= 299
Expand Down
88 changes: 87 additions & 1 deletion test/e2e/cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import {expectToFail} from '../helpers/expect-to-fail';
interface InvokeCommandOptions {
analyticsUrl?: string;
auth?: string;
token?: string;
mock: string;
providerName?: string;
swagger: string;
tag?: string;
outputDepth?: string;
envVars?: string;
}

const execute = (command: string): Promise<string> => {
Expand All @@ -28,7 +30,9 @@ const execute = (command: string): Promise<string> => {
};

const invokeCommand = (options: InvokeCommandOptions): Promise<string> => {
let command = `./bin/swagger-mock-validator.mjs ${options.swagger} ${options.mock}`;
let command = `${options.envVars ? `${options.envVars} ` : ''}./bin/swagger-mock-validator.mjs ${options.swagger} ${
options.mock
}`;

if (options.providerName) {
command += ` --provider ${options.providerName}`;
Expand All @@ -46,6 +50,10 @@ const invokeCommand = (options: InvokeCommandOptions): Promise<string> => {
command += ` --user ${options.auth}`;
}

if (options.token) {
command += ` --token ${options.token}`;
}

if (options.outputDepth) {
command += ` --outputDepth ${options.outputDepth}`;
}
Expand Down Expand Up @@ -387,7 +395,85 @@ describe('swagger-mock-validator/cli', () => {
jasmine.stringMatching('test/e2e/fixtures/pact-broker.json')
);
}, 30000);
it('should make an bearer token authenticated request to the provided pact broker url when asked to do so', async () => {
const token = 'token';

await invokeCommand({
token,
mock: urlTo('test/e2e/fixtures/pact-broker.json'),
providerName: 'provider-1',
swagger: urlTo('test/e2e/fixtures/swagger-provider.json')
});

expect(mockPactBroker.get).toHaveBeenCalledWith(
jasmine.objectContaining({authorization: 'Bearer token'}),
jasmine.stringMatching('test/e2e/fixtures/pact-broker.json')
);
}, 30000);
it('should make an authenticated request with a user/pass combo read from PACT_BROKER_USERNAME/PACT_BROKER_PASSWORD variable', async () => {
const user = 'user';
const pass = 'pass';

await invokeCommand({
envVars: `PACT_BROKER_USERNAME=${user} PACT_BROKER_PASSWORD=${pass}`,
mock: urlTo('test/e2e/fixtures/pact-broker.json'),
providerName: 'provider-1',
swagger: urlTo('test/e2e/fixtures/swagger-provider.json')
});

expect(mockPactBroker.get).toHaveBeenCalledWith(
jasmine.objectContaining({authorization: 'Basic dXNlcjpwYXNz'}),
jasmine.stringMatching('test/e2e/fixtures/pact-broker.json')
);
}, 30000);
it('should make an authenticated request with a bearer token read from PACT_BROKER_TOKEN variable', async () => {
const token = 'token';

await invokeCommand({
envVars: `PACT_BROKER_TOKEN=${token}`,
mock: urlTo('test/e2e/fixtures/pact-broker.json'),
providerName: 'provider-1',
swagger: urlTo('test/e2e/fixtures/swagger-provider.json')
});

expect(mockPactBroker.get).toHaveBeenCalledWith(
jasmine.objectContaining({authorization: 'Bearer token'}),
jasmine.stringMatching('test/e2e/fixtures/pact-broker.json')
);
}, 30000);
it('should prefer user variable, even if PACT_BROKER_USERNAME/PACT_BROKER_PASSWORD variable are set', async () => {
const user = 'user';
const pass = 'pass';

await invokeCommand({
auth: `${user}:${pass}`,
envVars: `PACT_BROKER_USERNAME=${user}_env PACT_BROKER_PASSWORD=${pass}_env`,
mock: urlTo('test/e2e/fixtures/pact-broker.json'),
providerName: 'provider-1',
swagger: urlTo('test/e2e/fixtures/swagger-provider.json')
});

expect(mockPactBroker.get).toHaveBeenCalledWith(
jasmine.objectContaining({authorization: 'Basic dXNlcjpwYXNz'}),
jasmine.stringMatching('test/e2e/fixtures/pact-broker.json')
);
}, 30000);
it('should prefer user variable, even if PACT_BROKER_TOKEN variable are set', async () => {
const token = 'token';

await invokeCommand({
token,
envVars: `PACT_BROKER_TOKEN=${token}_env`,
mock: urlTo('test/e2e/fixtures/pact-broker.json'),
providerName: 'provider-1',
swagger: urlTo('test/e2e/fixtures/swagger-provider.json')
});

expect(mockPactBroker.get).toHaveBeenCalledWith(
jasmine.objectContaining({authorization: 'Bearer token'}),
jasmine.stringMatching('test/e2e/fixtures/pact-broker.json')
);
}, 30000);
it('should format output objects to depth 0', async () => {
const result = await invokeCommand({
mock: 'test/e2e/fixtures/pact-working-consumer.json',
Expand Down
Loading