mirror of https://github.com/coder/coder.git
feat: Add user roles, but do not yet enforce them (#1200)
* chore: Rework roles to be expandable by name alone
This commit is contained in:
parent
ba4c3ce3b9
commit
35211e2190
|
@ -42,6 +42,7 @@ var AuditableResources = auditMap(map[any]map[string]Action{
|
|||
"created_at": ActionIgnore, // Never changes.
|
||||
"updated_at": ActionIgnore, // Changes, but is implicit and not helpful in a diff.
|
||||
"status": ActionTrack, // A user can update another user status
|
||||
"rbac_roles": ActionTrack, // A user's roles are mutable
|
||||
},
|
||||
&database.Workspace{}: {
|
||||
"id": ActionIgnore, // Never changes.
|
||||
|
|
|
@ -120,6 +120,14 @@ func New(options *Options) (http.Handler, func()) {
|
|||
r.Get("/", api.workspacesByOwner)
|
||||
})
|
||||
})
|
||||
r.Route("/members", func(r chi.Router) {
|
||||
r.Route("/{user}", func(r chi.Router) {
|
||||
r.Use(
|
||||
httpmw.ExtractUserParam(options.Database),
|
||||
)
|
||||
r.Put("/roles", api.putMemberRoles)
|
||||
})
|
||||
})
|
||||
})
|
||||
r.Route("/parameters/{scope}/{id}", func(r chi.Router) {
|
||||
r.Use(apiKeyMiddleware)
|
||||
|
@ -183,6 +191,10 @@ func New(options *Options) (http.Handler, func()) {
|
|||
r.Get("/", api.userByName)
|
||||
r.Put("/profile", api.putUserProfile)
|
||||
r.Put("/suspend", api.putUserSuspend)
|
||||
// TODO: @emyrk Might want to move these to a /roles group instead of /user.
|
||||
// As we include more roles like org roles, it makes less sense to scope these here.
|
||||
r.Put("/roles", api.putUserRoles)
|
||||
r.Get("/roles", api.userRoles)
|
||||
r.Get("/organizations", api.organizationsByUser)
|
||||
r.Post("/organizations", api.postOrganizationsByUser)
|
||||
r.Post("/keys", api.postAPIKey)
|
||||
|
|
|
@ -743,6 +743,43 @@ func (q *fakeQuerier) GetOrganizationIDsByMemberIDs(_ context.Context, ids []uui
|
|||
return getOrganizationIDsByMemberIDRows, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetOrganizationMembershipsByUserID(_ context.Context, userID uuid.UUID) ([]database.OrganizationMember, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
var memberships []database.OrganizationMember
|
||||
for _, organizationMember := range q.organizationMembers {
|
||||
mem := organizationMember
|
||||
if mem.UserID != userID {
|
||||
continue
|
||||
}
|
||||
memberships = append(memberships, mem)
|
||||
}
|
||||
return memberships, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateMemberRoles(_ context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
|
||||
for i, mem := range q.organizationMembers {
|
||||
if mem.UserID == arg.UserID && mem.OrganizationID == arg.OrgID {
|
||||
uniqueRoles := make([]string, 0, len(arg.GrantedRoles))
|
||||
exist := make(map[string]struct{})
|
||||
for _, r := range arg.GrantedRoles {
|
||||
if _, ok := exist[r]; ok {
|
||||
continue
|
||||
}
|
||||
exist[r] = struct{}{}
|
||||
uniqueRoles = append(uniqueRoles, r)
|
||||
}
|
||||
sort.Strings(uniqueRoles)
|
||||
|
||||
mem.Roles = uniqueRoles
|
||||
q.organizationMembers[i] = mem
|
||||
return mem, nil
|
||||
}
|
||||
}
|
||||
return database.OrganizationMember{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetProvisionerDaemons(_ context.Context) ([]database.ProvisionerDaemon, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
@ -1173,11 +1210,42 @@ func (q *fakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam
|
|||
UpdatedAt: arg.UpdatedAt,
|
||||
Username: arg.Username,
|
||||
Status: database.UserStatusActive,
|
||||
RBACRoles: arg.RBACRoles,
|
||||
}
|
||||
q.users = append(q.users, user)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateUserRoles(_ context.Context, arg database.UpdateUserRolesParams) (database.User, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
||||
for index, user := range q.users {
|
||||
if user.ID != arg.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
// Set new roles
|
||||
user.RBACRoles = arg.GrantedRoles
|
||||
// Remove duplicates and sort
|
||||
uniqueRoles := make([]string, 0, len(user.RBACRoles))
|
||||
exist := make(map[string]struct{})
|
||||
for _, r := range user.RBACRoles {
|
||||
if _, ok := exist[r]; ok {
|
||||
continue
|
||||
}
|
||||
exist[r] = struct{}{}
|
||||
uniqueRoles = append(uniqueRoles, r)
|
||||
}
|
||||
sort.Strings(uniqueRoles)
|
||||
user.RBACRoles = uniqueRoles
|
||||
|
||||
q.users[index] = user
|
||||
return user, nil
|
||||
}
|
||||
return database.User{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) UpdateUserProfile(_ context.Context, arg database.UpdateUserProfileParams) (database.User, error) {
|
||||
q.mutex.Lock()
|
||||
defer q.mutex.Unlock()
|
||||
|
|
|
@ -227,7 +227,8 @@ CREATE TABLE users (
|
|||
hashed_password bytea NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
status user_status DEFAULT 'active'::public.user_status NOT NULL
|
||||
status user_status DEFAULT 'active'::public.user_status NOT NULL,
|
||||
rbac_roles text[] DEFAULT '{}'::text[] NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE workspace_agents (
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE ONLY users
|
||||
DROP COLUMN IF EXISTS rbac_roles;
|
|
@ -0,0 +1,18 @@
|
|||
ALTER TABLE ONLY users
|
||||
ADD COLUMN IF NOT EXISTS rbac_roles text[] DEFAULT '{}' NOT NULL;
|
||||
|
||||
-- All users are site members. So give them the standard role.
|
||||
-- Also give them membership to the first org we retrieve. We should only have
|
||||
-- 1 organization at this point in the product.
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
rbac_roles = ARRAY ['member', 'organization-member:' || (SELECT id FROM organizations LIMIT 1)];
|
||||
|
||||
-- Give the first user created the admin role
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
rbac_roles = rbac_roles || ARRAY ['admin']
|
||||
WHERE
|
||||
id = (SELECT id FROM users ORDER BY created_at ASC LIMIT 1)
|
|
@ -398,6 +398,7 @@ type User struct {
|
|||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Status UserStatus `db:"status" json:"status"`
|
||||
RBACRoles []string `db:"rbac_roles" json:"rbac_roles"`
|
||||
}
|
||||
|
||||
type Workspace struct {
|
||||
|
|
|
@ -19,6 +19,7 @@ type querier interface {
|
|||
GetOrganizationByName(ctx context.Context, name string) (Organization, error)
|
||||
GetOrganizationIDsByMemberIDs(ctx context.Context, ids []uuid.UUID) ([]GetOrganizationIDsByMemberIDsRow, error)
|
||||
GetOrganizationMemberByUserID(ctx context.Context, arg GetOrganizationMemberByUserIDParams) (OrganizationMember, error)
|
||||
GetOrganizationMembershipsByUserID(ctx context.Context, userID uuid.UUID) ([]OrganizationMember, error)
|
||||
GetOrganizations(ctx context.Context) ([]Organization, error)
|
||||
GetOrganizationsByUserID(ctx context.Context, userID uuid.UUID) ([]Organization, error)
|
||||
GetParameterSchemasByJobID(ctx context.Context, jobID uuid.UUID) ([]ParameterSchema, error)
|
||||
|
@ -78,6 +79,7 @@ type querier interface {
|
|||
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
|
||||
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
|
||||
UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) error
|
||||
UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error)
|
||||
UpdateProvisionerDaemonByID(ctx context.Context, arg UpdateProvisionerDaemonByIDParams) error
|
||||
UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error
|
||||
UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error
|
||||
|
@ -86,6 +88,7 @@ type querier interface {
|
|||
UpdateTemplateDeletedByID(ctx context.Context, arg UpdateTemplateDeletedByIDParams) error
|
||||
UpdateTemplateVersionByID(ctx context.Context, arg UpdateTemplateVersionByIDParams) error
|
||||
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error)
|
||||
UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error)
|
||||
UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error)
|
||||
UpdateWorkspaceAgentConnectionByID(ctx context.Context, arg UpdateWorkspaceAgentConnectionByIDParams) error
|
||||
UpdateWorkspaceAutostart(ctx context.Context, arg UpdateWorkspaceAutostartParams) error
|
||||
|
|
|
@ -375,6 +375,44 @@ func (q *sqlQuerier) GetOrganizationMemberByUserID(ctx context.Context, arg GetO
|
|||
return i, err
|
||||
}
|
||||
|
||||
const getOrganizationMembershipsByUserID = `-- name: GetOrganizationMembershipsByUserID :many
|
||||
SELECT
|
||||
user_id, organization_id, created_at, updated_at, roles
|
||||
FROM
|
||||
organization_members
|
||||
WHERE
|
||||
user_id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetOrganizationMembershipsByUserID(ctx context.Context, userID uuid.UUID) ([]OrganizationMember, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getOrganizationMembershipsByUserID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []OrganizationMember
|
||||
for rows.Next() {
|
||||
var i OrganizationMember
|
||||
if err := rows.Scan(
|
||||
&i.UserID,
|
||||
&i.OrganizationID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
pq.Array(&i.Roles),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const insertOrganizationMember = `-- name: InsertOrganizationMember :one
|
||||
INSERT INTO
|
||||
organization_members (
|
||||
|
@ -415,6 +453,37 @@ func (q *sqlQuerier) InsertOrganizationMember(ctx context.Context, arg InsertOrg
|
|||
return i, err
|
||||
}
|
||||
|
||||
const updateMemberRoles = `-- name: UpdateMemberRoles :one
|
||||
UPDATE
|
||||
organization_members
|
||||
SET
|
||||
-- Remove all duplicates from the roles.
|
||||
roles = ARRAY(SELECT DISTINCT UNNEST($1 :: text[]))
|
||||
WHERE
|
||||
user_id = $2
|
||||
AND organization_id = $3
|
||||
RETURNING user_id, organization_id, created_at, updated_at, roles
|
||||
`
|
||||
|
||||
type UpdateMemberRolesParams struct {
|
||||
GrantedRoles []string `db:"granted_roles" json:"granted_roles"`
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
OrgID uuid.UUID `db:"org_id" json:"org_id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateMemberRoles, pq.Array(arg.GrantedRoles), arg.UserID, arg.OrgID)
|
||||
var i OrganizationMember
|
||||
err := row.Scan(
|
||||
&i.UserID,
|
||||
&i.OrganizationID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
pq.Array(&i.Roles),
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getOrganizationByID = `-- name: GetOrganizationByID :one
|
||||
SELECT
|
||||
id, name, description, created_at, updated_at
|
||||
|
@ -1821,7 +1890,7 @@ func (q *sqlQuerier) UpdateTemplateVersionByID(ctx context.Context, arg UpdateTe
|
|||
|
||||
const getUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one
|
||||
SELECT
|
||||
id, email, username, hashed_password, created_at, updated_at, status
|
||||
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
|
@ -1847,13 +1916,14 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
pq.Array(&i.RBACRoles),
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT
|
||||
id, email, username, hashed_password, created_at, updated_at, status
|
||||
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
|
@ -1873,6 +1943,7 @@ func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
pq.Array(&i.RBACRoles),
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -1893,7 +1964,7 @@ func (q *sqlQuerier) GetUserCount(ctx context.Context) (int64, error) {
|
|||
|
||||
const getUsers = `-- name: GetUsers :many
|
||||
SELECT
|
||||
id, email, username, hashed_password, created_at, updated_at, status
|
||||
id, email, username, hashed_password, created_at, updated_at, status, rbac_roles
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
|
@ -1978,6 +2049,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]User,
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
pq.Array(&i.RBACRoles),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -2000,10 +2072,11 @@ INSERT INTO
|
|||
username,
|
||||
hashed_password,
|
||||
created_at,
|
||||
updated_at
|
||||
updated_at,
|
||||
rbac_roles
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6) RETURNING id, email, username, hashed_password, created_at, updated_at, status
|
||||
($1, $2, $3, $4, $5, $6, $7) RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles
|
||||
`
|
||||
|
||||
type InsertUserParams struct {
|
||||
|
@ -2013,6 +2086,7 @@ type InsertUserParams struct {
|
|||
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
RBACRoles []string `db:"rbac_roles" json:"rbac_roles"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) {
|
||||
|
@ -2023,6 +2097,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
|
|||
arg.HashedPassword,
|
||||
arg.CreatedAt,
|
||||
arg.UpdatedAt,
|
||||
pq.Array(arg.RBACRoles),
|
||||
)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
|
@ -2033,6 +2108,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
pq.Array(&i.RBACRoles),
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -2045,7 +2121,7 @@ SET
|
|||
username = $3,
|
||||
updated_at = $4
|
||||
WHERE
|
||||
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status
|
||||
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles
|
||||
`
|
||||
|
||||
type UpdateUserProfileParams struct {
|
||||
|
@ -2071,6 +2147,39 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
pq.Array(&i.RBACRoles),
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateUserRoles = `-- name: UpdateUserRoles :one
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
-- Remove all duplicates from the roles.
|
||||
rbac_roles = ARRAY(SELECT DISTINCT UNNEST($1 :: text[]))
|
||||
WHERE
|
||||
id = $2
|
||||
RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles
|
||||
`
|
||||
|
||||
type UpdateUserRolesParams struct {
|
||||
GrantedRoles []string `db:"granted_roles" json:"granted_roles"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateUserRoles, pq.Array(arg.GrantedRoles), arg.ID)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.HashedPassword,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
pq.Array(&i.RBACRoles),
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -2082,7 +2191,7 @@ SET
|
|||
status = $2,
|
||||
updated_at = $3
|
||||
WHERE
|
||||
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status
|
||||
id = $1 RETURNING id, email, username, hashed_password, created_at, updated_at, status, rbac_roles
|
||||
`
|
||||
|
||||
type UpdateUserStatusParams struct {
|
||||
|
@ -2102,6 +2211,7 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
|
|||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Status,
|
||||
pq.Array(&i.RBACRoles),
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
@ -21,6 +21,15 @@ INSERT INTO
|
|||
VALUES
|
||||
($1, $2, $3, $4, $5) RETURNING *;
|
||||
|
||||
|
||||
-- name: GetOrganizationMembershipsByUserID :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
organization_members
|
||||
WHERE
|
||||
user_id = $1;
|
||||
|
||||
-- name: GetOrganizationIDsByMemberIDs :many
|
||||
SELECT
|
||||
user_id, array_agg(organization_id) :: uuid [ ] AS "organization_IDs"
|
||||
|
@ -30,3 +39,14 @@ WHERE
|
|||
user_id = ANY(@ids :: uuid [ ])
|
||||
GROUP BY
|
||||
user_id;
|
||||
|
||||
-- name: UpdateMemberRoles :one
|
||||
UPDATE
|
||||
organization_members
|
||||
SET
|
||||
-- Remove all duplicates from the roles.
|
||||
roles = ARRAY(SELECT DISTINCT UNNEST(@granted_roles :: text[]))
|
||||
WHERE
|
||||
user_id = @user_id
|
||||
AND organization_id = @org_id
|
||||
RETURNING *;
|
|
@ -33,10 +33,11 @@ INSERT INTO
|
|||
username,
|
||||
hashed_password,
|
||||
created_at,
|
||||
updated_at
|
||||
updated_at,
|
||||
rbac_roles
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6) RETURNING *;
|
||||
($1, $2, $3, $4, $5, $6, $7) RETURNING *;
|
||||
|
||||
-- name: UpdateUserProfile :one
|
||||
UPDATE
|
||||
|
@ -48,6 +49,16 @@ SET
|
|||
WHERE
|
||||
id = $1 RETURNING *;
|
||||
|
||||
-- name: UpdateUserRoles :one
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
-- Remove all duplicates from the roles.
|
||||
rbac_roles = ARRAY(SELECT DISTINCT UNNEST(@granted_roles :: text[]))
|
||||
WHERE
|
||||
id = @id
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetUsers :many
|
||||
SELECT
|
||||
*
|
||||
|
|
|
@ -28,3 +28,4 @@ rename:
|
|||
parameter_type_system_hcl: ParameterTypeSystemHCL
|
||||
userstatus: UserStatus
|
||||
gitsshkey: GitSSHKey
|
||||
rbac_roles: RBACRoles
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
package coderd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func (api *api) putMemberRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
// User is the user to modify
|
||||
// TODO: Until rbac authorize is implemented, only be able to change your
|
||||
// own roles. This also means you can grant yourself whatever roles you want.
|
||||
user := httpmw.UserParam(r)
|
||||
apiKey := httpmw.APIKey(r)
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
// TODO: @emyrk add proper `Authorize()` check here instead of a uuid match.
|
||||
// Proper authorize should check the granted roles are able to given within
|
||||
// the selected organization. Until then, allow anarchy
|
||||
if apiKey.UserID != user.ID {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("modifying other users is not supported at this time"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var params codersdk.UpdateRoles
|
||||
if !httpapi.Read(rw, r, ¶ms) {
|
||||
return
|
||||
}
|
||||
|
||||
updatedUser, err := api.updateOrganizationMemberRoles(r.Context(), database.UpdateMemberRolesParams{
|
||||
GrantedRoles: params.Roles,
|
||||
UserID: user.ID,
|
||||
OrgID: organization.ID,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, convertOrganizationMember(updatedUser))
|
||||
}
|
||||
|
||||
func (api *api) updateOrganizationMemberRoles(ctx context.Context, args database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
|
||||
// Enforce only site wide roles
|
||||
for _, r := range args.GrantedRoles {
|
||||
// Must be an org role for the org in the args
|
||||
orgID, ok := rbac.IsOrgRole(r)
|
||||
if !ok {
|
||||
return database.OrganizationMember{}, xerrors.Errorf("must only update organization roles")
|
||||
}
|
||||
|
||||
roleOrg, err := uuid.Parse(orgID)
|
||||
if err != nil {
|
||||
return database.OrganizationMember{}, xerrors.Errorf("role must have proper uuids for organization, %q does not", r)
|
||||
}
|
||||
|
||||
if roleOrg != args.OrgID {
|
||||
return database.OrganizationMember{}, xerrors.Errorf("must only pass roles for org %q", args.OrgID.String())
|
||||
}
|
||||
|
||||
if _, err := rbac.RoleByName(r); err != nil {
|
||||
return database.OrganizationMember{}, xerrors.Errorf("%q is not a supported role", r)
|
||||
}
|
||||
}
|
||||
|
||||
updatedUser, err := api.Database.UpdateMemberRoles(ctx, args)
|
||||
if err != nil {
|
||||
return database.OrganizationMember{}, xerrors.Errorf("update site roles: %w", err)
|
||||
}
|
||||
return updatedUser, nil
|
||||
}
|
||||
|
||||
func convertOrganizationMember(mem database.OrganizationMember) codersdk.OrganizationMember {
|
||||
return codersdk.OrganizationMember{
|
||||
UserID: mem.UserID,
|
||||
OrganizationID: mem.OrganizationID,
|
||||
CreatedAt: mem.CreatedAt,
|
||||
UpdatedAt: mem.UpdatedAt,
|
||||
Roles: mem.Roles,
|
||||
}
|
||||
}
|
|
@ -38,6 +38,23 @@ type authSubject struct {
|
|||
Roles []Role `json:"roles"`
|
||||
}
|
||||
|
||||
// AuthorizeByRoleName will expand all roleNames into roles before calling Authorize().
|
||||
// This is the function intended to be used outside this package.
|
||||
// The role is fetched from the builtin map located in memory.
|
||||
func (a RegoAuthorizer) AuthorizeByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, object Object) error {
|
||||
roles := make([]Role, 0, len(roleNames))
|
||||
for _, n := range roleNames {
|
||||
r, err := RoleByName(n)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get role permissions: %w", err)
|
||||
}
|
||||
roles = append(roles, r)
|
||||
}
|
||||
return a.Authorize(ctx, subjectID, roles, action, object)
|
||||
}
|
||||
|
||||
// Authorize allows passing in custom Roles.
|
||||
// This is really helpful for unit testing, as we can create custom roles to exercise edge cases.
|
||||
func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles []Role, action Action, object Object) error {
|
||||
input := map[string]interface{}{
|
||||
"subject": authSubject{
|
||||
|
|
|
@ -3,8 +3,11 @@ package rbac_test
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -14,19 +17,26 @@ import (
|
|||
|
||||
// subject is required because rego needs
|
||||
type subject struct {
|
||||
UserID string `json:"id"`
|
||||
Roles []rbac.Role `json:"roles"`
|
||||
UserID string `json:"id"`
|
||||
// For the unit test we want to pass in the roles directly, instead of just
|
||||
// by name. This allows us to test custom roles that do not exist in the product,
|
||||
// but test edge cases of the implementation.
|
||||
Roles []rbac.Role `json:"roles"`
|
||||
}
|
||||
|
||||
// TestAuthorizeDomain test the very basic roles that are commonly used.
|
||||
func TestAuthorizeDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
defOrg := "default"
|
||||
defOrg := uuid.New()
|
||||
unuseID := uuid.New()
|
||||
wrkID := "1234"
|
||||
|
||||
user := subject{
|
||||
UserID: "me",
|
||||
Roles: []rbac.Role{rbac.RoleMember, rbac.RoleOrgMember(defOrg)},
|
||||
Roles: []rbac.Role{
|
||||
must(rbac.RoleByName(rbac.RoleMember())),
|
||||
must(rbac.RoleByName(rbac.RoleOrgMember(defOrg))),
|
||||
},
|
||||
}
|
||||
|
||||
testAuthorize(t, "Member", user, []authTestCase{
|
||||
|
@ -44,10 +54,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.All(), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false},
|
||||
|
@ -57,10 +67,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
|
@ -99,10 +109,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.All(), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: false},
|
||||
|
@ -112,10 +122,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
|
@ -126,8 +136,8 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
user = subject{
|
||||
UserID: "me",
|
||||
Roles: []rbac.Role{
|
||||
rbac.RoleOrgAdmin(defOrg),
|
||||
rbac.RoleMember,
|
||||
must(rbac.RoleByName(rbac.RoleOrgAdmin(defOrg))),
|
||||
must(rbac.RoleByName(rbac.RoleMember())),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -146,10 +156,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.All(), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true},
|
||||
|
@ -159,10 +169,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
|
@ -173,8 +183,8 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
user = subject{
|
||||
UserID: "me",
|
||||
Roles: []rbac.Role{
|
||||
rbac.RoleAdmin,
|
||||
rbac.RoleMember,
|
||||
must(rbac.RoleByName(rbac.RoleAdmin())),
|
||||
must(rbac.RoleByName(rbac.RoleMember())),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -193,10 +203,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.All(), actions: allActions(), allow: true},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: true},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), actions: allActions(), allow: true},
|
||||
|
@ -206,10 +216,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: true},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: true},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), actions: allActions(), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: true},
|
||||
|
@ -221,7 +231,19 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
user = subject{
|
||||
UserID: "me",
|
||||
Roles: []rbac.Role{
|
||||
rbac.RoleWorkspaceAgent(wrkID),
|
||||
{
|
||||
Name: fmt.Sprintf("agent-%s", wrkID),
|
||||
// This is at the site level to prevent the token from losing access if the user
|
||||
// is kicked from the org
|
||||
Site: []rbac.Permission{
|
||||
{
|
||||
Negate: false,
|
||||
ResourceType: rbac.ResourceWorkspace.Type,
|
||||
ResourceID: wrkID,
|
||||
Action: rbac.ActionRead,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -245,10 +267,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.All(), allow: false},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID), allow: true},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), allow: false},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true},
|
||||
|
@ -258,10 +280,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), allow: false},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false},
|
||||
|
@ -288,10 +310,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.All()},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID)},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)},
|
||||
|
@ -301,10 +323,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.WithOwner("not-me")},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID)},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me")},
|
||||
|
@ -321,7 +343,7 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
Name: "ReadOnlyOrgAndUser",
|
||||
Site: []rbac.Permission{},
|
||||
Org: map[string][]rbac.Permission{
|
||||
defOrg: {{
|
||||
defOrg.String(): {{
|
||||
Negate: false,
|
||||
ResourceType: "*",
|
||||
ResourceID: "*",
|
||||
|
@ -360,10 +382,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.All(), allow: false},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), allow: false},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true},
|
||||
|
@ -373,10 +395,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID), allow: false},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false},
|
||||
|
@ -405,10 +427,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.All()},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner(user.UserID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID)},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)},
|
||||
|
@ -418,10 +440,10 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.WithOwner("not-me")},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithOwner("not-me")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID).WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unuseID)},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me")},
|
||||
|
@ -433,14 +455,27 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
// TestAuthorizeLevels ensures level overrides are acting appropriately
|
||||
//nolint:paralleltest
|
||||
func TestAuthorizeLevels(t *testing.T) {
|
||||
defOrg := "default"
|
||||
defOrg := uuid.New()
|
||||
unusedID := uuid.New()
|
||||
wrkID := "1234"
|
||||
|
||||
user := subject{
|
||||
UserID: "me",
|
||||
Roles: []rbac.Role{
|
||||
rbac.RoleAdmin,
|
||||
rbac.RoleOrgDenyAll(defOrg),
|
||||
must(rbac.RoleByName(rbac.RoleAdmin())),
|
||||
{
|
||||
Name: "org-deny:" + defOrg.String(),
|
||||
Org: map[string][]rbac.Permission{
|
||||
defOrg.String(): {
|
||||
{
|
||||
Negate: true,
|
||||
ResourceType: "*",
|
||||
ResourceID: "*",
|
||||
Action: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "user-deny-all",
|
||||
// List out deny permissions explicitly
|
||||
|
@ -476,10 +511,10 @@ func TestAuthorizeLevels(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.All()},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner(user.UserID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner(user.UserID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithID(wrkID)},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID)},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID)},
|
||||
|
@ -489,10 +524,10 @@ func TestAuthorizeLevels(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.WithOwner("not-me")},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner("not-me")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID)},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id")},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me")},
|
||||
|
@ -514,7 +549,7 @@ func TestAuthorizeLevels(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
rbac.RoleOrgAdmin(defOrg),
|
||||
must(rbac.RoleByName(rbac.RoleOrgAdmin(defOrg))),
|
||||
{
|
||||
Name: "user-deny-all",
|
||||
// List out deny permissions explicitly
|
||||
|
@ -549,10 +584,10 @@ func TestAuthorizeLevels(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.All(), allow: false},
|
||||
|
||||
// Other org + me + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID).WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner(user.UserID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner(user.UserID).WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner(user.UserID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithID(wrkID), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID), allow: false},
|
||||
|
||||
// Other org + other user + id
|
||||
{resource: rbac.ResourceWorkspace.InOrg(defOrg).WithOwner("not-me").WithID(wrkID), allow: true},
|
||||
|
@ -562,10 +597,10 @@ func TestAuthorizeLevels(t *testing.T) {
|
|||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false},
|
||||
|
||||
// Other org + other use + other id
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithOwner("not-me"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg("other"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID).WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.InOrg(unusedID), allow: false},
|
||||
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me").WithID("not-id"), allow: false},
|
||||
{resource: rbac.ResourceWorkspace.WithOwner("not-me"), allow: false},
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
package rbac
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
admin string = "admin"
|
||||
member string = "member"
|
||||
auditor string = "auditor"
|
||||
|
||||
orgAdmin string = "organization-admin"
|
||||
orgMember string = "organization-member"
|
||||
)
|
||||
|
||||
// The functions below ONLY need to exist for roles that are "defaulted" in some way.
|
||||
// Any other roles (like auditor), can be listed and let the user select/assigned.
|
||||
// Once we have a database implementation, the "default" roles can be defined on the
|
||||
// site and orgs, and these functions can be removed.
|
||||
|
||||
func RoleAdmin() string {
|
||||
return roleName(admin, "")
|
||||
}
|
||||
|
||||
func RoleMember() string {
|
||||
return roleName(member, "")
|
||||
}
|
||||
|
||||
func RoleOrgAdmin(organizationID uuid.UUID) string {
|
||||
return roleName(orgAdmin, organizationID.String())
|
||||
}
|
||||
|
||||
func RoleOrgMember(organizationID uuid.UUID) string {
|
||||
return roleName(orgMember, organizationID.String())
|
||||
}
|
||||
|
||||
var (
|
||||
// builtInRoles are just a hard coded set for now. Ideally we store these in
|
||||
// the database. Right now they are functions because the org id should scope
|
||||
// certain roles. When we store them in the database, each organization should
|
||||
// create the roles that are assignable in the org. This isn't a hard problem to solve,
|
||||
// it's just easier as a function right now.
|
||||
//
|
||||
// This map will be replaced by database storage defined by this ticket.
|
||||
// https://github.com/coder/coder/issues/1194
|
||||
builtInRoles = map[string]func(orgID string) Role{
|
||||
// admin grants all actions to all resources.
|
||||
admin: func(_ string) Role {
|
||||
return Role{
|
||||
Name: admin,
|
||||
Site: permissions(map[Object][]Action{
|
||||
ResourceWildcard: {WildcardSymbol},
|
||||
}),
|
||||
}
|
||||
},
|
||||
|
||||
// member grants all actions to all resources owned by the user
|
||||
member: func(_ string) Role {
|
||||
return Role{
|
||||
Name: member,
|
||||
User: permissions(map[Object][]Action{
|
||||
ResourceWildcard: {WildcardSymbol},
|
||||
}),
|
||||
}
|
||||
},
|
||||
|
||||
// auditor provides all permissions required to effectively read and understand
|
||||
// audit log events.
|
||||
// TODO: Finish the auditor as we add resources.
|
||||
auditor: func(_ string) Role {
|
||||
return Role{
|
||||
Name: "auditor",
|
||||
Site: permissions(map[Object][]Action{
|
||||
// Should be able to read all template details, even in orgs they
|
||||
// are not in.
|
||||
ResourceTemplate: {ActionRead},
|
||||
}),
|
||||
}
|
||||
},
|
||||
|
||||
// orgAdmin returns a role with all actions allows in a given
|
||||
// organization scope.
|
||||
orgAdmin: func(organizationID string) Role {
|
||||
return Role{
|
||||
Name: roleName(orgAdmin, organizationID),
|
||||
Org: map[string][]Permission{
|
||||
organizationID: {
|
||||
{
|
||||
Negate: false,
|
||||
ResourceType: "*",
|
||||
ResourceID: "*",
|
||||
Action: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
// orgMember has an empty set of permissions, this just implies their membership
|
||||
// in an organization.
|
||||
orgMember: func(organizationID string) Role {
|
||||
return Role{
|
||||
Name: roleName(orgMember, organizationID),
|
||||
Org: map[string][]Permission{
|
||||
organizationID: {},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// RoleByName returns the permissions associated with a given role name.
|
||||
// This allows just the role names to be stored and expanded when required.
|
||||
func RoleByName(name string) (Role, error) {
|
||||
roleName, orgID, err := roleSplit(name)
|
||||
if err != nil {
|
||||
return Role{}, xerrors.Errorf(":%w", err)
|
||||
}
|
||||
|
||||
roleFunc, ok := builtInRoles[roleName]
|
||||
if !ok {
|
||||
// No role found
|
||||
return Role{}, xerrors.Errorf("role %q not found", roleName)
|
||||
}
|
||||
|
||||
// Ensure all org roles are properly scoped a non-empty organization id.
|
||||
// This is just some defensive programming.
|
||||
role := roleFunc(orgID)
|
||||
if len(role.Org) > 0 && orgID == "" {
|
||||
return Role{}, xerrors.Errorf("expect a org id for role %q", roleName)
|
||||
}
|
||||
|
||||
return role, nil
|
||||
}
|
||||
|
||||
func IsOrgRole(roleName string) (string, bool) {
|
||||
_, orgID, err := roleSplit(roleName)
|
||||
if err == nil && orgID != "" {
|
||||
return orgID, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// roleName is a quick helper function to return
|
||||
// role_name:scopeID
|
||||
// If no scopeID is required, only 'role_name' is returned
|
||||
func roleName(name string, orgID string) string {
|
||||
if orgID == "" {
|
||||
return name
|
||||
}
|
||||
return name + ":" + orgID
|
||||
}
|
||||
|
||||
func roleSplit(role string) (name string, orgID string, err error) {
|
||||
arr := strings.Split(role, ":")
|
||||
if len(arr) > 2 {
|
||||
return "", "", xerrors.Errorf("too many colons in role name")
|
||||
}
|
||||
|
||||
if arr[0] == "" {
|
||||
return "", "", xerrors.Errorf("role cannot be the empty string")
|
||||
}
|
||||
|
||||
if len(arr) == 2 {
|
||||
return arr[0], arr[1], nil
|
||||
}
|
||||
return arr[0], "", nil
|
||||
}
|
||||
|
||||
// permissions is just a helper function to make building roles that list out resources
|
||||
// and actions a bit easier.
|
||||
func permissions(perms map[Object][]Action) []Permission {
|
||||
list := make([]Permission, 0, len(perms))
|
||||
for k, actions := range perms {
|
||||
for _, act := range actions {
|
||||
act := act
|
||||
list = append(list, Permission{
|
||||
Negate: false,
|
||||
ResourceType: k.Type,
|
||||
ResourceID: WildcardSymbol,
|
||||
Action: act,
|
||||
})
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package rbac
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestRoleByName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("BuiltIns", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
Role Role
|
||||
}{
|
||||
{Role: builtInRoles[admin]("")},
|
||||
{Role: builtInRoles[member]("")},
|
||||
{Role: builtInRoles[auditor]("")},
|
||||
|
||||
{Role: builtInRoles[orgAdmin](uuid.New().String())},
|
||||
{Role: builtInRoles[orgAdmin](uuid.New().String())},
|
||||
{Role: builtInRoles[orgAdmin](uuid.New().String())},
|
||||
|
||||
{Role: builtInRoles[orgMember](uuid.New().String())},
|
||||
{Role: builtInRoles[orgMember](uuid.New().String())},
|
||||
{Role: builtInRoles[orgMember](uuid.New().String())},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
c := c
|
||||
t.Run(c.Role.Name, func(t *testing.T) {
|
||||
role, err := RoleByName(c.Role.Name)
|
||||
require.NoError(t, err, "role exists")
|
||||
require.Equal(t, c.Role, role)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// nolint:paralleltest
|
||||
t.Run("Errors", func(t *testing.T) {
|
||||
var err error
|
||||
|
||||
_, err = RoleByName("")
|
||||
require.Error(t, err, "empty role")
|
||||
|
||||
_, err = RoleByName("too:many:colons")
|
||||
require.Error(t, err, "too many colons")
|
||||
|
||||
_, err = RoleByName(orgMember)
|
||||
require.Error(t, err, "expect orgID")
|
||||
})
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package rbac_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
)
|
||||
|
||||
func TestIsOrgRole(t *testing.T) {
|
||||
t.Parallel()
|
||||
randomUUID := uuid.New()
|
||||
|
||||
testCases := []struct {
|
||||
RoleName string
|
||||
OrgRole bool
|
||||
OrgID string
|
||||
}{
|
||||
// Not org roles
|
||||
{RoleName: rbac.RoleAdmin()},
|
||||
{RoleName: rbac.RoleMember()},
|
||||
{RoleName: "auditor"},
|
||||
|
||||
{
|
||||
RoleName: "a:bad:role",
|
||||
OrgRole: false,
|
||||
},
|
||||
{
|
||||
RoleName: "",
|
||||
OrgRole: false,
|
||||
},
|
||||
|
||||
// Org roles
|
||||
{
|
||||
RoleName: rbac.RoleOrgAdmin(randomUUID),
|
||||
OrgRole: true,
|
||||
OrgID: randomUUID.String(),
|
||||
},
|
||||
{
|
||||
RoleName: rbac.RoleOrgMember(randomUUID),
|
||||
OrgRole: true,
|
||||
OrgID: randomUUID.String(),
|
||||
},
|
||||
{
|
||||
RoleName: "test:example",
|
||||
OrgRole: true,
|
||||
OrgID: "example",
|
||||
},
|
||||
}
|
||||
|
||||
// nolint:paralleltest
|
||||
for _, c := range testCases {
|
||||
t.Run(c.RoleName, func(t *testing.T) {
|
||||
orgID, ok := rbac.IsOrgRole(c.RoleName)
|
||||
require.Equal(t, c.OrgRole, ok, "match expected org role")
|
||||
require.Equal(t, c.OrgID, orgID, "match expected org id")
|
||||
})
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
|
@ -16,14 +18,15 @@ func TestExample(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
authorizer, err := rbac.NewAuthorizer()
|
||||
require.NoError(t, err)
|
||||
defaultOrg := uuid.New()
|
||||
|
||||
// user will become an authn object, and can even be a database.User if it
|
||||
// fulfills the interface. Until then, use a placeholder.
|
||||
user := subject{
|
||||
UserID: "alice",
|
||||
Roles: []rbac.Role{
|
||||
rbac.RoleOrgAdmin("default"),
|
||||
rbac.RoleMember,
|
||||
must(rbac.RoleByName(rbac.RoleMember())),
|
||||
must(rbac.RoleByName(rbac.RoleOrgAdmin(defaultOrg))),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -38,17 +41,24 @@ func TestExample(t *testing.T) {
|
|||
//nolint:paralleltest
|
||||
t.Run("ReadOrgWorkspaces", func(t *testing.T) {
|
||||
// To read all workspaces on the org 'default'
|
||||
err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg("default"))
|
||||
err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg(defaultOrg))
|
||||
require.NoError(t, err, "this user can read all org workspaces in 'default'")
|
||||
})
|
||||
|
||||
//nolint:paralleltest
|
||||
t.Run("ReadMyWorkspace", func(t *testing.T) {
|
||||
// Note 'database.Workspace' could fulfill the object interface and be passed in directly
|
||||
err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg("default").WithOwner(user.UserID))
|
||||
err := authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg(defaultOrg).WithOwner(user.UserID))
|
||||
require.NoError(t, err, "this user can their workspace")
|
||||
|
||||
err = authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg("default").WithOwner(user.UserID).WithID("1234"))
|
||||
err = authorizer.Authorize(ctx, user.UserID, user.Roles, rbac.ActionRead, rbac.ResourceWorkspace.InOrg(defaultOrg).WithOwner(user.UserID).WithID("1234"))
|
||||
require.NoError(t, err, "this user can read workspace '1234'")
|
||||
})
|
||||
}
|
||||
|
||||
func must[T any](value T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package rbac
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const WildcardSymbol = "*"
|
||||
|
||||
// Resources are just typed objects. Making resources this way allows directly
|
||||
|
@ -46,11 +50,11 @@ func (z Object) All() Object {
|
|||
}
|
||||
|
||||
// InOrg adds an org OwnerID to the resource
|
||||
func (z Object) InOrg(orgID string) Object {
|
||||
func (z Object) InOrg(orgID uuid.UUID) Object {
|
||||
return Object{
|
||||
ResourceID: z.ResourceID,
|
||||
Owner: z.Owner,
|
||||
OrgID: orgID,
|
||||
OrgID: orgID.String(),
|
||||
Type: z.Type,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package rbac
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Permission is the format passed into the rego.
|
||||
type Permission struct {
|
||||
// Negate makes this a negative permission
|
||||
Negate bool `json:"negate"`
|
||||
|
@ -14,122 +13,15 @@ type Permission struct {
|
|||
// - Site level permissions apply EVERYWHERE
|
||||
// - Org level permissions apply to EVERYTHING in a given ORG
|
||||
// - User level permissions are the lowest
|
||||
// In most cases, you will just want to use the pre-defined roles
|
||||
// below.
|
||||
// This is the type passed into the rego as a json payload.
|
||||
// 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"`
|
||||
Site []Permission `json:"site"`
|
||||
// Org is a map of orgid to permissions. We represent orgid as a string.
|
||||
// We scope the organizations in the role so we can easily combine all the
|
||||
// roles.
|
||||
Org map[string][]Permission `json:"org"`
|
||||
User []Permission `json:"user"`
|
||||
}
|
||||
|
||||
// Roles are stored as structs, so they can be serialized and stored. Until we store them elsewhere,
|
||||
// const's will do just fine.
|
||||
var (
|
||||
// RoleAdmin is a role that allows everything everywhere.
|
||||
RoleAdmin = Role{
|
||||
Name: "admin",
|
||||
Site: permissions(map[Object][]Action{
|
||||
ResourceWildcard: {WildcardSymbol},
|
||||
}),
|
||||
}
|
||||
|
||||
// RoleMember is a role that allows access to user-level resources.
|
||||
RoleMember = Role{
|
||||
Name: "member",
|
||||
User: permissions(map[Object][]Action{
|
||||
ResourceWildcard: {WildcardSymbol},
|
||||
}),
|
||||
}
|
||||
|
||||
// RoleAuditor is an example on how to give more precise permissions
|
||||
RoleAuditor = Role{
|
||||
Name: "auditor",
|
||||
Site: permissions(map[Object][]Action{
|
||||
//ResourceAuditLogs: {ActionRead},
|
||||
// Should be able to read user details to associate with logs.
|
||||
// Without this the user-id in logs is not very helpful
|
||||
ResourceWorkspace: {ActionRead},
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
func RoleOrgDenyAll(orgID string) Role {
|
||||
return Role{
|
||||
Name: "org-deny-" + orgID,
|
||||
Org: map[string][]Permission{
|
||||
orgID: {
|
||||
{
|
||||
Negate: true,
|
||||
ResourceType: "*",
|
||||
ResourceID: "*",
|
||||
Action: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RoleOrgAdmin returns a role with all actions allows in a given
|
||||
// organization scope.
|
||||
func RoleOrgAdmin(orgID string) Role {
|
||||
return Role{
|
||||
Name: "org-admin-" + orgID,
|
||||
Org: map[string][]Permission{
|
||||
orgID: {
|
||||
{
|
||||
Negate: false,
|
||||
ResourceType: "*",
|
||||
ResourceID: "*",
|
||||
Action: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RoleOrgMember returns a role with default permissions in a given
|
||||
// organization scope.
|
||||
func RoleOrgMember(orgID string) Role {
|
||||
return Role{
|
||||
Name: "org-member-" + orgID,
|
||||
Org: map[string][]Permission{
|
||||
orgID: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RoleWorkspaceAgent returns a role with permission to read a given
|
||||
// workspace.
|
||||
func RoleWorkspaceAgent(workspaceID string) Role {
|
||||
return Role{
|
||||
Name: fmt.Sprintf("agent-%s", workspaceID),
|
||||
// This is at the site level to prevent the token from losing access if the user
|
||||
// is kicked from the org
|
||||
Site: []Permission{
|
||||
{
|
||||
Negate: false,
|
||||
ResourceType: ResourceWorkspace.Type,
|
||||
ResourceID: workspaceID,
|
||||
Action: ActionRead,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func permissions(perms map[Object][]Action) []Permission {
|
||||
list := make([]Permission, 0, len(perms))
|
||||
for k, actions := range perms {
|
||||
for _, act := range actions {
|
||||
act := act
|
||||
list = append(list, Permission{
|
||||
Negate: false,
|
||||
ResourceType: k.Type,
|
||||
ResourceID: WildcardSymbol,
|
||||
Action: act,
|
||||
})
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
|
113
coderd/users.go
113
coderd/users.go
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/coder/coder/coderd/gitsshkey"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/coderd/userpassword"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/cryptorand"
|
||||
|
@ -82,6 +83,21 @@ func (api *api) postFirstUser(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO: @emyrk this currently happens outside the database tx used to create
|
||||
// the user. Maybe I add this ability to grant roles in the createUser api
|
||||
// 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()},
|
||||
ID: user.ID,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusCreated, codersdk.CreateFirstUserResponse{
|
||||
UserID: user.ID,
|
||||
OrganizationID: organizationID,
|
||||
|
@ -344,6 +360,88 @@ func (api *api) putUserSuspend(rw http.ResponseWriter, r *http.Request) {
|
|||
httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser, organizations))
|
||||
}
|
||||
|
||||
func (api *api) userRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
|
||||
resp := codersdk.UserRoles{
|
||||
Roles: user.RBACRoles,
|
||||
OrganizationRoles: make(map[uuid.UUID][]string),
|
||||
}
|
||||
|
||||
memberships, err := api.Database.GetOrganizationMembershipsByUserID(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get user memberships: %s", err),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
for _, mem := range memberships {
|
||||
resp.OrganizationRoles[mem.OrganizationID] = mem.Roles
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (api *api) putUserRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
// User is the user to modify
|
||||
// TODO: Until rbac authorize is implemented, only be able to change your
|
||||
// own roles. This also means you can grant yourself whatever roles you want.
|
||||
user := httpmw.UserParam(r)
|
||||
apiKey := httpmw.APIKey(r)
|
||||
if apiKey.UserID != user.ID {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("modifying other users is not supported at this time"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var params codersdk.UpdateRoles
|
||||
if !httpapi.Read(rw, r, ¶ms) {
|
||||
return
|
||||
}
|
||||
|
||||
updatedUser, err := api.updateSiteUserRoles(r.Context(), database.UpdateUserRolesParams{
|
||||
GrantedRoles: params.Roles,
|
||||
ID: user.ID,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
organizationIDs, err := userOrganizationIDs(r.Context(), api, user)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get organization IDs: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, convertUser(updatedUser, organizationIDs))
|
||||
}
|
||||
|
||||
func (api *api) updateSiteUserRoles(ctx context.Context, args database.UpdateUserRolesParams) (database.User, error) {
|
||||
// Enforce only site wide roles
|
||||
for _, r := range args.GrantedRoles {
|
||||
if _, ok := rbac.IsOrgRole(r); ok {
|
||||
return database.User{}, xerrors.Errorf("must only update site wide roles")
|
||||
}
|
||||
|
||||
if _, err := rbac.RoleByName(r); err != nil {
|
||||
return database.User{}, xerrors.Errorf("%q is not a supported role", r)
|
||||
}
|
||||
}
|
||||
|
||||
updatedUser, err := api.Database.UpdateUserRoles(ctx, args)
|
||||
if err != nil {
|
||||
return database.User{}, xerrors.Errorf("update site roles: %w", err)
|
||||
}
|
||||
return updatedUser, nil
|
||||
}
|
||||
|
||||
// Returns organizations the parameterized user has access to.
|
||||
func (api *api) organizationsByUser(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
|
@ -440,7 +538,11 @@ func (api *api) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request)
|
|||
UserID: user.ID,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
Roles: []string{"organization-admin"},
|
||||
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)
|
||||
|
@ -604,6 +706,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
|
||||
// If no organization is provided, create a new one for the user.
|
||||
if req.OrganizationID == uuid.Nil {
|
||||
organization, err := db.InsertOrganization(ctx, database.InsertOrganizationParams{
|
||||
|
@ -616,7 +719,10 @@ func (api *api) createUser(ctx context.Context, req codersdk.CreateUserRequest)
|
|||
return xerrors.Errorf("create organization: %w", err)
|
||||
}
|
||||
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(),
|
||||
|
@ -624,6 +730,8 @@ func (api *api) createUser(ctx context.Context, req codersdk.CreateUserRequest)
|
|||
Username: req.Username,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
// All new users are defaulted to members of the site.
|
||||
RBACRoles: []string{rbac.RoleMember()},
|
||||
}
|
||||
// If a user signs up with OAuth, they can have no password!
|
||||
if req.Password != "" {
|
||||
|
@ -659,7 +767,8 @@ func (api *api) createUser(ctx context.Context, req codersdk.CreateUserRequest)
|
|||
UserID: user.ID,
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
Roles: []string{},
|
||||
// By default give them membership to the organization
|
||||
Roles: orgRoles,
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create organization member: %w", err)
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
|
@ -286,6 +287,118 @@ func TestUpdateUserProfile(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestGrantRoles(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("UpdateIncorrectRoles", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
admin := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, admin)
|
||||
member := coderdtest.CreateAnotherUser(t, admin, first.OrganizationID)
|
||||
|
||||
_, err := admin.UpdateUserRoles(ctx, codersdk.Me, codersdk.UpdateRoles{
|
||||
Roles: []string{rbac.RoleOrgMember(first.OrganizationID)},
|
||||
})
|
||||
require.Error(t, err, "org role in site")
|
||||
|
||||
_, err = admin.UpdateUserRoles(ctx, uuid.New(), codersdk.UpdateRoles{
|
||||
Roles: []string{rbac.RoleOrgMember(first.OrganizationID)},
|
||||
})
|
||||
require.Error(t, err, "user does not exist")
|
||||
|
||||
_, err = admin.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, codersdk.Me, codersdk.UpdateRoles{
|
||||
Roles: []string{rbac.RoleMember()},
|
||||
})
|
||||
require.Error(t, err, "site role in org")
|
||||
|
||||
_, err = admin.UpdateOrganizationMemberRoles(ctx, uuid.New(), codersdk.Me, codersdk.UpdateRoles{
|
||||
Roles: []string{rbac.RoleMember()},
|
||||
})
|
||||
require.Error(t, err, "role in org without membership")
|
||||
|
||||
_, err = member.UpdateUserRoles(ctx, first.UserID, codersdk.UpdateRoles{
|
||||
Roles: []string{rbac.RoleMember()},
|
||||
})
|
||||
require.Error(t, err, "member cannot change other's roles")
|
||||
|
||||
_, err = member.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, first.UserID, codersdk.UpdateRoles{
|
||||
Roles: []string{rbac.RoleMember()},
|
||||
})
|
||||
require.Error(t, err, "member cannot change other's org roles")
|
||||
})
|
||||
|
||||
t.Run("FirstUserRoles", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
client := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
roles, err := client.GetUserRoles(ctx, codersdk.Me)
|
||||
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")
|
||||
})
|
||||
|
||||
t.Run("GrantAdmin", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
admin := coderdtest.New(t, nil)
|
||||
first := coderdtest.CreateFirstUser(t, admin)
|
||||
|
||||
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.OrganizationRoles[first.OrganizationID],
|
||||
[]string{rbac.RoleOrgMember(first.OrganizationID)},
|
||||
)
|
||||
|
||||
// Grant
|
||||
// TODO: @emyrk this should be 'admin.UpdateUserRoles' once proper authz
|
||||
// is enforced.
|
||||
_, err = member.UpdateUserRoles(ctx, codersdk.Me, codersdk.UpdateRoles{
|
||||
Roles: []string{
|
||||
// Promote to site admin
|
||||
rbac.RoleMember(),
|
||||
rbac.RoleAdmin(),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err, "grant member admin role")
|
||||
|
||||
// Promote to org admin
|
||||
_, err = member.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, codersdk.Me, codersdk.UpdateRoles{
|
||||
Roles: []string{
|
||||
// Promote to org admin
|
||||
rbac.RoleOrgMember(first.OrganizationID),
|
||||
rbac.RoleOrgAdmin(first.OrganizationID),
|
||||
},
|
||||
})
|
||||
require.NoError(t, err, "grant member org admin role")
|
||||
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPutUserSuspend(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package codersdk
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OrganizationMember struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
Roles []string `db:"roles" json:"roles"`
|
||||
}
|
|
@ -72,6 +72,15 @@ type UpdateUserProfileRequest struct {
|
|||
Username string `json:"username" validate:"required,username"`
|
||||
}
|
||||
|
||||
type UpdateRoles struct {
|
||||
Roles []string `json:"roles" validate:"required"`
|
||||
}
|
||||
|
||||
type UserRoles struct {
|
||||
Roles []string `json:"roles"`
|
||||
OrganizationRoles map[uuid.UUID][]string `json:"organization_roles"`
|
||||
}
|
||||
|
||||
// LoginWithPasswordRequest enables callers to authenticate with email and password.
|
||||
type LoginWithPasswordRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
|
@ -172,6 +181,50 @@ func (c *Client) SuspendUser(ctx context.Context, userID uuid.UUID) (User, error
|
|||
return user, json.NewDecoder(res.Body).Decode(&user)
|
||||
}
|
||||
|
||||
// UpdateUserRoles grants the userID the specified roles.
|
||||
// Include ALL roles the user has.
|
||||
func (c *Client) UpdateUserRoles(ctx context.Context, userID uuid.UUID, req UpdateRoles) (User, error) {
|
||||
res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/roles", uuidOrMe(userID)), req)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return User{}, readBodyAsError(res)
|
||||
}
|
||||
var user User
|
||||
return user, json.NewDecoder(res.Body).Decode(&user)
|
||||
}
|
||||
|
||||
// UpdateOrganizationMemberRoles grants the userID the specified roles in an org.
|
||||
// Include ALL roles the user has.
|
||||
func (c *Client) UpdateOrganizationMemberRoles(ctx context.Context, organizationID, userID uuid.UUID, req UpdateRoles) (User, error) {
|
||||
res, err := c.request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/organizations/%s/members/%s/roles", organizationID, uuidOrMe(userID)), req)
|
||||
if err != nil {
|
||||
return User{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return User{}, readBodyAsError(res)
|
||||
}
|
||||
var user User
|
||||
return user, json.NewDecoder(res.Body).Decode(&user)
|
||||
}
|
||||
|
||||
// GetUserRoles returns all roles the user has
|
||||
func (c *Client) GetUserRoles(ctx context.Context, userID uuid.UUID) (UserRoles, error) {
|
||||
res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/roles", uuidOrMe(userID)), nil)
|
||||
if err != nil {
|
||||
return UserRoles{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return UserRoles{}, readBodyAsError(res)
|
||||
}
|
||||
var roles UserRoles
|
||||
return roles, json.NewDecoder(res.Body).Decode(&roles)
|
||||
}
|
||||
|
||||
// CreateAPIKey generates an API key for the user ID provided.
|
||||
func (c *Client) CreateAPIKey(ctx context.Context, userID uuid.UUID) (*GenerateAPIKeyResponse, error) {
|
||||
res, err := c.request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/keys", uuidOrMe(userID)), nil)
|
||||
|
|
|
@ -12,7 +12,7 @@ export interface AgentGitSSHKey {
|
|||
readonly private_key: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:96:6
|
||||
// From codersdk/users.go:105:6
|
||||
export interface AuthMethods {
|
||||
readonly password: boolean
|
||||
readonly github: boolean
|
||||
|
@ -44,7 +44,7 @@ export interface CreateFirstUserResponse {
|
|||
readonly organization_id: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:91:6
|
||||
// From codersdk/users.go:100:6
|
||||
export interface CreateOrganizationRequest {
|
||||
readonly name: string
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ export interface CreateWorkspaceRequest {
|
|||
readonly parameter_values: CreateParameterRequest[]
|
||||
}
|
||||
|
||||
// From codersdk/users.go:87:6
|
||||
// From codersdk/users.go:96:6
|
||||
export interface GenerateAPIKeyResponse {
|
||||
readonly key: string
|
||||
}
|
||||
|
@ -118,13 +118,13 @@ export interface GoogleInstanceIdentityToken {
|
|||
readonly json_web_token: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:76:6
|
||||
// From codersdk/users.go:85:6
|
||||
export interface LoginWithPasswordRequest {
|
||||
readonly email: string
|
||||
readonly password: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:82:6
|
||||
// From codersdk/users.go:91:6
|
||||
export interface LoginWithPasswordResponse {
|
||||
readonly session_token: string
|
||||
}
|
||||
|
@ -137,6 +137,15 @@ export interface Organization {
|
|||
readonly updated_at: string
|
||||
}
|
||||
|
||||
// From codersdk/organizationmember.go:9:6
|
||||
export interface OrganizationMember {
|
||||
readonly user_id: string
|
||||
readonly organization_id: string
|
||||
readonly created_at: string
|
||||
readonly updated_at: string
|
||||
readonly roles: string[]
|
||||
}
|
||||
|
||||
// From codersdk/parameters.go:26:6
|
||||
export interface Parameter {
|
||||
readonly id: string
|
||||
|
@ -245,6 +254,11 @@ export interface UpdateActiveTemplateVersion {
|
|||
readonly id: string
|
||||
}
|
||||
|
||||
// From codersdk/users.go:75:6
|
||||
export interface UpdateRoles {
|
||||
readonly roles: string[]
|
||||
}
|
||||
|
||||
// From codersdk/users.go:70:6
|
||||
export interface UpdateUserProfileRequest {
|
||||
readonly email: string
|
||||
|
@ -276,6 +290,12 @@ export interface User {
|
|||
readonly organization_ids: string[]
|
||||
}
|
||||
|
||||
// From codersdk/users.go:79:6
|
||||
export interface UserRoles {
|
||||
readonly roles: string[]
|
||||
readonly organization_roles: Record<string, string[]>
|
||||
}
|
||||
|
||||
// From codersdk/users.go:24:6
|
||||
export interface UsersRequest {
|
||||
readonly after_user: string
|
||||
|
|
Loading…
Reference in New Issue