From be2c26ad9907f8fd6e7af0bc29bedf7bc3ffb3a3 Mon Sep 17 00:00:00 2001 From: Qiu Jian Date: Fri, 19 Aug 2022 09:31:31 +0800 Subject: [PATCH] feature: verify user with sms code --- pkg/apigateway/handler/auth.go | 11 +++-- pkg/apigateway/handler/verify.go | 24 +++++++++++ pkg/apis/apigateway/regions.go | 2 - pkg/apis/identity/consts.go | 1 + pkg/apis/notify/receiver.go | 5 +++ pkg/keystone/tokens/auth.go | 27 ++++++++++++ pkg/keystone/tokens/util.go | 26 +++++++++++ pkg/keystone/util/session.go | 3 +- pkg/mcclient/input.go | 8 +++- pkg/mcclient/verify.go | 51 ++++++++++++++++++++++ pkg/notify/models/receiver.go | 55 +++++++++++++----------- pkg/notify/models/verify.go | 3 +- pkg/notify/tasks/subcontact_pull_task.go | 16 +++++++ 13 files changed, 197 insertions(+), 35 deletions(-) create mode 100644 pkg/apigateway/handler/verify.go create mode 100644 pkg/keystone/tokens/util.go create mode 100644 pkg/mcclient/verify.go diff --git a/pkg/apigateway/handler/auth.go b/pkg/apigateway/handler/auth.go index 0d43c1b53c..bdeeaf6c60 100644 --- a/pkg/apigateway/handler/auth.go +++ b/pkg/apigateway/handler/auth.go @@ -355,9 +355,14 @@ func (h *AuthHandlers) doCredentialLogin(ctx context.Context, req *http.Request, idpId, _ := body.GetString("idp_id") redirectUri := getSsoCallbackUrl(ctx, req, idpId) token, err = processSsoLoginData(body, cliIp, redirectUri) - if err != nil { - return nil, errors.Wrap(err, "processSsoLoginData") - } + // if err != nil { + // return nil, errors.Wrap(err, "processSsoLoginData") + // } + } else if body.Contains("verify_code") { // verify by mobile + verifyCode, _ := body.GetString("verify_code") + uid, _ := body.GetString("uid") + contactType, _ := body.GetString("contact_type") + token, err = processVerifyLoginData(uid, contactType, verifyCode, cliIp) } else { return nil, httperrors.NewInputParameterError("missing credential") } diff --git a/pkg/apigateway/handler/verify.go b/pkg/apigateway/handler/verify.go new file mode 100644 index 0000000000..7929a86667 --- /dev/null +++ b/pkg/apigateway/handler/verify.go @@ -0,0 +1,24 @@ +// 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 handler + +import ( + "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/auth" +) + +func processVerifyLoginData(uid, contactType, verifyCode string, cliIp string) (mcclient.TokenCredential, error) { + return auth.Client().AuthenticateVerify(uid, contactType, verifyCode, "", "", "", cliIp) +} diff --git a/pkg/apis/apigateway/regions.go b/pkg/apis/apigateway/regions.go index 37f3cbc28f..72b71dec98 100644 --- a/pkg/apis/apigateway/regions.go +++ b/pkg/apis/apigateway/regions.go @@ -32,8 +32,6 @@ type SRegionsReponse struct { Regions []string `json:"regions,allowempty"` Domains []string `json:"domains,allowempty"` - Captcha bool `json:"captcha,allowempty"` - Idps []SIdp `json:"idps,allowempty"` ReturnFullDomains bool `json:"return_full_domains"` diff --git a/pkg/apis/identity/consts.go b/pkg/apis/identity/consts.go index c0dd172894..49fed6e93b 100644 --- a/pkg/apis/identity/consts.go +++ b/pkg/apis/identity/consts.go @@ -37,6 +37,7 @@ const ( AUTH_METHOD_SAML = "saml" AUTH_METHOD_OIDC = "oidc" AUTH_METHOD_OAuth2 = "oauth2" + AUTH_METHOD_VERIFY = "verify" // AUTH_METHOD_ID_PASSWORD = 1 // AUTH_METHOD_ID_TOKEN = 2 diff --git a/pkg/apis/notify/receiver.go b/pkg/apis/notify/receiver.go index b120b51343..067a7adc8a 100644 --- a/pkg/apis/notify/receiver.go +++ b/pkg/apis/notify/receiver.go @@ -43,6 +43,9 @@ type ReceiverCreateInput struct { // description: enabled contact types for user // example: {"email", "mobile", "feishu", "dingtalk", "workwx"} EnabledContactTypes []string `json:"enabled_contact_types"` + + // force verified if admin create the records + ForceVerified bool `json:"force_verified"` } type SInternationalMobile struct { @@ -129,6 +132,8 @@ type ReceiverUpdateInput struct { // description: enabled contacts for user // example: {"email", "mobile", "feishu", "dingtalk", "workwx"} EnabledContactTypes []string `json:"enabled_contact_types"` + + ForceVerified bool `json:"force_verified"` } type ReceiverTriggerVerifyInput struct { diff --git a/pkg/keystone/tokens/auth.go b/pkg/keystone/tokens/auth.go index 8ece4901c4..60198f7e6e 100644 --- a/pkg/keystone/tokens/auth.go +++ b/pkg/keystone/tokens/auth.go @@ -26,12 +26,14 @@ import ( "yunion.io/x/sqlchemy" api "yunion.io/x/onecloud/pkg/apis/identity" + notify_api "yunion.io/x/onecloud/pkg/apis/notify" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/keystone/driver" "yunion.io/x/onecloud/pkg/keystone/models" "yunion.io/x/onecloud/pkg/keystone/options" "yunion.io/x/onecloud/pkg/keystone/saml" "yunion.io/x/onecloud/pkg/mcclient" + notify_modules "yunion.io/x/onecloud/pkg/mcclient/modules/notify" "yunion.io/x/onecloud/pkg/util/logclient" "yunion.io/x/onecloud/pkg/util/s3auth" ) @@ -391,6 +393,26 @@ func authUserByAccessKeyV3(ctx context.Context, input mcclient.SAuthenticationIn return usrExt, credential.ProjectId, aksk, nil } +func authUserByVerify(ctx context.Context, input mcclient.SAuthenticationInputV3) (*api.SUserExtended, error) { + extUser, err := models.UserManager.FetchUserExtended(input.Auth.Identity.Verify.Uid, "", "", "") + if err != nil { + return nil, errors.Wrap(err, "FetchUserExtended") + } + s, err := GetDefaulAdminSession(ctx, "") + if err != nil { + return nil, errors.Wrap(err, "GetDefaultAdminSession") + } + verifyInput := notify_api.ReceiverVerifyInput{ + ContactType: input.Auth.Identity.Verify.ContactType, + Token: input.Auth.Identity.Verify.VerifyCode, + } + _, err = notify_modules.NotifyReceiver.PerformAction(s, extUser.Id, "verify", jsonutils.Marshal(verifyInput)) + if err != nil { + return nil, errors.Wrap(err, "Verify") + } + return extUser, nil +} + // +onecloud:swagger-gen-route-method=POST // +onecloud:swagger-gen-route-path=/v3/auth/tokens // +onecloud:swagger-gen-route-tag=authentication @@ -445,6 +467,11 @@ func AuthenticateV3(ctx context.Context, input mcclient.SAuthenticationInputV3) if err != nil { return nil, errors.Wrap(err, "authUserByOAuth2") } + case api.AUTH_METHOD_VERIFY: + user, err = authUserByVerify(ctx, input) + if err != nil { + return nil, errors.Wrap(err, "authUserByVerify") + } default: // auth by other methods, e.g. password , etc... user, err = authUserByIdentityV3(ctx, input) diff --git a/pkg/keystone/tokens/util.go b/pkg/keystone/tokens/util.go new file mode 100644 index 0000000000..33715c17cf --- /dev/null +++ b/pkg/keystone/tokens/util.go @@ -0,0 +1,26 @@ +// 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" + + "yunion.io/x/onecloud/pkg/keystone/models" + "yunion.io/x/onecloud/pkg/mcclient" +) + +func GetDefaulAdminSession(ctx context.Context, region string) (*mcclient.ClientSession, error) { + return models.GetDefaultClientSession(ctx, GetDefaultAdminCredToken(), region), nil +} diff --git a/pkg/keystone/util/session.go b/pkg/keystone/util/session.go index 467f7eda48..2b05e01263 100644 --- a/pkg/keystone/util/session.go +++ b/pkg/keystone/util/session.go @@ -17,11 +17,10 @@ package util import ( "context" - "yunion.io/x/onecloud/pkg/keystone/models" "yunion.io/x/onecloud/pkg/keystone/tokens" "yunion.io/x/onecloud/pkg/mcclient" ) func GetDefaulAdminSession(ctx context.Context, region string) (*mcclient.ClientSession, error) { - return models.GetDefaultClientSession(ctx, tokens.GetDefaultAdminCredToken(), region), nil + return tokens.GetDefaulAdminSession(ctx, region) } diff --git a/pkg/mcclient/input.go b/pkg/mcclient/input.go index f5fee3a18e..65efa5ccef 100644 --- a/pkg/mcclient/input.go +++ b/pkg/mcclient/input.go @@ -83,6 +83,7 @@ type SAuthenticationIdentity struct { // | saml | 作为SAML 2.0 SP通过IDP认证 | // | oidc | 作为OpenID Connect/OAuth2 Client认证 | // | oauth2 | OAuth2认证 | + // | verify | 手机短信或邮箱认证 | // Methods []string `json:"methods,omitempty"` // 当认证方式为password时,通过该字段提供密码认证信息 @@ -127,7 +128,12 @@ type SAuthenticationIdentity struct { } `json:"oidc_auth,omitempty"` OAuth2 struct { Code string `json:"code,omitempty"` - } + } `json:"oauth2,omitempty"` + Verify struct { + Uid string `json:"uid,omitempty"` + VerifyCode string `json:"verify_code,omitempty"` + ContactType string `json:"contact_type,omitempty"` + } `json:"mobile,omitempty"` } type SAuthenticationInputV3 struct { diff --git a/pkg/mcclient/verify.go b/pkg/mcclient/verify.go new file mode 100644 index 0000000000..d22c911bb0 --- /dev/null +++ b/pkg/mcclient/verify.go @@ -0,0 +1,51 @@ +// 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 ( + api "yunion.io/x/onecloud/pkg/apis/identity" + "yunion.io/x/onecloud/pkg/httperrors" +) + +func (this *Client) AuthenticateVerify(uid, contactType, verifyCode string, projectId, projectName, projectDomain string, cliIp string) (TokenCredential, error) { + aCtx := SAuthContext{ + // OpenID Connect auth must comes from Web + Source: AuthSourceWeb, + Ip: cliIp, + } + return this.authenticateVerifyWithContext(uid, contactType, verifyCode, projectId, projectName, projectDomain, aCtx) +} + +func (this *Client) authenticateVerifyWithContext(uid, contactType, verifyCode string, projectId, projectName, projectDomain string, aCtx SAuthContext) (TokenCredential, error) { + if this.AuthVersion() != "v3" { + return nil, httperrors.ErrNotSupported + } + input := SAuthenticationInputV3{} + input.Auth.Identity.Methods = []string{api.AUTH_METHOD_VERIFY} + input.Auth.Identity.Verify.Uid = uid + input.Auth.Identity.Verify.ContactType = contactType + input.Auth.Identity.Verify.VerifyCode = verifyCode + 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 + } + } + input.Auth.Context = aCtx + return this._authV3Input(input) +} diff --git a/pkg/notify/models/receiver.go b/pkg/notify/models/receiver.go index af381d404c..009a351f20 100644 --- a/pkg/notify/models/receiver.go +++ b/pkg/notify/models/receiver.go @@ -845,13 +845,15 @@ func (r *SReceiver) CustomizeCreate(ctx context.Context, userCred mcclient.Token } // 需求:管理后台新建的联系人,手机号和邮箱无需进行校验 // 方案:检查请求者对于创建联系人 是否具有system scope - allowScope, _ := policy.PolicyManager.AllowScope(userCred, api.SERVICE_TYPE, ReceiverManager.KeywordPlural(), policy.PolicyActionCreate) - if allowScope == rbacutils.ScopeSystem { - if r.EnabledEmail.Bool() { - r.VerifiedEmail = tristate.True - } - if r.EnabledMobile.Bool() { - r.VerifiedMobile = tristate.True + if input.ForceVerified { + allowScope, _ := policy.PolicyManager.AllowScope(userCred, api.SERVICE_TYPE, ReceiverManager.KeywordPlural(), policy.PolicyActionCreate) + if allowScope == rbacutils.ScopeSystem { + if r.EnabledEmail.Bool() { + r.VerifiedEmail = tristate.True + } + if r.EnabledMobile.Bool() { + r.VerifiedMobile = tristate.True + } } } return nil @@ -914,21 +916,23 @@ func (r *SReceiver) PreUpdate(ctx context.Context, userCred mcclient.TokenCreden log.Errorf("PushCache: %v", err) } // 管理后台修改联系人,如果修改或者启用手机号和邮箱,无需进行校验 - allowScope, _ := policy.PolicyManager.AllowScope(userCred, api.SERVICE_TYPE, ReceiverManager.KeywordPlural(), policy.PolicyActionCreate) - if allowScope == rbacutils.ScopeSystem { - // 修改并启用 - if len(input.Email) != 0 && input.Email != r.Email && r.EnabledEmail.Bool() { - r.VerifiedEmail = tristate.True - } - if len(mobile) != 0 && mobile != r.Mobile && r.EnabledMobile.Bool() { - r.VerifiedMobile = tristate.True - } - // 从禁用变启用 - if !originEmailEnable.Bool() && r.EnabledEmail.Bool() { - r.VerifiedEmail = tristate.True - } - if !originMobileEnable.Bool() && r.EnabledMobile.Bool() { - r.VerifiedMobile = tristate.True + if input.ForceVerified { + allowScope, _ := policy.PolicyManager.AllowScope(userCred, api.SERVICE_TYPE, ReceiverManager.KeywordPlural(), policy.PolicyActionCreate) + if allowScope == rbacutils.ScopeSystem { + // 修改并启用 + if len(input.Email) != 0 && input.Email != r.Email && r.EnabledEmail.Bool() { + r.VerifiedEmail = tristate.True + } + if len(mobile) != 0 && mobile != r.Mobile && r.EnabledMobile.Bool() { + r.VerifiedMobile = tristate.True + } + // 从禁用变启用 + if !originEmailEnable.Bool() && r.EnabledEmail.Bool() { + r.VerifiedEmail = tristate.True + } + if !originMobileEnable.Bool() && r.EnabledMobile.Bool() { + r.VerifiedMobile = tristate.True + } } } r.Mobile = mobile @@ -947,7 +951,6 @@ func (r *SReceiver) PostUpdate(ctx context.Context, userCred mcclient.TokenCrede if err != nil { log.Errorf("unable to StartSubcontactPullTask: %v", err) } - } func (r *SReceiver) StartSubcontactPullTask(ctx context.Context, userCred mcclient.TokenCredential, params *jsonutils.JSONDict, parentTaskId string) error { @@ -1056,11 +1059,11 @@ func (r *SReceiver) PerformTriggerVerify(ctx context.Context, userCred mcclient. return nil, r.StartSubcontactPullTask(ctx, userCred, params, "") } _, err := VerificationManager.Create(ctx, r.Id, input.ContactType) - if err == ErrVerifyFrequently { + /*if err == ErrVerifyFrequently { return nil, httperrors.NewForbiddenError("Send verify message too frequently, please try again later") - } + }*/ if err != nil { - return nil, err + return nil, errors.Wrap(err, "VerifyManager.Create") } params := jsonutils.Marshal(input).(*jsonutils.JSONDict) diff --git a/pkg/notify/models/verify.go b/pkg/notify/models/verify.go index b171e62496..0c5e6d77d6 100644 --- a/pkg/notify/models/verify.go +++ b/pkg/notify/models/verify.go @@ -24,6 +24,7 @@ import ( "yunion.io/x/pkg/errors" "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/notify/options" ) @@ -54,7 +55,7 @@ type SVerification struct { Token string `width:"200" nullable:"false"` } -var ErrVerifyFrequently = errors.Error("Send validation messages too frequently") +var ErrVerifyFrequently = errors.Wrap(httperrors.ErrTooManyRequests, "Send validation messages too frequently") func (vm *SVerificationManager) generateVerifyToken() string { rnd := rand.New(rand.NewSource(time.Now().UnixNano())) diff --git a/pkg/notify/tasks/subcontact_pull_task.go b/pkg/notify/tasks/subcontact_pull_task.go index 8b503fe1b1..5dc4ecf2bd 100644 --- a/pkg/notify/tasks/subcontact_pull_task.go +++ b/pkg/notify/tasks/subcontact_pull_task.go @@ -27,6 +27,8 @@ import ( apis "yunion.io/x/onecloud/pkg/apis/notify" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/mcclient/auth" + "yunion.io/x/onecloud/pkg/mcclient/modules/identity" "yunion.io/x/onecloud/pkg/notify" "yunion.io/x/onecloud/pkg/notify/models" "yunion.io/x/onecloud/pkg/util/logclient" @@ -103,6 +105,20 @@ func (self *SubcontactPullTask) OnInit(ctx context.Context, obj db.IStandaloneMo self.taskFailed(ctx, receiver, reason) return } + // sync email and mobile to keystone + s := auth.GetSession(ctx, self.UserCred, "") + mobile := receiver.Mobile + if strings.HasPrefix(mobile, "+86 ") { + mobile = strings.TrimSpace(mobile[4:]) + } + params := map[string]string{ + "email": receiver.Email, + "mobile": receiver.Mobile, + } + _, err = identity.UsersV3.Update(s, receiver.Id, jsonutils.Marshal(params)) + if err != nil { + log.Errorf("update user email and mobile fail %s", err) + } // success receiver.SetStatus(self.UserCred, apis.RECEIVER_STATUS_READY, "") logclient.AddActionLogWithContext(ctx, receiver, logclient.ACT_PULL_SUBCONTACT, "", self.UserCred, true)