Skip to content

Commit a643670

Browse files
committed
Add endpoints to grant new roles to a user
Currently can't remove roles. So be careful
1 parent aa6fe88 commit a643670

File tree

10 files changed

+249
-30
lines changed

10 files changed

+249
-30
lines changed

coderd/coderd.go

+5
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ func New(options *Options) (http.Handler, func()) {
153153
r.Get("/listen", api.provisionerDaemonsListen)
154154
})
155155
})
156+
156157
r.Route("/users", func(r chi.Router) {
157158
r.Get("/first", api.firstUser)
158159
r.Post("/first", api.postFirstUser)
@@ -173,6 +174,10 @@ func New(options *Options) (http.Handler, func()) {
173174
r.Use(httpmw.ExtractUserParam(options.Database))
174175
r.Get("/", api.userByName)
175176
r.Put("/profile", api.putUserProfile)
177+
// TODO: @emyrk Might want to move these to a /roles group instead of /user.
178+
// As we include more roles like org roles, it makes less sense to scope these here.
179+
r.Put("/roles", api.putUserRoles)
180+
r.Get("/roles", api.getUserRoles)
176181
r.Get("/organizations", api.organizationsByUser)
177182
r.Post("/organizations", api.postOrganizationsByUser)
178183
r.Post("/keys", api.postAPIKey)

coderd/database/databasefake/databasefake.go

+16
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,21 @@ func (q *fakeQuerier) GetOrganizationMemberByUserID(_ context.Context, arg datab
689689
return database.OrganizationMember{}, sql.ErrNoRows
690690
}
691691

692+
func (q *fakeQuerier) GetOrganizationMembershipsByUserID(ctx context.Context, userID uuid.UUID) ([]database.OrganizationMember, error) {
693+
q.mutex.RLock()
694+
defer q.mutex.RUnlock()
695+
696+
var memberships []database.OrganizationMember
697+
for _, organizationMember := range q.organizationMembers {
698+
mem := organizationMember
699+
if mem.UserID != userID {
700+
continue
701+
}
702+
memberships = append(memberships, mem)
703+
}
704+
return memberships, nil
705+
}
706+
692707
func (q *fakeQuerier) GetProvisionerDaemons(_ context.Context) ([]database.ProvisionerDaemon, error) {
693708
q.mutex.RLock()
694709
defer q.mutex.RUnlock()
@@ -1118,6 +1133,7 @@ func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
11181133
CreatedAt: arg.CreatedAt,
11191134
UpdatedAt: arg.UpdatedAt,
11201135
Username: arg.Username,
1136+
RbacRoles: arg.RbacRoles,
11211137
}
11221138
q.users = append(q.users, user)
11231139
return user, nil

coderd/database/querier.go

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

+39-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/organizationmembers.sql

+9
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,12 @@ INSERT INTO
2020
)
2121
VALUES
2222
($1, $2, $3, $4, $5) RETURNING *;
23+
24+
25+
-- name: GetOrganizationMembershipsByUserID :many
26+
SELECT
27+
*
28+
FROM
29+
organization_members
30+
WHERE
31+
user_id = $1;

coderd/rbac/builtin.go

+6-19
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ const (
1212
member string = "member"
1313
auditor string = "auditor"
1414

15-
orgAdmin string = "organization-admin"
16-
orgMember string = "organization-member"
17-
orgManager string = "organization-manager"
15+
orgAdmin string = "organization-admin"
16+
orgMember string = "organization-member"
1817
)
1918

2019
// RoleName is a string that represents a registered rbac role. We want to store
@@ -26,7 +25,7 @@ type RoleName = string
2625

2726
// The functions below ONLY need to exist for roles that are "defaulted" in some way.
2827
// Any other roles (like auditor), can be listed and let the user select/assigned.
29-
// Once we have a database implementation, the "default" roles be be defined on the
28+
// Once we have a database implementation, the "default" roles can be defined on the
3029
// site and orgs, and these functions can be removed.
3130

3231
func RoleAdmin() RoleName {
@@ -49,10 +48,9 @@ func RoleOrgMember(organizationID uuid.UUID) RoleName {
4948
var (
5049
// builtInRoles are just a hard coded set for now. Ideally we store these in
5150
// the database. Right now they are functions because the org id should scope
52-
// certain roles. If we store them in the database, we will need to store
53-
// them such that the "org" permissions are dynamically changed by the
54-
// scopeID passed in. This isn't a hard problem to solve, it's just easier
55-
// as a function right now.
51+
// certain roles. When we store them in the database, each organization should
52+
// create the roles that are assignable in the org. This isn't a hard problem to solve,
53+
// it's just easier as a function right now.
5654
//
5755
// This map will be replaced by database storage defined by this ticket.
5856
// https://github.com/coder/coder/issues/1194
@@ -120,17 +118,6 @@ var (
120118
},
121119
}
122120
},
123-
124-
orgManager: func(organizationID string) Role {
125-
return Role{
126-
Name: roleName(orgMember, organizationID),
127-
Org: map[string][]Permission{
128-
organizationID: permissions(map[Object][]Action{
129-
ResourceWorkspace: {WildcardSymbol},
130-
}),
131-
},
132-
}
133-
},
134121
}
135122
)
136123

coderd/users.go

+74-4
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import (
1111
"strconv"
1212
"time"
1313

14-
"github.com/coder/coder/coderd/rbac"
15-
1614
"github.com/go-chi/chi/v5"
1715
"github.com/go-chi/render"
1816
"github.com/google/uuid"
@@ -23,6 +21,7 @@ import (
2321
"github.com/coder/coder/coderd/gitsshkey"
2422
"github.com/coder/coder/coderd/httpapi"
2523
"github.com/coder/coder/coderd/httpmw"
24+
"github.com/coder/coder/coderd/rbac"
2625
"github.com/coder/coder/coderd/userpassword"
2726
"github.com/coder/coder/codersdk"
2827
"github.com/coder/coder/cryptorand"
@@ -300,6 +299,69 @@ func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) {
300299
httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile))
301300
}
302301

