feature: invalidate auth token

This commit is contained in:
Qiu Jian
2023-10-04 01:00:24 +08:00
parent 39f7c87632
commit 400977c27f
19 changed files with 575 additions and 103 deletions
+8 -2
View File
@@ -656,8 +656,14 @@ func doLogout(ctx context.Context, w http.ResponseWriter, req *http.Request) {
token, _, _ := fetchAuthInfo(ctx, req)
if token != nil {
// valid login, log the event
user := logclient.NewSimpleObject(token.GetUserId(), token.GetUserName(), "user")
logclient.AddActionLogWithContext(ctx, user, logclient.ACT_LOGOUT, "", token, true)
err := auth.Remove(ctx, token.GetTokenString())
if err != nil {
log.Errorf("remove token fail %s", err)
return
} else {
user := logclient.NewSimpleObject(token.GetUserId(), token.GetUserName(), "user")
logclient.AddActionLogWithContext(ctx, user, logclient.ACT_LOGOUT, "", token, true)
}
}
clearAuthCookie(w)
appsrv.DisableClientCache(w)
+2
View File
@@ -39,4 +39,6 @@ type SUserExtended struct {
IsLocal bool
// IdpId string
// IdpName string
AuditIds []string
}
+4
View File
@@ -163,6 +163,10 @@ func (self *SCASDriver) Authenticate(ctx context.Context, ident mcclient.SAuthen
idp.TryUserJoinProject(self.casConfig.SIdpAttributeOptions, ctx, usr, domain.Id, attrs)
extUser.AuditIds = []string{
ident.CASTicket.Id,
}
return extUser, nil
}
+2
View File
@@ -308,5 +308,7 @@ func (drv *SLDAPDriver) Authenticate(ctx context.Context, ident mcclient.SAuthen
return nil, errors.Wrap(err, "Authenticate error")
}
usrExt.AuditIds = []string{username}
return usrExt, nil
}
+2
View File
@@ -118,6 +118,8 @@ func (self *SOAuth2Driver) Authenticate(ctx context.Context, ident mcclient.SAut
idp.TryUserJoinProject(options, ctx, usr, domain.Id, attrs)
extUser.AuditIds = []string{ident.OAuth2.Code}
return extUser, nil
}
+2
View File
@@ -188,6 +188,8 @@ func (self *SOIDCDriver) Authenticate(ctx context.Context, ident mcclient.SAuthe
idp.TryUserJoinProject(self.oidcConfig.SIdpAttributeOptions, ctx, usr, domain.Id, attrs)
extUser.AuditIds = []string{ident.OIDCAuth.Code}
return extUser, nil
}
+2
View File
@@ -157,6 +157,8 @@ func (self *SSAMLDriver) Authenticate(ctx context.Context, ident mcclient.SAuthe
idp.TryUserJoinProject(self.samlConfig.SIdpAttributeOptions, ctx, usr, domain.Id, attrs)
extUser.AuditIds = []string{resp.ID}
return extUser, nil
}
+3
View File
@@ -16,6 +16,7 @@ package sql
import (
"context"
"fmt"
"time"
"github.com/pkg/errors"
@@ -81,6 +82,8 @@ func (sql *SSQLDriver) Authenticate(ctx context.Context, ident mcclient.SAuthent
return nil, errors.Wrap(err, "usrExt.VerifyPassword")
}
localUser.ClearFailedAuth()
usrExt.AuditIds = []string{fmt.Sprintf("%d", localUser.Id)}
return usrExt, nil
}
+29 -11
View File
@@ -20,6 +20,7 @@ import (
"fmt"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/gotypes"
"yunion.io/x/pkg/tristate"
@@ -161,14 +162,14 @@ func (manager *SCredentialManager) ValidateCreateData(
return input, nil
}
func (self *SCredential) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error {
return self.SStandaloneResourceBase.ValidateDeleteCondition(ctx, nil)
func (cred *SCredential) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error {
return cred.SStandaloneResourceBase.ValidateDeleteCondition(ctx, nil)
}
func (self *SCredential) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.CredentialUpdateInput) (api.CredentialUpdateInput, error) {
func (cred *SCredential) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.CredentialUpdateInput) (api.CredentialUpdateInput, error) {
var err error
input.StandaloneResourceBaseUpdateInput, err = self.SStandaloneResourceBase.ValidateUpdateData(ctx, userCred, query, input.StandaloneResourceBaseUpdateInput)
input.StandaloneResourceBaseUpdateInput, err = cred.SStandaloneResourceBase.ValidateUpdateData(ctx, userCred, query, input.StandaloneResourceBaseUpdateInput)
if err != nil {
return input, errors.Wrap(err, "SStandaloneResourceBase.ValidateUpdateData")
}
@@ -210,13 +211,13 @@ func credentialExtra(cred *SCredential, out api.CredentialDetails) api.Credentia
return out
}
func (self *SCredential) getBlob() []byte {
return keys.CredentialKeyManager.Decrypt([]byte(self.EncryptedBlob))
func (cred *SCredential) getBlob() []byte {
return keys.CredentialKeyManager.Decrypt([]byte(cred.EncryptedBlob))
}
func (self *SCredential) GetAccessKeySecret() (*api.SAccessKeySecretBlob, error) {
if self.Type == api.ACCESS_SECRET_TYPE || self.Type == api.OIDC_CREDENTIAL_TYPE {
blobJson, err := jsonutils.Parse(self.getBlob())
func (cred *SCredential) GetAccessKeySecret() (*api.SAccessKeySecretBlob, error) {
if cred.Type == api.ACCESS_SECRET_TYPE || cred.Type == api.OIDC_CREDENTIAL_TYPE {
blobJson, err := jsonutils.Parse(cred.getBlob())
if err != nil {
return nil, errors.Wrap(err, "jsonutils.Parse")
}
@@ -245,8 +246,8 @@ func (manager *SCredentialManager) FilterByOwner(q *sqlchemy.SQuery, man db.Filt
return q
}
func (self *SCredential) GetOwnerId() mcclient.IIdentityProvider {
owner := db.SOwnerId{UserId: self.UserId}
func (cred *SCredential) GetOwnerId() mcclient.IIdentityProvider {
owner := db.SOwnerId{UserId: cred.UserId}
return &owner
}
@@ -363,3 +364,20 @@ func (manager *SCredentialManager) DeleteAll(ctx context.Context, userCred mccli
}
return nil
}
func (cred *SCredential) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
err := cred.SStandaloneResourceBase.Delete(ctx, userCred)
if err != nil {
return errors.Wrap(err, "SStandaloneResourceBase.Delete")
}
if cred.Type == api.ACCESS_SECRET_TYPE {
// clean tokens auth by this AKSK
err := TokenCacheManager.BatchInvalidate(ctx, api.AUTH_METHOD_AKSK, []string{cred.Id})
if err != nil {
log.Errorf("BatchInvalidate token failed %s", err)
}
}
return nil
}
+178
View File
@@ -0,0 +1,178 @@
// 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"
"sort"
"strings"
"time"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/timeutils"
"yunion.io/x/sqlchemy"
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/mcclient"
)
var TokenCacheManager *STokenCacheManager
func init() {
TokenCacheManager = &STokenCacheManager{
SModelBaseManager: db.NewModelBaseManager(
STokenCache{},
"token_cache_tbl",
"token_cache",
"token_caches",
),
}
TokenCacheManager.SetVirtualObject(TokenCacheManager)
}
type STokenCache struct {
db.SModelBase
Token string `width:"700" charset:"ascii" nullable:"false" primary:"true"`
ExpiredAt time.Time `nullable:"false"`
Valid bool
Method string `width:"32" charset:"ascii"`
AuditIds string `width:"700" charset:"utf8" index:"true"`
}
type STokenCacheManager struct {
db.SModelBaseManager
}
func joinAuditIds(ids []string) string {
sort.Strings(ids)
return strings.Join(ids, ",")
}
func (manager *STokenCacheManager) Save(ctx context.Context, token string, expiredAt time.Time, method string, auditIds []string) error {
return manager.insert(ctx, token, expiredAt, true, method, auditIds)
}
func (manager *STokenCacheManager) Invalidate(ctx context.Context, token string, expiredAt time.Time, method string, auditIds []string) error {
return manager.insert(ctx, token, expiredAt, false, method, auditIds)
}
func (manager *STokenCacheManager) BatchInvalidate(ctx context.Context, method string, auditIds []string) error {
invalidQueue := []sCacheCredential{
{
Method: method,
AuditIds: auditIds,
},
}
for i := 0; i < len(invalidQueue); i++ {
queues, err := manager.batchInvalidateInternal(ctx, invalidQueue[i])
if err != nil {
return errors.Wrap(err, "batchInvalidateInternal")
}
if len(queues) > 0 {
invalidQueue = append(invalidQueue, queues...)
}
}
return nil
}
type sCacheCredential struct {
Method string
AuditIds []string
}
func (manager *STokenCacheManager) batchInvalidateInternal(ctx context.Context, cred sCacheCredential) ([]sCacheCredential, error) {
q := manager.Query().Equals("method", cred.Method).Equals("audit_ids", joinAuditIds(cred.AuditIds))
tokens := make([]STokenCache, 0)
err := db.FetchModelObjects(manager, q, &tokens)
if err != nil {
return nil, errors.Wrap(err, "FetchModelObjects")
}
if len(tokens) == 0 {
return nil, nil
}
queues := make([]sCacheCredential, 0)
for i := range tokens {
token := tokens[i]
queues = append(queues, sCacheCredential{
Method: api.AUTH_METHOD_TOKEN,
AuditIds: []string{token.Token},
})
}
err = manager.TableSpec().GetTableSpec().UpdateBatch(
map[string]interface{}{
"valid": false,
},
map[string]interface{}{
"method": cred.Method,
"audit_ids": joinAuditIds(cred.AuditIds),
},
)
return queues, errors.Wrap(err, "UpdateBatch")
}
func (manager *STokenCacheManager) insert(ctx context.Context, token string, expiredAt time.Time, valid bool, method string, auditIds []string) error {
val := STokenCache{
Token: token,
ExpiredAt: expiredAt,
Valid: valid,
Method: method,
AuditIds: joinAuditIds(auditIds),
}
err := manager.TableSpec().InsertOrUpdate(ctx, &val)
return errors.Wrap(err, "InsertOrUpdate")
}
func (manager *STokenCacheManager) IsValid(token string) (bool, error) {
q := manager.Query().Equals("token", token)
tokenCache := STokenCache{}
err := q.First(&tokenCache)
if err != nil {
return false, errors.Wrap(err, "Query")
}
return tokenCache.Valid, nil
}
func (manager *STokenCacheManager) removeObsolete() error {
sql := fmt.Sprintf("DELETE FROM `%s` WHERE `expired_at` < ?", manager.TableSpec().Name())
db := sqlchemy.GetDBWithName(manager.TableSpec().GetDBName())
now := timeutils.UtcNow()
_, err := db.Exec(sql, now.Add(-24*time.Hour))
return errors.Wrap(err, "Exec Delete")
}
func RemoveObsoleteInvalidTokens(ctx context.Context, userCred mcclient.TokenCredential, start bool) {
err := TokenCacheManager.removeObsolete()
if err != nil {
log.Errorf("RemoveObsoleteInvalidTokens fail %s", err)
}
}
func (manager *STokenCacheManager) FetchInvalidTokens() ([]string, error) {
q := manager.Query("token").IsFalse("valid")
tokens := make([]STokenCache, 0)
err := db.FetchModelObjects(manager, q, &tokens)
if err != nil {
return nil, errors.Wrap(err, "FetchModelObjects")
}
ret := make([]string, len(tokens))
for i := range tokens {
ret[i] = tokens[i].Token
}
return ret, nil
}
+5
View File
@@ -17,6 +17,7 @@ package models
import (
"context"
"database/sql"
"fmt"
"time"
"yunion.io/x/jsonutils"
@@ -929,6 +930,10 @@ func (user *SUser) Delete(ctx context.Context, userCred mcclient.TokenCredential
if err != nil {
return errors.Wrap(err, "PasswordManager.delete")
}
batchErr := TokenCacheManager.BatchInvalidate(ctx, api.AUTH_METHOD_PASSWORD, []string{fmt.Sprintf("%d", localUser.Id)})
if batchErr != nil {
log.Errorf("BatchInvalidate fail %s", batchErr)
}
}
err = IdmappingManager.deleteByPublicId(user.Id, api.IdMappingEntityUser)
+2
View File
@@ -75,6 +75,8 @@ func InitHandlers(app *appsrv.Application) {
models.IdentityQuotaManager,
models.IdentityUsageManager,
models.IdentityPendingUsageManager,
models.TokenCacheManager,
} {
db.RegisterModelManager(manager)
}
+2
View File
@@ -100,6 +100,8 @@ func StartService() {
cron.AddJobEveryFewHour("AutoPurgeSplitable", 4, 30, 0, db.AutoPurgeSplitable, false)
cron.AddJobEveryFewDays("CheckAllUserPasswordIsExpired", 1, 8, 0, 0, models.CheckAllUserPasswordIsExpired, true)
cron.AddJobEveryFewHour("RemoveObsoleteInvalidTokens", 6, 0, 0, models.RemoveObsoleteInvalidTokens, true)
cron.Start()
defer cron.Stop()
}
+41 -6
View File
@@ -23,7 +23,6 @@ import (
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/s3auth"
"yunion.io/x/pkg/utils"
"yunion.io/x/sqlchemy"
api "yunion.io/x/onecloud/pkg/apis/identity"
@@ -47,12 +46,32 @@ func authUserByTokenV3(ctx context.Context, input mcclient.SAuthenticationInputV
}
func authUserByToken(ctx context.Context, tokenStr string) (*api.SUserExtended, error) {
valid, err := models.TokenCacheManager.IsValid(tokenStr)
if err == nil {
if !valid {
return nil, errors.Wrap(httperrors.ErrInvalidCredential, "invalid token")
} else {
// passthrough
}
} else {
if errors.Cause(err) != sql.ErrNoRows {
return nil, errors.Wrap(err, "TokenCacheManager.IsValid")
} else {
// passthrough
}
}
token := SAuthToken{}
err := token.ParseFernetToken(tokenStr)
err = token.ParseFernetToken(tokenStr)
if err != nil {
return nil, errors.Wrap(err, "token.ParseFernetToken")
}
return models.UserManager.FetchUserExtended(token.UserId, "", "", "")
extUser, err := models.UserManager.FetchUserExtended(token.UserId, "", "", "")
if err != nil {
return nil, errors.Wrap(err, "FetchUserExtended")
}
extUser.AuditIds = []string{tokenStr}
return extUser, nil
}
func authUserByPasswordV2(ctx context.Context, input mcclient.SAuthenticationInputV2) (*api.SUserExtended, error) {
@@ -390,6 +409,9 @@ func authUserByAccessKeyV3(ctx context.Context, input mcclient.SAuthenticationIn
if err != nil {
return nil, "", aksk, errors.Wrap(err, "UserManager.FetchUserExtended")
}
usrExt.AuditIds = []string{keyId}
return usrExt, credential.ProjectId, aksk, nil
}
@@ -410,6 +432,9 @@ func authUserByVerify(ctx context.Context, input mcclient.SAuthenticationInputV3
if err != nil {
return nil, errors.Wrap(err, "Verify")
}
extUser.AuditIds = []string{input.Auth.Identity.Verify.Uid}
return extUser, nil
}
@@ -496,7 +521,7 @@ func AuthenticateV3(ctx context.Context, input mcclient.SAuthenticationInputV3)
token := SAuthToken{}
token.UserId = user.Id
token.Method = method
token.AuditIds = []string{utils.GenRequestId(16)}
token.AuditIds = user.AuditIds
now := time.Now().UTC()
token.ExpiresAt = now.Add(time.Duration(options.Options.TokenExpirationSeconds) * time.Second)
token.Context = input.Auth.Context
@@ -540,6 +565,9 @@ func AuthenticateV3(ctx context.Context, input mcclient.SAuthenticationInputV3)
if err != nil {
return nil, errors.Wrap(err, "getTokenV3")
}
models.TokenCacheManager.Save(ctx, tokenV3.Id, token.ExpiresAt, token.Method, token.AuditIds)
return tokenV3, nil
}
@@ -600,7 +628,7 @@ func _authenticateV2(ctx context.Context, input mcclient.SAuthenticationInputV2)
token := SAuthToken{}
token.UserId = user.Id
token.Method = method
token.AuditIds = []string{utils.GenRequestId(16)}
token.AuditIds = user.AuditIds
now := time.Now().UTC()
token.ExpiresAt = now.Add(time.Duration(options.Options.TokenExpirationSeconds) * time.Second)
token.Context = input.Auth.Context
@@ -625,5 +653,12 @@ func _authenticateV2(ctx context.Context, input mcclient.SAuthenticationInputV2)
return nil, errors.Wrap(err, "project.FetchExtend")
}
return token.getTokenV2(ctx, user, projExt)
tokenV2, err := token.getTokenV2(ctx, user, projExt)
if err != nil {
return nil, errors.Wrap(err, "getTokenV2")
}
models.TokenCacheManager.Save(ctx, tokenV2.Token.Id, token.ExpiresAt, token.Method, token.AuditIds)
return tokenV2, nil
}
+40
View File
@@ -16,6 +16,7 @@ package tokens
import (
"context"
"database/sql"
"net/http"
"yunion.io/x/jsonutils"
@@ -40,6 +41,9 @@ func AddHandler(app *appsrv.Application) {
app.AddHandler2("GET", "/v3/auth/tokens", authenticateToken(verifyTokensV3), nil, "verify_tokens_v3", nil)
app.AddHandler2("GET", "/v3/auth/policies", authenticateToken(fetchTokenPolicies), nil, "fetch_token_policies", nil)
app.AddHandler2("POST", "/v3/auth/policies", authenticateToken(postTokenPolicies), nil, "post_token_policies", nil)
app.AddHandler2("DELETE", "/v3/auth/tokens", authenticateToken(invalidateTokenV3), nil, "delete_tokens_v3", nil)
app.AddHandler2("GET", "/v3/auth/tokens/invalid", authenticateToken(fetchInvalidTokensV3), nil, "fetch_revoked_tokens_v3", nil)
}
func FetchAuthContext(authCtx mcclient.SAuthContext, r *http.Request) mcclient.SAuthContext {
@@ -131,6 +135,24 @@ type VerifyTokenV2Param struct {
func verifyTokensV2(ctx context.Context, w http.ResponseWriter, r *http.Request) {
params, _, _ := appsrv.FetchEnv(ctx, w, r)
tokenStr := params["<token>"]
valid, err := models.TokenCacheManager.IsValid(tokenStr)
if err == nil {
if !valid {
httperrors.InvalidCredentialError(ctx, w, "invalid token")
return
} else {
// passthrough
}
} else {
if errors.Cause(err) != sql.ErrNoRows {
httperrors.GeneralServerError(ctx, w, err)
return
} else {
// passthrough
}
}
token, err := verifyCommon(ctx, w, tokenStr)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
@@ -179,6 +201,24 @@ type VerifyTokenV3Param struct {
// 200: tokens_AuthenticateV3Output
func verifyTokensV3(ctx context.Context, w http.ResponseWriter, r *http.Request) {
tokenStr := r.Header.Get(api.AUTH_SUBJECT_TOKEN_HEADER)
valid, err := models.TokenCacheManager.IsValid(tokenStr)
if err == nil {
if !valid {
httperrors.InvalidCredentialError(ctx, w, "invalid token")
return
} else {
// passthrough
}
} else {
if errors.Cause(err) != sql.ErrNoRows {
httperrors.GeneralServerError(ctx, w, err)
return
} else {
// passthrough
}
}
token, err := verifyCommon(ctx, w, tokenStr)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
+90
View File
@@ -0,0 +1,90 @@
// 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 tokens
import (
"context"
"net/http"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/rbacscope"
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/cloudcommon/policy"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/keystone/models"
)
// swagger:route DELETE /v3/auth/tokens authentication invalidateTokensV3
//
// keystone v3删除token API
//
// keystone v3删除token API
//
func invalidateTokenV3(ctx context.Context, w http.ResponseWriter, r *http.Request) {
tokenStr := r.Header.Get(api.AUTH_SUBJECT_TOKEN_HEADER)
err := invalidateToken(ctx, tokenStr)
if err != nil {
httperrors.GeneralServerError(ctx, w, err)
return
}
appsrv.SendNoContent(w)
}
func invalidateToken(ctx context.Context, tokenStr string) error {
adminToken := policy.FetchUserCredential(ctx)
if adminToken == nil || len(tokenStr) == 0 {
return httperrors.NewForbiddenError("missing auth token")
}
if adminToken.IsAllow(rbacscope.ScopeSystem, api.SERVICE_TYPE, "tokens", "delete").Result.IsDeny() {
return httperrors.NewForbiddenError("%s not allow to auth", adminToken.GetUserName())
}
token := SAuthToken{}
err := token.ParseFernetToken(tokenStr)
if err != nil {
return httperrors.NewInvalidCredentialError(errors.Wrapf(err, "invalid token").Error())
}
err = models.TokenCacheManager.Invalidate(ctx, tokenStr, token.ExpiresAt, token.Method, token.AuditIds)
if err != nil {
return errors.Wrap(err, "Insert")
}
return nil
}
// swagger:route GET /v3/auth/tokens/invalid authentication fetchInvalidTokensV3
//
// keystone v3获取被删除的token的列表API
//
// keystone v3获取被删除的token的列表API
//
func fetchInvalidTokensV3(ctx context.Context, w http.ResponseWriter, r *http.Request) {
}
func fetchInvalidTokens(ctx context.Context) ([]string, error) {
adminToken := policy.FetchUserCredential(ctx)
if adminToken == nil {
return nil, httperrors.NewForbiddenError("missing auth token")
}
if adminToken.IsAllow(rbacscope.ScopeSystem, api.SERVICE_TYPE, "tokens", "list", "invalid").Result.IsDeny() {
return nil, httperrors.NewForbiddenError("%s not allow to list invalid tokens", adminToken.GetUserName())
}
tokens, err := models.TokenCacheManager.FetchInvalidTokens()
if err != nil {
return nil, errors.Wrap(err, "TokenCacheManager.FetchInvalidTokens")
}
return tokens, nil
}
+1 -1
View File
@@ -21,9 +21,9 @@ import (
"time"
"github.com/golang-plus/uuid"
"github.com/pkg/errors"
"github.com/vmihailenco/msgpack"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/netutils"
"yunion.io/x/onecloud/pkg/mcclient"
+55 -2
View File
@@ -31,6 +31,7 @@ import (
"yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/cloudcommon/consts"
"yunion.io/x/onecloud/pkg/cloudcommon/syncman"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
@@ -140,6 +141,17 @@ func (c *TokenCacheVerify) Verify(ctx context.Context, cli *mcclient.Client, adm
return cred, nil
}
func (c *TokenCacheVerify) Remove(ctx context.Context, cli *mcclient.Client, adminToken, token string) error {
c.DeleteToken(token)
err := cli.Invalidate(ctx, adminToken, token)
if err != nil {
return errors.Wrap(err, "Invalidate")
}
return nil
}
type authManager struct {
syncman.SSyncManager
@@ -158,9 +170,35 @@ func newAuthManager(cli *mcclient.Client, info *AuthInfo) *authManager {
accessKeyCache: newAccessKeyCache(),
}
authm.InitSync(authm)
authm.startRefreshRevokeTokens()
return authm
}
func (a *authManager) startRefreshRevokeTokens() {
ticker := time.NewTicker(5 * time.Minute)
for range ticker.C {
err := a.refreshRevokeTokens(context.Background())
if err != nil {
log.Errorf("%s", err)
}
}
ticker.Stop()
}
func (a *authManager) refreshRevokeTokens(ctx context.Context) error {
if a.adminCredential == nil {
return fmt.Errorf("refreshRevokeTokens: No valid admin token credential")
}
tokens, err := a.client.FetchInvalidTokens(ctx, a.adminCredential.GetTokenString())
if err != nil {
return errors.Wrap(err, "client.FetchInvalidTokens")
}
for _, token := range tokens {
a.tokenCacheVerify.DeleteToken(token)
}
return nil
}
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")
@@ -174,15 +212,26 @@ func (a *authManager) verifyRequest(req http.Request, virtualHost bool) (mcclien
func (a *authManager) verify(ctx context.Context, token string) (mcclient.TokenCredential, error) {
if a.adminCredential == nil {
return nil, fmt.Errorf("No valid admin token credential")
return nil, errors.Wrap(httperrors.ErrInvalidCredential, "No valid admin token credential")
}
cred, err := a.tokenCacheVerify.Verify(ctx, a.client, a.adminCredential.GetTokenString(), token)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "tokenCacheVerify.Verify")
}
return cred, nil
}
func (a *authManager) remove(ctx context.Context, token string) error {
if a.adminCredential == nil {
return errors.Wrap(httperrors.ErrInvalidCredential, "No valid admin token credential")
}
err := a.tokenCacheVerify.Remove(ctx, a.client, a.adminCredential.GetTokenString(), token)
if err != nil {
return errors.Wrap(err, "tokenCacheVerify.Remove")
}
return nil
}
var (
defaultAuthSource = mcclient.AuthSourceSrv
)
@@ -312,6 +361,10 @@ func Verify(ctx context.Context, tokenId string) (mcclient.TokenCredential, erro
return manager.verify(ctx, tokenId)
}
func Remove(ctx context.Context, tokenId string) error {
return manager.remove(ctx, tokenId)
}
func VerifyRequest(req http.Request, virtualHost bool) (mcclient.TokenCredential, error) {
return manager.verifyRequest(req, virtualHost)
}
+107 -81
View File
@@ -91,41 +91,41 @@ func NewClient(authUrl string, timeout int, debug bool, insecure bool, certFile,
return &client
}
func (this *Client) HttpClient() *http.Client {
return this.httpconn
func (client *Client) HttpClient() *http.Client {
return client.httpconn
}
func (this *Client) SetHttpTransportProxyFunc(proxyFunc httputils.TransportProxyFunc) {
httputils.SetClientProxyFunc(this.httpconn, proxyFunc)
func (client *Client) SetHttpTransportProxyFunc(proxyFunc httputils.TransportProxyFunc) {
httputils.SetClientProxyFunc(client.httpconn, proxyFunc)
}
func (this *Client) GetClient() *http.Client {
return this.httpconn
func (client *Client) GetClient() *http.Client {
return client.httpconn
}
func (this *Client) SetTransport(ts http.RoundTripper) {
this.httpconn.Transport = ts
func (client *Client) SetTransport(ts http.RoundTripper) {
client.httpconn.Transport = ts
}
func (this *Client) SetDebug(debug bool) {
this.debug = debug
func (client *Client) SetDebug(debug bool) {
client.debug = debug
}
func (this *Client) GetDebug() bool {
return this.debug
func (client *Client) GetDebug() bool {
return client.debug
}
func (this *Client) AuthVersion() string {
pos := strings.LastIndexByte(this.authUrl, '/')
func (client *Client) AuthVersion() string {
pos := strings.LastIndexByte(client.authUrl, '/')
if pos > 0 {
return this.authUrl[pos+1:]
return client.authUrl[pos+1:]
} else {
return ""
}
}
func (this *Client) NewAuthTokenCredential() TokenCredential {
if this.AuthVersion() == "v3" {
func (client *Client) NewAuthTokenCredential() TokenCredential {
if client.AuthVersion() == "v3" {
return &TokenCredentialV3{}
}
return &TokenCredentialV2{}
@@ -153,15 +153,15 @@ func joinUrl(baseUrl, path string) string {
return fmt.Sprintf("%s%s", baseUrl, path)
}
func (this *Client) rawRequest(ctx context.Context, endpoint string, token string, method httputils.THttpMethod, url string, header http.Header, body io.Reader) (*http.Response, error) {
return httputils.Request(this.httpconn, ctx, method, joinUrl(endpoint, url), getDefaultHeader(header, token), body, this.debug)
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) {
return httputils.Request(client.httpconn, ctx, method, joinUrl(endpoint, url), getDefaultHeader(header, token), body, client.debug)
}
func (this *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) {
return httputils.JSONRequest(this.httpconn, ctx, method, joinUrl(endpoint, url), getDefaultHeader(header, token), body, this.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) {
return httputils.JSONRequest(client.httpconn, ctx, method, joinUrl(endpoint, url), getDefaultHeader(header, token), body, client.debug)
}
func (this *Client) _authV3(domainName, uname, passwd, projectId, projectName, projectDomain, token string, aCtx SAuthContext) (TokenCredential, error) {
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}
@@ -190,24 +190,24 @@ func (this *Client) _authV3(domainName, uname, passwd, projectId, projectName, p
// }
}
input.Auth.Context = aCtx
return this._authV3Input(input)
return client._authV3Input(input)
}
func (this *Client) _authV3Input(input SAuthenticationInputV3) (TokenCredential, error) {
hdr, rbody, err := this.jsonRequest(context.Background(), this.authUrl, "", "POST", "/auth/tokens", nil, jsonutils.Marshal(&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, fmt.Errorf("No X-Subject-Token in header")
return nil, errors.Wrap(httperrors.ErrInputParameter, "No X-Subject-Token in header")
}
ret, err := this.unmarshalV3Token(rbody, tokenId)
ret, err := client.unmarshalV3Token(rbody, tokenId)
return ret, err
}
func (this *Client) _authV2(uname, passwd, tenantId, tenantName, token string, aCtx SAuthContext) (TokenCredential, error) {
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
@@ -221,144 +221,170 @@ func (this *Client) _authV2(uname, passwd, tenantId, tenantName, token string, a
input.Auth.Token.Id = token
}
input.Auth.Context = aCtx
_, rbody, err := this.jsonRequest(context.Background(), this.authUrl, "", "POST", "/tokens", nil, jsonutils.Marshal(&input))
_, rbody, err := client.jsonRequest(context.Background(), client.authUrl, "", "POST", "/tokens", nil, jsonutils.Marshal(&input))
if err != nil {
return nil, err
}
return this.unmarshalV2Token(rbody)
return client.unmarshalV2Token(rbody)
}
func (this *Client) Authenticate(uname, passwd, domainName, tenantName, tenantDomain string) (TokenCredential, error) {
return this.AuthenticateApi(uname, passwd, domainName, tenantName, tenantDomain)
func (client *Client) Authenticate(uname, passwd, domainName, tenantName, tenantDomain string) (TokenCredential, error) {
return client.AuthenticateApi(uname, passwd, domainName, tenantName, tenantDomain)
}
func (this *Client) AuthenticateApi(uname, passwd, domainName, tenantName, tenantDomain string) (TokenCredential, error) {
return this.AuthenticateWithSource(uname, passwd, domainName, tenantName, tenantDomain, AuthSourceAPI)
func (client *Client) AuthenticateApi(uname, passwd, domainName, tenantName, tenantDomain string) (TokenCredential, error) {
return client.AuthenticateWithSource(uname, passwd, domainName, tenantName, tenantDomain, AuthSourceAPI)
}
func (this *Client) AuthenticateWeb(uname, passwd, domainName, tenantName, tenantDomain string, cliIp string) (TokenCredential, error) {
func (client *Client) AuthenticateWeb(uname, passwd, domainName, tenantName, tenantDomain string, cliIp string) (TokenCredential, error) {
aCtx := SAuthContext{
Source: AuthSourceWeb,
Ip: cliIp,
}
return this.authenticateWithContext(uname, passwd, domainName, tenantName, tenantDomain, aCtx)
return client.authenticateWithContext(uname, passwd, domainName, tenantName, tenantDomain, aCtx)
}
func (this *Client) AuthenticateOperator(uname, passwd, domainName, tenantName, tenantDomain string) (TokenCredential, error) {
return this.AuthenticateWithSource(uname, passwd, domainName, tenantName, tenantDomain, AuthSourceOperator)
func (client *Client) AuthenticateOperator(uname, passwd, domainName, tenantName, tenantDomain string) (TokenCredential, error) {
return client.AuthenticateWithSource(uname, passwd, domainName, tenantName, tenantDomain, AuthSourceOperator)
}
func (this *Client) AuthenticateWithSource(uname, passwd, domainName, tenantName, tenantDomain string, source string) (TokenCredential, error) {
func (client *Client) AuthenticateWithSource(uname, passwd, domainName, tenantName, tenantDomain string, source string) (TokenCredential, error) {
aCtx := SAuthContext{
Source: source,
}
return this.authenticateWithContext(uname, passwd, domainName, tenantName, tenantDomain, aCtx)
return client.authenticateWithContext(uname, passwd, domainName, tenantName, tenantDomain, aCtx)
}
func (this *Client) authenticateWithContext(uname, passwd, domainName, tenantName, tenantDomain string, aCtx SAuthContext) (TokenCredential, error) {
if this.AuthVersion() == "v3" {
return this._authV3(domainName, uname, passwd, "", 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 this._authV2(uname, passwd, "", tenantName, "", aCtx)
return client._authV2(uname, passwd, "", tenantName, "", aCtx)
}
func (this *Client) unmarshalV3Token(rbody jsonutils.JSONObject, tokenId string) (cred TokenCredential, err error) {
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 = fmt.Errorf("Invalid response when unmarshal V3 Token: %v", err)
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 {
this.SetServiceCatalog(cata)
client.SetServiceCatalog(cata)
}
return
}
func (this *Client) unmarshalV2Token(rbody jsonutils.JSONObject) (cred TokenCredential, err error) {
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 = fmt.Errorf("Invalid response when unmarshal V2 Token: %s", err)
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 {
this.SetServiceCatalog(cata)
client.SetServiceCatalog(cata)
}
return
}
err = fmt.Errorf("Invalid response: no access object")
err = errors.Wrap(httperrors.ErrInvalidFormat, "Invalid response: no access object")
return
}
func (this *Client) verifyV3(adminToken, token string) (TokenCredential, error) {
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 := this.jsonRequest(context.Background(), this.authUrl, "", "GET", "/auth/tokens", header, nil)
_, rbody, err := client.jsonRequest(context.Background(), client.authUrl, "", "GET", "/auth/tokens", header, nil)
if err != nil {
return nil, err
}
return this.unmarshalV3Token(rbody, token)
return client.unmarshalV3Token(rbody, token)
}
func (this *Client) verifyV2(adminToken, token string) (TokenCredential, error) {
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 := this.jsonRequest(context.Background(), this.authUrl, "", "GET", verifyUrl, header, nil)
_, rbody, err := client.jsonRequest(context.Background(), client.authUrl, "", "GET", verifyUrl, header, nil)
if err != nil {
return nil, err
}
return this.unmarshalV2Token(rbody)
return client.unmarshalV2Token(rbody)
}
func (this *Client) Verify(adminToken, token string) (cred TokenCredential, err error) {
if this.AuthVersion() == "v3" {
return this.verifyV3(adminToken, token)
func (client *Client) Verify(adminToken, token string) (cred TokenCredential, err error) {
if client.AuthVersion() == "v3" {
return client.verifyV3(adminToken, token)
}
return this.verifyV2(adminToken, token)
return client.verifyV2(adminToken, token)
}
func (this *Client) SetTenant(tenantId, tenantName, tenantDomain string, token TokenCredential) (TokenCredential, error) {
return this.SetProject(tenantId, tenantName, tenantDomain, 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 (this *Client) AuthenticateToken(token string, projName, projDomain string, source string) (TokenCredential, error) {
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 this.AuthVersion() == "v3" {
return this._authV3("", "", "", "", projName, projDomain, token, aCtx)
if client.AuthVersion() == "v3" {
return client._authV3("", "", "", "", projName, projDomain, token, aCtx)
} else {
return this._authV2("", "", "", projName, token, aCtx)
return client._authV2("", "", "", projName, token, aCtx)
}
}
func (this *Client) SetProject(tenantId, tenantName, tenantDomain string, token TokenCredential) (TokenCredential, error) {
func (client *Client) SetProject(tenantId, tenantName, tenantDomain string, token TokenCredential) (TokenCredential, error) {
aCtx := SAuthContext{
Source: token.GetLoginSource(),
Ip: token.GetLoginIp(),
}
if this.AuthVersion() == "v3" {
return this._authV3("", "", "", tenantId, tenantName, tenantDomain, token.GetTokenString(), aCtx)
if client.AuthVersion() == "v3" {
return client._authV3("", "", "", tenantId, tenantName, tenantDomain, token.GetTokenString(), aCtx)
} else {
return this._authV2("", "", "", tenantName, token.GetTokenString(), aCtx)
return client._authV2("", "", "", tenantName, token.GetTokenString(), aCtx)
}
}
func (this *Client) GetCommonEtcdEndpoint(token TokenCredential, region, interfaceType string) (*api.EndpointDetails, error) {
if this.AuthVersion() != "v3" {
return nil, errors.Errorf("current version %s not support get internal etcd endpoint", this.AuthVersion())
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 := this.GetServiceCatalog().getServiceURL(apis.SERVICE_TYPE_ETCD, region, "", interfaceType)
_, err := client.GetServiceCatalog().getServiceURL(apis.SERVICE_TYPE_ETCD, region, "", interfaceType)
if err != nil {
return nil, err
}
@@ -371,7 +397,7 @@ func (this *Client) GetCommonEtcdEndpoint(token TokenCredential, region, interfa
params.Add(jsonutils.NewString(region), "region")
epUrl := "/endpoints?" + params.QueryString()
_, rbody, err := this.jsonRequest(context.Background(), this.authUrl, token.GetTokenString(), httputils.GET, epUrl, nil, nil)
_, 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")
}
@@ -392,7 +418,7 @@ func (this *Client) GetCommonEtcdEndpoint(token TokenCredential, region, interfa
return endpoint, nil
}
func (this *Client) GetCommonEtcdTLSConfig(endpoint *api.EndpointDetails) (*tls.Config, error) {
func (client *Client) GetCommonEtcdTLSConfig(endpoint *api.EndpointDetails) (*tls.Config, error) {
if endpoint.CertId == "" {
return nil, nil
}
@@ -402,13 +428,13 @@ func (this *Client) GetCommonEtcdTLSConfig(endpoint *api.EndpointDetails) (*tls.
return seclib2.InitTLSConfigByData(caData, certData, keyData)
}
func (this *Client) NewSession(ctx context.Context, region, zone, endpointType string, token TokenCredential) *ClientSession {
func (client *Client) NewSession(ctx context.Context, region, zone, endpointType string, token TokenCredential) *ClientSession {
cata := token.GetServiceCatalog()
if this.GetServiceCatalog() == nil {
if client.GetServiceCatalog() == nil {
if cata == nil || cata.Len() == 0 {
log.Warningf("Missing service catalog in token")
} else {
this.SetServiceCatalog(cata)
client.SetServiceCatalog(cata)
}
}
if ctx == nil {
@@ -416,7 +442,7 @@ func (this *Client) NewSession(ctx context.Context, region, zone, endpointType s
}
return &ClientSession{
ctx: ctx,
client: this,
client: client,
region: region,
zone: zone,
endpointType: endpointType,