mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-05-06 21:52:54 +08:00
533 lines
17 KiB
Go
533 lines
17 KiB
Go
// 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 mcclient
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"yunion.io/x/jsonutils"
|
|
"yunion.io/x/log"
|
|
"yunion.io/x/pkg/appctx"
|
|
"yunion.io/x/pkg/errors"
|
|
"yunion.io/x/pkg/gotypes"
|
|
"yunion.io/x/pkg/util/httputils"
|
|
"yunion.io/x/pkg/util/rbacscope"
|
|
|
|
"yunion.io/x/onecloud/pkg/apis"
|
|
api "yunion.io/x/onecloud/pkg/apis/identity"
|
|
"yunion.io/x/onecloud/pkg/appsrv"
|
|
"yunion.io/x/onecloud/pkg/cloudcommon/consts"
|
|
"yunion.io/x/onecloud/pkg/httperrors"
|
|
"yunion.io/x/onecloud/pkg/util/rbacutils"
|
|
"yunion.io/x/onecloud/pkg/util/seclib2"
|
|
)
|
|
|
|
var listenerWorker *appsrv.SWorkerManager
|
|
|
|
type Client struct {
|
|
authUrl string
|
|
timeout int
|
|
debug bool
|
|
|
|
httpconn *http.Client
|
|
_serviceCatalog IServiceCatalog
|
|
|
|
catalogListeners []IServiceCatalogChangeListener
|
|
}
|
|
|
|
func init() {
|
|
listenerWorker = appsrv.NewWorkerManager("client_catalog_listener_worker", 1, 2048, false)
|
|
}
|
|
|
|
func NewClient(authUrl string, timeout int, debug bool, insecure bool, certFile, keyFile string) *Client {
|
|
var tlsConf *tls.Config
|
|
|
|
if len(certFile) > 0 && len(keyFile) > 0 {
|
|
var err error
|
|
tlsConf, err = seclib2.InitTLSConfig(certFile, keyFile)
|
|
if err != nil {
|
|
log.Errorf("load TLS failed %s", err)
|
|
}
|
|
}
|
|
|
|
if tlsConf == nil || gotypes.IsNil(tlsConf) {
|
|
tlsConf = &tls.Config{}
|
|
}
|
|
tlsConf.InsecureSkipVerify = insecure
|
|
|
|
tr := httputils.GetTransport(insecure)
|
|
tr.TLSClientConfig = tlsConf
|
|
tr.IdleConnTimeout = 5 * time.Second
|
|
tr.TLSHandshakeTimeout = 10 * time.Second
|
|
tr.ResponseHeaderTimeout = 0
|
|
|
|
client := Client{authUrl: authUrl,
|
|
timeout: timeout,
|
|
debug: debug,
|
|
httpconn: &http.Client{
|
|
Transport: tr,
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
}, // 不自动处理重定向请求
|
|
},
|
|
}
|
|
|
|
return &client
|
|
}
|
|
|
|
func (client *Client) HttpClient() *http.Client {
|
|
return client.httpconn
|
|
}
|
|
|
|
func (client *Client) SetHttpTransportProxyFunc(proxyFunc httputils.TransportProxyFunc) {
|
|
httputils.SetClientProxyFunc(client.httpconn, proxyFunc)
|
|
}
|
|
|
|
func (client *Client) GetClient() *http.Client {
|
|
return client.httpconn
|
|
}
|
|
|
|
func (client *Client) SetTransport(ts http.RoundTripper) {
|
|
client.httpconn.Transport = ts
|
|
}
|
|
|
|
func (client *Client) SetDebug(debug bool) {
|
|
client.debug = debug
|
|
}
|
|
|
|
func (client *Client) GetDebug() bool {
|
|
return client.debug
|
|
}
|
|
|
|
func (client *Client) AuthVersion() string {
|
|
pos := strings.LastIndexByte(client.authUrl, '/')
|
|
if pos > 0 {
|
|
return client.authUrl[pos+1:]
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (client *Client) NewAuthTokenCredential() TokenCredential {
|
|
if client.AuthVersion() == "v3" {
|
|
return &TokenCredentialV3{}
|
|
}
|
|
return &TokenCredentialV2{}
|
|
}
|
|
|
|
func getDefaultHeader(header http.Header, token string) http.Header {
|
|
if len(token) > 0 {
|
|
if header == nil {
|
|
header = http.Header{}
|
|
}
|
|
if len(header.Get(AUTH_TOKEN)) == 0 {
|
|
header.Add(AUTH_TOKEN, token)
|
|
}
|
|
}
|
|
return header
|
|
}
|
|
|
|
func joinUrl(baseUrl, path string) string {
|
|
base, version := SplitVersionedURL(baseUrl)
|
|
if len(version) > 0 {
|
|
if strings.HasPrefix(path, fmt.Sprintf("/%s/", version)) {
|
|
baseUrl = base
|
|
}
|
|
}
|
|
return fmt.Sprintf("%s%s", baseUrl, path)
|
|
}
|
|
|
|
func FixContext(ctx context.Context) context.Context {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
srvType := consts.GetServiceType()
|
|
if len(srvType) > 0 && len(appctx.AppContextServiceName(ctx)) == 0 {
|
|
ctx = context.WithValue(ctx, appctx.APP_CONTEXT_KEY_APPNAME, srvType)
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
func (client *Client) rawRequest(ctx context.Context, endpoint string, token string, method httputils.THttpMethod, url string, header http.Header, body io.Reader) (*http.Response, error) {
|
|
ctx = FixContext(ctx)
|
|
return httputils.Request(client.httpconn, ctx, method, joinUrl(endpoint, url), getDefaultHeader(header, token), body, client.debug)
|
|
}
|
|
|
|
func (client *Client) jsonRequest(ctx context.Context, endpoint string, token string, method httputils.THttpMethod, url string, header http.Header, body jsonutils.JSONObject) (http.Header, jsonutils.JSONObject, error) {
|
|
ctx = FixContext(ctx)
|
|
return httputils.JSONRequest(client.httpconn, ctx, method, joinUrl(endpoint, url), getDefaultHeader(header, token), body, client.debug)
|
|
}
|
|
|
|
func (client *Client) _authV3(domainName, uname, passwd, projectId, projectName, projectDomain, token string, aCtx SAuthContext) (TokenCredential, error) {
|
|
input := SAuthenticationInputV3{}
|
|
if len(uname) > 0 && len(passwd) > 0 { // Password authentication
|
|
input.Auth.Identity.Methods = []string{api.AUTH_METHOD_PASSWORD}
|
|
input.Auth.Identity.Password.User.Name = uname
|
|
input.Auth.Identity.Password.User.Password = passwd
|
|
if len(domainName) > 0 {
|
|
input.Auth.Identity.Password.User.Domain.Name = domainName
|
|
}
|
|
// else {
|
|
// input.Auth.Identity.Password.User.Domain.Name = api.DEFAULT_DOMAIN_ID
|
|
//}
|
|
} else if len(token) > 0 {
|
|
input.Auth.Identity.Methods = []string{api.AUTH_METHOD_TOKEN}
|
|
input.Auth.Identity.Token.Id = token
|
|
}
|
|
if len(projectId) > 0 {
|
|
input.Auth.Scope.Project.Id = projectId
|
|
}
|
|
if len(projectName) > 0 {
|
|
input.Auth.Scope.Project.Name = projectName
|
|
if len(projectDomain) > 0 {
|
|
input.Auth.Scope.Project.Domain.Name = projectDomain
|
|
}
|
|
// else {
|
|
// input.Auth.Scope.Project.Domain.Id = api.DEFAULT_DOMAIN_ID
|
|
// }
|
|
}
|
|
input.Auth.Context = aCtx
|
|
return client._authV3Input(input)
|
|
}
|
|
|
|
func (client *Client) _authV3Input(input SAuthenticationInputV3) (TokenCredential, error) {
|
|
hdr, rbody, err := client.jsonRequest(context.Background(), client.authUrl, "", "POST", "/auth/tokens", nil, jsonutils.Marshal(&input))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tokenId := hdr.Get("X-Subject-Token")
|
|
if len(tokenId) == 0 {
|
|
return nil, errors.Wrap(httperrors.ErrInputParameter, "No X-Subject-Token in header")
|
|
}
|
|
ret, err := client.unmarshalV3Token(rbody, tokenId)
|
|
return ret, err
|
|
}
|
|
|
|
func (client *Client) _authV2(uname, passwd, tenantId, tenantName, token string, aCtx SAuthContext) (TokenCredential, error) {
|
|
input := SAuthenticationInputV2{}
|
|
input.Auth.PasswordCredentials.Username = uname
|
|
input.Auth.PasswordCredentials.Password = passwd
|
|
if len(tenantName) > 0 {
|
|
input.Auth.TenantName = tenantName
|
|
}
|
|
if len(tenantId) > 0 {
|
|
input.Auth.TenantId = tenantId
|
|
}
|
|
if len(token) > 0 {
|
|
input.Auth.Token.Id = token
|
|
}
|
|
input.Auth.Context = aCtx
|
|
_, rbody, err := client.jsonRequest(context.Background(), client.authUrl, "", "POST", "/tokens", nil, jsonutils.Marshal(&input))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return client.unmarshalV2Token(rbody)
|
|
}
|
|
|
|
func (client *Client) Authenticate(uname, passwd, domainName, tenantName, tenantDomain string) (TokenCredential, error) {
|
|
return client.AuthenticateApi(uname, passwd, domainName, tenantName, tenantDomain)
|
|
}
|
|
|
|
func (client *Client) AuthenticateApi(uname, passwd, domainName, tenantName, tenantDomain string) (TokenCredential, error) {
|
|
return client.AuthenticateWithSource(uname, passwd, domainName, tenantName, tenantDomain, AuthSourceAPI)
|
|
}
|
|
|
|
func (client *Client) AuthenticateWeb(uname, passwd, domainName, tenantName, tenantDomain string, cliIp string) (TokenCredential, error) {
|
|
aCtx := SAuthContext{
|
|
Source: AuthSourceWeb,
|
|
Ip: cliIp,
|
|
}
|
|
return client.authenticateWithContext(uname, passwd, domainName, tenantName, tenantDomain, aCtx)
|
|
}
|
|
|
|
func (client *Client) AuthenticateOperator(uname, passwd, domainName, tenantName, tenantDomain string) (TokenCredential, error) {
|
|
return client.AuthenticateWithSource(uname, passwd, domainName, tenantName, tenantDomain, AuthSourceOperator)
|
|
}
|
|
|
|
func (client *Client) AuthenticateWithSource(uname, passwd, domainName, tenantName, tenantDomain string, source string) (TokenCredential, error) {
|
|
aCtx := SAuthContext{
|
|
Source: source,
|
|
}
|
|
return client.authenticateWithContext(uname, passwd, domainName, tenantName, tenantDomain, aCtx)
|
|
}
|
|
|
|
func (client *Client) authenticateWithContext(uname, passwd, domainName, tenantName, tenantDomain string, aCtx SAuthContext) (TokenCredential, error) {
|
|
if client.AuthVersion() == "v3" {
|
|
return client._authV3(domainName, uname, passwd, "", tenantName, tenantDomain, "", aCtx)
|
|
}
|
|
return client._authV2(uname, passwd, "", tenantName, "", aCtx)
|
|
}
|
|
|
|
func (client *Client) unmarshalV3Token(rbody jsonutils.JSONObject, tokenId string) (cred TokenCredential, err error) {
|
|
cred = &TokenCredentialV3{Id: tokenId}
|
|
err = rbody.Unmarshal(cred)
|
|
if err != nil {
|
|
err = errors.Wrap(err, "Invalid response when unmarshal V3 Token")
|
|
}
|
|
cata := cred.GetServiceCatalog()
|
|
if cata == nil || cata.Len() == 0 {
|
|
log.Warningf("No service catalog avaiable")
|
|
} else {
|
|
client.SetServiceCatalog(cata)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (client *Client) unmarshalV2Token(rbody jsonutils.JSONObject) (cred TokenCredential, err error) {
|
|
access, err := rbody.Get("access")
|
|
if err == nil {
|
|
cred = &TokenCredentialV2{}
|
|
err = access.Unmarshal(cred)
|
|
if err != nil {
|
|
err = errors.Wrap(err, "Invalid response when unmarshal V2 Token")
|
|
}
|
|
cata := cred.GetServiceCatalog()
|
|
if cata == nil || cata.Len() == 0 {
|
|
log.Warningf("No srvice catalog avaiable")
|
|
} else {
|
|
client.SetServiceCatalog(cata)
|
|
}
|
|
return
|
|
}
|
|
err = errors.Wrap(httperrors.ErrInvalidFormat, "Invalid response: no access object")
|
|
return
|
|
}
|
|
|
|
func (client *Client) verifyV3(adminToken, token string) (TokenCredential, error) {
|
|
header := http.Header{}
|
|
header.Add(api.AUTH_TOKEN_HEADER, adminToken)
|
|
header.Add(api.AUTH_SUBJECT_TOKEN_HEADER, token)
|
|
_, rbody, err := client.jsonRequest(context.Background(), client.authUrl, "", "GET", "/auth/tokens", header, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return client.unmarshalV3Token(rbody, token)
|
|
}
|
|
|
|
func (client *Client) verifyV2(adminToken, token string) (TokenCredential, error) {
|
|
header := http.Header{}
|
|
header.Add(api.AUTH_TOKEN_HEADER, adminToken)
|
|
verifyUrl := fmt.Sprintf("/tokens/%s", token)
|
|
_, rbody, err := client.jsonRequest(context.Background(), client.authUrl, "", "GET", verifyUrl, header, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return client.unmarshalV2Token(rbody)
|
|
}
|
|
|
|
func (client *Client) Verify(adminToken, token string) (cred TokenCredential, err error) {
|
|
if client.AuthVersion() == "v3" {
|
|
return client.verifyV3(adminToken, token)
|
|
}
|
|
return client.verifyV2(adminToken, token)
|
|
}
|
|
|
|
func (client *Client) Invalidate(ctx context.Context, adminToken, token string) error {
|
|
header := http.Header{}
|
|
header.Add(api.AUTH_TOKEN_HEADER, adminToken)
|
|
header.Add(api.AUTH_SUBJECT_TOKEN_HEADER, token)
|
|
_, _, err := client.jsonRequest(ctx, client.authUrl, "", "DELETE", "/auth/tokens", header, nil)
|
|
if err != nil {
|
|
return errors.Wrap(err, "jsonRequest")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (client *Client) FetchInvalidTokens(ctx context.Context, adminToken string) ([]string, error) {
|
|
header := http.Header{}
|
|
header.Add(api.AUTH_TOKEN_HEADER, adminToken)
|
|
_, resp, err := client.jsonRequest(ctx, client.authUrl, "", "GET", "/auth/tokens/invalid", header, nil)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "jsonRequest")
|
|
}
|
|
tokens := make([]string, 0)
|
|
err = resp.Unmarshal(&tokens, "tokens")
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Unmarshal")
|
|
}
|
|
return tokens, nil
|
|
}
|
|
|
|
func (client *Client) SetTenant(tenantId, tenantName, tenantDomain string, token TokenCredential) (TokenCredential, error) {
|
|
return client.SetProject(tenantId, tenantName, tenantDomain, token)
|
|
}
|
|
|
|
func (client *Client) AuthenticateToken(token string, projName, projDomain string, source string) (TokenCredential, error) {
|
|
aCtx := SAuthContext{
|
|
Source: source,
|
|
}
|
|
if client.AuthVersion() == "v3" {
|
|
return client._authV3("", "", "", "", projName, projDomain, token, aCtx)
|
|
} else {
|
|
return client._authV2("", "", "", projName, token, aCtx)
|
|
}
|
|
}
|
|
|
|
func (client *Client) SetProject(tenantId, tenantName, tenantDomain string, token TokenCredential) (TokenCredential, error) {
|
|
aCtx := SAuthContext{
|
|
Source: token.GetLoginSource(),
|
|
Ip: token.GetLoginIp(),
|
|
}
|
|
if client.AuthVersion() == "v3" {
|
|
return client._authV3("", "", "", tenantId, tenantName, tenantDomain, token.GetTokenString(), aCtx)
|
|
} else {
|
|
return client._authV2("", "", "", tenantName, token.GetTokenString(), aCtx)
|
|
}
|
|
}
|
|
|
|
func (client *Client) GetCommonEtcdEndpoint(token TokenCredential, region, interfaceType string) (*api.EndpointDetails, error) {
|
|
if client.AuthVersion() != "v3" {
|
|
return nil, errors.Errorf("current version %s not support get internal etcd endpoint", client.AuthVersion())
|
|
}
|
|
|
|
_, err := client.GetServiceCatalog().getServiceURL(apis.SERVICE_TYPE_ETCD, region, "", interfaceType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
params := jsonutils.NewDict()
|
|
params.Add(jsonutils.NewString(interfaceType), "interface")
|
|
params.Add(jsonutils.JSONTrue, "enabled")
|
|
params.Add(jsonutils.NewString(apis.SERVICE_TYPE_ETCD), "service")
|
|
params.Add(jsonutils.JSONTrue, "details")
|
|
params.Add(jsonutils.NewString(region), "region")
|
|
|
|
epUrl := "/endpoints?" + params.QueryString()
|
|
_, rbody, err := client.jsonRequest(context.Background(), client.authUrl, token.GetTokenString(), httputils.GET, epUrl, nil, nil)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "get internal etcd endpoint")
|
|
}
|
|
rets, err := rbody.GetArray("endpoints")
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "get endpoints response")
|
|
}
|
|
if len(rets) == 0 {
|
|
return nil, errors.Wrapf(httperrors.ErrNotFound, "not found service %s %s endpoint", apis.SERVICE_TYPE_ETCD, interfaceType)
|
|
}
|
|
if len(rets) > 1 {
|
|
return nil, errors.Errorf("fond %d duplicate serivce %s %s endpoint", len(rets), apis.SERVICE_TYPE_ETCD, interfaceType)
|
|
}
|
|
endpoint := new(api.EndpointDetails)
|
|
if err := rets[0].Unmarshal(endpoint); err != nil {
|
|
return nil, errors.Wrap(err, "unmarshal endpoint")
|
|
}
|
|
return endpoint, nil
|
|
}
|
|
|
|
func (client *Client) GetCommonEtcdTLSConfig(endpoint *api.EndpointDetails) (*tls.Config, error) {
|
|
if endpoint.CertId == "" {
|
|
return nil, nil
|
|
}
|
|
caData := []byte(endpoint.CaCertificate)
|
|
certData := []byte(endpoint.Certificate)
|
|
keyData := []byte(endpoint.PrivateKey)
|
|
return seclib2.InitTLSConfigByData(caData, certData, keyData)
|
|
}
|
|
|
|
func (client *Client) NewSession(ctx context.Context, region, zone, endpointType string, token TokenCredential) *ClientSession {
|
|
cata := token.GetServiceCatalog()
|
|
if client.GetServiceCatalog() == nil {
|
|
if cata == nil || cata.Len() == 0 {
|
|
log.Warningf("Missing service catalog in token")
|
|
} else {
|
|
client.SetServiceCatalog(cata)
|
|
}
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
return &ClientSession{
|
|
ctx: ctx,
|
|
client: client,
|
|
region: region,
|
|
zone: zone,
|
|
endpointType: endpointType,
|
|
token: token,
|
|
Header: http.Header{},
|
|
customizeServiceUrl: map[string]string{},
|
|
}
|
|
}
|
|
|
|
type SCheckPoliciesInput struct {
|
|
UserId string
|
|
ProjectId string
|
|
LoginIp string
|
|
}
|
|
|
|
type SFetchMatchPoliciesOutput struct {
|
|
Names map[rbacscope.TRbacScope][]string `json:"names"`
|
|
Policies rbacutils.TPolicyGroup `json:"policies"`
|
|
}
|
|
|
|
func (o *SFetchMatchPoliciesOutput) Decode(object jsonutils.JSONObject) error {
|
|
err := object.Unmarshal(&o.Names, "names")
|
|
if err != nil {
|
|
return errors.Wrap(err, "unmarshal names")
|
|
}
|
|
pData, err := object.Get("policies")
|
|
if err != nil {
|
|
return errors.Wrap(err, "Get policies")
|
|
}
|
|
o.Policies, err = rbacutils.DecodePolicyGroup(pData)
|
|
if err != nil {
|
|
return errors.Wrap(err, "DecodePolicyGroup")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o SFetchMatchPoliciesOutput) Encode() jsonutils.JSONObject {
|
|
output := jsonutils.NewDict()
|
|
output.Set("names", jsonutils.Marshal(o.Names))
|
|
output.Set("policies", o.Policies.Encode())
|
|
return output
|
|
}
|
|
|
|
func (client *Client) FetchMatchPolicies(ctx context.Context, token TokenCredential) (*SFetchMatchPoliciesOutput, error) {
|
|
header := http.Header{}
|
|
if token.GetTokenString() != "" {
|
|
header.Add(api.AUTH_TOKEN_HEADER, token.GetTokenString())
|
|
}
|
|
_, rbody, err := client.jsonRequest(ctx, client.authUrl, "", "GET", "/auth/policies", header, nil)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "jsonRequest")
|
|
}
|
|
output := &SFetchMatchPoliciesOutput{}
|
|
err = output.Decode(rbody)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "SFetchMatchPoliciesOutput.Decode")
|
|
}
|
|
return output, nil
|
|
}
|
|
|
|
func (client *Client) CheckMatchPolicies(ctx context.Context, adminToken TokenCredential, input SCheckPoliciesInput) (*SFetchMatchPoliciesOutput, error) {
|
|
_, rbody, err := client.jsonRequest(ctx, client.authUrl, adminToken.GetTokenString(), "POST", "/auth/policies", nil, jsonutils.Marshal(input))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "jsonRequest")
|
|
}
|
|
output := &SFetchMatchPoliciesOutput{}
|
|
err = output.Decode(rbody)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "SFetchMatchPoliciesOutput.Decode")
|
|
}
|
|
return output, nil
|
|
}
|