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
+17
View File
@@ -0,0 +1,17 @@
package model
type AuthzRole struct {
Id uint `json:"id" gorm:"primaryKey;autoIncrement"`
Key string `json:"key" gorm:"size:64;uniqueIndex;not null"`
Name string `json:"name" gorm:"size:100;not null"`
Description string `json:"description" gorm:"type:text"`
BuiltIn bool `json:"built_in"`
Enabled bool `json:"enabled"`
Sort int `json:"sort"`
CreatedAt int64 `json:"created_at" gorm:"autoCreateTime;column:created_at"`
UpdatedAt int64 `json:"updated_at" gorm:"autoUpdateTime;column:updated_at"`
}
func (AuthzRole) TableName() string {
return "authz_roles"
}
+16
View File
@@ -0,0 +1,16 @@
package model
type CasbinRule struct {
Id uint `gorm:"primaryKey;autoIncrement"`
Ptype string `gorm:"size:100;index:idx_casbin_rule,priority:1;uniqueIndex:idx_casbin_rule_unique,priority:1"`
V0 string `gorm:"size:100;index:idx_casbin_rule,priority:2;uniqueIndex:idx_casbin_rule_unique,priority:2"`
V1 string `gorm:"size:100;index:idx_casbin_rule,priority:3;uniqueIndex:idx_casbin_rule_unique,priority:3"`
V2 string `gorm:"size:100;index:idx_casbin_rule,priority:4;uniqueIndex:idx_casbin_rule_unique,priority:4"`
V3 string `gorm:"size:100;index:idx_casbin_rule,priority:5;uniqueIndex:idx_casbin_rule_unique,priority:5"`
V4 string `gorm:"size:100;index:idx_casbin_rule,priority:6;uniqueIndex:idx_casbin_rule_unique,priority:6"`
V5 string `gorm:"size:100;index:idx_casbin_rule,priority:7;uniqueIndex:idx_casbin_rule_unique,priority:7"`
}
func (CasbinRule) TableName() string {
return "casbin_rule"
}
+2 -2
View File
@@ -196,8 +196,8 @@ func RecordLoginLog(userId int, username string, content string, ip string, acti
}
// RecordOperationAuditLog 记录管理/高危操作审计日志(type=LogTypeManage)。
// logUserId 为日志归属者(面向用户的操作如额度调整归属目标用户,资源类操作如渠道/系统设置归属操作者),
// username 内部按 logUserId 查询。content 为英文兜底文本(导出/经典前端用)。
// logUserId 为日志归属者,管理审计日志应归属实际操作者;目标资源/用户放入
// action params。username 内部按 logUserId 查询。content 为英文兜底文本(导出/经典前端用)。
// action+params 写入 Other.op,供前端本地化渲染(普通用户可见,不含敏感信息)。
// adminInfo 存放操作者身份(写入 Other.admin_info,普通用户查询时剥离);
// auditInfo 存放路由/方法/结果等中间件兜底信息(写入 Other.audit_info,普通用户查询时剥离)。
+2
View File
@@ -297,6 +297,8 @@ func migrateDB() error {
&SystemInstance{},
&SystemTask{},
&SystemTaskLock{},
&CasbinRule{},
&AuthzRole{},
)
if err != nil {
return err
+61 -42
View File
@@ -22,37 +22,38 @@ const UserNameMaxLength = 20
// User if you add sensitive fields, don't forget to clean them in setupLogin function.
// Otherwise, the sensitive information will be saved on local storage in plain text!
type User struct {
Id int `json:"id"`
Username string `json:"username" gorm:"unique;index" validate:"max=20"`
Password string `json:"password" gorm:"not null;" validate:"min=8,max=20"`
OriginalPassword string `json:"original_password" gorm:"-:all"` // this field is only for Password change verification, don't save it to database!
DisplayName string `json:"display_name" gorm:"index" validate:"max=20"`
Role int `json:"role" gorm:"type:int;default:1"` // admin, common
Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled
Email string `json:"email" gorm:"index" validate:"max=50"`
GitHubId string `json:"github_id" gorm:"column:github_id;index"`
DiscordId string `json:"discord_id" gorm:"column:discord_id;index"`
OidcId string `json:"oidc_id" gorm:"column:oidc_id;index"`
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
TelegramId string `json:"telegram_id" gorm:"column:telegram_id;index"`
VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database!
AccessToken *string `json:"-" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
Quota int `json:"quota" gorm:"type:int;default:0"`
UsedQuota int `json:"used_quota" gorm:"type:int;default:0;column:used_quota"` // used quota
RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number
Group string `json:"group" gorm:"type:varchar(64);default:'default'"`
AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
AffCount int `json:"aff_count" gorm:"type:int;default:0;column:aff_count"`
AffQuota int `json:"aff_quota" gorm:"type:int;default:0;column:aff_quota"` // 邀请剩余额度
AffHistoryQuota int `json:"aff_history_quota" gorm:"type:int;default:0;column:aff_history"` // 邀请历史额度
InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
DeletedAt gorm.DeletedAt `gorm:"index"`
LinuxDOId string `json:"linux_do_id" gorm:"column:linux_do_id;index"`
Setting string `json:"setting" gorm:"type:text;column:setting"`
Remark string `json:"remark,omitempty" gorm:"type:varchar(255)" validate:"max=255"`
StripeCustomer string `json:"stripe_customer" gorm:"type:varchar(64);column:stripe_customer;index"`
CreatedAt int64 `json:"created_at" gorm:"autoCreateTime;column:created_at"`
LastLoginAt int64 `json:"last_login_at" gorm:"default:0;column:last_login_at"`
Id int `json:"id"`
Username string `json:"username" gorm:"unique;index" validate:"max=20"`
Password string `json:"password" gorm:"not null;" validate:"min=8,max=20"`
OriginalPassword string `json:"original_password" gorm:"-:all"` // this field is only for Password change verification, don't save it to database!
DisplayName string `json:"display_name" gorm:"index" validate:"max=20"`
Role int `json:"role" gorm:"type:int;default:1"` // admin, common
Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled
Email string `json:"email" gorm:"index" validate:"max=50"`
GitHubId string `json:"github_id" gorm:"column:github_id;index"`
DiscordId string `json:"discord_id" gorm:"column:discord_id;index"`
OidcId string `json:"oidc_id" gorm:"column:oidc_id;index"`
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
TelegramId string `json:"telegram_id" gorm:"column:telegram_id;index"`
VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database!
AccessToken *string `json:"-" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
Quota int `json:"quota" gorm:"type:int;default:0"`
UsedQuota int `json:"used_quota" gorm:"type:int;default:0;column:used_quota"` // used quota
RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number
Group string `json:"group" gorm:"type:varchar(64);default:'default'"`
AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
AffCount int `json:"aff_count" gorm:"type:int;default:0;column:aff_count"`
AffQuota int `json:"aff_quota" gorm:"type:int;default:0;column:aff_quota"` // 邀请剩余额度
AffHistoryQuota int `json:"aff_history_quota" gorm:"type:int;default:0;column:aff_history"` // 邀请历史额度
InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
DeletedAt gorm.DeletedAt `gorm:"index"`
LinuxDOId string `json:"linux_do_id" gorm:"column:linux_do_id;index"`
Setting string `json:"setting" gorm:"type:text;column:setting"`
Remark string `json:"remark,omitempty" gorm:"type:varchar(255)" validate:"max=255"`
StripeCustomer string `json:"stripe_customer" gorm:"type:varchar(64);column:stripe_customer;index"`
CreatedAt int64 `json:"created_at" gorm:"autoCreateTime;column:created_at"`
LastLoginAt int64 `json:"last_login_at" gorm:"default:0;column:last_login_at"`
AdminPermissions map[string]map[string]bool `json:"admin_permissions,omitempty" gorm:"-:all"`
}
func (user *User) ToBaseUser() *UserBase {
@@ -408,6 +409,11 @@ func (user *User) Insert(inviterId int) error {
return result.Error
}
user.finishInsert(inviterId)
return nil
}
func (user *User) finishInsert(inviterId int) {
// 用户创建成功后,根据角色初始化边栏配置
// 需要重新获取用户以确保有正确的ID和Role
var createdUser User
@@ -437,7 +443,10 @@ func (user *User) Insert(inviterId int) error {
_ = inviteUser(inviterId)
}
}
return nil
}
func (user *User) FinishInsert(inviterId int) {
user.finishInsert(inviterId)
}
// InsertWithTx inserts a new user within an existing transaction.
@@ -500,6 +509,13 @@ func (user *User) FinalizeOAuthUserCreation(inviterId int) {
}
func (user *User) Update(updatePassword bool) error {
if err := user.UpdateWithTx(DB, updatePassword); err != nil {
return err
}
return updateUserCache(*user)
}
func (user *User) UpdateWithTx(tx *gorm.DB, updatePassword bool) error {
var err error
if updatePassword {
user.Password, err = common.Password2Hash(user.Password)
@@ -508,16 +524,21 @@ func (user *User) Update(updatePassword bool) error {
}
}
newUser := *user
DB.First(&user, user.Id)
if err = DB.Model(user).Updates(newUser).Error; err != nil {
tx.First(&user, user.Id)
if err = tx.Model(user).Updates(newUser).Error; err != nil {
return err
}
// Update cache
return updateUserCache(*user)
return nil
}
func (user *User) Edit(updatePassword bool) error {
if err := user.EditWithTx(DB, updatePassword); err != nil {
return err
}
return updateUserCache(*user)
}
func (user *User) EditWithTx(tx *gorm.DB, updatePassword bool) error {
var err error
if updatePassword {
user.Password, err = common.Password2Hash(user.Password)
@@ -537,13 +558,11 @@ func (user *User) Edit(updatePassword bool) error {
updates["password"] = newUser.Password
}
DB.First(&user, user.Id)
if err = DB.Model(user).Updates(updates).Error; err != nil {
tx.First(&user, user.Id)
if err = tx.Model(user).Updates(updates).Error; err != nil {
return err
}
// Update cache
return updateUserCache(*user)
return nil
}
func (user *User) ClearBinding(bindingType string) error {