Skip to content

Commit

Permalink
feat: generate schema from openapi spec (#165)
Browse files Browse the repository at this point in the history
This feature allows users to generate a schema from an openApi spec in
the same way that the Verified Permissions console does it.

---------

Co-authored-by: Victor Moreno <[email protected]>
Co-authored-by: svenjaraether <[email protected]>
  • Loading branch information
3 people committed Jul 8, 2024
1 parent 8ac7dc6 commit 9b04d53
Show file tree
Hide file tree
Showing 7 changed files with 391 additions and 1 deletion.
34 changes: 34 additions & 0 deletions API.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ const policyStore = new PolicyStore(scope, "PolicyStore", {
});
```

## Schemas

If you want to have type safety when defining a schema, you can accomplish this in typescript. Simply use the `Schema` type exported by the `@cedar-policy/cedar-wasm`.

You can also generate a simple schema from a swagger file using the static function `schemaFromOpenApiSpec` in the PolicyStore construct. This functionality replicates what you can find in the AWS Verified Permissions console.

```ts
const validationSettingsStrict = {
mode: ValidationSettingsMode.STRICT,
};
const cedarJsonSchema = PolicyStore.schemaFromOpenApiSpec(
'path/to/swaggerfile.json',
'UserGroup',
);
const cedarSchema = {
cedarJson: JSON.stringify(cedarJsonSchema),
};
const policyStore = new PolicyStore(scope, "PolicyStore", {
schema: cedarSchema,
validationSettings: validationSettingsStrict,
description: "Policy store with schema generated from API Gateway",
});
```

## Identity Source

Define Identity Source with required properties:
Expand Down
60 changes: 60 additions & 0 deletions src/cedar-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,64 @@ export function validatePolicy(policyStatement: string, schemaStr: string) {
${validationResult.validationErrors.join('; ')}`,
);
}
}

export function cleanUpApiNameForNamespace(apiName: string): string {
const validCedarName = apiName.replace(/[^a-zA-Z0-9_]/g, '').trim();
if (validCedarName.length === 0) {
return 'ImportedApi';
}
if (/[0-9_]/.exec(validCedarName[0])) {
return `Api${validCedarName}`;
}
return validCedarName;
}

export function buildSchema(
namespace: string,
actionNames: string[],
principalGroupType?: string,
): Record<string, Record<string, any>> {
const additionalEntities: Record<string, any> = {};
if (principalGroupType) {
additionalEntities[principalGroupType] = {
shape: {
type: 'Record',
attributes: {},
},
};
}
const actions = actionNames.reduce((acc, actionName) => {
return {
...acc,
[actionName]: {
appliesTo: {
context: { type: 'Record', attributes: {} },
principalTypes: ['User'],
resourceTypes: ['Application'],
},
},
};
}, {});
return {
[namespace]: {
entityTypes: {
...additionalEntities,
User: {
shape: {
type: 'Record',
attributes: {},
},
memberOfTypes: principalGroupType ? [principalGroupType] : [],
},
Application: {
shape: {
type: 'Record',
attributes: {},
},
},
},
actions,
},
};
}
41 changes: 40 additions & 1 deletion src/policy-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as iam from 'aws-cdk-lib/aws-iam';
import { CfnPolicyStore } from 'aws-cdk-lib/aws-verifiedpermissions';
import { ArnFormat, IResource, Resource, Stack } from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { checkParseSchema, validatePolicy } from './cedar-helpers';
import { buildSchema, checkParseSchema, cleanUpApiNameForNamespace, validatePolicy } from './cedar-helpers';
import { Policy, PolicyDefinitionProperty } from './policy';
import {
AUTH_ACTIONS,
Expand Down Expand Up @@ -254,6 +254,45 @@ export class PolicyStore extends PolicyStoreBase {
return new Import(policyStoreArn, policyStoreId);
}

/**
* This method generates a schema based on an swagger file. It makes the same assumptions and decisions
* made in the Amazon Verified Permissions console. This feature is built for swagger files generated from an Amazon API Gateway
* export. It's possible that some swagger files generated by other tools will not work. In that case, please
* file an issue.
* @param swaggerFilePath absolute path to a swagger file in the local directory structure, in json format
* @param groupEntityTypeName optional parameter to specify the group entity type name. If passed, the schema's User type will have a parent of this type.
*/
public static schemaFromOpenApiSpec(swaggerFilePath: string, groupEntityTypeName?: string) {
const RELEVANT_HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head'];
const openApiSpecString = fs.readFileSync(swaggerFilePath, 'utf-8');
const openApiSpec = JSON.parse(openApiSpecString) as any;
if (!openApiSpec.paths) {
throw new Error('Invalid OpenAPI spec - missing paths object');
}
const namespace = cleanUpApiNameForNamespace(openApiSpec.info.title);

const pathUrls = Object.keys(openApiSpec.paths);
const actionNames = [];
for (const pathUrl of pathUrls) {
const pathDef = openApiSpec.paths[pathUrl];
if (!pathDef) {
continue;
}
let pathVerbs = Object.keys(pathDef);
if (pathVerbs.includes('x-amazon-apigateway-any-method')) {
pathVerbs = RELEVANT_HTTP_METHODS;
}
for (const httpVerb of pathVerbs) {
if (!RELEVANT_HTTP_METHODS.includes(httpVerb)) {
continue;
}
const actionName = `${httpVerb} ${pathUrl}`;
actionNames.push(actionName);
}
}
return buildSchema(namespace, actionNames, groupEntityTypeName);
}

private readonly policyStore: CfnPolicyStore;
/**
* ARN of the Policy Store.
Expand Down
10 changes: 10 additions & 0 deletions test/cedar-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { cleanUpApiNameForNamespace } from '../src/cedar-helpers';

describe('testing edge cases in cedar helpers', () => {
test('cleanUpApiNameForNamespace tests', () => {
expect(cleanUpApiNameForNamespace('test')).toBe('test');
expect(cleanUpApiNameForNamespace('bad-name')).toBe('badname');
expect(cleanUpApiNameForNamespace('---')).toBe('ImportedApi');
expect(cleanUpApiNameForNamespace('1234backend')).toBe('Api1234backend');
});
});
183 changes: 183 additions & 0 deletions test/podcastappswagger.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
{
"swagger" : "2.0",
"info" : {
"version" : "2024-06-18T20:07:31Z",
"title" : "PodcastApp"
},
"host" : "3se71cmpi4.execute-api.us-west-2.amazonaws.com",
"basePath" : "/prod",
"schemes" : [ "https" ],
"paths" : {
"/fakePatToGetUnitTestCoverage": null,
"/artists" : {
"get" : {
"responses" : { },
"security" : [ {
"AVPAuthorizer" : [ ]
} ]
},
"post" : {
"responses" : { }
},
"delete" : {
"responses" : { }
},
"options" : {
"consumes" : [ "application/json" ],
"responses" : {
"204" : {
"description" : "204 response",
"headers" : {
"Access-Control-Allow-Origin" : {
"type" : "string"
},
"Access-Control-Allow-Methods" : {
"type" : "string"
},
"Access-Control-Allow-Headers" : {
"type" : "string"
}
}
}
}
}
},
"/artists/{artistId}" : {
"delete" : {
"parameters" : [ {
"name" : "artistId",
"in" : "path",
"required" : true,
"type" : "string"
} ],
"responses" : { }
},
"options" : {
"consumes" : [ "application/json" ],
"parameters" : [ {
"name" : "artistId",
"in" : "path",
"required" : true,
"type" : "string"
} ],
"responses" : {
"204" : {
"description" : "204 response",
"headers" : {
"Access-Control-Allow-Origin" : {
"type" : "string"
},
"Access-Control-Allow-Methods" : {
"type" : "string"
},
"Access-Control-Allow-Headers" : {
"type" : "string"
}
}
}
}
},
"patch" : {
"parameters" : [ {
"name" : "artistId",
"in" : "path",
"required" : true,
"type" : "string"
} ],
"responses" : { }
}
},
"/podcasts" : {
"get" : {
"consumes" : [ "application/json" ],
"produces" : [ "application/json" ],
"responses" : {
"200" : {
"description" : "200 response",
"schema" : {
"$ref" : "#/definitions/Empty"
}
}
},
"security" : [ {
"AVPAuthorizer" : [ ]
} ]
},
"post" : {
"responses" : { }
},
"delete" : {
"responses" : { }
},
"options" : {
"consumes" : [ "application/json" ],
"responses" : {
"204" : {
"description" : "204 response",
"headers" : {
"Access-Control-Allow-Origin" : {
"type" : "string"
},
"Access-Control-Allow-Methods" : {
"type" : "string"
},
"Access-Control-Allow-Headers" : {
"type" : "string"
}
}
}
}
}
},
"/podcasts/{podcastId}" : {
"options" : {
"consumes" : [ "application/json" ],
"parameters" : [ {
"name" : "podcastId",
"in" : "path",
"required" : true,
"type" : "string"
} ],
"responses" : {
"204" : {
"description" : "204 response",
"headers" : {
"Access-Control-Allow-Origin" : {
"type" : "string"
},
"Access-Control-Allow-Methods" : {
"type" : "string"
},
"Access-Control-Allow-Headers" : {
"type" : "string"
}
}
}
}
},
"x-amazon-apigateway-any-method" : {
"parameters" : [ {
"name" : "podcastId",
"in" : "path",
"required" : true,
"type" : "string"
} ],
"responses" : { }
}
}
},
"securityDefinitions" : {
"AVPAuthorizer" : {
"type" : "apiKey",
"name" : "Unused",
"in" : "header",
"x-amazon-apigateway-authtype" : "custom"
}
},
"definitions" : {
"Empty" : {
"type" : "object",
"title" : "Empty Schema"
}
}
}
Loading

0 comments on commit 9b04d53

Please sign in to comment.