fix(cloudid): samluser owner (#25863)

This commit is contained in:
屈轩
2026-10-13 16:28:01 +08:00
committed by GitHub
parent ac5eacd390
commit ec541c0104
2 changed files with 117 additions and 0 deletions
+52
View File
@@ -20,6 +20,7 @@ import (
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/rbacscope"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/apis"
@@ -80,6 +81,44 @@ func (manager *SSamluserManager) GetIVirtualModelManager() db.IVirtualModelManag
return manager.GetVirtualObject().(db.IVirtualModelManager)
}
// ResourceScope 声明 samluser 归属本地用户。
// 模型同时带 domain_id,若不覆盖,scope=user 会被提升为 domain,
// 普通用户只有 user 范围的 list 权限,查询会 403。
func (manager *SSamluserManager) ResourceScope() rbacscope.TRbacScope {
return rbacscope.ScopeUser
}
// effectiveSamluserListScope 在未显式指定 scope、且不是 admin=true 时,
// 为域/系统读者保留原来的域范围默认视图。普通用户不改写,走 user scope。
func effectiveSamluserListScope(reqScope string, isAdmin, domainAllow bool) (string, bool) {
if reqScope == string(rbacscope.ScopeNone) {
reqScope = ""
}
if len(reqScope) > 0 || isAdmin || !domainAllow {
return "", false
}
return string(rbacscope.ScopeDomain), true
}
// samluserListOwnerConstraint 限制普通用户只能看到自己的 samluser。
// 域/系统读者保留请求里的 user 过滤。
func samluserListOwnerConstraint(callerId string, domainAllow bool) (ownerId string, restrict bool) {
if domainAllow {
return "", false
}
return callerId, true
}
func (manager *SSamluserManager) ValidateListConditions(ctx context.Context, userCred mcclient.TokenCredential, query *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
reqScope, _ := query.GetString("scope")
isAdmin := query.Contains("admin") && jsonutils.QueryBoolean(query, "admin", false)
domainAllow := db.IsDomainAllowList(userCred, manager).Result.IsAllow()
if scope, ok := effectiveSamluserListScope(reqScope, isAdmin, domainAllow); ok {
query.Set("scope", jsonutils.NewString(scope))
}
return query, nil
}
func (manager *SSamluserManager) FetchUniqValues(ctx context.Context, data jsonutils.JSONObject) jsonutils.JSONObject {
groupId, _ := data.GetString("cloudgroup_id")
return jsonutils.Marshal(map[string]string{"cloudgroup_id": groupId})
@@ -96,10 +135,20 @@ func (manager *SSamluserManager) FilterByUniqValues(q *sqlchemy.SQuery, values j
// SAML认证用户列表
func (manager *SSamluserManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query api.SamluserListInput) (*sqlchemy.SQuery, error) {
var err error
domainAllow := db.IsDomainAllowList(userCred, manager).Result.IsAllow()
ownerId, restrict := samluserListOwnerConstraint(userCred.GetUserId(), domainAllow)
if restrict {
// 丢弃 user/user_id,避免基类按请求参数查到其他用户。
query.UserId = ""
query.User = ""
}
q, err = manager.SStatusDomainLevelUserResourceBaseManager.ListItemFilter(ctx, q, userCred, query.StatusDomainLevelUserResourceListInput)
if err != nil {
return nil, err
}
if restrict {
q = q.Equals("owner_id", ownerId)
}
q, err = manager.SCloudgroupResourceBaseManager.ListItemFilter(ctx, q, userCred, query.CloudgroupResourceListInput)
if err != nil {
return nil, err
@@ -168,6 +217,9 @@ func (manager *SSamluserManager) ValidateCreateData(
return input, errors.Wrapf(err, "FetchUserById")
}
input.OwnerId = user.Id
if input.OwnerId != userCred.GetUserId() && !db.IsDomainAllowCreate(userCred, manager).Result.IsAllow() {
return input, httperrors.NewForbiddenError("not allow to create samluser for another user")
}
if len(input.Name) == 0 {
input.Name = user.Name
}
+65
View File
@@ -0,0 +1,65 @@
// 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"
)
func TestSamluserResourceScope(t *testing.T) {
if got := SamluserManager.ResourceScope(); got != rbacscope.ScopeUser {
t.Fatalf("ResourceScope = %s, want %s", got, rbacscope.ScopeUser)
}
}
func TestEffectiveSamluserListScope(t *testing.T) {
cases := []struct {
name string
reqScope string
isAdmin bool
domainAllow bool
wantScope string
wantSet bool
}{
{name: "domain reader keeps domain default", reqScope: "", domainAllow: true, wantScope: string(rbacscope.ScopeDomain), wantSet: true},
{name: "system reader keeps domain default", reqScope: "", domainAllow: true, wantScope: string(rbacscope.ScopeDomain), wantSet: true},
{name: "ordinary user keeps user scope", reqScope: "", domainAllow: false, wantSet: false},
{name: "explicit user scope stays", reqScope: "user", domainAllow: false, wantSet: false},
{name: "explicit domain scope stays for rbac to reject", reqScope: "domain", domainAllow: false, wantSet: false},
{name: "admin=true is not rewritten", reqScope: "", isAdmin: true, domainAllow: true, wantSet: false},
{name: "none scope uses domain default for domain reader", reqScope: "none", domainAllow: true, wantScope: string(rbacscope.ScopeDomain), wantSet: true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got, ok := effectiveSamluserListScope(c.reqScope, c.isAdmin, c.domainAllow)
if ok != c.wantSet || got != c.wantScope {
t.Fatalf("effectiveSamluserListScope(%q, %v, %v) = (%q, %v), want (%q, %v)", c.reqScope, c.isAdmin, c.domainAllow, got, ok, c.wantScope, c.wantSet)
}
})
}
}
func TestSamluserListOwnerConstraint(t *testing.T) {
owner, restrict := samluserListOwnerConstraint("user-a", false)
if !restrict || owner != "user-a" {
t.Fatalf("ordinary user constraint = (%q, %v), want (user-a, true)", owner, restrict)
}
owner, restrict = samluserListOwnerConstraint("admin", true)
if restrict || owner != "" {
t.Fatalf("domain reader constraint = (%q, %v), want (\"\", false)", owner, restrict)
}
}