aws-amplify / amplify-category-api

The AWS Amplify CLI is a toolchain for simplifying serverless web and mobile development. This plugin provides functionality for the API category, allowing for the creation and management of GraphQL and REST based backends for your amplify project.
https://docs.amplify.aws/
Apache License 2.0
89 stars 75 forks source link

Cannot access custom mutation from IAM authorised function (lambda) #2755

Closed kush-prof closed 1 week ago

kush-prof commented 1 month ago

Environment information

System:
  OS: macOS 14.5
  CPU: (8) arm64 Apple M1
  Memory: 173.38 MB / 8.00 GB
  Shell: /bin/zsh
Binaries:
  Node: 20.12.0 - ~/.nvm/versions/node/v20.12.0/bin/node
  Yarn: 1.22.19 - ~/.yarn/bin/yarn
  npm: 10.5.0 - ~/.nvm/versions/node/v20.12.0/bin/npm
  pnpm: undefined - undefined
NPM Packages:
  @aws-amplify/auth-construct: 1.2.0
  @aws-amplify/backend: 1.0.4
  @aws-amplify/backend-auth: 1.1.0
  @aws-amplify/backend-cli: 1.2.1
  @aws-amplify/backend-data: 1.1.0
  @aws-amplify/backend-deployer: 1.0.2
  @aws-amplify/backend-function: 1.3.0
  @aws-amplify/backend-output-schemas: 1.1.0
  @aws-amplify/backend-output-storage: 1.0.2
  @aws-amplify/backend-secret: 1.0.0
  @aws-amplify/backend-storage: 1.0.4
  @aws-amplify/cli-core: 1.1.1
  @aws-amplify/client-config: 1.1.1
  @aws-amplify/deployed-backend-client: 1.1.0
  @aws-amplify/form-generator: 1.0.0
  @aws-amplify/model-generator: 1.0.2
  @aws-amplify/platform-core: 1.0.3
  @aws-amplify/plugin-types: 1.1.0
  @aws-amplify/sandbox: 1.1.1
  @aws-amplify/schema-generator: 1.2.0
  aws-amplify: 6.4.3
  aws-cdk: 2.150.0
  aws-cdk-lib: 2.150.0
  typescript: 5.5.4
AWS environment variables:
  AWS_STS_REGIONAL_ENDPOINTS = regional
  AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1
  AWS_SDK_LOAD_CONFIG = 1
No CDK environment variables

Description

I have to write a scheduler lambda that should invoke my custom mutation on AppSync APIs but it seems I am unable to get it authorised to use the custom mutation. In the schema of AppSync on console I cannot see the directive @aws_iam on the custom mutation that I created and on the other hand it is present on all the out of box mutation and queires, this is preventing my lambda to access the custom mutations.

amplify/data/resource.ts

import { type ClientSchema, a, defineData, defineFunction } from "@aws-amplify/backend";
import { ingestRadarMatches } from "../functions/ingest-radar-matches/resource";

const schema = a
  .schema({
    TeamType: a.customType({
      id: a.string().required(),
      name: a.string().required(),
      abbreviation: a.string().required(),
      qualifier: a.enum(["home", "away"]),
      gender: a.string().required(),
      country: a.string(),
      country_code: a.string(),
    }),
    Matches: a
      .model({
        id: a.string().required(),
        __typename: a.string().required(),
        editable: a.boolean().default(true),
        teamA: a.ref("TeamType"),
        teamB: a.ref("TeamType"),
        startTime: a.datetime().required(),
        completionTime: a.datetime(),
        tournament: a.string().required(),
        sportType: a.enum(["cricket", "football"]),
        sportSubType: a.enum(["test", "odi", "t20", "t10"]),
        lineup: a.ref('LineUp').array(),
      })
      .identifier(["id", "__typename"])
      .secondaryIndexes((index) => [
        index("sportType").sortKeys(["startTime"]),
      ]),

    //

    createMatch: a
      .mutation()
      .arguments({
        id: a.string().required(),
        teamA: a.json(),
        teamB: a.json(),
        editable: a.boolean(),
        startTime: a.datetime().required(),
        tournament: a.string().required(),
        sportType: a.enum(["cricket", "football"]),
        sportSubType: a.enum(["test", "odi", "t20", "t10"]),
      })
      .returns(a.ref("Matches"))
      .handler([
        a.handler.custom({
          dataSource: a.ref("Matches"),
          entry: "./createMatch.js",
        }),
      ]),
  })
  .authorization((allow) => [allow.authenticated(), allow.resource(ingest)]);

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "userPool",
  },
});

