Skip to content

Commit d8232f9

Browse files
authored
Merge pull request #665 from actiontech/dms/feat-961
feat: add DMS IP/CIDR access restriction
2 parents dd90326 + 322cec6 commit d8232f9

13 files changed

Lines changed: 1124 additions & 0 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package v1
2+
3+
import base "github.com/actiontech/dms/pkg/dms-common/api/base/v1"
4+
5+
type AccessWhitelistRuleItem struct {
6+
UID string `json:"uid"`
7+
Source string `json:"source"`
8+
PolicyType string `json:"policy_type"`
9+
Remark string `json:"remark"`
10+
UpdatedAt string `json:"updated_at"`
11+
}
12+
13+
type AccessRestrictionConfig struct {
14+
Enabled bool `json:"enabled"`
15+
Rules []AccessWhitelistRuleItem `json:"rules"`
16+
}
17+
18+
// swagger:model GetAccessRestrictionReply
19+
type GetAccessRestrictionReply struct {
20+
Data AccessRestrictionConfig `json:"data"`
21+
base.GenericResp
22+
}
23+
24+
// swagger:model
25+
type UpdateAccessRestrictionReq struct {
26+
Enabled *bool `json:"enabled" validate:"required"`
27+
}
28+
29+
// swagger:model
30+
type CreateAccessWhitelistRuleReq struct {
31+
Source string `json:"source" validate:"required"`
32+
Remark string `json:"remark"`
33+
PolicyType string `json:"policy_type"`
34+
}
35+
36+
// swagger:model CreateAccessWhitelistRuleReply
37+
type CreateAccessWhitelistRuleReply struct {
38+
Data AccessWhitelistRuleItem `json:"data"`
39+
base.GenericResp
40+
}
41+
42+
// swagger:parameters UpdateAccessWhitelistRuleReq
43+
type UpdateAccessWhitelistRuleReq struct {
44+
// in:path
45+
RuleUID string `param:"rule_uid" json:"rule_uid" validate:"required"`
46+
Source string `json:"source" validate:"required"`
47+
Remark string `json:"remark"`
48+
PolicyType string `json:"policy_type"`
49+
}
50+
51+
// swagger:model UpdateAccessWhitelistRuleReply
52+
type UpdateAccessWhitelistRuleReply struct {
53+
Data AccessWhitelistRuleItem `json:"data"`
54+
base.GenericResp
55+
}
56+
57+
// swagger:parameters DeleteAccessWhitelistRuleReq
58+
type DeleteAccessWhitelistRuleReq struct {
59+
// in:path
60+
RuleUID string `param:"rule_uid" json:"rule_uid" validate:"required"`
61+
}
62+
63+
// swagger:model GetAccessRestrictionClientIPReply
64+
type GetAccessRestrictionClientIPReply struct {
65+
Data AccessRestrictionClientIP `json:"data"`
66+
base.GenericResp
67+
}
68+
69+
type AccessRestrictionClientIP struct {
70+
ClientIP string `json:"client_ip"`
71+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package middleware
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/actiontech/dms/internal/dms/biz"
9+
dmsV1 "github.com/actiontech/dms/pkg/dms-common/api/dms/v1"
10+
"github.com/labstack/echo/v4"
11+
)
12+
13+
const accessRestrictionDenyMsg = "禁止访问:来源 IP 不在访问白名单"
14+
15+
// AccessRestriction enforces IP/CIDR access restriction for all DMS entry points.
16+
// Order (must not reorder short-circuits):
17+
// 1. never-block register channel POST /v1/dms/proxys
18+
// 2. switch off → allow
19+
// 3. switch on → registered ProxyTarget host IP → allow
20+
// 4. whitelist hit → allow
21+
// 5. else HTTP 403 (not 401)
22+
//
23+
// Loopback is NOT auto-allowed. CloudBeaver paths are not exempted.
24+
func AccessRestriction(u *biz.AccessRestrictionUsecase, proxy *biz.DmsProxyUsecase) echo.MiddlewareFunc {
25+
return func(next echo.HandlerFunc) echo.HandlerFunc {
26+
return func(c echo.Context) error {
27+
if isNeverBlockRegisterPath(c) {
28+
return next(c)
29+
}
30+
if u == nil {
31+
return next(c)
32+
}
33+
enabled, err := u.IsEnabled(c.Request().Context())
34+
if err != nil || !enabled {
35+
// Fail-open on read error; AC-003: disabled → allow.
36+
return next(c)
37+
}
38+
39+
clientIP := biz.ExtractClientIP(c.Request())
40+
if proxy != nil && proxy.IsRegisteredServiceIP(clientIP) {
41+
return next(c)
42+
}
43+
matched, err := u.MatchClientIP(c.Request().Context(), clientIP)
44+
if err != nil {
45+
// Fail-open on match errors to avoid locking out operators on transient DB faults.
46+
return next(c)
47+
}
48+
if matched {
49+
return next(c)
50+
}
51+
msg := accessRestrictionDenyMsg
52+
if clientIP != "" {
53+
msg = fmt.Sprintf("%s(识别 IP:%s)", accessRestrictionDenyMsg, clientIP)
54+
}
55+
return echo.NewHTTPError(http.StatusForbidden, msg)
56+
}
57+
}
58+
}
59+
60+
func isNeverBlockRegisterPath(c echo.Context) bool {
61+
if c.Request().Method != http.MethodPost {
62+
return false
63+
}
64+
path := strings.TrimSuffix(c.Request().URL.Path, "/")
65+
// Exact register channel: /v1/dms/proxys (ProxyRouterGroup under /v1).
66+
return path == "/v1"+dmsV1.ProxyRouterGroup
67+
}

