Skip to content

feat: Implied 'member' roles for site and organization #1917

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 1, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: Member roles are implied and never exlpicitly added
  • Loading branch information
Emyrk committed May 31, 2022
commit 501e581a877779c2af1838e99500e675779fb82a
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
2 changes: 2 additions & 0 deletions coderd/database/databasefake/databasefake.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,6 +295,7 @@ 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())
}
}

Expand Down
14 changes: 10 additions & 4 deletions coderd/database/queries/users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,16 @@ WHERE

-- name: GetAllUserRoles :one
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
12 changes: 12 additions & 0 deletions coderd/httpmw/apikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ func APIKey(r *http.Request) database.APIKey {
return apiKey
}

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

// UserRoles returns the API key from the ExtractUserRoles handler.
func UserRoles(r *http.Request) database.GetAllUserRolesRow {
apiKey, ok := r.Context().Value(userRolesKey{}).(database.GetAllUserRolesRow)
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
40 changes: 0 additions & 40 deletions coderd/httpmw/authorize.go

This file was deleted.

14 changes: 7 additions & 7 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 Down
4 changes: 3 additions & 1 deletion coderd/members.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,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
2 changes: 0 additions & 2 deletions coderd/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,6 @@ 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),
},
})
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"`
Comment on lines +20 to 23
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just not send this role to the UI at all? I think the whole org-member: thing is just the Rego leaking out a bit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't send it to the UI. I just use the DisplayName to indicate that.

Pretty much in the current form all role lists are compiled from the builtin const. So that list needs to be filtered for the UI. I thought it was easy to just use this field, since it has no purpose for the member roles now

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah gotcha.

Site []Permission `json:"site"`
// Org is a map of orgid to permissions. We represent orgid as a string.
Expand Down
10 changes: 5 additions & 5 deletions coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
// and add some rbac bypass when calling api functions this way??
// Add the admin role to this first user.
_, err = api.Database.UpdateUserRoles(r.Context(), database.UpdateUserRolesParams{
GrantedRoles: []string{rbac.RoleAdmin(), rbac.RoleMember()},
GrantedRoles: []string{rbac.RoleAdmin()},
ID: user.ID,
})
if err != nil {
Expand Down Expand Up @@ -480,7 +480,9 @@ func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) {
return
}

added, removed := rbac.ChangeRoleSet(roles.Roles, params.Roles)
// The member role is always implied.
impliedTypes := append(params.Roles, rbac.RoleMember())
added, removed := rbac.ChangeRoleSet(roles.Roles, impliedTypes)
for _, roleName := range added {
// Assigning a role requires the create permission.
if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceRoleAssignment.WithID(roleName)) {
Expand Down Expand Up @@ -764,8 +766,6 @@ func (api *API) createUser(ctx context.Context, req codersdk.CreateUserRequest)
req.OrganizationID = organization.ID
orgRoles = append(orgRoles, rbac.RoleOrgAdmin(req.OrganizationID))
}
// Always also be a member.
orgRoles = append(orgRoles, rbac.RoleOrgMember(req.OrganizationID))

