Skip to content

Commit

Permalink
feat: Implied 'member' roles for site and organization (#1917)
Browse files Browse the repository at this point in the history
* feat: Member roles are implied and never exlpicitly added
* Rename "GetAllUserRoles" to "GetAuthorizationRoles"
* feat: Add migration to remove implied roles
* rename user auth role middleware
  • Loading branch information
Emyrk committed Jun 1, 2022
1 parent 2878346 commit cc87a0c
Show file tree
Hide file tree
Showing 21 changed files with 131 additions and 115 deletions.
4 changes: 2 additions & 2 deletions coderd/authorize.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import (
)

func AuthorizeFilter[O rbac.Objecter](api *API, r *http.Request, action rbac.Action, objects []O) []O {
roles := httpmw.UserRoles(r)
roles := httpmw.AuthorizationUserRoles(r)
return rbac.Filter(r.Context(), api.Authorizer, roles.ID.String(), roles.Roles, action, objects)
}

func (api *API) Authorize(rw http.ResponseWriter, r *http.Request, action rbac.Action, object rbac.Objecter) bool {
roles := httpmw.UserRoles(r)
roles := httpmw.AuthorizationUserRoles(r)
err := api.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, action, object.RBACObject())
if err != nil {
httpapi.Write(rw, http.StatusForbidden, httpapi.Response{
Expand Down
2 changes: 1 addition & 1 deletion coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ func CreateAnotherUser(t *testing.T, client *codersdk.Client, organizationID uui
organizationID, err := uuid.Parse(orgID)
require.NoError(t, err, fmt.Sprintf("parse org id %q", orgID))
_, err = client.UpdateOrganizationMemberRoles(context.Background(), organizationID, user.ID.String(),
codersdk.UpdateRoles{Roles: append(roles, rbac.RoleOrgMember(organizationID))})
codersdk.UpdateRoles{Roles: roles})
require.NoError(t, err, "update org membership roles")
}
}
Expand Down
8 changes: 5 additions & 3 deletions coderd/database/databasefake/databasefake.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ func (q *fakeQuerier) GetUsersByIDs(_ context.Context, ids []uuid.UUID) ([]datab
return users, nil
}

func (q *fakeQuerier) GetAllUserRoles(_ context.Context, userID uuid.UUID) (database.GetAllUserRolesRow, error) {
func (q *fakeQuerier) GetAuthorizationUserRoles(_ context.Context, userID uuid.UUID) (database.GetAuthorizationUserRolesRow, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()

Expand All @@ -286,6 +286,7 @@ func (q *fakeQuerier) GetAllUserRoles(_ context.Context, userID uuid.UUID) (data
if u.ID == userID {
u := u
roles = append(roles, u.RBACRoles...)
roles = append(roles, "member")
user = &u
break
}
Expand All @@ -294,14 +295,15 @@ func (q *fakeQuerier) GetAllUserRoles(_ context.Context, userID uuid.UUID) (data
for _, mem := range q.organizationMembers {
if mem.UserID == userID {
roles = append(roles, mem.Roles...)
roles = append(roles, "organization-member:"+mem.OrganizationID.String())
}
}

if user == nil {
return database.GetAllUserRolesRow{}, sql.ErrNoRows
return database.GetAuthorizationUserRolesRow{}, sql.ErrNoRows
}

return database.GetAllUserRolesRow{
return database.GetAuthorizationUserRolesRow{
ID: userID,
Username: user.Username,
Status: user.Status,
Expand Down
11 changes: 11 additions & 0 deletions coderd/database/migrations/000016_drop_member_roles.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--- Remove the now implied 'member' role.
UPDATE
users
SET
rbac_roles = array_append(rbac_roles, 'member');

--- Remove the now implied 'organization-member' role.
UPDATE
organization_members
SET
roles = array_append(roles, 'organization-member:'||organization_id::text);
11 changes: 11 additions & 0 deletions coderd/database/migrations/000016_drop_member_roles.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--- Remove the now implied 'member' role.
UPDATE
users
SET
rbac_roles = array_remove(rbac_roles, 'member');

--- Remove the now implied 'organization-member' role.
UPDATE
organization_members
SET
roles = array_remove(roles, 'organization-member:'||organization_id::text);
4 changes: 3 additions & 1 deletion coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 17 additions & 9 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 13 additions & 5 deletions coderd/database/queries/users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,20 @@ WHERE
id = $1 RETURNING *;


-- name: GetAllUserRoles :one
-- name: GetAuthorizationUserRoles :one
-- This function returns roles for authorization purposes. Implied member roles
-- are included.
SELECT
-- username is returned just to help for logging purposes
-- status is used to enforce 'suspended' users, as all roles are ignored
-- when suspended.
id, username, status, array_cat(users.rbac_roles, organization_members.roles) :: text[] AS roles
-- username is returned just to help for logging purposes
-- status is used to enforce 'suspended' users, as all roles are ignored
-- when suspended.
id, username, status,
array_cat(
-- All users are members
array_append(users.rbac_roles, 'member'),
-- All org_members get the org-member role for their orgs
array_append(organization_members.roles, 'organization-member:'||organization_members.organization_id::text)) :: text[]
AS roles
FROM
users
LEFT JOIN organization_members
Expand Down
15 changes: 14 additions & 1 deletion coderd/httpmw/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ func APIKey(r *http.Request) database.APIKey {
return apiKey
}

// User roles are the 'subject' field of Authorize()
type userRolesKey struct{}

// AuthorizationUserRoles returns the roles used for authorization.
// Comes from the ExtractAPIKey handler.
func AuthorizationUserRoles(r *http.Request) database.GetAuthorizationUserRolesRow {
apiKey, ok := r.Context().Value(userRolesKey{}).(database.GetAuthorizationUserRolesRow)
if !ok {
panic("developer error: user roles middleware not provided")
}
return apiKey
}

// OAuth2Configs is a collection of configurations for OAuth-based authentication.
// This should be extended to support other authentication types in the future.
type OAuth2Configs struct {
Expand Down Expand Up @@ -178,7 +191,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs) func(http.Handler) h
// If the key is valid, we also fetch the user roles and status.
// The roles are used for RBAC authorize checks, and the status
// is to block 'suspended' users from accessing the platform.
roles, err := db.GetAllUserRoles(r.Context(), key.UserID)
roles, err := db.GetAuthorizationUserRoles(r.Context(), key.UserID)
if err != nil {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: "roles not found",
Expand Down
40 changes: 0 additions & 40 deletions coderd/httpmw/authorize.go

This file was deleted.

16 changes: 8 additions & 8 deletions coderd/httpmw/authorize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,23 @@ func TestExtractUserRoles(t *testing.T) {
{
Name: "Member",
AddUser: func(db database.Store) (database.User, []string, string) {
roles := []string{rbac.RoleMember()}
roles := []string{}
user, token := addUser(t, db, roles...)
return user, roles, token
return user, append(roles, rbac.RoleMember()), token
},
},
{
Name: "Admin",
AddUser: func(db database.Store) (database.User, []string, string) {
roles := []string{rbac.RoleMember(), rbac.RoleAdmin()}
roles := []string{rbac.RoleAdmin()}
user, token := addUser(t, db, roles...)
return user, roles, token
return user, append(roles, rbac.RoleMember()), token
},
},
{
Name: "OrgMember",
AddUser: func(db database.Store) (database.User, []string, string) {
roles := []string{rbac.RoleMember()}
roles := []string{}
user, token := addUser(t, db, roles...)
org, err := db.InsertOrganization(context.Background(), database.InsertOrganizationParams{
ID: uuid.New(),
Expand All @@ -58,7 +58,7 @@ func TestExtractUserRoles(t *testing.T) {
})
require.NoError(t, err)

orgRoles := []string{rbac.RoleOrgMember(org.ID)}
orgRoles := []string{}
_, err = db.InsertOrganizationMember(context.Background(), database.InsertOrganizationMemberParams{
OrganizationID: org.ID,
UserID: user.ID,
Expand All @@ -67,7 +67,7 @@ func TestExtractUserRoles(t *testing.T) {
Roles: orgRoles,
})
require.NoError(t, err)
return user, append(roles, orgRoles...), token
return user, append(roles, append(orgRoles, rbac.RoleMember(), rbac.RoleOrgMember(org.ID))...), token
},
},
}
Expand All @@ -86,7 +86,7 @@ func TestExtractUserRoles(t *testing.T) {
httpmw.ExtractAPIKey(db, &httpmw.OAuth2Configs{}),
)
rtr.Get("/", func(_ http.ResponseWriter, r *http.Request) {
roles := httpmw.UserRoles(r)
roles := httpmw.AuthorizationUserRoles(r)
require.ElementsMatch(t, user.ID, roles.ID)
require.ElementsMatch(t, expRoles, roles.Roles)
})
Expand Down
4 changes: 3 additions & 1 deletion coderd/members.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) {
return
}

added, removed := rbac.ChangeRoleSet(member.Roles, params.Roles)
// The org-member role is always implied.
impliedTypes := append(params.Roles, rbac.RoleOrgMember(organization.ID))
added, removed := rbac.ChangeRoleSet(member.Roles, impliedTypes)
for _, roleName := range added {
// Assigning a role requires the create permission.
if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceOrgRoleAssignment.WithID(roleName).InOrg(organization.ID)) {
Expand Down
4 changes: 1 addition & 3 deletions coderd/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,11 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) {
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Roles: []string{
// Also assign member role incase they get demoted from admin
rbac.RoleOrgMember(organization.ID),
rbac.RoleOrgAdmin(organization.ID),
},
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
return xerrors.Errorf("create organization admin: %w", err)
}
return nil
})
Expand Down
4 changes: 2 additions & 2 deletions coderd/rbac/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ var (
member: func(_ string) Role {
return Role{
Name: member,
DisplayName: "Member",
DisplayName: "",
Site: permissions(map[Object][]Action{
// All users can read all other users and know they exist.
ResourceUser: {ActionRead},
Expand Down Expand Up @@ -116,7 +116,7 @@ var (
orgMember: func(organizationID string) Role {
return Role{
Name: roleName(orgMember, organizationID),
DisplayName: "Organization Member",
DisplayName: "",
Org: map[string][]Permission{
organizationID: {
{
Expand Down
4 changes: 3 additions & 1 deletion coderd/rbac/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ type Permission struct {
// Users of this package should instead **only** use the role names, and
// this package will expand the role names into their json payloads.
type Role struct {
Name string `json:"name"`
Name string `json:"name"`
// DisplayName is used for UI purposes. If the role has no display name,
// that means the UI should never display it.
DisplayName string `json:"display_name"`
Site []Permission `json:"site"`
// Org is a map of orgid to permissions. We represent orgid as a string.
Expand Down
6 changes: 5 additions & 1 deletion coderd/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (api *API) checkPermissions(rw http.ResponseWriter, r *http.Request) {
}

// use the roles of the user specified, not the person making the request.
roles, err := api.Database.GetAllUserRoles(r.Context(), user.ID)
roles, err := api.Database.GetAuthorizationUserRoles(r.Context(), user.ID)
if err != nil {
httpapi.Forbidden(rw)
return
Expand Down Expand Up @@ -91,6 +91,10 @@ func convertRole(role rbac.Role) codersdk.Role {
func convertRoles(roles []rbac.Role) []codersdk.Role {
converted := make([]codersdk.Role, 0, len(roles))
for _, role := range roles {
// Roles without display names should never be shown to the ui.
if role.DisplayName == "" {
continue
}
converted = append(converted, convertRole(role))
}
return converted
Expand Down

0 comments on commit cc87a0c

Please sign in to comment.