Skip to content

Commit e6dca52

Browse files
authored
feat(appsync): grant APIs for managing permissions (#8993)
**[ISSUE]** AppSync allows for `authorizationType` of `AWS_IAM`. However, no easy function or attribute to retrieve the `IAM Role` for later use. **[APPROACH]** Build out a basic granting interface that allows users to pass an `IAM Role` to grant permissions access. ``` const api = new appsync.GraphQLApi(...); const role = new iam.Role(..); api.grant(role, ... ); ``` **[NOTE]** Current implementation let's you use `IAM` authorization, but doesn't come with `grant`. Fixes #6772 #7871 #7313 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent add23bf commit e6dca52

15 files changed

+1597
-193
lines changed

packages/@aws-cdk/aws-appsync/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ nyc.config.js
1616
!.eslintrc.js
1717
!jest.config.js
1818

19-
junit.xml
19+
!test/verify/*.js
20+
junit.xml

packages/@aws-cdk/aws-appsync/README.md

Lines changed: 129 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -13,186 +13,149 @@
1313
---
1414
<!--END STABILITY BANNER-->
1515

16-
This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project.
16+
The `@aws-cdk/aws-appsync` package contains constructs for building flexible
17+
APIs that use GraphQL.
1718

18-
## Usage Example
19+
### Example
1920

20-
Given the following GraphQL schema file `schema.graphql`:
21+
Example of a GraphQL API with `AWS_IAM` authorization resolving into a DynamoDb
22+
backend data source.
2123

22-
```graphql
23-
type ServiceVersion {
24-
version: String!
25-
}
24+
GraphQL schema file `schema.graphql`:
2625

27-
type Customer {
28-
id: String!
29-
name: String!
26+
```gql
27+
type demo {
28+
id: String!
29+
version: String!
3030
}
31-
32-
input SaveCustomerInput {
33-
name: String!
31+
type Query {
32+
getDemos: [ test! ]
3433
}
35-
36-
type Order {
37-
customer: String!
38-
order: String!
34+
input DemoInput {
35+
version: String!
3936
}
40-
41-
type Query {
42-
getServiceVersion: ServiceVersion
43-
getCustomers: [Customer]
44-
getCustomer(id: String): Customer
37+
type Mutation {
38+
addDemo(input: DemoInput!): demo
4539
}
40+
```
4641

47-
input FirstOrderInput {
48-
product: String!
49-
quantity: Int!
50-
}
42+
CDK stack file `app-stack.ts`:
5143

44+
```ts
45+
import * as appsync from '@aws-cdk/aws-appsync';
46+
import * as db from '@aws-cdk/aws-dynamodb';
47+
48+
const api = new appsync.GraphQLApi(stack, 'Api', {
49+
name: 'demo',
50+
schemaDefinitionFile: join(__dirname, 'schema.graphql'),
51+
authorizationConfig: {
52+
defaultAuthorization: {
53+
authorizationType: appsync.AuthorizationType.IAM
54+
},
55+
},
56+
});
57+
58+
const demoTable = new db.Table(stack, 'DemoTable', {
59+
partitionKey: {
60+
name: 'id',
61+
type: AttributeType.STRING,
62+
},
63+
});
64+
65+
const demoDS = api.addDynamoDbDataSource('demoDataSource', 'Table for Demos"', demoTable);
66+
67+
// Resolver for the Query "getDemos" that scans the DyanmoDb table and returns the entire list.
68+
demoDS.createResolver({
69+
typeName: 'Query',
70+
fieldName: 'getDemos',
71+
requestMappingTemplate: MappingTemplate.dynamoDbScanTable(),
72+
responseMappingTemplate: MappingTemplate.dynamoDbResultList(),
73+
});
74+
75+
// Resolver for the Mutation "addDemo" that puts the item into the DynamoDb table.
76+
demoDS.createResolver({
77+
typeName: 'Mutation',
78+
fieldName: 'addDemo',
79+
requestMappingTemplate: MappingTemplate.dynamoDbPutItem(PrimaryKey.partition('id').auto(), Values.projecting('demo')),
80+
responseMappingTemplate: MappingTemplate.dynamoDbResultItem(),
81+
});
82+
```
83+
84+
## Permissions
85+
86+
When using `AWS_IAM` as the authorization type for GraphQL API, an IAM Role
87+
with correct permissions must be used for access to API.
88+
89+
When configuring permissions, you can specify specific resources to only be
90+
accessible by `IAM` authorization. For example, if you want to only allow mutability
91+
for `IAM` authorized access you would configure the following.
92+
93+
In `schema.graphql`:
94+
```ts
5295
type Mutation {
53-
addCustomer(customer: SaveCustomerInput!): Customer
54-
saveCustomer(id: String!, customer: SaveCustomerInput!): Customer
55-
removeCustomer(id: String!): Customer
56-
saveCustomerWithFirstOrder(customer: SaveCustomerInput!, order: FirstOrderInput!, referral: String): Order
57-
doPostOnAws: String!
96+
updateExample(...): ...
97+
@aws_iam
5898
}
5999
```
60100
61-
the following CDK app snippet will create a complete CRUD AppSync API:
101+
In `IAM`:
102+
```json
103+
{
104+
"Version": "2012-10-17",
105+
"Statement": [
106+
{
107+
"Effect": "Allow",
108+
"Action": [
109+
"appsync:GraphQL"
110+
],
111+
"Resource": [
112+
"arn:aws:appsync:REGION:ACCOUNT_ID:apis/GRAPHQL_ID/types/Mutation/fields/updateExample"
113+
]
114+
}
115+
]
116+
}
117+
```
118+
119+
See [documentation](https://docs.aws.amazon.com/appsync/latest/devguide/security.html#aws-iam-authorization) for more details.
120+
121+
To make this easier, CDK provides `grant` API.
122+
123+
Use the `grant` function for more granular authorization.
62124
63125
```ts
64-
export class ApiStack extends Stack {
65-
constructor(scope: Construct, id: string) {
66-
super(scope, id);
67-
68-
const userPool = new UserPool(this, 'UserPool', {
69-
userPoolName: 'myPool',
70-
});
71-
72-
const api = new GraphQLApi(this, 'Api', {
73-
name: `demoapi`,
74-
logConfig: {
75-
fieldLogLevel: FieldLogLevel.ALL,
76-
},
77-
authorizationConfig: {
78-
defaultAuthorization: {
79-
authorizationType: AuthorizationType.USER_POOL,
80-
userPoolConfig: {
81-
userPool,
82-
defaultAction: UserPoolDefaultAction.ALLOW
83-
},
84-
},
85-
additionalAuthorizationModes: [
86-
{
87-
authorizationType: AuthorizationType.API_KEY,
88-
}
89-
],
90-
},
91-
schemaDefinitionFile: './schema.graphql',
92-
});
93-
94-
const noneDS = api.addNoneDataSource('None', 'Dummy data source');
95-
96-
noneDS.createResolver({
97-
typeName: 'Query',
98-
fieldName: 'getServiceVersion',
99-
requestMappingTemplate: MappingTemplate.fromString(JSON.stringify({
100-
version: '2017-02-28',
101-
})),
102-
responseMappingTemplate: MappingTemplate.fromString(JSON.stringify({
103-
version: 'v1',
104-
})),
105-
});
106-
107-
const customerTable = new Table(this, 'CustomerTable', {
108-
billingMode: BillingMode.PAY_PER_REQUEST,
109-
partitionKey: {
110-
name: 'id',
111-
type: AttributeType.STRING,
112-
},
113-
});
114-
// If your table is already created you can also use use import table and use it as data source.
115-
const customerDS = api.addDynamoDbDataSource('Customer', 'The customer data source', customerTable);
116-
customerDS.createResolver({
117-
typeName: 'Query',
118-
fieldName: 'getCustomers',
119-
requestMappingTemplate: MappingTemplate.dynamoDbScanTable(),
120-
responseMappingTemplate: MappingTemplate.dynamoDbResultList(),
121-
});
122-
customerDS.createResolver({
123-
typeName: 'Query',
124-
fieldName: 'getCustomer',
125-
requestMappingTemplate: MappingTemplate.dynamoDbGetItem('id', 'id'),
126-
responseMappingTemplate: MappingTemplate.dynamoDbResultItem(),
127-
});
128-
customerDS.createResolver({
129-
typeName: 'Mutation',
130-
fieldName: 'addCustomer',
131-
requestMappingTemplate: MappingTemplate.dynamoDbPutItem(
132-
PrimaryKey.partition('id').auto(),
133-
Values.projecting('customer')),
134-
responseMappingTemplate: MappingTemplate.dynamoDbResultItem(),
135-
});
136-
customerDS.createResolver({
137-
typeName: 'Mutation',
138-
fieldName: 'saveCustomer',
139-
requestMappingTemplate: MappingTemplate.dynamoDbPutItem(
140-
PrimaryKey.partition('id').is('id'),
141-
Values.projecting('customer')),
142-
responseMappingTemplate: MappingTemplate.dynamoDbResultItem(),
143-
});
144-
customerDS.createResolver({
145-
typeName: 'Mutation',
146-
fieldName: 'saveCustomerWithFirstOrder',
147-
requestMappingTemplate: MappingTemplate.dynamoDbPutItem(
148-
PrimaryKey
149-
.partition('order').auto()
150-
.sort('customer').is('customer.id'),
151-
Values
152-
.projecting('order')
153-
.attribute('referral').is('referral')),
154-
responseMappingTemplate: MappingTemplate.dynamoDbResultItem(),
155-
});
156-
customerDS.createResolver({
157-
typeName: 'Mutation',
158-
fieldName: 'removeCustomer',
159-
requestMappingTemplate: MappingTemplate.dynamoDbDeleteItem('id', 'id'),
160-
responseMappingTemplate: MappingTemplate.dynamoDbResultItem(),
161-
});
162-
163-
const httpDS = api.addHttpDataSource('http', 'The http data source', 'https://aws.amazon.com/');
164-
165-
httpDS.createResolver({
166-
typeName: 'Mutation',
167-
fieldName: 'doPostOnAws',
168-
requestMappingTemplate: MappingTemplate.fromString(`{
169-
"version": "2018-05-29",
170-
"method": "POST",
171-
# if full path is https://api.xxxxxxxxx.com/posts then resourcePath would be /posts
172-
"resourcePath": "/path/123",
173-
"params":{
174-
"body": $util.toJson($ctx.args),
175-
"headers":{
176-
"Content-Type": "application/json",
177-
"Authorization": "$ctx.request.headers.Authorization"
178-
}
179-
}
180-
}`),
181-
responseMappingTemplate: MappingTemplate.fromString(`
182-
## Raise a GraphQL field error in case of a datasource invocation error
183-
#if($ctx.error)
184-
$util.error($ctx.error.message, $ctx.error.type)
185-
#end
186-
## if the response status code is not 200, then return an error. Else return the body **
187-
#if($ctx.result.statusCode == 200)
188-
## If response is 200, return the body.
189-
$ctx.result.body
190-
#else
191-
## If response is not 200, append the response to error block.
192-
$utils.appendError($ctx.result.body, "$ctx.result.statusCode")
193-
#end
194-
`),
195-
});
196-
}
197-
}
126+
const role = new iam.Role(stack, 'Role', {
127+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
128+
});
129+
const api = new appsync.GraphQLApi(stack, 'API', {
130+
definition
131+
});
132+
133+
api.grant(role, appsync.IamResource.custom('types/Mutation/fields/updateExample'), 'appsync:GraphQL')
198134
```
135+
136+
### IamResource
137+
138+
In order to use the `grant` functions, you need to use the class `IamResource`.
139+
140+
- `IamResource.custom(...arns)` permits custom ARNs and requires an argument.
141+
142+
- `IamResouce.ofType(type, ...fields)` permits ARNs for types and their fields.
143+
144+
- `IamResource.all()` permits ALL resources.
145+
146+
### Generic Permissions
147+
148+
Alternatively, you can use more generic `grant` functions to accomplish the same usage.
149+
150+
These include:
151+
- grantMutation (use to grant access to Mutation fields)
152+
- grantQuery (use to grant access to Query fields)
153+
- grantSubscription (use to grant access to Subscription fields)
154+
155+
```ts
156+
// For generic types
157+
api.grantMutation(role, 'updateExample');
158+
159+
// For custom types and granular design
160+
api.grant(role, appsync.IamResource.ofType('Mutation', 'updateExample'), 'appsync:GraphQL');
161+
```

0 commit comments

Comments
 (0)