/createMatches.js

import { util } from "@aws-appsync/utils";
import * as ddb from '@aws-appsync/utils/dynamodb'

export function request(ctx) {
    const { teamA, teamB } = ctx.args;
    console.log(ctx);
  return {
    operation: "PutItem",
    key: util.dynamodb.toMapValues({
      id: ctx.args.id,
    }),
    attributeValues: util.dynamodb.toMapValues({
        id: ctx.args.id,
        __typename: "Match",
        editable: ctx.args.editable,
        teamA: teamA,
        teamB: teamB,
        startTime: ctx.args.startTime,
        completionTime: ctx.args.completionTime,
        tournament: ctx.args.tournament,
        sportType: ctx.args.sportType,
        sportSubType: ctx.args.sportSubType,
        createdAt: util.time.nowISO8601(),
        updatedAt: util.time.nowISO8601(),
    }),
  };
}

export function response(ctx) {
  return ctx.result;
}

functions/ingest/handler.ts

export const handler: Handler = async (event, context) => {
    try {
        const lastMatchDate: any = await dataClient.graphql({
            query: `
                query ListMatchesBySportTypeAndStartTime($sportType: MatchesSportType! , $limit: Int, $sortDirection: ModelSortDirection) {
                    listMatchesBySportTypeAndStartTime(sportType: $sportType, limit: $limit, sortDirection: $sortDirection) {
                        items {
                            startTime
                        }
                    }
                }
            `,
            variables: {
                sportType: "cricket",
                limit: 1,
                sortDirection: "DESC"
            }
        });

            const lastStartTime = lastMatchDate.data?.listMatchesBySportTypeAndStartTime?.items[0]?.startTime;
            const today = new Date();
            let startDate = today;

            console.log('[] LAST DATE IS', lastStartTime);

            const res = await dataClient.graphql({
                query: `
                    mutation CreateMatch($id: String!, $startTime: AWSDateTime!, $tournament: String!, $sportType: CreateMatchSportType!, $sportSubType: CreateMatchSportSubType, $teamA: AWSJSON!, $teamB: AWSJSON!, $editable: Boolean!) {
                        createMatch(id: $id, startTime: $startTime, tournament: $tournament, sportType: $sportType, sportSubType: $sportSubType, teamA: $teamA, teamB: $teamB, editable: $editable) {
                            id
                            __typename
                            editable
                            teamA {
                                name
                                qualifier
                            }
                            teamB {
                                name
                                qualifier
                            }
                            startTime
                            completionTime
                            tournament
                            sportType
                            sportSubType
                        }
                    }
                `,
                variables: {
                    id: "sr:match:892348",
                    startTime: "2024-08-06T11:04:07+00:00",
                    tournament: "Test tournament",
                    sportType: "cricket",
                    sportSubType: "test",
                    editable: true,
                    teamA: JSON.stringify({
                        id: "",
                        name: "Team A",
                        abbreviation: "TEAM",
                        qualifier: "home",
                        gender: "male",
                        country: "india",
                        country_code: "IND"
                    }),
                    teamB: JSON.stringify({
                        id: "",
                        name: "Team A",
                        abbreviation: "TEAM",
                        qualifier: "home",
                        gender: "male",
                        country: "india",
                        country_code: "IND"
                    }),
                },
            });

            console.log('[] res of api call ', res);

            return { statusCode: 200, body: JSON.stringify({ message: 'Matches ingestion completed successfully' }) };
    } catch (error) {
            console.error('Error in handler:', error);
            return { statusCode: 500, body: JSON.stringify({ message: 'An error occurred during matches ingestion' }) };
    }
};

The ingest function is a scheduler that needs to invoke createMatch mutation. I get the following error:

Error in handler: {
  data: { createMatch: null },
  errors: [
    {
      path: [Array],
      data: null,
      errorType: 'Unauthorized',
      errorInfo: null,
      locations: [Array],
      message: 'Not Authorized to access createMatch on type Mutation'
    }
  ]
}

This is because in the schema of AppSync @aws_iam is not present in the above custom mutation while is present of auto generated mutations and queries

Screenshot 2024-08-09 at 7 51 09 PM
ykethan commented 1 month ago

