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:
Calcium-Ion
2026-06-27 17:01:59 +08:00
committed by GitHub
parent 6c35e1ef26
commit 4aee5f7d5a
52 changed files with 2778 additions and 255 deletions
+121
View File
@@ -0,0 +1,121 @@
package authz
import (
"strings"
"github.com/QuantumNous/new-api/model"
casbinmodel "github.com/casbin/casbin/v2/model"
"github.com/casbin/casbin/v2/persist"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type gormAdapter struct {
db *gorm.DB
}
func newGormAdapter(db *gorm.DB) *gormAdapter {
return &gormAdapter{db: db}
}
func (a *gormAdapter) LoadPolicy(m casbinmodel.Model) error {
var rules []model.CasbinRule
if err := a.db.Order("id asc").Find(&rules).Error; err != nil {
return err
}
for _, rule := range rules {
if err := persist.LoadPolicyLine(ruleToLine(rule), m); err != nil {
return err
}
}
return nil
}
func (a *gormAdapter) SavePolicy(m casbinmodel.Model) error {
return a.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("1 = 1").Delete(&model.CasbinRule{}).Error; err != nil {
return err
}
rules := make([]model.CasbinRule, 0)
for ptype, ast := range m["p"] {
for _, policy := range ast.Policy {
rules = append(rules, newRule(ptype, policy))
}
}
for ptype, ast := range m["g"] {
for _, policy := range ast.Policy {
rules = append(rules, newRule(ptype, policy))
}
}
if len(rules) == 0 {
return nil
}
return tx.Create(&rules).Error
})
}
func (a *gormAdapter) AddPolicy(_ string, ptype string, rule []string) error {
casbinRule := newRule(ptype, rule)
var count int64
if err := a.ruleQuery(a.db.Model(&model.CasbinRule{}), ptype, rule).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
return a.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&casbinRule).Error
}
func (a *gormAdapter) RemovePolicy(_ string, ptype string, rule []string) error {
return a.ruleQuery(a.db, ptype, rule).Delete(&model.CasbinRule{}).Error
}
func (a *gormAdapter) RemoveFilteredPolicy(_ string, ptype string, fieldIndex int, fieldValues ...string) error {
query := a.db.Where("ptype = ?", ptype)
for i, value := range fieldValues {
if value == "" {
continue
}
query = query.Where("v"+string(rune('0'+fieldIndex+i))+" = ?", value)
}
return query.Delete(&model.CasbinRule{}).Error
}
func (a *gormAdapter) ruleQuery(query *gorm.DB, ptype string, rule []string) *gorm.DB {
query = query.Where("ptype = ?", ptype)
for idx := 0; idx < 6; idx++ {
value := ""
if idx < len(rule) {
value = rule[idx]
}
query = query.Where("v"+string(rune('0'+idx))+" = ?", value)
}
return query
}
func newRule(ptype string, policy []string) model.CasbinRule {
rule := model.CasbinRule{Ptype: ptype}
values := []*string{&rule.V0, &rule.V1, &rule.V2, &rule.V3, &rule.V4, &rule.V5}
for idx, value := range policy {
if idx >= len(values) {
break
}
*values[idx] = value
}
return rule
}
func ruleToLine(rule model.CasbinRule) string {
parts := []string{rule.Ptype}
values := []string{rule.V0, rule.V1, rule.V2, rule.V3, rule.V4, rule.V5}
if rule.Ptype == "p" && rule.V0 != "" && rule.V1 != "" && rule.V2 != "" && rule.V3 == "" {
values[3] = EffectAllow
}
for _, value := range values {
if value == "" {
continue
}
parts = append(parts, value)
}
return strings.Join(parts, ", ")
}
+20
View File
@@ -0,0 +1,20 @@
package authz
import "github.com/QuantumNous/new-api/common"
// resolveSubjectRoles returns the role keys assigned to a subject. The mapping
// is derived from the caller's system role.
var resolveSubjectRoles = func(userID int, systemRole int) []string {
switch {
case systemRole >= common.RoleRootUser:
return []string{BuiltInRoleRoot}
case systemRole >= common.RoleAdminUser:
return []string{BuiltInRoleAdmin}
default:
return nil
}
}
// managedRoleKey is the role whose baseline per-user overrides are expressed
// relative to.
const managedRoleKey = BuiltInRoleAdmin
+229
View File
@@ -0,0 +1,229 @@
package authz
import (
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func newAuthzTestDB(t *testing.T) *gorm.DB {
t.Helper()
wasMaster := common.IsMasterNode
common.IsMasterNode = true
t.Cleanup(func() {
common.IsMasterNode = wasMaster
})
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
sqlDB, err := db.DB()
require.NoError(t, err)
sqlDB.SetMaxOpenConns(1)
require.NoError(t, db.AutoMigrate(&model.CasbinRule{}, &model.AuthzRole{}))
return db
}
func TestInitSeedsBuiltInRolesAndPoliciesOnce(t *testing.T) {
db := newAuthzTestDB(t)
require.NoError(t, Init(db))
require.NoError(t, Init(db))
// root is a superuser role and is granted everything implicitly, so only the
// admin baseline is written as explicit policy rows.
var count int64
require.NoError(t, db.Model(&model.CasbinRule{}).Count(&count).Error)
assert.Equal(t, int64(len(PermissionsForRole(BuiltInRoleAdmin))), count)
var roles []model.AuthzRole
require.NoError(t, db.Order("sort asc").Find(&roles).Error)
require.Len(t, roles, 2)
assert.Equal(t, BuiltInRoleRoot, roles[0].Key)
assert.Equal(t, BuiltInRoleAdmin, roles[1].Key)
assert.True(t, Can(1, common.RoleRootUser, ChannelSensitiveWrite))
assert.True(t, Can(2, common.RoleAdminUser, ChannelRead))
assert.True(t, Can(2, common.RoleAdminUser, ChannelOperate))
assert.True(t, Can(2, common.RoleAdminUser, ChannelWrite))
assert.False(t, Can(2, common.RoleAdminUser, ChannelSensitiveWrite))
assert.False(t, Can(3, common.RoleCommonUser, ChannelRead))
}
func TestInitOnSlaveOnlyLoadsPolicies(t *testing.T) {
wasMaster := common.IsMasterNode
common.IsMasterNode = false
t.Cleanup(func() {
common.IsMasterNode = wasMaster
})
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
sqlDB, err := db.DB()
require.NoError(t, err)
sqlDB.SetMaxOpenConns(1)
require.NoError(t, db.AutoMigrate(&model.CasbinRule{}, &model.AuthzRole{}))
require.NoError(t, Init(db))
var roleCount int64
require.NoError(t, db.Model(&model.AuthzRole{}).Count(&roleCount).Error)
assert.Equal(t, int64(0), roleCount)
var policyCount int64
require.NoError(t, db.Model(&model.CasbinRule{}).Count(&policyCount).Error)
assert.Equal(t, int64(0), policyCount)
assert.False(t, Can(2, common.RoleAdminUser, ChannelRead))
}
func TestSetUserPermissionsStoresOnlyOverrides(t *testing.T) {
db := newAuthzTestDB(t)
require.NoError(t, Init(db))
require.NoError(t, SetUserPermissions(42, PermissionsMap{
ResourceChannel: {
ActionRead: true,
ActionOperate: true,
ActionWrite: false,
ActionSensitiveWrite: true,
ActionSecretView: false,
"unknown": true,
},
"unknown": {
ActionRead: true,
},
}))
assert.True(t, Can(42, common.RoleAdminUser, ChannelSensitiveWrite))
assert.False(t, Can(42, common.RoleAdminUser, ChannelWrite))
assert.Equal(t, PermissionsMap{
ResourceChannel: {
ActionRead: true,
ActionOperate: true,
ActionWrite: false,
ActionSensitiveWrite: true,
ActionSecretView: false,
},
}, ExplicitUserPermissions(42))
assert.Equal(t, PermissionsMap{
ResourceChannel: {
ActionSensitiveWrite: true,
ActionWrite: false,
},
}, ExplicitUserOverrides(42))
var userPolicyCount int64
require.NoError(t, db.Model(&model.CasbinRule{}).Where("v0 = ?", UserSubject(42)).Count(&userPolicyCount).Error)
assert.Equal(t, int64(2), userPolicyCount)
require.NoError(t, SetUserPermissions(42, PermissionsMap{ResourceChannel: {
ActionRead: true,
ActionOperate: true,
ActionWrite: true,
ActionSensitiveWrite: false,
ActionSecretView: false,
}}))
assert.False(t, Can(42, common.RoleAdminUser, ChannelSensitiveWrite))
assert.Equal(t, PermissionsMap{
ResourceChannel: {
ActionRead: true,
ActionOperate: true,
ActionWrite: true,
ActionSensitiveWrite: false,
ActionSecretView: false,
},
}, ExplicitUserPermissions(42))
assert.Empty(t, ExplicitUserOverrides(42))
}
func TestClearUserAuthorizationRemovesOverrides(t *testing.T) {
db := newAuthzTestDB(t)
require.NoError(t, Init(db))
require.NoError(t, SetUserPermissions(90, PermissionsMap{ResourceChannel: {
ActionWrite: false,
ActionSensitiveWrite: true,
}}))
assert.True(t, Can(90, common.RoleAdminUser, ChannelSensitiveWrite))
assert.False(t, Can(90, common.RoleAdminUser, ChannelWrite))
require.NoError(t, ClearUserAuthorization(90))
assert.Empty(t, ExplicitUserOverrides(90))
assert.True(t, Can(90, common.RoleAdminUser, ChannelRead))
assert.True(t, Can(90, common.RoleAdminUser, ChannelWrite))
assert.False(t, Can(90, common.RoleAdminUser, ChannelSensitiveWrite))
assert.False(t, Can(90, common.RoleCommonUser, ChannelRead))
}
func TestSetUserPermissionsInTxDoesNotMutateEnforcerBeforeReload(t *testing.T) {
db := newAuthzTestDB(t)
require.NoError(t, Init(db))
require.NoError(t, db.Transaction(func(tx *gorm.DB) error {
return SetUserPermissionsInTx(tx, 42, PermissionsMap{ResourceChannel: {
ActionRead: true,
ActionOperate: true,
ActionWrite: true,
ActionSensitiveWrite: true,
ActionSecretView: false,
}})
}))
assert.False(t, Can(42, common.RoleAdminUser, ChannelSensitiveWrite))
require.NoError(t, ReloadPolicy())
assert.True(t, Can(42, common.RoleAdminUser, ChannelSensitiveWrite))
}
func TestSetUserPermissionsInTxRollbackLeavesNoPolicy(t *testing.T) {
db := newAuthzTestDB(t)
require.NoError(t, Init(db))
tx := db.Begin()
require.NoError(t, tx.Error)
require.NoError(t, SetUserPermissionsInTx(tx, 43, PermissionsMap{ResourceChannel: {
ActionSensitiveWrite: true,
}}))
require.NoError(t, tx.Rollback().Error)
require.NoError(t, ReloadPolicy())
assert.False(t, Can(43, common.RoleAdminUser, ChannelSensitiveWrite))
var count int64
require.NoError(t, db.Model(&model.CasbinRule{}).Where("v0 = ?", UserSubject(43)).Count(&count).Error)
assert.Equal(t, int64(0), count)
}
func TestAdapterAddPolicyIsIdempotent(t *testing.T) {
db := newAuthzTestDB(t)
adapter := newGormAdapter(db)
rule := []string{UserSubject(55), ResourceChannel, ActionSensitiveWrite, EffectAllow}
require.NoError(t, adapter.AddPolicy("p", "p", rule))
require.NoError(t, adapter.AddPolicy("p", "p", rule))
var count int64
require.NoError(t, db.Model(&model.CasbinRule{}).Where(
"ptype = ? AND v0 = ? AND v1 = ? AND v2 = ? AND v3 = ?",
"p",
UserSubject(55),
ResourceChannel,
ActionSensitiveWrite,
EffectAllow,
).Count(&count).Error)
assert.Equal(t, int64(1), count)
}
func TestCapabilitiesUseCatalogShape(t *testing.T) {
db := newAuthzTestDB(t)
require.NoError(t, Init(db))
capabilities := Capabilities(7, common.RoleAdminUser)
assert.True(t, capabilities[ResourceChannel][ActionRead])
assert.True(t, capabilities[ResourceChannel][ActionOperate])
assert.True(t, capabilities[ResourceChannel][ActionWrite])
assert.False(t, capabilities[ResourceChannel][ActionSensitiveWrite])
assert.False(t, capabilities[ResourceChannel][ActionSecretView])
}
+94
View File
@@ -0,0 +1,94 @@
package authz
import (
"fmt"
"sync"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/casbin/casbin/v2"
casbinmodel "github.com/casbin/casbin/v2/model"
"gorm.io/gorm"
)
var (
enforcerMu sync.RWMutex
enforcer *casbin.SyncedEnforcer
)
const modelText = `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act, eft
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act && p.eft == "allow"
`
func Init(db *gorm.DB) error {
if common.IsMasterNode {
if err := seedBuiltInRoles(db); err != nil {
return err
}
if err := resetBuiltInRolePolicies(db); err != nil {
return err
}
}
m, err := casbinmodel.NewModelFromString(modelText)
if err != nil {
return err
}
e, err := casbin.NewSyncedEnforcer(m, newGormAdapter(db))
if err != nil {
return err
}
e.EnableAutoSave(true)
enforcerMu.Lock()
enforcer = e
enforcerMu.Unlock()
if !common.IsMasterNode {
return nil
}
return seedDefaultPolicies()
}
func currentEnforcer() *casbin.SyncedEnforcer {
enforcerMu.RLock()
defer enforcerMu.RUnlock()
return enforcer
}
func ReloadPolicy() error {
enforcerMu.Lock()
defer enforcerMu.Unlock()
if enforcer == nil {
return fmt.Errorf("authz enforcer is not initialized")
}
return enforcer.LoadPolicy()
}
// StartPolicySync periodically reloads the authorization policy from the database.
// The enforcer keeps an in-memory snapshot, and permission changes are written
// straight to the DB (see SetUserPermissionsInTx) with only the local node's
// snapshot refreshed afterwards. Without this loop other instances in a
// multi-node deployment would keep serving stale permissions (including not
// honoring a revoked grant) until restart. Mirrors model.SyncOptions polling.
func StartPolicySync(frequency int) {
if frequency <= 0 {
return
}
for {
time.Sleep(time.Duration(frequency) * time.Second)
if err := ReloadPolicy(); err != nil {
common.SysError("failed to reload authz policy: " + err.Error())
}
}
}
+163
View File
@@ -0,0 +1,163 @@
package authz
import (
"fmt"
"sort"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/casbin/casbin/v2"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type overridePolicy struct {
Resource string
Action string
Effect string
}
func SetUserPermissions(userID int, permissions PermissionsMap) error {
e := currentEnforcer()
if e == nil {
return fmt.Errorf("authz enforcer is not initialized")
}
for resource, actions := range permissions {
if !isKnownResource(resource) {
continue
}
if _, err := e.RemoveFilteredPolicy(0, UserSubject(userID), resource); err != nil {
return err
}
for _, policy := range userOverridePolicies(e, resource, actions) {
if _, err := e.AddPolicy(UserSubject(userID), policy.Resource, policy.Action, policy.Effect); err != nil {
return err
}
}
}
return nil
}
func SetUserPermissionsInTx(tx *gorm.DB, userID int, permissions PermissionsMap) error {
e := currentEnforcer()
if e == nil {
return fmt.Errorf("authz enforcer is not initialized")
}
for resource, actions := range permissions {
if !isKnownResource(resource) {
continue
}
if err := tx.Where("ptype = ? AND v0 = ? AND v1 = ?", "p", UserSubject(userID), resource).Delete(&model.CasbinRule{}).Error; err != nil {
return err
}
policies := userOverridePolicies(e, resource, actions)
if len(policies) == 0 {
continue
}
rules := make([]model.CasbinRule, 0, len(policies))
for _, policy := range policies {
rules = append(rules, newRule("p", []string{UserSubject(userID), policy.Resource, policy.Action, policy.Effect}))
}
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&rules).Error; err != nil {
return err
}
}
return nil
}
func ClearUserPermissions(userID int) error {
e := currentEnforcer()
if e == nil {
return fmt.Errorf("authz enforcer is not initialized")
}
for _, resource := range registry {
if _, err := e.RemoveFilteredPolicy(0, UserSubject(userID), resource.Resource); err != nil {
return err
}
}
return nil
}
func ClearUserPermissionsInTx(tx *gorm.DB, userID int) error {
for _, resource := range registry {
if err := tx.Where("ptype = ? AND v0 = ? AND v1 = ?", "p", UserSubject(userID), resource.Resource).Delete(&model.CasbinRule{}).Error; err != nil {
return err
}
}
return nil
}
func ClearUserAuthorization(userID int) error {
return ClearUserPermissions(userID)
}
func ClearUserAuthorizationInTx(tx *gorm.DB, userID int) error {
return ClearUserPermissionsInTx(tx, userID)
}
// ExplicitUserPermissions returns the effective permission matrix for the
// managed role plus any per-user overrides.
func ExplicitUserPermissions(userID int) PermissionsMap {
return Capabilities(userID, common.RoleAdminUser)
}
// ExplicitUserOverrides returns only the per-user override entries.
func ExplicitUserOverrides(userID int) PermissionsMap {
e := currentEnforcer()
if e == nil {
return PermissionsMap{}
}
result := PermissionsMap{}
for _, resource := range registry {
policies, err := e.GetFilteredPolicy(0, UserSubject(userID), resource.Resource)
if err != nil {
return PermissionsMap{}
}
actions := make(map[string]bool, len(policies))
for _, policy := range policies {
if len(policy) >= 3 && isKnownPermission(Permission{Resource: policy[1], Action: policy[2]}) {
effect := policyEffect(policy)
if effect == EffectAllow || effect == EffectDeny {
actions[policy[2]] = effect == EffectAllow
}
}
}
if len(actions) > 0 {
result[resource.Resource] = actions
}
}
return result
}
// userOverridePolicies returns the override entries that differ from the managed
// role baseline; entries matching the baseline are omitted.
func userOverridePolicies(e *casbin.SyncedEnforcer, resource string, actions map[string]bool) []overridePolicy {
overrides := make([]overridePolicy, 0, len(actions))
for _, action := range catalogActions(resource) {
desired, ok := actions[action.Action]
if !ok {
continue
}
permission := Permission{Resource: resource, Action: action.Action}
if desired == roleBaselineAllows(e, managedRoleKey, permission) {
continue
}
effect := EffectDeny
if desired {
effect = EffectAllow
}
overrides = append(overrides, overridePolicy{
Resource: resource,
Action: action.Action,
Effect: effect,
})
}
sort.Slice(overrides, func(i, j int) bool {
return overrides[i].Action < overrides[j].Action
})
return overrides
}
+27
View File
@@ -0,0 +1,27 @@
package authz
import "strconv"
// Permission identifies a single action on a resource.
type Permission struct {
Resource string
Action string
}
// PermissionsMap is a resource -> action -> allowed lookup.
type PermissionsMap map[string]map[string]bool
const (
EffectAllow = "allow"
EffectDeny = "deny"
)
// UserSubject is the casbin subject string for a single user.
func UserSubject(userID int) string {
return "user:" + strconv.Itoa(userID)
}
// RoleSubject is the casbin subject string for a role.
func RoleSubject(roleKey string) string {
return "role:" + roleKey
}
+108
View File
@@ -0,0 +1,108 @@
package authz
// ActionDefinition describes a single action exposed by a resource. DefaultRoles
// lists the role keys that receive this action as part of their baseline grants.
type ActionDefinition struct {
Action string `json:"action"`
LabelKey string `json:"label_key"`
DescriptionKey string `json:"description_key"`
DefaultRoles []string `json:"-"`
}
// ResourceDefinition describes a resource and the actions it exposes.
type ResourceDefinition struct {
Resource string `json:"resource"`
LabelKey string `json:"label_key"`
Actions []ActionDefinition `json:"actions"`
}
var registry []ResourceDefinition
// RegisterResource adds a resource definition to the permission registry.
func RegisterResource(resource ResourceDefinition) {
registry = append(registry, resource)
}
// Catalog returns a copy of the registered resource definitions.
func Catalog() []ResourceDefinition {
result := make([]ResourceDefinition, 0, len(registry))
for _, resource := range registry {
result = append(result, ResourceDefinition{
Resource: resource.Resource,
LabelKey: resource.LabelKey,
Actions: append([]ActionDefinition(nil), resource.Actions...),
})
}
return result
}
// AllPermissions returns every registered permission.
func AllPermissions() []Permission {
permissions := make([]Permission, 0)
for _, resource := range registry {
for _, action := range resource.Actions {
permissions = append(permissions, Permission{
Resource: resource.Resource,
Action: action.Action,
})
}
}
return permissions
}
// PermissionsForRole returns the permissions whose DefaultRoles include roleKey.
func PermissionsForRole(roleKey string) []Permission {
permissions := make([]Permission, 0)
for _, resource := range registry {
for _, action := range resource.Actions {
if actionHasRole(action, roleKey) {
permissions = append(permissions, Permission{
Resource: resource.Resource,
Action: action.Action,
})
}
}
}
return permissions
}
func actionHasRole(action ActionDefinition, roleKey string) bool {
for _, r := range action.DefaultRoles {
if r == roleKey {
return true
}
}
return false
}
func isKnownResource(resource string) bool {
for _, known := range registry {
if known.Resource == resource {
return true
}
}
return false
}
func catalogActions(resource string) []ActionDefinition {
for _, known := range registry {
if known.Resource == resource {
return known.Actions
}
}
return nil
}
func isKnownPermission(permission Permission) bool {
for _, resource := range registry {
if resource.Resource != permission.Resource {
continue
}
for _, action := range resource.Actions {
if action.Action == permission.Action {
return true
}
}
}
return false
}
+83
View File
@@ -0,0 +1,83 @@
package authz
import "github.com/casbin/casbin/v2"
// Can reports whether the subject may perform the permission. A superuser role
// short-circuits to allow. Otherwise a per-user override wins, then the union of
// the subject's role baselines applies.
func Can(userID int, systemRole int, permission Permission) bool {
roles := resolveSubjectRoles(userID, systemRole)
if len(roles) == 0 {
return false
}
for _, role := range roles {
if isSuperuserRole(role) {
return true
}
}
if !isKnownPermission(permission) {
return false
}
e := currentEnforcer()
if e == nil {
return false
}
if effect, ok := explicitSubjectEffect(e, UserSubject(userID), permission); ok {
return effect == EffectAllow
}
for _, role := range roles {
if roleBaselineAllows(e, role, permission) {
return true
}
}
return false
}
// Capabilities returns the full resource/action matrix the subject is allowed.
func Capabilities(userID int, systemRole int) PermissionsMap {
result := make(PermissionsMap, len(registry))
for _, resource := range registry {
actions := make(map[string]bool, len(resource.Actions))
for _, action := range resource.Actions {
actions[action.Action] = Can(userID, systemRole, Permission{
Resource: resource.Resource,
Action: action.Action,
})
}
result[resource.Resource] = actions
}
return result
}
func roleBaselineAllows(e *casbin.SyncedEnforcer, roleKey string, permission Permission) bool {
effect, ok := explicitSubjectEffect(e, RoleSubject(roleKey), permission)
return ok && effect == EffectAllow
}
func explicitSubjectEffect(e *casbin.SyncedEnforcer, subject string, permission Permission) (string, bool) {
policies, err := e.GetFilteredPolicy(0, subject, permission.Resource, permission.Action)
if err != nil {
return "", false
}
hasAllow := false
for _, policy := range policies {
switch policyEffect(policy) {
case EffectDeny:
return EffectDeny, true
case EffectAllow:
hasAllow = true
}
}
if hasAllow {
return EffectAllow, true
}
return "", false
}
func policyEffect(policy []string) string {
if len(policy) < 4 || policy[3] == "" {
return EffectAllow
}
return policy[3]
}
+56
View File
@@ -0,0 +1,56 @@
package authz
const (
ResourceChannel = "channel"
ActionRead = "read"
ActionOperate = "operate"
ActionWrite = "write"
ActionSensitiveWrite = "sensitive_write"
ActionSecretView = "secret_view"
)
var (
ChannelRead = Permission{Resource: ResourceChannel, Action: ActionRead}
ChannelOperate = Permission{Resource: ResourceChannel, Action: ActionOperate}
ChannelWrite = Permission{Resource: ResourceChannel, Action: ActionWrite}
ChannelSensitiveWrite = Permission{Resource: ResourceChannel, Action: ActionSensitiveWrite}
ChannelSecretView = Permission{Resource: ResourceChannel, Action: ActionSecretView}
)
func init() {
RegisterResource(ResourceDefinition{
Resource: ResourceChannel,
LabelKey: "Channel Management",
Actions: []ActionDefinition{
{
Action: ActionRead,
LabelKey: "Read channels",
DescriptionKey: "View channel lists and details without secrets.",
DefaultRoles: []string{BuiltInRoleAdmin},
},
{
Action: ActionOperate,
LabelKey: "Operate channels",
DescriptionKey: "Test channels, refresh balances, and enable/disable individual, batch, or tagged channels.",
DefaultRoles: []string{BuiltInRoleAdmin},
},
{
Action: ActionWrite,
LabelKey: "Edit channel routing",
DescriptionKey: "Edit non-sensitive settings such as models, groups, and routing rules.",
DefaultRoles: []string{BuiltInRoleAdmin},
},
{
Action: ActionSensitiveWrite,
LabelKey: "Edit sensitive channel settings",
DescriptionKey: "Create channels or edit keys, base URLs, and overrides.",
},
{
Action: ActionSecretView,
LabelKey: "View channel secrets",
DescriptionKey: "Reserved for viewing complete channel keys after secure verification.",
},
},
})
}
+86
View File
@@ -0,0 +1,86 @@
package authz
const (
BuiltInRoleRoot = "root"
BuiltInRoleAdmin = "admin"
)
// RoleSpec describes a role. A superuser role is allowed every permission
// without an explicit policy entry.
type RoleSpec struct {
Key string
Name string
Description string
BuiltIn bool
Superuser bool
Sort int
}
var builtInRoles = []RoleSpec{
{
Key: BuiltInRoleRoot,
Name: "Root",
Description: "Built-in root authorization role",
BuiltIn: true,
Superuser: true,
Sort: 0,
},
{
Key: BuiltInRoleAdmin,
Name: "Admin",
Description: "Built-in admin authorization role",
BuiltIn: true,
Superuser: false,
Sort: 10,
},
}
// RoleDescriptor exposes a role together with its baseline grant matrix.
type RoleDescriptor struct {
Key string `json:"key"`
Name string `json:"name"`
BuiltIn bool `json:"built_in"`
Superuser bool `json:"superuser"`
Grants PermissionsMap `json:"grants"`
}
// Roles returns the role descriptors with their baseline grants.
func Roles() []RoleDescriptor {
result := make([]RoleDescriptor, 0, len(builtInRoles))
for _, spec := range builtInRoles {
result = append(result, RoleDescriptor{
Key: spec.Key,
Name: spec.Name,
BuiltIn: spec.BuiltIn,
Superuser: spec.Superuser,
Grants: roleGrants(spec),
})
}
return result
}
func roleGrants(spec RoleSpec) PermissionsMap {
grants := make(PermissionsMap, len(registry))
for _, resource := range registry {
actions := make(map[string]bool, len(resource.Actions))
for _, action := range resource.Actions {
actions[action.Action] = spec.Superuser || actionHasRole(action, spec.Key)
}
grants[resource.Resource] = actions
}
return grants
}
func roleSpec(roleKey string) (RoleSpec, bool) {
for _, spec := range builtInRoles {
if spec.Key == roleKey {
return spec, true
}
}
return RoleSpec{}, false
}
func isSuperuserRole(roleKey string) bool {
spec, ok := roleSpec(roleKey)
return ok && spec.Superuser
}
+62
View File
@@ -0,0 +1,62 @@
package authz
import (
"fmt"
"github.com/QuantumNous/new-api/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func seedBuiltInRoles(db *gorm.DB) error {
for _, spec := range builtInRoles {
role := model.AuthzRole{
Key: spec.Key,
Name: spec.Name,
Description: spec.Description,
BuiltIn: spec.BuiltIn,
Enabled: true,
Sort: spec.Sort,
}
if err := db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "key"}},
DoUpdates: clause.AssignmentColumns([]string{
"name",
"description",
"built_in",
"enabled",
"sort",
}),
}).Create(&role).Error; err != nil {
return err
}
}
return nil
}
func resetBuiltInRolePolicies(db *gorm.DB) error {
subjects := make([]string, 0, len(builtInRoles))
for _, spec := range builtInRoles {
subjects = append(subjects, RoleSubject(spec.Key))
}
return db.Where("ptype = ? AND v0 IN ?", "p", subjects).Delete(&model.CasbinRule{}).Error
}
func seedDefaultPolicies() error {
e := currentEnforcer()
if e == nil {
return fmt.Errorf("authz enforcer is not initialized")
}
for _, spec := range builtInRoles {
if spec.Superuser {
continue
}
for _, permission := range PermissionsForRole(spec.Key) {
if _, err := e.AddPolicy(RoleSubject(spec.Key), permission.Resource, permission.Action, EffectAllow); err != nil {
return err
}
}
}
return nil
}