302+
func (api *api) getUserRoles(rw http.ResponseWriter, r *http.Request) {
303+
user := httpmw.UserParam(r)
304+
305+
resp := codersdk.UserRoles{
306+
Roles: user.RbacRoles,
307+
}
308+
309+
memberships, err := api.Database.GetOrganizationMembershipsByUserID(r.Context(), user.ID)
310+
if err != nil {
311+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
312+
Message: fmt.Sprintf("get user memberships: %s", err),
313+
})
314+
return
315+
}
316+
317+
for _, mem := range memberships {
318+
resp.Roles = append(resp.Roles, mem.Roles...)
319+
}
320+
321+
httpapi.Write(rw, http.StatusOK, resp)
322+
}
323+
324+
func (api *api) putUserRoles(rw http.ResponseWriter, r *http.Request) {
325+
// User is the user to modify
326+
// TODO: Until rbac authorize is implemented, only be able to change your
327+
// own roles. This also means you can grant yourself whatever roles you want.
328+
user := httpmw.UserParam(r)
329+
apiKey := httpmw.APIKey(r)
330+
if apiKey.UserID != user.ID {
331+
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
332+
Message: fmt.Sprintf("modifying other users is not supported at this time"),
333+
})
334+
return
335+
}
336+
337+
var params codersdk.GrantUserRoles
338+
if !httpapi.Read(rw, r, &params) {
339+
return
340+
}
341+
342+
for _, r := range params.Roles {
343+
if _, err := rbac.RoleByName(r); err != nil {
344+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
345+
Message: fmt.Sprintf("%q is not a supported role", r),
346+
})
347+
return
348+
}
349+
}
350+
351+
updatedUser, err := api.Database.GrantUserRole(r.Context(), database.GrantUserRoleParams{
352+
GrantedRoles: params.Roles,
353+
ID: user.ID,
354+
})
355+
if err != nil {
356+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
357+
Message: fmt.Sprintf("get user: %s", err),
358+
})
359+
return
360+
}
361+
362+
httpapi.Write(rw, http.StatusOK, convertUser(updatedUser))
363+
}
364+
303365
// Returns organizations the parameterized user has access to.
304366
func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) {
305367
user := httpmw.UserParam(r)
@@ -396,7 +458,11 @@ func (api *api) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request)
396458
UserID: user.ID,
397459
CreatedAt: database.Now(),
398460
UpdatedAt: database.Now(),
399-
Roles: []string{"organization-admin"},
461+
Roles: []string{
462+
// Also assign member role incase they get demoted from admin
463+
rbac.RoleOrgMember(organization.ID),
464+
rbac.RoleOrgAdmin(organization.ID),
465+
},
400466
})
401467
if err != nil {
402468
return xerrors.Errorf("create organization member: %w", err)
@@ -889,6 +955,7 @@ func (api *api) createAPIKey(rw http.ResponseWriter, r *http.Request, params dat
889955
func (api *api) createUser(ctx context.Context, req codersdk.CreateUserRequest) (database.User, uuid.UUID, error) {
890956
var user database.User
891957
return user, req.OrganizationID, api.Database.InTx(func(db database.Store) error {
958+
var orgRoles []string
892959
// If no organization is provided, create a new one for the user.
893960
if req.OrganizationID == uuid.Nil {
894961
organization, err := db.InsertOrganization(ctx, database.InsertOrganizationParams{
@@ -901,7 +968,10 @@ func (api *api) createUser(ctx context.Context, req codersdk.CreateUserRequest)
901968
return xerrors.Errorf("create organization: %w", err)
902969
}
903970
req.OrganizationID = organization.ID
971+
orgRoles = append(orgRoles, rbac.RoleOrgAdmin(req.OrganizationID))
904972
}
973+
// Always also be a member
974+
orgRoles = append(orgRoles, rbac.RoleOrgMember(req.OrganizationID))
905975

906976
params := database.InsertUserParams{
907977
ID: uuid.New(),
@@ -947,7 +1017,7 @@ func (api *api) createUser(ctx context.Context, req codersdk.CreateUserRequest)
9471017
CreatedAt: database.Now(),
9481018
UpdatedAt: database.Now(),
9491019
// By default give them membership to the organization
950-
Roles: []string{rbac.RoleOrgMember(req.OrganizationID)},
1020+
Roles: orgRoles,
9511021
})
9521022
if err != nil {
9531023
return xerrors.Errorf("create organization member: %w", err)

coderd/users_test.go

+57
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"sort"
88
"testing"
99

10+
"github.com/coder/coder/coderd/rbac"
11+
1012
"github.com/google/uuid"
1113
"github.com/stretchr/testify/require"
1214

@@ -286,6 +288,61 @@ func TestUpdateUserProfile(t *testing.T) {
286288
})
287289
}
288290

291+
func TestGrantRoles(t *testing.T) {
292+
t.Parallel()
293+
t.Run("FirstUserRoles", func(t *testing.T) {
294+
t.Parallel()
295+
ctx := context.Background()
296+
client := coderdtest.New(t, nil)
297+
first := coderdtest.CreateFirstUser(t, client)
298+
299+
roles, err := client.GetUserRoles(ctx, codersdk.Me)
300+
require.NoError(t, err)
301+
require.ElementsMatch(t, roles.Roles, []string{
302+
rbac.RoleAdmin(),
303+
rbac.RoleMember(),
304+
rbac.RoleOrgMember(first.OrganizationID),
305+
rbac.RoleOrgAdmin(first.OrganizationID),
306+
}, "should be a member and admin")
307+
})
308+
309+
t.Run("GrantAdmin", func(t *testing.T) {
310+
t.Parallel()
311+
ctx := context.Background()
312+
admin := coderdtest.New(t, nil)
313+
first := coderdtest.CreateFirstUser(t, admin)
314+
315+
member := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID)
316+
roles, err := member.GetUserRoles(ctx, codersdk.Me)
317+
require.NoError(t, err)
318+
require.ElementsMatch(t, roles.Roles, []string{
319+
rbac.RoleMember(),
320+
rbac.RoleOrgMember(first.OrganizationID),
321+
}, "should be a member and admin")
322+
323+
// Grant
324+
// TODO: @emyrk this should be 'admin.GrantUserRoles' once proper authz
325+
// is enforced.
326+
_, err = member.GrantUserRoles(ctx, codersdk.Me, codersdk.GrantUserRoles{
327+
Roles: []string{
328+
rbac.RoleAdmin(),
329+
rbac.RoleOrgAdmin(first.OrganizationID),
330+
},
331+
})
332+
require.NoError(t, err, "grant member admin role")
333+
334+
roles, err = member.GetUserRoles(ctx, codersdk.Me)
335+
require.NoError(t, err)
336+
require.ElementsMatch(t, roles.Roles, []string{
337+
rbac.RoleMember(),
338+
rbac.RoleOrgMember(first.OrganizationID),
339+
340+
rbac.RoleAdmin(),
341+
rbac.RoleOrgAdmin(first.OrganizationID),
342+
}, "should be a member and admin")
343+
})
344+
}
345+
289346
func TestUserByName(t *testing.T) {
290347
t.Parallel()
291348
client := coderdtest.New(t, nil)

0 commit comments

Comments
 (0)