diff --git a/go.mod b/go.mod index 5151d63304..ec6056ab43 100644 --- a/go.mod +++ b/go.mod @@ -153,7 +153,7 @@ require ( yunion.io/x/jsonutils v0.0.0-20220106020632-953b71a4c3a8 yunion.io/x/log v0.0.0-20201210064738-43181789dc74 yunion.io/x/ovsdb v0.0.0-20200526071744-27bf0940cbc7 - yunion.io/x/pkg v0.0.0-20220227083757-28690b33ce38 + yunion.io/x/pkg v0.0.0-20220406030238-39fbc60d5d4e yunion.io/x/s3cli v0.0.0-20190917004522-13ac36d8687e yunion.io/x/sqlchemy v0.0.0-20220405074102-7b815a97eb67 yunion.io/x/structarg v0.0.0-20220312084958-9c6c79c7d1c6 diff --git a/go.sum b/go.sum index 4497695f7e..7d22633f1d 100644 --- a/go.sum +++ b/go.sum @@ -994,8 +994,8 @@ yunion.io/x/ovsdb v0.0.0-20200526071744-27bf0940cbc7/go.mod h1:0vLkNEhlmA64HViPB yunion.io/x/pkg v0.0.0-20190620104149-945c25821dbf/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E= yunion.io/x/pkg v0.0.0-20190628082551-f4033ba2ea30/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E= yunion.io/x/pkg v0.0.0-20200814072949-4f1b541857d6/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E= -yunion.io/x/pkg v0.0.0-20220227083757-28690b33ce38 h1:QV3gVskJfi1J0e5kTs4oTx8D+Mc0JFik3Qe6c1PgO90= -yunion.io/x/pkg v0.0.0-20220227083757-28690b33ce38/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E= +yunion.io/x/pkg v0.0.0-20220406030238-39fbc60d5d4e h1:ovxSHuAa8hOBiZPU+ezrwqmVuCN7QTNx0sRRDGMaAkY= +yunion.io/x/pkg v0.0.0-20220406030238-39fbc60d5d4e/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E= yunion.io/x/s3cli v0.0.0-20190917004522-13ac36d8687e h1:v+EzIadodSwkdZ/7bremd7J8J50Cise/HCylsOJngmo= yunion.io/x/s3cli v0.0.0-20190917004522-13ac36d8687e/go.mod h1:0iFKpOs1y4lbCxeOmq3Xx/0AcQoewVPwj62eRluioEo= yunion.io/x/sqlchemy v0.0.0-20220405074102-7b815a97eb67 h1:vZiiB5oc6wqEvr1UNmJPFCX3FmLznZbvoUZ90ONgAcE= diff --git a/pkg/apis/compute/cloudaccount.go b/pkg/apis/compute/cloudaccount.go index 0221001a3f..a33d942b10 100644 --- a/pkg/apis/compute/cloudaccount.go +++ b/pkg/apis/compute/cloudaccount.go @@ -207,6 +207,8 @@ type CloudaccountCreateInput struct { // swagger:ignore SubAccounts *cloudprovider.SubAccounts + + ReadOnly bool `json:"read_only"` } type CloudaccountShareModeInput struct { @@ -339,6 +341,8 @@ type CloudaccountUpdateInput struct { // 临时清除缺失的权限提示,云账号权限缺失依然会自动刷新 CleanLakeOfPermissions bool `json:"clean_lake_of_permissions"` + + ReadOnly bool `json:"read_only"` } type CloudaccountPerformPublicInput struct { diff --git a/pkg/apis/compute/cloudprovider.go b/pkg/apis/compute/cloudprovider.go index 9354aec940..aa71115bdd 100644 --- a/pkg/apis/compute/cloudprovider.go +++ b/pkg/apis/compute/cloudprovider.go @@ -138,6 +138,8 @@ type CloudproviderDetails struct { // 子订阅品牌信息 Brand string `json:"brand"` + ReadOnly bool `json:"read_only"` + ProjectMappingResourceInfo } diff --git a/pkg/cloudid/models/cloudaccount.go b/pkg/cloudid/models/cloudaccount.go index bebbbfeb80..373455c105 100644 --- a/pkg/cloudid/models/cloudaccount.go +++ b/pkg/cloudid/models/cloudaccount.go @@ -79,6 +79,8 @@ type SCloudaccount struct { SAMLAuth tristate.TriState `list:"domain" default:"false"` AccessUrl string `width:"64" charset:"ascii" nullable:"true" list:"domain" update:"domain" create:"domain_optional"` + + ReadOnly bool `default:"false" create:"domain_optional" list:"domain" update:"domain"` } func (manager *SCloudaccountManager) GetResourceCount() ([]db.SScopeResourceCount, error) { @@ -395,6 +397,7 @@ func (self *SCloudaccount) syncWithICloudaccount(ctx context.Context, userCred m self.SAMLAuth = account.SAMLAuth self.AccountId = account.AccountId self.AccessUrl = account.AccessUrl + self.ReadOnly = account.ReadOnly return nil }) if err != nil { @@ -481,6 +484,8 @@ type SCloudDelegate struct { Provider string Brand string + ReadOnly bool + Options struct { cloudprovider.SHCSOEndpoints cloudprovider.SApsaraEndpoints @@ -570,6 +575,8 @@ func (account *SCloudDelegate) GetProvider() (cloudprovider.ICloudProvider, erro Secret: passwd, ProxyFunc: proxyFunc, + ReadOnly: account.ReadOnly, + DefaultRegion: defaultRegion, Options: options.(*jsonutils.JSONDict), diff --git a/pkg/cloudprovider/cloudprovider.go b/pkg/cloudprovider/cloudprovider.go index c286112a41..abf212c959 100644 --- a/pkg/cloudprovider/cloudprovider.go +++ b/pkg/cloudprovider/cloudprovider.go @@ -166,6 +166,8 @@ type ProviderConfig struct { Account string Secret string + ReadOnly bool + AccountId string Options *jsonutils.JSONDict diff --git a/pkg/cloudprovider/consts.go b/pkg/cloudprovider/consts.go index 10b1bafa72..0150067e26 100644 --- a/pkg/cloudprovider/consts.go +++ b/pkg/cloudprovider/consts.go @@ -29,12 +29,13 @@ const ( CloudVMStatusDeploying = "deploying" CloudVMStatusOther = "other" - ErrNotFound = errors.ErrNotFound - ErrDuplicateId = errors.ErrDuplicateId - ErrInvalidStatus = errors.ErrInvalidStatus - ErrTimeout = errors.ErrTimeout - ErrNotImplemented = errors.ErrNotImplemented - ErrNotSupported = errors.ErrNotSupported + ErrNotFound = errors.ErrNotFound + ErrDuplicateId = errors.ErrDuplicateId + ErrInvalidStatus = errors.ErrInvalidStatus + ErrTimeout = errors.ErrTimeout + ErrNotImplemented = errors.ErrNotImplemented + ErrNotSupported = errors.ErrNotSupported + ErrAccountReadOnly = errors.ErrAccountReadOnly ErrInvalidProvider = httperrors.ErrInvalidProvider ErrNoBalancePermission = httperrors.ErrNoBalancePermission diff --git a/pkg/cloudprovider/transport.go b/pkg/cloudprovider/transport.go new file mode 100644 index 0000000000..d7d584872b --- /dev/null +++ b/pkg/cloudprovider/transport.go @@ -0,0 +1,35 @@ +// 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 cloudprovider + +import "net/http" + +type transport struct { + readOnlyCheck func(req *http.Request) error + ts *http.Transport +} + +func (self *transport) RoundTrip(req *http.Request) (*http.Response, error) { + err := self.readOnlyCheck(req) + if err != nil { + return nil, err + } + return self.ts.RoundTrip(req) +} + +func GetReadOnlyCheckTransport(ts *http.Transport, check func(req *http.Request) error) http.RoundTripper { + ret := &transport{ts: ts, readOnlyCheck: check} + return ret +} diff --git a/pkg/compute/models/cloudaccounts.go b/pkg/compute/models/cloudaccounts.go index bfdac06ba1..357d0bf0a1 100644 --- a/pkg/compute/models/cloudaccounts.go +++ b/pkg/compute/models/cloudaccounts.go @@ -166,6 +166,8 @@ type SCloudaccount struct { SProjectMappingResourceBase + ReadOnly bool `default:"false" create:"domain_optional" list:"domain" update:"domain"` + // 设置允许同步的账号及订阅 SubAccounts *cloudprovider.SubAccounts `nullable:"true" get:"user" create:"optional"` @@ -991,6 +993,8 @@ func (self *SCloudaccount) getProviderInternal(ctx context.Context) (cloudprovid DefaultRegion: defaultRegion, ProxyFunc: self.proxyFunc(), + ReadOnly: self.ReadOnly, + UpdatePermission: self.UpdatePermission(ctx), }) } diff --git a/pkg/compute/models/cloudproviders.go b/pkg/compute/models/cloudproviders.go index 8eca8e4cf9..771e7c2c82 100644 --- a/pkg/compute/models/cloudproviders.go +++ b/pkg/compute/models/cloudproviders.go @@ -870,6 +870,8 @@ func (self *SCloudprovider) GetProvider(ctx context.Context) (cloudprovider.IClo Secret: passwd, ProxyFunc: account.proxyFunc(), + ReadOnly: account.ReadOnly, + DefaultRegion: defaultRegion, Options: account.Options, @@ -1015,6 +1017,7 @@ func (manager *SCloudproviderManager) FetchCustomizeColumns( for i := range rows { if account, ok := accounts[accountIds[i]]; ok { rows[i].Cloudaccount = account.Name + rows[i].ReadOnly = account.ReadOnly rows[i].Brand = account.Brand ps := &rows[i].ProxySetting diff --git a/pkg/mcclient/mcclient.go b/pkg/mcclient/mcclient.go index e3aa07e512..a6976ad5ad 100644 --- a/pkg/mcclient/mcclient.go +++ b/pkg/mcclient/mcclient.go @@ -98,6 +98,14 @@ func (this *Client) SetHttpTransportProxyFunc(proxyFunc httputils.TransportProxy httputils.SetClientProxyFunc(this.httpconn, proxyFunc) } +func (this *Client) GetClient() *http.Client { + return this.httpconn +} + +func (this *Client) SetTransport(ts http.RoundTripper) { + this.httpconn.Transport = ts +} + func (this *Client) SetDebug(debug bool) { this.debug = debug } diff --git a/pkg/mcclient/options/cloudaccounts.go b/pkg/mcclient/options/cloudaccounts.go index bb22b4e88e..a49389b65c 100644 --- a/pkg/mcclient/options/cloudaccounts.go +++ b/pkg/mcclient/options/cloudaccounts.go @@ -121,6 +121,7 @@ type SCloudAccountCreateBaseOptions struct { ProxySetting string `help:"proxy setting id or name" json:"proxy_setting"` DryRun bool `help:"test create cloudaccount params"` ShowSubAccounts bool `help:"test and show subaccount info"` + ReadOnly bool `help:"Read only account"` } type SVMwareCloudAccountCreateOptions struct { @@ -560,6 +561,8 @@ type SCloudAccountUpdateBaseOptions struct { ProxySetting string `help:"proxy setting name or id" json:"proxy_setting"` SamlAuth string `help:"Enable or disable saml auth" choices:"true|false"` + ReadOnly *bool `help:"is account read only" negative:"no_read_only"` + CleanLakeOfPermissions bool `help:"clean lake of permissions"` Desc string `help:"Description" json:"description" token:"desc"` diff --git a/pkg/multicloud/aliyun/aliyun.go b/pkg/multicloud/aliyun/aliyun.go index 0228c84db3..8ba3742770 100644 --- a/pkg/multicloud/aliyun/aliyun.go +++ b/pkg/multicloud/aliyun/aliyun.go @@ -16,6 +16,8 @@ package aliyun import ( "fmt" + "net/http" + "net/url" "strings" "time" @@ -406,6 +408,22 @@ func (self *SAliyunClient) getSdkClient(regionId string) (*sdk.Client, error) { regionId, &sdk.Config{ HttpTransport: transport, + Transport: cloudprovider.GetReadOnlyCheckTransport(transport, func(req *http.Request) error { + if self.cpcfg.ReadOnly { + params, err := url.ParseQuery(req.URL.RawQuery) + if err != nil { + return errors.Wrapf(err, "ParseQuery(%s)", req.URL.RawQuery) + } + action := params.Get("Action") + for _, prefix := range []string{"Get", "List", "Describe"} { + if strings.HasPrefix(action, prefix) { + return nil + } + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, action) + } + return nil + }), }, &credentials.BaseCredential{ AccessKeyId: self.accessKey, @@ -512,6 +530,16 @@ func (client *SAliyunClient) getOssClientByEndpoint(endpoint string) (*oss.Clien // https_proxy setting // oss use no timeout client so as to send/download large files httpClient := client.cpcfg.AdaptiveTimeoutHttpClient() + transport, _ := httpClient.Transport.(*http.Transport) + httpClient.Transport = cloudprovider.GetReadOnlyCheckTransport(transport, func(req *http.Request) error { + if client.cpcfg.ReadOnly { + if req.Method == "GET" { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.RawPath) + } + return nil + }) cliOpts := []oss.ClientOption{ oss.HTTPClient(httpClient), } diff --git a/pkg/multicloud/apsara/apsara.go b/pkg/multicloud/apsara/apsara.go index f0ecf24a1c..66acf7e070 100644 --- a/pkg/multicloud/apsara/apsara.go +++ b/pkg/multicloud/apsara/apsara.go @@ -17,6 +17,8 @@ package apsara import ( "crypto/tls" "fmt" + "net/http" + "net/url" "strings" "time" @@ -277,6 +279,22 @@ func (self *SApsaraClient) getDefaultClient(regionId string) (*sdk.Client, error regionId, &sdk.Config{ HttpTransport: transport, + Transport: cloudprovider.GetReadOnlyCheckTransport(transport, func(req *http.Request) error { + if self.cpcfg.ReadOnly { + params, err := url.ParseQuery(req.URL.RawQuery) + if err != nil { + return errors.Wrapf(err, "ParseQuery(%s)", req.URL.RawQuery) + } + action := params.Get("Action") + for _, prefix := range []string{"Get", "List", "Describe"} { + if strings.HasPrefix(action, prefix) { + return nil + } + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, action) + } + return nil + }), }, &credentials.BaseCredential{ AccessKeyId: self.accessKey, @@ -347,6 +365,16 @@ func (client *SApsaraClient) getOssClient(regionId string) (*oss.Client, error) // https_proxy setting // oss use no timeout client so as to send/download large files httpClient := client.cpcfg.AdaptiveTimeoutHttpClient() + transport, _ := httpClient.Transport.(*http.Transport) + httpClient.Transport = cloudprovider.GetReadOnlyCheckTransport(transport, func(req *http.Request) error { + if client.cpcfg.ReadOnly { + if req.Method == "GET" { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) + } + return nil + }) cliOpts := []oss.ClientOption{ oss.HTTPClient(httpClient), } diff --git a/pkg/multicloud/aws/aws.go b/pkg/multicloud/aws/aws.go index 51fa3a32bb..b82367291c 100644 --- a/pkg/multicloud/aws/aws.go +++ b/pkg/multicloud/aws/aws.go @@ -15,7 +15,11 @@ package aws import ( + "bytes" "fmt" + "io/ioutil" + "net/http" + "net/url" "strings" "time" @@ -239,6 +243,35 @@ func (client *SAwsClient) getAwsSession(regionId string, assumeRole bool) (*sess return sess, nil } httpClient := client.cpcfg.AdaptiveTimeoutHttpClient() + transport, _ := httpClient.Transport.(*http.Transport) + httpClient.Transport = cloudprovider.GetReadOnlyCheckTransport(transport, func(req *http.Request) error { + if client.cpcfg.ReadOnly { + if req.ContentLength > 0 { + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return errors.Wrapf(err, "ioutil.ReadAll") + } + req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + params, err := url.ParseQuery(string(body)) + if err != nil { + return errors.Wrapf(err, "ParseQuery(%s)", string(body)) + } + action := params.Get("Action") + for _, prefix := range []string{"Get", "List", "Describe"} { + if strings.HasPrefix(action, prefix) { + return nil + } + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, action) + } + // s3 + if req.Method == "GET" || req.Method == "HEAD" { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) + } + return nil + }) s, err := session.NewSession(&sdk.Config{ Region: sdk.String(regionId), Credentials: credentials.NewStaticCredentials( diff --git a/pkg/multicloud/azure/azure.go b/pkg/multicloud/azure/azure.go index 6690c4cc2c..cd62cf61c6 100644 --- a/pkg/multicloud/azure/azure.go +++ b/pkg/multicloud/azure/azure.go @@ -158,6 +158,16 @@ func (self *SAzureClient) getClient(resource TAzureResource) (*autorest.Client, } httpClient := self.cpcfg.AdaptiveTimeoutHttpClient() + transport, _ := httpClient.Transport.(*http.Transport) + httpClient.Transport = cloudprovider.GetReadOnlyCheckTransport(transport, func(req *http.Request) error { + if self.cpcfg.ReadOnly { + if req.Method == "GET" { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) + } + return nil + }) client.Sender = httpClient self.env = env diff --git a/pkg/multicloud/bingocloud/bingo.go b/pkg/multicloud/bingocloud/bingo.go index 3fce99d068..a61a18e6fe 100644 --- a/pkg/multicloud/bingocloud/bingo.go +++ b/pkg/multicloud/bingocloud/bingo.go @@ -211,6 +211,13 @@ func (e sBingoError) Error() string { } func (self *SBingoCloudClient) invoke(action string, params map[string]string) (jsonutils.JSONObject, error) { + if self.cpcfg.ReadOnly { + for _, prefix := range []string{"Get", "List", "Describe"} { + if strings.HasPrefix(action, prefix) { + return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, action) + } + } + } var encode = func(k, v string) string { d := url.Values{} d.Set(k, v) diff --git a/pkg/multicloud/cloudpods/cloudpods.go b/pkg/multicloud/cloudpods/cloudpods.go index 6f8ea577e8..873914ffab 100644 --- a/pkg/multicloud/cloudpods/cloudpods.go +++ b/pkg/multicloud/cloudpods/cloudpods.go @@ -16,6 +16,7 @@ package cloudpods import ( "context" + "net/http" "strings" "yunion.io/x/jsonutils" @@ -93,6 +94,20 @@ type SCloudpodsClient struct { func (self *SCloudpodsClient) auth() error { client := mcclient.NewClient(self.authURL, 0, self.debug, true, "", "") client.SetHttpTransportProxyFunc(self.cpcfg.ProxyFunc) + ts, _ := client.GetClient().Transport.(*http.Transport) + client.SetTransport(cloudprovider.GetReadOnlyCheckTransport(ts, func(req *http.Request) error { + if self.cpcfg.ReadOnly { + if req.Method == "GET" || req.Method == "HEAD" { + return nil + } + // 认证 + if req.Method == "POST" && req.URL.Path == "/v3/auth/tokens" { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) + } + return nil + })) token, err := client.AuthenticateByAccessKey(self.accessKey, self.accessSecret, "cli") if err != nil { if errors.Cause(err) == httperrors.ErrUnauthorized { diff --git a/pkg/multicloud/ctyun/ctyun.go b/pkg/multicloud/ctyun/ctyun.go index b9bb00d5eb..efc157dc27 100644 --- a/pkg/multicloud/ctyun/ctyun.go +++ b/pkg/multicloud/ctyun/ctyun.go @@ -29,6 +29,7 @@ import ( "github.com/aliyun/alibaba-cloud-sdk-go/sdk/utils" "yunion.io/x/jsonutils" + "yunion.io/x/pkg/errors" api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudprovider" @@ -89,6 +90,16 @@ type SCtyunClient struct { func NewSCtyunClient(cfg *CtyunClientConfig) (*SCtyunClient, error) { httpClient := cfg.cpcfg.AdaptiveTimeoutHttpClient() + ts, _ := httpClient.Transport.(*http.Transport) + httpClient.Transport = cloudprovider.GetReadOnlyCheckTransport(ts, func(req *http.Request) error { + if cfg.cpcfg.ReadOnly { + if req.Method == "GET" { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) + } + return nil + }) client := &SCtyunClient{ CtyunClientConfig: cfg, httpClient: httpClient, diff --git a/pkg/multicloud/google/google.go b/pkg/multicloud/google/google.go index 9b651e2fa3..5e865e08f7 100644 --- a/pkg/multicloud/google/google.go +++ b/pkg/multicloud/google/google.go @@ -139,6 +139,17 @@ func NewGoogleClient(cfg *GoogleClientConfig) (*SGoogleClient, error) { } httpClient := cfg.cpcfg.AdaptiveTimeoutHttpClient() + ts, _ := httpClient.Transport.(*http.Transport) + httpClient.Transport = cloudprovider.GetReadOnlyCheckTransport(ts, func(req *http.Request) error { + if cfg.cpcfg.ReadOnly { + if req.Method == "GET" { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) + } + return nil + }) + ctx := context.Background() ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) diff --git a/pkg/multicloud/hcso/huawei.go b/pkg/multicloud/hcso/huawei.go index ed21880328..ef79bb2ef7 100644 --- a/pkg/multicloud/hcso/huawei.go +++ b/pkg/multicloud/hcso/huawei.go @@ -16,6 +16,7 @@ package hcso import ( "fmt" + "net/http" "strings" "time" @@ -166,6 +167,16 @@ func (self *SHuaweiClient) newGeneralAPIClient() (*client.Client, error) { } httpClient := self.cpcfg.AdaptiveTimeoutHttpClient() + ts, _ := httpClient.Transport.(*http.Transport) + httpClient.Transport = cloudprovider.GetReadOnlyCheckTransport(ts, func(req *http.Request) error { + if self.cpcfg.ReadOnly { + if req.Method == "GET" { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) + } + return nil + }) cli.SetHttpClient(httpClient) return cli, nil diff --git a/pkg/multicloud/hcso/region.go b/pkg/multicloud/hcso/region.go index 086542ff40..77b032adbd 100644 --- a/pkg/multicloud/hcso/region.go +++ b/pkg/multicloud/hcso/region.go @@ -16,6 +16,7 @@ package hcso import ( "fmt" + "net/http" "strings" "time" @@ -102,6 +103,18 @@ func (self *SRegion) getOBSClient() (*obs.ObsClient, error) { return nil, err } + client := obsClient.GetClient() + ts, _ := client.Transport.(*http.Transport) + client.Transport = cloudprovider.GetReadOnlyCheckTransport(ts, func(req *http.Request) error { + if self.client.cpcfg.ReadOnly { + if req.Method == "GET" || req.Method == "HEAD" { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) + } + return nil + }) + self.obsClient = obsClient } diff --git a/pkg/multicloud/huawei/huawei.go b/pkg/multicloud/huawei/huawei.go index 9c00a016b9..cf7eaf79f2 100644 --- a/pkg/multicloud/huawei/huawei.go +++ b/pkg/multicloud/huawei/huawei.go @@ -16,6 +16,7 @@ package huawei import ( "fmt" + "net/http" "strings" "time" @@ -171,6 +172,16 @@ func (self *SHuaweiClient) newGeneralAPIClient() (*client.Client, error) { } httpClient := self.cpcfg.AdaptiveTimeoutHttpClient() + ts, _ := httpClient.Transport.(*http.Transport) + httpClient.Transport = cloudprovider.GetReadOnlyCheckTransport(ts, func(req *http.Request) error { + if self.cpcfg.ReadOnly { + if req.Method == "GET" { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) + } + return nil + }) cli.SetHttpClient(httpClient) return cli, nil diff --git a/pkg/multicloud/huawei/obs/client.go b/pkg/multicloud/huawei/obs/client.go index 2c1a5c12ba..77914cbd1d 100644 --- a/pkg/multicloud/huawei/obs/client.go +++ b/pkg/multicloud/huawei/obs/client.go @@ -41,6 +41,10 @@ type ObsClient struct { httpClient *http.Client } +func (self *ObsClient) GetClient() *http.Client { + return self.httpClient +} + func New(ak, sk, endpoint string, configurers ...configurer) (*ObsClient, error) { conf := &config{securityProvider: &securityProvider{ak: ak, sk: sk}, endpoint: endpoint} conf.maxRetryCount = -1 diff --git a/pkg/multicloud/huawei/region.go b/pkg/multicloud/huawei/region.go index bee286cee0..8527302e9c 100644 --- a/pkg/multicloud/huawei/region.go +++ b/pkg/multicloud/huawei/region.go @@ -16,6 +16,7 @@ package huawei import ( "fmt" + "net/http" "strings" "time" @@ -99,6 +100,18 @@ func (self *SRegion) getOBSClient() (*obs.ObsClient, error) { return nil, err } + client := obsClient.GetClient() + ts, _ := client.Transport.(*http.Transport) + client.Transport = cloudprovider.GetReadOnlyCheckTransport(ts, func(req *http.Request) error { + if self.client.cpcfg.ReadOnly { + if req.Method == "GET" || req.Method == "HEAD" { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) + } + return nil + }) + self.obsClient = obsClient } diff --git a/pkg/multicloud/nutanix/nutanix.go b/pkg/multicloud/nutanix/nutanix.go index 6cdaf1d3a8..68c6b40053 100644 --- a/pkg/multicloud/nutanix/nutanix.go +++ b/pkg/multicloud/nutanix/nutanix.go @@ -131,6 +131,18 @@ func (cli *SNutanixClient) getDefaultClient(timeout time.Duration) *http.Client return nil, nil } httputils.SetClientProxyFunc(client, proxy) + + ts, _ := client.Transport.(*http.Transport) + client.Transport = cloudprovider.GetReadOnlyCheckTransport(ts, func(req *http.Request) error { + if cli.cpcfg.ReadOnly { + if req.Method == "GET" { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) + } + return nil + }) + return client } diff --git a/pkg/multicloud/openstack/openstack.go b/pkg/multicloud/openstack/openstack.go index 19cf996dbb..6f5692f8a6 100644 --- a/pkg/multicloud/openstack/openstack.go +++ b/pkg/multicloud/openstack/openstack.go @@ -423,6 +423,22 @@ func (cli *SOpenStackClient) getDefaultSession(regionName string) *mcclient.Clie func (cli *SOpenStackClient) getDefaultClient() *mcclient.Client { client := mcclient.NewClient(cli.authURL, 5, cli.debug, false, "", "") client.SetHttpTransportProxyFunc(cli.cpcfg.ProxyFunc) + _client := client.GetClient() + ts, _ := _client.Transport.(*http.Transport) + _client.Transport = cloudprovider.GetReadOnlyCheckTransport(ts, func(req *http.Request) error { + if cli.cpcfg.ReadOnly { + if req.Method == "GET" || req.Method == "HEAD" { + return nil + } + // 认证 + if req.Method == "POST" && strings.HasSuffix(req.URL.Path, "auth/tokens") { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) + } + return nil + }) + return client } diff --git a/pkg/multicloud/qcloud/qcloud.go b/pkg/multicloud/qcloud/qcloud.go index 22b8797310..5133ebc182 100644 --- a/pkg/multicloud/qcloud/qcloud.go +++ b/pkg/multicloud/qcloud/qcloud.go @@ -15,9 +15,11 @@ package qcloud import ( + "bytes" "context" "encoding/json" "fmt" + "io/ioutil" "net/http" "net/url" "strconv" @@ -608,7 +610,28 @@ func (client *SQcloudClient) getSdkClient(regionId string) (*common.Client, erro return nil, err } httpClient := client.cpcfg.AdaptiveTimeoutHttpClient() - cli.WithHttpTransport(httpClient.Transport) + ts, _ := httpClient.Transport.(*http.Transport) + cli.WithHttpTransport(cloudprovider.GetReadOnlyCheckTransport(ts, func(req *http.Request) error { + if client.cpcfg.ReadOnly { + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return errors.Wrapf(err, "ioutil.ReadAll") + } + req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + params, err := url.ParseQuery(string(body)) + if err != nil { + return errors.Wrapf(err, "ParseQuery(%s)", string(body)) + } + action := params.Get("Action") + for _, prefix := range []string{"Get", "List", "Describe"} { + if strings.HasPrefix(action, prefix) { + return nil + } + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, action) + } + return nil + })) return cli, nil } @@ -870,6 +893,9 @@ func (client *SQcloudClient) getCosClient(bucket *SBucket) (*cos.Client, error) BucketURL: u, } } + ts := &http.Transport{ + Proxy: client.cpcfg.ProxyFunc, + } cosClient := cos.NewClient( baseUrl, &http.Client{ @@ -881,9 +907,15 @@ func (client *SQcloudClient) getCosClient(bucket *SBucket) (*cos.Client, error) RequestBody: client.debug, ResponseHeader: client.debug, ResponseBody: client.debug, - Transport: &http.Transport{ - Proxy: client.cpcfg.ProxyFunc, - }, + Transport: cloudprovider.GetReadOnlyCheckTransport(ts, func(req *http.Request) error { + if client.cpcfg.ReadOnly { + if req.Method == "GET" || req.Method == "HEAD" { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s", req.Method, req.URL.Path) + } + return nil + }), }, }, }, diff --git a/pkg/multicloud/ucloud/utils.go b/pkg/multicloud/ucloud/utils.go index 67f81b251c..5c5a125af6 100644 --- a/pkg/multicloud/ucloud/utils.go +++ b/pkg/multicloud/ucloud/utils.go @@ -19,6 +19,9 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/cloudprovider" ) func unmarshalResult(resp jsonutils.JSONObject, respErr error, resultKey string, result interface{}) error { @@ -76,6 +79,9 @@ func doListPart(client *SUcloudClient, action string, params SParams, resultKey // 执行操作 func DoAction(client *SUcloudClient, action string, params SParams, resultKey string, result interface{}) error { + if client.cpcfg.ReadOnly { + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, action) + } params.SetAction(action) resp, err := jsonRequest(client, params) return unmarshalResult(resp, err, resultKey, result) diff --git a/pkg/multicloud/zstack/zstack.go b/pkg/multicloud/zstack/zstack.go index 49ebe4bc55..78689816c8 100644 --- a/pkg/multicloud/zstack/zstack.go +++ b/pkg/multicloud/zstack/zstack.go @@ -106,6 +106,21 @@ func getSignUrl(uri string) (string, error) { func NewZStackClient(cfg *ZstackClientConfig) (*SZStackClient, error) { httpClient := cfg.cpcfg.AdaptiveTimeoutHttpClient() + ts, _ := httpClient.Transport.(*http.Transport) + httpClient.Transport = cloudprovider.GetReadOnlyCheckTransport(ts, func(req *http.Request) error { + if cfg.cpcfg.ReadOnly { + if req.Method == "GET" || req.Method == "HEAD" { + return nil + } + // 认证 + if req.Method == "PUT" && req.URL.Path == "/zstack/v1/accounts/login" { + return nil + } + return errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) + } + return nil + }) + cli := &SZStackClient{ ZstackClientConfig: cfg, httpClient: httpClient, diff --git a/vendor/modules.txt b/vendor/modules.txt index 2bd884fafe..4a21028602 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1231,7 +1231,7 @@ yunion.io/x/log/hooks yunion.io/x/ovsdb/cli_util yunion.io/x/ovsdb/schema/ovn_nb yunion.io/x/ovsdb/types -# yunion.io/x/pkg v0.0.0-20220227083757-28690b33ce38 +# yunion.io/x/pkg v0.0.0-20220406030238-39fbc60d5d4e yunion.io/x/pkg/errors yunion.io/x/pkg/gotypes yunion.io/x/pkg/prettytable diff --git a/vendor/yunion.io/x/pkg/errors/consts.go b/vendor/yunion.io/x/pkg/errors/consts.go index 399142ec92..9ce2305204 100644 --- a/vendor/yunion.io/x/pkg/errors/consts.go +++ b/vendor/yunion.io/x/pkg/errors/consts.go @@ -10,13 +10,15 @@ const ( ErrEOF = Error("EOFError") ErrNetwork = Error("NetworkError") ErrConnectRefused = Error("ConnectRefusedError") + ErrConnectReset = Error("ConnectResetError") ErrTimeout = Error("TimeoutError") - ErrNotFound = Error("NotFoundError") - ErrDuplicateId = Error("DuplicateIdError") - ErrInvalidStatus = Error("InvalidStatusError") - ErrNotImplemented = Error("NotImplementedError") - ErrNotSupported = Error("NotSupportedError") + ErrNotFound = Error("NotFoundError") + ErrDuplicateId = Error("DuplicateIdError") + ErrInvalidStatus = Error("InvalidStatusError") + ErrNotImplemented = Error("NotImplementedError") + ErrNotSupported = Error("NotSupportedError") + ErrAccountReadOnly = Error("AccountReadOnlyError") ErrAggregate = Error("AggregateError") )