From 49d32d0b25f624a6609607181d2d7b91c33f7097 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 19 Jun 2026 11:13:57 +0800 Subject: [PATCH] fix(openflare): align admin permission model with Wavelet Unify OpenFlare console auth to user.IsAdmin and token_admin instead of the legacy Admin/Root role tiers. Route groups now use apiutil.AdminMiddlewares (LoginRequired + LoginAdminRequired) so admin checks cannot be skipped. Add middleware tests and extend integration coverage for 401/404/400 responses. --- .../apps/openflare/apiutil/middleware.go | 30 +--- .../apps/openflare/apiutil/middleware_test.go | 158 ++++++++++++++++++ .../openflare/integration/auth_option_test.go | 36 +++- .../router/v1/openflare/register_apply_log.go | 2 +- .../v1/openflare/register_config_version.go | 2 +- .../router/v1/openflare/register_dashboard.go | 2 +- .../router/v1/openflare/register_node.go | 2 +- .../v1/openflare/register_observability.go | 2 +- .../router/v1/openflare/register_option.go | 4 +- .../router/v1/openflare/register_origin.go | 2 +- .../router/v1/openflare/register_pages.go | 2 +- .../v1/openflare/register_proxy_route.go | 2 +- .../router/v1/openflare/register_tls.go | 8 +- .../router/v1/openflare/register_update.go | 2 +- .../router/v1/openflare/register_waf.go | 2 +- docs/changelog/index.md | 3 +- 16 files changed, 208 insertions(+), 51 deletions(-) create mode 100644 Wavelet/internal/apps/openflare/apiutil/middleware_test.go diff --git a/Wavelet/internal/apps/openflare/apiutil/middleware.go b/Wavelet/internal/apps/openflare/apiutil/middleware.go index 62e92d12..8082661f 100644 --- a/Wavelet/internal/apps/openflare/apiutil/middleware.go +++ b/Wavelet/internal/apps/openflare/apiutil/middleware.go @@ -6,32 +6,12 @@ package apiutil import ( "github.com/Rain-kl/Wavelet/internal/apps/admin" "github.com/Rain-kl/Wavelet/internal/apps/oauth" - "github.com/Rain-kl/Wavelet/internal/common" - "github.com/Rain-kl/Wavelet/internal/common/response" "github.com/gin-gonic/gin" ) -// AdminRequired ensures the caller is logged in as a Wavelet administrator. -func AdminRequired() gin.HandlerFunc { - return func(c *gin.Context) { - user, err := oauth.GetUserFromRequest(c) - if err != nil { - response.AbortUnauthorized(c, common.UnAuthorized) - return - } - oauth.SetToContext(c, oauth.UserObjKey, user) - - if tokenAuth, _ := oauth.GetFromContext[bool](c, oauth.TokenAuthKey); tokenAuth { - tokenAdmin, _ := oauth.GetFromContext[bool](c, oauth.TokenAdminKey) - if !tokenAdmin { - response.AbortNotFound(c, admin.TokenAdminRequired) - return - } - } - if !user.IsAdmin { - response.AbortNotFound(c, admin.AdminRequired) - return - } - c.Next() - } +// AdminMiddlewares returns Wavelet-standard middlewares for OpenFlare console routes. +// OpenFlare no longer distinguishes Admin vs Root tiers; all management endpoints share +// the same gate: user.IsAdmin for session users, token_admin for Access Token callers. +func AdminMiddlewares() []gin.HandlerFunc { + return []gin.HandlerFunc{oauth.LoginRequired(), admin.LoginAdminRequired()} } \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/apiutil/middleware_test.go b/Wavelet/internal/apps/openflare/apiutil/middleware_test.go new file mode 100644 index 00000000..bcd3f559 --- /dev/null +++ b/Wavelet/internal/apps/openflare/apiutil/middleware_test.go @@ -0,0 +1,158 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +package apiutil + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Rain-kl/Wavelet/internal/apps/admin" + "github.com/Rain-kl/Wavelet/internal/apps/oauth" + "github.com/Rain-kl/Wavelet/internal/common/response" + "github.com/Rain-kl/Wavelet/internal/config" + "github.com/Rain-kl/Wavelet/internal/db" + "github.com/Rain-kl/Wavelet/internal/db/idgen" + "github.com/Rain-kl/Wavelet/internal/model" + "github.com/Rain-kl/Wavelet/internal/testhelper" + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" + "github.com/glebarez/sqlite" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/gorm" +) + +func setupAdminMiddlewareTest(t *testing.T) (*gin.Engine, *gorm.DB, func()) { + t.Helper() + + dbConn, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ + DisableForeignKeyConstraintWhenMigrating: true, + }) + require.NoError(t, err) + require.NoError(t, dbConn.AutoMigrate(&model.User{}, &model.AccessToken{})) + db.SetDB(dbConn) + + sessionCookieName := "test_admin_middleware_session" + if config.Config.App.SessionCookieName != "" { + sessionCookieName = config.Config.App.SessionCookieName + } + store := cookie.NewStore([]byte("test_admin_middleware_session_secret")) + store.Options(oauth.GetSessionOptions(3600)) + engine := testhelper.NewTestGinEngine(sessions.Sessions(sessionCookieName, store)) + protected := engine.Group("/protected", AdminMiddlewares()...) + protected.GET("", func(c *gin.Context) { + c.JSON(http.StatusOK, response.OK(gin.H{"ok": true})) + }) + + cleanup := func() { + db.SetDB(nil) + } + + return engine, dbConn, cleanup +} + +func seedUser(t *testing.T, dbConn *gorm.DB, username string, isAdmin bool) *model.User { + t.Helper() + + user := &model.User{ + ID: idgen.NextUint64ID(), + Username: username, + Nickname: username, + Email: username + "@openflare.test", + IsActive: true, + IsAdmin: isAdmin, + } + require.NoError(t, dbConn.Create(user).Error) + return user +} + +func seedAccessToken(t *testing.T, dbConn *gorm.DB, user *model.User, isAdmin bool) string { + t.Helper() + + token, err := model.GenerateTokenString() + require.NoError(t, err) + require.NoError(t, dbConn.Create(&model.AccessToken{ + UserID: user.ID, + Name: user.Username + "-token", + TokenHash: model.HashToken(token), + MaskedToken: model.MaskTokenString(token), + IsAdmin: isAdmin, + }).Error) + return token +} + +func decodeResponse(t *testing.T, rec *httptest.ResponseRecorder) response.Any { + t.Helper() + + var resp response.Any + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) + return resp +} + +func TestAdminRequiredUnauthenticated(t *testing.T) { + engine, _, cleanup := setupAdminMiddlewareTest(t) + defer cleanup() + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/protected", nil) + engine.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusUnauthorized, rec.Code) + resp := decodeResponse(t, rec) + assert.NotEmpty(t, resp.ErrorMsg) +} + +func TestAdminRequiredNonAdminToken(t *testing.T) { + engine, dbConn, cleanup := setupAdminMiddlewareTest(t) + defer cleanup() + + user := seedUser(t, dbConn, "regular", false) + token := seedAccessToken(t, dbConn, user, false) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/protected", nil) + req.Header.Set("X-Access-Token", token) + engine.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + resp := decodeResponse(t, rec) + assert.Equal(t, admin.TokenAdminRequired, resp.ErrorMsg) +} + +func TestAdminRequiredAdminWithoutTokenAdmin(t *testing.T) { + engine, dbConn, cleanup := setupAdminMiddlewareTest(t) + defer cleanup() + + user := seedUser(t, dbConn, "admin-no-token-admin", true) + token := seedAccessToken(t, dbConn, user, false) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/protected", nil) + req.Header.Set("X-Access-Token", token) + engine.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + resp := decodeResponse(t, rec) + assert.Equal(t, admin.TokenAdminRequired, resp.ErrorMsg) +} + +func TestAdminRequiredAdminWithTokenAdmin(t *testing.T) { + engine, dbConn, cleanup := setupAdminMiddlewareTest(t) + defer cleanup() + + user := seedUser(t, dbConn, "admin", true) + token := seedAccessToken(t, dbConn, user, true) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/protected", nil) + req.Header.Set("X-Access-Token", token) + engine.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + resp := decodeResponse(t, rec) + assert.Empty(t, resp.ErrorMsg) +} \ No newline at end of file diff --git a/Wavelet/internal/apps/openflare/integration/auth_option_test.go b/Wavelet/internal/apps/openflare/integration/auth_option_test.go index 08b170c8..214213ad 100644 --- a/Wavelet/internal/apps/openflare/integration/auth_option_test.go +++ b/Wavelet/internal/apps/openflare/integration/auth_option_test.go @@ -4,8 +4,10 @@ package integration import ( + "bytes" "context" "net/http" + "net/http/httptest" "testing" "github.com/Rain-kl/Wavelet/internal/apps/admin" @@ -120,10 +122,10 @@ func TestGETStatusReturnsSuccessEnvelope(t *testing.T) { assert.NotEmpty(t, status.SystemName) } -func TestGETOptionRequiresRootAuth(t *testing.T) { +func TestGETOptionRequiresAdminAuth(t *testing.T) { dbConn, r := setupAuthOptionIntegration(t) commonToken := seedUserWithAccessToken(t, dbConn, "commonuser", "password123", false) - rootToken := seedUserWithAccessToken(t, dbConn, "rootuser", "password123", true) + adminToken := seedUserWithAccessToken(t, dbConn, "adminuser", "password123", true) t.Run("unauthenticated", func(t *testing.T) { w := performJSONRequest(t, r, http.MethodGet, apiPath("/option/"), nil, nil) @@ -132,26 +134,42 @@ func TestGETOptionRequiresRootAuth(t *testing.T) { assert.NotEmpty(t, resp.ErrorMsg) }) - t.Run("common user forbidden", func(t *testing.T) { + t.Run("non-admin user forbidden", func(t *testing.T) { w := performJSONRequest(t, r, http.MethodGet, apiPath("/option/"), nil, adminAuthHeaders(commonToken)) assert.Equal(t, http.StatusNotFound, w.Code) resp := decodeAPIResponse(t, w) assert.Equal(t, admin.TokenAdminRequired, resp.ErrorMsg) }) - t.Run("root user allowed", func(t *testing.T) { - w := performJSONRequest(t, r, http.MethodGet, apiPath("/option/"), nil, adminAuthHeaders(rootToken)) + t.Run("admin user allowed", func(t *testing.T) { + w := performJSONRequest(t, r, http.MethodGet, apiPath("/option/"), nil, adminAuthHeaders(adminToken)) assert.Equal(t, http.StatusOK, w.Code) requireAPIOK(t, w) }) } +func TestPOSTOptionUpdateRejectsInvalidParams(t *testing.T) { + dbConn, r := setupAuthOptionIntegration(t) + adminToken := seedUserWithAccessToken(t, dbConn, "adminuser", "password123", true) + + req := httptest.NewRequest(http.MethodPost, apiPath("/option/update"), bytes.NewReader([]byte("{invalid"))) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Access-Token", adminToken) + + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + resp := decodeAPIResponse(t, w) + assert.NotEmpty(t, resp.ErrorMsg) +} + func TestGETNodesWithAccessToken(t *testing.T) { dbConn, r := setupAuthOptionIntegration(t) require.NoError(t, dbConn.AutoMigrate(&model.OpenFlareNode{})) - rootToken := seedUserWithAccessToken(t, dbConn, "admin", "password123", true) + adminToken := seedUserWithAccessToken(t, dbConn, "admin", "password123", true) - w := performJSONRequest(t, r, http.MethodGet, apiPath("/nodes/"), nil, adminAuthHeaders(rootToken)) + w := performJSONRequest(t, r, http.MethodGet, apiPath("/nodes/"), nil, adminAuthHeaders(adminToken)) assert.Equal(t, http.StatusOK, w.Code) requireAPIOK(t, w) @@ -159,7 +177,7 @@ func TestGETNodesWithAccessToken(t *testing.T) { func TestOptionHotReloadAfterUpdate(t *testing.T) { dbConn, r := setupAuthOptionIntegration(t) - rootToken := seedUserWithAccessToken(t, dbConn, "admin", "password123", true) + adminToken := seedUserWithAccessToken(t, dbConn, "admin", "password123", true) statusBefore := getStatusSystemName(t, r, nil) assert.NotEmpty(t, statusBefore) @@ -167,7 +185,7 @@ func TestOptionHotReloadAfterUpdate(t *testing.T) { updateResp := performJSONRequest(t, r, http.MethodPost, apiPath("/option/update"), map[string]string{ "key": "SystemName", "value": "HotReloadIntegration", - }, adminAuthHeaders(rootToken)) + }, adminAuthHeaders(adminToken)) assert.Equal(t, http.StatusOK, updateResp.Code) requireAPIOK(t, updateResp) diff --git a/Wavelet/internal/router/v1/openflare/register_apply_log.go b/Wavelet/internal/router/v1/openflare/register_apply_log.go index 43182da8..4356f9ee 100644 --- a/Wavelet/internal/router/v1/openflare/register_apply_log.go +++ b/Wavelet/internal/router/v1/openflare/register_apply_log.go @@ -11,7 +11,7 @@ import ( func registerApplyLogRoutes(apiGroup *gin.RouterGroup) { applyLogRoute := apiGroup.Group("/apply-logs") - applyLogRoute.Use(apiutil.AdminRequired()) + applyLogRoute.Use(apiutil.AdminMiddlewares()...) { apiutil.RegisterCollection(applyLogRoute, "GET", apply_log.GetApplyLogs) applyLogRoute.POST("/cleanup", apply_log.CleanupApplyLogs) diff --git a/Wavelet/internal/router/v1/openflare/register_config_version.go b/Wavelet/internal/router/v1/openflare/register_config_version.go index a7daebf6..e8e56432 100644 --- a/Wavelet/internal/router/v1/openflare/register_config_version.go +++ b/Wavelet/internal/router/v1/openflare/register_config_version.go @@ -11,7 +11,7 @@ import ( func registerConfigVersionRoutes(apiGroup *gin.RouterGroup) { configVersionGroup := apiGroup.Group("/config-versions") - configVersionGroup.Use(apiutil.AdminRequired()) + configVersionGroup.Use(apiutil.AdminMiddlewares()...) { apiutil.RegisterCollection(configVersionGroup, "GET", config_version.ListConfigVersionsHandler) configVersionGroup.GET("/active", config_version.GetActiveConfigVersionHandler) diff --git a/Wavelet/internal/router/v1/openflare/register_dashboard.go b/Wavelet/internal/router/v1/openflare/register_dashboard.go index 21bb7e7d..780f11a1 100644 --- a/Wavelet/internal/router/v1/openflare/register_dashboard.go +++ b/Wavelet/internal/router/v1/openflare/register_dashboard.go @@ -11,7 +11,7 @@ import ( func registerDashboardRoutes(apiGroup *gin.RouterGroup) { dashboardRoute := apiGroup.Group("/dashboard") - dashboardRoute.Use(apiutil.AdminRequired()) + dashboardRoute.Use(apiutil.AdminMiddlewares()...) { dashboardRoute.GET("/overview", dashboard.GetOverviewHandler) } diff --git a/Wavelet/internal/router/v1/openflare/register_node.go b/Wavelet/internal/router/v1/openflare/register_node.go index f8e6ffe2..9fa12189 100644 --- a/Wavelet/internal/router/v1/openflare/register_node.go +++ b/Wavelet/internal/router/v1/openflare/register_node.go @@ -11,7 +11,7 @@ import ( func registerNodeRoutes(apiGroup *gin.RouterGroup) { nodeRoute := apiGroup.Group("/nodes") - nodeRoute.Use(apiutil.AdminRequired()) + nodeRoute.Use(apiutil.AdminMiddlewares()...) { nodeRoute.GET("/bootstrap-token", node.GetBootstrapTokenHandler) nodeRoute.POST("/bootstrap-token/rotate", node.RotateBootstrapTokenHandler) diff --git a/Wavelet/internal/router/v1/openflare/register_observability.go b/Wavelet/internal/router/v1/openflare/register_observability.go index 1d38d227..ea927821 100644 --- a/Wavelet/internal/router/v1/openflare/register_observability.go +++ b/Wavelet/internal/router/v1/openflare/register_observability.go @@ -11,7 +11,7 @@ import ( func registerObservabilityRoutes(apiGroup *gin.RouterGroup) { accessLogRoute := apiGroup.Group("/access-logs") - accessLogRoute.Use(apiutil.AdminRequired()) + accessLogRoute.Use(apiutil.AdminMiddlewares()...) { apiutil.RegisterCollection(accessLogRoute, "GET", observability.GetAccessLogsHandler) accessLogRoute.GET("/folds", observability.GetFoldedAccessLogsHandler) diff --git a/Wavelet/internal/router/v1/openflare/register_option.go b/Wavelet/internal/router/v1/openflare/register_option.go index 68754be4..9552f766 100644 --- a/Wavelet/internal/router/v1/openflare/register_option.go +++ b/Wavelet/internal/router/v1/openflare/register_option.go @@ -15,7 +15,7 @@ func registerOptionRoutes(apiGroup *gin.RouterGroup) { apiGroup.GET("/about", option.GetAboutHandler) optionRoute := apiGroup.Group("/option") - optionRoute.Use(apiutil.AdminRequired()) + optionRoute.Use(apiutil.AdminMiddlewares()...) { apiutil.RegisterCollection(optionRoute, "GET", option.ListOptionsHandler) optionRoute.POST("/update", option.UpdateOptionHandler) @@ -25,7 +25,7 @@ func registerOptionRoutes(apiGroup *gin.RouterGroup) { } uptimeKumaRoute := apiGroup.Group("/uptimekuma") - uptimeKumaRoute.Use(apiutil.AdminRequired()) + uptimeKumaRoute.Use(apiutil.AdminMiddlewares()...) { uptimeKumaRoute.POST("/sync", option.SyncUptimeKumaHandler) } diff --git a/Wavelet/internal/router/v1/openflare/register_origin.go b/Wavelet/internal/router/v1/openflare/register_origin.go index ca26b496..ec9aacba 100644 --- a/Wavelet/internal/router/v1/openflare/register_origin.go +++ b/Wavelet/internal/router/v1/openflare/register_origin.go @@ -11,7 +11,7 @@ import ( func registerOriginRoutes(apiGroup *gin.RouterGroup) { originRoute := apiGroup.Group("/origins") - originRoute.Use(apiutil.AdminRequired()) + originRoute.Use(apiutil.AdminMiddlewares()...) { apiutil.RegisterCollection(originRoute, "GET", origin.GetOrigins) originRoute.GET("/:id", origin.GetOrigin) diff --git a/Wavelet/internal/router/v1/openflare/register_pages.go b/Wavelet/internal/router/v1/openflare/register_pages.go index aa7e64a5..2ddf4945 100644 --- a/Wavelet/internal/router/v1/openflare/register_pages.go +++ b/Wavelet/internal/router/v1/openflare/register_pages.go @@ -11,7 +11,7 @@ import ( func registerPagesRoutes(apiGroup *gin.RouterGroup) { pagesRoute := apiGroup.Group("/pages") - pagesRoute.Use(apiutil.AdminRequired()) + pagesRoute.Use(apiutil.AdminMiddlewares()...) { apiutil.RegisterCollection(pagesRoute, "GET", pages.ListProjectsHandler) pagesRoute.GET("/:id", pages.GetProjectHandler) diff --git a/Wavelet/internal/router/v1/openflare/register_proxy_route.go b/Wavelet/internal/router/v1/openflare/register_proxy_route.go index ea10e0b5..5f67a32f 100644 --- a/Wavelet/internal/router/v1/openflare/register_proxy_route.go +++ b/Wavelet/internal/router/v1/openflare/register_proxy_route.go @@ -11,7 +11,7 @@ import ( func registerProxyRouteRoutes(apiGroup *gin.RouterGroup) { proxyRouteGroup := apiGroup.Group("/proxy-routes") - proxyRouteGroup.Use(apiutil.AdminRequired()) + proxyRouteGroup.Use(apiutil.AdminMiddlewares()...) { apiutil.RegisterCollection(proxyRouteGroup, "GET", proxy_route.GetProxyRoutes) proxyRouteGroup.GET("/:id", proxy_route.GetProxyRouteHandler) diff --git a/Wavelet/internal/router/v1/openflare/register_tls.go b/Wavelet/internal/router/v1/openflare/register_tls.go index 62176378..8d87809a 100644 --- a/Wavelet/internal/router/v1/openflare/register_tls.go +++ b/Wavelet/internal/router/v1/openflare/register_tls.go @@ -11,7 +11,7 @@ import ( func registerTLSRoutes(apiGroup *gin.RouterGroup) { managedDomainRoute := apiGroup.Group("/managed-domains") - managedDomainRoute.Use(apiutil.AdminRequired()) + managedDomainRoute.Use(apiutil.AdminMiddlewares()...) { apiutil.RegisterCollection(managedDomainRoute, "GET", tls.GetManagedDomains) managedDomainRoute.GET("/match", tls.MatchManagedDomainCertificateHandler) @@ -21,7 +21,7 @@ func registerTLSRoutes(apiGroup *gin.RouterGroup) { } tlsCertificateRoute := apiGroup.Group("/tls-certificates") - tlsCertificateRoute.Use(apiutil.AdminRequired()) + tlsCertificateRoute.Use(apiutil.AdminMiddlewares()...) { apiutil.RegisterCollection(tlsCertificateRoute, "GET", tls.GetCertificates) tlsCertificateRoute.GET("/:id", tls.GetCertificateDetail) @@ -37,13 +37,13 @@ func registerTLSRoutes(apiGroup *gin.RouterGroup) { } acmeAccountRoute := apiGroup.Group("/acme-accounts") - acmeAccountRoute.Use(apiutil.AdminRequired()) + acmeAccountRoute.Use(apiutil.AdminMiddlewares()...) { acmeAccountRoute.GET("/default", tls.GetDefaultAcmeAccountHandler) } dnsAccountRoute := apiGroup.Group("/dns-accounts") - dnsAccountRoute.Use(apiutil.AdminRequired()) + dnsAccountRoute.Use(apiutil.AdminMiddlewares()...) { apiutil.RegisterCollection(dnsAccountRoute, "GET", tls.GetDNSAccounts) apiutil.RegisterCollection(dnsAccountRoute, "POST", tls.CreateDNSAccountHandler) diff --git a/Wavelet/internal/router/v1/openflare/register_update.go b/Wavelet/internal/router/v1/openflare/register_update.go index a5370d66..7d919cbf 100644 --- a/Wavelet/internal/router/v1/openflare/register_update.go +++ b/Wavelet/internal/router/v1/openflare/register_update.go @@ -11,7 +11,7 @@ import ( func registerUpdateRoutes(apiGroup *gin.RouterGroup) { updateRoute := apiGroup.Group("/update") - updateRoute.Use(apiutil.AdminRequired()) + updateRoute.Use(apiutil.AdminMiddlewares()...) { updateRoute.GET("/latest-release", update.GetLatestReleaseHandler) updateRoute.GET("/logs/ws", update.StreamServerUpgradeLogsHandler) diff --git a/Wavelet/internal/router/v1/openflare/register_waf.go b/Wavelet/internal/router/v1/openflare/register_waf.go index 8bcaf47f..bea01a41 100644 --- a/Wavelet/internal/router/v1/openflare/register_waf.go +++ b/Wavelet/internal/router/v1/openflare/register_waf.go @@ -11,7 +11,7 @@ import ( func registerWAFRoutes(apiGroup *gin.RouterGroup) { wafRoute := apiGroup.Group("/waf") - wafRoute.Use(apiutil.AdminRequired()) + wafRoute.Use(apiutil.AdminMiddlewares()...) { wafRoute.GET("/ip-groups", waf.ListIPGroupsHandler) wafRoute.GET("/ip-groups/:id", waf.GetIPGroupHandler) diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 744b69db..01fe1910 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -65,6 +65,7 @@ sidebar: false ### 变更 +- OpenFlare 管理端权限模型对齐 Wavelet:取消旧系统 Admin/Root 三级角色区分,统一以 `user.IsAdmin` 为管理门槛;Access Token 访问敏感接口(Option、Update 等)须 `token_admin=true`;权限不足返回 HTTP 404 + `error_msg`,参数错误返回 HTTP 400 + `error_msg`(`apiutil.AdminMiddlewares` = `oauth.LoginRequired` + `admin.LoginAdminRequired`)。 - 移除 Wavelet 顶栏 OpenFlare 服务端版本入口按钮;版本升级能力保留在 Admin 设置 OpenFlare 运维 Tab。 - 将 Wavelet 默认上游仓库调整为 `Rain-kl/OpenFlare`,站点名称、邮件模板、前端默认标题与页脚品牌统一初始化为 OpenFlare;新增 goose 迁移回填既有环境的旧 Wavelet 默认值。 - Wavelet WAF IP 组列表新增「查看」操作,支持在查看弹窗中浏览当前 IP 并移除单条 IP;自动类型同步裁剪 `ext_ips` 以与 `ip_list` 保持一致。 @@ -75,7 +76,7 @@ sidebar: false - 将 OpenFlare 路由注册包从 `internal/router/openflare/` 迁移至 `internal/router/v1/openflare/`,与 v1 路由分层目录结构对齐。 - OpenFlare 管理端路由注册从 `RegisterCustomRoutes` 移至 `v1.RegisterV1Routes`,API 前缀由 `/api/v1/custom/openflare` 调整为 `/api/v1/openflare`。 - OpenFlare 管理控制台 API 统一迁移至 `/api/v1/openflare/*`,响应格式对齐 Wavelet `{error_msg, data}` + `response.Abort*`。 -- `/api/*` 仅保留 Agent/Relay/Flared 节点协议路由(legacy `{success, message, data}` 信封);控制台鉴权改为 `apiutil.AdminRequired()`。 +- `/api/*` 仅保留 Agent/Relay/Flared 节点协议路由(legacy `{success, message, data}` 信封);控制台鉴权改为 `apiutil.AdminMiddlewares()`。 - 前端 OpenFlare Service 层由 `LegacyOpenFlareBaseService` 切换为 `OpenFlareBaseService`(`BaseService` + v1 路径)。 - 为 OpenFlare v1 管理端 API 补充 Swagger 注解(`/api/v1/openflare/*`,约 99 个端点)。 - 移除已无引用的 `internal/apps/openflare/auth/`、`compat/auth.go`、`compat/routes.go` 及前端 `legacy-base.service.ts`。