Amplify-cli: @auth on two 1-many connections

Created on 24 Jul 2019  路  6Comments  路  Source: aws-amplify/amplify-cli

* Which Category is your question related to? *

AppSync

* What AWS Services are you utilizing? *

Cognito, AppSync
@aws-amplify/cli v1.8.4
aws-amplify v1.1.32

* Provide additional details e.g. code snippets *

I have 2 tables with differing levels of auth on them, and I have a table which joins connections between the two.

The Brand model has dynamic group auth, which is assigned to a user in Cognito (e.g. "brand-1, brand-2, brand-3") and then each row has one or more of those groups in the groups field. Users are also assigned to a generic "brand" group in Cognito, which means they can read everything from the Event model.

type Brand @model @auth (rules: [
    { allow: groups, groups: ["admin"], operations: [create, read, update, delete] },
    { allow: groups, groupsField: "groups", operations: [read, update] }
]) {
    id: ID!
    name: String!
    groups: [String!]
    eventBrands: [EventBrand!] @connection(name: "EventBrandBrand")
}

type Event @model @auth (rules: [
    { allow: groups, groups: ["admin"], operations: [create, read, update, delete] },
    { allow: groups, groups: ["brand"], operations: [read] }
]) {
    id: ID!
    name: String
    brands: [EventBrand!] @connection(name: "EventBrandEvent")
}

type EventBrand @model {
    id: ID!
    event: Event! @connection(name: "EventBrandEvent")
    brand: Brand! @connection(name: "EventBrandBrand")
}

For arguments sake I have entered two brands, and two events. I have then added an EventBrand for brand-1/event-1, and I have added a second EventBrand for brand-2/event-2.

When I query the EventBrand model, I get results back, which is great - but instead of ignoring items with connections I'm not authorised to retrieve, it simply returns a null item and generates an error for each of those connections.

This doesn't really suit our use case, as our users could in theory be paging through empty page after empty page until they get theirs.

Response:

{  
   "data":{  
      "listEventBrands":{  
         "items":[  
            null,     // <---- NULL ITEM HERE
            {  
               "id":"2eb450cd-3b27-4657-89a4-f4cf709757b4",
               "event":{  
                  "id":"15cd1ab9-051c-4a2b-9709-ff81dcb63cb8",
                  "name":"Event 1",
               },
               "brand":{  
                  "id":"1c66565b-af2f-4dc4-97a8-043be12b0de0",
                  "name":"Brand 1",
                  "groups":[  
                     "brand-1"
                  ]
               }
            }
         ],
         "nextToken":null
      }
   },
   "errors":[  
      {  
         "path":[  
            "listEventBrands",
            "items",
            0,
            "brand"
         ],
         "data":null,
         "errorType":"Unauthorized",
         "errorInfo":null,
         "locations":[  
            {  
               "line":19,
               "column":7,
               "sourceName":null
            }
         ],
         "message":"Not Authorized to access brand on type EventBrand"
      }
   ]
}

I then tried adding @auth directive to the EventBrand model, and targeting the brand.groups field. The response mapping template looks correct, but I get nothing returned.

Model:

type EventBrand @model @auth (rules: [
    { allow: groups, groups: ["admin"], operations: [create, read, update, delete] },
    { allow: groups, groupsField: "brand.groups", operations: [read] }
]) {
    id: ID!
    event: Event! @connection(name: "EventBrandEvent")
    brand: Brand! @connection(name: "EventBrandBrand")
}

Response mapping template:

## [Start] If not static group authorized, filter items **
#if( ! $isStaticGroupAuthorized )
  #set( $items = [] )
  #foreach( $item in $ctx.result.items )
    ## [Start] Dynamic Group Authorization Checks **
    #set( $userGroups = $util.defaultIfNull($ctx.identity.claims.get("cognito:groups"), []) )
    #set( $isLocalDynamicGroupAuthorized = false )
    ## Authorization rule: { allow: groups, groupsField: "brand.groups" } **
    #set( $allowedGroups = $item.brand.groups )
    #foreach( $userGroup in $userGroups )
      #if( $util.isList($allowedGroups) )
        #if( $allowedGroups.contains($userGroup) )
          #set( $isLocalDynamicGroupAuthorized = true )
        #end
      #end
      #if( $util.isString($allowedGroups) )
        #if( $allowedGroups == $userGroup )
          #set( $isLocalDynamicGroupAuthorized = true )
        #end
      #end
    #end
    ## [End] Dynamic Group Authorization Checks **


    ## No Owner Authorization Rules **


    #if( ($isLocalDynamicGroupAuthorized == true || $isLocalOwnerAuthorized == true) )
      $util.qr($items.add($item))
    #end
  #end
  #set( $ctx.result.items = $items )
