Restrict lambda resolver to owner for GraphQL API using Amplify - graphql

In my schema.graphql file I have the following:
type Floorplan #model #auth(rules: [
{ allow: private, operations: [read], provider: userPools },
{ allow: owner, provider: userPools }
]) #aws_cognito_user_pools {
id: ID! #primaryKey
name: String!
bedrooms: Int!
}
input FloorplanInput {
id: ID!
name: String!
bedrooms: Int!
}
type Mutation {
floorplanLambda(input: FloorplanInput): Floorplan
#function(name: "floorplanLambda-${env}")
#aws_cognito_user_pools
}
I created the lambda function to perform custom validation before updating.
The problem is that any authenticated user can update other users' floor plans. I thought adding #aws_cognito_user_pools would resolve this, but it doesn't.
Question: What do I need to add to lock down the floorplanLambda function so that it can only be successfully called by the owner of the Floorplan model?
When calling floorplanLambda I receive the error: "Not Authorized to access floorplanLambda on type Floorplan". I'm making the call with authMode AMAZON_COGNITO_USER_POOLS.
For some more context, I followed this tutorial to create the custom mutation lambda function: https://www.theclouddeveloper.io/use-lambda-resolvers-in-your-graph-ql-api-with-aws-amplify

So according to the response to my GitHub issue, this workflow is not currently supported. You can follow it here: https://github.com/aws-amplify/amplify-category-api/issues/528#issuecomment-1157894170
A workaround was provided by:
...setting the auth rule to private and then perform
validation in the lambda function...
I was thinking to create a custom auth function and then chain it in front of my custom mutation. Not sure if that will work but I'll report back once I've made some progress.

Related

Amplify, Cognito authentication and GraphQL API

I am a bit confused about how authentication works using AWS Amplify (Cognito and AppSync bundle).
I have setup my authentication method to Cognito and I designed a datamodel using GraphQL API schema.
It looks almost like this (I skipped some elements has they are not relevant to my question):
type Grower #model
#auth(rules: [
{ allow: owner, ownerField: "admins" },
{ allow: owner, ownerField: "members", operations: [read, update, create]},
{ allow: owner, ownerField: "guests", operations: [read]}
]) {
code: String!
name: String!
admins: [String]
members: [String]
guests: [String]
workers: [Workers] #hasMany
orchards: [Orchard] #hasMany
}
type Orchard #model
#auth(rules: [
{ allow: owner }
]){
name: String!
location: String
grower: Grower! #belongsTo
harvests: [Harvest] #hasMany
visits: [Visit] #hasMany
}
Given this schema, will a user declared in the Grower.admins field be able to access an Orchard and its fields if it belongs to the Grower?
And, the opposite, will a user not declared as a member of this Grower (neither guests, member or admins) be able to see the Orchard?
Basically, I would like to understand if my authentication rules 'propagate' throught nested models.
Any comment about this datastructure is welcomed.

AWS Amplify with GraphQL - Defining authentication rules by different types of users

Using Amplify, GraphQL, AppSync, Cognito, DynamoDB
Having the following model:
type Post
#model
{
id: ID!
content: String!
author: String!
}
I want my rules to enable the following case:
Only Admin users can create, update and delete Post
Some Posts where only premium users allow to read
Some Posts where all logged in users allow to read
Some Posts where all users (also unauthenticated) allow to read
What is the best way to implement it using the mentioned tools?
Thanks
From your question, it is not clear how you define "Some Posts" and how you would differentiate one from another. If I was designing this, I would have at least one more field in my Post type to manage the access level (For example: 3 (Admin) > 2 (Premium) > 1 (Logged-in) > 0 (Unregistered)), like so;
type Post
#model
{
id: ID!
content: String!
author: String!
accessLevel: Int!
}
To manage this on user level, I think your best bet is to manage it using Cognito groups (like mentioned in the official documentation) and assign appropriate permission for each group.
Things you would need in Cognito:
A user pool which will contain all of your registered users.
A user group for premium members.
A user group for your admins.
Things you would need in your AppSync:
For Admin users to create, update and delete Post:
type Mutation {
createPost(id:ID!, content:String!, author:String!):Post!
#aws_auth(cognito_groups: ["Admin"])
updatePost(id:ID!, content:String!, author:String!):Post!
#aws_auth(cognito_groups: ["Admin"])
deletePost(id:ID!, content:String!, author:String!):Post!
#aws_auth(cognito_groups: ["Admin"])
}
For some posts only visible to premium, logged-in or unregistered users to read:
type Query {
getPost(id:ID!):Post!
#aws_api_key #aws_cognito_user_pools
}
Furthermore, you can use the accessLevel in your resolver to filter out the result based on which post you want to be visible to premium, logged-in or unregistered users.
I used #Myz answers.
And https://aws.amazon.com/blogs/mobile/graphql-security-appsync-amplify/ for full solution:
type Post
#model
#auth(
rules: [
{ allow: owner }
{ allow: groups, groups: ["Admin"], operations: [create, update, delete] }
{ allow: groups, groupsField: "group", operations: [read] }
]
) {
id: ID!
content: String!
author: String!
group: [String] # or String for a single group
}

