From c19daaf19b94c84d00445a8b631bce4ff3107685 Mon Sep 17 00:00:00 2001 From: samwaf Date: Thu, 22 Oct 2026 12:55:53 +0800 Subject: [PATCH] feat: expose application management feature status #1028 --- api/waf_app_api.go | 4 + router/waf_app_router.go | 4 +- router/waf_app_router_test.go | 144 ++++++++++++++++++++++++++++ wafupgradenotice/upgrade_notes.yaml | 16 ++++ 4 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 router/waf_app_router_test.go diff --git a/api/waf_app_api.go b/api/waf_app_api.go index 545feb6..667d46c 100644 --- a/api/waf_app_api.go +++ b/api/waf_app_api.go @@ -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{ diff --git a/router/waf_app_router.go b/router/waf_app_router.go index c1d3a46..cc2054b 100644 --- a/router/waf_app_router.go +++ b/router/waf_app_router.go @@ -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()) // 只读 / 低风险操作(无需操作密码) diff --git a/router/waf_app_router_test.go b/router/waf_app_router_test.go new file mode 100644 index 0000000..ff66045 --- /dev/null +++ b/router/waf_app_router_test.go @@ -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) + } +} diff --git a/wafupgradenotice/upgrade_notes.yaml b/wafupgradenotice/upgrade_notes.yaml index 8534fb7..0c07500 100644 --- a/wafupgradenotice/upgrade_notes.yaml +++ b/wafupgradenotice/upgrade_notes.yaml @@ -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