amplify-js icon indicating copy to clipboard operation
amplify-js copied to clipboard

Unable to use "iam" authorization for custom mutations in v6.2

Open gpavlov2016 opened this issue 1 year ago • 5 comments

Before opening, please confirm:

JavaScript Framework

Next.js

Amplify APIs

GraphQL API

Amplify Version

v6

Amplify Categories

No response

Backend

Amplify Gen 2 (Preview)

Environment information

# Put output below this line
System:
    OS: Windows 11 10.0.22631
    CPU: (20) x64 13th Gen Intel(R) Core(TM) i9-13900H
    Memory: 2.44 GB / 31.68 GB
  Binaries:
    Node: 18.19.1 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.19 - ~\AppData\Roaming\npm\yarn.CMD
    npm: 10.2.4 - C:\Program Files\nodejs\npm.CMD
    pnpm: 8.15.4 - ~\AppData\Local\pnpm\pnpm.EXE
  Browsers:
    Edge: Chromium (123.0.2420.97)
    Internet Explorer: 11.0.22621.3527
  npmPackages:
    %name%:  0.1.0
    @ampproject/toolbox-optimizer:  undefined ()
    @aws-amplify/backend: ^1.0.0 => 1.0.0
    @aws-amplify/backend-cli: ^1.0.1 => 1.0.1
    @aws-amplify/ui-react: ^6.1.9 => 6.1.9
    @aws-amplify/ui-react-internal:  undefined ()
    @aws-sdk/s3-presigned-post: ^3.568.0 => 3.568.0
    @babel/core:  undefined ()
    @babel/runtime:  7.22.5
    @edge-runtime/cookies:  4.1.1
    @edge-runtime/ponyfill:  2.4.2
    @edge-runtime/primitives:  4.1.0
    @hapi/accept:  undefined ()
    @heroicons/react: ^2.1.3 => 2.1.3
    @mswjs/interceptors:  undefined ()
    @napi-rs/triples:  undefined ()
    @next/font:  undefined ()
    @opentelemetry/api:  undefined ()
    @types/node: ^20.12.8 => 20.12.8
    @types/react: ^18.3.1 => 18.3.1
    @types/react-dom: ^18.3.0 => 18.3.0
    @vercel/nft:  undefined ()
    @vercel/og:  0.6.2
    acorn:  undefined ()
    amphtml-validator:  undefined ()
    anser:  undefined ()
    arg:  undefined ()
    assert:  undefined ()
    async-retry:  undefined ()
    async-sema:  undefined ()
    autoprefixer: ^10.4.19 => 10.4.19
    aws-amplify: ^6.2.0 => 6.2.0
    aws-amplify/adapter-core:  undefined ()
    aws-amplify/analytics:  undefined ()
    aws-amplify/analytics/kinesis:  undefined ()
    aws-amplify/analytics/kinesis-firehose:  undefined ()
    aws-amplify/analytics/personalize:  undefined ()
    aws-amplify/analytics/pinpoint:  undefined ()
    aws-amplify/api:  undefined ()
    aws-amplify/api/server:  undefined ()
    aws-amplify/auth:  undefined ()
    aws-amplify/auth/cognito:  undefined ()
    aws-amplify/auth/cognito/server:  undefined ()
    aws-amplify/auth/enable-oauth-listener:  undefined ()
    aws-amplify/auth/server:  undefined ()
    aws-amplify/data:  undefined ()
    aws-amplify/data/server:  undefined ()
    aws-amplify/datastore:  undefined ()
    aws-amplify/in-app-messaging:  undefined ()
    aws-amplify/in-app-messaging/pinpoint:  undefined ()
    aws-amplify/push-notifications:  undefined ()
    aws-amplify/push-notifications/pinpoint:  undefined ()
    aws-amplify/storage:  undefined ()
    aws-amplify/storage/s3:  undefined ()
    aws-amplify/storage/s3/server:  undefined ()
    aws-amplify/storage/server:  undefined ()
    aws-amplify/utils:  undefined ()
    aws-cdk: ^2.140.0 => 2.140.0
    aws-cdk-lib: ^2.140.0 => 2.140.0
    aws-sdk: ^2.1613.0 => 2.1613.0
    babel-packages:  undefined ()
    browserify-zlib:  undefined ()
    browserslist:  undefined ()
    buffer:  undefined ()
    bytes:  undefined ()
    ci-info:  undefined ()
    cli-select:  undefined ()
    client-only:  0.0.1
    commander:  undefined ()
    comment-json:  undefined ()
    compression:  undefined ()
    conf:  undefined ()
    constants-browserify:  undefined ()
    constructs: ^10.3.0 => 10.3.0
    content-disposition:  undefined ()
    content-type:  undefined ()
    cookie:  undefined ()
    cross-spawn:  undefined ()
    crypto-browserify:  undefined ()
    css.escape:  undefined ()
    data-uri-to-buffer:  undefined ()
    debug:  undefined ()
    devalue:  undefined ()
    domain-browser:  undefined ()
    edge-runtime:  undefined ()
    esbuild: ^0.20.2 => 0.20.2
    eslint: ^8.57.0 => 8.57.0
    eslint-config-next: 14.2.3 => 14.2.3
    events:  undefined ()
    find-cache-dir:  undefined ()
    find-up:  undefined ()
    fresh:  undefined ()
    get-orientation:  undefined ()
    glob:  undefined ()
    gzip-size:  undefined ()
    http-proxy:  undefined ()
    http-proxy-agent:  undefined ()
    https-browserify:  undefined ()
    https-proxy-agent:  undefined ()
    icss-utils:  undefined ()
    ignore-loader:  undefined ()
    image-size:  undefined ()
    is-animated:  undefined ()
    is-docker:  undefined ()
    is-wsl:  undefined ()
    jest-worker:  undefined ()
    json5:  undefined ()
    jsonwebtoken:  undefined ()
    loader-runner:  undefined ()
    loader-utils:  undefined ()
    lodash.curry:  undefined ()
    lru-cache:  undefined ()
    mini-css-extract-plugin:  undefined ()
    nanoid:  undefined ()
    native-url:  undefined ()
    neo-async:  undefined ()
    next: 14.2.3 => 14.2.3
    node-fetch:  undefined ()
    node-html-parser:  undefined ()
    ora:  undefined ()
    os-browserify:  undefined ()
    p-limit:  undefined ()
    path-browserify:  undefined ()
    picomatch:  undefined ()
    platform:  undefined ()
    postcss: ^8.4.38 => 8.4.38 (8.4.31)
    postcss-flexbugs-fixes:  undefined ()
    postcss-modules-extract-imports:  undefined ()
    postcss-modules-local-by-default:  undefined ()
    postcss-modules-scope:  undefined ()
    postcss-modules-values:  undefined ()
    postcss-preset-env:  undefined ()
    postcss-safe-parser:  undefined ()
    postcss-scss:  undefined ()
    postcss-value-parser:  undefined ()
    process:  undefined ()
    punycode:  undefined ()
    querystring-es3:  undefined ()
    raw-body:  undefined ()
    react: ^18.3.1 => 18.3.1
    react-builtin:  undefined ()
    react-dom: ^18.3.1 => 18.3.1
    react-dom-builtin:  undefined ()
    react-dom-experimental-builtin:  undefined ()
    react-experimental-builtin:  undefined ()
    react-is:  18.2.0
    react-refresh:  0.12.0
    react-server-dom-turbopack-builtin:  undefined ()
    react-server-dom-turbopack-experimental-builtin:  undefined ()
    react-server-dom-webpack-builtin:  undefined ()
    react-server-dom-webpack-experimental-builtin:  undefined ()
    regenerator-runtime:  0.13.4
    sass-loader:  undefined ()
    scheduler-builtin:  undefined ()
    scheduler-experimental-builtin:  undefined ()
    schema-utils:  undefined ()
    semver:  undefined ()
    send:  undefined ()
    server-only:  0.0.1
    setimmediate:  undefined ()
    shell-quote:  undefined ()
    source-map:  undefined ()
    source-map08:  undefined ()
    stacktrace-parser:  undefined ()
    stream-browserify:  undefined ()
    stream-http:  undefined ()
    string-hash:  undefined ()
    string_decoder:  undefined ()
    strip-ansi:  undefined ()
    superstruct:  undefined ()
    tailwindcss: ^3.4.3 => 3.4.3
    tar:  undefined ()
    terser:  undefined ()
    text-table:  undefined ()
    timers-browserify:  undefined ()
    tsx: ^4.9.0 => 4.9.0
    tty-browserify:  undefined ()
    typescript: ^5.4.5 => 5.4.5 (4.4.4, 4.9.5)
    ua-parser-js:  undefined ()
    unistore:  undefined ()
    util:  undefined ()
    vm-browserify:  undefined ()
    watchpack:  undefined ()
    web-vitals:  undefined ()
    webpack:  undefined ()
    webpack-sources:  undefined ()
    ws:  undefined ()
    zod:  undefined ()
  npmGlobalPackages:
    @aws-amplify/cli: 12.10.3
    corepack: 0.22.0
    create-next-app: 14.2.3
    npm: 10.2.4