#end
## [End] If not static group authorized, filter items **

Result:

{"data":{"listEventBrands":{"items":[],"nextToken":null}}}

I have tried debug the Request Mapping template, but have had no success/can't figure out how to do it.

I also tried querying the Event or Brand models and passing a filter where brand/event connection is null, but the brand is not part of the ModelEventFilterInput type.

export type ModelEventFilterInput = {
  id?: ModelIDFilterInput | null,
  name?: ModelStringFilterInput | null,
  and?: Array< ModelEventFilterInput | null > | null,
  or?: Array< ModelEventFilterInput | null > | null,
  not?: ModelEventFilterInput | null,
};
export type ModelBrandFilterInput = {
  id?: ModelIDFilterInput | null,
  name?: ModelStringFilterInput | null,
  groups?: ModelStringFilterInput | null,
  and?: Array< ModelBrandFilterInput | null > | null,
  or?: Array< ModelBrandFilterInput | null > | null,
  not?: ModelBrandFilterInput | null,
};

So my questions are:

1) Am I implementing the auth correctly?
2) If I am, is there a way to ignore the null items from the response?
3) Do I have an option 3?

bug dependency-issue graphql-transformer

Most helpful comment

@nathan-quinn, about Example 2 -
I understand the problem, but what should be the solution:
I'd like to define the groups value on the main model (let say "Account"), and "child" models (e.g. "projects") should inherit somehow the same group values of the parent.

Do I have to add the same groups value on all models?

All 6 comments

@struct78
I reproduced what you reported here.
Not to say you should update your schema like this, just to test things out, if you update the schema and remove ! for the Brand in the type EventBrand like this

type EventBrand @model {
    id: ID!
    event: Event! @connection(name: "EventBrandEvent")
    brand: Brand @connection(name: "EventBrandBrand")
}

You will see the event part is returned and the brand part is null with the Unauthorized error.

{
  "data": {
    "listEventBrands": {
      "items": [
        {
          "event": {
            "id": "27569136-5911-48fc-8b5d-230aa4b2652b",
            "name": "event1"
          },
          "brand": {
            "id": "c9faa3d9-d75b-44f7-aa81-2fd6bb24c261",
            "name": "brand1"
          }
        },
        {
          "event": {
            "id": "8264731b-8413-4d5e-89d8-ed38ab6c2dc2",
            "name": "event2"
          },
          "brand": null
        }
      ]
    }
  },
  "errors": [
    {
      "path": [
        "listEventBrands",
        "items",
        1,
        "brand"
      ],
      "data": null,
      "errorType": "Unauthorized",
      "errorInfo": null,
      "locations": [
        {
          "line": 58,
          "column": 7,
          "sourceName": null
        }
      ],
      "message": "Not Authorized to access brand on type EventBrand"
    }
  ]
}

I will mark this as a bug.
For now you will have to insert your own logic to avoid showing this to your customers.

@UnleashedMind Thank you for getting back to me.

We have gone back to just using the listBrands query, which works with the auth just fine, and we get back the following structure:

[{
  id: "...",
  name: "Brand A", 
  eventBrands: {
      items: [{
          brand: {
              id: "8a757fce-4e5d-4822-b025-3c638aa35ac6",
              name: "Brand A",
          },
          event: {
              id: "bc6b9cc4-0d35-4c28-8fe7-3fa7e162458b",
              name: "Event A",
          }
      }]
  },
  groups: ['7b01b2e6-48b5-4c2d-8c8c-759daf609973']
}]

It doesn't seem like I can write a filter to filter by a particular event ID, because the ModelBrandFilterInput type doesn't allow me to:

export type ModelBrandFilterInput = {
  id?: ModelIDFilterInput | null,
  name?: ModelStringFilterInput | null,
  groups?: ModelStringFilterInput | null,
  and?: Array< ModelBrandFilterInput | null > | null,
  or?: Array< ModelBrandFilterInput | null > | null,
  not?: ModelBrandFilterInput | null,
};

I tried updating the model so that a brand had a string array of event IDs, so my model looks like this:

