aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.36k stars 2.1k forks source link

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

Open gpavlov2016 opened 2 weeks ago

gpavlov2016 commented 2 weeks ago

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

chrisbonifacio commented 1 week ago

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 commented 1 week ago

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?

gpavlov2016 commented 1 week ago

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:

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

chrisbonifacio commented 1 week ago

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