fix(cloudid): omit clouduser secret from list and get responses (#25651)

Return console login details from the login-info action; skip secret in resource JSON and logs.

Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jian Qiu
2026-09-10 17:24:44 +08:00
committed by GitHub
co-authored by Qiu Jian Cursor
parent 23d7222bd0
commit 3c47ebba45
6 changed files with 166 additions and 65 deletions
+7
View File
@@ -215,6 +215,13 @@ type ClouduserSyncInput struct {
type ClouduserUpdateInput struct {
}
type ClouduserLoginInfo struct {
Account string `json:"account"`
Username string `json:"username"`
Password string `json:"password"`
Url string `json:"url"`
}
type ClouduserResetPasswordInput struct {
// 若此参数为空, 默认会生成随机12位密码
//
+1 -1
View File
@@ -109,7 +109,7 @@ type SClouduser struct {
apis.SExternalizedResourceBase
SCloudaccountResourceBase
SCloudproviderResourceBase
Secret string `json:"secret"`
Secret string `json:"-"`
// 是否可以控制台登录
IsConsoleLogin *bool `json:"is_console_login,omitempty"`
// 手机号码
+1 -1
View File
@@ -71,7 +71,7 @@ type SClouduser struct {
SCloudaccountResourceBase
SCloudproviderResourceBase
Secret string `length:"0" charset:"ascii" nullable:"true" list:"user" create:"domain_optional"`
Secret string `length:"0" charset:"ascii" nullable:"true" create:"domain_optional" log:"skip"`
// 是否可以控制台登录
IsConsoleLogin tristate.TriState `default:"false" list:"user" create:"optional"`
// 手机号码
+75
View File
@@ -0,0 +1,75 @@
// 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 (
"context"
"fmt"
"net/url"
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
api "yunion.io/x/onecloud/pkg/apis/cloudid"
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
)
func (self *SClouduser) GetDetailsLoginInfo(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject) (*api.ClouduserLoginInfo, error) {
if len(self.Secret) == 0 {
return nil, httperrors.NewNotFoundError("No login secret found")
}
password, err := self.GetPassword()
if err != nil {
return nil, errors.Wrap(err, "GetPassword")
}
account, err := self.GetCloudaccount()
if err != nil {
return nil, errors.Wrap(err, "GetCloudaccount")
}
return formatClouduserLoginInfo(self.Name, account.Provider, account.IamLoginUrl, password), nil
}
func formatClouduserLoginInfo(name, provider, iamLoginUrl, password string) *api.ClouduserLoginInfo {
username := name
account := ""
switch provider {
case computeapi.CLOUD_PROVIDER_ALIYUN:
suffix := strings.TrimPrefix(iamLoginUrl, "https://signin.aliyun.com/")
suffix = strings.TrimSuffix(suffix, "/login.htm")
if len(suffix) > 0 {
username = fmt.Sprintf("%s@%s", name, suffix)
account = suffix
}
case computeapi.CLOUD_PROVIDER_QCLOUD, computeapi.CLOUD_PROVIDER_HUAWEI:
u, _ := url.Parse(iamLoginUrl)
if u != nil {
account = u.Query().Get("account")
}
case computeapi.CLOUD_PROVIDER_AWS:
account = strings.TrimPrefix(iamLoginUrl, "https://")
if info := strings.Split(account, "."); len(info) > 0 {
account = info[0]
}
}
return &api.ClouduserLoginInfo{
Account: account,
Username: username,
Password: password,
Url: iamLoginUrl,
}
}
@@ -0,0 +1,81 @@
// 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 (
"reflect"
"testing"
"yunion.io/x/pkg/utils"
computeapi "yunion.io/x/onecloud/pkg/apis/compute"
)
func TestClouduserSecretFieldTags(t *testing.T) {
ft, ok := reflect.TypeOf(SClouduser{}).FieldByName("Secret")
if !ok {
t.Fatal("Secret field missing")
}
if got := ft.Tag.Get("list"); got != "" {
t.Fatalf("Secret list tag = %q, want empty", got)
}
if got := ft.Tag.Get("get"); got != "" {
t.Fatalf("Secret get tag = %q, want empty", got)
}
}
func TestGetPassword(t *testing.T) {
plain := "P@ssw0rd12"
user := &SClouduser{}
user.Id = "clouduser-id"
sec, err := utils.EncryptAESBase64(user.Id, plain)
if err != nil {
t.Fatalf("EncryptAESBase64: %v", err)
}
user.Secret = sec
got, err := user.GetPassword()
if err != nil {
t.Fatalf("GetPassword: %v", err)
}
if got != plain {
t.Fatalf("GetPassword = %q, want %q", got, plain)
}
}
func TestFormatClouduserLoginInfo(t *testing.T) {
info := formatClouduserLoginInfo("alice", computeapi.CLOUD_PROVIDER_ALIYUN, "https://signin.aliyun.com/mycorp.onaliyun.com/login.htm", "secret")
if info.Username != "alice@mycorp.onaliyun.com" {
t.Fatalf("aliyun username = %q", info.Username)
}
if info.Account != "mycorp.onaliyun.com" {
t.Fatalf("aliyun account = %q", info.Account)
}
if info.Password != "secret" || info.Url == "" {
t.Fatalf("unexpected login info: %+v", info)
}
info = formatClouduserLoginInfo("bob", computeapi.CLOUD_PROVIDER_AWS, "https://123456789012.signin.aws.amazon.com/console", "secret")
if info.Account != "123456789012" {
t.Fatalf("aws account = %q", info.Account)
}
if info.Username != "bob" {
t.Fatalf("aws username = %q", info.Username)
}
info = formatClouduserLoginInfo("carol", computeapi.CLOUD_PROVIDER_QCLOUD, "https://cloud.tencent.com/login?account=corp", "secret")
if info.Account != "corp" {
t.Fatalf("qcloud account = %q", info.Account)
}
}
+1 -63
View File
@@ -15,16 +15,8 @@
package cloudid
import (
"fmt"
"net/url"
"strings"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/utils"
api "yunion.io/x/onecloud/pkg/apis/compute"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/modulebase"
"yunion.io/x/onecloud/pkg/mcclient/modules"
@@ -47,59 +39,5 @@ func init() {
}
func (this *SClouduserManager) GetLoginInfo(s *mcclient.ClientSession, id string, params jsonutils.JSONObject) (jsonutils.JSONObject, error) {
data, err := this.Get(s, id, nil)
if err != nil {
return nil, err
}
user := struct {
Id string
Name string
Secret string
Provider string
IamLoginUrl string
}{}
err = data.Unmarshal(&user)
if err != nil {
return nil, errors.Wrap(err, "data.Unmarshal")
}
if len(user.Secret) == 0 {
return nil, httperrors.NewNotFoundError("No login secret found")
}
password, err := utils.DescryptAESBase64(user.Id, user.Secret)
if err != nil {
return nil, errors.Wrap(err, "Descrypt")
}
account := ""
switch user.Provider {
case api.CLOUD_PROVIDER_ALIYUN:
suffix := strings.TrimPrefix(user.IamLoginUrl, "https://signin.aliyun.com/")
suffix = strings.TrimSuffix(suffix, "/login.htm")
if len(suffix) > 0 {
user.Name = fmt.Sprintf("%s@%s", user.Name, suffix)
account = suffix
}
case api.CLOUD_PROVIDER_QCLOUD, api.CLOUD_PROVIDER_HUAWEI:
u, _ := url.Parse(user.IamLoginUrl)
if u != nil {
account = u.Query().Get("account")
}
case api.CLOUD_PROVIDER_AWS:
account = strings.TrimPrefix(user.IamLoginUrl, "https://")
if info := strings.Split(account, "."); len(info) > 0 {
account = info[0]
}
}
return jsonutils.Marshal(map[string]string{
"account": account,
"username": user.Name,
"password": password,
"url": user.IamLoginUrl,
}), nil
return this.GetSpecific(s, id, "login-info", params)
}