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:
+106
-4
@@ -12,11 +12,13 @@ import (
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/i18n"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
relaychannel "github.com/QuantumNous/new-api/relay/channel"
|
||||
"github.com/QuantumNous/new-api/relay/channel/gemini"
|
||||
"github.com/QuantumNous/new-api/relay/channel/ollama"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/service/authz"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -820,6 +822,11 @@ func EditTagChannels(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
if (channelTag.ParamOverride != nil || channelTag.HeaderOverride != nil) &&
|
||||
!authz.Can(c.GetInt("id"), c.GetInt("role"), authz.ChannelSensitiveWrite) {
|
||||
common.ApiErrorI18n(c, i18n.MsgAuthInsufficientPrivilege)
|
||||
return
|
||||
}
|
||||
if channelTag.ParamOverride != nil {
|
||||
trimmed := strings.TrimSpace(*channelTag.ParamOverride)
|
||||
if trimmed != "" && !json.Valid([]byte(trimmed)) {
|
||||
@@ -896,13 +903,36 @@ type PatchChannel struct {
|
||||
KeyMode *string `json:"key_mode"` // 多key模式下密钥覆盖或者追加
|
||||
}
|
||||
|
||||
type ChannelStatusRequest struct {
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
type ChannelStatusBatchRequest struct {
|
||||
Ids []int `json:"ids"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
func UpdateChannel(c *gin.Context) {
|
||||
channel := PatchChannel{}
|
||||
err := c.ShouldBindJSON(&channel)
|
||||
rawBody, err := c.GetRawData()
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if err := common.Unmarshal(rawBody, &channel); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
var requestData map[string]any
|
||||
if err := common.Unmarshal(rawBody, &requestData); err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
if _, ok := requestData["status"]; ok {
|
||||
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
|
||||
return
|
||||
}
|
||||
clearChannelReadOnlyFields(&channel, requestData)
|
||||
|
||||
// 使用统一的校验函数
|
||||
if err := validateChannel(&channel.Channel, false); err != nil {
|
||||
@@ -925,6 +955,12 @@ func UpdateChannel(c *gin.Context) {
|
||||
// Always copy the original ChannelInfo so that fields like IsMultiKey and MultiKeySize are retained.
|
||||
channel.ChannelInfo = originChannel.ChannelInfo
|
||||
|
||||
if channelHasSensitiveChanges(&channel, originChannel, requestData) &&
|
||||
!authz.Can(c.GetInt("id"), c.GetInt("role"), authz.ChannelSensitiveWrite) {
|
||||
common.ApiErrorI18n(c, i18n.MsgAuthInsufficientPrivilege)
|
||||
return
|
||||
}
|
||||
|
||||
// If the request explicitly specifies a new MultiKeyMode, apply it on top of the original info.
|
||||
if channel.MultiKeyMode != nil && *channel.MultiKeyMode != "" {
|
||||
channel.ChannelInfo.MultiKeyMode = constant.MultiKeyMode(*channel.MultiKeyMode)
|
||||
@@ -1019,9 +1055,6 @@ func UpdateChannel(c *gin.Context) {
|
||||
service.ResetProxyClientCache()
|
||||
// 记录变更的字段名(语言无关的字段标识),密钥仅记录"已更换"绝不记录内容。
|
||||
changedFields := make([]string, 0)
|
||||
if channel.Status != originChannel.Status {
|
||||
changedFields = append(changedFields, "status")
|
||||
}
|
||||
if channel.Models != originChannel.Models {
|
||||
changedFields = append(changedFields, "models")
|
||||
}
|
||||
@@ -1052,6 +1085,66 @@ func UpdateChannel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
func UpdateChannelStatus(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
|
||||
return
|
||||
}
|
||||
req := ChannelStatusRequest{}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || !isManageableChannelStatus(req.Status) {
|
||||
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
|
||||
return
|
||||
}
|
||||
changed := model.UpdateChannelStatus(id, "", req.Status, "manual operation")
|
||||
if changed {
|
||||
model.InitChannelCache()
|
||||
service.ResetProxyClientCache()
|
||||
}
|
||||
recordManageAudit(c, "channel.status_update", map[string]interface{}{
|
||||
"id": id,
|
||||
"status": req.Status,
|
||||
"changed": changed,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": changed,
|
||||
})
|
||||
}
|
||||
|
||||
func BatchUpdateChannelStatus(c *gin.Context) {
|
||||
req := ChannelStatusBatchRequest{}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || len(req.Ids) == 0 || !isManageableChannelStatus(req.Status) {
|
||||
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
|
||||
return
|
||||
}
|
||||
changedCount := 0
|
||||
for _, id := range req.Ids {
|
||||
if model.UpdateChannelStatus(id, "", req.Status, "manual batch operation") {
|
||||
changedCount++
|
||||
}
|
||||
}
|
||||
if changedCount > 0 {
|
||||
model.InitChannelCache()
|
||||
service.ResetProxyClientCache()
|
||||
}
|
||||
recordManageAudit(c, "channel.status_update_batch", map[string]interface{}{
|
||||
"count": changedCount,
|
||||
"total": len(req.Ids),
|
||||
"status": req.Status,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": changedCount,
|
||||
})
|
||||
}
|
||||
|
||||
func isManageableChannelStatus(status int) bool {
|
||||
return status == common.ChannelStatusEnabled || status == common.ChannelStatusManuallyDisabled
|
||||
}
|
||||
|
||||
// equalStringPtr 比较两个 *string 是否相等(均为 nil 视为相等)。
|
||||
func equalStringPtr(a, b *string) bool {
|
||||
if a == nil && b == nil {
|
||||
@@ -1364,6 +1457,11 @@ func ManageMultiKeys(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
if multiKeyActionRequiresSensitiveWrite(request.Action) &&
|
||||
!authz.Can(c.GetInt("id"), c.GetInt("role"), authz.ChannelSensitiveWrite) {
|
||||
common.ApiErrorI18n(c, i18n.MsgAuthInsufficientPrivilege)
|
||||
return
|
||||
}
|
||||
|
||||
// get_key_status 为只读查询,不记录审计;其余为修改操作,记录审计并跳过中间件兜底。
|
||||
if request.Action == "get_key_status" {
|
||||
@@ -1808,6 +1906,10 @@ func ManageMultiKeys(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func multiKeyActionRequiresSensitiveWrite(action string) bool {
|
||||
return action == "delete_key" || action == "delete_disabled_keys"
|
||||
}
|
||||
|
||||
// OllamaPullModel 拉取 Ollama 模型
|
||||
func OllamaPullModel(c *gin.Context) {
|
||||
var req struct {
|
||||
|
||||
Reference in New Issue
Block a user