mirror of https://github.com/coder/coder.git
feat: Implied 'member' roles for site and organization (#1917)
* 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
This commit is contained in:
parent
2878346f19
commit
cc87a0cf6b
|
@ -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{
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -22,10 +22,12 @@ type querier interface {
|
|||
DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error
|
||||
DeleteParameterValueByID(ctx context.Context, id uuid.UUID) error
|
||||
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
|
||||
GetAllUserRoles(ctx context.Context, userID uuid.UUID) (GetAllUserRolesRow, error)
|
||||
// GetAuditLogsBefore retrieves `limit` number of audit logs before the provided
|
||||
// ID.
|
||||
GetAuditLogsBefore(ctx context.Context, arg GetAuditLogsBeforeParams) ([]AuditLog, error)
|
||||
// This function returns roles for authorization purposes. Implied member roles
|
||||
// are included.
|
||||
GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error)
|
||||
GetFileByHash(ctx context.Context, hash string) (File, error)
|
||||
GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error)
|
||||
GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error)
|
||||
|
|
|
@ -2088,12 +2088,18 @@ func (q *sqlQuerier) UpdateTemplateVersionDescriptionByJobID(ctx context.Context
|
|||
return err
|
||||
}
|
||||
|
||||
const getAllUserRoles = `-- name: GetAllUserRoles :one
|
||||
const getAuthorizationUserRoles = `-- name: GetAuthorizationUserRoles :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
|
||||
|
@ -2102,16 +2108,18 @@ WHERE
|
|||
id = $1
|
||||
`
|
||||
|
||||
type GetAllUserRolesRow struct {
|
||||
type GetAuthorizationUserRolesRow struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Username string `db:"username" json:"username"`
|
||||
Status UserStatus `db:"status" json:"status"`
|
||||
Roles []string `db:"roles" json:"roles"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetAllUserRoles(ctx context.Context, userID uuid.UUID) (GetAllUserRolesRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getAllUserRoles, userID)
|
||||
var i GetAllUserRolesRow
|
||||
// This function returns roles for authorization purposes. Implied member roles
|
||||
// are included.
|
||||
func (q *sqlQuerier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getAuthorizationUserRoles, userID)
|
||||
var i GetAuthorizationUserRolesRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
package httpmw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// ExtractUserRoles requires authentication using a valid API key.
|
||||
func ExtractUserRoles(db database.Store) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
apiKey := APIKey(r)
|
||||
role, err := db.GetAllUserRoles(r.Context(), apiKey.UserID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: "roles not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), userRolesKey{}, role)
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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},
|
||||
|
@ -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: {
|
||||
{
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
|
|
|
@ -113,6 +113,8 @@ func TestListRoles(t *testing.T) {
|
|||
require.NoError(t, err, "create org")
|
||||
|
||||
const forbidden = "forbidden"
|
||||
siteRoles := convertRoles(rbac.RoleAdmin(), "auditor")
|
||||
orgRoles := convertRoles(rbac.RoleOrgAdmin(admin.OrganizationID))
|
||||
|
||||
testCases := []struct {
|
||||
Name string
|
||||
|
@ -127,14 +129,14 @@ func TestListRoles(t *testing.T) {
|
|||
x, err := member.ListSiteRoles(ctx)
|
||||
return x, err
|
||||
},
|
||||
ExpectedRoles: convertRoles(rbac.SiteRoles()),
|
||||
ExpectedRoles: siteRoles,
|
||||
},
|
||||
{
|
||||
Name: "OrgMemberListOrg",
|
||||
APICall: func() ([]codersdk.Role, error) {
|
||||
return member.ListOrganizationRoles(ctx, admin.OrganizationID)
|
||||
},
|
||||
ExpectedRoles: convertRoles(rbac.OrganizationRoles(admin.OrganizationID)),
|
||||
ExpectedRoles: orgRoles,
|
||||
},
|
||||
{
|
||||
Name: "NonOrgMemberListOrg",
|
||||
|
@ -149,14 +151,14 @@ func TestListRoles(t *testing.T) {
|
|||
APICall: func() ([]codersdk.Role, error) {
|
||||
return orgAdmin.ListSiteRoles(ctx)
|
||||
},
|
||||
ExpectedRoles: convertRoles(rbac.SiteRoles()),
|
||||
ExpectedRoles: siteRoles,
|
||||
},
|
||||
{
|
||||
Name: "OrgAdminListOrg",
|
||||
APICall: func() ([]codersdk.Role, error) {
|
||||
return orgAdmin.ListOrganizationRoles(ctx, admin.OrganizationID)
|
||||
},
|
||||
ExpectedRoles: convertRoles(rbac.OrganizationRoles(admin.OrganizationID)),
|
||||
ExpectedRoles: orgRoles,
|
||||
},
|
||||
{
|
||||
Name: "OrgAdminListOtherOrg",
|
||||
|
@ -171,14 +173,14 @@ func TestListRoles(t *testing.T) {
|
|||
APICall: func() ([]codersdk.Role, error) {
|
||||
return client.ListSiteRoles(ctx)
|
||||
},
|
||||
ExpectedRoles: convertRoles(rbac.SiteRoles()),
|
||||
ExpectedRoles: siteRoles,
|
||||
},
|
||||
{
|
||||
Name: "AdminListOrg",
|
||||
APICall: func() ([]codersdk.Role, error) {
|
||||
return client.ListOrganizationRoles(ctx, admin.OrganizationID)
|
||||
},
|
||||
ExpectedRoles: convertRoles(rbac.OrganizationRoles(admin.OrganizationID)),
|
||||
ExpectedRoles: orgRoles,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -200,17 +202,18 @@ func TestListRoles(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func convertRole(role rbac.Role) codersdk.Role {
|
||||
func convertRole(roleName string) codersdk.Role {
|
||||
role, _ := rbac.RoleByName(roleName)
|
||||
return codersdk.Role{
|
||||
DisplayName: role.DisplayName,
|
||||
Name: role.Name,
|
||||
}
|
||||
}
|
||||
|
||||
func convertRoles(roles []rbac.Role) []codersdk.Role {
|
||||
converted := make([]codersdk.Role, 0, len(roles))
|
||||
for _, role := range roles {
|
||||
converted = append(converted, convertRole(role))
|
||||
func convertRoles(roleNames ...string) []codersdk.Role {
|
||||
converted := make([]codersdk.Role, 0, len(roleNames))
|
||||
for _, roleName := range roleNames {
|
||||
converted = append(converted, convertRole(roleName))
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
@ -473,7 +473,7 @@ func (api *API) userRoles(rw http.ResponseWriter, r *http.Request) {
|
|||
func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
// User is the user to modify.
|
||||
user := httpmw.UserParam(r)
|
||||
roles := httpmw.UserRoles(r)
|
||||
roles := httpmw.AuthorizationUserRoles(r)
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
if apiKey.UserID == user.ID {
|
||||
|
@ -488,7 +488,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)) {
|
||||
|
@ -757,7 +759,7 @@ func (api *API) createAPIKey(rw http.ResponseWriter, r *http.Request, params dat
|
|||
func (api *API) createUser(ctx context.Context, req codersdk.CreateUserRequest) (database.User, uuid.UUID, error) {
|
||||
var user database.User
|
||||
return user, req.OrganizationID, api.Database.InTx(func(db database.Store) error {
|
||||
var orgRoles []string
|
||||
orgRoles := make([]string, 0)
|
||||
// If no organization is provided, create a new one for the user.
|
||||
if req.OrganizationID == uuid.Nil {
|
||||
organization, err := db.InsertOrganization(ctx, database.InsertOrganizationParams{
|
||||
|
@ -772,8 +774,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(),
|
||||
|
@ -782,7 +782,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 != "" {
|
||||
|
|
|
@ -416,43 +416,43 @@ func TestGrantRoles(t *testing.T) {
|
|||
member := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID)
|
||||
|
||||
_, 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, first.UserID.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)
|
||||
|
@ -480,11 +480,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")
|
||||
})
|
||||
|
@ -498,12 +496,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)
|
||||
|
@ -513,7 +509,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(),
|
||||
},
|
||||
})
|
||||
|
@ -523,7 +518,6 @@ func TestGrantRoles(t *testing.T) {
|
|||
_, err = admin.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, memberUser.ID.String(), codersdk.UpdateRoles{
|
||||
Roles: []string{
|
||||
// Promote to org admin
|
||||
rbac.RoleOrgMember(first.OrganizationID),
|
||||
rbac.RoleOrgAdmin(first.OrganizationID),
|
||||
},
|
||||
})
|
||||
|
@ -532,12 +526,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")
|
||||
})
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ type UpdateUserPasswordRequest struct {
|
|||
}
|
||||
|
||||
type UpdateRoles struct {
|
||||
Roles []string `json:"roles" validate:"required"`
|
||||
Roles []string `json:"roles" validate:""`
|
||||
}
|
||||
|
||||
type UserRoles struct {
|
||||
|
|
Loading…
Reference in New Issue