feat: publish v1.0.0-rc.36-custom.1
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 9m20s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 55m7s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s

This commit is contained in:
Codex committed 2026-09-11 05:07:48 +08:00
commit bb7968b612
2381 files changed
+551494

No files matched your search

+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 := range 6 {
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
+273
View File
@@ -0,0 +1,273 @@
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,
},
ResourceTaskPlugin: {
ActionBind: false,
},
ResourceAudit: {ActionRead: 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,
},
ResourceTaskPlugin: {
ActionBind: false,
},
ResourceAudit: {ActionRead: 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])
assert.False(t, capabilities[ResourceTaskPlugin][ActionBind])
}
func TestTaskPluginBindIsRootOnlyUntilGranted(t *testing.T) {
db := newAuthzTestDB(t)
require.NoError(t, Init(db))
var bindAction *ActionDefinition
for _, resource := range Catalog() {
if resource.Resource != ResourceTaskPlugin {
continue
}
assert.Equal(t, "Task Plugin", resource.LabelKey)
for i := range resource.Actions {
if resource.Actions[i].Action == ActionBind {
bindAction = &resource.Actions[i]
}
}
}
require.NotNil(t, bindAction)
assert.Equal(t, "Bind task plugins", bindAction.LabelKey)
assert.Equal(t, "List registered task plugins and bind them when creating or editing task plugin channels.", bindAction.DescriptionKey)
assert.Empty(t, bindAction.DefaultRoles)
assert.False(t, Can(2, common.RoleAdminUser, TaskPluginBind))
assert.True(t, Can(1, common.RoleRootUser, TaskPluginBind))
enforcer := currentEnforcer()
require.NotNil(t, enforcer)
_, err := enforcer.AddPolicy(RoleSubject(BuiltInRoleAdmin), ResourceTaskPlugin, ActionBind, EffectAllow)
require.NoError(t, err)
assert.True(t, Can(2, common.RoleAdminUser, TaskPluginBind))
_, err = enforcer.RemovePolicy(RoleSubject(BuiltInRoleAdmin), ResourceTaskPlugin, ActionBind, EffectAllow)
require.NoError(t, err)
assert.False(t, Can(2, common.RoleAdminUser, TaskPluginBind))
}
+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
}
+105
View File
@@ -0,0 +1,105 @@
package authz
import "slices"
// 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 {
return slices.Contains(action.DefaultRoles, roleKey)
}
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 "slices"
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
}
if slices.ContainsFunc(roles, isSuperuserRole) {
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]
}
+17
View File
@@ -0,0 +1,17 @@
package authz
const ResourceAudit = "audit"
var AuditRead = Permission{Resource: ResourceAudit, Action: ActionRead}
func init() {
RegisterResource(ResourceDefinition{
Resource: ResourceAudit,
LabelKey: "Audit Logs",
Actions: []ActionDefinition{{
Action: ActionRead,
LabelKey: "View other accounts' audit logs",
DescriptionKey: "View audit records from user and admin roles. Root records are always excluded.",
}},
})
}
+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.",
},
},
})
}
+23
View File
@@ -0,0 +1,23 @@
package authz
const (
ResourceTaskPlugin = "task_plugin"
ActionBind = "bind"
)
var TaskPluginBind = Permission{Resource: ResourceTaskPlugin, Action: ActionBind}
func init() {
RegisterResource(ResourceDefinition{
Resource: ResourceTaskPlugin,
LabelKey: "Task Plugin",
Actions: []ActionDefinition{
{
Action: ActionBind,
LabelKey: "Bind task plugins",
DescriptionKey: "List registered task plugins and bind them when creating or editing task plugin channels.",
},
},
})
}
+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
}