AWS Amplify GraphQL CreateOrUpdate / Upsert mutation - graphql

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.

Related

Restrict lambda resolver to owner for GraphQL API using Amplify

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.

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
}

How to do #auth for one-to-many #connection with AppSync GraphQL Transform?

My question is very similar to this question: How to do field level #auth for bi-directional one-to-many #connection with AppSync GraphQL Transform? . I'll ask with a slightly different graphql schema and hope for a more generalized answer. An example schema is:
type Blog #model #auth(rules: [{ allow: owner }, { allow: public, operations: [read] }]) {
id: ID!
name: String!
posts: [Post] #connection }
type Post #model #auth(rules: [{ allow: owner }, { allow: public, operations: [read] }]) {
title: String!
bodyText: String!
Suppose a user with username "User1" creates a Blog item. This db entry ends up as
Blog: { owner: "User1", id: "0b7f6862-1a46-4006-8953-e440334c47ec", name: "My First Blog!" }
Now User2 comes along knowing the id of User1's Blog -> namely, "0b7f6862-1a46-4006-8953-e440334c47ec". That user runs a createPost mutation with
input: { postBlogId: "0b7f6862-1a46-4006-8953-e440334c47ec", title: "I'm User1 and I Hate Puppies!", bodyText: "Puppies should be illegal!" }
This works! User2 has injected a post that will show up in queries for User1's blog when following the connection to Posts. That seems bad.
The accepted solution in the other question seems to rely on the id field in the table that's the "one" field in the "one-to-many" connection relationship happening to double as the owner field. But the problem seems to exist in any one-to-many connection, most of which won't have that constraint.
Have people found a way to protect this? I agree with the other question that this seems like something everyone must hit.
I thought of adding field level auth on the id field to restrict it to only allow read by owner plus a secondary id key to give to other users to run Gets with, but doing field level auth on required fields like id breaks subscriptions. The only other way I can think to do it is to block create and update for any types on the "many" side of the "one-to-many" connection relationship and then add custom mutations that run lambdas on the server that can query the other table to verify that the user has permissions to make the connection. But it seems like the amplify docs would have mentioned it somewhere if that's what you had to do? Would really appreciate being told I missed something dumb.
Apparently this is a duplicate of How to check permissions of an entity on create in appsync
The RFC for automatically doing this is still open and hasn't had an update in a year. https://github.com/aws-amplify/amplify-cli/issues/1055

AWS Amplify GraphQL filter by dynamic Cognito User Group

Given the following AWS Amplify GraphQL Schema (schema.graphql):
type Organization
#model
#auth(rules: [
{ allow: groups, groups: ["Full-Access-Admin"], mutations: [create, update, delete], queries: [list, get] },
{ allow: owner },
{ allow: groups, groupsField: "orgAdminsCognitoGroup", mutations: null, queries: [list, get] }
]) {
id: ID!
name: String!
address: String!
industry: [String]!
owner: String
orgAdminsCognitoGroup: String
}
I can filter out all organizations except the ones that belong to the current authenticated user via the following:
res = await API.graphql(graphqlOperation(listOrganizations, {
// todo: filter by owner OR by is org admin
filter: {
owner: {
eq: this.props.currentUser.username
}
}
}));
but is there anyway to also filter by the orgAdminsCognitGroup which is a dynamic group in Cognito belonging to the organization? I have not found any success trying to use an additional #model to help with the #auth rules to protect each entity.
So, the question is wanting to filter groups that the user is either the owner of, or in the 'orgAdminsCognitoGroup'?
I think it's possible, though I don't think the best way is what you had in mind. Instead, I might recommend you set up a response mapping template that does some server side filtering for you.
Specifically, you would first get the groups from the current user's auth token:
#set($claimPermissions = $ctx.identity.claims.get("cognito:groups"))
Then you could iterate over every organization in the results. If any have an owner that is the current user, add them to a response list. If they aren't, continue to check the orgAdminsCognitoGroup. You'd do that by checking whether or not $claimPermissions contains the group that the orgAdmin is set to for that organization. If it is contained, add it to the response list. If not, ignore it and continue iterating.
It would be possible, theoretically, to do this client side with the token the user has signed in with. Much in the same way the response mapping template did it, the groups the user is in are inside the token. If you crack it open and pull out the groups, you could apply the filtering there. I would recommend not doing this for security reasons, though it is possible.

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