Describe the bug

Consider the use case in which a lambda function needs to use a custom mutation. Before v6.2 we could use allow.authenticated('iam'), however since the upgrade to v6.2 it doesn't work anymore. Neither does the .authorization((allow) => [ allow.resource(...) ]) applied to the whole schema, this only works for non-custom queries and mutations.

Expected behavior

An authorization mechanism to authorize a lambda function to execute a custom mutation should exist

Reproduction steps

Define the schema:

const schema = a.schema({
  Impression: a.model({
    videoId: a.string().required(),
    impressions: a.integer().default(0)
  }).identifier(['videoId'])
    .authorization((allow) => [
      allow.publicApiKey(),
      allow.authenticated()
    ]),

  //Executes atomic increment operation on the impressions field of the Impression model
  increaseImpression: a
    .mutation()
    .arguments({
      videoId: a.string(),
      count: a.integer()
    })
    .returns(a.ref('Impression'))
    .authorization((allow) => [
      allow.authenticated()
    ])
    .handler(a.handler.custom({
      dataSource: a.ref('Impression'),
      entry: './increment-impression.js'
    })),
})
  .authorization((allow) => [
    allow.resource(incrementImpression),
  ]);

export type Schema = ClientSchema<typeof schema>;

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

Define a function:

export const incrementImpression = defineFunction({
});

