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: generate schema from cloudformation template #222

Closed
wants to merge 2 commits into from
Closed
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: 31 additions & 0 deletions API.md

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

28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,37 @@ const policyStore = new PolicyStore(scope, "PolicyStore", {

If you want to have type safety when defining a schema, you can accomplish this **<ins>only</ins>** 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.
You can also generate simple schemas using the static functions `schemaFromOpenApiSpec` or `schemaFromCfTemplate` in the PolicyStore construct. This functionality replicates what you can find in the AWS Verified Permissions console.

Generate a schema from an OpenAPI spec:

```ts
const validationSettingsStrict = {
mode: ValidationSettingsMode.STRICT,
};
const cedarJsonSchema = PolicyStore.schemaFromOpenApiSpec(
'path/to/swaggerfile.json',
'UserGroup',
"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",
});
```

Generate a schema from a CloudFormation template:

```ts
const validationSettingsStrict = {
mode: ValidationSettingsMode.STRICT,
};
const cedarJsonSchema = PolicyStore.schemaFromCfTemplate(
"path/to/cftemplate.json",
"UserGroup"
);
const cedarSchema = {
cedarJson: JSON.stringify(cedarJsonSchema),
Expand Down
72 changes: 72 additions & 0 deletions src/policy-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,78 @@ export class PolicyStore extends PolicyStoreBase {
return buildSchema(namespace, actionNames, groupEntityTypeName);
}

/**
* This method generates a schema based on a JSON CloudFormation template containing exactly one
* RestApi. It makes the same assumptions and decisions made in the Amazon Verified Permissions console.
*
* @param cfTemplateFilePath absolute path to a CloudFormation template 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 schemaFromCfTemplate(cfTemplateFilePath: string, groupEntityTypeName?: string) {
/**
* Builds the full path of an AWS::ApiGateway::Method by traversing up the resource hierarchy.
*/
function buildResourcePath(cfTemplate: any, startResourceId: string): string {
let currResourceId: string | undefined = startResourceId;
const parts: string[] = [];
while (currResourceId) {
const resource: any = cfTemplate.Resources[currResourceId];
if (!resource || resource.Type !== 'AWS::ApiGateway::Resource') {
break;
}
parts.push(resource.Properties.PathPart);
currResourceId = resource.Properties.ParentId.Ref;
}
return '/' + parts.reverse().join('/');
}

const RELEVANT_HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head'];
const cfTemplateString = fs.readFileSync(cfTemplateFilePath, 'utf-8');
const cfTemplate = JSON.parse(cfTemplateString) as any;
if (!cfTemplate.Resources) {
throw new Error('Invalid CF template - missing Resources object');
}
const resources = cfTemplate.Resources;

let apiResource;
for (const key in resources) {
const resource = resources[key];
if (resource.Type === 'AWS::ApiGateway::RestApi') {
if (apiResource) {
throw new Error('Invalid CF template - multiple RestApis found');
}
apiResource = resource;
}
}
if (!apiResource) {
throw new Error('Invalid CF template - no RestApi found');
}
if (!apiResource.Properties.Name) {
throw new Error('Invalid CF template - RestApi missing Name property');
}
const namespace = cleanUpApiNameForNamespace(apiResource.Properties.Name);

const actionNames = [];
for (const key in resources) {
const resource = resources[key];
if (resource.Type !== 'AWS::ApiGateway::Method') {
continue;
}
const httpMethod = resource.Properties.HttpMethod.toLowerCase();
const pathUrl = buildResourcePath(cfTemplate, resource.Properties.ResourceId.Ref);

if (httpMethod === 'any') {
for (const method of RELEVANT_HTTP_METHODS) {
actionNames.push(`${method} ${pathUrl}`);
}
} else if (RELEVANT_HTTP_METHODS.includes(httpMethod)) {
actionNames.push(`${httpMethod} ${pathUrl}`);
}
}

return buildSchema(namespace, actionNames, groupEntityTypeName);
}

private readonly policyStore: CfnPolicyStore;
/**
* ARN of the Policy Store.
Expand Down
118 changes: 118 additions & 0 deletions test/podcastappcloudformation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
{
"Resources": {
"PodcastAppApi262252D3": {
"Type": "AWS::ApiGateway::RestApi",
"Properties": {
"Name": "PodcastApp"
}
},
"PodcastAppApiartistsGET932A4BE6": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"HttpMethod": "GET",
"ResourceId": {
"Ref": "PodcastAppApiartistsE74E9D28"
}
}
},
"PodcastAppApiartistsPOSTE5F58123": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"HttpMethod": "POST",
"ResourceId": {
"Ref": "PodcastAppApiartistsE74E9D28"
}
}
},
"PodcastAppApiartistsDELETED8ECABA1": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"HttpMethod": "DELETE",
"ResourceId": {
"Ref": "PodcastAppApiartistsE74E9D28"
}
}
},
"PodcastAppApiartistsartistId67271C01": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"ParentId": {
"Ref": "PodcastAppApiartistsE74E9D28"
},
"PathPart": "{artistId}"
}
},
"PodcastAppApiartistsartistIdDELETE33FE0AE3": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"HttpMethod": "DELETE",
"ResourceId": {
"Ref": "PodcastAppApiartistsartistId67271C01"
}
}
},
"PodcastAppApiartistsartistIdPATCH59F1CFD1": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"HttpMethod": "PATCH",
"ResourceId": {
"Ref": "PodcastAppApiartistsartistId67271C01"
}
}
},
"PodcastAppApipodcasts497B898A": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"ParentId": {
"Fn::GetAtt": ["PodcastAppApi262252D3", "RootResourceId"]
},
"PathPart": "podcasts"
}
},
"PodcastAppApipodcastsGET85451B06": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"HttpMethod": "GET",
"ResourceId": {
"Ref": "PodcastAppApipodcasts497B898A"
}
}
},
"PodcastAppApipodcastsPOSTFC659675": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"HttpMethod": "POST",
"ResourceId": {
"Ref": "PodcastAppApipodcasts497B898A"
}
}
},
"PodcastAppApipodcastsDELETE86D19CD6": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"HttpMethod": "DELETE",
"ResourceId": {
"Ref": "PodcastAppApipodcasts497B898A"
}
}
},
"PodcastAppApipodcastspodcastId41A31CCC": {
"Type": "AWS::ApiGateway::Resource",
"Properties": {
"ParentId": {
"Ref": "PodcastAppApipodcasts497B898A"
},
"PathPart": "{podcastId}"
}
},
"PodcastAppApipodcastspodcastIdANY3158475C": {
"Type": "AWS::ApiGateway::Method",
"Properties": {
"HttpMethod": "ANY",
"ResourceId": {
"Ref": "PodcastAppApipodcastspodcastId41A31CCC"
}
}
}
}
}
Loading