type Brand @model @auth (rules: [
    { allow: groups, groups: ["admin"], operations: [create, read, update, delete] },
    { allow: groups, groupsField: "groups", operations: [read, update] }
]) {
    id: ID!
    name: String!
    events: [String!]
    groups: [String!]
}

I then tried to pass it a few filters:

    filter: {
        events: {
            contains: 'bc6b9cc4-0d35-4c28-8fe7-3fa7e162458b',
        },
    }
    filter: {
        events: {
            eq: 'bc6b9cc4-0d35-4c28-8fe7-3fa7e162458b',
        },
    }

None of which return any items. Looking at the updated ModelBrandFilterInput type, events has a ModelStringFilterInput type:

export type ModelBrandFilterInput = {
  id?: ModelIDFilterInput | null,
  name?: ModelStringFilterInput | null,
  events?: ModelStringFilterInput | null,
  groups?: ModelStringFilterInput | null,
  and?: Array< ModelBrandFilterInput | null > | null,
  or?: Array< ModelBrandFilterInput | null > | null,
  not?: ModelBrandFilterInput | null,
};

Which doesn't seem to be support arrays, just strings:

export type ModelStringFilterInput = {
  ne?: string | null,
  eq?: string | null,
  le?: string | null,
  lt?: string | null,
  ge?: string | null,
  gt?: string | null,
  contains?: string | null,
  notContains?: string | null,
  between?: Array< string | null > | null,
  beginsWith?: string | null,
};

I attempted to write a custom resolver, but I quickly hit the limits of my talent and my understanding of the documentation. I don't know GraphQL that well, but I wanted to understand the problem a bit better 鈥斅爄s this a GraphQL problem, or an Amplify problem? Is this a feature that could be introduced in the future? Am I just doing something wrong? Is there a workaround?

For now my solution is to set the limit parameter on the query to a very big number and do the record paging and filtering on the client, which kind of defeats the purpose of using Appsync in the first place.

@struct78 @UnleashedMind
Just to add some clarity on the original two issues here:

Example 1

type EventBrand @model {
    id: ID!
    event: Event! @connection(name: "EventBrandEvent")
    brand: Brand! @connection(name: "EventBrandBrand")
}

query test {
  listEventBrands { <-- Scan / Query on The EventBrand join table.  No Auth is defined on the model, so this will return every item.  Each item will have eventBrandId, eventId, and brandId
    items {
       id
       event { # <-- GetItem on Event.  Auth is applied.  If they don't have access then it will error and return null for the item.  This cascades upwards because the EventBrand model has Event marked as non-nullable.
         id
         name
       }
       brand {  # <-- GetItem on Brand.  Auth is applied.  If they don't have access then it will error and return null for the item.  This cascades upwards because the EventBrand model has Brand marked as non-nullable.
           id
           name
       }
    }
  }
}

Example 2

Here you are trying to reference a nested field as the groupName in the @auth directive. This won't work because brand is not resolved when the response mapping VTL that is generated by amplify is executing.

#set( $allowedGroups = $item.brand.groups )

$allowedGroups will always be nothing here.

@nathan-quinn, about Example 2 -
I understand the problem, but what should be the solution:
I'd like to define the groups value on the main model (let say "Account"), and "child" models (e.g. "projects") should inherit somehow the same group values of the parent.

Do I have to add the same groups value on all models?

I had this issue. It was because I had generated the schema with @model(queries: null) or something along those lines, which was not adding @aws-api-key to my models when amplify creates the actual build schema. I use api keys to make requests. Remove the (queries: null) part of the models to generate queries and then amplify push to update your backend.

@rahul-nath that was a huge help, thank you. I'm not sure why (queries: null) drops the authorization attributes but I saw the same with @aws_iam and @aws_cognito_user_pools

Another workaround I found on SO (https://stackoverflow.com/questions/59588247/why-cant-i-read-relational-data-when-i-use-iam-for-auth-but-can-read-when-auth) was to explicitly define the connection and its associated attributes. I think dropping the (queries: null) is cleaner but if someone can't use that then add an explicit type like:

type ModelComputedConnection @aws_cognito_user_pools @aws_iam {
  items: [Computed]
  nextToken: String
}

which you can take straight out of the compiled build/schema.graphql and just append your @aws_* directives as needed.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

adriatikgashi picture adriatikgashi  路  3Comments

davo301 picture davo301  路  3Comments

onlybakam picture onlybakam  路  3Comments

mwarger picture mwarger  路  3Comments

zjullion picture zjullion  路  3Comments