264 lines
8.3 KiB
Go
264 lines
8.3 KiB
Go
package server
|
|
|
|
import (
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/jmoiron/sqlx"
|
|
"strings"
|
|
"vdi-cloud/platform/internal/model"
|
|
"vdi-cloud/platform/internal/store"
|
|
)
|
|
|
|
const terminalListSQL = `SELECT id,sn,name,group_name,enabled,
|
|
CASE WHEN NOT enabled THEN 'disabled' WHEN status='unregistered' THEN 'unregistered'
|
|
WHEN last_seen>now()-interval '90 seconds' THEN 'online' ELSE 'offline' END status,
|
|
agent_version,last_seen,CASE WHEN enabled AND last_seen>now()-interval '90 seconds' THEN session_desktop_id ELSE NULL END session_desktop_id,
|
|
session_state,config_version,fps,rtt FROM terminals`
|
|
|
|
// Only authorization and identity changes share this lock. Device heartbeats and
|
|
// command delivery use their own terminal row and remain independent.
|
|
func (s *Server) lockAuthorization(c *gin.Context, tx *sqlx.Tx) bool {
|
|
if _, err := tx.ExecContext(c.Request.Context(), "SELECT pg_advisory_xact_lock(7651092029)"); err != nil {
|
|
s.dbError(c, err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *Server) updateTerminal(c *gin.Context) {
|
|
var in struct {
|
|
Name string `json:"name" binding:"required,max=80"`
|
|
Group string `json:"group" binding:"max=80"`
|
|
}
|
|
if !s.decode(c, &in) {
|
|
return
|
|
}
|
|
in.Name = strings.TrimSpace(in.Name)
|
|
in.Group = strings.TrimSpace(in.Group)
|
|
if in.Name == "" {
|
|
s.fail(c, 400, "invalid_request", "终端名称不能为空")
|
|
return
|
|
}
|
|
tx, err := s.DB.BeginTxx(c.Request.Context(), nil)
|
|
if err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
var id string
|
|
if err = tx.GetContext(c.Request.Context(), &id, "UPDATE terminals SET name=$1,group_name=$2 WHERE id=$3 RETURNING id", in.Name, in.Group, c.Param("id")); err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
if err = accountAudit(c.Request.Context(), tx, s.admin(c).Username, "update_terminal", id, c.GetString("requestId")); err == nil {
|
|
err = tx.Commit()
|
|
}
|
|
if err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
c.JSON(200, gin.H{"accepted": true})
|
|
}
|
|
|
|
func (s *Server) disableTerminal(c *gin.Context) { s.resetTerminalIdentity(c, false) }
|
|
func (s *Server) reactivateTerminal(c *gin.Context) { s.resetTerminalIdentity(c, true) }
|
|
func (s *Server) resetTerminalIdentity(c *gin.Context, reactivate bool) {
|
|
ctx := c.Request.Context()
|
|
tx, err := s.DB.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
if !s.lockAuthorization(c, tx) {
|
|
return
|
|
}
|
|
var state struct {
|
|
ID string `db:"id"`
|
|
Enabled bool `db:"enabled"`
|
|
Active bool `db:"active"`
|
|
}
|
|
err = tx.GetContext(ctx, &state, "SELECT id,enabled,(session_desktop_id IS NOT NULL AND last_seen>now()-interval '90 seconds') active FROM terminals WHERE id=$1 FOR UPDATE", c.Param("id"))
|
|
if err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
if !state.Enabled && !reactivate {
|
|
c.JSON(200, gin.H{"accepted": true})
|
|
return
|
|
}
|
|
if state.Active {
|
|
s.fail(c, 409, "terminal_in_use", "终端有活动会话或连接预约,请先结束或重置会话")
|
|
return
|
|
}
|
|
var pending bool
|
|
if err = tx.GetContext(ctx, &pending, "SELECT EXISTS(SELECT 1 FROM tasks WHERE kind='terminal' AND target_id=$1 AND status IN ('queued','running'))", state.ID); err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
if pending {
|
|
s.fail(c, 409, "terminal_task_pending", "终端有未完成命令,请等待回执或核对超时结果")
|
|
return
|
|
}
|
|
secret, bootstrap, status, action := "", "", "disabled", "disable_terminal"
|
|
if reactivate {
|
|
secret = store.ID("activate_")
|
|
bootstrap = store.Hash(secret)
|
|
status = "unregistered"
|
|
action = "reactivate_terminal"
|
|
}
|
|
_, err = tx.ExecContext(ctx, `UPDATE terminals SET enabled=$1,status=$2,token_hash=NULL,bootstrap_hash=$3,
|
|
last_seen=NULL,agent_version='',fps=NULL,rtt=NULL,session_id='',session_state='idle',session_desktop_id=NULL WHERE id=$4`, reactivate, status, bootstrap, state.ID)
|
|
if err == nil {
|
|
_, err = tx.ExecContext(ctx, "DELETE FROM connection_tickets WHERE terminal_id=$1", state.ID)
|
|
}
|
|
if err == nil {
|
|
_, err = tx.ExecContext(ctx, "DELETE FROM terminal_policy_capabilities WHERE terminal_id=$1", state.ID)
|
|
}
|
|
if err == nil {
|
|
_, err = tx.ExecContext(ctx, "DELETE FROM terminal_metrics_current WHERE terminal_id=$1", state.ID)
|
|
}
|
|
if err == nil {
|
|
err = closeSessionRecords(ctx, tx, state.ID, "identity_reset")
|
|
}
|
|
if err == nil {
|
|
_, err = tx.ExecContext(ctx, `UPDATE terminal_policy_deliveries SET status='pending',error_code='',acknowledged_at=NULL WHERE terminal_id=$1 AND config_version=(SELECT config_version FROM terminals WHERE id=$1)`, state.ID)
|
|
}
|
|
if err == nil {
|
|
err = accountAudit(ctx, tx, s.admin(c).Username, action, state.ID, c.GetString("requestId"))
|
|
}
|
|
if err == nil {
|
|
err = tx.Commit()
|
|
}
|
|
if err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
if reactivate {
|
|
c.JSON(200, gin.H{"id": state.ID, "activationSecret": secret})
|
|
} else {
|
|
c.JSON(200, gin.H{"accepted": true})
|
|
}
|
|
}
|
|
|
|
func (s *Server) updateUser(c *gin.Context) {
|
|
var in struct {
|
|
DisplayName string `json:"displayName" binding:"required,max=80"`
|
|
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_request", "用户姓名不能为空")
|
|
return
|
|
}
|
|
ctx := c.Request.Context()
|
|
tx, err := s.DB.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
if !s.lockAuthorization(c, tx) {
|
|
return
|
|
}
|
|
var user model.User
|
|
err = tx.GetContext(ctx, &user, "UPDATE users SET display_name=$1,enabled=$2 WHERE id=$3 RETURNING *", in.DisplayName, *in.Enabled, c.Param("id"))
|
|
if err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
if _, err = tx.ExecContext(ctx, "DELETE FROM connection_tickets WHERE terminal_id=$1 AND consumed_at IS NULL", user.TerminalID); err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
user.DesktopIDs = []string{}
|
|
if err = tx.SelectContext(ctx, &user.DesktopIDs, "SELECT desktop_id FROM bindings WHERE user_id=$1 ORDER BY desktop_id", user.ID); err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
if err = accountAudit(ctx, tx, s.admin(c).Username, "update_user", user.ID, c.GetString("requestId")); err == nil {
|
|
err = tx.Commit()
|
|
}
|
|
if err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
c.JSON(200, user)
|
|
}
|
|
|
|
func uniqueIDs(items []string) []string {
|
|
result := []string{}
|
|
seen := map[string]bool{}
|
|
for _, id := range items {
|
|
if !seen[id] {
|
|
seen[id] = true
|
|
result = append(result, id)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
func (s *Server) batchBindings(c *gin.Context) {
|
|
var in struct {
|
|
UserIDs []string `json:"userIds" binding:"required,min=1,max=100,dive,required,max=100"`
|
|
DesktopIDs []string `json:"desktopIds" binding:"required,min=1,max=100,dive,required,max=100"`
|
|
Action string `json:"action" binding:"required,oneof=grant revoke"`
|
|
}
|
|
if !s.decode(c, &in) {
|
|
return
|
|
}
|
|
in.UserIDs = uniqueIDs(in.UserIDs)
|
|
in.DesktopIDs = uniqueIDs(in.DesktopIDs)
|
|
ctx := c.Request.Context()
|
|
tx, err := s.DB.BeginTxx(ctx, nil)
|
|
if err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
defer tx.Rollback()
|
|
if !s.lockAuthorization(c, tx) {
|
|
return
|
|
}
|
|
var ids []string
|
|
if err = tx.SelectContext(ctx, &ids, "SELECT id FROM users WHERE id=ANY($1) ORDER BY id FOR UPDATE", in.UserIDs); err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
if len(ids) != len(in.UserIDs) {
|
|
s.fail(c, 400, "invalid_user", "所选用户不存在,请刷新列表")
|
|
return
|
|
}
|
|
var count int
|
|
if err = tx.GetContext(ctx, &count, "SELECT count(*) FROM desktops WHERE id=ANY($1)", in.DesktopIDs); err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
if count != len(in.DesktopIDs) {
|
|
s.fail(c, 400, "invalid_desktop", "所选桌面不存在,请刷新列表")
|
|
return
|
|
}
|
|
for _, id := range ids {
|
|
if in.Action == "grant" {
|
|
_, err = tx.ExecContext(ctx, "INSERT INTO bindings(user_id,desktop_id) SELECT $1,unnest($2::text[]) ON CONFLICT DO NOTHING", id, in.DesktopIDs)
|
|
} else {
|
|
_, err = tx.ExecContext(ctx, "DELETE FROM bindings WHERE user_id=$1 AND desktop_id=ANY($2)", id, in.DesktopIDs)
|
|
}
|
|
if err == nil {
|
|
_, err = tx.ExecContext(ctx, "DELETE FROM connection_tickets WHERE consumed_at IS NULL AND terminal_id=(SELECT terminal_id FROM users WHERE id=$1)", id)
|
|
}
|
|
if err == nil {
|
|
err = accountAudit(ctx, tx, s.admin(c).Username, "batch_"+in.Action+"_bindings", id, c.GetString("requestId"))
|
|
}
|
|
if err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
s.dbError(c, err)
|
|
return
|
|
}
|
|
c.JSON(200, gin.H{"accepted": true, "userCount": len(ids)})
|
|
}
|