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 77 forks source link

Guest `@auth` with dynamic groups #518

Open loganpowell opened 2 years ago

loganpowell commented 2 years ago

Which Category is your question related to? Auth, API(GraphQL)

Amplify CLI Version 7.6.22

What AWS Services are you utilizing?

Provide additional details e.g. code snippets. Be sure to remove any sensitive data.

👋

Hello! I have been looking high and low for an answer to this question, but couldn't find anything in either open or closed issues here, SO, or the official Amplify docs:

I am trying to use dynamic group authorization for one of my @models and can't figure out how to populate the public... provider: iam with a default group assignment to end up something like this:

   "accessToken":{
      "jwtToken":"eyJraWQiOiI4N2pRRnpqSm..",
      "payload":{
          "sub":"ceba8336-b0e0-4c3d-8abc-af08c002b4de",
          "device_key":"us-east-2_94234234234b-4cec-ae49-b1f90555d979",
          "cognito:groups":[
              "public" //<- 👀
          ]
       }
   }

when augmenting Amplify.configure() Object with:

{ 
    graphql_headers: async () => {
        try {
            const token = (await Auth.currentSession()).getIdToken().getJwtToken()
            return { Authorization: token }
        } catch (e) {
            console.error(e)
            return {}
        }
    }
}

Is there a way for me to add all guests (non-authenticated) to a default group in the IAM policy that's leveraged by the public auth in Amplify?

I want to do something like this in my schema.graphql

type Node 
    @model 
    @auth (rules: [ { allow: groups, operations: [ read ], groupsField: "readerGroups" } ])
{
    id: ID! @primaryKey
    readerGroups: [ String ]
}

with "public" being able to be toggled on/off by adding/removing them from the readerGroups.

Help would be greatly appreciated!

🙏

josefaidt commented 2 years ago

Hey @loganpowell :wave: thanks for raising this! I've transferred this to our API repo for better assistance 🙂

loganpowell commented 2 years ago

Thank you, @josefaidt!

josefaidt commented 2 years ago

Hey @loganpowell what is the use case you're looking to accomplish? We can leverage the AWS_IAM auth mode to authenticate with public IAM for guest users:

import { API } from 'aws-amplify';
import * as queries from './graphql/queries';

const todos = await API.graphql({
  query: queries.listTodos,
  authMode: 'AWS_IAM'
});

ref https://docs.amplify.aws/lib/graphqlapi/query-data/q/platform/js/#custom-authorization-mode

As a follow-up, can you clarify this line?

Is there a way for me to add all guests (non-authenticated) to a default group in the IAM policy that's leveraged by the public auth in Amplify? ... with "public" being able to be toggled on/off by adding/removing them from the readerGroups.

Are you aiming to toggle what guests can view this data?

loganpowell commented 2 years ago

Hi @josefaidt! Thank you for following up.

Are you aiming to toggle what guests can view this data?

Sorry for the confusion, but no. Let me explain..

I am aware that I can set the authMode: "AWS_IAM", but that doesn't change the @auth permissions set via the schema. I am trying to use dynamic groups to control access to data.

what is the use case you're looking to accomplish?

I am building a CMS, which will have a Post that can be in a variety of different permission statuses:

  1. draft: only the owner will have access
  2. review: a group will be assigned that will then have access
  3. public: viewable by the public (thinking also a group)

I want the owner to be able to set the permissions dynamically, when the post is ready for publication. I'm thinking by using groupsField in a post's @auth rule, the owner can add/remove groups (including the public group) on demand.

RE:

As a follow-up, can you clarify this line?

Is there a way for me to add all guests (non-authenticated) to a default group in the IAM policy that's leveraged by the public auth in Amplify? ... with "public" being able to be toggled on/off by adding/removing them from the readerGroups.

I can't set the permissions statically on the table because that won't accomplish these aims. I have to use some kind of dynamic authorization setting and I figure dynamic groups is the only way currently afforded by Amplify to do so. Please correct me if I'm mistaken.

loganpowell commented 2 years ago

As a follow up, I found a blog post that goes into some detail about how the author implemented "owner" privileges to guests.

Highlights from this blog:

In AppSync, the resolvers' identity context from an unauthenticated Cognito identity (i.e., IAM) has the following shape:

// unauthenticated
{
    "accountId" : "string",
    "cognitoIdentityPoolId" : "string",
    "cognitoIdentityId" : "string",
    "sourceIp" : ["string"],
    "username" : "string", // IAM user principal
    "userArn" : "string",
    "cognitoIdentityAuthType" : "string", // authenticated/unauthenticated based on the identity type
    "cognitoIdentityAuthProvider" : "string" // the auth provider that was used to obtain the credentials
}

Whereas a request from an authenticated Cognito identity looks like this:

// authenticated
{
    "sub" : "uuid",
    "issuer" : "string",
    "username" : "string",
    "claims" : { ... }, // <- this is where cognito:groups lives
    "sourceIp" : ["x.x.x.x"],
    "defaultAuthStrategy" : "string"
}

by adjusting the auth phase in the resolvers output via Amplify - in the authors case - like so:

#set( $isAuthorized = false ) 
#set( $identityId = "")

