Is your feature request related to a problem? Please describe.
Authorization by Group is great but since Cognito has a 25 group limit on userpool (see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-user-groups.html), this method of authorization becomes very limiting.
Imagine a project management app where you can have many teams, each with their own tasks and users. You want to allow each team to have access to all tasks. How do you enforce this access? @auth seems like it would work but it is limited by the fact that Cognito officially supports 25 groups per userpool max. This limits the app to 25 teams max.
Describe the solution you'd like
Either for cognito to support more than 25 groups, or some other alternative within the graphql transformer to specify a model that represents the group which is then connected to the users and the resolver logic checks if the user belongs to the group(s) specified.
Describe alternatives you've considered
Have not thought through exact solutions yet. It's either to have an "owners" field instead of groups field but that gets cumbersome as you have to update owners on multiple models everytime group structure changes. Have a custom lambda that generates its own identityField like what's mentioned in https://github.com/aws-amplify/amplify-cli/issues/317 also works. Or just changing resolver logic manually to check if user belongs to a group where group is defined by the application rather than cognito.
@hisham This is a good point. Here are a few ideas:
The first (obvious and potentially unhelpful) suggestion is to ask Cognito for an increase to the number of groups you can have. I believe they can increase this into the hundreds when asked which may be able to solve your problem depending on the number of teams.
The second (and hopefully more helpful) suggestion is to use your data model to encode group membership rather than using the credential that comes from Cognito. Assuming a user can belong to multiple teams, conceptually, the model you are describing is a many-to-many relationship between users and teams and a one-to-many relationship between teams and tasks. Once you have modeled the relationship, you can use the semantics of the relationship itself to authorize access to objects.
type User @model @auth(rules: [{allow: owner}, {allow: groups: groups: ["UserAdmin"]}]) {
id: ID!
username: String!
teams: [TeamMembership] @connection(name: "UserTeams")
}
# Create a join model to facilitate the many-to-many relationship.
# Only "TeamMembershipAdmin" members can create, update, and delete membership
# objects that add users to teams.
type TeamMembership @model(queries: null) @auth(rules: [{ allow: groups, groups: ["TeamMembershipAdmin"]}]) {
id: ID!
team: Team! @connection(name: "TeamMembership")
member: User! @connection(name: "UserTeams")
}
# Only "Admin" members can create, update, and delete teams themselves.
type Team @model(queries: null) @auth(rules: [{ allow: groups, groups: "Admin", queries: null }]) {
id: ID!
name: String!
members: [TeamMembership] @connection(name: "TeamMembership")
tasks: [Task] @connection(name: "TeamTasks")
}
# Non @model types are stored inline as maps and lists in other @model types.
type SubTask {
content: String!
due: String!
}
# There is currently no way to specify that only members of a team may create
# items but you can get very close with a combo of group and owner auth.
# I am passing queries null so no top level task queries are generated and
# users must instead read them via User.teams.team.tasks
type Task @model(queries: null) @auth(
rules: [
{ allow: groups, groups: ["TaskAdmin"], mutations: [create]},
{ allow: owner, ownerField: "editors", mutations: [update, delete] }
]
) {
id: ID!
title: String!
team: Team @connection(name: "TeamTasks")
# Only "TaskAdmins" can create users but any user who's username is
# in this list will be able to update, delete, get, and list tasks.
editors: [String]
# Note: This is not a @connection because the non @model type will be inlined
subtasks: [SubTask]
}
The trick here is that a user of your API cannot access Team, TeamMembership, or Task objects at the top level because their queries are disabled. The user must then access their data by first querying through Query.getUser
which is itself owner authorized. Once the user has proven that they are who they say they are, they can use fields off the User type to get associated objects such as teams and tasks.
E.G.
query GetTopLevelUserAndTeamsAndTasks {
getUser(id: "my-user-id") {
id
username
teams {
items {
team {
id
name
tasks {
items {
id
title
subtasks {
content
due
}
}
}
}
}
}
}
}
Note: The authorization rules I specify above depend on this PR (which implements multi-owner auth among other things) getting merged first: https://github.com/aws-amplify/amplify-cli/pull/285
Thanks for this @mikeparisstuff. Yes I did submit a request to Cognito to increase the group limit to 500. They are currently reviewing the request since the service team needs to approve it so atleast it was not an instant no! :)
As for your other proposed solution, I'm having trouble understanding why the need for the "editors" field in type Task. I want the "editors" to be anyone in "team". With your solution, it seems if a person is added to a Team, they also have to be added to the "editors" field in each Task that belongs to that team?
So if we remove the "editors" field from task, and also remove the "allow: owner" rule from Task, then anyone would be able to update and delete a task, but since they only get access to tasks through their user object, they will only know about tasks that belong to their team. However if they guess the uuid of another task outside their team or just loop through all possible uuids, and modify the client app accordingly, they technically would be able to change that task I believe...very remote chance ofcourse they can guess uuid of another task but the possibility is there that ultimately the server does not enforce who can get or update these tasks (without the editors field).
Oh wait, is editors a calculated or resolved attribute similar to team but instead of returning team objects, it returns a string array of all user ids of the members in team? I guess that could work. But I don't think the graphql transformer supports this out of the box - need to add that resolver manually. And scan query on dynamo normally has a limit, max 1mb I think (and your generated resolvers default to 10 items), so if you have more than 10 people in the team and hit the scan limit, you need to deal with pagination in the backend to get the entire list. I guess we can make editors resolver smarter so that it does a query on team but with a filter to return team members with owner id that match the current logged in user's identity id - and then just return an array with that user only. That would work I think. Really in this case 'editors' would be changed to a 'canEdit' boolean field but since @auth rules do not support boolean fields you'd keep it as 'editors' which always returns either null or an array containing the current user's id if they are authorized.
Agree with @hisham that while closer, this doesn't really solve for the "team-wide" access issue. I wonder if a new type of @auth strategy thru connections could?
For example: { allow: connection, connectionName: "TeamTasks", connectionField: "members" }
and then on Team model, use the multi-owner strategy { allow: owner, ownerField: "members" }
.
And then using nested resolvers, we can first get the Team and pass the members down thru $ctx.source.members to the Task resolver which then checks that a member is allowed.
I'm not sure if this is too use-case specific or even possible, but just throwing it out there.
Just as an update - my request with Cognito to increase max groups limit in my userpool is still pending. I submitted the request 2 weeks ago and it's in the cognito service team's queue for approval. Given it's taking this long, that option of asking cognito service team for the limit increase is probably not a feasible option for a production app.
@lennybr What you have described is similar to the functionality I had in mind, but we are blocked on a dependent feature that should be rolling out later this year.
fyi - good news, cognito increased my limit to 500 groups in the userpool. Took a while but they finally did it. So I guess I could use that option though if #317 is resolved through pipeline resolvers which I assume is the ideal/planned solution then I prefer to go that route. I'll try to keep myself busy with other things to buy some time...
馃憤
I was in a similar situation and just noticed that the Pre-Token generator Lambda trigger in Cognito which allows to override groups upon login allows to add more than 25 (unlimited?) groups, so what we are probably going to do now is to save the "groups" a user belongs to as part of our user object in our database and just get that and add the groups with the Pre-Token generator lambda trigger instead of actually adding the user to the groups permanently.
fyi, going to start implementing proper group auth in my app now. I was hoping pipeline resolver support (https://github.com/aws-amplify/amplify-cli/issues/1055) would be added by now to the graphql transformer but it doesn't look like it, and I assume it's still a month or two away from that. Many people are asking for proper multi-tenancy support so I hope the amplify team is prioritizing accordingly as this is a core use case of any serious app. Thank you!
@D2KX - I'm looking at doing similar setup to yours. Did you setup the Pre-token generator lambda manually or via cloudformation? It does not seem it can be added via cloudformation. See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cognito-userpool-lambdaconfig.html and https://forums.aws.amazon.com/thread.jspa?threadID=268907
Hey guys also trying to implement group authorization in amplify.
Im trying to take a slightly different approach by creating a custom attribute for the user in cognito userGroup
And them in my schema I use the identityField in my models like this
type Room
@model
@auth(rules: [
{ allow: owner, ownerField: "owner", operations: [create, update, delete, read]},
{ allow: owner, ownerField: "userGroup", identityField: "cognito:custom:group", operations: [create, update, delete, read] }
]) {
id: ID!
room_name: String!
organization: String
owner: String
userGroup: String
}
Is it possible to make this approach work??
@gkpty You might have problems with your solution. Normally, you will have users be part of many groups. Currently, you need to use "allow: groups" to correctly handle an array of values. But, "allow: groups" only works with the "cognito:group" field.
A current workaround is to use the Cognito preToken Lambda. Inside that lambda you can make dynamo calls to find additional groups for that user. Then add to the "cognito:groups" claims list.
Last week Cloudformation added the preToken Lambda, so it should be much easier to deploy by modifying the Cognito Cloudformation template.
Hey @RossWilliams, Thanks for this!
So i dont want to use cognito groups because of the 500 groups hard limit. If i understand correctly, in the pre-token lambda trigger i would be able to add the user to a 'fake' group lets say orgA-admins
by using groupsToOverride
? https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html
then i would be able to use static group auth for the fake group passed in the pre-token lambda trigger as follows:
@auth(rules: [
{ allow: owner, ownerField: "owner", operations: [create, update, delete, read]},
{ allow: groups, groups: ["orgA-admins"], operations: [create, update, delete, read]}
]) {
Is this approach correct?
Thanks a lot!!
Hey Ross, Thanks for this!
So i dont want to use cognito groups because of the 500 groups hard limit. If i understand correctly, in the pre-token lambda trigger i would be able to add the user to a 'fake' group lets say
orgA-admins
by usinggroupsToOverride
? https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.htmlthen i would be able to use static group auth for the fake group passed in the pre-token lambda trigger as follows:
@auth(rules: [ { allow: owner, ownerField: "owner", operations: [create, update, delete, read]}, { allow: groups, groups: ["orgA-admins"], operations: [create, update, delete, read]} ]) {
Is this approach correct?
Thanks a lot!!
Yes this approach worked for me @gkpty.
Thanks @hisham !
I tried this approach but still cant get users from an organization
to create, read, update, delete stuff created by other users of that organization. I have 2 custom attributes in the users: organization
, and role
which together make up the user's group.
I implemented the a lambda function as follows and then i set it up in my cognito pool as a pre token Generation trigger:
exports.handler = (event, context, callback) => {
const userOrg = event.request.userAttributes['custom:organization'];
const userRole = event.request.userAttributes['custom:role'];
const group = `${userOrg}-${userRole}`;
event.response = {
"claimsOverrideDetails": {
"groupOverrideDetails": {
"groupsToOverride": [group],
"iamRolesToOverride": ["arn:aws:iam::xxxxx:role/xxxx-dev-xxxxx-authRole"],
"preferredRole": "arn:aws:iam::xxxxx:role/xxxx-dev-xxxxx-authRole"
}
}
};
// Return to Amazon Cognito
callback(null, event);
};
I also modified my schema as follows:
type Room
@model
@auth(rules: [
{ allow: owner },
{ allow: groups, groups: ["Admin"] },
{ allow: groups, groupsField: "adminGroup" },
{ allow: groups, groupsField: "userGroup", operations: [read] }
]) {
id: ID!
room_name: String!
organization: String
owner: String
userGroup: String
adminGroup: String
}
Any idea what i could be missing? Thanks a lot!!
@gkpty looks good to me. If it doesn't work, either the cognito groups is not passed through properly, or userGroup in your db is not set properly. Make sure userGroup / adminGroup in your db matches exactly the cognito group the user belongs to. You can for now assign a cognito group manually to one of your users in cognito console (manually, not through your pre-token generator). And see if that works. If it works, it means your pre-token generator is not being called / or is not working properly.
Also I recommend you add your group to the user's existing groups rather than completely override it, but up to you. Here is essentially what my lambda pre-signup code looks like:
let groups = event.request.groupConfiguration.groupsToOverride;
let mygroup = getMyGroup();
event.response = {
claimsOverrideDetails: {
groupOverrideDetails: {
groupsToOverride: [mygroup, ...groups]
}
}
};
return event;
Hey @hisham thanks a lot!
in the end the above code worked correctly. I had just forgotten to copy my /schema.graphql into backend/api/.../schema.grqphql and had pushed some previous changes.
really appreciate the help!
Closing this in favor of #317
25 groups?! Are you sure this is correct? From what I read here: https://docs.aws.amazon.com/cognito/latest/developerguide/limits.html#quota-categorization - You're able to create 10,000 groups per user pool?
25 groups?! Are you sure this is correct? From what I read here: https://docs.aws.amazon.com/cognito/latest/developerguide/limits.html#quota-categorization - You're able to create 10,000 groups per user pool?
Hi @jamesone. You are correct, Cognito recently changed the limit to 10,000.
Most helpful comment
@gkpty looks good to me. If it doesn't work, either the cognito groups is not passed through properly, or userGroup in your db is not set properly. Make sure userGroup / adminGroup in your db matches exactly the cognito group the user belongs to. You can for now assign a cognito group manually to one of your users in cognito console (manually, not through your pre-token generator). And see if that works. If it works, it means your pre-token generator is not being called / or is not working properly.
Also I recommend you add your group to the user's existing groups rather than completely override it, but up to you. Here is essentially what my lambda pre-signup code looks like: