Skip to content

Commit 330bd12

Browse files
authored
support login totp mfa (#4538)
Signed-off-by: Patrick Zhao <zhaoyu@koderover.com>
1 parent e7d6d8f commit 330bd12

30 files changed

Lines changed: 2062 additions & 122 deletions

File tree

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ require (
7777
github.com/patrickmn/go-cache v2.1.0+incompatible
7878
github.com/pingcap/tidb/parser v0.0.0-20230922051344-241e8464cde0
7979
github.com/pkg/errors v0.9.1
80+
github.com/pquerna/otp v1.5.0
8081
github.com/prometheus/client_golang v1.22.0
8182
github.com/redis/go-redis/v9 v9.7.3
8283
github.com/rfyiamcool/cronlib v1.2.1
@@ -156,6 +157,7 @@ require (
156157
github.com/andybalholm/cascadia v1.3.2 // indirect
157158
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
158159
github.com/beorn7/perks v1.0.1 // indirect
160+
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
159161
github.com/bytedance/sonic v1.9.1 // indirect
160162
github.com/cespare/xxhash/v2 v2.3.0 // indirect
161163
github.com/chai2010/gettext-go v1.0.2 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
203203
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
204204
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
205205
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
206+
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
207+
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
206208
github.com/bradleyfalzon/ghinstallation v1.1.1 h1:pmBXkxgM1WeF8QYvDLT5kuQiHMcmf+X015GI0KM/E3I=
207209
github.com/bradleyfalzon/ghinstallation v1.1.1/go.mod h1:vyCmHTciHx/uuyN82Zc3rXN3X2KTK8nUTCrTMwAhcug=
208210
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
@@ -913,6 +915,8 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF
913915
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
914916
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
915917
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
918+
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
919+
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
916920
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
917921
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
918922
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=

pkg/microservice/aslan/core/common/repository/models/settings.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type CustomTheme struct {
6666

6767
type SecuritySettings struct {
6868
TokenExpirationTime int64 `json:"token_expiration_time" bson:"token_expiration_time"`
69+
MFAEnabled bool `json:"mfa_enabled" bson:"mfa_enabled"`
6970
}
7071

7172
type PrivacySettings struct {

pkg/microservice/aslan/core/common/repository/mongodb/settings.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,11 @@ func (c *SystemSettingColl) UpdateConcurrencySetting(workflowConcurrency, buildC
8080
return err
8181
}
8282

83-
func (c *SystemSettingColl) UpdateSecuritySetting(tokenExpirationTime int64) error {
83+
func (c *SystemSettingColl) UpdateSecuritySetting(tokenExpirationTime int64, mfaEnabled bool) error {
8484
id, _ := primitive.ObjectIDFromHex(setting.LocalClusterID)
8585
change := bson.M{"$set": bson.M{
8686
"security.token_expiration_time": tokenExpirationTime,
87+
"security.mfa_enabled": mfaEnabled,
8788
}}
8889
query := bson.M{"_id": id}
8990
_, err := c.UpdateOne(context.TODO(), query, change)
@@ -138,7 +139,7 @@ func (c *SystemSettingColl) InitSystemSettings() error {
138139
},
139140
},
140141
Privacy: &models.PrivacySettings{ImprovementPlan: true},
141-
Security: &models.SecuritySettings{TokenExpirationTime: 24},
142+
Security: &models.SecuritySettings{TokenExpirationTime: 24, MFAEnabled: false},
142143
})
143144
}
144145
return nil

pkg/microservice/aslan/core/system/handler/security.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,16 @@ import (
2626

2727
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/system/service"
2828
internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
29-
"github.com/koderover/zadig/v2/pkg/tool/log"
3029
)
3130

31+
// @Summary 更新安全与隐私设置
32+
// @Description 更新系统安全与隐私设置,包括 token 过期时间、MFA 开关和改进计划开关
33+
// @Tags system
34+
// @Accept json
35+
// @Produce json
36+
// @Param body body service.SecurityAndPrivacySettings true "body"
37+
// @Success 200
38+
// @Router /api/aslan/system/security [post]
3239
func CreateOrUpdateSecuritySettings(c *gin.Context) {
3340
ctx, err := internalhandler.NewContextWithAuthorization(c)
3441
defer func() { internalhandler.JSONResponse(c, ctx) }()
@@ -43,14 +50,16 @@ func CreateOrUpdateSecuritySettings(c *gin.Context) {
4350

4451
data, err := c.GetRawData()
4552
if err != nil {
46-
log.Errorf("upsert security settings GetRawData err : %s", err)
53+
ctx.RespErr = fmt.Errorf("upsert security settings get raw data err: %w", err)
54+
return
4755
}
4856
if err = json.Unmarshal(data, args); err != nil {
49-
log.Errorf("upsert security settings Unmarshal err : %s", err)
57+
ctx.RespErr = fmt.Errorf("upsert security settings unmarshal err: %w", err)
58+
return
5059
}
5160

52-
detail := fmt.Sprintf("token expiration: %d \n improvement plan: %v", args.TokenExpirationTime, args.ImprovementPlan)
53-
detailEn := fmt.Sprintf("Token Expiration: %d \n Improvement Plan: %v", args.TokenExpirationTime, args.ImprovementPlan)
61+
detail := fmt.Sprintf("token expiration: %d \n mfa enabled: %v \n improvement plan: %v", args.TokenExpirationTime, args.MFAEnabled, args.ImprovementPlan)
62+
detailEn := fmt.Sprintf("Token Expiration: %d \n MFA Enabled: %v \n Improvement Plan: %v", args.TokenExpirationTime, args.MFAEnabled, args.ImprovementPlan)
5463
internalhandler.InsertOperationLog(c, ctx.UserName, "", "更新", "安全与隐私", detail, detailEn, string(data), types.RequestBodyTypeJSON, ctx.Logger)
5564

5665
// authorization checks
@@ -59,11 +68,6 @@ func CreateOrUpdateSecuritySettings(c *gin.Context) {
5968
return
6069
}
6170

62-
if err != nil {
63-
ctx.RespErr = fmt.Errorf("failed to update sonar integration: %s", err)
64-
return
65-
}
66-
6771
if args.TokenExpirationTime > 8640 {
6872
ctx.RespErr = errors.New("token expiration time cannot be greater than 8640 hour")
6973
return

pkg/microservice/aslan/core/system/service/security.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,22 @@ limitations under the License.
1717
package service
1818

1919
import (
20+
"encoding/json"
21+
"time"
22+
23+
configbase "github.com/koderover/zadig/v2/pkg/config"
24+
"github.com/koderover/zadig/v2/pkg/setting"
25+
aslanclient "github.com/koderover/zadig/v2/pkg/shared/client/aslan"
26+
"github.com/koderover/zadig/v2/pkg/tool/cache"
2027
"go.uber.org/zap"
2128

2229
commonrepo "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb"
2330
)
2431

32+
const securitySettingsCacheTTL = 30 * time.Second
33+
2534
func CreateOrUpdateSecuritySettings(args *SecurityAndPrivacySettings, logger *zap.SugaredLogger) error {
26-
err := commonrepo.NewSystemSettingColl().UpdateSecuritySetting(args.TokenExpirationTime)
35+
err := commonrepo.NewSystemSettingColl().UpdateSecuritySetting(args.TokenExpirationTime, args.MFAEnabled)
2736
if err != nil {
2837
logger.Errorf("failed to update security settings, error: %s", err)
2938
return err
@@ -34,6 +43,10 @@ func CreateOrUpdateSecuritySettings(args *SecurityAndPrivacySettings, logger *za
3443
logger.Errorf("failed to update privacy settings, error: %s", err)
3544
}
3645

46+
if cacheErr := syncSystemSecuritySettingsCache(logger); cacheErr != nil {
47+
logger.Warnf("failed to sync security settings cache: %v", cacheErr)
48+
}
49+
3750
return err
3851
}
3952

@@ -44,8 +57,10 @@ func GetSecuritySettings(logger *zap.SugaredLogger) (*SecurityAndPrivacySettings
4457
return nil, err
4558
}
4659
var tokenExpirationTime int64 = 24
60+
var mfaEnabled bool
4761
if systemSetting.Security != nil {
4862
tokenExpirationTime = systemSetting.Security.TokenExpirationTime
63+
mfaEnabled = systemSetting.Security.MFAEnabled
4964
}
5065

5166
var improvementPlan bool = true
@@ -54,6 +69,29 @@ func GetSecuritySettings(logger *zap.SugaredLogger) (*SecurityAndPrivacySettings
5469
}
5570
return &SecurityAndPrivacySettings{
5671
TokenExpirationTime: tokenExpirationTime,
72+
MFAEnabled: mfaEnabled,
5773
ImprovementPlan: improvementPlan,
5874
}, nil
5975
}
76+
77+
func syncSystemSecuritySettingsCache(logger *zap.SugaredLogger) error {
78+
settings, err := GetSecuritySettings(logger)
79+
if err != nil {
80+
return err
81+
}
82+
83+
payload, err := json.Marshal(&aslanclient.SystemSetting{
84+
TokenExpirationTime: settings.TokenExpirationTime,
85+
MFAEnabled: settings.MFAEnabled,
86+
ImprovementPlan: settings.ImprovementPlan,
87+
})
88+
if err != nil {
89+
return err
90+
}
91+
92+
return cache.NewRedisCache(configbase.RedisCommonCacheTokenDB()).Write(
93+
setting.SystemSecuritySettingsCacheKey,
94+
string(payload),
95+
securitySettingsCacheTTL,
96+
)
97+
}

pkg/microservice/aslan/core/system/service/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ type OpenAPICluster struct {
250250

251251
type SecurityAndPrivacySettings struct {
252252
TokenExpirationTime int64 `json:"token_expiration_time"`
253+
MFAEnabled bool `json:"mfa_enabled"`
253254
ImprovementPlan bool `json:"improvement_plan"`
254255
}
255256

pkg/microservice/user/core/handler/login/local.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ import (
2323
internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
2424
)
2525

26+
// @Summary 本地登录
27+
// @Description 本地账号密码登录,若命中 MFA 策略则返回 mfa_required、required_action 和 mfa_challenge_token,由前端继续完成 MFA 流程
28+
// @Tags user
29+
// @Accept json
30+
// @Produce json
31+
// @Param body body login.LoginArgs true "body"
32+
// @Success 200 {object} login.User
33+
// @Router /api/v1/login [post]
2634
func LocalLogin(c *gin.Context) {
2735
ctx := internalhandler.NewContext(c)
2836
defer func() { internalhandler.JSONResponse(c, ctx) }()
@@ -45,6 +53,12 @@ type getCaptchaResp struct {
4553
Content string `json:"content"`
4654
}
4755

56+
// @Summary 获取登录验证码
57+
// @Description 当登录失败次数达到阈值后,前端可调用该接口获取验证码
58+
// @Tags user
59+
// @Produce json
60+
// @Success 200 {object} getCaptchaResp
61+
// @Router /api/v1/captcha [get]
4862
func GetCaptcha(c *gin.Context) {
4963
ctx := internalhandler.NewContext(c)
5064
defer func() { internalhandler.JSONResponse(c, ctx) }()
@@ -65,6 +79,12 @@ type LocalLogoutResp struct {
6579
RedirectURL string `json:"redirect_url"`
6680
}
6781

82+
// @Summary 退出登录
83+
// @Description 清理当前用户登录态;若为第三方登录,可返回额外登出跳转地址
84+
// @Tags user
85+
// @Produce json
86+
// @Success 200 {object} LocalLogoutResp
87+
// @Router /api/v1/logout [get]
6888
func LocalLogout(c *gin.Context) {
6989
ctx := internalhandler.NewContext(c)
7090
defer func() { internalhandler.JSONResponse(c, ctx) }()
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
Copyright 2021 The KodeRover Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package login
18+
19+
import (
20+
"github.com/gin-gonic/gin"
21+
22+
loginsvc "github.com/koderover/zadig/v2/pkg/microservice/user/core/service/login"
23+
internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
24+
)
25+
26+
// @Summary 初始化登录态 MFA 配置
27+
// @Description 基于 mfa_challenge_token 生成当前登录挑战所需的 MFA 配置数据,返回 secret、二维码和 required_action
28+
// @Tags user
29+
// @Accept json
30+
// @Produce json
31+
// @Param body body loginsvc.MFASetupArgs true "body"
32+
// @Success 200 {object} loginsvc.MFASetupResp
33+
// @Router /api/v1/login/mfa/setup [post]
34+
func MFASetup(c *gin.Context) {
35+
ctx := internalhandler.NewContext(c)
36+
defer func() { internalhandler.JSONResponse(c, ctx) }()
37+
38+
args := &loginsvc.MFASetupArgs{}
39+
if err := c.ShouldBindJSON(args); err != nil {
40+
ctx.RespErr = err
41+
return
42+
}
43+
ctx.Resp, ctx.RespErr = loginsvc.SetupMFA(args, ctx.Logger)
44+
}
45+
46+
// @Summary 完成登录态 MFA 绑定
47+
// @Description 在登录挑战阶段提交 OTP 完成首次 MFA 绑定,成功后返回正式登录态和恢复码
48+
// @Tags user
49+
// @Accept json
50+
// @Produce json
51+
// @Param body body loginsvc.MFAEnrollArgs true "body"
52+
// @Success 200 {object} loginsvc.User
53+
// @Router /api/v1/login/mfa/enroll [post]
54+
func MFAEnroll(c *gin.Context) {
55+
ctx := internalhandler.NewContext(c)
56+
defer func() { internalhandler.JSONResponse(c, ctx) }()
57+
58+
args := &loginsvc.MFAEnrollArgs{}
59+
if err := c.ShouldBindJSON(args); err != nil {
60+
ctx.RespErr = err
61+
return
62+
}
63+
ctx.Resp, ctx.RespErr = loginsvc.EnrollMFA(args, ctx.Logger)
64+
}
65+
66+
// @Summary 完成登录态 MFA 验证
67+
// @Description 在登录挑战阶段提交 OTP 或恢复码完成 MFA 验证,成功后返回正式登录态
68+
// @Tags user
69+
// @Accept json
70+
// @Produce json
71+
// @Param body body loginsvc.MFAVerifyArgs true "body"
72+
// @Success 200 {object} loginsvc.User
73+
// @Router /api/v1/login/mfa/verify [post]
74+
func MFAVerify(c *gin.Context) {
75+
ctx := internalhandler.NewContext(c)
76+
defer func() { internalhandler.JSONResponse(c, ctx) }()
77+
78+
args := &loginsvc.MFAVerifyArgs{}
79+
if err := c.ShouldBindJSON(args); err != nil {
80+
ctx.RespErr = err
81+
return
82+
}
83+
ctx.Resp, ctx.RespErr = loginsvc.VerifyMFA(args, ctx.Logger)
84+
}

0 commit comments

Comments
 (0)