fix(apiserver): require rbac:control scope all to create, extend or delete roles (bug-1958)
Cause: RoleController.CreateRole, AddPermissionsForRole and
DeleteRoleAndPermissions relied on RequirePermission("rbac:control"), which
accepts any scope, so a user with rbac:control scoped to self could add
cluster:control to a role it holds.
Fix: checkRoleChangePermission() in controllers/roles.go refuses with 403
unless one of the caller's grants (checked before deduplication) is
rbac:control with scope all, reusing canChangeUserRoles from bug-1952.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
954c96cf5b
commit
736ed81d65
3 changed files with 161 additions and 0 deletions
|
|
@ -507,6 +507,10 @@ func (rc *RoleController) AddPermissionsForRole(ctx *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !checkRoleChangePermission(ctx, "add permissions to", req.RoleName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err := preCheckRoleRequest(&req, false)
|
err := preCheckRoleRequest(&req, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to create role request", zap.Error(err))
|
logger.Error("Failed to create role request", zap.Error(err))
|
||||||
|
|
@ -549,6 +553,11 @@ func (rc *RoleController) DeleteRoleAndPermissions(ctx *gin.Context) {
|
||||||
ReturnError(ctx, common.ERROR_CODE, err.Error(), nil)
|
ReturnError(ctx, common.ERROR_CODE, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !checkRoleChangePermission(ctx, "delete", req_rolename) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// step 1: role name whether exists
|
// step 1: role name whether exists
|
||||||
allRoleNames := store.GetAllRoleNames(false)
|
allRoleNames := store.GetAllRoleNames(false)
|
||||||
found := false
|
found := false
|
||||||
|
|
@ -602,6 +611,10 @@ func (rc *RoleController) CreateRole(ctx *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !checkRoleChangePermission(ctx, "create", req.RoleName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// precheck the request parameter
|
// precheck the request parameter
|
||||||
err := preCheckRoleRequest(&req, true)
|
err := preCheckRoleRequest(&req, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -632,6 +645,30 @@ func (rc *RoleController) CreateRole(ctx *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const errNoRoleDefinitionPermission = "no permission: creating, changing or deleting roles requires rbac:control with scope all"
|
||||||
|
|
||||||
|
// checkRoleChangePermission enforces that only an rbac:control grant with
|
||||||
|
// scope all may create, extend or delete roles (bug-1958): with a narrower
|
||||||
|
// scope such as self a user could add any permission, cluster:control
|
||||||
|
// included, to a role it already holds. The rule is the one canChangeUserRoles
|
||||||
|
// applies to user-role assignment (bug-1952). On refusal it writes the error
|
||||||
|
// response and returns false; the caller must return.
|
||||||
|
func checkRoleChangePermission(ctx *gin.Context, action string, roleName string) bool {
|
||||||
|
token := ctx.GetHeader("Authorization")
|
||||||
|
submitUser, err := GetUsernameFromToken(token)
|
||||||
|
if err != nil {
|
||||||
|
ReturnError(ctx, common.ERROR_CODE, err.Error(), nil)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !canChangeUserRoles(submitUser) {
|
||||||
|
logger.Error("no permission to "+action+" role", zap.String("submit_user", submitUser),
|
||||||
|
zap.String("role_name", roleName))
|
||||||
|
ReturnError(ctx, common.ERROR_AUTH_CODE, errNoRoleDefinitionPermission, nil)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func preCheckRole(role_name string) error {
|
func preCheckRole(role_name string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
122
src/frontend/apiserver/controllers/roles_rbac_test.go
Normal file
122
src/frontend/apiserver/controllers/roles_rbac_test.go
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
//go:build !cgo
|
||||||
|
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"apiserver/store"
|
||||||
|
"apiserver/utils"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RBAC fixture for bug-1958: mallory holds rbac:control scoped to self,
|
||||||
|
// root holds it scoped to all. spare_role is a custom role nobody holds.
|
||||||
|
const roleChangeRBAC = `
|
||||||
|
permissions:
|
||||||
|
- rbac:control
|
||||||
|
- rbac:view
|
||||||
|
- cluster:control
|
||||||
|
roles:
|
||||||
|
rbac_self:
|
||||||
|
permissions:
|
||||||
|
- action: rbac:control
|
||||||
|
domain:
|
||||||
|
- type: self
|
||||||
|
rbac_admin:
|
||||||
|
permissions:
|
||||||
|
- action: rbac:control
|
||||||
|
domain:
|
||||||
|
- type: all
|
||||||
|
spare_role:
|
||||||
|
permissions:
|
||||||
|
- action: rbac:view
|
||||||
|
domain:
|
||||||
|
- type: self
|
||||||
|
users:
|
||||||
|
mallory:
|
||||||
|
roles:
|
||||||
|
- name: rbac_self
|
||||||
|
root:
|
||||||
|
roles:
|
||||||
|
- name: rbac_admin
|
||||||
|
`
|
||||||
|
|
||||||
|
func newRoleChangeRouter() *gin.Engine {
|
||||||
|
r := newTestRouter()
|
||||||
|
rc := &RoleController{}
|
||||||
|
r.POST("/xce/v1/roles/create", RequirePermission("rbac:control"), rc.CreateRole)
|
||||||
|
r.POST("/xce/v1/roles/:roleid/permissions", RequirePermission("rbac:control"), rc.AddPermissionsForRole)
|
||||||
|
r.DELETE("/xce/v1/roles/:rolename", RequirePermission("rbac:control"), rc.DeleteRoleAndPermissions)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
const clusterControlPerm = `{"permissions":[{"permission_name":"cluster:control","domain_type":"all"}]}`
|
||||||
|
|
||||||
|
// bug-1958: rbac:control scoped to self must not add cluster:control (or
|
||||||
|
// anything) to a role the user holds.
|
||||||
|
func TestAddPermissionsForRole_SelfScopeCannotExtendOwnRole(t *testing.T) {
|
||||||
|
setupRBACTest(t, roleChangeRBAC)
|
||||||
|
r := newRoleChangeRouter()
|
||||||
|
|
||||||
|
w := doRoleRequest(r, http.MethodPost, "/xce/v1/roles/rbac_self/permissions", "mallory", clusterControlPerm)
|
||||||
|
if utils.ContainsField(store.GetRoleAllPermissions("rbac_self"), "cluster:control") {
|
||||||
|
t.Errorf("privilege escalation: mallory (rbac:control scope self) added cluster:control to its own role (status %d, %s)",
|
||||||
|
w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if w.Code != http.StatusForbidden {
|
||||||
|
t.Errorf("status = %d, want 403 (%s)", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bug-1958: rbac:control scoped to self must not create roles.
|
||||||
|
func TestCreateRole_SelfScopeRejected(t *testing.T) {
|
||||||
|
setupRBACTest(t, roleChangeRBAC)
|
||||||
|
r := newRoleChangeRouter()
|
||||||
|
|
||||||
|
w := doRoleRequest(r, http.MethodPost, "/xce/v1/roles/create", "mallory",
|
||||||
|
`{"role_name":"evil","permissions":[{"permission_name":"cluster:control","domain_type":"all"}]}`)
|
||||||
|
if w.Code != http.StatusForbidden {
|
||||||
|
t.Errorf("status = %d, want 403 (%s)", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if utils.ContainsField(store.GetAllRoleNames(false), "evil") {
|
||||||
|
t.Error("role was created although the request was rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bug-1958: rbac:control scoped to self must not delete roles.
|
||||||
|
func TestDeleteRole_SelfScopeRejected(t *testing.T) {
|
||||||
|
setupRBACTest(t, roleChangeRBAC)
|
||||||
|
r := newRoleChangeRouter()
|
||||||
|
|
||||||
|
w := doRoleRequest(r, http.MethodDelete, "/xce/v1/roles/spare_role", "mallory", "")
|
||||||
|
if w.Code != http.StatusForbidden {
|
||||||
|
t.Errorf("status = %d, want 403 (%s)", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if !utils.ContainsField(store.GetAllRoleNames(false), "spare_role") {
|
||||||
|
t.Error("role was deleted although the request was rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bug-1958: rbac:control scoped to all may still create, extend and delete
|
||||||
|
// roles.
|
||||||
|
func TestRoleChanges_AllScopeAllowed(t *testing.T) {
|
||||||
|
setupRBACTest(t, roleChangeRBAC)
|
||||||
|
r := newRoleChangeRouter()
|
||||||
|
|
||||||
|
// ReturnSuccess always answers HTTP 200 (code 201 is in the envelope).
|
||||||
|
w := doRoleRequest(r, http.MethodPost, "/xce/v1/roles/create", "root",
|
||||||
|
`{"role_name":"ops","permissions":[{"permission_name":"rbac:view","domain_type":"all"}]}`)
|
||||||
|
if w.Code != http.StatusOK || !utils.ContainsField(store.GetAllRoleNames(false), "ops") {
|
||||||
|
t.Fatalf("root creating role: status %d (%s)", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
w = doRoleRequest(r, http.MethodPost, "/xce/v1/roles/ops/permissions", "root", clusterControlPerm)
|
||||||
|
if w.Code != http.StatusOK || !utils.ContainsField(store.GetRoleAllPermissions("ops"), "cluster:control") {
|
||||||
|
t.Fatalf("root extending role: status %d (%s), perms %v", w.Code, w.Body.String(), store.GetRoleAllPermissions("ops"))
|
||||||
|
}
|
||||||
|
w = doRoleRequest(r, http.MethodDelete, "/xce/v1/roles/ops", "root", "")
|
||||||
|
if w.Code != http.StatusOK || utils.ContainsField(store.GetAllRoleNames(false), "ops") {
|
||||||
|
t.Errorf("root deleting role: status %d (%s)", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -480,6 +480,8 @@ const errNoRoleChangePermission = "no permission: granting or revoking user role
|
||||||
// outright: letting it change its own roles would let it grant itself any
|
// outright: letting it change its own roles would let it grant itself any
|
||||||
// role, cluster_admin included. All grants are checked before
|
// role, cluster_admin included. All grants are checked before
|
||||||
// deduplication, which would otherwise prefer a narrower scope over all.
|
// deduplication, which would otherwise prefer a narrower scope over all.
|
||||||
|
// Role definitions (create, add permissions, delete) follow the same rule
|
||||||
|
// (bug-1958, see canChangeRoles).
|
||||||
func canChangeUserRoles(user string) bool {
|
func canChangeUserRoles(user string) bool {
|
||||||
for _, p := range collectUserPermissionsWithType(user) {
|
for _, p := range collectUserPermissionsWithType(user) {
|
||||||
if p.Permission == "rbac:control" && p.DomainType == models.DomainTypeAll {
|
if p.Permission == "rbac:control" && p.DomainType == models.DomainTypeAll {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue