* Which Category is your question related to? *
AppSync, Amplify, Cognito
* What AWS Services are you utilizing? *
AppSync, Amplify, Cognito
* Provide additional details e.g. code snippets *
The question is complicated. I write it down in the technical design doc to make it as clear as possible.
The app is for managing projects.
I propose modeling the relations as followed.
Modified from https://github.com/aws-amplify/amplify-cli/issues/318#issuecomment-431471227.
type Task @model {
id: ID!
name: String!
project: Project! @connection(name: "ProjectTasks")
}
type Project @model {
id: ID!
name: String!
tasks: [Task] @connection(name: "ProjectTasks")
users: [ProjectMembership] @connection(name: "ProjectMembership_Project")
}
type User @model {
id: ID!
name: String!
projects: [ProjectMembership] @connection(name: "ProjectMembership_User")
}
# join model to encode many-to-many relationship
type ProjectMembership @model {
id: ID!
user: User! @connection(name: "ProjectMembership_User")
project: Project! @connection(name: "ProjectMembership_Project")
role: Role!
}
type Role {
canCreate: Boolean!
canUpdate: Boolean!
canDelete: Boolean!
canGet: Boolean!
canList: Boolean!
canSearch: Boolean!
// custom fields
canX: Boolean!
}
I have User
and ProjectMembership
type - I use data model to encode group membership rather than using the credential that comes from Cognito, because I don’t know how to leverage @auth
in this case.
Questions:
@auth
here? User.id
?It looks pretty much the same as { allow: ..., mutations: [...], queries: [...] }
, but again I don’t know how to leverage @auth
to reduce boilerplate.
I want to implement below APIs.
The implementation seems straightforward. I will rely on codegen getUser
resolver and below GraphQL query:
query {
getUser(id: "user-id") {
projects {
project
}
}
}
getRole
is to serve task methods that will be discussed later. Not sure if I want to expose this to frontend.
Even though all task methods (i.e., list, get, search, create, update, delete) have an argument role
, it does NOT necessarily mean, I want to get an task (let’s use getTask
as an example) for my frontend React App like this:
getRole
in frontend React App to get role into memory.role.canGet=false
, return unauthorized error, otherwise go to next step.API.graphql(graphqlOperation(getTaskQuery, {params: ...}))
to get the task from GraphQL backend, where getTaskQuery
is a GraphQL query such as query {getTask(…) {id name}}
.Instead, I am against authentication in the frontend and I hope it done implicitly in GraphQL backend. Ideally, to get an task:
API.graphql(graphqlOperation(getTaskQuery, {params: ...}))
with param taskId
on GraphQL backend.$ctx.identity
.$ctx.result
.ProjectMembership
model.role.canGet=false
, return $util.unauthorized()
, otherwise return the result.3.b can be saved if I also pass project info from React App to GraphQL backend, but here I prefer making the API simple to better clarify my problem.
Questions:
Let’s assume super_user role equals to can get/list/search/create/update/delete, while normal_user role equals to cannot delete, look back to Jack’s example (see Relations), I can try this:
type Task
@model
@auth(rules: [
# super user is allowed all operations
{ allow: groups, groups: ["CleanJacksHomeInSfSuperUser", "CleanJacksMomsHomeInSeaSuperUser"] },
# normal user is not allowed delete operation
{ allow: groups, groups: ["CleanJacksHomeInSfNormalUser", "CleanJacksMomsHomeInSeaNormalUser"], mutations: [create, update] }
]) {
id: ID!
name: String!
project: Project! @connection(name: "ProjectTasks")
}
type Project
@model
@auth(rules: [
{ allow: owner }
]) {
id: ID!
name: String!
tasks: [Task] @connection(name: "ProjectTasks")
}
And in AWS Cognito, I create 4 groups:
And assign user Jack to groups:
This should work. However, it doesn’t seem scalable. I will have to maintains #(project) * #(user type) of groups, which will hit group count limit mentioned at https://github.com/aws-amplify/amplify-cli/issues/318 easily. I also need to update the schema every time there is a new project or a new user type. To me, the cons dominates its pros: less and easier code.
In summary:
@auth
in User
and ProjectMembership
?User.id
?@UnleashedMind This all looks pretty good. For something like this you may consider not using the @auth
directives but instead implementing the fine-grained authorization / access control yourself (as to have more control over the actual implementation for each operation since there are not too many operations).
You could also use the @auth
directive & edit the configuration we give to you.
re: Pipeline resolvers, this would be a possibility. Check out this post (not yet published) or check out the documentation if you'd like to see an example of something similar -> https://medium.com/@dabit3/intro-to-aws-appsync-graphql-pipeline-functions-3df87ceddac1
Pipeline resolvers would be great, but if they are to offer any value here, they'll need to be supported by the CLI.
@dabit3 This definitely works. Will post a solution here soon.
@YikSanChan Could you use Cognito groups to nest users' permissions?
@jkeys-ecg-nmsu From https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-user-groups.html#user-pool-user-groups-limitations, Groups cannot be nested.
Right, you'd put users in every pool that grants permissions.
implementing the fine-grained authorization / access control yourself (as to have more control over the actual implementation for each operation since there are not too many operations).
@dabit3 how would one implement that themselves in a way that doesn't only run client-side?
@troygoode I plan to have a post describing how I do this.
@troygoode Typically would be a matter of updating the resolvers to take care of most of this.
I.e. mutations (adding data about the user when creating a mutation):
$util.qr($context.args.input.put("userId", $context.identity.sub))
& queries (filtering / querying indexes for only the data you need based on identity):
{
"version" : "2017-02-28",
"operation" : "Query",
"index": "userId-index",
"query" : {
"expression": "userId = :userId",
"expressionValues" : {
":userId" : $util.dynamodb.toDynamoDBJson($ctx.identity.sub)
}
}
}
@dabit3 According to your post, is it possible to trigger createUser
resolver whenever a new user is created in Cognito or via amplify-js withAuthenticator
UI?
@YikSanChan only if you have a corresponding User table in DynamoDB, so yes it's possible but it needs to go through DyanmoDB / AppSync!
@dabit3 Yes, I have the User table in DynamoDB, and AppSync creates createUser
for this table. Then where should I add the logic to ensure this createUser
get triggered whenever I sign up a new user via Cognito console or via amplify-js withAuthenticator
UI?
I have typically done this on the client.
I've implemented it before a couple of different ways, most recently like this:
When the user signs in, query for the user from your DB. If the user returns, then you know the user has already been created. If you do not get a response from the API (error, no such user), you create a new User.
The unique identifier I use to query / create is either the userId or the subject (sub), both are unique identifiers.
You can get the user's identity by calling Auth.currentAuthenticatedUser()
From client side, gotcha. Thanks @dabit3 !
Closed as I solved this already. Will update the thread once I have summarized the process in a post.
@YikSanChan Any chance you have a little code to share... It doesn't matter if it's unstructered :) I'm sitting with the exact same problem, and having a hard time, figuring out what to add to my resolver...
@YikSanChan I'm with @BrianAndersen78 - would love some direction on how you solved this We have the same exact use case and are quite confused...
Even just a high level overview if you don't have the time to share a code snippet will be immensely helpful. Did you use Pipeline functions/custom resolvers linked to DynamoDB? Something else?
Thanks in advance!
@YikSanChan
it exist pretty strange type of people which ask a question on github/stackoverflow/...
,
then they received some advices/suggestions from the community,
then they say 'All is fine, i resolved the problem'
,
BUT HOW I DID THAT I WILL SAY TO YOU MAYBE Tomorrow/Later/Next_month/Next_months/Next_Life/...
'!
then community try to ask topic starter during next time about his results, but without any luck. pretty cool situation
i always "like" such people
@BrianAndersen78 @HemalR @lon9man do you still need help in figuring out many-to-many auth? I was stuck but figured it out using resolver templates
@KandarpAjvalia
Yes please! I gave up on this (was a side project) but I am about to rewrite a more serious one in the next 2-4 weeks and would like Amplify as an option.
And thanks for the kind offer!
Let's take the example from above where we have the
Model
type Task @model {
id: ID!
name: String!
project: Project! @connection(name: "ProjectTasks")
}
type Project @model {
id: ID!
name: String!
tasks: [Task] @connection(name: "ProjectTasks")
users: [ProjectMembership] @connection(name: "ProjectMembership_Project")
}
type User @model {
id: ID!
name: String!
projects: [ProjectMembership] @connection(name: "ProjectMembership_User")
}
# join model to encode many-to-many relationship
type ProjectMembership @model {
id: ID!
user: User! @connection(name: "ProjectMembership_User")
project: Project! @connection(name: "ProjectMembership_Project")
role: Role!
}
type Role {
canCreate: Boolean!
canUpdate: Boolean!
canDelete: Boolean!
canGet: Boolean!
canList: Boolean!
canSearch: Boolean!
// custom fields
canX: Boolean!
}
Main things here are
For example we need to provide access to the admins group and the owner of project to view all the users in the project, so we need to use @auth
type ProjectMembership
@model
@auth(
rules: [
{
allow: owner,
queries: [get, list],
mutations: [create, update]
},
{
allow: groups,
groups: ["admin"],
queries: [get, list],
mutations: [create, update, delete]
}
]
){
id: ID!
user: User! @connection(name: "ProjectMembership_User")
project: Project! @connection(name: "ProjectMembership_Project")
role: Role!
}
type Project
@model
@auth(
rules: [
{
allow: owner,
queries: [get, list],
mutations: [create, update]
},
{
allow: groups,
groups: ["admin"],
queries: [get, list],
mutations: [create, update, delete]
}
]
){
id: ID!
name: String!
tasks: [Task] @connection(name: "ProjectTasks")
users: [ProjectMembership] @connection(name: "ProjectMembership_Project")
}
Push the changes
Well this would work as expected if we are directly accessing project membership, but it would not work when it is in a nested call for example: when getting all Project.ProjectMemberships. You can go ahead and see the difference when you have a direct call and a nested call by going to your AppSync API > Schema
Example: where you can find the resolvers
and finding resolvers for
Example: where you can find the response mapping template
You will see that the Auth that exists in the response mapping template of ProjectMembership(1) does not exist in the response template of Project.ProjectMembership(2).
To get the auth for Project.ProjectMembership just as ProjectMembership, clear the resolver for Project.ProjectMembership the you can copy and paste the entire response mapping template from ProjectMembership to Project.ProjectMembership as ProjectMembership's response mapping template contains the Auth.
Save the resolver for Project.ProjectMembership and it should work.
This is how I got the basic auth for many-to-many working.
@KandarpAjvalia Brilliant thank you!
It makes sense (I haven't implemented it... yet!). But I'm a lot more confident of giving it another whack now. Much appreciated.
Most helpful comment
@YikSanChan Any chance you have a little code to share... It doesn't matter if it's unstructered :) I'm sitting with the exact same problem, and having a hard time, figuring out what to add to my resolver...