Skip to content
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
13 changes: 10 additions & 3 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ async function main(): Promise<void> {
type: 'string',
default: 'SimpleRest',
choices: ['SimpleRest'] as const
})
.option('base-path', {
describe: 'The base path of your API. Include the path portion of the url only. ' +
'If your API spec has a `servers` entry, this parameter must align with one of the servers in the api spec.',
type: 'string'
});
})
.command('generate-policies', 'Generate policies for a Cedar schema', (yargs) => {
Expand All @@ -47,6 +52,7 @@ async function main(): Promise<void> {
const apiSpecFile = argv['api-spec'] as string;
const namespace = argv.namespace as string;
const mappingType = argv['mapping-type'] as MappingType;
const serverBasePath = typeof argv['base-path'] === 'string' ? argv['base-path'] : undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: might this make more sense as just basePath to be consistent with CLI option base-path? (or change the option to server-base-path)


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

const authMapping = Tools.generateApiMappingSchemaFromOpenAPISpec(
const authMapping = Tools.generateApiMappingSchemaFromOpenAPISpec({
openApiSpec,
namespace,
mappingType
);
mappingType,
basePath: serverBasePath,
});

const fileNames = ['v2.cedarschema.json', 'v4.cedarschema.json'];
console.log(`Cedar schema successfully generated. Your schema files are named: ${fileNames.join(', ')}.`);
Expand Down
51 changes: 47 additions & 4 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,71 @@ export interface SimpleRestAuthMapping {
schemaV4: string;
}

export interface GenerateSchemaFromOpenApiSpecOptions {
openApiSpec: OpenAPIV3.Document;
namespace: string;
mappingType: MappingType;
basePath?: string;
}

export class Tools {
private static openAPIToCedarPrimitiveTypeMap = {
string: {type: 'String' as const},
number: {type: 'Long' as const},
integer: {type: 'Long' as const},
boolean: {type: 'Boolean' as const},
}
private static sanitizePath(pathStr: string): string {
const trimmed = pathStr.split('/')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to add a .map(segment => segment.trim()) here to avoid accidental white space end up in the sanitized path

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I know the CLI specifically says to enter a base-path ...but might it be worth checking for schema/host and stripping that out too (or generating an error at the CLI section)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is just for the path not for the full url. Trim is a good call

.map(segment => segment.trim())
.filter(segment => segment !== '');
return `/${trimmed.join('/')}`;
}
/**
* How action names are computed:
* - If your API spec has operation id's then those are used as cedar actions
* - Otherwise the action name is the http verb and the path template
* How resource names are computed:
* - For the SimpleRest mapping type, the resource is always {namespace}::Application::"{namespace}"
* @param openApiSpec an openapi v3 spec as parsed json
* @param namespace cedar namespace for your application
* @param options of type GenerateSchemaFromOpenApiSpecOptions. Includes openApiSpec, namespace, mappingType
* @returns
*/
public static generateApiMappingSchemaFromOpenAPISpec(openApiSpec: OpenAPIV3.Document, namespace: string, mappingType: MappingType): AuthMapping {
public static generateApiMappingSchemaFromOpenAPISpec(options: GenerateSchemaFromOpenApiSpecOptions): AuthMapping {
const {openApiSpec, namespace, mappingType} = options;
if (!openApiSpec.paths) {
throw new Error('Invalid OpenAPI spec - missing paths object');
}

if (!namespace) {
throw new Error('Invalid input - missing namespace');
}
const servers = openApiSpec.servers;

if (options.basePath && Array.isArray(servers)) {
const basePathExistsInServersArray = servers
.map(server => server.url || '')
.some(serverUrl => {
const normalizedBasePath = this.sanitizePath(options.basePath || '');
return serverUrl.endsWith(normalizedBasePath) || serverUrl.endsWith(`${normalizedBasePath}/`)
});
if (!basePathExistsInServersArray) {
throw new Error('Base Path option was provided but it does not match any of the `servers` entries in the API spec.');
}
}

let basePath = '';
if (Array.isArray(servers)) {
if (servers.length > 1) {
if (!options.basePath) {
throw new Error('Invalid input. API spec specifies more than one `server` entry. Server Base Path parameter required for disambiguation.');
}
basePath = this.sanitizePath(options.basePath);
} else if (servers.length === 1) {
const fullBaseUrl = new URL(servers[0].url);
basePath = this.sanitizePath(fullBaseUrl.pathname);
}
}


const RESERVED_WORDS = ['if', 'in', 'is', '__cedar'];
const schemaNamespaceRegex = /^[_a-zA-Z][_a-zA-Z0-9]*(?:::(?:[_a-zA-Z][_a-zA-Z0-9]*))*$/;
Expand Down Expand Up @@ -100,6 +140,7 @@ export class Tools {
if (!operationObject) {
continue;
}
const httpPathTemplateWithBasePath = `${basePath}${httpPathTemplate}`;
const {actionName, actionDefinition} = Tools.generateActionDefinitionFromOperationObject(
httpVerb,
httpPathTemplate,
Expand All @@ -112,7 +153,7 @@ export class Tools {
...actionDefinition,
annotations: {
httpVerb,
httpPathTemplate,
httpPathTemplate: httpPathTemplateWithBasePath,
}
},

Expand Down Expand Up @@ -231,6 +272,8 @@ export class Tools {
cedarExtension.appliesToResourceTypes.every((value) => typeof value === 'string');
if (isValidValue) {
resourceTypes = cedarExtension.appliesToResourceTypes;
} else {
throw new Error(`Invalid x-cedar extension in operation definition for ${httpVerb} ${httpPathTemplate}`);
}
}
let attributes = {};
Expand Down
12 changes: 10 additions & 2 deletions tests/schemaGenPropTests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,11 @@ describe('schema generation proptests', () => {
};
}
}
const generatedCedarSchemaStr = Tools.generateApiMappingSchemaFromOpenAPISpec(openApiSpec, 'NS', 'SimpleRest').schemaV4;
const generatedCedarSchemaStr = Tools.generateApiMappingSchemaFromOpenAPISpec({
openApiSpec,
namespace: 'NS',
mappingType: 'SimpleRest',
}).schemaV4;
const generatedCedarSchema = JSON.parse(generatedCedarSchemaStr);
expect(Object.keys(generatedCedarSchema['NS'].actions).length).to.equal(numActionsInApiSpec);
}
Expand Down Expand Up @@ -167,7 +171,11 @@ describe('schema generation proptests', () => {
}
} as OpenAPIV3.Document;

const generatedCedarSchemaStr = Tools.generateApiMappingSchemaFromOpenAPISpec(openApiSpec, 'NS', 'SimpleRest').schemaV4;
const generatedCedarSchemaStr = Tools.generateApiMappingSchemaFromOpenAPISpec({
openApiSpec,
namespace: 'NS',
mappingType: 'SimpleRest',
}).schemaV4;
const generatedCedarSchema = JSON.parse(generatedCedarSchemaStr);

expect(generatedCedarSchema.NS.commonTypes).toBeDefined();
Expand Down
Loading