internal/apiserver/service/dms_controller.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5026,6 +5026,183 @@ func (ctl *DMSController) UpdateSystemVariables(c echo.Context) error {
50265026
return NewOkResp(c)
50275027
}
50285028

5029+
// swagger:route GET /v1/dms/configurations/access_restriction Configuration GetAccessRestriction
5030+
//
5031+
// Get access restriction configuration.
5032+
//
5033+
// responses:
5034+
// 200: body:GetAccessRestrictionReply
5035+
// default: body:GenericResp
5036+
func (ctl *DMSController) GetAccessRestriction(c echo.Context) error {
5037+
currentUserUid, err := jwt.GetUserUidStrFromContext(c)
5038+
if err != nil {
5039+
return NewErrResp(c, err, apiError.DMSServiceErr)
5040+
}
5041+
reply, err := ctl.DMS.GetAccessRestriction(c.Request().Context(), currentUserUid)
5042+
if err != nil {
5043+
return NewErrResp(c, err, apiError.DMSServiceErr)
5044+
}
5045+
return NewOkRespWithReply(c, reply)
5046+
}
5047+
5048+
// swagger:operation PATCH /v1/dms/configurations/access_restriction Configuration UpdateAccessRestriction
5049+
//
5050+
// Update access restriction switch.
5051+
//
5052+
// ---
5053+
// parameters:
5054+
// - name: access_restriction
5055+
// in: body
5056+
// required: true
5057+
// schema:
5058+
// "$ref": "#/definitions/UpdateAccessRestrictionReq"
5059+
// responses:
5060+
// '200':
5061+
// description: GenericResp
5062+
// schema:
5063+
// "$ref": "#/definitions/GenericResp"
5064+
// default:
5065+
// description: GenericResp
5066+
// schema:
5067+
// "$ref": "#/definitions/GenericResp"
5068+
func (ctl *DMSController) UpdateAccessRestriction(c echo.Context) error {
5069+
req := new(aV1.UpdateAccessRestrictionReq)
5070+
err := bindAndValidateReq(c, req)
5071+
if err != nil {
5072+
return NewErrResp(c, err, apiError.BadRequestErr)
5073+
}
5074+
currentUserUid, err := jwt.GetUserUidStrFromContext(c)
5075+
if err != nil {
5076+
return NewErrResp(c, err, apiError.DMSServiceErr)
5077+
}
5078+
err = ctl.DMS.UpdateAccessRestriction(c.Request().Context(), currentUserUid, req, biz.ExtractClientIP(c.Request()))
5079+
if err != nil {
5080+
return NewErrResp(c, err, apiError.DMSServiceErr)
5081+
}
5082+
return NewOkResp(c)
5083+
}
5084+
5085+
// swagger:operation POST /v1/dms/configurations/access_restriction/rules Configuration CreateAccessWhitelistRule
5086+
//
5087+
// Create access whitelist rule.
5088+
//
5089+
// ---
5090+
// parameters:
5091+
// - name: rule
5092+
// in: body
5093+
// required: true
5094+
// schema:
5095+
// "$ref": "#/definitions/CreateAccessWhitelistRuleReq"
5096+
// responses:
5097+
// '200':
5098+
// description: CreateAccessWhitelistRuleReply
5099+
// schema:
5100+
// "$ref": "#/definitions/CreateAccessWhitelistRuleReply"
5101+
// default:
5102+
// description: GenericResp
5103+
// schema:
5104+
// "$ref": "#/definitions/GenericResp"
5105+
func (ctl *DMSController) CreateAccessWhitelistRule(c echo.Context) error {
5106+
req := new(aV1.CreateAccessWhitelistRuleReq)
5107+
err := bindAndValidateReq(c, req)
5108+
if err != nil {
5109+
return NewErrResp(c, err, apiError.BadRequestErr)
5110+
}
5111+
currentUserUid, err := jwt.GetUserUidStrFromContext(c)
5112+
if err != nil {
5113+
return NewErrResp(c, err, apiError.DMSServiceErr)
5114+
}
5115+
reply, err := ctl.DMS.CreateAccessWhitelistRule(c.Request().Context(), currentUserUid, req)
5116+
if err != nil {
5117+
return NewErrResp(c, err, apiError.DMSServiceErr)
5118+
}
5119+
return NewOkRespWithReply(c, reply)
5120+
}
5121+
5122+
// swagger:operation PUT /v1/dms/configurations/access_restriction/rules/{rule_uid} Configuration UpdateAccessWhitelistRule
5123+
//
5124+
// Update access whitelist rule.
5125+
//
5126+
// ---
5127+
// parameters:
5128+
// - name: rule_uid
5129+
// in: path
5130+
// required: true
5131+
// type: string
5132+
// - name: rule
5133+
// in: body
5134+
// required: true
5135+
// schema:
5136+
// "$ref": "#/definitions/UpdateAccessWhitelistRuleReq"
5137+
// responses:
5138+
// '200':
5139+
// description: UpdateAccessWhitelistRuleReply
5140+
// schema:
5141+
// "$ref": "#/definitions/UpdateAccessWhitelistRuleReply"
5142+
// default:
5143+
// description: GenericResp
5144+
// schema:
5145+
// "$ref": "#/definitions/GenericResp"
5146+
func (ctl *DMSController) UpdateAccessWhitelistRule(c echo.Context) error {
5147+
req := new(aV1.UpdateAccessWhitelistRuleReq)
5148+
err := bindAndValidateReq(c, req)
5149+
if err != nil {
5150+
return NewErrResp(c, err, apiError.BadRequestErr)
5151+
}
5152+
currentUserUid, err := jwt.GetUserUidStrFromContext(c)
5153+
if err != nil {
5154+
return NewErrResp(c, err, apiError.DMSServiceErr)
5155+
}
5156+
reply, err := ctl.DMS.UpdateAccessWhitelistRule(c.Request().Context(), currentUserUid, req)
5157+
if err != nil {
5158+
return NewErrResp(c, err, apiError.DMSServiceErr)
5159+
}
5160+
return NewOkRespWithReply(c, reply)
5161+
}
5162+
5163+
// swagger:route DELETE /v1/dms/configurations/access_restriction/rules/{rule_uid} Configuration DeleteAccessWhitelistRule
5164+
//
5165+
// Delete access whitelist rule.
5166+
//
5167+
// responses:
5168+
// 200: body:GenericResp
5169+
// default: body:GenericResp
5170+
func (ctl *DMSController) DeleteAccessWhitelistRule(c echo.Context) error {
5171+
req := new(aV1.DeleteAccessWhitelistRuleReq)
5172+
err := bindAndValidateReq(c, req)
5173+
if err != nil {
5174+
return NewErrResp(c, err, apiError.BadRequestErr)
5175+
}
5176+
currentUserUid, err := jwt.GetUserUidStrFromContext(c)
5177+
if err != nil {
5178+
return NewErrResp(c, err, apiError.DMSServiceErr)
5179+
}
5180+
err = ctl.DMS.DeleteAccessWhitelistRule(c.Request().Context(), currentUserUid, req)
5181+
if err != nil {
5182+
return NewErrResp(c, err, apiError.DMSServiceErr)
5183+
}
5184+
return NewOkResp(c)
5185+
}
5186+
5187+
// swagger:route GET /v1/dms/configurations/access_restriction/client_ip Configuration GetAccessRestrictionClientIP
5188+
//
5189+
// Get current request client IP for access restriction.
5190+
//
5191+
// responses:
5192+
// 200: body:GetAccessRestrictionClientIPReply
5193+
// default: body:GenericResp
5194+
func (ctl *DMSController) GetAccessRestrictionClientIP(c echo.Context) error {
5195+
currentUserUid, err := jwt.GetUserUidStrFromContext(c)
5196+
if err != nil {
5197+
return NewErrResp(c, err, apiError.DMSServiceErr)
5198+
}
5199+
reply, err := ctl.DMS.GetAccessRestrictionClientIP(c.Request().Context(), currentUserUid, c.Request())
5200+
if err != nil {
5201+
return NewErrResp(c, err, apiError.DMSServiceErr)
5202+
}
5203+
return NewOkRespWithReply(c, reply)
5204+
}
5205+
50295206
// swagger:operation POST /v1/dms/operation_records OperationRecord AddOperationRecord
50305207
//
50315208
// Add operation record.

internal/apiserver/service/router.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,12 @@ func (s *APIServer) initRouter() error {
218218
configurationV1.GET("/license/usage", s.DMSController.GetLicenseUsage) /* TODO AdminUserAllowed()*/
219219
configurationV1.GET("/system_variables", s.DMSController.GetSystemVariables) /* TODO AdminUserAllowed()*/
220220
configurationV1.PATCH("/system_variables", s.DMSController.UpdateSystemVariables) /* TODO AdminUserAllowed()*/
221+
configurationV1.GET("/access_restriction", s.DMSController.GetAccessRestriction)
222+
configurationV1.PATCH("/access_restriction", s.DMSController.UpdateAccessRestriction)
223+
configurationV1.POST("/access_restriction/rules", s.DMSController.CreateAccessWhitelistRule)
224+
configurationV1.PUT("/access_restriction/rules/:rule_uid", s.DMSController.UpdateAccessWhitelistRule)
225+
configurationV1.DELETE("/access_restriction/rules/:rule_uid", s.DMSController.DeleteAccessWhitelistRule)
226+
configurationV1.GET("/access_restriction/client_ip", s.DMSController.GetAccessRestrictionClientIP)
221227
// notify
222228
notificationV1 := v1.Group(dmsV1.NotificationRouterGroup)
223229
notificationV1.POST("", s.DMSController.Notify) /* TODO AdminUserAllowed()*/
@@ -384,6 +390,9 @@ func (s *APIServer) installMiddleware() error {
384390
}
385391
}(allowedMethods))
386392

393+
// Access restriction: early global gate (before JWT). Order: register never-block → off allow → registered IP → whitelist → 403.
394+
s.echo.Use(dmsMiddleware.AccessRestriction(s.DMSController.DMS.AccessRestrictionUsecase, s.DMSController.DMS.DmsProxyUsecase))
395+
387396
var skipJWTPaths = []string{
388397
dmsV1.SessionRouterGroup,
389398
"/v1/dms/sessions/refresh",

0 commit comments

Comments
 (0)