From ec541c01044449d9da980af16e22e6cdbbeb453a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B1=88=E8=BD=A9?= Date: Tue, 13 Oct 2026 16:28:01 +0800 Subject: [PATCH] fix(cloudid): samluser owner (#25863) --- pkg/cloudid/models/samluser.go | 52 ++++++++++++++++++ pkg/cloudid/models/samluser_scope_test.go | 65 +++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 pkg/cloudid/models/samluser_scope_test.go diff --git a/pkg/cloudid/models/samluser.go b/pkg/cloudid/models/samluser.go index 733554b066..091634d8ca 100644 --- a/pkg/cloudid/models/samluser.go +++ b/pkg/cloudid/models/samluser.go @@ -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 } diff --git a/pkg/cloudid/models/samluser_scope_test.go b/pkg/cloudid/models/samluser_scope_test.go new file mode 100644 index 0000000000..8ab4ee3c39 --- /dev/null +++ b/pkg/cloudid/models/samluser_scope_test.go @@ -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) + } +}