feat: expose application management feature status

#1028
This commit is contained in:
samwaf
2026-10-22 12:55:53 +08:00
parent 76ad786835
commit c19daaf19b
4 changed files with 167 additions and 1 deletions
+4
View File
@@ -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{
+3 -1
View File
@@ -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())
// 只读 / 低风险操作(无需操作密码)
+144
View File
@@ -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)
}
}
+16
View File
@@ -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