Skip to content

Commit a777488

Browse files
authored
enable configuring functions to access GraphQL API in defineData (#1116)
1 parent 9d42ac1 commit a777488

13 files changed

+645
-171
lines changed

.changeset/breezy-eyes-appear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@aws-amplify/backend-data': minor
3+
---
4+
5+
plumb function access definition from schema into IAM policies attached to the functions

package-lock.json

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend-data/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
},
1919
"license": "Apache-2.0",
2020
"devDependencies": {
21-
"@aws-amplify/data-schema": "^0.13.8",
21+
"@aws-amplify/data-schema": "^0.13.11",
2222
"@aws-amplify/backend-platform-test-stubs": "^0.3.3-beta.0",
2323
"@aws-amplify/platform-core": "^0.5.0-beta.1"
2424
},
@@ -31,6 +31,6 @@
3131
"@aws-amplify/backend-output-schemas": "^0.7.0-beta.0",
3232
"@aws-amplify/data-construct": "^1.4.1",
3333
"@aws-amplify/plugin-types": "^0.9.0-beta.0",
34-
"@aws-amplify/data-schema-types": "^0.7.6"
34+
"@aws-amplify/data-schema-types": "^0.7.7"
3535
}
3636
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { beforeEach, describe, it } from 'node:test';
2+
import {
3+
AppSyncApiAction,
4+
AppSyncPolicyGenerator,
5+
} from './app_sync_policy_generator.js';
6+
import { App, Stack } from 'aws-cdk-lib';
7+
import { GraphqlApi } from 'aws-cdk-lib/aws-appsync';
8+
import { AccountPrincipal, Role } from 'aws-cdk-lib/aws-iam';
9+
import { Template } from 'aws-cdk-lib/assertions';
10+
11+
void describe('AppSyncPolicyGenerator', () => {
12+
let stack: Stack;
13+
let graphqlApi: GraphqlApi;
14+
15+
beforeEach(() => {
16+
const app = new App();
17+
stack = new Stack(app, 'testStack');
18+
graphqlApi = new GraphqlApi(stack, 'testApi', {
19+
name: 'testName',
20+
definition: {
21+
schema: {
22+
bind: () => ({
23+
apiId: 'testApi',
24+
definition: 'test schema',
25+
}),
26+
},
27+
},
28+
});
29+
});
30+
const singleActionTestCases: {
31+
action: AppSyncApiAction;
32+
expectedResourceSuffix: string;
33+
}[] = [
34+
{
35+
action: 'query',
36+
expectedResourceSuffix: 'Query/*',
37+
},
38+
{
39+
action: 'mutate',
40+
expectedResourceSuffix: 'Mutation/*',
41+
},
42+
{
43+
action: 'listen',
44+
expectedResourceSuffix: 'Subscription/*',
45+
},
46+
];
47+
48+
singleActionTestCases.forEach(({ action, expectedResourceSuffix }) => {
49+
void it(`generates policy for ${action} action`, () => {
50+
const policyGenerator = new AppSyncPolicyGenerator(graphqlApi);
51+
52+
const queryPolicy = policyGenerator.generateGraphqlAccessPolicy([action]);
53+
54+
// we have to attach the policy to a role, otherwise CDK erases the policy from the stack
55+
queryPolicy.attachToRole(
56+
new Role(stack, 'testRole', {
57+
assumedBy: new AccountPrincipal('1234'),
58+
})
59+
);
60+
61+
const template = Template.fromStack(stack);
62+
template.hasResourceProperties('AWS::IAM::Policy', {
63+
PolicyDocument: {
64+
Statement: [
65+
{
66+
Action: 'appsync:GraphQL',
67+
Resource: {
68+
'Fn::Join': [
69+
'',
70+
[
71+
{
72+
'Fn::GetAtt': ['testApiD6ECAB50', 'Arn'],
73+
},
74+
`/types/${expectedResourceSuffix}`,
75+
],
76+
],
77+
},
78+
},
79+
],
80+
},
81+
});
82+
});
83+
});
84+
85+
void it('generates policy for multiple actions', () => {
86+
const policyGenerator = new AppSyncPolicyGenerator(graphqlApi);
87+
88+
const queryPolicy = policyGenerator.generateGraphqlAccessPolicy([
89+
'query',
90+
'mutate',
91+
'listen',
92+
]);
93+
94+
// we have to attach the policy to a role, otherwise CDK erases the policy from the stack
95+
queryPolicy.attachToRole(
96+
new Role(stack, 'testRole', {
97+
assumedBy: new AccountPrincipal('1234'),
98+
})
99+
);
100+
101+
const template = Template.fromStack(stack);
102+
template.hasResourceProperties('AWS::IAM::Policy', {
103+
PolicyDocument: {
104+
Statement: [
105+
{
106+
Action: 'appsync:GraphQL',
107+
Resource: [
108+
{
109+
'Fn::Join': [
110+
'',
111+
[
112+
{
113+
'Fn::GetAtt': ['testApiD6ECAB50', 'Arn'],
114+
},
115+
`/types/Query/*`,
116+
],
117+
],
118+
},
119+
{
120+
'Fn::Join': [
121+
'',
122+
[
123+
{
124+
'Fn::GetAtt': ['testApiD6ECAB50', 'Arn'],
125+
},
126+
`/types/Mutation/*`,
127+
],
128+
],
129+
},
130+
{
131+
'Fn::Join': [
132+
'',
133+
[
134+
{
135+
'Fn::GetAtt': ['testApiD6ECAB50', 'Arn'],
136+
},
137+
`/types/Subscription/*`,
138+
],
139+
],
140+
},
141+
],
142+
},
143+
],
144+
},
145+
});
146+
});
147+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Stack } from 'aws-cdk-lib';
2+
import { IGraphqlApi } from 'aws-cdk-lib/aws-appsync';
3+
import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam';
4+
5+
export type AppSyncApiAction = 'query' | 'mutate' | 'listen';
6+
7+
/**
8+
* Generates policies for accessing an AppSync GraphQL API
9+
*/
10+
export class AppSyncPolicyGenerator {
11+
private readonly stack: Stack;
12+
private readonly policyPrefix = 'GraphqlAccessPolicy';
13+
private policyCount = 1;
14+
/**
15+
* Initialize with the GraphqlAPI that the policies will be scoped to
16+
*/
17+
constructor(private readonly graphqlApi: IGraphqlApi) {
18+
this.stack = Stack.of(graphqlApi);
19+
}
20+
/**
21+
* Generates a policy that grants GraphQL data-plane access to the provided actions
22+
*
23+
* The naming is a bit wonky here because the IAM action is always "appsync:GraphQL".
24+
* The input "action" maps to the "type" in the resource name part of the ARN which is "Query", "Mutation" or "Subscription"
25+
*/
26+
generateGraphqlAccessPolicy(actions: AppSyncApiAction[]) {
27+
const resources = actions
28+
// convert from actions to GraphQL Type
29+
.map((action) => actionToTypeMap[action])
30+
// convert Type to resourceName
31+
.map((type) => [this.graphqlApi.arn, 'types', type, '*'].join('/'));
32+
return new Policy(this.stack, `${this.policyPrefix}${this.policyCount++}`, {
33+
statements: [
34+
new PolicyStatement({
35+
actions: ['appsync:GraphQL'],
36+
resources,
37+
}),
38+
],
39+
});
40+
}
41+
}
42+
43+
const actionToTypeMap: Record<AppSyncApiAction, string> = {
44+
query: 'Query',
45+
mutate: 'Mutation',
46+
listen: 'Subscription',
47+
};

packages/backend-data/src/convert_authorization_modes.test.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import {
1111
convertAuthorizationModesToCDK,
1212
isUsingDefaultApiKeyAuth,
1313
} from './convert_authorization_modes.js';
14-
import { Code, Function, IFunction, Runtime } from 'aws-cdk-lib/aws-lambda';
15-
import { FunctionInstanceProvider } from './convert_functions.js';
14+
import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda';
1615
import {
1716
AmplifyFunction,
1817
AuthResources,
1918
ConstructFactory,
19+
ConstructFactoryGetInstanceProps,
2020
ResourceProvider,
2121
} from '@aws-amplify/plugin-types';
2222

@@ -60,13 +60,10 @@ void describe('convertAuthorizationModesToCDK', () => {
6060
let authenticatedUserRole: IRole;
6161
let unauthenticatedUserRole: IRole;
6262
let providedAuthConfig: ProvidedAuthConfig;
63-
let functionInstanceProvider: FunctionInstanceProvider;
63+
const getInstancePropsStub: ConstructFactoryGetInstanceProps =
64+
{} as unknown as ConstructFactoryGetInstanceProps;
6465

6566
void beforeEach(() => {
66-
functionInstanceProvider = {
67-
provide: (func: ConstructFactory<AmplifyFunction>): IFunction =>
68-
func as unknown as IFunction,
69-
};
7067
stack = new Stack();
7168
userPool = new UserPool(stack, 'TestPool');
7269
authenticatedUserRole = Role.fromRoleName(stack, 'AuthRole', 'MyAuthRole');
@@ -92,7 +89,7 @@ void describe('convertAuthorizationModesToCDK', () => {
9289

9390
assert.deepStrictEqual(
9491
convertAuthorizationModesToCDK(
95-
functionInstanceProvider,
92+
getInstancePropsStub,
9693
undefined,
9794
undefined
9895
),
@@ -114,7 +111,7 @@ void describe('convertAuthorizationModesToCDK', () => {
114111

115112
assert.deepStrictEqual(
116113
convertAuthorizationModesToCDK(
117-
functionInstanceProvider,
114+
getInstancePropsStub,
118115
providedAuthConfig,
119116
undefined
120117
),
@@ -147,7 +144,7 @@ void describe('convertAuthorizationModesToCDK', () => {
147144

148145
assert.deepStrictEqual(
149146
convertAuthorizationModesToCDK(
150-
functionInstanceProvider,
147+
getInstancePropsStub,
151148
undefined,
152149
authModes
153150
),
@@ -172,7 +169,7 @@ void describe('convertAuthorizationModesToCDK', () => {
172169

173170
assert.deepStrictEqual(
174171
convertAuthorizationModesToCDK(
175-
functionInstanceProvider,
172+
getInstancePropsStub,
176173
undefined,
177174
authModes
178175
),
@@ -195,9 +192,6 @@ void describe('convertAuthorizationModesToCDK', () => {
195192
},
196193
}),
197194
};
198-
functionInstanceProvider = {
199-
provide: (): IFunction => authFn,
200-
};
201195

202196
const authModes: AuthorizationModes = {
203197
lambdaAuthorizationMode: {
@@ -215,7 +209,7 @@ void describe('convertAuthorizationModesToCDK', () => {
215209

216210
assert.deepStrictEqual(
217211
convertAuthorizationModesToCDK(
218-
functionInstanceProvider,
212+
getInstancePropsStub,
219213
undefined,
220214
authModes
221215
),
@@ -240,7 +234,7 @@ void describe('convertAuthorizationModesToCDK', () => {
240234

241235
assert.deepStrictEqual(
242236
convertAuthorizationModesToCDK(
243-
functionInstanceProvider,
237+
getInstancePropsStub,
244238
providedAuthConfig,
245239
authModes
246240
),
@@ -254,7 +248,7 @@ void describe('convertAuthorizationModesToCDK', () => {
254248
};
255249

256250
const convertedOutput = convertAuthorizationModesToCDK(
257-
functionInstanceProvider,
251+
getInstancePropsStub,
258252
providedAuthConfig,
259253
authModes
260254
);

0 commit comments

Comments
 (0)