Repository: cdklabs/cdk-verified-permissions (language: python)

Issue:
proposal: schemaFromRestApi Method
Hi team,

It would be great to be able to generate a schema based off of CloudFormation templates, rather than having to deploy the API, export a swagger spec, and then generate based off of that.

What do you think of a new method `schemaFromCloudFormationTemplate()`?. I am thinking it would work exactly like `schemaFromOpenApiSpec()`, just with a different source. 

If this aligns with the project’s direction, I’d be happy to make a contribution.


Discussion / hints:
Hi @joshkraft, thanks for your proposal and your contribution!

Let me discuss it with the team and come back to you
Hi @joshkraft,

We discussed your proposal internally and would like to hear more about your use case, specifically about the need to generate a schema from a CloudFormation template

Rather than parsing the template and generate a schema, which would imply several assumptions (e.g. no more than 1 API per template), we suggest to implement a functionality that generates the schema from another CDK construct e.g. [Rest API](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigateway.RestApi.html)

Also, for users that want/need to start from a CloudFormation template the suggested way would be to [import it in CDK](https://docs.aws.amazon.com/cdk/v2/guide/use_cfn_template.html)

What do you think?
Hi Andrea - thanks for the response. I think working with the RestApi construct would work for our use case. Would you be open to a contribution that adds something like schemaFromRestApi to this project?
Hi Josh,

Yes definitely - I think it will align nicely with our design!

Feel free to submit a PR, thank you very much

Fix patch:
diff --git a/API.md b/API.md
index 5d01533..a21a06a 100644
--- a/API.md
+++ b/API.md
@@ -967,6 +967,7 @@ Permits an IAM principal all write & read operations on the policy store: Create
 | <code><a href="#@cdklabs/cdk-verified-permissions.PolicyStore.fromPolicyStoreAttributes">fromPolicyStoreAttributes</a></code> | Creates a PolicyStore construct that represents an external Policy Store. |
 | <code><a href="#@cdklabs/cdk-verified-permissions.PolicyStore.fromPolicyStoreId">fromPolicyStoreId</a></code> | Create a PolicyStore construct that represents an external policy store via policy store id. |
 | <code><a href="#@cdklabs/cdk-verified-permissions.PolicyStore.schemaFromOpenApiSpec">schemaFromOpenApiSpec</a></code> | This method generates a schema based on an swagger file. |
+| <code><a href="#@cdklabs/cdk-verified-permissions.PolicyStore.schemaFromRestApi">schemaFromRestApi</a></code> | This method generates a schema based on an AWS CDK RestApi construct. |
 
 ---
 
@@ -1155,6 +1156,37 @@ If passed, the schema's User type will have a parent of this type.
 
 ---
 
+##### `schemaFromRestApi` <a name="schemaFromRestApi" id="@cdklabs/cdk-verified-permissions.PolicyStore.schemaFromRestApi"></a>
+
+```typescript
+import { PolicyStore } from '@cdklabs/cdk-verified-permissions'
+
+PolicyStore.schemaFromRestApi(restApi: RestApi, groupEntityTypeName?: string)
+```
+
+This method generates a schema based on an AWS CDK RestApi construct.
+
+It makes the same assumptions
+and decisions made in the Amazon Verified Permissions console.
+
+###### `restApi`<sup>Required</sup> <a name="restApi" id="@cdklabs/cdk-verified-permissions.PolicyStore.schemaFromRestApi.parameter.restApi"></a>
+
+- *Type:* aws-cdk-lib.aws_apigateway.RestApi
+
+The RestApi construct instance from which to generate the schema.
+
+---
+
+###### `groupEntityTypeName`<sup>Optional</sup> <a name="groupEntityTypeName" id="@cdklabs/cdk-verified-permissions.PolicyStore.schemaFromRestApi.parameter.groupEntityTypeName"></a>
+
+- *Type:* string
+
+Specifies a group entity type name.
+
+If passed, the schema's User type will have a parent of this type.
+
+---
+
 #### Properties <a name="Properties" id="Properties"></a>
 
 | **Name** | **Type** | **Description** |
diff --git a/README.md b/README.md
index 4cc1910..48f1271 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,9 @@ 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 `schemaFromRestApi` 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 = {
@@ -83,6 +85,26 @@ const policyStore = new PolicyStore(scope, "PolicyStore", {
 });
 ```
 
+Generate a schema from a RestApi construct:
+
+```ts
+const validationSettingsStrict = {
+  mode: ValidationSettingsMode.STRICT,
+};
+const cedarJsonSchema = PolicyStore.schemaFromRestApi(
+  new RestApi(scope, "RestApi"),
+  "UserGroup",
+);
+const cedarSchema = {
+  cedarJson: JSON.stringify(cedarJsonSchema),
+};
+const policyStore = new PolicyStore(scope, "PolicyStore", {
+  schema: cedarSchema,
+  validationSettings: validationSettingsStrict,
+  description: "Policy store with schema generated from RestApi construct",
+});
+```
+
 ## Identity Source
 
 Define Identity Source with Cognito Configuration and required properties:
diff --git a/rosetta/default.ts-fixture b/rosetta/default.ts-fixture
index 234ceb0..1ccdabb 100644
--- a/rosetta/default.ts-fixture
+++ b/rosetta/default.ts-fixture
@@ -1,6 +1,7 @@
 // Fixture with packages imported, but nothing else
 import { IdentitySource, Policy, PolicyType, PolicyTemplate, AddPolicyOptions, PolicyStore, ValidationSettingsMode } from '@cdklabs/cdk-verified-permissions';
 import { UserPool } from 'aws-cdk-lib/aws-cognito';
+import { RestApi } from 'aws-cdk-lib/aws-apigateway';
 import { Stack } from 'aws-cdk-lib';
 import { Construct } from 'constructs';
 
diff --git a/src/policy-store.ts b/src/policy-store.ts
index 513c7e4..d7d2395 100644
--- a/src/policy-store.ts
+++ b/src/policy-store.ts
@@ -1,5 +1,6 @@
 import * as fs from 'fs';
 import * as path from 'path';
+import { RestApi } from 'aws-cdk-lib/aws-apigateway';
 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';
@@ -12,6 +13,8 @@ import {
   WRITE_ACTIONS,
 } from './private/permissions';
 
+const RELEVANT_HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head'];
+
 export interface Schema {
   readonly cedarJson: string;
 }
@@ -263,7 +266,6 @@ export class PolicyStore extends PolicyStoreBase {
    * @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) {
@@ -293,6 +295,31 @@ export class PolicyStore extends PolicyStoreBase {
     return buildSchema(namespace, actionNames, groupEntityTypeName);
   }
 
+  /**
+   * This method generates a schema based on an AWS CDK RestApi construct. It makes the same assumptions
+   * and decisions made in the Amazon Verified Permissions console.
+   *
+   * @param restApi The RestApi construct instance from which to generate the schema.
+   * @param groupEntityTypeName Specifies a group entity type name. If passed, the schema's User type will have a parent of this type.
+   */
+  public static schemaFromRestApi(restApi: RestApi, groupEntityTypeName?: string) {
+    const namespace = cleanUpApiNameForNamespace(restApi.restApiName);
+    const actionNames: string[] = [];
+    for (const method of restApi.methods) {
+      const pathVerb = method.httpMethod.toLowerCase();
+      const pathUrl = method.resource.path;
+      if (pathVerb === 'any') {
+        for (const verb of RELEVANT_HTTP_METHODS) {
+          actionNames.push(`${verb} ${pathUrl}`);
+        }
+      }
+	  if (RELEVANT_HTTP_METHODS.includes(pathVerb)) {
+      	actionNames.push(`${pathVerb} ${pathUrl}`);
+	  }
+    }
+    return buildSchema(namespace, actionNames, groupEntityTypeName);
+  }
+
   private readonly policyStore: CfnPolicyStore;
   /**
    * ARN of the Policy Store.


Test patch:
diff --git a/test/policy-store.test.ts b/test/policy-store.test.ts
index fbd0345..1903e92 100644
--- a/test/policy-store.test.ts
+++ b/test/policy-store.test.ts
@@ -3,6 +3,7 @@ import * as path from 'path';
 import * as cedar from '@cedar-policy/cedar-wasm/nodejs';
 import { ArnFormat, Aws, Stack } from 'aws-cdk-lib';
 import { Template } from 'aws-cdk-lib/assertions';
+import { RestApi } from 'aws-cdk-lib/aws-apigateway';
 import * as iam from 'aws-cdk-lib/aws-iam';
 import { CfnPolicy, CfnPolicyStore } from 'aws-cdk-lib/aws-verifiedpermissions';
 import { getResourceLogicalId } from './utils';
@@ -624,69 +625,164 @@ describe('Policy store with policies from a path', () => {
   });
 });
 
-describe('generating schemas from OpenApi specs', () => {
-  test('generate schema from openApi spec fails if swagger file has no paths', () => {
-    expect(() => {
-      PolicyStore.schemaFromOpenApiSpec(
-        path.join(__dirname, 'schema.json'),
+describe('Policy Store schema generation', () => {
+  const expectedActions = [
+    'get /artists',
+    'post /artists',
+    'delete /artists',
+    'patch /artists/{artistId}',
+    'delete /artists/{artistId}',
+    'get /podcasts',
+    'post /podcasts',
+    'delete /podcasts',
+    'get /podcasts/{podcastId}',
+    'post /podcasts/{podcastId}',
+    'put /podcasts/{podcastId}',
+    'patch /podcasts/{podcastId}',
+    'delete /podcasts/{podcastId}',
+    'head /podcasts/{podcastId}',
+  ];
+
+  describe('generating schemas from OpenApi specs', () => {
+    test('generate schema from openApi spec fails if swagger file has no paths', () => {
+      expect(() => {
+        PolicyStore.schemaFromOpenApiSpec(
+          path.join(__dirname, 'schema.json'),
+          'UserGroup',
+        );
+      }).toThrow('Invalid OpenAPI spec - missing paths object');
+    });
+
+    test('generate schema from openApi spec with userGroups', () => {
+      // GIVEN
+      const stack = new Stack(undefined, 'Stack');
+
+      // WHEN
+      const schema = PolicyStore.schemaFromOpenApiSpec(
+        path.join(__dirname, 'podcastappswagger.json'),
         'UserGroup',
       );
-    }).toThrow();
-  });
-
-  test('generate schema from openApi spec with userGroups', () => {
-    // GIVEN
-    const stack = new Stack(undefined, 'Stack');
+      const pStore = new PolicyStore(stack, 'PolicyStore', {
+        validationSettings: {
+          mode: ValidationSettingsMode.STRICT,
+        },
+        schema: {
+          cedarJson: JSON.stringify(schema),
+        },
+      });
 
-    // WHEN
-    const schema = PolicyStore.schemaFromOpenApiSpec(
-      path.join(__dirname, 'podcastappswagger.json'),
-      'UserGroup',
-    );
-    const pStore = new PolicyStore(stack, 'PolicyStore', {
-      validationSettings: {
-        mode: ValidationSettingsMode.STRICT,
-      },
-      schema: {
-        cedarJson: JSON.stringify(schema),
-      },
+      // THEN
+      expect(pStore.schema?.cedarJson).toBeDefined();
+      expect(Object.keys(schema.PodcastApp.entityTypes)).toStrictEqual([
+        'UserGroup',
+        'User',
+        'Application',
+      ]);
+      // it should have the eight explicitly defined actions plus the 6 derived from the 'any' definition
+      expect(Object.keys(schema.PodcastApp.actions).length).toEqual(8 + 6);
+      expect(Object.keys(schema.PodcastApp.actions).sort()).toEqual(expectedActions.sort());
     });
 
-    // THEN
-    expect(pStore.schema?.cedarJson).toBeDefined();
-    expect(Object.keys(schema.PodcastApp.entityTypes)).toStrictEqual([
-      'UserGroup',
-      'User',
-      'Application',
-    ]);
-    // it should have the eight explicitly defined actions plus the 6 derived from the 'any' definition
-    expect(Object.keys(schema.PodcastApp.actions).length).toEqual(8 + 6);
+    test('generate schema from openApi spec without userGroups', () => {
+      // GIVEN
+      const stack = new Stack(undefined, 'Stack');
+
+      // WHEN
+      const schema = PolicyStore.schemaFromOpenApiSpec(
+        path.join(__dirname, 'podcastappswagger.json'),
+
+      );
+      const pStore = new PolicyStore(stack, 'PolicyStore', {
+        validationSettings: {
+          mode: ValidationSettingsMode.STRICT,
+        },
+        schema: {
+          cedarJson: JSON.stringify(schema),
+        },
+      });
+
+      // THEN
+      expect(pStore.schema?.cedarJson).toBeDefined();
+      expect(Object.keys(schema.PodcastApp.entityTypes)).toStrictEqual([
+        'User',
+        'Application',
+      ]);
+      // it should have the eight explicitly defined actions plus the 6 derived from the 'any' definition
+      expect(Object.keys(schema.PodcastApp.actions).length).toEqual(8 + 6);
+      expect(Object.keys(schema.PodcastApp.actions).sort()).toEqual(expectedActions.sort());
+    });
   });
-  test('generate schema from openApi spec without userGroups', () => {
-    // GIVEN
-    const stack = new Stack(undefined, 'Stack');
 
-    // WHEN
-    const schema = PolicyStore.schemaFromOpenApiSpec(
-      path.join(__dirname, 'podcastappswagger.json'),
+  describe('generating schemas from RestApi constructs', () => {
+    function buildRestApi() {
+      const stack = new Stack(undefined, 'Stack');
+      const api = new RestApi(stack, 'PodcastApp');
+
+      const artists = api.root.addResource('artists');
+      artists.addMethod('GET');
+      artists.addMethod('POST');
+      artists.addMethod('DELETE');
+      const artist = artists.addResource('{artistId}');
+      artist.addMethod('PATCH');
+      artist.addMethod('DELETE');
+      const podcasts = api.root.addResource('podcasts');
+      podcasts.addMethod('GET');
+      podcasts.addMethod('POST');
+      podcasts.addMethod('DELETE');
+      const podcast = podcasts.addResource('{podcastId}');
+      podcast.addMethod('ANY');
+
+	  return { stack, api };
+    }
+
+    test('generate schema from RestApi with userGroups', () => {
+      // GIVEN
+      const { stack, api } = buildRestApi();
+
+      // WHEN
+      const schema = PolicyStore.schemaFromRestApi(api, 'UserGroup');
+      const pStore = new PolicyStore(stack, 'PolicyStore', {
+        validationSettings: {
+          mode: ValidationSettingsMode.STRICT,
+        },
+        schema: {
+          cedarJson: JSON.stringify(schema),
+        },
+      });
 
-    );
-    const pStore = new PolicyStore(stack, 'PolicyStore', {
-      validationSettings: {
-        mode: ValidationSettingsMode.STRICT,
-      },
-      schema: {
-        cedarJson: JSON.stringify(schema),
-      },
+      // THEN
+      expect(pStore.schema?.cedarJson).toBeDefined();
+      expect(Object.keys(schema.PodcastApp.entityTypes)).toStrictEqual([
+        'UserGroup',
+        'User',
+        'Application',
+      ]);
+      // it should have the eight explicitly defined actions plus the 6 derived from the 'any' definition
+      expect(Object.keys(schema.PodcastApp.actions).length).toEqual(8 + 6);
+      expect(Object.keys(schema.PodcastApp.actions).sort()).toEqual(expectedActions.sort());
     });
 
-    // THEN
-    expect(pStore.schema?.cedarJson).toBeDefined();
-    expect(Object.keys(schema.PodcastApp.entityTypes)).toStrictEqual([
-      'User',
-      'Application',
-    ]);
-    // it should have the eight explicitly defined actions plus the 6 derived from the 'any' definition
-    expect(Object.keys(schema.PodcastApp.actions).length).toEqual(8 + 6);
+    test('generate schema from RestApi without userGroups', () => {
+      // GIVEN
+      const { stack, api } = buildRestApi();
+
+      // WHEN
+      const schema = PolicyStore.schemaFromRestApi(api);
+      const pStore = new PolicyStore(stack, 'PolicyStore', {
+        validationSettings: {
+          mode: ValidationSettingsMode.STRICT,
+        },
+        schema: {
+          cedarJson: JSON.stringify(schema),
+        },
+      });
+
+      // THEN
+      expect(pStore.schema?.cedarJson).toBeDefined();
+      expect(Object.keys(schema.PodcastApp.entityTypes)).toStrictEqual(['User', 'Application']);
+      // it should have the eight explicitly defined actions plus the 6 derived from the 'any' definition
+      expect(Object.keys(schema.PodcastApp.actions).length).toEqual(8 + 6);
+      expect(Object.keys(schema.PodcastApp.actions).sort()).toEqual(expectedActions.sort());
+    });
   });
 });
\ No newline at end of file