Hey,👋 thanks for raising this! I'm going to transfer this over to our API repository for better assistance 🙂

chrisbonifacio commented 1 month ago

Hi @kush-prof , it seems like your schema and Lambda are configured correctly to have access to mutations. However, the default auth mode on your API is userPool, as seen in defineData. I can't see that in your Lambda you are setting the client's authMode to identityPool so that it uses an IAM role to sign the request.

This page might be helpful:

https://docs.amplify.aws/react/build-a-backend/data/customize-authz/grant-lambda-function-access-to-api/

This section in particular:

In the handler file for your function, configure the Amplify data client

// amplify/functions/data-access.ts
import { Amplify } from 'aws-amplify';
import { generateClient } from 'aws-amplify/data';
import { Schema } from '../data/resource';
import { env } from '$amplify/env/<function-name>'; // replace with your function name

Amplify.configure(
  {
    API: {
      GraphQL: {
        endpoint: env.<amplifyData>_GRAPHQL_ENDPOINT, // replace with your defineData name
        region: env.AWS_REGION,
        defaultAuthMode: 'identityPool' // required for request to be signed with IAM role for Lambda
      }
    }
  },
  {
kush-prof commented 1 month ago

Hey @chrisbonifacio thanks for the response.

Sorry for missing that part in the initial report. Here is the snippet from handler.ts

import type { Handler } from 'aws-lambda';
import axios from 'axios';
import { RadarMatchSummaryResponse, ScheduleMatchesResponse } from '../types/types';

const API_KEY = process.env.RADAR_API_KEY;
const BASE_URL = 'https://api.sportradar.com/cricket-t2/en';

import { Amplify } from 'aws-amplify';
import { generateClient } from 'aws-amplify/data';
import { data, Schema } from '../../data/resource';
import { env } from '$amplify/env/ingest-radar-matches'; // replace with your function name

Amplify.configure(
  {
    API: {
      GraphQL: {
        endpoint: env.AMPLIFY_DATA_GRAPHQL_ENDPOINT, // replace with your defineData name
        region: env.AWS_REGION,
        defaultAuthMode: 'identityPool',
      }
    }
  },
  {
    Auth: {
      credentialsProvider: {
        getCredentialsAndIdentityId: async () => ({
          credentials: {
            accessKeyId: env.AWS_ACCESS_KEY_ID,
            secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
            sessionToken: env.AWS_SESSION_TOKEN,
          },
        }),
        clearCredentialsAndIdentityId: () => {
          /* noop */
        },
      },
    },
  }
);

const dataClient = generateClient<Schema>();

Still facing same error.

kush-prof commented 1 month ago

@chrisbonifacio I would like to emphasise on the point that I do not see the @aws_iam decorator on the custom created mutation in the AppSync schema on AWS console. When I manually put that in the schema and save it. The function starts to work properly that tells me that the custom created mutation does not allow the IAM access to it and only the cognito user pool access as it has @aws_cognito_user_pools

Screenshot 2024-08-10 at 12 13 37 AM
Sterv commented 1 month ago

@kush-prof I was able to solve this for function handler by attaching the apiRestPolicy to an IAM user like this

// create a new API stack
const apiStack = backend.createStack("api-stack");

const restApiName = "myRestApi";

// create a new REST API
const myRestApi = new RestApi(apiStack, restApiName, {
  restApiName: "myRestApi",
  deploy: true,
  deployOptions: {
    stageName: "dev",
  },
  defaultCorsPreflightOptions: {
    allowOrigins: Cors.ALL_ORIGINS, // Restrict this to domains you trust
    allowMethods: Cors.ALL_METHODS, // Specify only the methods you need to allow
    allowHeaders: Cors.DEFAULT_HEADERS, // Specify only the headers you need to allow
  },
});

// create a new Lambda integration
const lambdaIntegration = new LambdaIntegration(
  backend.myApiFunction.resources.lambda
);

// create a new resource path with IAM authorization
const routePath = myRestApi.root.addResource("todos", {
  defaultMethodOptions: {
    authorizationType: AuthorizationType.IAM,
  },
});

// add methods you would like to create to the resource path
routePath.addMethod("GET", lambdaIntegration);
routePath.addMethod("POST", lambdaIntegration);
routePath.addMethod("DELETE", lambdaIntegration);
routePath.addMethod("PUT", lambdaIntegration);

// add a proxy resource path to the API
ticketsPath.addProxy({
  anyMethod: true,
  defaultIntegration: lambdaIntegration,
});

// create a new IAM policy to allow Invoke access to the API
const apiRestPolicy = new Policy(apiStack, `RestApiPolicy`, {
  statements: [
    new PolicyStatement({
      actions: ["execute-api:Invoke"],
      resources: [
        `${myRestApi.arnForExecuteApi("*", `/${todos}`, "dev")}`,
        `${myRestApi.arnForExecuteApi("*", `/${todos}/*`, "dev")}`,
      ],
    }),
  ],
});

const devApiConsumer = User.fromUserName(
  apiStack,
  "your-iam-user-name",
  "your-iam-user-name"
);

const users = [devApiConsumer];

users.forEach((user) => {
  user.attachInlinePolicy(apiRestPolicy);
});

// add outputs to the configuration file
backend.addOutput({
  custom: {
    API: {
      [myRestApi.restApiName]: {
        endpoint: myRestApi.url,
        region: Stack.of(myRestApi).region,
        apiName: myRestApi.restApiName,
      },
    },
  },
});
kush-prof commented 1 month ago

@Sterv thanks for the reply but I don' think that your solution fits my exact usecase.

@chrisbonifacio I was wondering if we have a more elegant solution or is it a bug in amplify or simply am I missing something?

chrisbonifacio commented 1 month ago

Hi @kush-prof, apologies for the delay. We still need to try and reproduce the issue.

Unless I'm mistaken, I don't think that the schema necessarily needs to have @aws_iam for the Lambda to have access as long as IAM is an auth mode on the API.

If the Lambda's execution role has permissions to execute Queries & Mutations, I think that should be enough. Maybe the permissions are not being added to the role policy correctly.

Can you check the Lambda's execution role and confirm if it allows GraphQL queries/mutations on your API?

If not, then this is probably a bug and in the meantime you can try using my function.addToRolePolicy to grant your Lambda permissions in the backend.ts file

chrisbonifacio commented 1 month ago

Also, I noticed that your auth rule is allow.resources(ingest) but what you're importing is ingestRadarMatches. Seems like a typo or maybe you changed something after copy/pasting?

Secondly, looking at the schema again, you don't seem to have an auth rule that suggests IAM is an auth mode on the API. You only have allow.authenticated() and allow.resource. The identityPool auth mode is facilitated by allow.guest(), or allow.authenticated("identityPool") this is what puts @aws_iam directive in the schema.

kush-prof commented 1 month ago

@chrisbonifacio adding allow.authenticated("identityPool") is adding @aws_iam to the schema however it is not supported if we are writing custom functions. Getting the following error:

identityPool-based auth (allow.guest() and allow.authenticated('identityPool')) is not supported with a.handler.custom
Caused By: identityPool-based auth (allow.guest() and allow.authenticated('identityPool')) is not supported with a.handler.custom

Resolution: Check your data schema definition for syntax and type errors.

Is there any workaround for it or we will just simply have to use lambda functions here. Ideally I would want to use the custom js resolver here rather than a lambda function serving as a resolver for AppSync, I think it holds some benefits (maybe I'm wrong!).

So the idea is to create a custom mutation and get it triggered by a scheduler to do some operation on DynamoDB tables, since we cannot get the table names in the scheduler lambda directly (would've preferred this over anything else) I need to trigger a custom mutation, now since custom mutation with IAM permission do not allow custom js resolver in the lambda function attached to the mutation we will have to do a graphql mutation itself.

Let me know if you need more clarification.

chrisbonifacio commented 3 weeks ago

Hi @kush-prof, the only difference I can tell between my reproduction app and the details in this issue is our @aws-amplify/backend and @aws-amplify/backend-cli versions. Could you make sure you are using the latest versions of those packages?

You might also have to update transitive dependencies like @aws-amplify/data-schema. You can do so by running:

npm update @aws-amplify/data-schema
chrisbonifacio commented 1 week ago

Hi 👋 Closing this as we have not heard back from you. If you are still experiencing this issue and in need of assistance, please feel free to comment and provide us with any information previously requested by our team members so we can re-open this issue and be better able to assist you.

Thank you!

github-actions[bot] commented 1 week ago

This issue is now closed. Comments on closed issues are hard for our team to see. If you need more assistance, please open a new issue that references this one.