How can I use IAM to invoke AppSync wtihin AWS Lambda? - amazon-web-services

I'm currently in the process of implementing a subscription mutation within AWS Lambda using AppSync. I want to use IAM and avoid using any other type of AUTH mechanism as I'm calling it within the AWS stack. Unfortunately, I'm receiving the following 403 error:
(Excerpt from an SQS' CloudWatch log)
{
"errorMessage": "Response not successful: Received status code 403",
"name": "ServerError",
"errorType": "UnrecognizedClientException",
"message": "The security token included in the request is invalid."
}
I've tried following these to no avail, but I don't know what I'm missing:
https://medium.com/#jan.hesters/how-to-use-aws-appsync-in-lambda-functions-e593a9cef1d5
https://www.edwardbeazer.com/using-appsync-client-from-lambda/
https://adrianhall.github.io/cloud/2018/10/26/backend-graphql-trigger-appsync/
How to send GraphQL mutation from one server to another?
AWS Appsync + HTTP DataSources + AWS IAM
AWS Appsync Invoke mutate from Lambda?
Here's the code that I'm currently calling it from:
import AWS from "aws-sdk";
import { AWSAppSyncClient } from "aws-appsync";
import { Mutation, mutations } from "./mutations/";
import "cross-fetch/polyfill";
/**
*
*/
AWS.config.update({
region: Config.region,
});
export class AppSyncClient {
client: AWSAppSyncClient<any>;
constructor() {
if (!env.APPSYNC_ENDPOINT) {
throw new Error("APPSYNC_ENDPOINT not defined");
}
/**
* We create the AppSyncClient with the AWS_IAM
* authentication.
*/
this.client = new AWSAppSyncClient({
url: env.APPSYNC_ENDPOINT,
region: Config.region,
auth: {
credentials: AWS.config.credentials!,
type: "AWS_IAM",
},
disableOffline: true,
});
}
/**
* Sends a mutation on the AppSync Client
* #param mutate The Mutation that will be sent with the variables.
* #returns
*/
sendMutation(mutate: Mutation) {
const mutation = mutations[mutate.type] as any;
const variables = mutate.variables;
console.log("Sending the mutation");
console.log("Variables is ", JSON.stringify(variables));
return this.client.mutate({
mutation,
fetchPolicy: "network-only",
variables,
});
}
}
Here's the current IAM from the Lambda SQS:
{
"Statement": [
{
"Action": [
"appsync:GraphQL"
],
"Effect": "Allow",
"Resource": [
"arn:aws:appsync:us-east-2:747936726382:apis/myapi"
]
}
],
"Version": "2012-10-17"
}
I know it is not an IAM problem from the lambda, because I've tried momentarily giving it full access, and I still got the 403 error.
I've also verified that AppSync has the IAM permission configured (as an additional provider).
Do you guys have any ideas? I'm impressed that this is a ghost topic with such little configuraiton references.

I finally nailed it. I went and re-read for third time Adrian Hall's post, and it did lead me to the solution.
Please note that I installed the AWS AppSync client which is not needed but simplifies the process (otherwise you'd have to sign the URL yourself. For that see Adrian Hall's post).
There are a couple of things:
You need to polyfill "fetch" by including either cross-fetch (Otherwise you're going to get hit by Invariant Violation from the Apollo Client which AppSync internally uses).
You need to pass the lambda's internal IAM credentials (Which I didn't even know existed) to the configuration portion of the AppSyncClient.
You need to add the proper permission to the IAM role of the lambda, in this case: ["appsync:GraphQL"] for the action.
Here's some code:
This is the AppSync code.
// The code is written in TypeScript.
// https://adrianhall.github.io/cloud/2018/10/26/backend-graphql-trigger-appsync/
// https://www.edwardbeazer.com/using-appsync-client-from-lambda/
import { env } from "process";
import { Config, env as Env } from "../../../../shared";
// This is such a bad practice
import AWS from "aws-sdk";
import { AWSAppSyncClient } from "aws-appsync";
import { Mutation, mutations } from "./mutations/";
// Very important, otherwise it won't work!!! You'll have Invariant Violation
// from Apollo Client.
import "cross-fetch/polyfill";
/**
*
*/
AWS.config.update({
region: Config.region,
credentials: new AWS.Credentials(
env.AWS_ACCESS_KEY_ID!,
env.AWS_SECRET_ACCESS_KEY!,
env.AWS_SESSION_TOKEN!
),
});
export class AppSyncClient {
client: AWSAppSyncClient<any>;
constructor() {
// Your AppSync endpoint - The Full URL.
if (!Env.APPSYNC_ENDPOINT) {
throw new Error("APPSYNC_ENDPOINT not defined");
}
/**
* We create the AppSyncClient with the AWS_IAM
* authentication.
*/
this.client = new AWSAppSyncClient({
url: Env.APPSYNC_ENDPOINT,
region: Config.region,
auth: {
credentials: AWS.config.credentials!,
type: "AWS_IAM",
},
disableOffline: true,
});
}
/**
* Sends a mutation on the AppSync Client
* #param mutate The Mutation that will be sent with the variables.
* #returns
*/
// The mutation is a object that holds the mutation in
// the `gql` tag. You can ommit this part.
sendMutation(mutate: Mutation) {
const mutation = mutations[mutate.type] as any;
const variables = mutate.variables;
// This is the important part.
return this.client.mutate({
mutation,
// Specify "no-cache" in the policy.
// network-only won't work.
fetchPolicy: "no-cache",
variables,
});
}
}
We need to enable IAM in the AppSync authorization mechanism. Yes, it is possible to have multiple Authentication enabled. I'm currently using OPEN_ID and IAM simultaneously.
https://us-east-2.console.aws.amazon.com/appsync/home?region=us-east-2#/myappsync-id/v1/settings
Here's the Lambda's IAM policy that executes the GQL:
{
"Statement": [
{
"Action": [
"appsync:GraphQL"
],
"Effect": "Allow",
"Resource": [
"arn:aws:appsync:us-east-2:747936726382:apis/ogolfgja65edlmhkcpp3lcmwli/*"
]
}
],
"Version": "2012-10-17"
}
You can further restrict here in the following fashion:
arn:${Partition}:appsync:${Region}:${Account}:apis/${GraphQLAPIId}/types/${TypeName}/fields/${FieldName}
arn:aws:appsync:us-east-2:747936726382:apis/ogolfgja65edlmhkcpp3lcmwli/types/Mutation/field/myCustomField"
Note, we need to better restrict this as we are currently giving it entire access to the API.
In your .gql file (AppSync GraphQL schema), add the #aws_iam directive to the mutation that is being used to send the subscriptions to, in order to restrict access from the front-end.
type Mutation {
addUsersMutationSubscription(
input: AddUsersSagaResultInput!
): AddUsersSagaResult #aws_iam
}

Related

Unable to auth into AppSync GraphQL api from Lambda using IAM and AppSyncClient

I'm using an amplify stack and need to perform some actions to my graphql api which has dynamodb behind it. The request in my lambda function returns an Unauthorized error: "Not Authorized to access getSourceSync on type SourceSync", where getSourceSync is the gql query and SourceSync is the model name.
My schema.grapqhl for this particular model is set up as following. Note auth rule allow private provider iam:
type SourceSync #model (subscriptions: { level: off }) #auth(rules: [
{allow: private, provider: iam}
{allow: groups, groups: ["Admins"], provider: userPools},
{allow: groups, groups: ["Users"], operations: [create], provider: userPools},
{allow: groups, groupsField: "readGroups", operations: [create, read], provider: userPools},
{allow: groups, groupsField: "editGroups", provider: userPools}]) {
id: ID! #primaryKey
name: String
settings_id: ID #index(name: "bySettingsId", queryField: "sourceSyncBySettingsId")
settings: Settings #hasOne(fields: ["settings_id"])
childLookup: String
createdAt: AWSDateTime!
updatedAt: AWSDateTime!
_createdBy: String
_lastChangedBy: String
_localChanges: AWSJSON
readGroups: [String]
editGroups: [String]
}
My lambda function's role has the following inline policy attached to it. (Actual ID values have been omitted for security purposes on this post):
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"appsync:GraphQL"
],
"Resource": [
"arn:aws:appsync:us-east-1:111myaccountID:apis/11mygraphqlapiID/*"
],
"Effect": "Allow"
},
{
"Action": [
"appsync:GetType"
],
"Resource": [
"*"
],
"Effect": "Allow"
}
]
}
And finally my lambda function is set up as follows with a simple query test:
/* stuff */
"use strict";
const axios = require("axios");
const awsAppSync = require("aws-appsync").default;
const gql = require("graphql-tag");
require("cross-fetch/polyfill");
const { PassThrough } = require("stream");
const aws = require("aws-sdk");
aws.config.update({
region: process.env.AWS_REGION,
});
const appSync = new aws.AppSync();
const graphqlClient = new awsAppSync({
url: process.env.API_GRAPHQLAPIENDPOINTOUTPUT,
region: process.env.AWS_REGION,
auth: {
type: "AWS_IAM",
credentials: aws.config.credentials,
},
disableOffline: true
});
exports.handler = async (event, context) => {
console.log('context :: '+JSON.stringify(context));
console.log('aws config :: '+JSON.stringify(aws.config));
const sourceSyncTypes = await appSync
.getType({
apiId: process.env.API_GRAPHQLAPIIDOUTPUT,
format: "JSON",
typeName: "SourceSync",
})
.promise();
console.log('ss = '+JSON.stringify(sourceSyncTypes));
try {
const qs = gql`query GetSourceSync {
getSourceSync(id: "ov3") {
id
name
}
}`;
const res = await graphqlClient.query({query: qs, fetchPolicy: 'no-cache'});
console.log(JSON.stringify(res));
}
catch(e) {
console.log('ERR :: '+e);
console.log(JSON.stringify(e));
}
};
Found the solution, there seems to be an issue with triggering a rebuild of the resolvers on the api after permitting a function to access the graphql api. However there is a distinction to note:
If the graphql api is part of an amplify app stack, then only functions created through the amplify cli for that app (ex: amplify add function) and that are given access to the api through there will be able to access the api.
additonally during the update when you either create, or update the function to give it permissions, you must ensure that during the amplify push operation, the api stack will also be updating. you can trigger this by simply adding or removing a space in a comment inside of your amplify/backend/api//schema.graphql file.
If the function was created "adhoc" directly through the aws console, but it is trying to access a graphql api that was created as part of an amplify app stack, then you will need to put that function's role in amplify/backend/api/< apiname>/custom-roles.json in the format
{
"adminRoleNames": ["<role name>", "<role name 2>", ...]
}
Documentation references here.
If neither your api or lambda function were created with the amplify cli as part of an app stack, then just need to give access to the graphql resources for query, mutation and subscription to the lambda's role in IAM, via inline policies or a pre-defined policy.

