In an AppSync GraphQL transform, can I set an #auth rule AuthStrategy based on a field in the table? - amazon-web-services

Is there a way to conditionally set more than one AuthStrategy depending on a field in the table?
Let's say I have a blog Post type:
type Post
#model
#auth(rules: [
{ allow: owner, operations: [read, update, delete] },
])
{
id: ID!
owner: String!
visibility: Visibility!
title: String!
body: String!
whitelist: [String!]
}
enum Visibility {
PUBLIC,
PRIVATE,
PROTECTED,
SECRET
}
I want the creator to be able to set whether the Post is:
Public: anyone can see it, logged in or not.
Private: any logged-in user can see it.
Protected: this array of users can see it.
Secret: only the creator can see it.
I know how to hardcode each of these. But programmatically...
Is this even possible, or are AppSync transforms just too basic, and I need to use a custom resolver?

It turns out, this does require a custom resolver—or custom, nested resolver—which is pretty easy.
The official docs on this authorization scenario: https://docs.aws.amazon.com/appsync/latest/devguide/security-authorization-use-cases.html.
And a great Hacker Noon article on this authorization scenario: https://hackernoon.com/graphql-authorization-with-multiple-data-sources-using-aws-appsync-dfae2e350bf2.

Related

AWS Amplify / GraphQl - How to limit auth user api calls based on #auth owner field pointing at a many to many relationship?

Which Category is your question related to?
GraphQL Api
Amplify CLI Version
10.5.1
What AWS Services are you utilizing?
AWS, Amplify, Cognito User Pools
I am trying to figure out how to limit an authorized user that is signed into to only be able to make queries like accounts they are assigned to. I do not want a auth user to be able to query accounts they are not associated with if they somehow have their account id.
So the modals I am working with are Account and User and they should have a many to many relationship. I am also using the version of amplify that uses the transformer v2 version for the graphql schema. So I cannot use the V1 setup that most documentation out there uses.
Current Schema
type Account #model #auth(rules: [
{allow: groups, groups: ["Admin"], operations: [read, create, update, delete]}
{allow: owner, ownerField: "users", operations: [read]}
]) {
id: ID!
account_name: String
products: AWSJSON
users: [User] #manyToMany(relationName: "AccountUser")
}
type User #model #auth(rules: [
{allow: groups, groups: ["Admin"], operations: [read, create, update, delete]},
{allow: owner, ownerField: "accounts", operations: [read]}
]) {
id: ID!
last_name: String
first_name: String
accounts: [Account] #manyToMany(relationName: "AccountUser")
email: AWSEmail
}
To break down what is going on the #manyToMany relationship creates a join table called AccountUser.
So the approach I was trying to go with was the multi-owner approach by setting the ownerFields. But the API throws errors like "Validation error of type FieldUndefined: Field 'accounts' in type 'AccountUser' is undefined # 'accountUsersByAccountId/items/accounts'"
I am guessing that this is because I set the ownerFields to data that at some point could have no data in it. This error showed up when I called a join table api endpoint called accountUsersByAccountId. Before I added the ownerField auth rule everything worked just fine but any auth user could query any account if they had the information.
Conclusion
I dont think I am on the right track here. I know the issue of limiting a user's ability to query certain data is a common usecase but I cannot find any examples for what I am trying to do.
From the examples I have looked at they would have me do the following. But there is no relationship for accounts and I dont think that is going to go over well if an account is deleted but the ID still exists in the user.
type User #model
#auth(rules: [
# Authorize the update mutation and both queries.
{ allow: owner, ownerField: "accounts", operations: [read] },
# Admin users can access any operation.
{ allow: groups, groups: ["Admin"] }
]) {
id: ID!
last_name: String
first_name: String
accounts: [String]
email: AWSEmail
}
I also cannot do groups cause I know there is a limit to the number of groups cognito allows and isnt sailable friendly.
Am I going about this the wrong way, can anyone let me know how they handled API limitations based on a users relationship to an account.

Not authorized to access type creation GRAPHQL

I am having trouble with
"message": "Not Authorized to access createMerchant on type Merchant"
I'm executing this on the AWS AppSync Request tab.
I'm able to list (this one too), create other types but just not this one.
input CreateMerchantInput {
id: ID
pass: AWSURL
validated: Boolean!
_version: Int
}
type MerchantType #aws_iam
#aws_cognito_user_pools {
id: ID!
role: String!
merchantSpotMerchantTypes(
filter: ModelMerchantSpotMerchantTypeFilterInput,
sortDirection: ModelSortDirection,
limit: Int,
nextToken: String
): ModelMerchantSpotMerchantTypeConnection
createdAt: AWSDateTime!
updatedAt: AWSDateTime!
_version: Int!
_deleted: Boolean
_lastChangedAt: AWSTimestamp!
}
Not sure how to fix this. Rights seem ok to me and I don't know where to look else.
The solution might depend on how you're authenticating the call - are you using Cognito? IAM?
"Rights seem ok" ... well, they might seem ok to you but still not be correct :) If it's IAM then you should probably post your policy.
But you don't seem to have any explicit #auth directives, so -- in the absence of other info -- I'm going to guess that you're perhaps using Cognito and that you might have some custom VTL resolvers in your amplify project that are affecting createMerchant (perhaps accidentally). But it is hard to say for certain without more detail on your project.

