From e35b3c68005a1b672474d40013e6c3d656a0660a Mon Sep 17 00:00:00 2001 From: samwaf Date: Wed, 21 Oct 2026 16:07:57 +0800 Subject: [PATCH] feat: per-entry enable switches for IP/URL black and white lists #IJL0GB --- api/waf_allow_ip.go | 39 +++- api/waf_allow_url.go | 39 +++- api/waf_block_ip.go | 39 +++- api/waf_block_url.go | 39 +++- model/allowlist.go | 2 + model/blocklist.go | 2 + model/request/waf_allow_ip_req.go | 7 + model/request/waf_allow_url_req.go | 7 + model/request/waf_block_ip_req.go | 7 + model/request/waf_block_url_req.go | 7 + router/waf_allow_ip.go | 1 + router/waf_allow_url.go | 1 + router/waf_block_ip.go | 1 + router/waf_block_url.go | 1 + service/waf_service/cross_engine_core_test.go | 190 +++++++++++++++++- service/waf_service/waf_allow_ip.go | 23 ++- service/waf_service/waf_allow_url.go | 21 ++ service/waf_service/waf_block_ip.go | 23 ++- service/waf_service/waf_block_url.go | 21 ++ service/waf_service/waf_ip_lookup_service.go | 10 +- wafdb/migrations_core.go | 48 +++++ wafenginecore/wafworker.go | 10 +- waftask/batch/ip_allow_processor.go | 6 +- waftask/batch/ip_deny_processor.go | 6 +- wafupgradenotice/upgrade_notes.yaml | 24 +++ 25 files changed, 555 insertions(+), 19 deletions(-) diff --git a/api/waf_allow_ip.go b/api/waf_allow_ip.go index 0b972ee..1bf2fe6 100644 --- a/api/waf_allow_ip.go +++ b/api/waf_allow_ip.go @@ -37,6 +37,10 @@ func (w *WafAllowIpApi) AddApi(c *gin.Context) { return } req.IpType, req.Ip, req.GroupCode = ipType, ip, groupCode + if req.IsEnable != nil && *req.IsEnable != 0 && *req.IsEnable != 1 { + response.FailWithMessage("is_enable 取值只能是 0 或 1", c) + return + } err = wafIpAllowService.CheckIsExistApi(req) if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { @@ -156,6 +160,10 @@ func (w *WafAllowIpApi) ModifyAllowIpApi(c *gin.Context) { return } req.IpType, req.Ip, req.GroupCode = ipType, ip, groupCode + if req.IsEnable != nil && *req.IsEnable != 0 && *req.IsEnable != 1 { + response.FailWithMessage("is_enable 取值只能是 0 或 1", c) + return + } //编辑前先取旧记录,拿到可能被本次编辑改掉的旧 host_code(issue #898) bean := wafIpAllowService.GetDetailByIdApi(req.Id) @@ -172,13 +180,42 @@ func (w *WafAllowIpApi) ModifyAllowIpApi(c *gin.Context) { } } +// ToggleApi 切换IP白名单条目启用状态 +// @Summary 切换IP白名单条目启用状态 +// @Description 启用或停用单条IP白名单记录,停用的条目不再对引擎生效 +// @Tags 网站防护-IP白名单 +// @Accept json +// @Produce json +// @Param data body request.WafAllowIpToggleReq true "条目ID与目标状态" +// @Success 200 {object} response.Response "操作成功" +// @Security ApiKeyAuth +// @Router /wafhost/ipwhite/toggle [post] +func (w *WafAllowIpApi) ToggleApi(c *gin.Context) { + var req request.WafAllowIpToggleReq + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage("解析失败", c) + return + } + if *req.IsEnable != 0 && *req.IsEnable != 1 { + response.FailWithMessage("is_enable 取值只能是 0 或 1", c) + return + } + hostCode, err := wafIpAllowService.ToggleApi(req) + if err != nil { + response.FailWithMessage("操作失败", c) + return + } + w.NotifyWaf(hostCode) + response.OkWithMessage("操作成功", c) +} + /* * 通知到waf引擎实时生效 */ func (w *WafAllowIpApi) NotifyWaf(host_code string) { var ipWhites []model.IPAllowList - global.GWAF_LOCAL_DB.Where("host_code = ? ", host_code).Find(&ipWhites) + global.GWAF_LOCAL_DB.Where("host_code = ? and is_enable = 1", host_code).Find(&ipWhites) var chanInfo = spec.ChanCommonHost{ HostCode: host_code, Type: enums.ChanTypeAllowIP, diff --git a/api/waf_allow_url.go b/api/waf_allow_url.go index 2a7c24d..aa7b6f0 100644 --- a/api/waf_allow_url.go +++ b/api/waf_allow_url.go @@ -30,6 +30,10 @@ func (w *WafAllowUrlApi) AddApi(c *gin.Context) { var req request.WafAllowUrlAddReq err := c.ShouldBindJSON(&req) if err == nil { + if req.IsEnable != nil && *req.IsEnable != 0 && *req.IsEnable != 1 { + response.FailWithMessage("is_enable 取值只能是 0 或 1", c) + return + } err = wafUrlAllowService.CheckIsExistApi(req) if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { err = wafUrlAllowService.AddApi(req) @@ -142,6 +146,10 @@ func (w *WafAllowUrlApi) ModifyAllowUrlApi(c *gin.Context) { var req request.WafAllowUrlEditReq err := c.ShouldBindJSON(&req) if err == nil { + if req.IsEnable != nil && *req.IsEnable != 0 && *req.IsEnable != 1 { + response.FailWithMessage("is_enable 取值只能是 0 或 1", c) + return + } //编辑前先取旧记录,拿到可能被本次编辑改掉的旧 host_code(issue #898) bean := wafUrlAllowService.GetDetailByIdApi(req.Id) err = wafUrlAllowService.ModifyApi(req) @@ -157,13 +165,42 @@ func (w *WafAllowUrlApi) ModifyAllowUrlApi(c *gin.Context) { } } +// ToggleApi 切换URL白名单条目启用状态 +// @Summary 切换URL白名单条目启用状态 +// @Description 启用或停用单条URL白名单记录,停用的条目不再对引擎生效 +// @Tags 网站防护-URL白名单 +// @Accept json +// @Produce json +// @Param data body request.WafAllowUrlToggleReq true "条目ID与目标状态" +// @Success 200 {object} response.Response "操作成功" +// @Security ApiKeyAuth +// @Router /wafhost/urlwhite/toggle [post] +func (w *WafAllowUrlApi) ToggleApi(c *gin.Context) { + var req request.WafAllowUrlToggleReq + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage("解析失败", c) + return + } + if *req.IsEnable != 0 && *req.IsEnable != 1 { + response.FailWithMessage("is_enable 取值只能是 0 或 1", c) + return + } + hostCode, err := wafUrlAllowService.ToggleApi(req) + if err != nil { + response.FailWithMessage("操作失败", c) + return + } + w.NotifyWaf(hostCode) + response.OkWithMessage("操作成功", c) +} + /* * 通知到waf引擎实时生效 */ func (w *WafAllowUrlApi) NotifyWaf(host_code string) { var urlWhites []model.URLAllowList - global.GWAF_LOCAL_DB.Where("host_code = ? ", host_code).Find(&urlWhites) + global.GWAF_LOCAL_DB.Where("host_code = ? and is_enable = 1", host_code).Find(&urlWhites) var chanInfo = spec.ChanCommonHost{ HostCode: host_code, Type: enums.ChanTypeAllowURL, diff --git a/api/waf_block_ip.go b/api/waf_block_ip.go index e3f9874..55b6752 100644 --- a/api/waf_block_ip.go +++ b/api/waf_block_ip.go @@ -41,6 +41,10 @@ func (w *WafBlockIpApi) AddApi(c *gin.Context) { return } req.IpType, req.Ip, req.GroupCode = ipType, ip, groupCode + if req.IsEnable != nil && *req.IsEnable != 0 && *req.IsEnable != 1 { + response.FailWithMessage("is_enable 取值只能是 0 或 1", c) + return + } // 封禁层级:""/waf=WAF应用层(默认) | system=系统防火墙 | both=两者 layer := req.TargetLayer @@ -196,6 +200,10 @@ func (w *WafBlockIpApi) ModifyBlockIpApi(c *gin.Context) { return } req.IpType, req.Ip, req.GroupCode = ipType, ip, groupCode + if req.IsEnable != nil && *req.IsEnable != 0 && *req.IsEnable != 1 { + response.FailWithMessage("is_enable 取值只能是 0 或 1", c) + return + } //编辑前先取旧记录,拿到可能被本次编辑改掉的旧 host_code(issue #898) bean := wafIpBlockService.GetDetailByIdApi(req.Id) @@ -271,13 +279,42 @@ func (w *WafBlockIpApi) DelAllBlockIpApi(c *gin.Context) { } } +// ToggleApi 切换IP黑名单条目启用状态 +// @Summary 切换IP黑名单条目启用状态 +// @Description 启用或停用单条IP黑名单记录,停用的条目不再对引擎生效 +// @Tags 网站防护-IP黑名单 +// @Accept json +// @Produce json +// @Param data body request.WafBlockIpToggleReq true "条目ID与目标状态" +// @Success 200 {object} response.Response "操作成功" +// @Security ApiKeyAuth +// @Router /wafhost/ipblock/toggle [post] +func (w *WafBlockIpApi) ToggleApi(c *gin.Context) { + var req request.WafBlockIpToggleReq + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage("解析失败", c) + return + } + if *req.IsEnable != 0 && *req.IsEnable != 1 { + response.FailWithMessage("is_enable 取值只能是 0 或 1", c) + return + } + hostCode, err := wafIpBlockService.ToggleApi(req) + if err != nil { + response.FailWithMessage("操作失败", c) + return + } + w.NotifyWaf(hostCode) + response.OkWithMessage("操作成功", c) +} + /* * 通知到waf引擎实时生效 */ func (w *WafBlockIpApi) NotifyWaf(host_code string) { var ipWhites []model.IPBlockList - global.GWAF_LOCAL_DB.Where("host_code = ? ", host_code).Find(&ipWhites) + global.GWAF_LOCAL_DB.Where("host_code = ? and is_enable = 1", host_code).Find(&ipWhites) var chanInfo = spec.ChanCommonHost{ HostCode: host_code, Type: enums.ChanTypeBlockIP, diff --git a/api/waf_block_url.go b/api/waf_block_url.go index 79f3bb2..ab45fe1 100644 --- a/api/waf_block_url.go +++ b/api/waf_block_url.go @@ -30,6 +30,10 @@ func (w *WafBlockUrlApi) AddApi(c *gin.Context) { var req request.WafBlockUrlAddReq err := c.ShouldBindJSON(&req) if err == nil { + if req.IsEnable != nil && *req.IsEnable != 0 && *req.IsEnable != 1 { + response.FailWithMessage("is_enable 取值只能是 0 或 1", c) + return + } err = wafUrlBlockService.CheckIsExistApi(req) if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { err = wafUrlBlockService.AddApi(req) @@ -142,6 +146,10 @@ func (w *WafBlockUrlApi) ModifyBlockUrlApi(c *gin.Context) { var req request.WafBlockUrlEditReq err := c.ShouldBindJSON(&req) if err == nil { + if req.IsEnable != nil && *req.IsEnable != 0 && *req.IsEnable != 1 { + response.FailWithMessage("is_enable 取值只能是 0 或 1", c) + return + } //编辑前先取旧记录,拿到可能被本次编辑改掉的旧 host_code(issue #898) bean := wafUrlBlockService.GetDetailByIdApi(req.Id) err = wafUrlBlockService.ModifyApi(req) @@ -157,13 +165,42 @@ func (w *WafBlockUrlApi) ModifyBlockUrlApi(c *gin.Context) { } } +// ToggleApi 切换URL黑名单条目启用状态 +// @Summary 切换URL黑名单条目启用状态 +// @Description 启用或停用单条URL黑名单记录,停用的条目不再对引擎生效 +// @Tags 网站防护-URL黑名单 +// @Accept json +// @Produce json +// @Param data body request.WafBlockUrlToggleReq true "条目ID与目标状态" +// @Success 200 {object} response.Response "操作成功" +// @Security ApiKeyAuth +// @Router /wafhost/urlblock/toggle [post] +func (w *WafBlockUrlApi) ToggleApi(c *gin.Context) { + var req request.WafBlockUrlToggleReq + if err := c.ShouldBindJSON(&req); err != nil { + response.FailWithMessage("解析失败", c) + return + } + if *req.IsEnable != 0 && *req.IsEnable != 1 { + response.FailWithMessage("is_enable 取值只能是 0 或 1", c) + return + } + hostCode, err := wafUrlBlockService.ToggleApi(req) + if err != nil { + response.FailWithMessage("操作失败", c) + return + } + w.NotifyWaf(hostCode) + response.OkWithMessage("操作成功", c) +} + /* * 通知到waf引擎实时生效 */ func (w *WafBlockUrlApi) NotifyWaf(host_code string) { var urlWhites []model.URLBlockList - global.GWAF_LOCAL_DB.Where("host_code = ? ", host_code).Find(&urlWhites) + global.GWAF_LOCAL_DB.Where("host_code = ? and is_enable = 1", host_code).Find(&urlWhites) var chanInfo = spec.ChanCommonHost{ HostCode: host_code, Type: enums.ChanTypeBlockURL, diff --git a/model/allowlist.go b/model/allowlist.go index 46c744f..7a1b242 100644 --- a/model/allowlist.go +++ b/model/allowlist.go @@ -13,6 +13,7 @@ type IPAllowList struct { // 存量行为空串,等同于 ip,判定处一律只判 == IPEntryTypeGroup。 IpType string `gorm:"size:20" json:"ip_type"` GroupCode string `gorm:"size:64" json:"group_code"` //IpType=group 时指向 ip_group.group_code + IsEnable int `gorm:"column:is_enable" json:"is_enable"` //1启用 0停用(停用条目不下发引擎) } type URLAllowList struct { baseorm.BaseOrm @@ -20,4 +21,5 @@ type URLAllowList struct { CompareType string `gorm:"size:50" json:"compare_type"` //判断类型,包含、开始、结束、完全匹配 Url string `gorm:"type:text" json:"url"` //请求地址 Remarks string `gorm:"size:500" json:"remarks"` //备注 + IsEnable int `gorm:"column:is_enable" json:"is_enable"` //1启用 0停用(停用条目不下发引擎) } diff --git a/model/blocklist.go b/model/blocklist.go index d9905ae..e4e7dd9 100644 --- a/model/blocklist.go +++ b/model/blocklist.go @@ -13,6 +13,7 @@ type IPBlockList struct { // 存量行为空串,等同于 ip,判定处一律只判 == IPEntryTypeGroup。 IpType string `gorm:"size:20" json:"ip_type"` GroupCode string `gorm:"size:64" json:"group_code"` //IpType=group 时指向 ip_group.group_code + IsEnable int `gorm:"column:is_enable" json:"is_enable"` //1启用 0停用(停用条目不下发引擎) } type URLBlockList struct { @@ -21,4 +22,5 @@ type URLBlockList struct { CompareType string `gorm:"size:50" json:"compare_type" form:"compare_type"` //对比方式 Url string `gorm:"type:text" json:"url"` //限制请求地址 Remarks string `gorm:"size:500" json:"remarks"` //备注 + IsEnable int `gorm:"column:is_enable" json:"is_enable"` //1启用 0停用(停用条目不下发引擎) } diff --git a/model/request/waf_allow_ip_req.go b/model/request/waf_allow_ip_req.go index f2b2b34..7678a30 100644 --- a/model/request/waf_allow_ip_req.go +++ b/model/request/waf_allow_ip_req.go @@ -10,6 +10,7 @@ type WafAllowIpAddReq struct { Remarks string `json:"remarks"` //备注 IpType string `json:"ip_type"` //条目类型: ""/ip(单条) | group(引用IP组) GroupCode string `json:"group_code"` //IpType=group 时的IP组短码 + IsEnable *int `json:"is_enable"` //启用状态:nil=新增默认启用/编辑不改动,0停用 1启用 } type WafAllowIpDelReq struct { Id string `json:"id" form:"id"` //白名单IP唯一键 @@ -25,6 +26,7 @@ type WafAllowIpEditReq struct { Remarks string `json:"remarks"` //备注 IpType string `json:"ip_type"` //条目类型: ""/ip(单条) | group(引用IP组) GroupCode string `json:"group_code"` //IpType=group 时的IP组短码 + IsEnable *int `json:"is_enable"` //启用状态:nil=新增默认启用/编辑不改动,0停用 1启用 } type WafAllowIpSearchReq struct { HostCode string `json:"host_code" ` //主机码 @@ -39,3 +41,8 @@ type WafAllowIpBatchDelReq struct { type WafAllowIpDelAllReq struct { HostCode string `json:"host_code" form:"host_code"` //网站唯一码,为空则删除所有 } + +type WafAllowIpToggleReq struct { + Id string `json:"id" form:"id" binding:"required"` //条目唯一键 + IsEnable *int `json:"is_enable" form:"is_enable" binding:"required"` //1启用 0停用(必填,防漏字段被当成停用) +} diff --git a/model/request/waf_allow_url_req.go b/model/request/waf_allow_url_req.go index 1d66de3..daa7c8b 100644 --- a/model/request/waf_allow_url_req.go +++ b/model/request/waf_allow_url_req.go @@ -7,6 +7,7 @@ type WafAllowUrlAddReq struct { CompareType string `json:"compare_type" form:"compare_type" binding:"required"` //对比方式 Url string `json:"url" binding:"required"` //白名单url Remarks string `json:"remarks"` //备注 + IsEnable *int `json:"is_enable"` //启用状态:nil=新增默认启用/编辑不改动,0停用 1启用 } type WafAllowUrlDelReq struct { Id string `json:"id" form:"id"` //白名单url唯一键 @@ -21,6 +22,7 @@ type WafAllowUrlEditReq struct { CompareType string `json:"compare_type" form:"compare_type" binding:"required"` //对比方式 Url string `json:"url" binding:"required"` //白名单url Remarks string `json:"remarks"` //备注 + IsEnable *int `json:"is_enable"` //启用状态:nil=新增默认启用/编辑不改动,0停用 1启用 } type WafAllowUrlSearchReq struct { @@ -36,3 +38,8 @@ type WafAllowUrlBatchDelReq struct { type WafAllowUrlDelAllReq struct { HostCode string `json:"host_code" form:"host_code"` //网站唯一码 } + +type WafAllowUrlToggleReq struct { + Id string `json:"id" form:"id" binding:"required"` //条目唯一键 + IsEnable *int `json:"is_enable" form:"is_enable" binding:"required"` //1启用 0停用(必填,防漏字段被当成停用) +} diff --git a/model/request/waf_block_ip_req.go b/model/request/waf_block_ip_req.go index 69f319d..62c9963 100644 --- a/model/request/waf_block_ip_req.go +++ b/model/request/waf_block_ip_req.go @@ -12,6 +12,7 @@ type WafBlockIpAddReq struct { TargetLayer string `json:"target_layer"` //封禁层级: ""/waf(WAF应用层) | system(系统防火墙) | both(两者) IpType string `json:"ip_type"` //条目类型: ""/ip(单条) | group(引用IP组) GroupCode string `json:"group_code"` //IpType=group 时的IP组短码 + IsEnable *int `json:"is_enable"` //启用状态:nil=新增默认启用/编辑不改动,0停用 1启用 } type WafBlockIpEditReq struct { @@ -21,6 +22,7 @@ type WafBlockIpEditReq struct { Remarks string `json:"remarks"` //备注 IpType string `json:"ip_type"` //条目类型: ""/ip(单条) | group(引用IP组) GroupCode string `json:"group_code"` //IpType=group 时的IP组短码 + IsEnable *int `json:"is_enable"` //启用状态:nil=新增默认启用/编辑不改动,0停用 1启用 } type WafBlockIpDelReq struct { Id string `json:"id" form:"id"` //Block IP唯一键 @@ -44,3 +46,8 @@ type WafBlockIpBatchDelReq struct { type WafBlockIpDelAllReq struct { HostCode string `json:"host_code" form:"host_code"` //网站唯一码,为空则删除所有 } + +type WafBlockIpToggleReq struct { + Id string `json:"id" form:"id" binding:"required"` //条目唯一键 + IsEnable *int `json:"is_enable" form:"is_enable" binding:"required"` //1启用 0停用(必填,防漏字段被当成停用) +} diff --git a/model/request/waf_block_url_req.go b/model/request/waf_block_url_req.go index 9ab5ba0..111bf5e 100644 --- a/model/request/waf_block_url_req.go +++ b/model/request/waf_block_url_req.go @@ -7,6 +7,7 @@ type WafBlockUrlAddReq struct { CompareType string `json:"compare_type" form:"compare_type" binding:"required"` //对比方式 Url string `json:"url" binding:"required"` //Block url Remarks string `json:"remarks"` //备注 + IsEnable *int `json:"is_enable"` //启用状态:nil=新增默认启用/编辑不改动,0停用 1启用 } type WafBlockUrlSearchReq struct { HostCode string `json:"host_code" ` //主机码 @@ -26,6 +27,7 @@ type WafBlockUrlEditReq struct { CompareType string `json:"compare_type" form:"compare_type" binding:"required"` //对比方式 Url string `json:"url" binding:"required"` //Block url Remarks string `json:"remarks"` //备注 + IsEnable *int `json:"is_enable"` //启用状态:nil=新增默认启用/编辑不改动,0停用 1启用 } // 批量删除请求结构体 @@ -37,3 +39,8 @@ type WafBlockUrlBatchDelReq struct { type WafBlockUrlDelAllReq struct { HostCode string `json:"host_code"` // 网站唯一码,为空则删除所有 } + +type WafBlockUrlToggleReq struct { + Id string `json:"id" form:"id" binding:"required"` //条目唯一键 + IsEnable *int `json:"is_enable" form:"is_enable" binding:"required"` //1启用 0停用(必填,防漏字段被当成停用) +} diff --git a/router/waf_allow_ip.go b/router/waf_allow_ip.go index 114a9c1..a6a4a6a 100644 --- a/router/waf_allow_ip.go +++ b/router/waf_allow_ip.go @@ -16,6 +16,7 @@ func (receiver *AllowIpRouter) InitAllowIpRouter(group *gin.RouterGroup) { allowIpRouter.POST("/api/v1/wafhost/ipwhite/add", AllowIpRouterApi.AddApi) allowIpRouter.GET("/api/v1/wafhost/ipwhite/del", AllowIpRouterApi.DelAllowIpApi) allowIpRouter.POST("/api/v1/wafhost/ipwhite/edit", AllowIpRouterApi.ModifyAllowIpApi) + allowIpRouter.POST("/api/v1/wafhost/ipwhite/toggle", AllowIpRouterApi.ToggleApi) allowIpRouter.POST("/api/v1/wafhost/ipwhite/batchdel", AllowIpRouterApi.BatchDelAllowIpApi) allowIpRouter.POST("/api/v1/wafhost/ipwhite/delall", AllowIpRouterApi.DelAllAllowIpApi) diff --git a/router/waf_allow_url.go b/router/waf_allow_url.go index e3bcecb..fa5cd54 100644 --- a/router/waf_allow_url.go +++ b/router/waf_allow_url.go @@ -16,6 +16,7 @@ func (receiver *AllowUrlRouter) InitAllowUrlRouter(group *gin.RouterGroup) { allowUrlRouter.POST("/api/v1/wafhost/urlwhite/add", AllowUrlRouterApi.AddApi) allowUrlRouter.GET("/api/v1/wafhost/urlwhite/del", AllowUrlRouterApi.DelAllowUrlApi) allowUrlRouter.POST("/api/v1/wafhost/urlwhite/edit", AllowUrlRouterApi.ModifyAllowUrlApi) + allowUrlRouter.POST("/api/v1/wafhost/urlwhite/toggle", AllowUrlRouterApi.ToggleApi) // 新增批量删除路由 allowUrlRouter.POST("/api/v1/wafhost/urlwhite/batchdel", AllowUrlRouterApi.BatchDelAllowUrlApi) // 新增全部删除路由 diff --git a/router/waf_block_ip.go b/router/waf_block_ip.go index adec6d4..f522c18 100644 --- a/router/waf_block_ip.go +++ b/router/waf_block_ip.go @@ -17,6 +17,7 @@ func (receiver *BlockIpRouter) InitBlockIpRouter(group *gin.RouterGroup) { router.GET("/api/v1/wafhost/ipblock/recommend-layer", api.GetRecommendLayerApi) // 推荐封禁层级(智能默认) router.GET("/api/v1/wafhost/ipblock/del", api.DelBlockIpApi) router.POST("/api/v1/wafhost/ipblock/edit", api.ModifyBlockIpApi) + router.POST("/api/v1/wafhost/ipblock/toggle", api.ToggleApi) router.POST("/api/v1/wafhost/ipblock/batch/del", api.BatchDelBlockIpApi) router.POST("/api/v1/wafhost/ipblock/delall", api.DelAllBlockIpApi) } diff --git a/router/waf_block_url.go b/router/waf_block_url.go index a8d1c75..fce5d36 100644 --- a/router/waf_block_url.go +++ b/router/waf_block_url.go @@ -16,6 +16,7 @@ func (receiver *BlockUrlRouter) InitBlockUrlRouter(group *gin.RouterGroup) { router.POST("/api/v1/wafhost/urlblock/add", api.AddApi) router.GET("/api/v1/wafhost/urlblock/del", api.DelBlockUrlApi) router.POST("/api/v1/wafhost/urlblock/edit", api.ModifyBlockUrlApi) + router.POST("/api/v1/wafhost/urlblock/toggle", api.ToggleApi) router.POST("/api/v1/wafhost/urlblock/batchdel", api.BatchDelBlockUrlApi) router.POST("/api/v1/wafhost/urlblock/delall", api.DelAllBlockUrlApi) } diff --git a/service/waf_service/cross_engine_core_test.go b/service/waf_service/cross_engine_core_test.go index 0c5b15d..1245e96 100644 --- a/service/waf_service/cross_engine_core_test.go +++ b/service/waf_service/cross_engine_core_test.go @@ -32,6 +32,8 @@ func runCoreCases(t *testing.T, db *gorm.DB) { // ————— 新增:各 service 完整 CRUD 回路 ————— runCoreCRUDCases(t, db) runCoreCRUDCasesB(t, db) + // 名单条目启用开关(is_enable)三库回归 + runListEnableSwitchCases(t, db) // ————— 升级须知:生成幂等 / 状态流转 / 降级不生成 ————— runUpgradeNoticeCases(t, db) } @@ -324,12 +326,12 @@ func runCoreCRUDCases(t *testing.T, db *gorm.DB) { t.Run("BatchTask", func(t *testing.T) { name := "bt_" + sfx() fatalIf(t, WafBatchServiceApp.AddApi(request.BatchTaskAddReq{ - BatchTaskName: name, BatchType: "ip_black", BatchSourceType: "local", BatchTriggerType: "manual", BatchSource: "/tmp/x", BatchExecuteMethod: "append", + BatchTaskName: name, BatchType: "ipdeny", BatchSourceType: "local", BatchTriggerType: "manual", BatchSource: "/tmp/x", BatchExecuteMethod: "append", })) var bean model.BatchTask firstBy(t, db, &bean, "batch_task_name = ?", name) fatalIf(t, WafBatchServiceApp.ModifyApi(request.BatchTaskEditReq{ - Id: bean.Id, BatchTaskName: name, BatchType: "ip_black", BatchSourceType: "local", BatchTriggerType: "manual", BatchSource: "/tmp/y", BatchExecuteMethod: "append", + Id: bean.Id, BatchTaskName: name, BatchType: "ipdeny", BatchSourceType: "local", BatchTriggerType: "manual", BatchSource: "/tmp/y", BatchExecuteMethod: "append", })) got := WafBatchServiceApp.GetDetailByIdApi(bean.Id) if got.BatchSource != "/tmp/y" { @@ -377,3 +379,187 @@ func runCoreCRUDCases(t *testing.T, db *gorm.DB) { assertGone(t, db, &model.DelayMsg{}, bean.Id) }) } + +// ========================= 名单条目启用开关(is_enable) ========================= + +// runListEnableSwitchCases 四类名单 is_enable 三库一致性: +// - service 新增未传开关字段 → 启用(产品默认) +// - service 新增显式 0 → 停用(弹窗关着开关新增;防 default 标签把 0 吞成 1 的回归) +// - toggle 0→读回 0→toggle 1→读回 1 +// - 裸结构体 Create 不带该字段 → 0(字段无 default 标签的语义锚; +// 若有人往模型上加回 default 标签,此用例会先红) +func runListEnableSwitchCases(t *testing.T, db *gorm.DB) { + type iostub struct { + addNil func() error + addZero func() error + addOne func() error + toggle func(id string, v int) error + findByHC func(hostCode string) (int, error) + } + zero, one := 0, 1 + mk := []struct { + name string + kit func() iostub + }{ + {"LSV_AllowIP", func() iostub { + return iostub{ + addNil: func() error { + return WafWhiteIpServiceApp.AddApi(request.WafAllowIpAddReq{HostCode: "hc_nil", Ip: "10.10.0.1", Remarks: "x"}) + }, + addZero: func() error { + return WafWhiteIpServiceApp.AddApi(request.WafAllowIpAddReq{HostCode: "hc_zero", Ip: "10.10.0.2", Remarks: "x", IsEnable: &zero}) + }, + addOne: func() error { + return WafWhiteIpServiceApp.AddApi(request.WafAllowIpAddReq{HostCode: "hc_one", Ip: "10.10.0.3", Remarks: "x", IsEnable: &one}) + }, + toggle: func(id string, v int) error { + val := v + _, err := WafWhiteIpServiceApp.ToggleApi(request.WafAllowIpToggleReq{Id: id, IsEnable: &val}) + return err + }, + findByHC: func(hostCode string) (int, error) { + var b model.IPAllowList + if err := db.Where("host_code = ?", hostCode).First(&b).Error; err != nil { + return -1, err + } + return b.IsEnable, nil + }, + } + }}, + {"LSV_BlockIP", func() iostub { + return iostub{ + addNil: func() error { + return WafBlockIpServiceApp.AddApi(request.WafBlockIpAddReq{HostCode: "hc_nil", Ip: "10.10.1.1", Remarks: "x"}) + }, + addZero: func() error { + return WafBlockIpServiceApp.AddApi(request.WafBlockIpAddReq{HostCode: "hc_zero", Ip: "10.10.1.2", Remarks: "x", IsEnable: &zero}) + }, + addOne: func() error { + return WafBlockIpServiceApp.AddApi(request.WafBlockIpAddReq{HostCode: "hc_one", Ip: "10.10.1.3", Remarks: "x", IsEnable: &one}) + }, + toggle: func(id string, v int) error { + val := v + _, err := WafBlockIpServiceApp.ToggleApi(request.WafBlockIpToggleReq{Id: id, IsEnable: &val}) + return err + }, + findByHC: func(hostCode string) (int, error) { + var b model.IPBlockList + if err := db.Where("host_code = ?", hostCode).First(&b).Error; err != nil { + return -1, err + } + return b.IsEnable, nil + }, + } + }}, + {"LSV_AllowURL", func() iostub { + return iostub{ + addNil: func() error { + return WafWhiteUrlServiceApp.AddApi(request.WafAllowUrlAddReq{HostCode: "hc_nil", CompareType: "等于", Url: "/lsv-nil", Remarks: "x"}) + }, + addZero: func() error { + return WafWhiteUrlServiceApp.AddApi(request.WafAllowUrlAddReq{HostCode: "hc_zero", CompareType: "等于", Url: "/lsv-zero", Remarks: "x", IsEnable: &zero}) + }, + addOne: func() error { + return WafWhiteUrlServiceApp.AddApi(request.WafAllowUrlAddReq{HostCode: "hc_one", CompareType: "等于", Url: "/lsv-one", Remarks: "x", IsEnable: &one}) + }, + toggle: func(id string, v int) error { + val := v + _, err := WafWhiteUrlServiceApp.ToggleApi(request.WafAllowUrlToggleReq{Id: id, IsEnable: &val}) + return err + }, + findByHC: func(hostCode string) (int, error) { + var b model.URLAllowList + if err := db.Where("host_code = ?", hostCode).First(&b).Error; err != nil { + return -1, err + } + return b.IsEnable, nil + }, + } + }}, + {"LSV_BlockURL", func() iostub { + return iostub{ + addNil: func() error { + return WafBlockUrlServiceApp.AddApi(request.WafBlockUrlAddReq{HostCode: "hc_nil", CompareType: "等于", Url: "/lsvb-nil", Remarks: "x"}) + }, + addZero: func() error { + return WafBlockUrlServiceApp.AddApi(request.WafBlockUrlAddReq{HostCode: "hc_zero", CompareType: "等于", Url: "/lsvb-zero", Remarks: "x", IsEnable: &zero}) + }, + addOne: func() error { + return WafBlockUrlServiceApp.AddApi(request.WafBlockUrlAddReq{HostCode: "hc_one", CompareType: "等于", Url: "/lsvb-one", Remarks: "x", IsEnable: &one}) + }, + toggle: func(id string, v int) error { + val := v + _, err := WafBlockUrlServiceApp.ToggleApi(request.WafBlockUrlToggleReq{Id: id, IsEnable: &val}) + return err + }, + findByHC: func(hostCode string) (int, error) { + var b model.URLBlockList + if err := db.Where("host_code = ?", hostCode).First(&b).Error; err != nil { + return -1, err + } + return b.IsEnable, nil + }, + } + }}, + } + for _, it := range mk { + it := it + t.Run(it.name, func(t *testing.T) { + k := it.kit() + fatalIf(t, k.addNil()) + if v, err := k.findByHC("hc_nil"); err != nil || v != 1 { + t.Fatalf("新增未传开关字段应为1(启用),实际 %d err=%v", v, err) + } + fatalIf(t, k.addZero()) + if v, err := k.findByHC("hc_zero"); err != nil || v != 0 { + t.Fatalf("新增显式0应为0(停用),实际 %d err=%v(default 标签吞 0?)", v, err) + } + fatalIf(t, k.addOne()) + if v, err := k.findByHC("hc_one"); err != nil || v != 1 { + t.Fatalf("新增显式1应为1,实际 %d err=%v", v, err) + } + // toggle 往返:0 落库、1 落库 + var b model.IPAllowList + _ = b + var id string + switch it.name { + case "LSV_AllowIP": + var r model.IPAllowList + firstBy(t, db, &r, "host_code = ?", "hc_zero") + id = r.Id + case "LSV_BlockIP": + var r model.IPBlockList + firstBy(t, db, &r, "host_code = ?", "hc_zero") + id = r.Id + case "LSV_AllowURL": + var r model.URLAllowList + firstBy(t, db, &r, "host_code = ?", "hc_zero") + id = r.Id + case "LSV_BlockURL": + var r model.URLBlockList + firstBy(t, db, &r, "host_code = ?", "hc_zero") + id = r.Id + } + fatalIf(t, k.toggle(id, 0)) + if v, err := k.findByHC("hc_zero"); err != nil || v != 0 { + t.Fatalf("toggle 0 应落库 0,实际 %d err=%v", v, err) + } + fatalIf(t, k.toggle(id, 1)) + if v, err := k.findByHC("hc_zero"); err != nil || v != 1 { + t.Fatalf("toggle 1 应落库 1,实际 %d err=%v", v, err) + } + }) + } + + // 语义锚:不带 default 标签的字段,裸结构体 Create 零值落库 0 + // (批量导入等绕过 service 的插入路径依赖显式写 1;此用例防止有人加回 default 标签) + t.Run("BareCreateNoDefault", func(t *testing.T) { + hc := "hc_bare_" + sfx() + fatalIf(t, db.Create(&model.IPAllowList{BaseOrm: newBase(uuid.GenUUID()), HostCode: hc, Ip: "10.10.2.1"}).Error) + var b model.IPAllowList + firstBy(t, db, &b, "host_code = ?", hc) + if b.IsEnable != 0 { + t.Fatalf("裸 Create 零值应落库 0(无 default 标签语义),实际 %d", b.IsEnable) + } + }) +} diff --git a/service/waf_service/waf_allow_ip.go b/service/waf_service/waf_allow_ip.go index baecc29..51309c1 100644 --- a/service/waf_service/waf_allow_ip.go +++ b/service/waf_service/waf_allow_ip.go @@ -16,6 +16,10 @@ type WafWhiteIpService struct{} var WafWhiteIpServiceApp = new(WafWhiteIpService) func (receiver *WafWhiteIpService) AddApi(wafWhiteIpAddReq request.WafAllowIpAddReq) error { + isEnable := 1 + if wafWhiteIpAddReq.IsEnable != nil { + isEnable = *wafWhiteIpAddReq.IsEnable + } var wafHost = &model.IPAllowList{ BaseOrm: baseorm.BaseOrm{ Id: uuid.GenUUID(), @@ -29,14 +33,28 @@ func (receiver *WafWhiteIpService) AddApi(wafWhiteIpAddReq request.WafAllowIpAdd Remarks: wafWhiteIpAddReq.Remarks, IpType: wafWhiteIpAddReq.IpType, GroupCode: wafWhiteIpAddReq.GroupCode, + IsEnable: isEnable, } global.GWAF_LOCAL_DB.Create(wafHost) return nil } +// ToggleApi 切换条目启用状态,返回所属网站编码供调用方通知引擎 +func (receiver *WafWhiteIpService) ToggleApi(req request.WafAllowIpToggleReq) (string, error) { + var bean model.IPAllowList + if err := global.GWAF_LOCAL_DB.Where("id = ?", req.Id).First(&bean).Error; err != nil { + return "", err + } + err := global.GWAF_LOCAL_DB.Model(model.IPAllowList{}).Where("id = ?", req.Id).Updates(map[string]interface{}{ + "is_enable": *req.IsEnable, + "UPDATE_TIME": customtype.JsonTime(time.Now()), + }).Error + return bean.HostCode, err +} + // CheckIsExistApi 判重。唯一性由 (host_code, ip_type, ip, group_code) 共同决定: // 同一站点重复引用同一个 IP 组也算重复。 -// ip_type 用 IN ('', 'ip') 兼容存量行的空串。 +// ip_type 用 IN (”, 'ip') 兼容存量行的空串。 func (receiver *WafWhiteIpService) CheckIsExistApi(wafWhiteIpAddReq request.WafAllowIpAddReq) error { if wafWhiteIpAddReq.IpType == model.IPEntryTypeGroup { return global.GWAF_LOCAL_DB.First(&model.IPAllowList{}, @@ -68,6 +86,9 @@ func (receiver *WafWhiteIpService) ModifyApi(wafWhiteIpEditReq request.WafAllowI "group_code": wafWhiteIpEditReq.GroupCode, "UPDATE_TIME": customtype.JsonTime(time.Now()), } + if wafWhiteIpEditReq.IsEnable != nil { + ipWhiteMap["is_enable"] = *wafWhiteIpEditReq.IsEnable + } err := global.GWAF_LOCAL_DB.Model(model.IPAllowList{}).Where("id = ?", wafWhiteIpEditReq.Id).Updates(ipWhiteMap).Error return err diff --git a/service/waf_service/waf_allow_url.go b/service/waf_service/waf_allow_url.go index 04e6e2c..1e5330b 100644 --- a/service/waf_service/waf_allow_url.go +++ b/service/waf_service/waf_allow_url.go @@ -16,6 +16,10 @@ type WafWhiteUrlService struct{} var WafWhiteUrlServiceApp = new(WafWhiteUrlService) func (receiver *WafWhiteUrlService) AddApi(req request.WafAllowUrlAddReq) error { + isEnable := 1 + if req.IsEnable != nil { + isEnable = *req.IsEnable + } var bean = &model.URLAllowList{ BaseOrm: baseorm.BaseOrm{ Id: uuid.GenUUID(), @@ -28,11 +32,25 @@ func (receiver *WafWhiteUrlService) AddApi(req request.WafAllowUrlAddReq) error CompareType: req.CompareType, Url: req.Url, Remarks: req.Remarks, + IsEnable: isEnable, } global.GWAF_LOCAL_DB.Create(bean) return nil } +// ToggleApi 切换条目启用状态,返回所属网站编码供调用方通知引擎 +func (receiver *WafWhiteUrlService) ToggleApi(req request.WafAllowUrlToggleReq) (string, error) { + var bean model.URLAllowList + if err := global.GWAF_LOCAL_DB.Where("id = ?", req.Id).First(&bean).Error; err != nil { + return "", err + } + err := global.GWAF_LOCAL_DB.Model(model.URLAllowList{}).Where("id = ?", req.Id).Updates(map[string]interface{}{ + "is_enable": *req.IsEnable, + "UPDATE_TIME": customtype.JsonTime(time.Now()), + }).Error + return bean.HostCode, err +} + func (receiver *WafWhiteUrlService) CheckIsExistApi(req request.WafAllowUrlAddReq) error { return global.GWAF_LOCAL_DB.First(&model.URLAllowList{}, "host_code = ? and url= ?", req.HostCode, req.Url).Error @@ -51,6 +69,9 @@ func (receiver *WafWhiteUrlService) ModifyApi(req request.WafAllowUrlEditReq) er "Remarks": req.Remarks, "UPDATE_TIME": customtype.JsonTime(time.Now()), } + if req.IsEnable != nil { + ipWhiteMap["is_enable"] = *req.IsEnable + } err := global.GWAF_LOCAL_DB.Model(model.URLAllowList{}).Where("id = ?", req.Id).Updates(ipWhiteMap).Error return err diff --git a/service/waf_service/waf_block_ip.go b/service/waf_service/waf_block_ip.go index 5f891d0..8801688 100644 --- a/service/waf_service/waf_block_ip.go +++ b/service/waf_service/waf_block_ip.go @@ -16,6 +16,10 @@ type WafBlockIpService struct{} var WafBlockIpServiceApp = new(WafBlockIpService) func (receiver *WafBlockIpService) AddApi(req request.WafBlockIpAddReq) error { + isEnable := 1 + if req.IsEnable != nil { + isEnable = *req.IsEnable + } var bean = &model.IPBlockList{ BaseOrm: baseorm.BaseOrm{ Id: uuid.GenUUID(), @@ -29,14 +33,28 @@ func (receiver *WafBlockIpService) AddApi(req request.WafBlockIpAddReq) error { Remarks: req.Remarks, IpType: req.IpType, GroupCode: req.GroupCode, + IsEnable: isEnable, } global.GWAF_LOCAL_DB.Create(bean) return nil } +// ToggleApi 切换条目启用状态,返回所属网站编码供调用方通知引擎 +func (receiver *WafBlockIpService) ToggleApi(req request.WafBlockIpToggleReq) (string, error) { + var bean model.IPBlockList + if err := global.GWAF_LOCAL_DB.Where("id = ?", req.Id).First(&bean).Error; err != nil { + return "", err + } + err := global.GWAF_LOCAL_DB.Model(model.IPBlockList{}).Where("id = ?", req.Id).Updates(map[string]interface{}{ + "is_enable": *req.IsEnable, + "UPDATE_TIME": customtype.JsonTime(time.Now()), + }).Error + return bean.HostCode, err +} + // CheckIsExistApi 判重。唯一性由 (host_code, ip_type, ip, group_code) 共同决定: // 同一站点重复引用同一个 IP 组也算重复。 -// ip_type 用 IN ('', 'ip') 兼容存量行的空串。 +// ip_type 用 IN (”, 'ip') 兼容存量行的空串。 func (receiver *WafBlockIpService) CheckIsExistApi(req request.WafBlockIpAddReq) error { if req.IpType == model.IPEntryTypeGroup { return global.GWAF_LOCAL_DB.First(&model.IPBlockList{}, @@ -67,6 +85,9 @@ func (receiver *WafBlockIpService) ModifyApi(req request.WafBlockIpEditReq) erro "group_code": req.GroupCode, "UPDATE_TIME": customtype.JsonTime(time.Now()), } + if req.IsEnable != nil { + ipWhiteMap["is_enable"] = *req.IsEnable + } err := global.GWAF_LOCAL_DB.Model(model.IPBlockList{}).Where("id = ?", req.Id).Updates(ipWhiteMap).Error return err diff --git a/service/waf_service/waf_block_url.go b/service/waf_service/waf_block_url.go index 271b59b..2ce2b5b 100644 --- a/service/waf_service/waf_block_url.go +++ b/service/waf_service/waf_block_url.go @@ -16,6 +16,10 @@ type WafBlockUrlService struct{} var WafBlockUrlServiceApp = new(WafBlockUrlService) func (receiver *WafBlockUrlService) AddApi(req request.WafBlockUrlAddReq) error { + isEnable := 1 + if req.IsEnable != nil { + isEnable = *req.IsEnable + } var bean = &model.URLBlockList{ BaseOrm: baseorm.BaseOrm{ Id: uuid.GenUUID(), @@ -28,11 +32,25 @@ func (receiver *WafBlockUrlService) AddApi(req request.WafBlockUrlAddReq) error Url: req.Url, CompareType: req.CompareType, Remarks: req.Remarks, + IsEnable: isEnable, } global.GWAF_LOCAL_DB.Create(bean) return nil } +// ToggleApi 切换条目启用状态,返回所属网站编码供调用方通知引擎 +func (receiver *WafBlockUrlService) ToggleApi(req request.WafBlockUrlToggleReq) (string, error) { + var bean model.URLBlockList + if err := global.GWAF_LOCAL_DB.Where("id = ?", req.Id).First(&bean).Error; err != nil { + return "", err + } + err := global.GWAF_LOCAL_DB.Model(model.URLBlockList{}).Where("id = ?", req.Id).Updates(map[string]interface{}{ + "is_enable": *req.IsEnable, + "UPDATE_TIME": customtype.JsonTime(time.Now()), + }).Error + return bean.HostCode, err +} + func (receiver *WafBlockUrlService) CheckIsExistApi(req request.WafBlockUrlAddReq) error { return global.GWAF_LOCAL_DB.First(&model.URLBlockList{}, "host_code = ? and url= ?", req.HostCode, req.Url).Error @@ -51,6 +69,9 @@ func (receiver *WafBlockUrlService) ModifyApi(req request.WafBlockUrlEditReq) er "CompareType": req.CompareType, "UPDATE_TIME": customtype.JsonTime(time.Now()), } + if req.IsEnable != nil { + modfiyMap["is_enable"] = *req.IsEnable + } err := global.GWAF_LOCAL_DB.Model(model.URLBlockList{}).Where("id = ?", req.Id).Updates(modfiyMap).Error return err diff --git a/service/waf_service/waf_ip_lookup_service.go b/service/waf_service/waf_ip_lookup_service.go index 203868b..a6c70a2 100644 --- a/service/waf_service/waf_ip_lookup_service.go +++ b/service/waf_service/waf_ip_lookup_service.go @@ -255,7 +255,8 @@ func (r *WafIPLookupService) hostLabel(code string, names map[string]string) str func (r *WafIPLookupService) matchAllowList(ip string, names map[string]string, resp *response2.IPLookupResp) { var rows []model.IPAllowList - if err := global.GWAF_LOCAL_DB.Find(&rows).Error; err != nil { + //与引擎同口径:停用条目不参与判定,诊断结论才不会背离实际拦截行为 + if err := global.GWAF_LOCAL_DB.Where("is_enable = 1").Find(&rows).Error; err != nil { resp.Degraded = append(resp.Degraded, srcIPWhite) return } @@ -279,7 +280,7 @@ func (r *WafIPLookupService) matchAllowList(ip string, names map[string]string, func (r *WafIPLookupService) matchBlockList(ip string, names map[string]string, resp *response2.IPLookupResp) { var rows []model.IPBlockList - if err := global.GWAF_LOCAL_DB.Find(&rows).Error; err != nil { + if err := global.GWAF_LOCAL_DB.Where("is_enable = 1").Find(&rows).Error; err != nil { resp.Degraded = append(resp.Degraded, srcIPBlack) return } @@ -366,7 +367,8 @@ func (r *WafIPLookupService) groupRefs() map[string]groupRef { refs := map[string]groupRef{} var blockRows []model.IPBlockList - global.GWAF_LOCAL_DB.Where("ip_type = ?", model.IPEntryTypeGroup).Find(&blockRows) + //引用统计同样只算启用条目,与引擎侧组短码抽取同口径 + global.GWAF_LOCAL_DB.Where("ip_type = ? and is_enable = 1", model.IPEntryTypeGroup).Find(&blockRows) for _, row := range blockRows { if row.GroupCode == "" { continue @@ -375,7 +377,7 @@ func (r *WafIPLookupService) groupRefs() map[string]groupRef { } var allowRows []model.IPAllowList - global.GWAF_LOCAL_DB.Where("ip_type = ?", model.IPEntryTypeGroup).Find(&allowRows) + global.GWAF_LOCAL_DB.Where("ip_type = ? and is_enable = 1", model.IPEntryTypeGroup).Find(&allowRows) for _, row := range allowRows { if row.GroupCode == "" { continue diff --git a/wafdb/migrations_core.go b/wafdb/migrations_core.go index f96aafe..3a0553b 100644 --- a/wafdb/migrations_core.go +++ b/wafdb/migrations_core.go @@ -2406,6 +2406,54 @@ func RunCoreDBMigrations(db *gorm.DB) error { return nil }, }, + // 迁移: 四类名单表(IP/URL 黑白名单)新增条目级启用开关列。 + // 存量行回填 1(启用),升级前后生效行为不变 + { + ID: "202610210001_add_list_entry_is_enable", + Migrate: func(tx *gorm.DB) error { + zlog.Info("迁移 202610210001: 四类名单表新增条目启用开关列") + type listCol struct { + model any + field string + col string + } + cols := []listCol{ + {&model.IPAllowList{}, "IsEnable", "is_enable"}, + {&model.URLAllowList{}, "IsEnable", "is_enable"}, + {&model.IPBlockList{}, "IsEnable", "is_enable"}, + {&model.URLBlockList{}, "IsEnable", "is_enable"}, + } + for _, c := range cols { + if tx.Migrator().HasColumn(c.model, c.col) { + continue + } + if err := tx.Migrator().AddColumn(c.model, c.field); err != nil { + return fmt.Errorf("新增名单条目启用列失败: %w", err) + } + } + // 只回填 NULL:0 是「用户主动停用」的合法值,不能动 + for _, c := range cols { + if err := tx.Model(c.model).Where("is_enable IS NULL").Update("is_enable", 1).Error; err != nil { + return fmt.Errorf("回填名单条目启用默认值失败: %w", err) + } + } + zlog.Info("迁移 202610210001: 完成") + return nil + }, + Rollback: func(tx *gorm.DB) error { + zlog.Info("回滚 202610210001: 删除四类名单表的条目启用开关列") + // 注意:删列即丢弃开关状态——用户停用过的条目回滚后回到「全部生效」, + // 再升级也不会恢复停用标记 + for _, m := range []any{&model.IPAllowList{}, &model.URLAllowList{}, &model.IPBlockList{}, &model.URLBlockList{}} { + if tx.Migrator().HasColumn(m, "is_enable") { + if err := tx.Migrator().DropColumn(m, "is_enable"); err != nil { + return err + } + } + } + return nil + }, + }, }) // 执行迁移 diff --git a/wafenginecore/wafworker.go b/wafenginecore/wafworker.go index e11333c..3391db5 100644 --- a/wafenginecore/wafworker.go +++ b/wafenginecore/wafworker.go @@ -120,21 +120,21 @@ func (waf *WafEngine) LoadHost(inHost model.Hosts) []innerbean.ServerRunTime { //加载并编译CC多规则(编译放在加载期,请求期只做取值与比较) ccRules := LoadCompiledCCRules(inHost.Code) - //查询ip白名单 + //查询ip白名单(停用条目不下发引擎,下同) var ipwhitelist []model.IPAllowList - global.GWAF_LOCAL_DB.Where("host_code=? ", inHost.Code).Find(&ipwhitelist) + global.GWAF_LOCAL_DB.Where("host_code=? and is_enable = 1", inHost.Code).Find(&ipwhitelist) //查询url白名单 var urlwhitelist []model.URLAllowList - global.GWAF_LOCAL_DB.Where("host_code=? ", inHost.Code).Find(&urlwhitelist) + global.GWAF_LOCAL_DB.Where("host_code=? and is_enable = 1", inHost.Code).Find(&urlwhitelist) //查询ip黑名单 var ipblocklist []model.IPBlockList - global.GWAF_LOCAL_DB.Where("host_code=? ", inHost.Code).Find(&ipblocklist) + global.GWAF_LOCAL_DB.Where("host_code=? and is_enable = 1", inHost.Code).Find(&ipblocklist) //查询url白名单 var urlblocklist []model.URLBlockList - global.GWAF_LOCAL_DB.Where("host_code=? ", inHost.Code).Find(&urlblocklist) + global.GWAF_LOCAL_DB.Where("host_code=? and is_enable = 1", inHost.Code).Find(&urlblocklist) //查询url隐私保护 var ldpurls []model.LDPUrl diff --git a/waftask/batch/ip_allow_processor.go b/waftask/batch/ip_allow_processor.go index 34d7552..7481239 100644 --- a/waftask/batch/ip_allow_processor.go +++ b/waftask/batch/ip_allow_processor.go @@ -57,7 +57,7 @@ func (p *IPAllowProcessor) GetExistingItems(items []string, task model.BatchTask // NotifyEngine 通知引擎更新 func (p *IPAllowProcessor) NotifyEngine(task model.BatchTask) { var ipWhites []model.IPAllowList - global.GWAF_LOCAL_DB.Where("host_code = ? ", task.BatchHostCode).Find(&ipWhites) + global.GWAF_LOCAL_DB.Where("host_code = ? and is_enable = 1", task.BatchHostCode).Find(&ipWhites) var chanInfo = spec.ChanCommonHost{ HostCode: task.BatchHostCode, Type: enums.ChanTypeAllowIP, @@ -86,6 +86,8 @@ func (p *IPAllowProcessor) processAppendBatch(items []string, existMap map[strin Remarks: time.Now().Format("20060102") + "批量导入 任务ID:" + task.Id, // 显式标记为「单条IP」:批量导入不产生 IP 组引用 IpType: model.IPEntryTypeIP, + // 显式启用:字段无 default 标签,零值会被如实落库为停用 + IsEnable: 1, }) } } @@ -137,6 +139,8 @@ func (p *IPAllowProcessor) processOverwriteBatch(items []string, existMap map[st Remarks: time.Now().Format("20060102") + "批量导入 任务ID:" + task.Id, // 显式标记为「单条IP」:批量导入不产生 IP 组引用 IpType: model.IPEntryTypeIP, + // 显式启用:字段无 default 标签,零值会被如实落库为停用 + IsEnable: 1, }) } } diff --git a/waftask/batch/ip_deny_processor.go b/waftask/batch/ip_deny_processor.go index 141c737..3db7a91 100644 --- a/waftask/batch/ip_deny_processor.go +++ b/waftask/batch/ip_deny_processor.go @@ -57,7 +57,7 @@ func (p *IPDenyProcessor) GetExistingItems(items []string, task model.BatchTask, // NotifyEngine 通知引擎更新 func (p *IPDenyProcessor) NotifyEngine(task model.BatchTask) { var ipBlocks []model.IPBlockList - global.GWAF_LOCAL_DB.Where("host_code = ? ", task.BatchHostCode).Find(&ipBlocks) + global.GWAF_LOCAL_DB.Where("host_code = ? and is_enable = 1", task.BatchHostCode).Find(&ipBlocks) var chanInfo = spec.ChanCommonHost{ HostCode: task.BatchHostCode, Type: enums.ChanTypeBlockIP, @@ -86,6 +86,8 @@ func (p *IPDenyProcessor) processAppendBatch(items []string, existMap map[string Remarks: time.Now().Format("20060102") + "批量导入 任务ID:" + task.Id, // 显式标记为「单条IP」:批量导入不产生 IP 组引用 IpType: model.IPEntryTypeIP, + // 显式启用:字段无 default 标签,零值会被如实落库为停用 + IsEnable: 1, }) } } @@ -137,6 +139,8 @@ func (p *IPDenyProcessor) processOverwriteBatch(items []string, existMap map[str Remarks: time.Now().Format("20060102") + "批量导入 任务ID:" + task.Id, // 显式标记为「单条IP」:批量导入不产生 IP 组引用 IpType: model.IPEntryTypeIP, + // 显式启用:字段无 default 标签,零值会被如实落库为停用 + IsEnable: 1, }) } } diff --git a/wafupgradenotice/upgrade_notes.yaml b/wafupgradenotice/upgrade_notes.yaml index 732f28e..8534fb7 100644 --- a/wafupgradenotice/upgrade_notes.yaml +++ b/wafupgradenotice/upgrade_notes.yaml @@ -12,6 +12,30 @@ # 5. fresh_install: true 的条目只在"全新安装"时生成,不参与版本区间计算 notes: + - id: v1_3_25_list_entry_switches + version: v1.3.25 + kind: check + level: low + page: /waf/wafipwhitelist + apply: + type: navigate + zh: + title: IP/URL 黑白名单支持按条目启用停用 + detail: >- + IP白名单、URL白名单、IP黑名单、URL黑名单的每条记录新增启用开关,在列表中直接切换, + 新增条目默认启用,存量条目升级后保持启用。停用的条目不再参与对应站点的防护判定 + (全局名单同理),配置仍保留在列表中,随时可重新启用;批量和编辑等原有操作不受影响。 + revert: 在对应名单页面把条目开关重新打开,即恢复该条目的防护效果。 + en: + title: Per-entry enable switches for IP/URL black- and whitelists + detail: >- + Each entry in the IP whitelist, URL whitelist, IP blacklist and URL blacklist now has an + enable switch toggled directly in the list. New entries default to enabled and existing + entries stay enabled after the upgrade. Disabled entries stop taking effect for the + corresponding website (global entries likewise) but remain in the list and can be + re-enabled at any time; batch import and editing behave as before. + revert: Turn the entry switch back on in the corresponding list page to restore its effect. + - id: v1_3_25_rule_ban version: v1.3.25 kind: check