feat: Add user roles, but do not yet enforce them#1200
Conversation
Currently can't remove roles. So be careful
| // TODO: @emyrk Might want to move these to a /roles group instead of /user. | ||
| // As we include more roles like org roles, it makes less sense to scope these here. | ||
| r.Put("/roles", api.putUserRoles) | ||
| r.Get("/roles", api.getUserRoles) |
There was a problem hiding this comment.
I'd rather not bikeshed this url placement atm. I'll move the path when I talk with FE. I know the mvp for endpoints, and will change them in another PR.
Really just trying to get some footing for roles so I can start enforcing them without breaking things.
Codecov Report
@@ Coverage Diff @@
## main #1200 +/- ##
==========================================
+ Coverage 65.45% 65.59% +0.14%
==========================================
Files 268 269 +1
Lines 17029 17337 +308
Branches 162 162
==========================================
+ Hits 11147 11373 +226
- Misses 4710 4772 +62
- Partials 1172 1192 +20
Continue to review full report at Codecov.
|
kylecarbs
left a comment
There was a problem hiding this comment.
All minor things. I'll approve once we resolve some of the outstanding q's.
| orgAdmin string = "organization-admin" | ||
| orgMember string = "organization-member" |
There was a problem hiding this comment.
Could we reuse the admin and member strings? From what I can tell, it'll be scoped to an organization anyways.
There was a problem hiding this comment.
We totally can, they are scoped. I was adding the organization prefix because I imagine in the UI they will strip the "scopeID" when assigning roles. I kinda like the fact org roles have an org prefix to prevent any misunderstanding.
Eg: A dropdown list of roles you can add to a org member, will say organization-member, not organization-member:00000000-00000000-00000000-00000000.
There was a problem hiding this comment.
We probably wouldn't have a single dropdown to show organization and site-level roles though, because that'd easily be misinterpreted by users IMO.
| // ListSiteRoles lists all roles that can be applied to a user. | ||
| // Note: This should be a list in a database, but until then we build | ||
| // the list from the builtins. | ||
| func ListSiteRoles() []string { |
There was a problem hiding this comment.
Doesn't seem this is used anywhere right now!
There was a problem hiding this comment.
It's not, but it will be when I add the endpoints for retrieving roles that can applied. I wrote this to show how to get the roles for that endpoint in this design.
When we go to a database model, these won't be dynamic lists, so all this looping goes away. I'll drop this and bring it back in my next pr
There was a problem hiding this comment.
That makes sense, thanks for the context!
| UPDATE | ||
| users | ||
| SET | ||
| rbac_roles = ARRAY ['member', 'organization-member:' || (SELECT id FROM organizations LIMIT 1)]; | ||
|
|
||
| -- Give the first user created the admin role | ||
| UPDATE | ||
| users | ||
| SET | ||
| rbac_roles = rbac_roles || ARRAY ['admin'] | ||
| WHERE | ||
| id = (SELECT id FROM users ORDER BY created_at ASC LIMIT 1) |
There was a problem hiding this comment.
We might not want to update this data in a migration right now. I'm concerned that changing this behavior will be difficult down the road, so I'd prefer if we just straight-up broke the schema right now (eg. add the rbac_roles directly to the base.up migration. I'm not strict on this though, and it's just a hunch. Any thoughts?
There was a problem hiding this comment.
I think we should try to not break migrations, lest we become comfortable in the habit and it bite us squarely where we sit down the line.
There was a problem hiding this comment.
Is anyone running a persistent V2? If we are only running in dev mode, I'll drop all this.
I just didn't want to break any current deployments. But if that's cool, I'm more than happy to drop this code to assign roles in the migration 👍
There was a problem hiding this comment.
Apparently we have a dev and QA instance, so this will be required to not totally break those.
- Remove string type alias, use string - Drop unused funcs - Fix down migration
- UpdateRoles vs Grant/RemoveRoles - UpdateRoles and UpdateMemberRoles
| @@ -0,0 +1,18 @@ | |||
| ALTER TABLE ONLY users | |||
| ADD COLUMN IF NOT EXISTS rbac_roles text[] DEFAULT '{}' NOT NULL; | |||
There was a problem hiding this comment.
TIL postgres supports native text arrays 😄
| organization_members | ||
| SET | ||
| -- Remove all duplicates from the roles. | ||
| roles = ARRAY(SELECT DISTINCT UNNEST(@granted_roles :: text[])) |
There was a problem hiding this comment.
Neat trick! For posterity here, UNNEST turns an array into a one-row temporary table, SELECT DISTINCT removes duplicates, and then it just becomes an array again.
| func (api *api) putMemberRoles(rw http.ResponseWriter, r *http.Request) { | ||
| // User is the user to modify | ||
| // TODO: Until rbac authorize is implemented, only be able to change your | ||
| // own roles. This also means you can grant yourself whatever roles you want. |
There was a problem hiding this comment.
nit: formatting, also "only be able to change your own roles"?
There was a problem hiding this comment.
Yea, since authorize is not yet used, I let you change your roles to w/e you want. This is what we are doing for a lot of user endpoints atm
| } | ||
|
|
||
| // UpdateOrganizationMemberRoles grants the userID the specified roles in an org. | ||
| // Include ALL roles the user has. |
There was a problem hiding this comment.
clarification: if a user has roles {A, B, C} and we call UpdateUserRoles with {D}, does this mean the user ends up with just {D} and not {A, B, C, D}? That seems like a sharp edge.
There was a problem hiding this comment.
This is correct. I actually had it as grant D before, but if we match the current UI, it actually makes sense to pass {A, B, C, D}.
The alternative is to run the diff from the UI as 2 requests.
PUT {D}
DELETE {A, B, C}
I think we can implement this logic better in the BE. I could add a route/query param to "Add" the role vs update for example. I think this is something we can make easier once the UI needs are better understood too. But w/e the end result is, the database call is ok to do an ALL, and let the Golang figure out the diffs.
kylecarbs
left a comment
There was a problem hiding this comment.
The separation of user and organization roles in the API feels really nice. Great work 🥳🥳🥳
* chore: Rework roles to be expandable by name alone

What this does
Adds some builtin roles and assigns them to users and org members. This does not implement enforcing roles at this time.
Granting user's roles was added to test the api. See how to grant roles from the SDK. The "strange" part is that the
rbaclibrary is used to find the role name. In practice, there will be an endpoint to list all roles that can be granted, and the user will select from a list. The code for this exists, but I want to add this in future PRs as I work with the FE on endpoint format.coder/coderd/users_test.go
Lines 326 to 330 in a643670
Future Work