AWS Amplify GraphQL CreateOrUpdate / Upsert mutation

Minor newbie with GraphQL so I'm a little bit lost here. How would I go about creating a CreateOrUpdate mutation/resolver for a model on AWS Amplify?
I'm running a lambda function after every user auth to hit a third party API and pull back relevant data to that user. The entities that come back may already be stored but related to another user. I'm assuming there's a better way than attempting to Create, catching an error and then attempting to Fetch & Update with the new user appended to the users field?!
type Event
#model
#auth(rules: [
{allow: public, provider: apiKey, operations: [read, create, update, delete]}
{allow: owner, ownerField: "users"}
])
#key(fields: ["venue", "date"])
{
id: ID!
venue: String!
date: AWSDate!
ref: String!
users: [String]!
}
Any help massively appreciated (even just good resources to read up on writing resolvers - looking at the generated Mutation.updateEvent.req.vtl file for inspiration is a bit intimidating)
You need to override the generated resolver for update mutation.
Just copy the content of the autogenerated resolvers and make some changes.
File name may look like this:
<project-root>/amplify/backend/api/<api-name>/build/resolvers/<TypeName>.<FieldName>.<req/res>.vlt
To override, copy this file to:
<project-root>/amplify/backend/api/<api-name>/resolvers/<TypeName>.<FieldName>.<req/res>.vlt
For example: amplify/backend/api/blog/build/resolvers/Mutation.updatePost.req.vtl
Then remove these line:
## Begin - key condition **
#if( $ctx.stash.metadata.modelObjectKey )
#set( $keyConditionExpr = {} )
#set( $keyConditionExprNames = {} )
#foreach( $entry in $ctx.stash.metadata.modelObjectKey.entrySet() )
$util.qr($keyConditionExpr.put("keyCondition$velocityCount", {
"attributeExists": true
}))
$util.qr($keyConditionExprNames.put("#keyCondition$velocityCount", "$entry.key"))
#end
$util.qr($ctx.stash.conditions.add($keyConditionExpr))
#else
$util.qr($ctx.stash.conditions.add({
"id": {
"attributeExists": true
}
}))
#end
That part of vtl code checks that the id on update mutation should exists.

Query and Mutate GraphQL from Lambda function in AWS-Amplify using Cognito for auth

I created a GraphQL api via the amplify api add command and added the schema below. I'm using cognito for auth.
type User #model
#auth(rules: [{ allow: owner }]) {
id: ID!
videos: [Video!] #connection(keyName: "videosByUser", fields: ["id"])
adverts: [Advert] #connection(keyName:"advertsByUser", fields: ["id"])
}
type Video #model
#key(name: "videosByUser", fields: ["userId"])
#auth(rules: [{ allow: owner, operations: [create, update, delete] }]) {
id: ID!
title: String!
description: String!
size: Float!
length: Float!
hashMarks: [Float!]!
userId: ID!
# bidrectional connection, if needed
# user: User! #connection(fields: ["userId"])
adverts: [VideoAdverts!] #connection(keyName: "advertsByVideo", fields: ["id"])
streamingLink: AWSURL
}
type VideoAdverts #model(queries: null)
#key(name: "advertsByVideo", fields: ["videoId", "advertId"])
#key(name: "videosByAdvert", fields: ["advertId", "videoId"]) {
id: ID!
videoId: ID!
advertId: ID!
video: Video! #connection(fields: ["videoId"])
advert: Advert! #connection(fields: ["advertId"])
}
type Advert #model
#key(name: "advertsByUser", fields: ["userId"])
#auth(rules: [{ allow: owner, operations: [create, update, delete] }]) {
id: ID!
title: String!
description: String!
size: Float!
length: Float!
creatorId: ID!
# bidrectional connection, if needed
# creator: Creator! #connection(fields: ["creatorId"])
videos: [VideoAdverts!] #connection(keyName: "videosByAdvert", fields: ["id"])
blacklist: [AdvertBlacklist!] #connection(keyName: "blacklistByAdvert", fields: ["id"])
startDate: AWSDateTime
endDate: AWSDateTime
}
This is my first amplify project and I'm having trouble figuring out how to implement the following use cases:
Use a lambda function to query data and return to a client.
Use a cron triggered lambda function to make an API call and use a mutation to update some fields.
All I have found so far from googling involves using lambdas to interact with data added via the amplify storage add command.
A few other examples I found here on Stackoverflow do not use cognito for auth.
Looks like I will be able to use cloudwatch to trigger lambdas, and so my main problem now is how to actually query and mutate a GraphQL api from a lambda, using cognito for authentication.
Any help would be super helpful, thanks :)
The key to authenticating your Lambda functions to interact with your AppSync API is to configure multiple authentication methods. You are using Cognito for your front end application users, however, you do not want to use that for your Lambda function authentication. AppSync supports multiple authentications mechanisms for your API. In your case, you will want to add IAM as the second authentication mechanism.
You can do this from the Amplify CLI:
$ amplify update api
Scanning for plugins...
Plugin scan successful
? Please select from one of the below mentioned services: GraphQL
? Choose the default authorization type for the API Amazon Cognito User Pool
Use a Cognito user pool configured as a part of this project.
? Do you want to configure advanced settings for the GraphQL API Yes, I want
to make some additional changes.
? Configure additional auth types? Yes
? Choose the additional authorization types you want to configure for the API IAM

AWS-Amplify API module: how to make GraphQL fields unique?

AWS-Amplify provides a couple of directives to build an GraphQL-API. But I haven't found out how to ensure uniqueness for fields.
I want to do something like in GraphCool:
type Tag #model #searchable {
id: ID!
label: String! #isUnique
}
This is an AWS-Amplify specific question. It's not about how to do this with generic GraphQL. It's very specifically about how to do this with AWS-Amplify's API module. (https://aws-amplify.github.io/docs/js/api)
Hey thanks for the question. This is not yet possible by default using the amplify-cli but you could do this yourself using pipeline resolvers and an extra index on your DynamoDB table. The steps to do this are as follows:
Create a GSI on the table where the label is the HASH KEY.
Create a pipeline resolver on the Mutation.createTag field in your schema. You can turn off the auto-generated Mutation.createTag mutation by changing your #model definition to #model(mutations: { update: "updateTag", delete: "deleteTag" }).
Create a function named LookupLabel that issues a Query against the new GSI where the label = $ctx.args.input.label. If this returns a value, throw an error with $util.error("Label is not unique"). If it returns no values then continue.
Create a function named CreateTag that issues a PutItem against the Tag table.
Add those two functions in order to your pipeline resolver.
You can read more about pipeline resolvers here https://docs.aws.amazon.com/appsync/latest/devguide/pipeline-resolvers.html.
As of writing amplify does not yet support custom & pipeline resolvers but you can read more about the feature here https://github.com/aws-amplify/amplify-cli/issues/574 as it will be supported in the future. For now you can add the resolver manually in the AWS AppSync console or via your own CloudFormation template that targets the id of the API created by Amplify. It would also be helpful if you create an issue here (https://github.com/aws-amplify/amplify-cli/issues) and tag this as a feature request because it would be possible to automate this with an #unique directive but this would need to be planned.
Thanks
Update: now you can use #primarykey and #index annotations:
https://docs.amplify.aws/cli/migration/transformer-migration/#what-is-changing
basic:
profile #model {
name
email #primaryKey - has to be unique
other
}
so if you needed something like:
profile #model {
name
email: String! #hasOne
other
}
email #model {
email: String! #primaryKey
}
if you are on an older version see below
I will eventually be testing this out to see if this works but you might be able to do something like rename the id to a string!
so...
type Tag #model #key["id"] {
id: String!
}
or:
type Customer #model #key(fields: ["email"]) {
email: String!
username: String
}
this second one is taken directly from the docs: https://docs.amplify.aws/cli/graphql-transformer/key#designing-data-models-using-key
The docs were updated recently so hopefully they are easier for everyone to understand.
If you need a more advanced workflow with allot of keys, and stuff like that then you just have to separate things out and make more types for example:
type Customer #model {
id: String!
email: Email! #hasOne
username: String
}
type email #model #key(fields: ["email"]) {
email: String!
}

Resources