Skip to content

Commit

Permalink
feat(oas): add full support for API key security schemes (#178)
Browse files Browse the repository at this point in the history
closes #175
  • Loading branch information
derevnjuk authored Feb 20, 2023
1 parent 987ec8e commit baae5b4
Show file tree
Hide file tree
Showing 38 changed files with 604 additions and 191 deletions.
44 changes: 34 additions & 10 deletions packages/oas/src/converter/DefaultConverter.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import { Converter } from './Converter';
import type { Converter } from './Converter';
import { BaseUrlParser, SubConverterRegistry } from './parts';
import { SubPart } from './SubPart';
import $RefParser, { JSONSchema } from '@apidevtools/json-schema-ref-parser';
import {
SecurityRequirementsFactory,
SecurityRequirementsParser
} from './security';
import type { PathItemObject } from '../types';
import { getOperation } from '../utils';
import $RefParser, { JSONSchema } from '@apidevtools/json-schema-ref-parser';
import type {
Header,
OpenAPI,
OpenAPIV2,
OpenAPIV3,
PostData,
QueryString,
Request
} from '@har-sdk/core';
import pointer from 'json-pointer';

type PathItemObject = OpenAPIV2.PathItemObject | OpenAPIV3.PathItemObject;

export class DefaultConverter implements Converter {
private spec: OpenAPI.Document;
private securityRequirements?: SecurityRequirementsParser<OpenAPI.Document>;

constructor(
private readonly baseUrlParser: BaseUrlParser,
private readonly subConverterRegistry: SubConverterRegistry
private readonly subConverterRegistry: SubConverterRegistry,
private readonly securityRequirementsFactory: SecurityRequirementsFactory
) {}

public async convert(spec: OpenAPI.Document): Promise<Request[]> {
Expand All @@ -29,6 +33,10 @@ export class DefaultConverter implements Converter {
{ resolve: { file: false, http: false } }
)) as OpenAPI.Document;

this.securityRequirements = this.securityRequirementsFactory.create(
this.spec
);

return Object.entries(this.spec.paths).flatMap(
([path, pathMethods]: [string, PathItemObject]) =>
Object.keys(pathMethods)
Expand All @@ -53,9 +61,8 @@ export class DefaultConverter implements Converter {
method
);

return {
const request: Omit<Request, 'url'> = {
queryString,
url: this.buildUrl(path, method, queryString),
method: method.toUpperCase(),
headers: this.convertPart<Header[]>(SubPart.HEADERS, path, method),
httpVersion: 'HTTP/1.1',
Expand All @@ -64,6 +71,23 @@ export class DefaultConverter implements Converter {
bodySize: 0,
...(postData ? { postData } : {})
};

this.authorizeRequest(path, method, request);

return {
...request,
url: this.buildUrl(path, method, queryString)
};
}

private authorizeRequest(
path: string,
method: string,
request: Omit<Request, 'url'>
): void {
const operation = getOperation(this.spec, path, method);
const claims = this.securityRequirements?.parse(operation) ?? [];
claims.forEach((x) => x.authorizeRequest(request));
}

private buildUrl(
Expand All @@ -85,7 +109,7 @@ export class DefaultConverter implements Converter {
private serializeQueryString(items: QueryString[]): string {
return items.length
? `?${items
.map((p) => Object.values(p).map((x) => encodeURIComponent(x)))
.map((p) => [p.name, p.value].map(encodeURIComponent))
.map(([name, value]: string[]) => `${name}=${value}`)
.join('&')}`
: '';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ConvertError } from '../../errors';
import { ConvertError } from '../errors';
import { sample, Schema } from '@har-sdk/openapi-sampler';
import pointer from 'json-pointer';
import { OpenAPI } from '@har-sdk/core';
import type { OpenAPI } from '@har-sdk/core';

export class Sampler {
public sampleParam(
Expand Down
4 changes: 3 additions & 1 deletion packages/oas/src/converter/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './parts';
export * from './security';
export * from './Converter';
export * from './DefaultConverter';
export * from './parts';
export * from './Sampler';
2 changes: 1 addition & 1 deletion packages/oas/src/converter/parts/BaseUrlParser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isOASV2, isOASV3 } from '../../utils';
import { ConvertError } from '../../errors';
import { Sampler } from './Sampler';
import { Sampler } from '../Sampler';
import { UriTemplator } from './UriTemplator';
import {
normalizeUrl,
Expand Down
2 changes: 1 addition & 1 deletion packages/oas/src/converter/parts/SubConverterFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Oas2HeadersConverter, Oas3HeadersConverter } from './headers';
import { Oas2PathConverter, Oas3PathConverter } from './path';
import { Oas2BodyConverter, Oas3RequestBodyConverter } from './postdata';
import { Oas2QueryStringConverter, Oas3QueryStringConverter } from './query';
import { Sampler } from './Sampler';
import { Sampler } from '../Sampler';
import { OpenAPI, OpenAPIV2, OpenAPIV3 } from '@har-sdk/core';

export class SubConverterFactory {
Expand Down
121 changes: 9 additions & 112 deletions packages/oas/src/converter/parts/headers/HeadersConverter.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
import { OperationObject, ParameterObject } from '../../../types';
import { filterLocationParams, getParameters } from '../../../utils';
import {
filterLocationParams,
getOperation,
getParameters
} from '../../../utils';
import { LocationParam } from '../LocationParam';
import { Sampler } from '../Sampler';
import { Sampler } from '../../Sampler';
import { SubConverter } from '../../SubConverter';
import { Header, OpenAPI, OpenAPIV2, OpenAPIV3 } from '@har-sdk/core';
import { Header, OpenAPI } from '@har-sdk/core';
import jsonPointer from 'json-pointer';

type SecurityRequirementObject =
| OpenAPIV2.SecurityRequirementObject
| OpenAPIV3.SecurityRequirementObject;

type SecuritySchemeObject =
| OpenAPIV2.SecuritySchemeObject
| OpenAPIV3.SecuritySchemeObject;

export abstract class HeadersConverter<T extends OpenAPI.Document>
implements SubConverter<Header[]>
{
protected constructor(
protected readonly spec: T,
private readonly spec: T,
private readonly sampler: Sampler
) {}

Expand All @@ -32,46 +28,18 @@ export abstract class HeadersConverter<T extends OpenAPI.Document>
headerParam: LocationParam<ParameterObject>
): Header;

protected abstract getSecuritySchemes():
| Record<string, SecuritySchemeObject>
| undefined;

public convert(path: string, method: string): Header[] {
const headers: Header[] = [];
const pathObj = this.spec.paths[path][method];
const pathObj = getOperation(this.spec, path, method);

headers.push(...this.createContentTypeHeaders(pathObj));
headers.push(...this.createAcceptHeaders(pathObj));

headers.push(...this.parseFromParams(path, method));
headers.push(...this.parseSecurityRequirements(pathObj));

return headers;
}

protected parseApiKeyScheme(
securityScheme: SecuritySchemeObject
): Header | undefined {
if ('in' in securityScheme && securityScheme.in === 'header') {
return this.createAuthHeader('API-Key', securityScheme.name);
}
}

protected createAuthHeader(
type: 'Basic' | 'Bearer' | 'API-Key',
header = 'authorization'
): Header {
const token = this.sampler.sample({
type: 'string',
format: 'base64'
});

return this.createHeader(
header,
`${type === 'API-Key' ? '' : `${type} `}${token}`
);
}

protected createHeader(name: string, value: string): Header {
return {
value,
Expand All @@ -85,20 +53,6 @@ export abstract class HeadersConverter<T extends OpenAPI.Document>
);
}

protected parseSecurityScheme(
securityScheme: SecuritySchemeObject
): Header | undefined {
const authType = securityScheme.type.toLowerCase();
switch (authType) {
case 'basic':
return this.createAuthHeader('Basic');
case 'oauth2':
return this.createAuthHeader('Bearer');
case 'apikey':
return this.parseApiKeyScheme(securityScheme);
}
}

private parseFromParams(path: string, method: string): Header[] {
const params: ParameterObject[] = getParameters(this.spec, path, method);
const tokens = ['paths', path, method];
Expand All @@ -125,61 +79,4 @@ export abstract class HeadersConverter<T extends OpenAPI.Document>
});
});
}

private getSecurityRequirementObjects(
pathObj: OperationObject
): SecurityRequirementObject[] | undefined {
if (Array.isArray(pathObj.security)) {
return pathObj.security;
} else if (Array.isArray(this.spec.security)) {
return this.spec.security;
}
}

private parseSecurityRequirements(pathObj: OperationObject): Header[] {
const securityRequirements = this.getSecurityRequirementObjects(pathObj);
if (!securityRequirements) {
return [];
}

const securitySchemes = this.getSecuritySchemes();
if (!securitySchemes) {
return [];
}

return this.createAuthHeaders(securityRequirements, securitySchemes);
}

private createAuthHeaders(
securityRequirements: SecurityRequirementObject[],
securitySchemes: Record<string, SecuritySchemeObject>
): Header[] {
for (const obj of securityRequirements) {
const headers = this.parseSecurityRequirement(obj, securitySchemes);
if (headers.length) {
return headers;
}
}

return [];
}

private parseSecurityRequirement(
securityRequirement: SecurityRequirementObject,
securitySchemes: Record<string, SecuritySchemeObject>
): Header[] {
const headers: Header[] = [];

for (const schemeName of Object.keys(securityRequirement)) {
const header = securitySchemes[schemeName]
? this.parseSecurityScheme(securitySchemes[schemeName])
: undefined;

if (header) {
headers.push(header);
}
}

return headers;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LocationParam } from '../LocationParam';
import { Oas2ValueSerializer } from '../Oas2ValueSerializer';
import { Sampler } from '../Sampler';
import { Sampler } from '../../Sampler';
import { HeadersConverter } from './HeadersConverter';
import { Header, OpenAPIV2 } from '@har-sdk/core';

Expand Down Expand Up @@ -29,10 +29,4 @@ export class Oas2HeadersConverter extends HeadersConverter<OpenAPIV2.Document> {
this.oas2ValueSerializer.serialize(headerParam) as string
);
}

protected getSecuritySchemes():
| Record<string, OpenAPIV2.SecuritySchemeObject>
| undefined {
return this.spec.securityDefinitions;
}
}
34 changes: 1 addition & 33 deletions packages/oas/src/converter/parts/headers/Oas3HeadersConverter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isObject } from '../../../utils';
import { LocationParam } from '../LocationParam';
import { Sampler } from '../Sampler';
import { Sampler } from '../../Sampler';
import { UriTemplator } from '../UriTemplator';
import { HeadersConverter } from './HeadersConverter';
import { Header, OpenAPIV3 } from '@har-sdk/core';
Expand Down Expand Up @@ -59,36 +59,4 @@ export class Oas3HeadersConverter extends HeadersConverter<OpenAPIV3.Document> {
)
);
}

