功能:扩展云管资源运维、账号授权及监控管理

This commit is contained in:
songyiming committed 2026-11-10 17:52:52 +08:00
1 parent eef485306f
commit 6134d49c16
87 files changed
+29498 -126

No files matched your search

+261
View File
@@ -0,0 +1,261 @@
package server
import (
"context"
"database/sql"
"errors"
"regexp"
"strings"
"unicode/utf8"
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt"
"vdi-cloud/platform/internal/model"
"vdi-cloud/platform/internal/store"
)
func (s *Server) administratorList(c *gin.Context) {
items := []model.Administrator{}
if err := s.DB.SelectContext(c.Request.Context(), &items, "SELECT * FROM administrators ORDER BY username"); err != nil {
s.dbError(c, err)
return
}
list(c, items)
}
func validNewPassword(password string) bool {
return utf8.RuneCountInString(password) >= 12 && len(password) <= 72 && strings.TrimSpace(password) != ""
}
// All account writes share a transaction lock, then recheck the actor's current role.
// This also prevents concurrent administrators from disabling each other and leaving no owner.
func (s *Server) accountTransaction(c *gin.Context, superOnly bool) (*sqlx.Tx, model.Administrator) {
var actor model.Administrator
ctx := c.Request.Context()
tx, err := s.DB.BeginTxx(ctx, nil)
if err != nil {
s.dbError(c, err)
return nil, actor
}
if _, err = tx.ExecContext(ctx, "SELECT pg_advisory_xact_lock(7651092028)"); err == nil {
err = tx.GetContext(ctx, &actor, "SELECT a.* FROM administrators a JOIN admin_sessions x ON x.admin_id=a.id WHERE a.id=$1 AND x.id=$2 AND x.expires_at>now() FOR UPDATE OF a", s.admin(c).ID, c.GetString("sessionId"))
}
if err != nil {
tx.Rollback()
if errors.Is(err, sql.ErrNoRows) {
s.fail(c, 401, "unauthorized", "登录已失效,请重新登录")
} else {
s.dbError(c, err)
}
return nil, actor
}
if !actor.Enabled || superOnly && actor.Role != "super_admin" {
tx.Rollback()
s.fail(c, 403, "forbidden", "当前账号没有该操作权限")
return nil, actor
}
return tx, actor
}
func accountAudit(ctx context.Context, tx *sqlx.Tx, actor, action, target, requestID string) error {
_, err := tx.ExecContext(ctx, "INSERT INTO audit_logs(id,actor,action,target,result,request_id) VALUES($1,$2,$3,$4,'success',$5)", store.ID("audit_"), actor, action, target, requestID)
return err
}
func (s *Server) createAdministrator(c *gin.Context) {
var in struct {
Username string `json:"username" binding:"required,max=80"`
DisplayName string `json:"displayName" binding:"required,max=80"`
Role string `json:"role" binding:"required,oneof=super_admin operator viewer"`
Password string `json:"password" binding:"required,max=72"`
}
if !s.decode(c, &in) {
return
}
in.Username = strings.TrimSpace(in.Username)
in.DisplayName = strings.TrimSpace(in.DisplayName)
if !regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{2,79}$`).MatchString(in.Username) || in.DisplayName == "" {
s.fail(c, 400, "invalid_account", "账号使用 3~80 位字母、数字、点、下划线或短横线,显示名称不能为空")
return
}
if !validNewPassword(in.Password) {
s.fail(c, 400, "invalid_password", "新密码至少 12 个字符,UTF-8 编码不超过 72 字节")
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
if err != nil {
s.dbError(c, err)
return
}
tx, actor := s.accountTransaction(c, true)
if tx == nil {
return
}
defer tx.Rollback()
var item model.Administrator
err = tx.GetContext(c.Request.Context(), &item, "INSERT INTO administrators(id,username,display_name,role,password_hash) VALUES($1,$2,$3,$4,$5) RETURNING *", store.ID("adm_"), in.Username, in.DisplayName, in.Role, string(hash))
if conflict(err) {
s.fail(c, 409, "account_conflict", "管理员账号已存在")
return
}
if err != nil {
s.dbError(c, err)
return
}
if err = accountAudit(c.Request.Context(), tx, actor.Username, "create_administrator", item.Username, c.GetString("requestId")); err == nil {
err = tx.Commit()
}
if err != nil {
s.dbError(c, err)
return
}
c.JSON(201, item)
}
func (s *Server) updateAdministrator(c *gin.Context) {
var in struct {
DisplayName string `json:"displayName" binding:"required,max=80"`
Role string `json:"role" binding:"required,oneof=super_admin operator viewer"`
Enabled *bool `json:"enabled" binding:"required"`
}
if !s.decode(c, &in) {
return
}
in.DisplayName = strings.TrimSpace(in.DisplayName)
if in.DisplayName == "" {
s.fail(c, 400, "invalid_account", "显示名称不能为空")
return
}
tx, actor := s.accountTransaction(c, true)
if tx == nil {
return
}
defer tx.Rollback()
ctx := c.Request.Context()
var item model.Administrator
if err := tx.GetContext(ctx, &item, "SELECT * FROM administrators WHERE id=$1 FOR UPDATE", c.Param("id")); err != nil {
s.dbError(c, err)
return
}
if item.Enabled && item.Role == "super_admin" && (!*in.Enabled || in.Role != "super_admin") {
var count int
if err := tx.GetContext(ctx, &count, "SELECT count(*) FROM administrators WHERE enabled AND role='super_admin'"); err != nil {
s.dbError(c, err)
return
}
if count <= 1 {
s.fail(c, 409, "last_super_admin", "至少保留一个启用的超级管理员")
return
}
}
if item.ID == actor.ID && (!*in.Enabled || in.Role != actor.Role) {
s.fail(c, 409, "self_account_protected", "不能停用自己或修改自己的角色,请使用其他超级管理员账号操作")
return
}
invalidate := item.Role != in.Role || item.Enabled != *in.Enabled
err := tx.GetContext(ctx, &item, "UPDATE administrators SET display_name=$1,role=$2,enabled=$3 WHERE id=$4 RETURNING *", in.DisplayName, in.Role, *in.Enabled, item.ID)
if err == nil && invalidate {
_, err = tx.ExecContext(ctx, "DELETE FROM admin_sessions WHERE admin_id=$1", item.ID)
}
if err == nil {
err = accountAudit(ctx, tx, actor.Username, "update_administrator", item.Username, c.GetString("requestId"))
}
if err == nil {
err = tx.Commit()
}
if err != nil {
s.dbError(c, err)
return
}
c.JSON(200, item)
}
func (s *Server) resetAdministratorPassword(c *gin.Context) {
var in struct {
Password string `json:"password" binding:"required,max=72"`
}
if !s.decode(c, &in) {
return
}
if !validNewPassword(in.Password) {
s.fail(c, 400, "invalid_password", "新密码至少 12 个字符,UTF-8 编码不超过 72 字节")
return
}
tx, actor := s.accountTransaction(c, true)
if tx == nil {
return
}
defer tx.Rollback()
if c.Param("id") == actor.ID {
s.fail(c, 409, "use_own_password", "修改本人密码请使用修改密码入口并验证原密码")
return
}
var item model.Administrator
ctx := c.Request.Context()
if err := tx.GetContext(ctx, &item, "SELECT * FROM administrators WHERE id=$1 FOR UPDATE", c.Param("id")); err != nil {
s.dbError(c, err)
return
}
if err := replacePassword(ctx, tx, item.ID, in.Password); err != nil {
s.dbError(c, err)
return
}
err := accountAudit(ctx, tx, actor.Username, "reset_administrator_password", item.Username, c.GetString("requestId"))
if err == nil {
err = tx.Commit()
}
if err != nil {
s.dbError(c, err)
return
}
c.Status(204)
}
func replacePassword(ctx context.Context, tx *sqlx.Tx, id, password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err == nil {
_, err = tx.ExecContext(ctx, "UPDATE administrators SET password_hash=$1 WHERE id=$2", string(hash), id)
}
if err == nil {
_, err = tx.ExecContext(ctx, "DELETE FROM admin_sessions WHERE admin_id=$1", id)
}
return err
}
func (s *Server) changeOwnPassword(c *gin.Context) {
var in struct {
CurrentPassword string `json:"currentPassword" binding:"required,max=200"`
NewPassword string `json:"newPassword" binding:"required,max=72"`
}
if !s.decode(c, &in) {
return
}
if !validNewPassword(in.NewPassword) {
s.fail(c, 400, "invalid_password", "新密码至少 12 个字符,UTF-8 编码不超过 72 字节")
return
}
tx, actor := s.accountTransaction(c, false)
if tx == nil {
return
}
defer tx.Rollback()
if bcrypt.CompareHashAndPassword([]byte(actor.PasswordHash), []byte(in.CurrentPassword)) != nil {
s.fail(c, 400, "current_password_invalid", "原密码不正确")
return
}
ctx := c.Request.Context()
if err := replacePassword(ctx, tx, actor.ID, in.NewPassword); err != nil {
s.dbError(c, err)
return
}
err := accountAudit(ctx, tx, actor.Username, "change_own_password", actor.Username, c.GetString("requestId"))
if err == nil {
err = tx.Commit()
}
if err != nil {
s.dbError(c, err)
return
}
c.Status(204)
}