#if( $util.authType() == "IAM Authorization" )
+ #set( $identityId = $ctx.identity.cognitoIdentityId )
#elseif( $util.authType() == "User Pool Authorization" )
  #set( $identityId = $ctx.identity.username )
#end

#if( $identityId != $ctx.result.identityId )
  $util.unauthorized()
#end

The author toggles between cognitoIdentityId and username depending on the auth type.

The author uses a set of Node.js scripts to post-process VTL templates in order to automate this replacement across resolvers.

My Case

In my case, I have something like this in my resolver context:

#if( $util.authType() == "IAM Authorization" )
  #set( $adminRoles = ["COPECognitoPostConfirmTriggerAddToViewers-migration"] )
  #foreach( $adminRole in $adminRoles )
    #if( $ctx.identity.userArn.contains($adminRole) && $ctx.identity.userArn != $ctx.stash.authRole && $ctx.identity.userArn != $ctx.stash.unauthRole )
      #return($util.toJson({}))
    #end
  #end
$util.unauthorized()
#end
#if( $util.authType() == "User Pool Authorization" )
  #if( !$isAuthorized )
    #set( $staticGroupRoles = [{"claim":"cognito:groups","entity":"Admins","allowedFields":[]}] )
    #foreach( $groupRole in $staticGroupRoles )
      #set( $groupsInToken = $util.defaultIfNull($ctx.identity.claims.get($groupRole.claim), []) )
      #if( $groupsInToken.contains($groupRole.entity) )
        #if( !$groupRole.allowedFields.isEmpty() )
          $util.qr($allowedFields.addAll($groupRole.allowedFields))
        #else
          #set( $isAuthorized = true )
          #break
        #end
      #end
    #end

I'm not sure where I could put a similar logic to grant all IAM users a group claim (e.g., Public).

josefaidt commented 2 years ago

Hey @loganpowell :wave: thank you for clarifying and including those details with the use case! And apologies for the delay here -- I've created two additional feature requests as I worked through the reproduction for this issue! That was certainly helpful! If I understand correctly it may be best to leverage two separate Lambda functions as a custom query resolver to return only the "reviewable" and "published" posts:

enum PostStatus {
  draft
  review
  published
}

type Post @model @auth(rules: [
  { allow: owner, operations: [create, read, update, delete] },
  { allow: private, provider: iam }
]) {
  id: ID! @primaryKey
  status: PostStatus @auth(rules: [{ allow: owner, operations: [read] }, { allow: private, provider: iam }])
}

type Query {
  listPublishedPosts: [Post]! @auth(rules: [{ allow: public }]) @function(name: "listPublishedPosts-${env}")
  listReviewablePosts: [Post]! @auth(rules: [{ allow: groups, groups: ["reviewers"] }]) @function(name: "listReviewablePosts-${env}")
}

With this owners will be able to CRUD their own Posts, and instead of filtering client-side and risking exposing unpublished Posts (with public IAM or API Key auth) we can use the Lambda resolvers to call the filtered query using IAM auth:

query LIST_PUBLISHED_POSTS {
  listPublishedPosts(filter: {
    status: {
      eq: published
    }
  }) {
    id
    status
  }
}

From here, calling listPublishedPosts client-side will use API key auth to retrieve the public data. And calling listReviewablePosts client-side will similarly check to see if the user is a member of the reviewers group.

As a final note this example does not also cover the functionality of changing the post status, however for the sake of this example I have already included the private IAM auth rule to mitigate the owners' ability to change their own post from draft straight to published. Please note this is a sample schema and does not quite cover all use cases such as leveraging user groups to update the Post's status

The author uses a set of Node.js scripts to post-process VTL templates in order to automate this replacement across resolvers.

I would error on the side of caution with post-processing the VTL templates in favor of a Lambda resolver or custom VTL resolver. This way we mitigate the risk of these VTL templates changing over time as additional features or bug fixes are released and potentially breaking the post-processing script.

Finally, I think this would make a great feature request to have a conditional auth rule based on a value within the model such as:

@auth(rules: [{ allow: public, where: { status: { eq: published } } }])
loganpowell commented 2 years ago

Hi @josefaidt sorry for the delayed response 🙇 I've been AFK for a long time (on vacation) and didn't see this until now (catching up on a backlog). I have to look deeper into your response, but I wanted to confirm I really appreciate it! Let me parse through this please...

loganpowell commented 2 years ago

Ok, did some reading/thinking

As a final note this example does not also cover the functionality of changing the post status, however for the sake of this example I have already included the private IAM auth rule to mitigate the owners' ability to change their own post from draft straight to published. Please note this is a sample schema and does not quite cover all use cases such as leveraging user groups to update the Post's status

I think your proposal is getting to the core of the big idea - dynamic privileges. I've made a comment on your referenced feature request and think it's brilliant.

I would error on the side of caution with post-processing the VTL templates in favor of a Lambda resolver or custom VTL resolver. This way we mitigate the risk of these VTL templates changing over time as additional features or bug fixes are released and potentially breaking the post-processing script.

I agree. Not only this, but also VTL is not so user friendly and thus a bit out of range for the average target Amplify user IMHO. EDIT: I also have reservations about Lambda custom resolvers due to the potential bottleneck WRT concurrency if a lot of resolvers are going to be triggered on every GraphQL call.