Cannot give aws-lambda access to aws-appsync API

I am working on a project where users can upload files into a S3 bucket, these uploaded files are mapped to a GraphQL key (which was generated by Amplify CLI), and an aws-lambda function is triggered. All of this is working, but the next step I want is for this aws-lambda function to create a second file with the same ownership attributes and POST the location of the saved second file to the GraphQL API.
I figured that this shouldn't be too difficult but I am having a lot of difficulty and can't understand where the problem lies.
BACKGROUND/DETAILS
I want the owner of the data (the uploader) to be the only user who is able to access the data, with the aws-lambda function operating in an admin role and able to POST/GET to API of any owner.
The GraphQL schema looks like this:
type FileUpload #model
#auth(rules: [
{ allow: owner}]) {
id: ID!
foo: String
bar: String
}
And I also found this seemingly-promising AWS guide which I thought would give an IAM role admin access (https://docs.amplify.aws/cli/graphql/authorization-rules/#configure-custom-identity-and-group-claims) which I followed by creating the file amplify/backend/api/<your-api-name>/custom-roles.json and saved it with
{
"adminRoleNames": ["<YOUR_IAM_ROLE_NAME>"]
}
I replaced "<YOUR_IAM_ROLE_NAME>" with an IAM Role which I have given broad access to, including this appsync access:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"appsync:*"
],
"Resource": "*"
}
]
}
Which is the role given to my aws-lambda function.
When I attempt to run a simple API query in my aws-lambda function with the above settings I get this error
response string:
{
"data": {
"getFileUpload": null
},
"errors": [
{
"path": [
"getFileUpload"
],
"data": null,
"errorType": "Unauthorized",
"errorInfo": null,
"locations": [
{
"line": 3,
"column": 11,
"sourceName": null
}
],
"message": "Not Authorized to access getFileUpload on type Query"
}
]
}
my actual python lambda script is
import http
API_URL = '<MY_API_URL>'
API_KEY = '<>MY_API_KEY'
HOST = API_URL.replace('https://','').replace('/graphql','')
def queryAPI():
conn = http.client.HTTPSConnection(HOST, 443)
headers = {
'Content-type': 'application/graphql',
'x-api-key': API_KEY,
'host': HOST
}
print('conn: ', conn)
query = '''
{
getFileUpload(id: "<ID_HERE>") {
description
createdAt
baseFilePath
}
}
'''
graphql_query = {
'query': query
}
query_data = json.dumps(graphql_query)
print('query data: ', query_data)
conn.request('POST', '/graphql', query_data, headers)
response = conn.getresponse()
response_string = response.read().decode('utf-8')
print('response string: ', response_string)
I pass in the API key and API URL above in addition to giving AWS-lambda the IAM role. I understand that only one is probably needed, but I am trying to get the process to work then pare it back.
QUESTION(s)
As far as I understand, I am
providing the appropriate #auth rules to my GraphQL schema based on my goals and (2 below)
giving my aws-lambda function sufficient IAM authorization (via both IAM role and API key) to override any potential restrictive #auth rules of my GraphQL schema
But clearly something is not working. Can anyone point me towards a problem that I am overlooking?
I had similar problem just yesterday.
It was not 1:1 what you're trying to do, but maybe it's still helpful.
So I was trying to give lambda functions permissions to access the data based on my graphql schema. The schema had different #auth directives, which caused the lambda functions to not have access to the data anymore. Even though I gave them permissions via the cli and IAM roles. Although the documentation says this should work, it didn't:
if you grant a Lambda function in your Amplify project access to the GraphQL API via amplify update function, then the Lambda function's IAM execution role is allow-listed to honor the permissions granted on the Query, Mutation, and Subscription types.
Therefore, these functions have special access privileges that are scoped based on their IAM policy instead of any particular #auth rule.
So I ended up adding #auth(rules: [{ allow: custom }]) to all parts of my schema that I want to access via lambda functions.
When doing this, make sure to add "lambda" as auth mode to your api via amplify update api.
In the authentication lambda function, you could then check if the user, who is invoking the function, has access to the requested query/S3 Data.

