| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- // 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
- }
|