import { Amplify } from 'aws-amplify';
import { generateClient } from 'aws-amplify/data';
import { env } from '@env/increment-impression';
import { modelIntrospection } from '../../../amplifyconfiguration.json';
import { Schema } from '../../data/resource';

Amplify.configure(
  {
    API: {
      GraphQL: {
        endpoint: env.AMPLIFY_DATA_GRAPHQL_ENDPOINT, // replace with your defineData name
        region: env.AWS_REGION,
        defaultAuthMode: 'iam',
        modelIntrospection: modelIntrospection as never
      }
    }
  },
  {
    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>();

export const handler = async (event: any) => {
const { data, errors } = await dataClient.mutations.increaseImpression({ videoId:'', count:10 }, { authMode: 'iam' });
    if (errors) {
      console.log('errors: ', errors);
    }
    console.log('data: ', data);
}

This results in the following error:

errors: [ { path: [ 'increaseImpression' ], data: null, errorType: 'Unauthorized', errorInfo: null, locations: [ [Object] ], message: 'Not Authorized to access increaseImpression on type Mutation' } ]

Same problem can be seen by running the mutation from the AppSync console: image Unless the schema is manually edited and the @aws_iam is added to the increaseImpression declaration - then it works!

Code Snippet

// Put your code below this line.

Log output

// Put your logs below this line


aws-exports.js

No response

Manual configuration

No response

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

gpavlov2016 avatar May 04 '24 02:05 gpavlov2016

Hi @gpavlov2016 can you try changing your mutations handler from custom to function?

ex:

const schema = a.schema({
  Impression: a
    .model({
      videoId: a.string().required(),
      impressions: a.integer().default(0),
    })
    .identifier(["videoId"])
    .authorization((allow) => [allow.publicApiKey(), allow.authenticated()]),

  //Executes atomic increment operation on the impressions field of the Impression model
  increaseImpression: a
    .mutation()
    .arguments({
      videoId: a.string(),
      count: a.integer(),
    })
    .returns(a.ref("Impression"))
    .authorization((allow) => [allow.authenticated()])
    .handler(a.handler.function(incrementImpression)),
});

Mutation performed from the AppSync console with IAM:

image

chrisbonifacio avatar May 06 '24 15:05 chrisbonifacio

Although, I am a little confused about the shared code. Your mutation is using the Lambda as the handler but the handler's logic is also invoking the mutation.

So, the mutation is invoking itself? Is that intentional?

chrisbonifacio avatar May 06 '24 16:05 chrisbonifacio

Apologies for the confusion, there are two different functions, I probably should have picked better names for them. Let me try to explain the situation:

  • I need to call the increment impression custom mutation from Android
  • Android Amplify framework currently doesn't support custom mutations issue open here
  • As a solution I created a lambda function (increaseImpression) that can be called through a REST API endpoint that in turn will call the custom increment impression mutation.
  • I used IAM authentication to authorize the Lambda to execute the mutation.
  • With the 6.2 update the IAM authentication has been removed so I am back to square one - call increment impression custom mutation from Android doesn't work.

So it's not really the mutation handler that I need to authorize but an external function to invoke that handler through GraphQL.

One of the ideas that I am exploring based on your suggestion is to use a function handler instead of custom resolver for the custom mutation implementation but unfortunately the documentation omits the example for this use case and instead shows how to implement a query handler that doesn't include accessing the DB. Link to documentation image

image

gpavlov2016 avatar May 06 '24 21:05 gpavlov2016

Oh okay, I see. In that case, the schema level allow.resources should suffice 🤔

I'll try to reproduce again with an external lambda that is separate from the handler

chrisbonifacio avatar May 06 '24 22:05 chrisbonifacio

@chrisbonifacio hi, in the docs i can see a reference to allow.resource however this isn't included in the latest amplify modules - please advise?

System: OS: macOS 14.5 CPU: (12) arm64 Apple M2 Pro Memory: 153.28 MB / 32.00 GB Shell: /bin/zsh Binaries: Node: 20.9.0 - ~/.nvm/versions/node/v20.9.0/bin/node Yarn: undefined - undefined npm: 10.1.0 - ~/.nvm/versions/node/v20.9.0/bin/npm pnpm: undefined - undefined NPM Packages: @aws-amplify/backend: 1.0.2 @aws-amplify/backend-cli: 1.0.3 aws-amplify: 6.3.3 aws-cdk: 2.140.0 aws-cdk-lib: 2.140.0 typescript: 5.4.5 AWS environment variables: AWS_STS_REGIONAL_ENDPOINTS = regional AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1 AWS_SDK_LOAD_CONFIG = 1 No CDK environment variables

domthomas1 avatar May 23 '24 14:05 domthomas1

Hi @domthomas1 can you share your schema where you're trying to call allow.resource()?

chrisbonifacio avatar Jun 13 '24 15:06 chrisbonifacio

Hi @chrisbonifacio, an example schema below. The differences to the docs that I can see are that I am referencing a separate function rather than defining it from within the data resources file which contains the schema, plus I am linking the authorization to the model directly rather than the schema (I have a number of models with different auth why at that level).

import { type ClientSchema, a, defineData } from '@aws-amplify/backend'; 

import { exampleHandler } from '../functions/example-handler/resource';

const schema = a.schema({

    ExampleA: a
    .model({
      name: a.string(),
      description: a.string()
    })
    .authorization(allow => [allow.owner().to(['create', 'update', 'list'])]), 

    ExampleB: a
    .model({
      name: a.string(),
      description: a.string()
    })
    .authorization(allow => [allow.resource(exampleHandler)])

});

export type Schema = ClientSchema<typeof schema>;

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

domthomas1 avatar Jun 13 '24 15:06 domthomas1

Hi @gpavlov2016 Apologies for the delay. Are you still experiencing this issue?

Looking at the schema you shared, it seems you are using allow.resource on a model. This is not supported. The way to grant a Lambda access to the API (queries/mutations/subscriptions) is to set an authorization rule on the schema itself like so:

const functionWithDataAccess = defineFunction({
  entry: '../functions/data-access.ts'
});

const schema = a
  .schema({
    Todo: a.model({
      name: a.string(),
      description: a.string()
    })
  })
  .authorization(allow => [allow.resource(functionWithDataAccess)]);

Alternatively, you can also grant access to graphql query/mutation/subscriptions separately like so:

const myLambda = defineFunction({
  entry: "./handler.ts",
});

const backend = defineBackend({
  auth,
  data,
  myLambda,
});

backend.data.resources.graphqlApi.grantMutation(
  backend.myLambda.resources.lambda
);

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

chrisbonifacio avatar Jun 24 '24 18:06 chrisbonifacio

Hi @chrisbonifacio, thanks for your response, the first approach works with my data schema (no typescript errors)

domthomas1 avatar Jun 24 '24 22:06 domthomas1

Hi @gpavlov2016 I found an away on AWS amplify discord group.

const ddbProductDataSourceRoleArn = backend.data.resources.cfnResources.cfnDataSources["ProductTable"].serviceRoleArn

const ddbProductTable = backend.data.resources.tables["Product"]

if (ddbProductTable) {
  const role = Role.fromRoleArn(Stack.of(backend.data), 'DynamoDBServiceRoleArn', ddbProductDataSourceRoleArn)
  role.addToPrincipalPolicy(new PolicyStatement({
    actions: [
      "dynamodb:PutItem",
      "dynamodb:BatchGetItem"
    ],
    resources: [
      ddbProductTable.tableArn
    ],
  }))
}  

But it seems work around.

I think a better solution is use lambda handler functions for now.

raiuri avatar Jul 15 '24 22:07 raiuri

I am having trouble adding a custom JS resolver to my schema. When I try to use it, it throws errors. Here is my schema:

const schema = a.schema({
  RoomMember: a
    .model({
      userId: a.string().required(),
      roomId: a.string().required(),
      roomName: a.string(),
      userFullName: a.string().required(),
      user: a.belongsTo('User', 'userId'),
      room: a.belongsTo('Room', 'roomId'),
    })
    .secondaryIndexes((index) => [
      index('userId').sortKeys(['roomId']),
      index('roomId').sortKeys(['userFullName']),
      index('roomId').sortKeys(['roomName']),
    ])
    .authorization((allow) => [allow.publicApiKey()]),

  User: a
    .model({
      fullName: a.string().required(),
      avatarUrl: a.string(),
      users: a.hasMany('RoomMember', 'userId'),
    })
    .authorization((allow) => [allow.publicApiKey()]),

  Room: a
    .model({
      roomName: a.string(),
      ownerId: a.string().required(),
      members: a.hasMany('RoomMember', 'roomId'),
      messages: a.hasMany('Message', 'roomId'),
    })
    .authorization((allow) => [allow.publicApiKey()]),

  Message: a
    .model({
      text: a.string(),
      attachmentUrl: a.string(),
      userId: a.string().required(),
      roomId: a.string().required(),
      room: a.belongsTo('Room', 'roomId'),
    })
    .secondaryIndexes((index) => [index('roomId').sortKeys(['text'])])
    .authorization((allow) => [allow.publicApiKey()]),

  batchAddUsers: a
    .mutation()
    .arguments({
      users: a.string().array().required(),
    })
    .returns(a.string())
    .authorization((allow) => [allow.publicApiKey()])
    .handler(
      a.handler.custom({
        dataSource: a.ref('User'),
        entry: './resolvers/batchCreateUsers.resolver.js',
      }),
    ),

  batchAddRoomMembers: a
    .mutation()
    .arguments({
      roomId: a.string().required(),
      users: a.string().array().required(),
    })
    .returns(a.string())
    .authorization((allow) => [allow.publicApiKey()])
    .handler(
      a.handler.custom({
        dataSource: a.ref('RoomMember'),
        entry: './resolvers/batchCreateRoomMember.resolver.js',
      }),
    ),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'apiKey',
    apiKeyAuthorizationMode: { expiresInDays: 7 },
  },
});

Here is the error: :"{ "data": { "batchAddUsers": null }, "errors": [ { "path": [ "batchAddUsers" ], "data": null, "errorType": "DynamoDB:DynamoDbException", "errorInfo": null, "locations": [ { "line": 6, "column": 3, "sourceName": null } ], "message": "User: arn:aws:sts::339712945914:assumed-role/UserIAMRole0e5381-ztkmmcwfabel5d3zhzduxwnbgq-NONE/APPSYNC_ASSUME_ROLE is not authorized to perform: dynamodb:BatchWriteItem on resource: arn:aws:dynamodb:us-east-1:339712945914:table/User because no identity-based policy allows the dynamodb:BatchWriteItem action (Service: DynamoDb, Status Code: 400, Request ID: 5H0C1D2HF8P6E79U0FIO9708KJVV4KQNSO5AEMVJF66Q9ASUAAJG)" } ] }

luunminh avatar Jul 22 '24 01:07 luunminh