vdi-cloud/platform/internal/server/admin_sessions.go

126 lines
3.5 KiB
Go

package server
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strings"
"time"
"unicode"
"github.com/gin-gonic/gin"
)
type administratorSession struct {
ID string `json:"-" db:"id"`
Key string `json:"key" db:"-"`
CreatedAt *time.Time `json:"createdAt" db:"created_at"`
ExpiresAt time.Time `json:"expiresAt" db:"expires_at"`
ClientIP string `json:"clientIp" db:"client_ip"`
UserAgent string `json:"userAgent" db:"user_agent"`
Current bool `json:"current" db:"-"`
}
// A management handle must never disclose the JWT's session identifier.
func (s *Server) administratorSessionKey(id string) string {
mac := hmac.New(sha256.New, []byte(s.Config.JWTSecret))
mac.Write([]byte("vdi-admin-session-management\x00" + id))
return hex.EncodeToString(mac.Sum(nil))
}
func sessionUserAgent(raw string) string {
runes := []rune(strings.Map(func(r rune) rune {
if unicode.IsControl(r) {
return ' '
}
return r
}, raw))
if len(runes) > 500 {
runes = runes[:500]
}
return string(runes)
}
func (s *Server) administratorSessions(c *gin.Context) {
tx, _ := s.accountTransaction(c, true)
if tx == nil {
return
}
defer tx.Rollback()
var username string
if err := tx.GetContext(c.Request.Context(), &username, "SELECT username FROM administrators WHERE id=$1", c.Param("id")); err != nil {
s.dbError(c, err)
return
}
items := []administratorSession{}
err := tx.SelectContext(c.Request.Context(), &items, `SELECT x.id,x.expires_at,d.created_at,COALESCE(d.client_ip,'') client_ip,COALESCE(d.user_agent,'') user_agent
FROM admin_sessions x LEFT JOIN admin_session_details d ON d.session_id=x.id
WHERE x.admin_id=$1 AND x.expires_at>now() ORDER BY x.expires_at DESC,x.id DESC`, c.Param("id"))
if err != nil {
s.dbError(c, err)
return
}
for i := range items {
items[i].Key = s.administratorSessionKey(items[i].ID)
items[i].Current = items[i].ID == c.GetString("sessionId")
}
list(c, items)
}
func (s *Server) revokeAdministratorSession(c *gin.Context) {
key := c.Param("sessionKey")
if len(key) != 64 {
s.fail(c, 404, "not_found", "登录会话不存在或已失效")
return
}
tx, actor := s.accountTransaction(c, true)
if tx == nil {
return
}
defer tx.Rollback()
ctx := c.Request.Context()
var username string
if err := tx.GetContext(ctx, &username, "SELECT username FROM administrators WHERE id=$1 FOR UPDATE", c.Param("id")); err != nil {
s.dbError(c, err)
return
}
var ids []string
if err := tx.SelectContext(ctx, &ids, "SELECT id FROM admin_sessions WHERE admin_id=$1 AND expires_at>now()", c.Param("id")); err != nil {
s.dbError(c, err)
return
}
for _, id := range ids {
if !hmac.Equal([]byte(s.administratorSessionKey(id)), []byte(key)) {
continue
}
if id == c.GetString("sessionId") {
s.fail(c, 409, "current_session_protected", "当前登录请使用退出登录入口")
return
}
result, err := tx.ExecContext(ctx, "DELETE FROM admin_sessions WHERE id=$1 AND admin_id=$2 AND expires_at>now()", id, c.Param("id"))
if err != nil {
s.dbError(c, err)
return
}
count, err := result.RowsAffected()
if err != nil {
s.dbError(c, err)
return
}
if count == 0 {
break
}
err = accountAudit(ctx, tx, actor.Username, "revoke_administrator_session", username+":"+key, c.GetString("requestId"))
if err == nil {
err = tx.Commit()
}
if err != nil {
s.dbError(c, err)
return
}
c.Status(204)
return
}
s.fail(c, 404, "not_found", "登录会话不存在或已失效")
}