126 lines
3.5 KiB
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", "登录会话不存在或已失效")
|
|
}
|