fix: keystone-role-assignments-list-scope (#25652)

Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
This commit is contained in:
Jian Qiu
2026-09-10 17:25:32 +08:00
committed by GitHub
parent 3c47ebba45
commit 0590e3a434
5 changed files with 239 additions and 4 deletions

View File

@@ -32,6 +32,7 @@ import (
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/keystone/options"
"yunion.io/x/onecloud/pkg/mcclient"
@@ -584,6 +585,17 @@ func AddAdhocHandlers(version string, app *appsrv.Application) {
}
func roleAssignmentHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
userCred := auth.FetchUserCredential(ctx, policy.FilterPolicyCredential)
if userCred == nil {
httperrors.UnauthorizedError(ctx, w, "unauthorized")
return
}
allowScope, policyResult := policy.PolicyManager.AllowScope(userCred, api.SERVICE_TYPE, "role_assignments", policy.PolicyActionList)
if policyResult.Result.IsDeny() {
httperrors.ForbiddenError(ctx, w, "not allow to list role assignments")
return
}
_, query, _ := appsrv.FetchEnv(ctx, w, r)
input := api.RoleAssignmentsInput{}
err := query.Unmarshal(&input)
@@ -592,6 +604,12 @@ func roleAssignmentHandler(ctx context.Context, w http.ResponseWriter, r *http.R
return
}
restrictDomainId, err := checkRoleAssignmentListInput(allowScope, userCred, &input)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
includeNames := (input.IncludeNames != nil)
effective := (input.Effective != nil)
includeSub := (input.IncludeSubtree != nil)
@@ -607,7 +625,7 @@ func roleAssignmentHandler(ctx context.Context, w http.ResponseWriter, r *http.R
offset = *input.Offset
}
results, total, err := AssignmentManager.FetchAll(
results, total, err := AssignmentManager.fetchAll(
input.User.Id,
input.Group.Id,
input.Role.Id,
@@ -620,6 +638,7 @@ func roleAssignmentHandler(ctx context.Context, w http.ResponseWriter, r *http.R
input.Domains,
input.Projects,
input.ProjectDomains,
restrictDomainId,
includeNames, effective, includeSub, includeSystem, includePolicies,
limit, offset)
@@ -639,6 +658,7 @@ func roleAssignmentHandler(ctx context.Context, w http.ResponseWriter, r *http.R
func (manager *SAssignmentManager) queryAll(
userId, groupId, roleId, domainId, projectId string, projectDomainId string,
users, groups, roles, domains, projects, projectDomains []string,
restrictDomainId string,
) *sqlchemy.SQuery {
assigments := manager.Query().SubQuery()
q := assigments.Query(
@@ -749,6 +769,13 @@ func (manager *SAssignmentManager) queryAll(
))
q = q.In("domain_id", subq.SubQuery()).In("type", []string{api.AssignmentUserDomain, api.AssignmentGroupDomain})
}
if len(restrictDomainId) > 0 {
projSubq := ProjectManager.Query("id").Equals("domain_id", restrictDomainId).SubQuery()
q = q.Filter(sqlchemy.OR(
sqlchemy.In(q.Field("project_id"), projSubq),
sqlchemy.Equals(q.Field("domain_id"), restrictDomainId),
))
}
return q
}
@@ -807,13 +834,27 @@ func (manager *SAssignmentManager) FetchAll(
userStrs, groupStrs, roleStrs, domainStrs, projectStrs, projectDomainStrs []string,
includeNames, effective, includeSub, includeSystem, includePolicies bool,
limit, offset int) ([]api.SRoleAssignment, int64, error) {
return manager.fetchAll(
userId, groupId, roleId, domainId, projectId, projectDomainId,
userStrs, groupStrs, roleStrs, domainStrs, projectStrs, projectDomainStrs,
"",
includeNames, effective, includeSub, includeSystem, includePolicies,
limit, offset)
}
func (manager *SAssignmentManager) fetchAll(
userId, groupId, roleId, domainId, projectId string, projectDomainId string,
userStrs, groupStrs, roleStrs, domainStrs, projectStrs, projectDomainStrs []string,
restrictDomainId string,
includeNames, effective, includeSub, includeSystem, includePolicies bool,
limit, offset int) ([]api.SRoleAssignment, int64, error) {
var q *sqlchemy.SQuery
if effective {
usrq := manager.queryAll(userId, "", roleId, domainId, projectId, projectDomainId, userStrs, nil, roleStrs, domainStrs, projectStrs, projectDomainStrs).In("type", []string{api.AssignmentUserProject, api.AssignmentUserDomain})
usrq := manager.queryAll(userId, "", roleId, domainId, projectId, projectDomainId, userStrs, nil, roleStrs, domainStrs, projectStrs, projectDomainStrs, restrictDomainId).In("type", []string{api.AssignmentUserProject, api.AssignmentUserDomain})
memberships := UsergroupManager.Query("user_id", "group_id").SubQuery()
grpproj := manager.queryAll("", groupId, roleId, domainId, projectId, projectDomainId, nil, groupStrs, roleStrs, domainStrs, projectStrs, projectDomainStrs).In("type", []string{api.AssignmentGroupProject, api.AssignmentGroupDomain}).SubQuery()
grpproj := manager.queryAll("", groupId, roleId, domainId, projectId, projectDomainId, nil, groupStrs, roleStrs, domainStrs, projectStrs, projectDomainStrs, restrictDomainId).In("type", []string{api.AssignmentGroupProject, api.AssignmentGroupDomain}).SubQuery()
q2 := grpproj.Query(
grpproj.Field("type"),
memberships.Field("user_id"),
@@ -837,7 +878,7 @@ func (manager *SAssignmentManager) FetchAll(
q = sqlchemy.Union(usrq, q2).Query().Distinct()
} else {
q = manager.queryAll(userId, groupId, roleId, domainId, projectId, projectDomainId, userStrs, groupStrs, roleStrs, domainStrs, projectStrs, projectDomainStrs).Distinct()
q = manager.queryAll(userId, groupId, roleId, domainId, projectId, projectDomainId, userStrs, groupStrs, roleStrs, domainStrs, projectStrs, projectDomainStrs, restrictDomainId).Distinct()
}
if !includeSystem {

View File

@@ -0,0 +1,67 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"yunion.io/x/pkg/util/rbacscope"
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
)
func checkRoleAssignmentListInput(allowScope rbacscope.TRbacScope, userCred mcclient.TokenCredential, input *api.RoleAssignmentsInput) (string, error) {
switch allowScope {
case rbacscope.ScopeSystem:
return "", nil
case rbacscope.ScopeDomain:
domainId := userCred.GetProjectDomainId()
if err := rejectRoleAssignmentMismatch(input.ProjectDomainId, domainId); err != nil {
return "", err
}
if err := rejectRoleAssignmentMismatch(input.Scope.Domain.Id, domainId); err != nil {
return "", err
}
return domainId, nil
case rbacscope.ScopeProject:
projectId := userCred.GetProjectId()
if err := rejectRoleAssignmentMismatch(input.Scope.Project.Id, projectId); err != nil {
return "", err
}
input.Scope.Project.Id = projectId
input.IncludePolicies = nil
return "", nil
case rbacscope.ScopeUser:
userId := userCred.GetUserId()
if err := rejectRoleAssignmentMismatch(input.User.Id, userId); err != nil {
return "", err
}
if len(input.Group.Id) > 0 || len(input.Groups) > 0 {
return "", httperrors.NewForbiddenError("not allow to list role assignments")
}
input.User.Id = userId
input.IncludePolicies = nil
return "", nil
default:
return "", httperrors.NewForbiddenError("not allow to list role assignments")
}
}
func rejectRoleAssignmentMismatch(requested, allowed string) error {
if len(requested) > 0 && requested != allowed {
return httperrors.NewForbiddenError("not allow to list role assignments")
}
return nil
}

View File

@@ -0,0 +1,120 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"testing"
"yunion.io/x/pkg/util/rbacscope"
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/mcclient"
)
func testRoleAssignmentCred() *mcclient.SSimpleToken {
return &mcclient.SSimpleToken{
UserId: "user-1",
ProjectId: "proj-1",
ProjectDomainId: "domain-1",
}
}
func TestCheckRoleAssignmentListInputSystem(t *testing.T) {
input := api.RoleAssignmentsInput{}
input.User.Id = "other-user"
restrict, err := checkRoleAssignmentListInput(rbacscope.ScopeSystem, testRoleAssignmentCred(), &input)
if err != nil {
t.Fatalf("system scope: %v", err)
}
if restrict != "" {
t.Fatalf("system restrictDomainId = %q", restrict)
}
if input.User.Id != "other-user" {
t.Fatalf("system must not rewrite user id, got %q", input.User.Id)
}
}
func TestCheckRoleAssignmentListInputUser(t *testing.T) {
cred := testRoleAssignmentCred()
input := api.RoleAssignmentsInput{}
restrict, err := checkRoleAssignmentListInput(rbacscope.ScopeUser, cred, &input)
if err != nil {
t.Fatalf("user scope: %v", err)
}
if restrict != "" {
t.Fatalf("user restrictDomainId = %q", restrict)
}
if input.User.Id != cred.UserId {
t.Fatalf("user id = %q, want %q", input.User.Id, cred.UserId)
}
if input.IncludePolicies != nil {
t.Fatal("user scope must not request include_policies")
}
input.User.Id = "other-user"
if _, err := checkRoleAssignmentListInput(rbacscope.ScopeUser, cred, &input); err == nil {
t.Fatal("expected error when listing another user")
}
input = api.RoleAssignmentsInput{}
input.Group.Id = "group-1"
if _, err := checkRoleAssignmentListInput(rbacscope.ScopeUser, cred, &input); err == nil {
t.Fatal("expected error when listing by group")
}
}
func TestCheckRoleAssignmentListInputDomain(t *testing.T) {
cred := testRoleAssignmentCred()
input := api.RoleAssignmentsInput{}
restrict, err := checkRoleAssignmentListInput(rbacscope.ScopeDomain, cred, &input)
if err != nil {
t.Fatalf("domain scope: %v", err)
}
if restrict != cred.ProjectDomainId {
t.Fatalf("restrictDomainId = %q, want %q", restrict, cred.ProjectDomainId)
}
input.ProjectDomainId = "other-domain"
if _, err := checkRoleAssignmentListInput(rbacscope.ScopeDomain, cred, &input); err == nil {
t.Fatal("expected error when listing another domain")
}
}
func TestCheckRoleAssignmentListInputProject(t *testing.T) {
cred := testRoleAssignmentCred()
input := api.RoleAssignmentsInput{}
restrict, err := checkRoleAssignmentListInput(rbacscope.ScopeProject, cred, &input)
if err != nil {
t.Fatalf("project scope: %v", err)
}
if restrict != "" {
t.Fatalf("project restrictDomainId = %q", restrict)
}
if input.Scope.Project.Id != cred.ProjectId {
t.Fatalf("project id = %q, want %q", input.Scope.Project.Id, cred.ProjectId)
}
input.Scope.Project.Id = "other-proj"
if _, err := checkRoleAssignmentListInput(rbacscope.ScopeProject, cred, &input); err == nil {
t.Fatal("expected error when listing another project")
}
}
func TestCheckRoleAssignmentListInputNone(t *testing.T) {
input := api.RoleAssignmentsInput{}
if _, err := checkRoleAssignmentListInput(rbacscope.ScopeNone, testRoleAssignmentCred(), &input); err == nil {
t.Fatal("expected error when scope is none")
}
}

View File

@@ -68,6 +68,12 @@ var (
Action: PolicyActionDelete,
Result: rbacutils.Allow,
},
{
Service: api.SERVICE_TYPE,
Resource: "role_assignments",
Action: PolicyActionList,
Result: rbacutils.Allow,
},
},
},
{

View File

@@ -35,6 +35,7 @@ var (
}
identityUserResources = []string{
"credentials",
"role_assignments",
}
)