mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-10-31 02:01:44 +08:00
@@ -23,6 +23,10 @@ const appUploadMaxBytes = 200 * 1024 * 1024 // 200 MB
|
||||
|
||||
type WafAppApi struct{}
|
||||
|
||||
func (w *WafAppApi) GetFeatureStatusApi(c *gin.Context) {
|
||||
response.OkWithData(gin.H{"enabled": global.GWAF_CAN_APP_MANAGE}, c)
|
||||
}
|
||||
|
||||
// recordSysLog 写安全审计日志到日志队列
|
||||
func recordSysLog(opType, content string) {
|
||||
log := model.WafSysLog{
|
||||
|
||||
@@ -11,7 +11,9 @@ type WafAppRouter struct{}
|
||||
func (r *WafAppRouter) InitWafAppRouter(group *gin.RouterGroup) {
|
||||
appApi := api.APIGroupAPP.WafAppApi
|
||||
|
||||
// 功能开关守卫:application_manage: false 时全部返回 403
|
||||
group.GET("/api/v1/application/app/feature_status", appApi.GetFeatureStatusApi)
|
||||
|
||||
// 应用操作在 application_manage: false 时返回 403
|
||||
appGroup := group.Group("", middleware.AppManageEnabled())
|
||||
|
||||
// 只读 / 低风险操作(无需操作密码)
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"SamWaf/global"
|
||||
"SamWaf/middleware"
|
||||
"SamWaf/wafsec"
|
||||
"encoding/json"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const appFeatureStatusPath = "/api/v1/application/app/feature_status"
|
||||
|
||||
func TestAppFeatureStatusAndDisabledList(t *testing.T) {
|
||||
priorEnabled, priorLegacy, priorKey := global.GWAF_CAN_APP_MANAGE, global.GCONFIG_COMM_LEGACY_KEY, global.GWAF_COMMUNICATION_KEY
|
||||
global.GCONFIG_COMM_LEGACY_KEY = true
|
||||
global.GWAF_COMMUNICATION_KEY = []byte("0123456789abcdef0123456789abcdef")
|
||||
t.Cleanup(func() {
|
||||
global.GWAF_CAN_APP_MANAGE, global.GCONFIG_COMM_LEGACY_KEY, global.GWAF_COMMUNICATION_KEY = priorEnabled, priorLegacy, priorKey
|
||||
})
|
||||
|
||||
r := gin.New()
|
||||
new(WafAppRouter).InitWafAppRouter(r.Group(""))
|
||||
for _, enabled := range []bool{false, true} {
|
||||
t.Run(strconv.FormatBool(enabled), func(t *testing.T) {
|
||||
global.GWAF_CAN_APP_MANAGE = enabled
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, appFeatureStatusPath, nil))
|
||||
var reply struct {
|
||||
Code int `json:"code"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &reply); err != nil || rec.Code != http.StatusOK || reply.Code != 0 {
|
||||
t.Fatalf("feature status response=%s status=%d err=%v", rec.Body.String(), rec.Code, err)
|
||||
}
|
||||
plain, err := wafsec.AesDecrypt(reply.Data, global.GWAF_COMMUNICATION_KEY)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var fields map[string]bool
|
||||
if err := json.Unmarshal(plain, &fields); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, exists := fields["enabled"]
|
||||
if len(fields) != 1 || !exists || got != enabled {
|
||||
t.Fatalf("feature status data=%s want only enabled=%v", plain, enabled)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
global.GWAF_CAN_APP_MANAGE = false
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/api/v1/application/app/list", nil))
|
||||
var reply struct {
|
||||
Code int `json:"code"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &reply); err != nil || rec.Code != http.StatusForbidden || reply.Code != 403 {
|
||||
t.Fatalf("disabled list response=%s status=%d err=%v", rec.Body.String(), rec.Code, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppFeatureStatusRejectsAPIKey(t *testing.T) {
|
||||
r := gin.New()
|
||||
group := r.Group("", middleware.TokenOnlyAuth())
|
||||
new(WafAppRouter).InitWafAppRouter(group)
|
||||
req := httptest.NewRequest(http.MethodGet, appFeatureStatusPath, nil)
|
||||
req.Header.Set("X-API-Key", "test-api-key")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
var reply struct {
|
||||
Code int `json:"code"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &reply); err != nil || reply.Code != -999 {
|
||||
t.Fatalf("API key response=%s err=%v", rec.Body.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppRouterProductionTokenOnlyMount(t *testing.T) {
|
||||
file, err := parser.ParseFile(token.NewFileSet(), "../wafmangeweb/localserver.go", nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mounts := 0
|
||||
tokenOnlyAuth, systemAdmin := false, false
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
call, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
sel, ok := call.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if sel.Sel.Name == "InitWafAppRouter" {
|
||||
mounts++
|
||||
if len(call.Args) != 1 {
|
||||
t.Fatal("application router must receive its authenticated group")
|
||||
}
|
||||
group, ok := call.Args[0].(*ast.Ident)
|
||||
if !ok || group.Name != "TokenOnlyRouterGroup" {
|
||||
t.Fatal("application router must be mounted on TokenOnlyRouterGroup")
|
||||
}
|
||||
}
|
||||
group, ok := sel.X.(*ast.Ident)
|
||||
if !ok || group.Name != "TokenOnlyRouterGroup" || sel.Sel.Name != "Use" {
|
||||
return true
|
||||
}
|
||||
for _, arg := range call.Args {
|
||||
mw, ok := arg.(*ast.CallExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fn, ok := mw.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
pkg, ok := fn.X.(*ast.Ident)
|
||||
if !ok || pkg.Name != "middleware" {
|
||||
continue
|
||||
}
|
||||
if fn.Sel.Name == "TokenOnlyAuth" {
|
||||
tokenOnlyAuth = true
|
||||
}
|
||||
if fn.Sel.Name == "RequireRole" && len(mw.Args) == 1 {
|
||||
role, ok := mw.Args[0].(*ast.SelectorExpr)
|
||||
if ok {
|
||||
pkg, ok := role.X.(*ast.Ident)
|
||||
systemAdmin = ok && pkg.Name == "enums" && role.Sel.Name == "ROLE_SYSTEM_ADMIN"
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if mounts != 1 || !tokenOnlyAuth || !systemAdmin {
|
||||
t.Fatalf("application mounts=%d TokenOnlyAuth=%v system admin=%v", mounts, tokenOnlyAuth, systemAdmin)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,22 @@
|
||||
# 5. fresh_install: true 的条目只在"全新安装"时生成,不参与版本区间计算
|
||||
|
||||
notes:
|
||||
- id: v1_3_25_app_manage_setup_guide
|
||||
version: v1.3.25
|
||||
kind: check
|
||||
level: low
|
||||
page: /application/AppList
|
||||
apply:
|
||||
type: navigate
|
||||
zh:
|
||||
title: 应用管理未开启时显示配置引导
|
||||
detail: 应用管理页面会先检查功能状态;未开启时显示配置位置、开启方法与重启提示,开启后显示应用列表。启用功能与设置应用操作密码仍需由管理员在 conf/config.yml 中完成,页面不提供在线修改配置。
|
||||
revert: 无需额外操作;修改 application_manage 配置并重启后,页面按当前功能状态显示。
|
||||
en:
|
||||
title: Setup guidance when application management is disabled
|
||||
detail: The application management page checks the feature status first. When disabled, it shows the configuration location, setup steps and restart reminder; when enabled, it displays the application list. Administrators still enable the feature and set the application operation password in conf/config.yml. The page does not edit configuration online.
|
||||
revert: No additional action is required. After changing application_manage and restarting, the page reflects the current feature status.
|
||||
|
||||
- id: v1_3_25_list_entry_switches
|
||||
version: v1.3.25
|
||||
kind: check
|
||||
|
||||
Reference in New Issue
Block a user