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 RestApi construct #235

Merged
merged 3 commits into from
Sep 9, 2024
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
32 changes: 32 additions & 0 deletions API.md

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

24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions rosetta/default.ts-fixture
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
29 changes: 28 additions & 1 deletion src/policy-store.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down
204 changes: 150 additions & 54 deletions test/policy-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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());
});
});
});