protected getSecuritySchemes():
| Record<string, OpenAPIV3.SecuritySchemeObject>
| undefined {
return this.spec.components?.securitySchemes as Record<
string,
OpenAPIV3.SecuritySchemeObject
>;
}

protected parseSecurityScheme(
securityScheme: OpenAPIV3.SecuritySchemeObject
): Header | undefined {
const header = super.parseSecurityScheme(securityScheme);
if (header) {
return header;
}

const httpScheme =
'scheme' in securityScheme
? securityScheme.scheme.toLowerCase()
: undefined;

switch (httpScheme) {
case 'basic':
return this.createAuthHeader('Basic');
case 'bearer':
return this.createAuthHeader('Bearer');
default:
return this.parseApiKeyScheme(securityScheme);
}
}
}
1 change: 0 additions & 1 deletion packages/oas/src/converter/parts/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { BaseUrlParser } from './BaseUrlParser';
export { Sampler } from './Sampler';
export { SubConverterFactory } from './SubConverterFactory';
export { SubConverterRegistry } from './SubConverterRegistry';
2 changes: 1 addition & 1 deletion packages/oas/src/converter/parts/path/Oas2PathConverter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LocationParam } from '../LocationParam';
import { Sampler } from '../Sampler';
import { Sampler } from '../../Sampler';
import { Oas2ValueSerializer } from '../Oas2ValueSerializer';
import { PathConverter } from './PathConverter';
import { OpenAPIV2 } from '@har-sdk/core';
Expand Down
2 changes: 1 addition & 1 deletion packages/oas/src/converter/parts/path/Oas3PathConverter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ParameterObject } from '../../../types';
import { LocationParam } from '../LocationParam';
import { Sampler } from '../Sampler';
import { Sampler } from '../../Sampler';
import { UriTemplator } from '../UriTemplator';
import { PathConverter } from './PathConverter';
import { OpenAPIV3 } from '@har-sdk/core';
Expand Down
2 changes: 1 addition & 1 deletion packages/oas/src/converter/parts/path/PathConverter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ParameterObject } from '../../../types';
import { getParameters, filterLocationParams } from '../../../utils';
import { LocationParam } from '../LocationParam';
import { Sampler } from '../Sampler';
import { Sampler } from '../../Sampler';
import { SubConverter } from '../../SubConverter';
import jsonPointer from 'json-pointer';
import { OpenAPI } from '@har-sdk/core';
Expand Down
2 changes: 1 addition & 1 deletion packages/oas/src/converter/parts/postdata/BodyConverter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Sampler } from '../Sampler';
import { Sampler } from '../../Sampler';
import { SubConverter } from '../../SubConverter';
import { OpenAPI, OpenAPIV3, PostData } from '@har-sdk/core';
import { toXML, XmlElement } from 'jstoxml';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BodyConverter } from './BodyConverter';
import { Sampler } from '../Sampler';
import { Sampler } from '../../Sampler';
import { filterLocationParams, getParameters, isOASV2 } from '../../../utils';
import { OpenAPIV2, PostData } from '@har-sdk/core';

Expand Down
Loading

0 comments on commit baae5b4

Please sign in to comment.