mirror of https://github.com/coder/coder.git
feat: Add template-admin + user-admin role for managing templates + users (#3490)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
This commit is contained in:
parent
c41261cf6e
commit
40e68cb80b
|
@ -340,7 +340,7 @@ func New(options *Options) *API {
|
|||
r.Get("/", api.workspaceAgent)
|
||||
r.Post("/peer", api.postWorkspaceAgentWireguardPeer)
|
||||
r.Get("/dial", api.workspaceAgentDial)
|
||||
r.Get("/turn", api.workspaceAgentTurn)
|
||||
r.Get("/turn", api.userWorkspaceAgentTurn)
|
||||
r.Get("/pty", api.workspaceAgentPTY)
|
||||
r.Get("/iceservers", api.workspaceAgentICEServers)
|
||||
r.Get("/derp", api.derpMap)
|
||||
|
|
|
@ -220,6 +220,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||
|
||||
// Some quick reused objects
|
||||
workspaceRBACObj := rbac.ResourceWorkspace.InOrg(organization.ID).WithOwner(workspace.OwnerID.String())
|
||||
workspaceExecObj := rbac.ResourceWorkspaceExecution.InOrg(organization.ID).WithOwner(workspace.OwnerID.String())
|
||||
|
||||
// skipRoutes allows skipping routes from being checked.
|
||||
skipRoutes := map[string]string{
|
||||
|
@ -268,7 +269,6 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||
"GET:/api/v2/workspaceagents/me/wireguardlisten": {NoAuthorize: true},
|
||||
"POST:/api/v2/workspaceagents/me/keys": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaceagents/{workspaceagent}/iceservers": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {NoAuthorize: true},
|
||||
"GET:/api/v2/workspaceagents/{workspaceagent}/derp": {NoAuthorize: true},
|
||||
|
||||
// These endpoints have more assertions. This is good, add more endpoints to assert if you can!
|
||||
|
@ -331,12 +331,16 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
|
|||
AssertObject: workspaceRBACObj,
|
||||
},
|
||||
"GET:/api/v2/workspaceagents/{workspaceagent}/dial": {
|
||||
AssertAction: rbac.ActionUpdate,
|
||||
AssertObject: workspaceRBACObj,
|
||||
AssertAction: rbac.ActionCreate,
|
||||
AssertObject: workspaceExecObj,
|
||||
},
|
||||
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {
|
||||
AssertAction: rbac.ActionCreate,
|
||||
AssertObject: workspaceExecObj,
|
||||
},
|
||||
"GET:/api/v2/workspaceagents/{workspaceagent}/pty": {
|
||||
AssertAction: rbac.ActionUpdate,
|
||||
AssertObject: workspaceRBACObj,
|
||||
AssertAction: rbac.ActionCreate,
|
||||
AssertObject: workspaceExecObj,
|
||||
},
|
||||
"GET:/api/v2/workspaces/": {
|
||||
StatusCode: http.StatusOK,
|
||||
|
|
|
@ -17,6 +17,10 @@ func (w Workspace) RBACObject() rbac.Object {
|
|||
return rbac.ResourceWorkspace.InOrg(w.OrganizationID).WithOwner(w.OwnerID.String())
|
||||
}
|
||||
|
||||
func (w Workspace) ExecutionRBAC() rbac.Object {
|
||||
return rbac.ResourceWorkspaceExecution.InOrg(w.OrganizationID).WithOwner(w.OwnerID.String())
|
||||
}
|
||||
|
||||
func (m OrganizationMember) RBACObject() rbac.Object {
|
||||
return rbac.ResourceOrganizationMember.InOrg(m.OrganizationID)
|
||||
}
|
||||
|
|
|
@ -9,9 +9,11 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
admin string = "admin"
|
||||
member string = "member"
|
||||
auditor string = "auditor"
|
||||
admin string = "admin"
|
||||
member string = "member"
|
||||
templateAdmin string = "template-admin"
|
||||
userAdmin string = "user-admin"
|
||||
auditor string = "auditor"
|
||||
|
||||
orgAdmin string = "organization-admin"
|
||||
orgMember string = "organization-member"
|
||||
|
@ -26,6 +28,14 @@ func RoleAdmin() string {
|
|||
return roleName(admin, "")
|
||||
}
|
||||
|
||||
func RoleTemplateAdmin() string {
|
||||
return roleName(templateAdmin, "")
|
||||
}
|
||||
|
||||
func RoleUserAdmin() string {
|
||||
return roleName(userAdmin, "")
|
||||
}
|
||||
|
||||
func RoleMember() string {
|
||||
return roleName(member, "")
|
||||
}
|
||||
|
@ -93,6 +103,31 @@ var (
|
|||
}
|
||||
},
|
||||
|
||||
templateAdmin: func(_ string) Role {
|
||||
return Role{
|
||||
Name: templateAdmin,
|
||||
DisplayName: "Template Admin",
|
||||
Site: permissions(map[Object][]Action{
|
||||
ResourceTemplate: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||
// CRUD all files, even those they did not upload.
|
||||
ResourceFile: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||
ResourceWorkspace: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||
// CRUD to provisioner daemons for now.
|
||||
ResourceProvisionerDaemon: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||
}),
|
||||
}
|
||||
},
|
||||
|
||||
userAdmin: func(_ string) Role {
|
||||
return Role{
|
||||
Name: userAdmin,
|
||||
DisplayName: "User Admin",
|
||||
Site: permissions(map[Object][]Action{
|
||||
ResourceUser: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||
}),
|
||||
}
|
||||
},
|
||||
|
||||
// orgAdmin returns a role with all actions allows in a given
|
||||
// organization scope.
|
||||
orgAdmin: func(organizationID string) Role {
|
||||
|
@ -153,11 +188,13 @@ var (
|
|||
// map[actor_role][assign_role]<can_assign>
|
||||
assignRoles = map[string]map[string]bool{
|
||||
admin: {
|
||||
admin: true,
|
||||
auditor: true,
|
||||
member: true,
|
||||
orgAdmin: true,
|
||||
orgMember: true,
|
||||
admin: true,
|
||||
auditor: true,
|
||||
member: true,
|
||||
orgAdmin: true,
|
||||
orgMember: true,
|
||||
templateAdmin: true,
|
||||
userAdmin: true,
|
||||
},
|
||||
orgAdmin: {
|
||||
orgAdmin: true,
|
||||
|
|
|
@ -18,6 +18,8 @@ func TestRoleByName(t *testing.T) {
|
|||
}{
|
||||
{Role: builtInRoles[admin]("")},
|
||||
{Role: builtInRoles[member]("")},
|
||||
{Role: builtInRoles[templateAdmin]("")},
|
||||
{Role: builtInRoles[userAdmin]("")},
|
||||
{Role: builtInRoles[auditor]("")},
|
||||
|
||||
{Role: builtInRoles[orgAdmin](uuid.New().String())},
|
||||
|
|
|
@ -111,6 +111,7 @@ func TestRolePermissions(t *testing.T) {
|
|||
// currentUser is anything that references "me", "mine", or "my".
|
||||
currentUser := uuid.New()
|
||||
adminID := uuid.New()
|
||||
templateAdminID := uuid.New()
|
||||
orgID := uuid.New()
|
||||
otherOrg := uuid.New()
|
||||
|
||||
|
@ -124,9 +125,12 @@ func TestRolePermissions(t *testing.T) {
|
|||
otherOrgMember := authSubject{Name: "org_member_other", UserID: uuid.NewString(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(otherOrg)}}
|
||||
otherOrgAdmin := authSubject{Name: "org_admin_other", UserID: uuid.NewString(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(otherOrg), rbac.RoleOrgAdmin(otherOrg)}}
|
||||
|
||||
templateAdmin := authSubject{Name: "template-admin", UserID: templateAdminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleTemplateAdmin()}}
|
||||
userAdmin := authSubject{Name: "user-admin", UserID: templateAdminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleUserAdmin()}}
|
||||
|
||||
// requiredSubjects are required to be asserted in each test case. This is
|
||||
// to make sure one is not forgotten.
|
||||
requiredSubjects := []authSubject{memberMe, admin, orgMemberMe, orgAdmin, otherOrgAdmin, otherOrgMember}
|
||||
requiredSubjects := []authSubject{memberMe, admin, orgMemberMe, orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin}
|
||||
|
||||
testCases := []struct {
|
||||
// Name the test case to better locate the failing test case.
|
||||
|
@ -146,7 +150,7 @@ func TestRolePermissions(t *testing.T) {
|
|||
Actions: []rbac.Action{rbac.ActionRead},
|
||||
Resource: rbac.ResourceUser,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin},
|
||||
true: {admin, memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin, templateAdmin, userAdmin},
|
||||
false: {},
|
||||
},
|
||||
},
|
||||
|
@ -155,8 +159,8 @@ func TestRolePermissions(t *testing.T) {
|
|||
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
|
||||
Resource: rbac.ResourceUser,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin},
|
||||
false: {memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin},
|
||||
true: {admin, userAdmin},
|
||||
false: {memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin, templateAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -165,8 +169,18 @@ func TestRolePermissions(t *testing.T) {
|
|||
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
|
||||
Resource: rbac.ResourceWorkspace.InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, orgMemberMe, orgAdmin},
|
||||
false: {memberMe, otherOrgAdmin, otherOrgMember},
|
||||
true: {admin, orgMemberMe, orgAdmin, templateAdmin},
|
||||
false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "MyWorkspaceInOrgExecution",
|
||||
// When creating the WithID won't be set, but it does not change the result.
|
||||
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
|
||||
Resource: rbac.ResourceWorkspaceExecution.InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, orgAdmin, orgMemberMe},
|
||||
false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -174,8 +188,8 @@ func TestRolePermissions(t *testing.T) {
|
|||
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
|
||||
Resource: rbac.ResourceTemplate.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, orgAdmin},
|
||||
false: {memberMe, orgMemberMe, otherOrgAdmin, otherOrgMember},
|
||||
true: {admin, orgAdmin, templateAdmin},
|
||||
false: {memberMe, orgMemberMe, otherOrgAdmin, otherOrgMember, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -183,8 +197,8 @@ func TestRolePermissions(t *testing.T) {
|
|||
Actions: []rbac.Action{rbac.ActionRead},
|
||||
Resource: rbac.ResourceTemplate.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, orgMemberMe, orgAdmin},
|
||||
false: {memberMe, otherOrgAdmin, otherOrgMember},
|
||||
true: {admin, orgMemberMe, orgAdmin, templateAdmin},
|
||||
false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -192,8 +206,8 @@ func TestRolePermissions(t *testing.T) {
|
|||
Actions: []rbac.Action{rbac.ActionCreate},
|
||||
Resource: rbac.ResourceFile,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin},
|
||||
false: {orgMemberMe, orgAdmin, memberMe, otherOrgAdmin, otherOrgMember},
|
||||
true: {admin, templateAdmin},
|
||||
false: {orgMemberMe, orgAdmin, memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -201,8 +215,8 @@ func TestRolePermissions(t *testing.T) {
|
|||
Actions: []rbac.Action{rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
|
||||
Resource: rbac.ResourceFile.WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, memberMe, orgMemberMe},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
|
||||
true: {admin, memberMe, orgMemberMe, templateAdmin},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -211,7 +225,7 @@ func TestRolePermissions(t *testing.T) {
|
|||
Resource: rbac.ResourceOrganization,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -220,7 +234,7 @@ func TestRolePermissions(t *testing.T) {
|
|||
Resource: rbac.ResourceOrganization.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, orgAdmin},
|
||||
false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe},
|
||||
false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -229,7 +243,7 @@ func TestRolePermissions(t *testing.T) {
|
|||
Resource: rbac.ResourceOrganization.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, orgAdmin, orgMemberMe},
|
||||
false: {otherOrgAdmin, otherOrgMember, memberMe},
|
||||
false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -238,7 +252,7 @@ func TestRolePermissions(t *testing.T) {
|
|||
Resource: rbac.ResourceRoleAssignment,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin},
|
||||
false: {orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe},
|
||||
false: {orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -246,7 +260,7 @@ func TestRolePermissions(t *testing.T) {
|
|||
Actions: []rbac.Action{rbac.ActionRead},
|
||||
Resource: rbac.ResourceRoleAssignment,
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe},
|
||||
true: {admin, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
||||
false: {},
|
||||
},
|
||||
},
|
||||
|
@ -256,7 +270,7 @@ func TestRolePermissions(t *testing.T) {
|
|||
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, orgAdmin},
|
||||
false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe},
|
||||
false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -265,7 +279,7 @@ func TestRolePermissions(t *testing.T) {
|
|||
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, orgAdmin, orgMemberMe},
|
||||
false: {otherOrgAdmin, otherOrgMember, memberMe},
|
||||
false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -274,7 +288,7 @@ func TestRolePermissions(t *testing.T) {
|
|||
Resource: rbac.ResourceAPIKey.WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, orgMemberMe, memberMe},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -283,7 +297,7 @@ func TestRolePermissions(t *testing.T) {
|
|||
Resource: rbac.ResourceUserData.WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, orgMemberMe, memberMe},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
|
||||
false: {orgAdmin, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -292,7 +306,7 @@ func TestRolePermissions(t *testing.T) {
|
|||
Resource: rbac.ResourceOrganizationMember.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, orgAdmin},
|
||||
false: {orgMemberMe, memberMe, otherOrgAdmin, otherOrgMember},
|
||||
false: {orgMemberMe, memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -301,7 +315,7 @@ func TestRolePermissions(t *testing.T) {
|
|||
Resource: rbac.ResourceOrganizationMember.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]authSubject{
|
||||
true: {admin, orgAdmin, orgMemberMe},
|
||||
false: {memberMe, otherOrgAdmin, otherOrgMember},
|
||||
false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -396,10 +410,14 @@ func TestListRoles(t *testing.T) {
|
|||
|
||||
// If this test is ever failing, just update the list to the roles
|
||||
// expected from the builtin set.
|
||||
// Always use constant strings, as if the names change, we need to write
|
||||
// a SQL migration to change the name on the backend.
|
||||
require.ElementsMatch(t, []string{
|
||||
"admin",
|
||||
"member",
|
||||
"auditor",
|
||||
"template-admin",
|
||||
"user-admin",
|
||||
},
|
||||
siteRoleNames)
|
||||
|
||||
|
|
|
@ -22,6 +22,15 @@ var (
|
|||
Type: "workspace",
|
||||
}
|
||||
|
||||
// ResourceWorkspaceExecution CRUD. Org + User owner
|
||||
// create = workspace remote execution
|
||||
// read = ?
|
||||
// update = ?
|
||||
// delete = ?
|
||||
ResourceWorkspaceExecution = Object{
|
||||
Type: "workspace_execution",
|
||||
}
|
||||
|
||||
// ResourceAuditLog
|
||||
// read = access audit log
|
||||
ResourceAuditLog = Object{
|
||||
|
|
|
@ -13,23 +13,27 @@ import (
|
|||
|
||||
// assignableSiteRoles returns all site wide roles that can be assigned.
|
||||
func (api *API) assignableSiteRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
// TODO: @emyrk in the future, allow granular subsets of roles to be returned based on the
|
||||
// role of the user.
|
||||
|
||||
actorRoles := httpmw.AuthorizationUserRoles(r)
|
||||
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceRoleAssignment) {
|
||||
httpapi.Forbidden(rw)
|
||||
return
|
||||
}
|
||||
|
||||
roles := rbac.SiteRoles()
|
||||
httpapi.Write(rw, http.StatusOK, convertRoles(roles))
|
||||
assignable := make([]rbac.Role, 0)
|
||||
for _, role := range roles {
|
||||
if rbac.CanAssignRole(actorRoles.Roles, role.Name) {
|
||||
assignable = append(assignable, role)
|
||||
}
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, convertRoles(assignable))
|
||||
}
|
||||
|
||||
// assignableSiteRoles returns all site wide roles that can be assigned.
|
||||
func (api *API) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
// TODO: @emyrk in the future, allow granular subsets of roles to be returned based on the
|
||||
// role of the user.
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
actorRoles := httpmw.AuthorizationUserRoles(r)
|
||||
|
||||
if !api.Authorize(r, rbac.ActionRead, rbac.ResourceOrgRoleAssignment.InOrg(organization.ID)) {
|
||||
httpapi.Forbidden(rw)
|
||||
|
@ -37,7 +41,14 @@ func (api *API) assignableOrgRoles(rw http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
roles := rbac.OrganizationRoles(organization.ID)
|
||||
httpapi.Write(rw, http.StatusOK, convertRoles(roles))
|
||||
assignable := make([]rbac.Role, 0)
|
||||
for _, role := range roles {
|
||||
if rbac.CanAssignRole(actorRoles.Roles, role.Name) {
|
||||
assignable = append(assignable, role)
|
||||
}
|
||||
}
|
||||
|
||||
httpapi.Write(rw, http.StatusOK, convertRoles(assignable))
|
||||
}
|
||||
|
||||
func (api *API) checkPermissions(rw http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -120,7 +120,7 @@ func TestListRoles(t *testing.T) {
|
|||
require.NoError(t, err, "create org")
|
||||
|
||||
const forbidden = "Forbidden"
|
||||
siteRoles := convertRoles(rbac.RoleAdmin(), "auditor")
|
||||
siteRoles := convertRoles(rbac.RoleAdmin(), "auditor", "template-admin", "user-admin")
|
||||
orgRoles := convertRoles(rbac.RoleOrgAdmin(admin.OrganizationID))
|
||||
|
||||
testCases := []struct {
|
||||
|
@ -131,19 +131,20 @@ func TestListRoles(t *testing.T) {
|
|||
AuthorizedError string
|
||||
}{
|
||||
{
|
||||
// Members cannot assign any roles
|
||||
Name: "MemberListSite",
|
||||
APICall: func(ctx context.Context) ([]codersdk.Role, error) {
|
||||
x, err := member.ListSiteRoles(ctx)
|
||||
return x, err
|
||||
},
|
||||
ExpectedRoles: siteRoles,
|
||||
ExpectedRoles: []codersdk.Role{},
|
||||
},
|
||||
{
|
||||
Name: "OrgMemberListOrg",
|
||||
APICall: func(ctx context.Context) ([]codersdk.Role, error) {
|
||||
return member.ListOrganizationRoles(ctx, admin.OrganizationID)
|
||||
},
|
||||
ExpectedRoles: orgRoles,
|
||||
ExpectedRoles: []codersdk.Role{},
|
||||
},
|
||||
{
|
||||
Name: "NonOrgMemberListOrg",
|
||||
|
@ -158,7 +159,7 @@ func TestListRoles(t *testing.T) {
|
|||
APICall: func(ctx context.Context) ([]codersdk.Role, error) {
|
||||
return orgAdmin.ListSiteRoles(ctx)
|
||||
},
|
||||
ExpectedRoles: siteRoles,
|
||||
ExpectedRoles: []codersdk.Role{},
|
||||
},
|
||||
{
|
||||
Name: "OrgAdminListOrg",
|
||||
|
|
|
@ -70,7 +70,7 @@ func (api *API) workspaceAgentDial(rw http.ResponseWriter, r *http.Request) {
|
|||
|
||||
workspaceAgent := httpmw.WorkspaceAgentParam(r)
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
if !api.Authorize(r, rbac.ActionUpdate, workspace) {
|
||||
if !api.Authorize(r, rbac.ActionCreate, workspace.ExecutionRBAC()) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
@ -302,6 +302,19 @@ func (api *API) workspaceAgentICEServers(rw http.ResponseWriter, _ *http.Request
|
|||
httpapi.Write(rw, http.StatusOK, api.ICEServers)
|
||||
}
|
||||
|
||||
// userWorkspaceAgentTurn is a user connecting to a remote workspace agent
|
||||
// through turn.
|
||||
func (api *API) userWorkspaceAgentTurn(rw http.ResponseWriter, r *http.Request) {
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
if !api.Authorize(r, rbac.ActionCreate, workspace.ExecutionRBAC()) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
// Passed authorization
|
||||
api.workspaceAgentTurn(rw, r)
|
||||
}
|
||||
|
||||
// workspaceAgentTurn proxies a WebSocket connection to the TURN server.
|
||||
func (api *API) workspaceAgentTurn(rw http.ResponseWriter, r *http.Request) {
|
||||
api.websocketWaitMutex.Lock()
|
||||
|
@ -364,7 +377,7 @@ func (api *API) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) {
|
|||
|
||||
workspaceAgent := httpmw.WorkspaceAgentParam(r)
|
||||
workspace := httpmw.WorkspaceParam(r)
|
||||
if !api.Authorize(r, rbac.ActionUpdate, workspace) {
|
||||
if !api.Authorize(r, rbac.ActionCreate, workspace.ExecutionRBAC()) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
@ -478,7 +491,7 @@ func (api *API) postWorkspaceAgentWireguardPeer(rw http.ResponseWriter, r *http.
|
|||
workspace = httpmw.WorkspaceParam(r)
|
||||
)
|
||||
|
||||
if !api.Authorize(r, rbac.ActionUpdate, workspace) {
|
||||
if !api.Authorize(r, rbac.ActionCreate, workspace.ExecutionRBAC()) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -43,7 +43,8 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
|
|||
})
|
||||
return
|
||||
}
|
||||
if !api.Authorize(r, rbac.ActionRead, workspace) {
|
||||
|
||||
if !api.Authorize(r, rbac.ActionCreate, workspace.ExecutionRBAC()) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -14,8 +14,7 @@ type Role struct {
|
|||
DisplayName string `json:"display_name"`
|
||||
}
|
||||
|
||||
// ListSiteRoles lists all available site wide roles.
|
||||
// This is not user specific.
|
||||
// ListSiteRoles lists all assignable site wide roles.
|
||||
func (c *Client) ListSiteRoles(ctx context.Context) ([]Role, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/roles", nil)
|
||||
if err != nil {
|
||||
|
@ -29,8 +28,7 @@ func (c *Client) ListSiteRoles(ctx context.Context) ([]Role, error) {
|
|||
return roles, json.NewDecoder(res.Body).Decode(&roles)
|
||||
}
|
||||
|
||||
// ListOrganizationRoles lists all available roles for a given organization.
|
||||
// This is not user specific.
|
||||
// ListOrganizationRoles lists all assignable roles for a given organization.
|
||||
func (c *Client) ListOrganizationRoles(ctx context.Context, org uuid.UUID) ([]Role, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/roles", org.String()), nil)
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { FC } from "react"
|
||||
import {
|
||||
HelpTooltip,
|
||||
HelpTooltipLink,
|
||||
HelpTooltipLinksGroup,
|
||||
HelpTooltipText,
|
||||
HelpTooltipTitle,
|
||||
} from "./HelpTooltip"
|
||||
|
||||
export const Language = {
|
||||
title: "What is a role?",
|
||||
text:
|
||||
"Coder role-based access control (RBAC) provides fine-grained access management. " +
|
||||
"View our docs on how to use the available roles.",
|
||||
link: "User Roles",
|
||||
}
|
||||
|
||||
export const UserRoleHelpTooltip: FC = () => {
|
||||
return (
|
||||
<HelpTooltip size="small">
|
||||
<HelpTooltipTitle>{Language.title}</HelpTooltipTitle>
|
||||
<HelpTooltipText>{Language.text}</HelpTooltipText>
|
||||
<HelpTooltipLinksGroup>
|
||||
<HelpTooltipLink href="https://coder.com/docs/coder-oss/latest/users">
|
||||
{Language.link}
|
||||
</HelpTooltipLink>
|
||||
</HelpTooltipLinksGroup>
|
||||
</HelpTooltip>
|
||||
)
|
||||
}
|
|
@ -2,4 +2,5 @@ export { AgentHelpTooltip } from "./AgentHelpTooltip"
|
|||
export { AuditHelpTooltip } from "./AuditHelpTooltip"
|
||||
export { OutdatedHelpTooltip } from "./OutdatedHelpTooltip"
|
||||
export { ResourcesHelpTooltip } from "./ResourcesHelpTooltip"
|
||||
export { UserRoleHelpTooltip } from "./UserRoleHelpTooltip"
|
||||
export { WorkspaceHelpTooltip } from "./WorkspaceHelpTooltip"
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { fireEvent, screen } from "@testing-library/react"
|
||||
import { Language as TooltipLanguage } from "components/Tooltips/HelpTooltip/HelpTooltip"
|
||||
import { Language as UserRoleLanguage } from "components/Tooltips/UserRoleHelpTooltip"
|
||||
import { render } from "testHelpers/renderHelpers"
|
||||
import { UsersTable } from "./UsersTable"
|
||||
|
||||
describe("AuditPage", () => {
|
||||
it("renders a page with a title and subtitle", async () => {
|
||||
// When
|
||||
render(
|
||||
<UsersTable
|
||||
onSuspendUser={() => jest.fn()}
|
||||
onActivateUser={() => jest.fn()}
|
||||
onResetUserPassword={() => jest.fn()}
|
||||
onUpdateUserRoles={() => jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Then
|
||||
const tooltipIcon = await screen.findByRole("button", { name: TooltipLanguage.ariaLabel })
|
||||
fireEvent.mouseOver(tooltipIcon)
|
||||
expect(await screen.findByText(UserRoleLanguage.title)).toBeInTheDocument()
|
||||
})
|
||||
})
|
|
@ -6,6 +6,8 @@ import TableHead from "@material-ui/core/TableHead"
|
|||
import TableRow from "@material-ui/core/TableRow"
|
||||
import { FC } from "react"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
import { UserRoleHelpTooltip } from "../Tooltips"
|
||||
import { UsersTableBody } from "./UsersTableBody"
|
||||
|
||||
export const Language = {
|
||||
|
@ -44,7 +46,12 @@ export const UsersTable: FC<UsersTableProps> = ({
|
|||
<TableRow>
|
||||
<TableCell>{Language.usernameLabel}</TableCell>
|
||||
<TableCell>{Language.statusLabel}</TableCell>
|
||||
<TableCell>{Language.rolesLabel}</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<span>{Language.rolesLabel}</span>
|
||||
<UserRoleHelpTooltip />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
{/* 1% is a trick to make the table cell width fit the content */}
|
||||
{canEditUsers && <TableCell width="1%" />}
|
||||
</TableRow>
|
||||
|
|
Loading…
Reference in New Issue