Files
cloudpods/pkg/mcclient/auth/auth.go
2020-04-01 19:59:58 +08:00

394 lines
11 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 auth
import (
"context"
"fmt"
"net/http"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/util/cache"
"yunion.io/x/onecloud/pkg/mcclient"
)
var (
manager *authManager
defaultTimeout int = 600 // maybe time.Duration better
defaultCacheCount int64 = 100000
initCh chan bool = make(chan bool)
globalEndpointType string
)
const (
PublicEndpointType string = "public"
InternalEndpointType string = "internal"
)
type AuthInfo struct {
AuthUrl string
// Domain not need when v2 auth
Domain string
Username string
Passwd string
// Project is tenant when v2 auth
Project string
ProjectDomain string
}
func SetTimeout(t time.Duration) {
defaultTimeout = int(t)
}
func SetEndpointType(epType string) {
globalEndpointType = epType
}
func NewV2AuthInfo(authUrl, user, passwd, tenant string) *AuthInfo {
return NewAuthInfo(authUrl, "", user, passwd, tenant, "")
}
func NewAuthInfo(authUrl, domain, user, passwd, project, projectDomain string) *AuthInfo {
return &AuthInfo{
AuthUrl: authUrl,
Domain: domain,
Username: user,
Passwd: passwd,
Project: project,
ProjectDomain: projectDomain,
}
}
type cacheItem struct {
credential mcclient.TokenCredential
}
func (item *cacheItem) Size() int {
return 1
}
type TokenCacheVerify struct {
*cache.LRUCache
}
func NewTokenCacheVerify() *TokenCacheVerify {
return &TokenCacheVerify{
LRUCache: cache.NewLRUCache(defaultCacheCount),
}
}
func (c *TokenCacheVerify) AddToken(cred mcclient.TokenCredential) error {
item := &cacheItem{cred}
c.Set(cred.GetTokenString(), item)
return nil
}
func (c *TokenCacheVerify) GetToken(token string) (mcclient.TokenCredential, bool) {
item, found := c.Get(token)
if !found {
return nil, false
}
return item.(*cacheItem).credential, true
}
func (c *TokenCacheVerify) DeleteToken(token string) bool {
return c.Delete(token)
}
func (c *TokenCacheVerify) Verify(cli *mcclient.Client, adminToken, token string) (mcclient.TokenCredential, error) {
cred, found := c.GetToken(token)
if found {
if cred.IsValid() {
return cred, nil
} else {
c.DeleteToken(token)
log.Infof("Remove expired cache token: %s", token)
}
}
cred, err := cli.Verify(adminToken, token)
if err != nil {
return nil, err
}
cred = mcclient.SimplifyToken(cred)
err = c.AddToken(cred)
if err != nil {
return nil, fmt.Errorf("Add %s credential to cache: %#v", cred.GetTokenString(), err)
}
callbackAuthhooks(cred)
// log.Debugf("Add token: %s", cred)
return cred, nil
}
type authManager struct {
client *mcclient.Client
info *AuthInfo
adminCredential mcclient.TokenCredential
tokenCacheVerify *TokenCacheVerify
accessKeyCache *sAccessKeyCache
}
func newAuthManager(cli *mcclient.Client, info *AuthInfo) *authManager {
return &authManager{
client: cli,
info: info,
tokenCacheVerify: NewTokenCacheVerify(),
accessKeyCache: newAccessKeyCache(),
}
}
func (a *authManager) verifyRequest(req http.Request, virtualHost bool) (mcclient.TokenCredential, error) {
if a.adminCredential == nil {
return nil, fmt.Errorf("No valid admin token credential")
}
cred, err := a.accessKeyCache.Verify(a.client, req, virtualHost)
if err != nil {
return nil, err
}
return cred, nil
}
func (a *authManager) verify(token string) (mcclient.TokenCredential, error) {
if a.adminCredential == nil {
return nil, fmt.Errorf("No valid admin token credential")
}
cred, err := a.tokenCacheVerify.Verify(a.client, a.adminCredential.GetTokenString(), token)
if err != nil {
return nil, err
}
return cred, nil
}
func (a *authManager) authAdmin() error {
var token mcclient.TokenCredential
var err error
token, err = a.client.AuthenticateWithSource(
a.info.Username, a.info.Passwd, a.info.Domain,
a.info.Project, a.info.ProjectDomain, mcclient.AuthSourceSrv)
if err != nil {
log.Errorf("Admin auth failed: %s", err)
return err
}
if token != nil {
a.adminCredential = token
return nil
} else {
return fmt.Errorf("Auth token is nil")
}
}
func (a *authManager) reAuth() {
redoSleepTime := 60 * time.Second
for {
err := a.authAdmin()
if err == nil {
break
}
log.Errorf("Reauth failed: %s, try it again after %v", err, redoSleepTime)
time.Sleep(redoSleepTime)
}
expire := a.adminCredential.GetExpires()
duration := expire.Sub(time.Now())
time.AfterFunc(time.Duration(duration.Nanoseconds()/2), a.reAuth)
}
func (a *authManager) GetServiceURL(service, region, zone, endpointType string) (string, error) {
if endpointType == "" && globalEndpointType != "" {
endpointType = globalEndpointType
}
return a.adminCredential.GetServiceURL(service, region, zone, endpointType)
}
func (a *authManager) GetServiceURLs(service, region, zone, endpointType string) ([]string, error) {
if endpointType == "" && globalEndpointType != "" {
endpointType = globalEndpointType
}
return a.adminCredential.GetServiceURLs(service, region, zone, endpointType)
}
func (a *authManager) getTokenString() string {
return a.adminCredential.GetTokenString()
}
func (a *authManager) isExpired() bool {
return time.Now().After(a.adminCredential.GetExpires())
}
func (a *authManager) isAuthed() bool {
if a == nil {
return false
}
if a.adminCredential == nil || a.isExpired() {
return false
}
return true
}
func (a *authManager) init() error {
if err := a.authAdmin(); err != nil {
initCh <- false
log.Fatalf("Auth manager init err: %v", err)
}
log.Infof("Get token: %v", a.getTokenString())
expire := a.adminCredential.GetExpires()
duration := expire.Sub(time.Now())
time.AfterFunc(time.Duration(duration.Nanoseconds()/2), a.reAuth)
initCh <- true
return nil
}
func GetCatalogData(serviceTypes []string, region string) jsonutils.JSONObject {
return manager.adminCredential.GetCatalogData(serviceTypes, region)
}
func Verify(tokenId string) (mcclient.TokenCredential, error) {
return manager.verify(tokenId)
}
func VerifyRequest(req http.Request, virtualHost bool) (mcclient.TokenCredential, error) {
return manager.verifyRequest(req, virtualHost)
}
func GetServiceURL(service, region, zone, endpointType string) (string, error) {
return manager.GetServiceURL(service, region, zone, endpointType)
}
func GetPublicServiceURL(service, region, zone string) (string, error) {
return manager.GetServiceURL(service, region, zone, PublicEndpointType)
}
func GetServiceURLs(service, region, zone, endpointType string) ([]string, error) {
return manager.GetServiceURLs(service, region, zone, endpointType)
}
func GetTokenString() string {
return manager.getTokenString()
}
func IsAuthed() bool {
return manager.isAuthed()
}
func Client() *mcclient.Client {
return manager.client
}
func AdminCredential() mcclient.TokenCredential {
return manager.adminCredential
}
func AdminSession(ctx context.Context, region, zone, endpointType, apiVersion string) *mcclient.ClientSession {
cli := Client()
if cli == nil {
return nil
}
if endpointType == "" && globalEndpointType != "" {
endpointType = globalEndpointType
}
return cli.NewSession(ctx, region, zone, endpointType, AdminCredential(), apiVersion)
}
func AdminSessionWithInternal(ctx context.Context, region, zone, apiVersion string) *mcclient.ClientSession {
return AdminSession(ctx, region, zone, "internal", apiVersion)
}
func AdminSessionWithPublic(ctx context.Context, region, zone, apiVersion string) *mcclient.ClientSession {
return AdminSession(ctx, region, zone, "public", apiVersion)
}
type AuthCompletedCallback func()
func (callback *AuthCompletedCallback) Run() {
f := *callback
for {
initOk := <-initCh
if initOk && manager.isAuthed() {
log.V(10).Infof("Auth completed, run callback: %v", *callback)
f()
return
}
log.Warningf("Auth manager not ready, check it again...")
}
}
func AsyncInit(info *AuthInfo, debug, insecure bool, certFile, keyFile string, callback AuthCompletedCallback) {
cli := mcclient.NewClient(info.AuthUrl, defaultTimeout, debug, insecure, certFile, keyFile)
manager = newAuthManager(cli, info)
go manager.init()
if callback != nil {
go callback.Run()
}
}
func Init(info *AuthInfo, debug, insecure bool, certFile, keyFile string) {
done := make(chan bool, 1)
f := func() {
done <- true
}
AsyncInit(info, debug, insecure, certFile, keyFile, f)
<-done
}
func GetAdminSession(ctx context.Context, region string,
apiVersion string) *mcclient.ClientSession {
return GetSession(ctx, manager.adminCredential, region, apiVersion)
}
func GetAdminSessionWithPublic(ctx context.Context, region string,
apiVersion string) *mcclient.ClientSession {
return GetSessionWithPublic(ctx, manager.adminCredential, region, apiVersion)
}
func GetAdminSessionWithInternal(
ctx context.Context, region string, apiVersion string) *mcclient.ClientSession {
return GetSessionWithInternal(ctx, manager.adminCredential, region, apiVersion)
}
func GetSession(ctx context.Context, token mcclient.TokenCredential, region string, apiVersion string) *mcclient.ClientSession {
if len(globalEndpointType) != 0 {
return getSessionByType(ctx, token, region, apiVersion, globalEndpointType)
}
return GetSessionWithInternal(ctx, token, region, apiVersion)
}
func GetSessionWithInternal(ctx context.Context, token mcclient.TokenCredential, region string, apiVersion string) *mcclient.ClientSession {
return getSessionByType(ctx, token, region, apiVersion, InternalEndpointType)
}
func GetSessionWithPublic(ctx context.Context, token mcclient.TokenCredential, region string, apiVersion string) *mcclient.ClientSession {
return getSessionByType(ctx, token, region, apiVersion, PublicEndpointType)
}
func getSessionByType(ctx context.Context, token mcclient.TokenCredential, region string, apiVersion string, epType string) *mcclient.ClientSession {
return manager.client.NewSession(ctx, region, "", epType, token, apiVersion)
}
// use for climc test only
func InitFromClientSession(session *mcclient.ClientSession) {
cli := session.GetClient()
token := session.GetToken()
info := &AuthInfo{}
manager = &authManager{
client: cli,
info: info,
adminCredential: token,
}
}