mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: implement deleting custom roles (#14101)
* chore: implement deleting custom roles * add trigger to delete role from organization members on delete * chore: add comments to explain populated field
This commit is contained in:
@@ -269,6 +269,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
httpmw.ExtractOrganizationParam(api.Database),
|
||||
)
|
||||
r.Patch("/organizations/{organization}/members/roles", api.patchOrgRoles)
|
||||
r.Delete("/organizations/{organization}/members/roles/{roleName}", api.deleteOrgRole)
|
||||
})
|
||||
|
||||
r.Route("/organizations/{organization}/groups", func(r chi.Router) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
@@ -92,7 +93,8 @@ func (api *API) patchOrgRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
},
|
||||
},
|
||||
ExcludeOrgRoles: false,
|
||||
OrganizationID: organization.ID,
|
||||
// Linter requires all fields to be set. This field is not actually required.
|
||||
OrganizationID: organization.ID,
|
||||
})
|
||||
// If it is a 404 (not found) error, ignore it.
|
||||
if err != nil && !httpapi.Is404Error(err) {
|
||||
@@ -131,6 +133,86 @@ func (api *API) patchOrgRoles(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Role(inserted))
|
||||
}
|
||||
|
||||
// deleteOrgRole will remove a custom role from an organization
|
||||
//
|
||||
// @Summary Delete a custom organization role
|
||||
// @ID delete-a-custom-organization-role
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Param organization path string true "Organization ID" format(uuid)
|
||||
// @Param roleName path string true "Role name"
|
||||
// @Tags Members
|
||||
// @Success 200 {array} codersdk.Role
|
||||
// @Router /organizations/{organization}/members/roles/{roleName} [delete]
|
||||
func (api *API) deleteOrgRole(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
auditor = api.AGPL.Auditor.Load()
|
||||
organization = httpmw.OrganizationParam(r)
|
||||
aReq, commitAudit = audit.InitRequest[database.CustomRole](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionDelete,
|
||||
OrganizationID: organization.ID,
|
||||
})
|
||||
)
|
||||
defer commitAudit()
|
||||
|
||||
rolename := chi.URLParam(r, "roleName")
|
||||
roles, err := api.Database.CustomRoles(ctx, database.CustomRolesParams{
|
||||
LookupRoles: []database.NameOrganizationPair{
|
||||
{
|
||||
Name: rolename,
|
||||
OrganizationID: organization.ID,
|
||||
},
|
||||
},
|
||||
ExcludeOrgRoles: false,
|
||||
// Linter requires all fields to be set. This field is not actually required.
|
||||
OrganizationID: organization.ID,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
if len(roles) == 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: fmt.Sprintf("No custom role with the name %s found", rolename),
|
||||
Detail: "no role found",
|
||||
Validations: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
if len(roles) > 1 {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: fmt.Sprintf("Multiple roles with the name %s found", rolename),
|
||||
Detail: "multiple roles found, this should never happen",
|
||||
Validations: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
aReq.Old = roles[0]
|
||||
|
||||
err = api.Database.DeleteCustomRole(ctx, database.DeleteCustomRoleParams{
|
||||
Name: rolename,
|
||||
OrganizationID: uuid.NullUUID{
|
||||
UUID: organization.ID,
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
if httpapi.IsUnauthorizedError(err) {
|
||||
httpapi.Forbidden(rw)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
aReq.New = database.CustomRole{}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func sdkPermissionToDB(p codersdk.Permission) database.CustomRolePermission {
|
||||
return database.CustomRolePermission{
|
||||
Negate: p.Negate,
|
||||
|
||||
@@ -310,6 +310,134 @@ func TestCustomOrganizationRole(t *testing.T) {
|
||||
_, err := owner.PatchOrganizationRole(ctx, newRole)
|
||||
require.ErrorContains(t, err, "Resource not found")
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)}
|
||||
owner, first := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureCustomRoles: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
orgAdmin, orgAdminUser := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID))
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
createdRole, err := orgAdmin.PatchOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
|
||||
require.NoError(t, err, "upsert role")
|
||||
|
||||
//nolint:gocritic // org_admin cannot assign to themselves
|
||||
_, err = owner.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, orgAdminUser.ID.String(), codersdk.UpdateRoles{
|
||||
// Give the user this custom role, to ensure when it is deleted, the user
|
||||
// is ok to be used.
|
||||
Roles: []string{createdRole.Name, rbac.ScopedRoleOrgAdmin(first.OrganizationID).Name},
|
||||
})
|
||||
require.NoError(t, err, "assign custom role to user")
|
||||
|
||||
existingRoles, err := orgAdmin.ListOrganizationRoles(ctx, first.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
|
||||
exists := slices.ContainsFunc(existingRoles, func(role codersdk.AssignableRoles) bool {
|
||||
return role.Name == createdRole.Name
|
||||
})
|
||||
require.True(t, exists, "custom role should exist")
|
||||
|
||||
// Delete the role
|
||||
err = orgAdmin.DeleteOrganizationRole(ctx, first.OrganizationID, createdRole.Name)
|
||||
require.NoError(t, err)
|
||||
|
||||
existingRoles, err = orgAdmin.ListOrganizationRoles(ctx, first.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
|
||||
exists = slices.ContainsFunc(existingRoles, func(role codersdk.AssignableRoles) bool {
|
||||
return role.Name == createdRole.Name
|
||||
})
|
||||
require.False(t, exists, "custom role should be deleted")
|
||||
|
||||
// Verify you can still assign roles.
|
||||
// There used to be a bug that if a member had a delete role, they
|
||||
// could not be assigned roles anymore.
|
||||
//nolint:gocritic // org_admin cannot assign to themselves
|
||||
_, err = owner.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, orgAdminUser.ID.String(), codersdk.UpdateRoles{
|
||||
Roles: []string{rbac.ScopedRoleOrgAdmin(first.OrganizationID).Name},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// Verify deleting a custom role cascades to all members
|
||||
t.Run("DeleteRoleCascadeMembers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)}
|
||||
owner, first := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureCustomRoles: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
orgAdmin, orgAdminUser := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID))
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
createdRole, err := orgAdmin.PatchOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
|
||||
require.NoError(t, err, "upsert role")
|
||||
|
||||
customRoleIdentifier := rbac.RoleIdentifier{
|
||||
Name: createdRole.Name,
|
||||
OrganizationID: first.OrganizationID,
|
||||
}
|
||||
|
||||
// Create a few members with the role
|
||||
coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, customRoleIdentifier)
|
||||
coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID), customRoleIdentifier)
|
||||
coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(first.OrganizationID), rbac.ScopedRoleOrgAuditor(first.OrganizationID), customRoleIdentifier)
|
||||
|
||||
// Verify members have the custom role
|
||||
originalMembers, err := orgAdmin.OrganizationMembers(ctx, first.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, originalMembers, 5) // 3 members + org admin + owner
|
||||
for _, member := range originalMembers {
|
||||
if member.UserID == orgAdminUser.ID || member.UserID == first.UserID {
|
||||
continue
|
||||
}
|
||||
|
||||
require.True(t, slices.ContainsFunc(member.Roles, func(role codersdk.SlimRole) bool {
|
||||
return role.Name == customRoleIdentifier.Name
|
||||
}), "member should have custom role")
|
||||
}
|
||||
|
||||
err = orgAdmin.DeleteOrganizationRole(ctx, first.OrganizationID, createdRole.Name)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the role was removed from all members
|
||||
members, err := orgAdmin.OrganizationMembers(ctx, first.OrganizationID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, members, 5) // 3 members + org admin + owner
|
||||
for _, member := range members {
|
||||
require.False(t, slices.ContainsFunc(member.Roles, func(role codersdk.SlimRole) bool {
|
||||
return role.Name == customRoleIdentifier.Name
|
||||
}), "role should be removed from all users")
|
||||
|
||||
// Verify the rest of the member's roles are unchanged
|
||||
original := originalMembers[slices.IndexFunc(originalMembers, func(haystack codersdk.OrganizationMemberWithUserData) bool {
|
||||
return haystack.UserID == member.UserID
|
||||
})]
|
||||
originalWithoutCustom := slices.DeleteFunc(original.Roles, func(role codersdk.SlimRole) bool {
|
||||
return role.Name == customRoleIdentifier.Name
|
||||
})
|
||||
require.ElementsMatch(t, originalWithoutCustom, member.Roles, "original roles are unchanged")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestListRoles(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user