Skip to content

Commit 4334a30

Browse files
author
Victor Moreno
committed
Support base paths in api specs
1 parent adce3a8 commit 4334a30

File tree

4 files changed

+340
-40
lines changed

4 files changed

+340
-40
lines changed

src/bin.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ async function main(): Promise<void> {
2727
type: 'string',
2828
default: 'SimpleRest',
2929
choices: ['SimpleRest'] as const
30+
})
31+
.option('base-path', {
32+
describe: 'The base path of your API. Include the path portion of the url only. ' +
33+
'If your API spec has a `servers` entry, this parameter must align with one of the servers in the api spec.',
34+
type: 'string'
3035
});
3136
})
3237
.command('generate-policies', 'Generate policies for a Cedar schema', (yargs) => {
@@ -47,6 +52,7 @@ async function main(): Promise<void> {
4752
const apiSpecFile = argv['api-spec'] as string;
4853
const namespace = argv.namespace as string;
4954
const mappingType = argv['mapping-type'] as MappingType;
55+
const serverBasePath = typeof argv['base-path'] === 'string' ? argv['base-path'] : undefined;
5056

5157
// Check if API spec file exists
5258
if (!fs.existsSync(apiSpecFile)) {
@@ -63,11 +69,12 @@ async function main(): Promise<void> {
6369
process.exit(1);
6470
}
6571

66-
const authMapping = Tools.generateApiMappingSchemaFromOpenAPISpec(
72+
const authMapping = Tools.generateApiMappingSchemaFromOpenAPISpec({
6773
openApiSpec,
6874
namespace,
69-
mappingType
70-
);
75+
mappingType,
76+
basePath: serverBasePath,
77+
});
7178

7279
const fileNames = ['v2.cedarschema.json', 'v4.cedarschema.json'];
7380
console.log(`Cedar schema successfully generated. Your schema files are named: ${fileNames.join(', ')}.`);

src/tools.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,71 @@ export interface SimpleRestAuthMapping {
1515
schemaV4: string;
1616
}
1717

18+
export interface GenerateSchemaFromOpenApiSpecOptions {
19+
openApiSpec: OpenAPIV3.Document;
20+
namespace: string;
21+
mappingType: MappingType;
22+
basePath?: string;
23+
}
24+
1825
export class Tools {
1926
private static openAPIToCedarPrimitiveTypeMap = {
2027
string: {type: 'String' as const},
2128
number: {type: 'Long' as const},
2229
integer: {type: 'Long' as const},
2330
boolean: {type: 'Boolean' as const},
2431
}
32+
private static sanitizePath(pathStr: string): string {
33+
const trimmed = pathStr.split('/')
34+
.map(segment => segment.trim())
35+
.filter(segment => segment !== '');
36+
return `/${trimmed.join('/')}`;
37+
}
2538
/**
2639
* How action names are computed:
2740
* - If your API spec has operation id's then those are used as cedar actions
2841
* - Otherwise the action name is the http verb and the path template
2942
* How resource names are computed:
3043
* - For the SimpleRest mapping type, the resource is always {namespace}::Application::"{namespace}"
31-
* @param openApiSpec an openapi v3 spec as parsed json
32-
* @param namespace cedar namespace for your application
44+
* @param options of type GenerateSchemaFromOpenApiSpecOptions. Includes openApiSpec, namespace, mappingType
3345
* @returns
3446
*/
35-
public static generateApiMappingSchemaFromOpenAPISpec(openApiSpec: OpenAPIV3.Document, namespace: string, mappingType: MappingType): AuthMapping {
47+
public static generateApiMappingSchemaFromOpenAPISpec(options: GenerateSchemaFromOpenApiSpecOptions): AuthMapping {
48+
const {openApiSpec, namespace, mappingType} = options;
3649
if (!openApiSpec.paths) {
3750
throw new Error('Invalid OpenAPI spec - missing paths object');
3851
}
3952

4053
if (!namespace) {
4154
throw new Error('Invalid input - missing namespace');
4255
}
56+
const servers = openApiSpec.servers;
57+
58+
if (options.basePath && Array.isArray(servers)) {
59+
const basePathExistsInServersArray = servers
60+
.map(server => server.url || '')
61+
.some(serverUrl => {
62+
const normalizedBasePath = this.sanitizePath(options.basePath || '');
63+
return serverUrl.endsWith(normalizedBasePath) || serverUrl.endsWith(`${normalizedBasePath}/`)
64+
});
65+
if (!basePathExistsInServersArray) {
66+
throw new Error('Base Path option was provided but it does not match any of the `servers` entries in the API spec.');
67+
}
68+
}
69+
70+
let basePath = '';
71+
if (Array.isArray(servers)) {
72+
if (servers.length > 1) {
73+
if (!options.basePath) {
74+
throw new Error('Invalid input. API spec specifies more than one `server` entry. Server Base Path parameter required for disambiguation.');
75+
}
76+
basePath = this.sanitizePath(options.basePath);
77+
} else if (servers.length === 1) {
78+
const fullBaseUrl = new URL(servers[0].url);
79+
basePath = this.sanitizePath(fullBaseUrl.pathname);
80+
}
81+
}
82+
4383

4484
const RESERVED_WORDS = ['if', 'in', 'is', '__cedar'];
4585
const schemaNamespaceRegex = /^[_a-zA-Z][_a-zA-Z0-9]*(?:::(?:[_a-zA-Z][_a-zA-Z0-9]*))*$/;
@@ -100,6 +140,7 @@ export class Tools {
100140
if (!operationObject) {
101141
continue;
102142
}
143+
const httpPathTemplateWithBasePath = `${basePath}${httpPathTemplate}`;
103144
const {actionName, actionDefinition} = Tools.generateActionDefinitionFromOperationObject(
104145
httpVerb,
105146
httpPathTemplate,
@@ -112,7 +153,7 @@ export class Tools {
112153
...actionDefinition,
113154
annotations: {
114155
httpVerb,
115-
httpPathTemplate,
156+
httpPathTemplate: httpPathTemplateWithBasePath,
116157
}
117158
},
118159

@@ -231,6 +272,8 @@ export class Tools {
231272
cedarExtension.appliesToResourceTypes.every((value) => typeof value === 'string');
232273
if (isValidValue) {
233274
resourceTypes = cedarExtension.appliesToResourceTypes;
275+
} else {
276+
throw new Error(`Invalid x-cedar extension in operation definition for ${httpVerb} ${httpPathTemplate}`);
234277
}
235278
}
236279
let attributes = {};

tests/schemaGenPropTests.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,11 @@ describe('schema generation proptests', () => {
122122
};
123123
}
124124
}
125-
const generatedCedarSchemaStr = Tools.generateApiMappingSchemaFromOpenAPISpec(openApiSpec, 'NS', 'SimpleRest').schemaV4;
125+
const generatedCedarSchemaStr = Tools.generateApiMappingSchemaFromOpenAPISpec({
126+
openApiSpec,
127+
namespace: 'NS',
128+
mappingType: 'SimpleRest',
129+
}).schemaV4;
126130
const generatedCedarSchema = JSON.parse(generatedCedarSchemaStr);
127131
expect(Object.keys(generatedCedarSchema['NS'].actions).length).to.equal(numActionsInApiSpec);
128132
}
@@ -167,7 +171,11 @@ describe('schema generation proptests', () => {
167171
}
168172
} as OpenAPIV3.Document;
169173

170-
const generatedCedarSchemaStr = Tools.generateApiMappingSchemaFromOpenAPISpec(openApiSpec, 'NS', 'SimpleRest').schemaV4;
174+
const generatedCedarSchemaStr = Tools.generateApiMappingSchemaFromOpenAPISpec({
175+
openApiSpec,
176+
namespace: 'NS',
177+
mappingType: 'SimpleRest',
178+
}).schemaV4;
171179
const generatedCedarSchema = JSON.parse(generatedCedarSchemaStr);
172180

173181
expect(generatedCedarSchema.NS.commonTypes).toBeDefined();

0 commit comments

Comments
 (0)