mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-21 06:51:25 +02:00
fix(rbac): protect built-in administrator from losing access
This commit is contained in:
1 parent
e6121e83eb
commit
be2c8c04e5
12 files changed
+319
-3
No files matched your search
@@ -1164,6 +1164,9 @@ func (db *DB) UpdateRBACUser(userID, displayName string, enabled *bool, roleIDs
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
if err := validateBuiltinAdminUpdate(tx, userID, enabled, roleIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if enabled != nil {
|
||||
if _, err := tx.Exec(`UPDATE rbac_users SET display_name = ?, enabled = ?, updated_at = ? WHERE id = ?`, strings.TrimSpace(displayName), boolToInt(*enabled), time.Now(), userID); err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// validateBuiltinAdminUpdate runs before any account writes in the same
|
||||
// transaction. The built-in administrator is the permanent management fallback.
|
||||
func validateBuiltinAdminUpdate(tx *sql.Tx, userID string, enabled *bool, roleIDs *[]string) error {
|
||||
var builtin bool
|
||||
if err := tx.QueryRow(`SELECT is_builtin FROM rbac_users WHERE id = ?`, userID).Scan(&builtin); err != nil {
|
||||
return err
|
||||
}
|
||||
if !builtin && userID != "admin" {
|
||||
return nil
|
||||
}
|
||||
if enabled != nil && !*enabled {
|
||||
return errors.New("内置管理员不能停用")
|
||||
}
|
||||
if roleIDs != nil {
|
||||
for _, roleID := range *roleIDs {
|
||||
if strings.TrimSpace(roleID) == RBACSystemRoleAdmin {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("内置管理员必须保留管理员角色")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuiltinAdminRejectsLossOfAccessAtomically(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
enabled *bool
|
||||
roles *[]string
|
||||
}{
|
||||
{name: "empty roles", roles: roleList()},
|
||||
{name: "operator only", roles: roleList(RBACSystemRoleOperator)},
|
||||
{name: "blank role", roles: roleList(" ")},
|
||||
{name: "disable", enabled: boolValue(false)},
|
||||
{name: "disable while retaining admin", enabled: boolValue(false), roles: roleList(RBACSystemRoleAdmin)},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
db := newRBACTestDB(t)
|
||||
if err := db.BootstrapRBAC("original-hash", map[string]string{"rbac:write": "manage", "auth:self": "self"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before, err := db.GetRBACUserByID("admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.UpdateRBACUser("admin", "must not persist", tc.enabled, tc.roles); err == nil {
|
||||
t.Fatal("destructive admin update succeeded")
|
||||
}
|
||||
after, err := db.GetRBACUserByID("admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(before, after) {
|
||||
t.Fatalf("rejected update changed account: before=%#v after=%#v", before, after)
|
||||
}
|
||||
access, err := db.ResolveRBACAccess("admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(access.Roles) != 1 || access.Roles[0].ID != RBACSystemRoleAdmin || !access.Permissions["rbac:write"] || access.Scope != RBACScopeAll {
|
||||
t.Fatalf("admin access changed: %#v", access)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltinAdminAllowsSafeMaintenanceAndMemberChanges(t *testing.T) {
|
||||
db := newRBACTestDB(t)
|
||||
if err := db.BootstrapRBAC("hash", map[string]string{"rbac:write": "manage"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.UpdateRBACUser("admin", "Renamed", nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.UpdateRBACUser("admin", "Renamed", boolValue(true), roleList(" admin ", RBACSystemRoleViewer)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.UpdateRBACUserPassword("admin", "new-hash"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
admin, err := db.GetRBACUserByID("admin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if admin.DisplayName != "Renamed" || admin.PasswordHash != "new-hash" || !admin.Enabled {
|
||||
t.Fatalf("safe maintenance failed: %#v", admin)
|
||||
}
|
||||
member, err := db.CreateRBACUser("operator", "Operator", "hash", true, []string{RBACSystemRoleAdmin, RBACSystemRoleOperator})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.UpdateRBACUser(member.ID, "Member", boolValue(false), roleList()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
access, err := db.ResolveRBACAccess(member.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if access.User.Enabled || len(access.Roles) != 0 {
|
||||
t.Fatalf("ordinary member changes blocked: %#v", access)
|
||||
}
|
||||
if err := db.DeleteRBACUser("admin"); err == nil {
|
||||
t.Fatal("deleted builtin admin")
|
||||
}
|
||||
}
|
||||
|
||||
func roleList(roles ...string) *[]string { return &roles }
|
||||
func boolValue(value bool) *bool { return &value }
|
||||
Reference in new issue
Block a user