mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
chore: implement custom role assignment for organization admins (#13570)
* chore: static role assignment mapping Until a dynamic approach is created in the database, only org-admins can assign custom organization roles.
This commit is contained in:
@ -157,7 +157,7 @@ func TestUpsertCustomRoles(t *testing.T) {
|
|||||||
org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
||||||
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
codersdk.ResourceWorkspace: {codersdk.ActionRead},
|
||||||
}),
|
}),
|
||||||
errorContains: "not allowed to grant this permission",
|
errorContains: "forbidden",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "user-escalation",
|
name: "user-escalation",
|
||||||
|
@ -239,10 +239,10 @@ var (
|
|||||||
rbac.ResourceApiKey.Type: rbac.ResourceApiKey.AvailableActions(),
|
rbac.ResourceApiKey.Type: rbac.ResourceApiKey.AvailableActions(),
|
||||||
rbac.ResourceGroup.Type: {policy.ActionCreate, policy.ActionUpdate},
|
rbac.ResourceGroup.Type: {policy.ActionCreate, policy.ActionUpdate},
|
||||||
rbac.ResourceAssignRole.Type: rbac.ResourceAssignRole.AvailableActions(),
|
rbac.ResourceAssignRole.Type: rbac.ResourceAssignRole.AvailableActions(),
|
||||||
|
rbac.ResourceAssignOrgRole.Type: rbac.ResourceAssignOrgRole.AvailableActions(),
|
||||||
rbac.ResourceSystem.Type: {policy.WildcardSymbol},
|
rbac.ResourceSystem.Type: {policy.WildcardSymbol},
|
||||||
rbac.ResourceOrganization.Type: {policy.ActionCreate, policy.ActionRead},
|
rbac.ResourceOrganization.Type: {policy.ActionCreate, policy.ActionRead},
|
||||||
rbac.ResourceOrganizationMember.Type: {policy.ActionCreate},
|
rbac.ResourceOrganizationMember.Type: {policy.ActionCreate},
|
||||||
rbac.ResourceAssignOrgRole.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionDelete},
|
|
||||||
rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionUpdate},
|
rbac.ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionUpdate},
|
||||||
rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(),
|
rbac.ResourceUser.Type: rbac.ResourceUser.AvailableActions(),
|
||||||
rbac.ResourceWorkspaceDormant.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop},
|
rbac.ResourceWorkspaceDormant.Type: {policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop},
|
||||||
@ -622,7 +622,7 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r
|
|||||||
roleAssign := rbac.ResourceAssignRole
|
roleAssign := rbac.ResourceAssignRole
|
||||||
shouldBeOrgRoles := false
|
shouldBeOrgRoles := false
|
||||||
if orgID != nil {
|
if orgID != nil {
|
||||||
roleAssign = roleAssign.InOrg(*orgID)
|
roleAssign = rbac.ResourceAssignOrgRole.InOrg(*orgID)
|
||||||
shouldBeOrgRoles = true
|
shouldBeOrgRoles = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -697,9 +697,15 @@ func (q *querier) canAssignRoles(ctx context.Context, orgID *uuid.UUID, added, r
|
|||||||
|
|
||||||
for _, roleName := range grantedRoles {
|
for _, roleName := range grantedRoles {
|
||||||
if _, isCustom := customRolesMap[roleName]; isCustom {
|
if _, isCustom := customRolesMap[roleName]; isCustom {
|
||||||
// For now, use a constant name so our static assign map still works.
|
// To support a dynamic mapping of what roles can assign what, we need
|
||||||
|
// to store this in the database. For now, just use a static role so
|
||||||
|
// owners and org admins can assign roles.
|
||||||
|
if roleName.IsOrgRole() {
|
||||||
|
roleName = rbac.CustomOrganizationRole(roleName.OrganizationID)
|
||||||
|
} else {
|
||||||
roleName = rbac.CustomSiteRole()
|
roleName = rbac.CustomSiteRole()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !rbac.CanAssignRole(actor.Roles, roleName) {
|
if !rbac.CanAssignRole(actor.Roles, roleName) {
|
||||||
return xerrors.Errorf("not authorized to assign role %q", roleName)
|
return xerrors.Errorf("not authorized to assign role %q", roleName)
|
||||||
@ -3476,10 +3482,16 @@ func (q *querier) UpsertCustomRole(ctx context.Context, arg database.UpsertCusto
|
|||||||
return database.CustomRole{}, NoActorError
|
return database.CustomRole{}, NoActorError
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: If this is an org role, check the org assign role type.
|
// Org and site role upsert share the same query. So switch the assertion based on the org uuid.
|
||||||
|
if arg.OrganizationID.UUID != uuid.Nil {
|
||||||
|
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil {
|
||||||
|
return database.CustomRole{}, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAssignRole); err != nil {
|
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAssignRole); err != nil {
|
||||||
return database.CustomRole{}, err
|
return database.CustomRole{}, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if arg.OrganizationID.UUID == uuid.Nil && len(arg.OrgPermissions) > 0 {
|
if arg.OrganizationID.UUID == uuid.Nil && len(arg.OrgPermissions) > 0 {
|
||||||
return database.CustomRole{}, xerrors.Errorf("organization permissions require specifying an organization id")
|
return database.CustomRole{}, xerrors.Errorf("organization permissions require specifying an organization id")
|
||||||
|
@ -625,7 +625,7 @@ func (s *MethodTestSuite) TestOrganization() {
|
|||||||
UserID: u.ID,
|
UserID: u.ID,
|
||||||
Roles: []string{codersdk.RoleOrganizationAdmin},
|
Roles: []string{codersdk.RoleOrganizationAdmin},
|
||||||
}).Asserts(
|
}).Asserts(
|
||||||
rbac.ResourceAssignRole.InOrg(o.ID), policy.ActionAssign,
|
rbac.ResourceAssignOrgRole.InOrg(o.ID), policy.ActionAssign,
|
||||||
rbac.ResourceOrganizationMember.InOrg(o.ID).WithID(u.ID), policy.ActionCreate)
|
rbac.ResourceOrganizationMember.InOrg(o.ID).WithID(u.ID), policy.ActionCreate)
|
||||||
}))
|
}))
|
||||||
s.Run("UpdateOrganization", s.Subtest(func(db database.Store, check *expects) {
|
s.Run("UpdateOrganization", s.Subtest(func(db database.Store, check *expects) {
|
||||||
@ -681,8 +681,8 @@ func (s *MethodTestSuite) TestOrganization() {
|
|||||||
WithCancelled(sql.ErrNoRows.Error()).
|
WithCancelled(sql.ErrNoRows.Error()).
|
||||||
Asserts(
|
Asserts(
|
||||||
mem, policy.ActionRead,
|
mem, policy.ActionRead,
|
||||||
rbac.ResourceAssignRole.InOrg(o.ID), policy.ActionAssign, // org-mem
|
rbac.ResourceAssignOrgRole.InOrg(o.ID), policy.ActionAssign, // org-mem
|
||||||
rbac.ResourceAssignRole.InOrg(o.ID), policy.ActionDelete, // org-admin
|
rbac.ResourceAssignOrgRole.InOrg(o.ID), policy.ActionDelete, // org-admin
|
||||||
).Returns(out)
|
).Returns(out)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -1257,7 +1257,7 @@ func (s *MethodTestSuite) TestUser() {
|
|||||||
}), convertSDKPerm),
|
}), convertSDKPerm),
|
||||||
}).Asserts(
|
}).Asserts(
|
||||||
// First check
|
// First check
|
||||||
rbac.ResourceAssignRole, policy.ActionCreate,
|
rbac.ResourceAssignOrgRole.InOrg(orgID), policy.ActionCreate,
|
||||||
// Escalation checks
|
// Escalation checks
|
||||||
rbac.ResourceTemplate.InOrg(orgID), policy.ActionCreate,
|
rbac.ResourceTemplate.InOrg(orgID), policy.ActionCreate,
|
||||||
rbac.ResourceTemplate.InOrg(orgID), policy.ActionRead,
|
rbac.ResourceTemplate.InOrg(orgID), policy.ActionRead,
|
||||||
|
@ -87,6 +87,10 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) {
|
|||||||
UserID: member.UserID,
|
UserID: member.UserID,
|
||||||
OrgID: organization.ID,
|
OrgID: organization.ID,
|
||||||
})
|
})
|
||||||
|
if httpapi.Is404Error(err) {
|
||||||
|
httpapi.Forbidden(rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
|
@ -28,6 +28,7 @@ var (
|
|||||||
// ResourceAssignOrgRole
|
// ResourceAssignOrgRole
|
||||||
// Valid Actions
|
// Valid Actions
|
||||||
// - "ActionAssign" :: ability to assign org scoped roles
|
// - "ActionAssign" :: ability to assign org scoped roles
|
||||||
|
// - "ActionCreate" :: ability to create/delete/edit custom roles within an organization
|
||||||
// - "ActionDelete" :: ability to delete org scoped roles
|
// - "ActionDelete" :: ability to delete org scoped roles
|
||||||
// - "ActionRead" :: view what roles are assignable
|
// - "ActionRead" :: view what roles are assignable
|
||||||
ResourceAssignOrgRole = Object{
|
ResourceAssignOrgRole = Object{
|
||||||
|
@ -218,6 +218,7 @@ var RBACPermissions = map[string]PermissionDefinition{
|
|||||||
ActionAssign: actDef("ability to assign org scoped roles"),
|
ActionAssign: actDef("ability to assign org scoped roles"),
|
||||||
ActionRead: actDef("view what roles are assignable"),
|
ActionRead: actDef("view what roles are assignable"),
|
||||||
ActionDelete: actDef("ability to delete org scoped roles"),
|
ActionDelete: actDef("ability to delete org scoped roles"),
|
||||||
|
ActionCreate: actDef("ability to create/delete/edit custom roles within an organization"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"oauth2_app": {
|
"oauth2_app": {
|
||||||
|
@ -25,6 +25,7 @@ const (
|
|||||||
// This is used for what roles can assign other roles.
|
// This is used for what roles can assign other roles.
|
||||||
// TODO: Make this more dynamic to allow other roles to grant.
|
// TODO: Make this more dynamic to allow other roles to grant.
|
||||||
customSiteRole string = "custom-site-role"
|
customSiteRole string = "custom-site-role"
|
||||||
|
customOrganizationRole string = "custom-organization-role"
|
||||||
|
|
||||||
orgAdmin string = "organization-admin"
|
orgAdmin string = "organization-admin"
|
||||||
orgMember string = "organization-member"
|
orgMember string = "organization-member"
|
||||||
@ -127,6 +128,9 @@ func (r *RoleIdentifier) UnmarshalJSON(data []byte) error {
|
|||||||
|
|
||||||
func RoleOwner() RoleIdentifier { return RoleIdentifier{Name: owner} }
|
func RoleOwner() RoleIdentifier { return RoleIdentifier{Name: owner} }
|
||||||
func CustomSiteRole() RoleIdentifier { return RoleIdentifier{Name: customSiteRole} }
|
func CustomSiteRole() RoleIdentifier { return RoleIdentifier{Name: customSiteRole} }
|
||||||
|
func CustomOrganizationRole(orgID uuid.UUID) RoleIdentifier {
|
||||||
|
return RoleIdentifier{Name: customOrganizationRole, OrganizationID: orgID}
|
||||||
|
}
|
||||||
func RoleTemplateAdmin() RoleIdentifier { return RoleIdentifier{Name: templateAdmin} }
|
func RoleTemplateAdmin() RoleIdentifier { return RoleIdentifier{Name: templateAdmin} }
|
||||||
func RoleUserAdmin() RoleIdentifier { return RoleIdentifier{Name: userAdmin} }
|
func RoleUserAdmin() RoleIdentifier { return RoleIdentifier{Name: userAdmin} }
|
||||||
func RoleMember() RoleIdentifier { return RoleIdentifier{Name: member} }
|
func RoleMember() RoleIdentifier { return RoleIdentifier{Name: member} }
|
||||||
@ -314,6 +318,9 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
|||||||
DisplayName: "User Admin",
|
DisplayName: "User Admin",
|
||||||
Site: Permissions(map[string][]policy.Action{
|
Site: Permissions(map[string][]policy.Action{
|
||||||
ResourceAssignRole.Type: {policy.ActionAssign, policy.ActionDelete, policy.ActionRead},
|
ResourceAssignRole.Type: {policy.ActionAssign, policy.ActionDelete, policy.ActionRead},
|
||||||
|
// Need organization assign as well to create users. At present, creating a user
|
||||||
|
// will always assign them to some organization.
|
||||||
|
ResourceAssignOrgRole.Type: {policy.ActionAssign, policy.ActionDelete, policy.ActionRead},
|
||||||
ResourceUser.Type: {
|
ResourceUser.Type: {
|
||||||
policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete,
|
policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete,
|
||||||
policy.ActionUpdatePersonal, policy.ActionReadPersonal,
|
policy.ActionUpdatePersonal, policy.ActionReadPersonal,
|
||||||
@ -361,7 +368,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
|||||||
Site: []Permission{},
|
Site: []Permission{},
|
||||||
Org: map[string][]Permission{
|
Org: map[string][]Permission{
|
||||||
// Org admins should not have workspace exec perms.
|
// Org admins should not have workspace exec perms.
|
||||||
organizationID.String(): append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant), Permissions(map[string][]policy.Action{
|
organizationID.String(): append(allPermsExcept(ResourceWorkspace, ResourceWorkspaceDormant, ResourceAssignRole), Permissions(map[string][]policy.Action{
|
||||||
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop},
|
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop},
|
||||||
ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH),
|
ResourceWorkspace.Type: slice.Omit(ResourceWorkspace.AvailableActions(), policy.ActionApplicationConnect, policy.ActionSSH),
|
||||||
})...),
|
})...),
|
||||||
@ -417,6 +424,7 @@ var assignRoles = map[string]map[string]bool{
|
|||||||
templateAdmin: true,
|
templateAdmin: true,
|
||||||
userAdmin: true,
|
userAdmin: true,
|
||||||
customSiteRole: true,
|
customSiteRole: true,
|
||||||
|
customOrganizationRole: true,
|
||||||
},
|
},
|
||||||
owner: {
|
owner: {
|
||||||
owner: true,
|
owner: true,
|
||||||
@ -427,6 +435,7 @@ var assignRoles = map[string]map[string]bool{
|
|||||||
templateAdmin: true,
|
templateAdmin: true,
|
||||||
userAdmin: true,
|
userAdmin: true,
|
||||||
customSiteRole: true,
|
customSiteRole: true,
|
||||||
|
customOrganizationRole: true,
|
||||||
},
|
},
|
||||||
userAdmin: {
|
userAdmin: {
|
||||||
member: true,
|
member: true,
|
||||||
@ -435,6 +444,7 @@ var assignRoles = map[string]map[string]bool{
|
|||||||
orgAdmin: {
|
orgAdmin: {
|
||||||
orgAdmin: true,
|
orgAdmin: true,
|
||||||
orgMember: true,
|
orgMember: true,
|
||||||
|
customOrganizationRole: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -596,6 +606,13 @@ func RoleByName(name RoleIdentifier) (Role, error) {
|
|||||||
return Role{}, xerrors.Errorf("expect a org id for role %q", name.String())
|
return Role{}, xerrors.Errorf("expect a org id for role %q", name.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This can happen if a custom role shares the same name as a built-in role.
|
||||||
|
// You could make an org role called "owner", and we should not return the
|
||||||
|
// owner role itself.
|
||||||
|
if name.OrganizationID != role.Identifier.OrganizationID {
|
||||||
|
return Role{}, xerrors.Errorf("role %q not found", name.String())
|
||||||
|
}
|
||||||
|
|
||||||
return role, nil
|
return role, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,6 +279,15 @@ func TestRolePermissions(t *testing.T) {
|
|||||||
Name: "OrgRoleAssignment",
|
Name: "OrgRoleAssignment",
|
||||||
Actions: []policy.Action{policy.ActionAssign, policy.ActionDelete},
|
Actions: []policy.Action{policy.ActionAssign, policy.ActionDelete},
|
||||||
Resource: rbac.ResourceAssignOrgRole.InOrg(orgID),
|
Resource: rbac.ResourceAssignOrgRole.InOrg(orgID),
|
||||||
|
AuthorizeMap: map[bool][]authSubject{
|
||||||
|
true: {owner, orgAdmin, userAdmin},
|
||||||
|
false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "CreateOrgRoleAssignment",
|
||||||
|
Actions: []policy.Action{policy.ActionCreate},
|
||||||
|
Resource: rbac.ResourceAssignOrgRole.InOrg(orgID),
|
||||||
AuthorizeMap: map[bool][]authSubject{
|
AuthorizeMap: map[bool][]authSubject{
|
||||||
true: {owner, orgAdmin},
|
true: {owner, orgAdmin},
|
||||||
false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
||||||
@ -289,8 +298,8 @@ func TestRolePermissions(t *testing.T) {
|
|||||||
Actions: []policy.Action{policy.ActionRead},
|
Actions: []policy.Action{policy.ActionRead},
|
||||||
Resource: rbac.ResourceAssignOrgRole.InOrg(orgID),
|
Resource: rbac.ResourceAssignOrgRole.InOrg(orgID),
|
||||||
AuthorizeMap: map[bool][]authSubject{
|
AuthorizeMap: map[bool][]authSubject{
|
||||||
true: {owner, orgAdmin, orgMemberMe},
|
true: {owner, orgAdmin, orgMemberMe, userAdmin, userAdmin},
|
||||||
false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
|
false: {otherOrgAdmin, otherOrgMember, memberMe, templateAdmin},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -144,9 +144,14 @@ func assignableRoles(actorRoles rbac.ExpandableRoles, roles []rbac.Role, customR
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, role := range customRoles {
|
for _, role := range customRoles {
|
||||||
|
canAssign := rbac.CanAssignRole(actorRoles, rbac.CustomSiteRole())
|
||||||
|
if role.RoleIdentifier().IsOrgRole() {
|
||||||
|
canAssign = rbac.CanAssignRole(actorRoles, rbac.CustomOrganizationRole(role.OrganizationID.UUID))
|
||||||
|
}
|
||||||
|
|
||||||
assignable = append(assignable, codersdk.AssignableRoles{
|
assignable = append(assignable, codersdk.AssignableRoles{
|
||||||
Role: db2sdk.Role(role),
|
Role: db2sdk.Role(role),
|
||||||
Assignable: rbac.CanAssignRole(actorRoles, role.RoleIdentifier()),
|
Assignable: canAssign,
|
||||||
BuiltIn: false,
|
BuiltIn: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ const (
|
|||||||
var RBACResourceActions = map[RBACResource][]RBACAction{
|
var RBACResourceActions = map[RBACResource][]RBACAction{
|
||||||
ResourceWildcard: {},
|
ResourceWildcard: {},
|
||||||
ResourceApiKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
ResourceApiKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||||
ResourceAssignOrgRole: {ActionAssign, ActionDelete, ActionRead},
|
ResourceAssignOrgRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead},
|
||||||
ResourceAssignRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead},
|
ResourceAssignRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead},
|
||||||
ResourceAuditLog: {ActionCreate, ActionRead},
|
ResourceAuditLog: {ActionCreate, ActionRead},
|
||||||
ResourceDebugInfo: {ActionRead},
|
ResourceDebugInfo: {ActionRead},
|
||||||
|
@ -203,6 +203,7 @@ func TestCustomOrganizationRole(t *testing.T) {
|
|||||||
_, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, codersdk.Role{
|
_, err := owner.PatchOrganizationRole(ctx, first.OrganizationID, codersdk.Role{
|
||||||
Name: "Bad_Name", // No underscores allowed
|
Name: "Bad_Name", // No underscores allowed
|
||||||
DisplayName: "Testing Purposes",
|
DisplayName: "Testing Purposes",
|
||||||
|
OrganizationID: first.OrganizationID.String(),
|
||||||
SitePermissions: nil,
|
SitePermissions: nil,
|
||||||
OrganizationPermissions: nil,
|
OrganizationPermissions: nil,
|
||||||
UserPermissions: nil,
|
UserPermissions: nil,
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
"github.com/coder/coder/v2/coderd/schedule/cron"
|
"github.com/coder/coder/v2/coderd/schedule/cron"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||||
@ -237,3 +238,61 @@ func TestCreateFirstUser_Entitlements_Trial(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, entitlements.Trial, "Trial license should be immediately active.")
|
require.True(t, entitlements.Trial, "Trial license should be immediately active.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAssignCustomOrgRoles verifies an organization admin (not just an owner) can create
|
||||||
|
// a custom role and assign it to an organization user.
|
||||||
|
func TestAssignCustomOrgRoles(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
dv := coderdtest.DeploymentValues(t)
|
||||||
|
dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)}
|
||||||
|
|
||||||
|
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||||
|
Options: &coderdtest.Options{
|
||||||
|
DeploymentValues: dv,
|
||||||
|
IncludeProvisionerDaemon: true,
|
||||||
|
},
|
||||||
|
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||||
|
Features: license.Features{
|
||||||
|
codersdk.FeatureCustomRoles: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID))
|
||||||
|
tv := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
||||||
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, tv.ID)
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitShort)
|
||||||
|
// Create a custom role as an organization admin that allows making templates.
|
||||||
|
auditorRole, err := client.PatchOrganizationRole(ctx, owner.OrganizationID, codersdk.Role{
|
||||||
|
Name: "org-template-admin",
|
||||||
|
OrganizationID: owner.OrganizationID.String(),
|
||||||
|
DisplayName: "Template Admin",
|
||||||
|
SitePermissions: nil,
|
||||||
|
OrganizationPermissions: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
|
||||||
|
codersdk.ResourceTemplate: codersdk.RBACResourceActions[codersdk.ResourceTemplate], // All template perms
|
||||||
|
}),
|
||||||
|
UserPermissions: nil,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
createTemplateReq := codersdk.CreateTemplateRequest{
|
||||||
|
Name: "name",
|
||||||
|
DisplayName: "Template",
|
||||||
|
VersionID: tv.ID,
|
||||||
|
}
|
||||||
|
memberClient, member := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||||
|
// Check the member cannot create a template
|
||||||
|
_, err = memberClient.CreateTemplate(ctx, owner.OrganizationID, createTemplateReq)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Assign new role to the member as the org admin
|
||||||
|
_, err = client.UpdateOrganizationMemberRoles(ctx, owner.OrganizationID, member.ID.String(), codersdk.UpdateRoles{
|
||||||
|
Roles: []string{auditorRole.Name},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Now the member can create the template
|
||||||
|
_, err = memberClient.CreateTemplate(ctx, owner.OrganizationID, createTemplateReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user