Skip to content

Commit 35211e2

Browse files
authored
feat: Add user roles, but do not yet enforce them (coder#1200)
* chore: Rework roles to be expandable by name alone
1 parent ba4c3ce commit 35211e2

26 files changed

+1150
-232
lines changed

coderd/audit/table.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{
4242
"created_at": ActionIgnore, // Never changes.
4343
"updated_at": ActionIgnore, // Changes, but is implicit and not helpful in a diff.
4444
"status": ActionTrack, // A user can update another user status
45+
"rbac_roles": ActionTrack, // A user's roles are mutable
4546
},
4647
&database.Workspace{}: {
4748
"id": ActionIgnore, // Never changes.

coderd/coderd.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ func New(options *Options) (http.Handler, func()) {
120120
r.Get("/", api.workspacesByOwner)
121121
})
122122
})
123+
r.Route("/members", func(r chi.Router) {
124+
r.Route("/{user}", func(r chi.Router) {
125+
r.Use(
126+
httpmw.ExtractUserParam(options.Database),
127+
)
128+
r.Put("/roles", api.putMemberRoles)
129+
})
130+
})
123131
})
124132
r.Route("/parameters/{scope}/{id}", func(r chi.Router) {
125133
r.Use(apiKeyMiddleware)
@@ -183,6 +191,10 @@ func New(options *Options) (http.Handler, func()) {
183191
r.Get("/", api.userByName)
184192
r.Put("/profile", api.putUserProfile)
185193
r.Put("/suspend", api.putUserSuspend)
194+
// TODO: @emyrk Might want to move these to a /roles group instead of /user.
195+
// As we include more roles like org roles, it makes less sense to scope these here.
196+
r.Put("/roles", api.putUserRoles)
197+
r.Get("/roles", api.userRoles)
186198
r.Get("/organizations", api.organizationsByUser)
187199
r.Post("/organizations", api.postOrganizationsByUser)
188200
r.Post("/keys", api.postAPIKey)

coderd/database/databasefake/databasefake.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,43 @@ func (q *fakeQuerier) GetOrganizationIDsByMemberIDs(_ context.Context, ids []uui
743743
return getOrganizationIDsByMemberIDRows, nil
744744
}
745745

746+
func (q *fakeQuerier) GetOrganizationMembershipsByUserID(_ context.Context, userID uuid.UUID) ([]database.OrganizationMember, error) {
747+
q.mutex.RLock()
748+
defer q.mutex.RUnlock()
749+
750+
var memberships []database.OrganizationMember
751+
for _, organizationMember := range q.organizationMembers {
752+
mem := organizationMember
753+
if mem.UserID != userID {
754+
continue
755+
}
756+
memberships = append(memberships, mem)
757+
}
758+
return memberships, nil
759+
}
760+
761+
func (q *fakeQuerier) UpdateMemberRoles(_ context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
762+
for i, mem := range q.organizationMembers {
763+
if mem.UserID == arg.UserID && mem.OrganizationID == arg.OrgID {
764+
uniqueRoles := make([]string, 0, len(arg.GrantedRoles))
765+
exist := make(map[string]struct{})
766+
for _, r := range arg.GrantedRoles {
767+
if _, ok := exist[r]; ok {
768+
continue
769+
}
770+
exist[r] = struct{}{}
771+
uniqueRoles = append(uniqueRoles, r)
772+
}
773+
sort.Strings(uniqueRoles)
774+
775+
mem.Roles = uniqueRoles
776+
q.organizationMembers[i] = mem
777+
return mem, nil
778+
}
779+
}
780+
return database.OrganizationMember{}, sql.ErrNoRows
781+
}
782+
746783
func (q *fakeQuerier) GetProvisionerDaemons(_ context.Context) ([]database.ProvisionerDaemon, error) {
747784
q.mutex.RLock()
748785
defer q.mutex.RUnlock()
@@ -1173,11 +1210,42 @@ func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
11731210
UpdatedAt: arg.UpdatedAt,
11741211
Username: arg.Username,
11751212
Status: database.UserStatusActive,
1213+
RBACRoles: arg.RBACRoles,
11761214
}
11771215
q.users = append(q.users, user)
11781216
return user, nil
11791217
}
11801218

1219+
func (q *fakeQuerier) UpdateUserRoles(_ context.Context, arg database.UpdateUserRolesParams) (database.User, error) {
1220+
q.mutex.Lock()
1221+
defer q.mutex.Unlock()
1222+
1223+
for index, user := range q.users {
1224+
if user.ID != arg.ID {
1225+
continue
1226+
}
1227+
1228+
// Set new roles
1229+
user.RBACRoles = arg.GrantedRoles
1230+
// Remove duplicates and sort
1231+
uniqueRoles := make([]string, 0, len(user.RBACRoles))
1232+
exist := make(map[string]struct{})
1233+
for _, r := range user.RBACRoles {
1234+
if _, ok := exist[r]; ok {
1235+
continue
1236+
}
1237+
exist[r] = struct{}{}
1238+
uniqueRoles = append(uniqueRoles, r)
1239+
}
1240+
sort.Strings(uniqueRoles)
1241+
user.RBACRoles = uniqueRoles
1242+
1243+
q.users[index] = user
1244+
return user, nil
1245+
}
1246+
return database.User{}, sql.ErrNoRows
1247+
}
1248+
11811249
func (q *fakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUserProfileParams) (database.User, error) {
11821250
q.mutex.Lock()
11831251
defer q.mutex.Unlock()

coderd/database/dump.sql

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE ONLY users
2+
DROP COLUMN IF EXISTS rbac_roles;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
ALTER TABLE ONLY users
2+
ADD COLUMN IF NOT EXISTS rbac_roles text[] DEFAULT '{}' NOT NULL;
3+
4+
-- All users are site members. So give them the standard role.
5+
-- Also give them membership to the first org we retrieve. We should only have
6+
-- 1 organization at this point in the product.
7+
UPDATE
8+
users
9+
SET
10+
rbac_roles = ARRAY ['member', 'organization-member:' || (SELECT id FROM organizations LIMIT 1)];
11+
12+
-- Give the first user created the admin role
13+
UPDATE
14+
users
15+
SET
16+
rbac_roles = rbac_roles || ARRAY ['admin']
17+
WHERE
18+
id = (SELECT id FROM users ORDER BY created_at ASC LIMIT 1)

coderd/database/models.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/querier.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)