Can I implement fields as a set (non-duplicated list) in AWS Amplify Datastore?

I'm a beginner at AWS Amplify and GraphQL.
I came here because I couldn't find it no matter how hard I looked.
My GraphQL code is as follows:
type User #model #auth(rules: [{ allow: owner }]) {
id: ID!
assets: [Asset]! #hasMany
}
I don't want 'Asset' model to be duplicated.
So I thought to make 'assets' as a set (non-duplicated list).
But is that possible to implement?

AWS Amplify AppSync GraphQL #Auth directive: How can I forbid to list of elements for public user

I am creating an React App with Amplify backend. So far that is working great but I want to forbid that certain user can list some Elements. Let me give an example graphql definition:
type Customer #model
#auth(rules: [
{allow: public, , operations: [read,create,update]},
{allow: groups, groups: ["admin","partner"] }])
{
id: ID!
firstName: String
lastName: String
email: String
phone: String
}
We have Customers that are not logged in so they are public. They know their id (because it is in the url after they created the Customer Entry) and they should be able to update, read and create their own user. Thats working good.
Unfortunately they can also use the listCustomers query. So they can see all the other entries. Can I forbid this in some way? As I understand the operation "read" means "get" and "list".
The same should be for cognito groups. "admin" should be able to do everything including "list" and "partner" should only be able to "get".
Does anyone has an idea? I have read the docs and googled it but seems like I do not find an answer.
Best regards
To answer my question:
You can add { allow: public, queries: [get, list], mutations: [create,update]} for authorization rules. Unfortunately this is deprecated. I have no idea if there will be sth similar in the futur.
See: https://docs.amplify.aws/cli/graphql-transformer/auth/#definition
type Customer #model #auth(rules: [
{ allow: public, queries: [get], mutations: [create,update]},
{ allow: groups, groups: ["admin"] },
{ allow: groups, groups: ["partner"], queries: [get], mutations: []}])
{
id: ID!
firstName: String
lastName: String
email: String
phone: String
request: [Request] #connection(keyName: "byCustomer", fields: ["id"])
}

Filtering List Query By Another Table's Field (a.k.a Cross-Table or Nested Filtering) in AWS Amplify GraphQL DynamoDB

Which Category is your question related to?
DynamoDB, AppSync(GraphQL)
Amplify CLI Version
4.50.2
Provide additional details e.g. code snippets
BACKGROUND:
I'm new in AWS serverless app systems and as a frontend dev, I'm quite enjoying it thanks to auto-generated APIs, tables, connections, resolvers etc. I'm using Angular/Ionic in frontend and S3, DynamoDB, AppSync, Cognito, Amplify-cli for the backend.
WHAT I HAVE:
Here is a part of my schema. I can easily use auto-generated APIs to List/Get Feedbacks with additional filters (i.e. score: { ge: 3 }). And thanks to the #connection I can see the User's details in the listed Feedback items.
type User #model #auth(rules: [{ allow: owner }]) {
id: ID!
email: String!
name: String!
region: String!
sector: String!
companyType: String!
}
type Feedback #model #auth(rules: [{ allow: owner }]) {
id: ID!
user: User #connection
score: Int!
content: String
}
WHAT I WANT:
I want to list Feedbacks based on several fields on User type, such as user's region (i.e. user.region: { contains: 'United States' }). Now I searched for a solution quite a lot like, #2311 , and I learned that amplify codegen only creates top-level filtering. In order to use cross-table filtering, I believe I need to modify resolvers, lambda functions, queries and inputs. Which, for a beginner, it looks quite complex.
WHAT I TRIED/CONSIDERED:
I tried listing all Users and Feedbacks separately and filtering them in front-end. But then the client downloads all these unnecessary data. Also because of the pagination limit, user experience takes a hit as they see an empty list and repeatedly need to click Load More button.
Thanks to some suggestions, I also thought about duplicating the User details in Feedback table to be able to search/filter them. Then the problem is that if User updates his/her info, duplicated values will be out-of-date. Also there will be too many duplicated data, as I need this feature for other tables also.
I also heard about using ElasticSearch for this problem but someone mentioned for a simple filtering he got 30$ monthly cost, so I got cold feet.
I tried the resolver solution to add a custom filtering in it. But I found that quite complex for a beginner. Also I will need this cross-table filtering in many other tables as well, so I think would be hard to manage. If that is the best-practice, I'd appreciate it if someone can guide me through it.
QUESTIONS:
What would be the easiest/beginner-friendly solution for me to achieve this cross-table filtering? I am open to alternative solutions.
Is this cross-table filtering a bad approach for a no-SQL setup? Since I need some relationship between two tables. (I thought #connection would be enough). Should I switch to an SQL setup before it is too late?
Is it possible for Amplify to auto-generate a solution for this in the future? I feel like many people are experiencing the same issue.
Thank you in advance.
Amplify, and really DynamoDB in general, requires you to think about your access patterns ahead of time. There is a lot of really good information out there to help guide you through what this thought process can look like. Particularly, I like Nader Dabit's https://dev.to/dabit3/data-modeling-in-depth-with-graphql-aws-amplify-17-data-access-patterns-4meh
At first glance, I think I would add a new #key called byCountry to the User model, which will create a new Global Secondary Index on that property for you in DDB and will give you some new query methods as well. Check out https://docs.amplify.aws/cli/graphql-transformer/key#designing-data-models-using-key for more examples.
Once you have User.getByCountry in place, you should then be able to also bring back each user's Feedbacks.
query USAUsersWithFeedbacks {
listUsersByCountry(country: "USA") {
items {
feedbacks {
items {
content
}
nextToken
}
}
nextToken
}
}
Finally, you can use JavaScript to fetch all while the nextToken is not null. You will be able to re-use this function for each country you are interested in and you should be able to extend this example for other properties by adding additional #keys.
My former answer can still be useful for others in specific scenarios, but I found a better way to achieve nested filtering when I realized you can filter nested items in custom queries.
Schema:
type User #model {
id: ID!
email: String!
name: String!
region: String!
sector: String!
companyType: String!
feedbacks: [Feedback] #connection # <-- User has many feedbacks
}
Custom query:
query ListUserWithFeedback(
$filter: ModelUserFilterInput # <-- Filter Users by Region or any other User field
$limit: Int
$nextToken: String
$filterFeedback: ModelFeedbackFilterInput # <-- Filter inner Feedbacks by Feedback fields
$nextTokenFeedback: String
) {
listUsers(filter: $filter, limit: $limit, nextToken: $nextToken) {
items {
id
email
name
region
sector
companyType
feedbacks(filter: $filterFeedback, nextToken: $nextTokenFeedback) {
items {
content
createdAt
id
score
}
nextToken
}
createdAt
updatedAt
}
nextToken
}
}
$filter can be something like:
{ region: { contains: 'Turkey' } }
$filterFeedback can be like:
{
and: [{ content: { contains: 'hello' }, score: { ge: 4 } }]
}
This way both Users and Feedbacks can be filtered at the same time.
Ok thanks to #alex's answers I implemented the following. The idea is instead of listing Feedbacks and trying to filter them by User fields, we list Users and collect their Feedbacks from the response:
Updated schema.graphql as follows:
type User
#model
#auth(rules: [{ allow: owner }])
#key(name: "byRegion", fields: ["region"], queryField: "userByRegion") # <-- added byRegion key {
id: ID!
email: String!
name: String!
region: String!
sector: String!
companyType: String!
feedbacks: [Feedback] #connection # <-- added feedbacks connection
}
Added userFeedbacksId parameter while calling CreateFeedback. So they will appear while listing Users.
Added custom query UserByRegionWithFeedback under src/graphql/custom-queries.graphl and used amplify codegen to build it:
query UserByRegionWithFeedback(
$region: String
$sortDirection: ModelSortDirection
$filter: ModelUserFilterInput
$limit: Int
$nextToken: String # <-- nextToken for getting more Users
$nextTokenFeedback: String # <-- nextToken for getting more Feedbacks
) {
userByRegion(
region: $region
sortDirection: $sortDirection
filter: $filter
limit: $limit
nextToken: $nextToken
) {
items {
id
email
name
region
sector
companyType
feedbacks(nextToken: $nextTokenFeedback) {
items {
content
createdAt
id
score
}
nextToken
}
createdAt
updatedAt
owner
}
nextToken
}
}
Now I call this API like the following:
nextToken = {
user: null,
feedback: null
};
feedbacks: any;
async listFeedbacks() {
try {
const res = await this.api.UserByRegionWithFeedback(
'Turkey', // <-- region: filter Users by their region, I will add UI input later
null, // <-- sortDirection
null, // <-- filter
null, // <-- limit
this.nextToken.feedback == null ? this.nextToken.user : null, // <-- User nextToken: Only send if Feedback NextToken is null
this.nextToken.feedback // <-- Feedback nextToken
);
// Get User NextToken
this.nextToken.user = res.nextToken;
// Initialize Feedback NextToken as null
this.nextToken.feedback = null;
// Loop Users in the response
res.items.map((user) => {
// Get Feedback NextToken from User if it is not null (Or else last User in the list could overrite it)
if (user.feedbacks.nextToken) {
this.nextToken.feedback = user.feedbacks.nextToken;
}
// Push the feedback items into the list to diplay in UI
this.feedbacks.push(...user.feedbacks.items);
});
} catch (error) {
this.handleError.show(error);
}
}
Lastly I added a Load More button in the UI which calls listFeedbacks() function. So if there is any Feedback NextToken, I send it to the API. (Note that multiple user feedbacks can have a nextToken).
If all feedbacks are ok and if there is a User NextToken, I send that to the API and repeat the process for new Users.
I believe this could be much simpler with an SQL setup, but this will work for now. I hope it helps others in my situation. And if there is any ideas to make this better I'm all ears.