Serverless my custom authorizer is not working

I have the following serverless.yaml:
getSth:
handler: src/handlers/getSth.getSth
events:
- http:
path: getSth
method: get
cors: true
private: true
authorizer: authorizerFunc
authorizerFunc:
handler: src/handlers/authorizer.authorizer
getSth handler:
module.exports.getSth = async (event, context) => {
const response = {
statusCode: 200,
body: JSON.stringify({message: "nice you can call this});
}
return response;
}
authorizerFunc:
module.exports.authorizer = async (event, context) => {
console.log('i will fail your authorization');
let response = {
isAuthorized: false,
context: {
stringKey: "value",
numberKey: 1,
booleanKey: true,
arrayKey: ["value1", "value2"],
mapKey: { value1: "value2" },
},
};
return response;
}
That results in getting respons 200 in spite of the fact authorizer should not allow to execute that getSth function. Also console log 'I will fail your authorization' is not logged.
What am I doing wrong ?
I have tried to analyse your code and find several points where you can start digging.
Private functions
The key private: true actually makes API Gateway require an API key. I did now not try myself but perhaps private: true and an authorizer do not go together.
Strange still that you are able to call the function then. How do you call the function? From the CLI or through API Gateway and an API testing tool such as Postman or Insomnia?
Authorizer Configuration
Your authorizer configuration is definitely correct. We do have the very same setup in our code.
Authorizer Events
An authorizer function gets an APIGatewayTokenAuthorizerEvent in and should reply with a APIGatewayAuthorizerResult. The latter looks closely like an IAM statement and we do not use the field isAuthorized: false as per your example. I do not understand where this field is coming from. Our result to allow a request looks more or less like the following:
{
"principalId": "<our auth0 user-id>",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Action": "execute-api:Invoke",
"Effect": "Allow",
"Resource": "*"
}]
}
}
Note how the field principalId refers to the username we get from our identity provider (Auth0). And in reality looks something like this: auth0|6f84a3z162c72d0d0d000a00.
Further, we can allow or deny the function call via the Effect field which can hold the values Allow or Deny.
Finally, you can specify which resource the caller is permitted to call. For simplicity of this answer I put * there. Of course in the real world you can pull the ARN of the called function from the event and context and pass that into the policy document.
Opinion
We also had a hard way of figuring this out via documentation from AWS. Of course for AWS the preferred integration would be via AWS Cognito (which I also do prefer due to the more streamlined integration. We benefited quite a bit form the use of TypeScript here which we use to enforce types in and out of our Serverless functions. This way it was rather easy to figure out how the response needs to look like.
Background
We use the custom authorizer integration to allow a user base already existing in Auth0 consume our Serverless based APIs via application clients or single page applications.

AWS generate dynamic credential for S3 folder level access?

I'm new to AWS and still figuring out how to do things.
Part of my web application is using AWS S3 for file storage, but I want each user to be only able to access specific folders(for CRUD) in the bucket.
The backend server will track what folders the user will be able to access.
I know it is possible to define policies that allow access to specific folders(by matching prefix of objects), but can I generate these policies dynamically and get credentials with these policies attached (probably with Cognito?). So that these credentials could be passed to client-side to enable access to S3 folders.
I'm wondering if it is possible to do that and what services are required to achieve this.
You should change your view, each time you want to share a file with one of your users, you should check your database about their permissions( folders they have access) and if logical things on your side are correct, generate a presigned URL for access to that object.
How presigned URL works.
When you generate a presigned URL for accessing to an object, you can set the time limit too, it means after that time, the URL not work and expired.
For more information about the presigned URL, read the following documents on Amazon Web services website:
Generate a Pre-signed Object URL Using the AWS SDK for Java
Generate a Pre-signed Object URL Using AWS SDK for .NET
Also, if you want to create users and assign the right policy for access them to their folder you can follow these instructions:
You can use the IAM API to creating a user for each of your users, and attach the right policy for each of them.
For example, for creating the new user, you should use the following API
/* The following create-user command creates an IAM user named Bob in the current account. */
var params = {
UserName: "Bob"
};
iam.createUser(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
/*
data = {
User: {
Arn: "arn:aws:iam::123456789012:user/Bob",
CreateDate: <Date Representation>,
Path: "/",
UserId: "AKIAIOSFODNN7EXAMPLE",
UserName: "Bob"
}
}
*/
});
For more info about the Create user API, read the following
https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateUser.html
After creating a user, you should create a policy for each of them with CreatePolicy API.
var params = {
PolicyDocument: 'STRING_VALUE', /* required */
PolicyName: 'STRING_VALUE', /* required */
Description: 'STRING_VALUE',
Path: 'STRING_VALUE'
};
iam.createPolicy(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
For more info about the Create policy read the following doc:
https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreatePolicy.html
And finally, you should assign the policy you created before to each user by the AttachUserPolicy API.
/* The following command attaches the AWS managed policy named AdministratorAccess to the IAM user named Alice. */
var params = {
PolicyArn: "arn:aws:iam::aws:policy/AdministratorAccess",
UserName: "Alice"
};
iam.attachUserPolicy(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
For more info about the AttachUserPolicy API read the following doc:
https://docs.aws.amazon.com/IAM/latest/APIReference/API_AttachUserPolicy.html
The last part is about the which policy you should create and assign to each of them; we use the following policy for listing objects in each folder:
{
"Sid": "AllowListingOfUserFolder",
"Action": ["s3:ListBucket"],
"Effect": "Allow",
"Resource": ["arn:aws:s3:::my-company"],
"Condition":{"StringLike":{"s3:prefix":["home/David/*"]}}
}
And the following policy for actions in each folder:
{
"Sid": "AllowAllS3ActionsInUserFolder",
"Effect": "Allow",
"Action": ["s3:*"],
"Resource": ["arn:aws:s3:::my-company/home/David/*"]
}
For more detailed info about that policies read the following article by Jim Scharf:
https://aws.amazon.com/blogs/security/writing-iam-policies-grant-access-to-user-specific-folders-in-an-amazon-s3-bucket/

AWS Amplify AppSync IAM 401

I'm getting GraphQLError: Request failed with status code 401
I followed the automatic configuration instructions from:
https://aws.github.io/aws-amplify/media/api_guide#automated-configuration-with-cli
I tried looking, but there are a lack of resources for IAM. It looks like everything should be setup automatically, and done with the Amplify CLI after I put in the IAM access key and secret.
Is further setup required? Here is my code:
import Amplify, { API, graphqlOperation, Hub } from "aws-amplify";
import aws_config from "../../aws-exports";
Amplify.configure(aws_config);
const ListKeywords = `query ListKeywords {
listKeyword {
keyword {
id
name
}
}
}`;
const loop = async () => {
const allKeywords = await API.graphql(graphqlOperation(ListKeywords));
}
Could it also be because my GraphQL resolvers are not setup yet for ListKeywords?
If you're using IAM as the Authorization type on your AppSync API then the issue is the Cognito Role being used with the Auth category when invoking Amplify.configure() isn't granted permissions for GraphQL operations. It needs something like this attached:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"appsync:GraphQL"
],
"Resource": [
"arn:aws:appsync:us-west-2:123456789012:apis/YourGraphQLApiId/*"
]
}
]
}
More details here: https://docs.aws.amazon.com/appsync/latest/devguide/security.html
Not sure if this helps but I've been struggling with this for a while and found that if I add the API and use IAM as the auth method I need to add 'auth' to the schema too.
See below:
type TimeLapseCamera #model
#auth(rules: [
{ allow: private, provider: iam }
])
{
...
}
I just tested this and my web page is successfully adding a record.
Note to other comment; I do not have AWS at all in this - its a simple VUE app with Amplify.
I just changed ~/.aws/credentials and now it's working.
Looks like even if you have project specific configuration via Amplify's command line tools or ~/.awsmobile/aws-config.js, it still relies on ~/.aws