params := database.InsertUserParams{
ID: uuid.New(),
Expand All @@ -774,7 +774,7 @@ func (api *API) createUser(ctx context.Context, req codersdk.CreateUserRequest)
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
// All new users are defaulted to members of the site.
RBACRoles: []string{rbac.RoleMember()},
RBACRoles: []string{},
}
// If a user signs up with OAuth, they can have no password!
if req.Password != "" {
Expand Down
26 changes: 9 additions & 17 deletions coderd/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,43 +416,43 @@ func TestGrantRoles(t *testing.T) {
require.NoError(t, err, "member user")

_, err = admin.UpdateUserRoles(ctx, codersdk.Me, codersdk.UpdateRoles{
Roles: []string{rbac.RoleOrgMember(first.OrganizationID)},
Roles: []string{rbac.RoleOrgAdmin(first.OrganizationID)},
})
require.Error(t, err, "org role in site")
requireStatusCode(t, err, http.StatusBadRequest)

_, err = admin.UpdateUserRoles(ctx, uuid.New().String(), codersdk.UpdateRoles{
Roles: []string{rbac.RoleOrgMember(first.OrganizationID)},
Roles: []string{rbac.RoleOrgAdmin(first.OrganizationID)},
})
require.Error(t, err, "user does not exist")
requireStatusCode(t, err, http.StatusBadRequest)

_, err = admin.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, codersdk.Me, codersdk.UpdateRoles{
Roles: []string{rbac.RoleMember()},
Roles: []string{rbac.RoleAdmin()},
})
require.Error(t, err, "site role in org")
requireStatusCode(t, err, http.StatusBadRequest)

_, err = admin.UpdateOrganizationMemberRoles(ctx, uuid.New(), codersdk.Me, codersdk.UpdateRoles{
Roles: []string{rbac.RoleMember()},
Roles: []string{},
})
require.Error(t, err, "role in org without membership")
requireStatusCode(t, err, http.StatusNotFound)

_, err = member.UpdateUserRoles(ctx, first.UserID.String(), codersdk.UpdateRoles{
Roles: []string{rbac.RoleMember()},
Roles: []string{},
})
require.Error(t, err, "member cannot change other's roles")
requireStatusCode(t, err, http.StatusForbidden)

_, err = member.UpdateUserRoles(ctx, memberUser.ID.String(), codersdk.UpdateRoles{
Roles: []string{rbac.RoleMember()},
Roles: []string{},
})
require.Error(t, err, "member cannot change any roles")
requireStatusCode(t, err, http.StatusForbidden)

_, err = member.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, first.UserID.String(), codersdk.UpdateRoles{
Roles: []string{rbac.RoleMember()},
Roles: []string{},
})
require.Error(t, err, "member cannot change other's org roles")
requireStatusCode(t, err, http.StatusForbidden)
Expand All @@ -468,11 +468,9 @@ func TestGrantRoles(t *testing.T) {
require.NoError(t, err)
require.ElementsMatch(t, roles.Roles, []string{
rbac.RoleAdmin(),
rbac.RoleMember(),
}, "should be a member and admin")

require.ElementsMatch(t, roles.OrganizationRoles[first.OrganizationID], []string{
rbac.RoleOrgMember(first.OrganizationID),
rbac.RoleOrgAdmin(first.OrganizationID),
}, "should be a member and admin")
})
Expand All @@ -486,12 +484,10 @@ func TestGrantRoles(t *testing.T) {
member := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID)
roles, err := member.GetUserRoles(ctx, codersdk.Me)
require.NoError(t, err)
require.ElementsMatch(t, roles.Roles, []string{
rbac.RoleMember(),
}, "should be a member and admin")
require.ElementsMatch(t, roles.Roles, []string{}, "should be a member")
require.ElementsMatch(t,
roles.OrganizationRoles[first.OrganizationID],
[]string{rbac.RoleOrgMember(first.OrganizationID)},
[]string{},
)

memberUser, err := member.User(ctx, codersdk.Me)
Expand All @@ -501,7 +497,6 @@ func TestGrantRoles(t *testing.T) {
_, err = admin.UpdateUserRoles(ctx, memberUser.ID.String(), codersdk.UpdateRoles{
Roles: []string{
// Promote to site admin
rbac.RoleMember(),
rbac.RoleAdmin(),
},
})
Expand All @@ -511,7 +506,6 @@ func TestGrantRoles(t *testing.T) {
_, err = member.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, codersdk.Me, codersdk.UpdateRoles{
Roles: []string{
// Promote to org admin
rbac.RoleOrgMember(first.OrganizationID),
rbac.RoleOrgAdmin(first.OrganizationID),
},
})
Expand All @@ -520,12 +514,10 @@ func TestGrantRoles(t *testing.T) {
roles, err = member.GetUserRoles(ctx, codersdk.Me)
require.NoError(t, err)
require.ElementsMatch(t, roles.Roles, []string{
rbac.RoleMember(),
rbac.RoleAdmin(),
}, "should be a member and admin")

require.ElementsMatch(t, roles.OrganizationRoles[first.OrganizationID], []string{
rbac.RoleOrgMember(first.OrganizationID),
rbac.RoleOrgAdmin(first.OrganizationID),
}, "should be a member and admin")
})
Expand Down
2 changes: 1 addition & 1 deletion coderd/workspaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func TestAdminViewAllWorkspaces(t *testing.T) {

// This other user is not in the first user's org. Since other is an admin, they can
// still see the "first" user's workspace.
other := coderdtest.CreateAnotherUser(t, client, otherOrg.ID, rbac.RoleAdmin(), rbac.RoleMember())
other := coderdtest.CreateAnotherUser(t, client, otherOrg.ID, rbac.RoleAdmin())
otherWorkspaces, err := other.Workspaces(context.Background(), codersdk.WorkspaceFilter{})
require.NoError(t, err, "(other) fetch workspaces")

Expand Down
2 changes: 1 addition & 1 deletion codersdk/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ type UpdateUserPasswordRequest struct {
}

type UpdateRoles struct {
Roles []string `json:"roles" validate:"required"`
Roles []string `json:"roles" validate:""`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does sending { "roles": [] } now mean "remove all roles assigned to the user but keep their membership"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The UI and user will never see the member role. That role is only fetched for authorization.

}

type UserRoles struct {
Expand Down