feat: better admin permissions (#5755)
* feat: add casbin admin permissions * feat: improve audit logging to associate logs with actual operators and target users * feat: enhance admin permissions and UI interactions for sensitive actions * Refactor authz RBAC and tighten channel permissions * Split channel authz field policy * Address channel authz review findings
This commit is contained in:
+2
-42
@@ -225,48 +225,8 @@ func SetApiRouter(router *gin.Engine) {
|
||||
ratioSyncRoute.GET("/channels", controller.GetSyncableChannels)
|
||||
ratioSyncRoute.POST("/fetch", controller.FetchUpstreamRatios)
|
||||
}
|
||||
channelRoute := apiRouter.Group("/channel")
|
||||
channelRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
channelRoute.GET("/", controller.GetAllChannels)
|
||||
channelRoute.GET("/search", controller.SearchChannels)
|
||||
channelRoute.GET("/models", controller.ChannelListModels)
|
||||
channelRoute.GET("/models_enabled", controller.EnabledListModels)
|
||||
channelRoute.GET("/ops", controller.GetChannelOps)
|
||||
channelRoute.GET("/:id", controller.GetChannel)
|
||||
channelRoute.POST("/:id/key", middleware.RootAuth(), middleware.CriticalRateLimit(), middleware.DisableCache(), middleware.SecureVerificationRequired(), controller.GetChannelKey)
|
||||
channelRoute.GET("/test", controller.TestAllChannels)
|
||||
channelRoute.GET("/test/:id", controller.TestChannel)
|
||||
channelRoute.GET("/update_balance", controller.UpdateAllChannelsBalance)
|
||||
channelRoute.GET("/update_balance/:id", controller.UpdateChannelBalance)
|
||||
channelRoute.POST("/", controller.AddChannel)
|
||||
channelRoute.PUT("/", controller.UpdateChannel)
|
||||
channelRoute.DELETE("/disabled", controller.DeleteDisabledChannel)
|
||||
channelRoute.POST("/tag/disabled", controller.DisableTagChannels)
|
||||
channelRoute.POST("/tag/enabled", controller.EnableTagChannels)
|
||||
channelRoute.PUT("/tag", controller.EditTagChannels)
|
||||
channelRoute.DELETE("/:id", controller.DeleteChannel)
|
||||
channelRoute.POST("/batch", controller.DeleteChannelBatch)
|
||||
channelRoute.POST("/fix", controller.FixChannelsAbilities)
|
||||
channelRoute.GET("/fetch_models/:id", controller.FetchUpstreamModels)
|
||||
channelRoute.POST("/fetch_models", middleware.RootAuth(), controller.FetchModels)
|
||||
channelRoute.POST("/:id/codex/refresh", controller.RefreshCodexChannelCredential)
|
||||
channelRoute.GET("/:id/codex/usage", controller.GetCodexChannelUsage)
|
||||
channelRoute.GET("/:id/codex/usage/reset-credits", controller.GetCodexChannelRateLimitResetCredits)
|
||||
channelRoute.POST("/:id/codex/usage/reset", controller.ResetCodexChannelUsage)
|
||||
channelRoute.POST("/ollama/pull", controller.OllamaPullModel)
|
||||
channelRoute.POST("/ollama/pull/stream", controller.OllamaPullModelStream)
|
||||
channelRoute.DELETE("/ollama/delete", controller.OllamaDeleteModel)
|
||||
channelRoute.GET("/ollama/version/:id", controller.OllamaVersion)
|
||||
channelRoute.POST("/batch/tag", controller.BatchSetChannelTag)
|
||||
channelRoute.GET("/tag/models", controller.GetTagModels)
|
||||
channelRoute.POST("/copy/:id", controller.CopyChannel)
|
||||
channelRoute.POST("/multi_key/manage", controller.ManageMultiKeys)
|
||||
channelRoute.POST("/upstream_updates/apply", controller.ApplyChannelUpstreamModelUpdates)
|
||||
channelRoute.POST("/upstream_updates/apply_all", controller.ApplyAllChannelUpstreamModelUpdates)
|
||||
channelRoute.POST("/upstream_updates/detect", controller.DetectChannelUpstreamModelUpdates)
|
||||
channelRoute.POST("/upstream_updates/detect_all", controller.DetectAllChannelUpstreamModelUpdates)
|
||||
}
|
||||
registerChannelRoutes(apiRouter)
|
||||
registerAuthzRoutes(apiRouter)
|
||||
tokenRoute := apiRouter.Group("/token")
|
||||
tokenRoute.Use(middleware.UserAuth())
|
||||
{
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/QuantumNous/new-api/controller"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// registerAuthzRoutes mounts the authorization API under its own /authz
|
||||
// namespace. GET /authz/catalog returns the permission schema (resources,
|
||||
// actions, and role baselines) used by the client permission editor.
|
||||
func registerAuthzRoutes(apiRouter *gin.RouterGroup) {
|
||||
authzRoute := apiRouter.Group("/authz")
|
||||
authzRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
authzRoute.GET("/catalog", controller.GetPermissionCatalog)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/QuantumNous/new-api/controller"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
"github.com/QuantumNous/new-api/service/authz"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type permissionRoute struct {
|
||||
method string
|
||||
path string
|
||||
permission authz.Permission
|
||||
handler gin.HandlerFunc
|
||||
}
|
||||
|
||||
func registerChannelRoutes(apiRouter *gin.RouterGroup) {
|
||||
channelRoute := apiRouter.Group("/channel")
|
||||
channelRoute.Use(middleware.AdminAuth())
|
||||
|
||||
channelRoute.POST("/:id/key",
|
||||
middleware.RootAuth(),
|
||||
middleware.CriticalRateLimit(),
|
||||
middleware.DisableCache(),
|
||||
middleware.SecureVerificationRequired(),
|
||||
controller.GetChannelKey,
|
||||
)
|
||||
|
||||
for _, route := range channelPermissionRoutes {
|
||||
channelRoute.Handle(route.method, route.path,
|
||||
middleware.RequirePermission(route.permission),
|
||||
route.handler,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var channelPermissionRoutes = []permissionRoute{
|
||||
{method: http.MethodGet, path: "/", permission: authz.ChannelRead, handler: controller.GetAllChannels},
|
||||
{method: http.MethodGet, path: "/search", permission: authz.ChannelRead, handler: controller.SearchChannels},
|
||||
{method: http.MethodGet, path: "/models", permission: authz.ChannelRead, handler: controller.ChannelListModels},
|
||||
{method: http.MethodGet, path: "/models_enabled", permission: authz.ChannelRead, handler: controller.EnabledListModels},
|
||||
{method: http.MethodGet, path: "/ops", permission: authz.ChannelRead, handler: controller.GetChannelOps},
|
||||
{method: http.MethodGet, path: "/:id", permission: authz.ChannelRead, handler: controller.GetChannel},
|
||||
{method: http.MethodGet, path: "/test", permission: authz.ChannelOperate, handler: controller.TestAllChannels},
|
||||
{method: http.MethodGet, path: "/test/:id", permission: authz.ChannelOperate, handler: controller.TestChannel},
|
||||
{method: http.MethodGet, path: "/update_balance", permission: authz.ChannelOperate, handler: controller.UpdateAllChannelsBalance},
|
||||
{method: http.MethodGet, path: "/update_balance/:id", permission: authz.ChannelOperate, handler: controller.UpdateChannelBalance},
|
||||
{method: http.MethodPost, path: "/", permission: authz.ChannelSensitiveWrite, handler: controller.AddChannel},
|
||||
{method: http.MethodPut, path: "/", permission: authz.ChannelWrite, handler: controller.UpdateChannel},
|
||||
{method: http.MethodPost, path: "/status/batch", permission: authz.ChannelOperate, handler: controller.BatchUpdateChannelStatus},
|
||||
{method: http.MethodPost, path: "/:id/status", permission: authz.ChannelOperate, handler: controller.UpdateChannelStatus},
|
||||
{method: http.MethodDelete, path: "/disabled", permission: authz.ChannelSensitiveWrite, handler: controller.DeleteDisabledChannel},
|
||||
{method: http.MethodPost, path: "/tag/disabled", permission: authz.ChannelOperate, handler: controller.DisableTagChannels},
|
||||
{method: http.MethodPost, path: "/tag/enabled", permission: authz.ChannelOperate, handler: controller.EnableTagChannels},
|
||||
{method: http.MethodPut, path: "/tag", permission: authz.ChannelWrite, handler: controller.EditTagChannels},
|
||||
{method: http.MethodDelete, path: "/:id", permission: authz.ChannelSensitiveWrite, handler: controller.DeleteChannel},
|
||||
{method: http.MethodPost, path: "/batch", permission: authz.ChannelSensitiveWrite, handler: controller.DeleteChannelBatch},
|
||||
{method: http.MethodPost, path: "/fix", permission: authz.ChannelOperate, handler: controller.FixChannelsAbilities},
|
||||
{method: http.MethodGet, path: "/fetch_models/:id", permission: authz.ChannelOperate, handler: controller.FetchUpstreamModels},
|
||||
{method: http.MethodPost, path: "/fetch_models", permission: authz.ChannelSensitiveWrite, handler: controller.FetchModels},
|
||||
{method: http.MethodPost, path: "/:id/codex/refresh", permission: authz.ChannelSensitiveWrite, handler: controller.RefreshCodexChannelCredential},
|
||||
{method: http.MethodGet, path: "/:id/codex/usage", permission: authz.ChannelRead, handler: controller.GetCodexChannelUsage},
|
||||
{method: http.MethodGet, path: "/:id/codex/usage/reset-credits", permission: authz.ChannelRead, handler: controller.GetCodexChannelRateLimitResetCredits},
|
||||
{method: http.MethodPost, path: "/:id/codex/usage/reset", permission: authz.ChannelOperate, handler: controller.ResetCodexChannelUsage},
|
||||
{method: http.MethodPost, path: "/ollama/pull", permission: authz.ChannelSensitiveWrite, handler: controller.OllamaPullModel},
|
||||
{method: http.MethodPost, path: "/ollama/pull/stream", permission: authz.ChannelSensitiveWrite, handler: controller.OllamaPullModelStream},
|
||||
{method: http.MethodDelete, path: "/ollama/delete", permission: authz.ChannelSensitiveWrite, handler: controller.OllamaDeleteModel},
|
||||
{method: http.MethodGet, path: "/ollama/version/:id", permission: authz.ChannelSensitiveWrite, handler: controller.OllamaVersion},
|
||||
{method: http.MethodPost, path: "/batch/tag", permission: authz.ChannelWrite, handler: controller.BatchSetChannelTag},
|
||||
{method: http.MethodGet, path: "/tag/models", permission: authz.ChannelRead, handler: controller.GetTagModels},
|
||||
{method: http.MethodPost, path: "/copy/:id", permission: authz.ChannelSensitiveWrite, handler: controller.CopyChannel},
|
||||
{method: http.MethodPost, path: "/multi_key/manage", permission: authz.ChannelOperate, handler: controller.ManageMultiKeys},
|
||||
{method: http.MethodPost, path: "/upstream_updates/apply", permission: authz.ChannelWrite, handler: controller.ApplyChannelUpstreamModelUpdates},
|
||||
{method: http.MethodPost, path: "/upstream_updates/apply_all", permission: authz.ChannelWrite, handler: controller.ApplyAllChannelUpstreamModelUpdates},
|
||||
{method: http.MethodPost, path: "/upstream_updates/detect", permission: authz.ChannelOperate, handler: controller.DetectChannelUpstreamModelUpdates},
|
||||
{method: http.MethodPost, path: "/upstream_updates/detect_all", permission: authz.ChannelOperate, handler: controller.DetectAllChannelUpstreamModelUpdates},
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/controller"
|
||||
"github.com/QuantumNous/new-api/service/authz"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestChannelStatusRoutesUseOperatePermission(t *testing.T) {
|
||||
assertChannelRoutePermission(t, http.MethodPost, "/:id/status", authz.ChannelOperate, controller.UpdateChannelStatus)
|
||||
assertChannelRoutePermission(t, http.MethodPost, "/status/batch", authz.ChannelOperate, controller.BatchUpdateChannelStatus)
|
||||
assertChannelRoutePermission(t, http.MethodPut, "/", authz.ChannelWrite, controller.UpdateChannel)
|
||||
}
|
||||
|
||||
func TestChannelDeleteRoutesUseSensitiveWritePermission(t *testing.T) {
|
||||
assertChannelRoutePermission(t, http.MethodDelete, "/:id", authz.ChannelSensitiveWrite, controller.DeleteChannel)
|
||||
assertChannelRoutePermission(t, http.MethodPost, "/batch", authz.ChannelSensitiveWrite, controller.DeleteChannelBatch)
|
||||
assertChannelRoutePermission(t, http.MethodDelete, "/disabled", authz.ChannelSensitiveWrite, controller.DeleteDisabledChannel)
|
||||
assertChannelRoutePermission(t, http.MethodPut, "/", authz.ChannelWrite, controller.UpdateChannel)
|
||||
assertChannelRoutePermission(t, http.MethodPut, "/tag", authz.ChannelWrite, controller.EditTagChannels)
|
||||
assertChannelRoutePermission(t, http.MethodPost, "/batch/tag", authz.ChannelWrite, controller.BatchSetChannelTag)
|
||||
}
|
||||
|
||||
func TestChannelStatusRoutesRegisterWithoutConflict(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
api := engine.Group("/api")
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
registerChannelRoutes(api)
|
||||
})
|
||||
}
|
||||
|
||||
func assertChannelRoutePermission(t *testing.T, method string, path string, permission authz.Permission, handler any) {
|
||||
t.Helper()
|
||||
for _, route := range channelPermissionRoutes {
|
||||
if route.method == method && route.path == path {
|
||||
assert.Equal(t, permission, route.permission)
|
||||
assert.Equal(t, reflect.ValueOf(handler).Pointer(), reflect.ValueOf(route.handler).Pointer())
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("route %s %s not found", method, path)
|
||||
}
|
||||
Reference in New Issue
Block a user