2026-07-14 17:37:08 +08:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
2026-07-30 16:09:13 +08:00
|
|
|
"crypto/rand"
|
|
|
|
|
"encoding/hex"
|
2026-07-14 17:37:08 +08:00
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
2026-07-30 16:09:13 +08:00
|
|
|
"net/url"
|
2026-07-14 17:37:08 +08:00
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-07-15 14:05:22 +08:00
|
|
|
"github.com/1024XEngineer/xinfra/server/internal/config"
|
|
|
|
|
"github.com/1024XEngineer/xinfra/server/internal/service"
|
2026-07-30 16:09:13 +08:00
|
|
|
gmsm2 "github.com/tjfoc/gmsm/sm2"
|
|
|
|
|
gmx509 "github.com/tjfoc/gmsm/x509"
|
2026-07-14 17:37:08 +08:00
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type CloudDMHandler struct {
|
|
|
|
|
cfg config.Config
|
|
|
|
|
audit *service.AuditService
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewCloudDMHandler(cfg config.Config, audit *service.AuditService) *CloudDMHandler {
|
|
|
|
|
return &CloudDMHandler{cfg: cfg, audit: audit}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *CloudDMHandler) Login(c *gin.Context) {
|
|
|
|
|
claims, ok := CurrentClaims(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
targetURL := strings.TrimSpace(h.cfg.CloudDMTargetURL)
|
|
|
|
|
if targetURL == "" {
|
|
|
|
|
h.writeAudit(c, claims.UserID, claims.Username, "deny", "clouddm target url is not configured")
|
|
|
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "clouddm target url is not configured"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 16:09:13 +08:00
|
|
|
if claims.IsAdmin {
|
|
|
|
|
targetURL, err := h.adminLogin(c, targetURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.writeAudit(c, claims.UserID, claims.Username, "deny", err.Error())
|
|
|
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
h.writeAudit(c, claims.UserID, claims.Username, "allow", "platform admin uses clouddm admin account")
|
|
|
|
|
if strings.Contains(c.GetHeader("Accept"), "application/json") {
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"target_url": targetURL})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Redirect(http.StatusFound, targetURL)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 17:37:08 +08:00
|
|
|
jumpURL, err := h.loginJumpURL(targetURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
h.writeAudit(c, claims.UserID, claims.Username, "deny", err.Error())
|
|
|
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h.writeAudit(c, claims.UserID, claims.Username, "allow", "")
|
|
|
|
|
if strings.Contains(c.GetHeader("Accept"), "application/json") {
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"target_url": jumpURL})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Redirect(http.StatusFound, jumpURL)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *CloudDMHandler) loginJumpURL(targetURL string) (string, error) {
|
|
|
|
|
requestURL := strings.TrimRight(targetURL, "/") + "/requestJumpUrl"
|
|
|
|
|
body, _ := json.Marshal(gin.H{"type": "OIDC"})
|
|
|
|
|
req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader(body))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
|
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
|
|
|
return "", fmt.Errorf("clouddm requestJumpUrl failed: status %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result struct {
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
Data string `json:"data"`
|
|
|
|
|
Msg string `json:"msg"`
|
|
|
|
|
MsgContent string `json:"msgContent"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(raw, &result); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if !result.Success || strings.TrimSpace(result.Data) == "" {
|
|
|
|
|
reason := strings.TrimSpace(result.MsgContent)
|
|
|
|
|
if reason == "" {
|
|
|
|
|
reason = strings.TrimSpace(result.Msg)
|
|
|
|
|
}
|
|
|
|
|
if reason == "" {
|
|
|
|
|
reason = "empty clouddm jump url"
|
|
|
|
|
}
|
|
|
|
|
return "", fmt.Errorf("clouddm requestJumpUrl failed: %s", reason)
|
|
|
|
|
}
|
|
|
|
|
return result.Data, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 16:09:13 +08:00
|
|
|
func (h *CloudDMHandler) adminLogin(c *gin.Context, targetURL string) (string, error) {
|
|
|
|
|
username := strings.TrimSpace(h.cfg.CloudDMAdminUsername)
|
|
|
|
|
password := h.cfg.CloudDMAdminPassword
|
|
|
|
|
if username == "" || password == "" {
|
|
|
|
|
return "", fmt.Errorf("clouddm admin account is not configured")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
publicKey, err := h.fetchPublicKey(targetURL)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
encryptedPassword, err := encryptCloudDMPassword(publicKey, password)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loginURL := strings.TrimSpace(h.cfg.CloudDMLoginURL)
|
|
|
|
|
if loginURL == "" {
|
|
|
|
|
loginURL = strings.TrimRight(targetURL, "/") + "/login"
|
|
|
|
|
}
|
|
|
|
|
body, _ := json.Marshal(gin.H{
|
|
|
|
|
"accountType": "SUB_ACCOUNT",
|
|
|
|
|
"loginType": "PASSWORD",
|
|
|
|
|
"account": username,
|
|
|
|
|
"password": encryptedPassword,
|
|
|
|
|
})
|
|
|
|
|
req, err := http.NewRequest(http.MethodPost, loginURL, bytes.NewReader(body))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
|
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 8192))
|
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
|
|
|
return "", fmt.Errorf("clouddm admin login failed: status %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result struct {
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
Data struct {
|
|
|
|
|
NeedMore bool `json:"needMore"`
|
|
|
|
|
NeedMfa bool `json:"needMfa"`
|
|
|
|
|
} `json:"data"`
|
|
|
|
|
Msg string `json:"msg"`
|
|
|
|
|
MsgContent string `json:"msgContent"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(raw, &result); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if !result.Success || result.Data.NeedMore || result.Data.NeedMfa {
|
|
|
|
|
reason := strings.TrimSpace(result.MsgContent)
|
|
|
|
|
if reason == "" {
|
|
|
|
|
reason = strings.TrimSpace(result.Msg)
|
|
|
|
|
}
|
|
|
|
|
if reason == "" {
|
|
|
|
|
reason = "admin login did not complete"
|
|
|
|
|
}
|
|
|
|
|
return "", fmt.Errorf("clouddm admin login failed: %s", reason)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, cookie := range resp.Header.Values("Set-Cookie") {
|
|
|
|
|
c.Writer.Header().Add("Set-Cookie", cookie)
|
|
|
|
|
}
|
|
|
|
|
return h.publicSQLURL(), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *CloudDMHandler) fetchPublicKey(targetURL string) (string, error) {
|
|
|
|
|
requestURL := strings.TrimRight(targetURL, "/") + "/api/entry/dmGlobalSettings"
|
|
|
|
|
req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader([]byte("{}")))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
|
|
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 8192))
|
|
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
|
|
|
return "", fmt.Errorf("clouddm dmGlobalSettings failed: status %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result struct {
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
Data struct {
|
|
|
|
|
PublicKey string `json:"publicKey"`
|
|
|
|
|
} `json:"data"`
|
|
|
|
|
Msg string `json:"msg"`
|
|
|
|
|
MsgContent string `json:"msgContent"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(raw, &result); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if !result.Success || strings.TrimSpace(result.Data.PublicKey) == "" {
|
|
|
|
|
reason := strings.TrimSpace(result.MsgContent)
|
|
|
|
|
if reason == "" {
|
|
|
|
|
reason = strings.TrimSpace(result.Msg)
|
|
|
|
|
}
|
|
|
|
|
if reason == "" {
|
|
|
|
|
reason = "empty public key"
|
|
|
|
|
}
|
|
|
|
|
return "", fmt.Errorf("clouddm dmGlobalSettings failed: %s", reason)
|
|
|
|
|
}
|
|
|
|
|
return result.Data.PublicKey, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func encryptCloudDMPassword(publicKey, password string) (string, error) {
|
|
|
|
|
pub, err := gmx509.ReadPublicKeyFromHex(strings.TrimSpace(publicKey))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
ciphertext, err := gmsm2.Encrypt(pub, []byte(password), rand.Reader, gmsm2.C1C3C2)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return hex.EncodeToString(ciphertext), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *CloudDMHandler) publicSQLURL() string {
|
|
|
|
|
base := strings.TrimSpace(h.cfg.CloudDMPublicURL)
|
|
|
|
|
if base == "" {
|
|
|
|
|
if redirectURI, err := url.Parse(strings.TrimSpace(h.cfg.CloudDMRedirectURI)); err == nil && redirectURI.Scheme != "" && redirectURI.Host != "" {
|
|
|
|
|
base = redirectURI.Scheme + "://" + redirectURI.Host
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if base == "" {
|
|
|
|
|
base = strings.TrimSpace(h.cfg.CloudDMTargetURL)
|
|
|
|
|
}
|
|
|
|
|
base = strings.TrimRight(base, "/")
|
|
|
|
|
if strings.Contains(base, "#") {
|
|
|
|
|
return base
|
|
|
|
|
}
|
|
|
|
|
return base + "/#/sql"
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 17:37:08 +08:00
|
|
|
func (h *CloudDMHandler) writeAudit(c *gin.Context, userID uint64, username, decision, reason string) {
|
|
|
|
|
h.audit.Write(service.AuditEntry{
|
|
|
|
|
ActorUserID: userID,
|
|
|
|
|
ActorUsername: username,
|
|
|
|
|
ClientIP: c.ClientIP(),
|
|
|
|
|
UserAgent: c.Request.UserAgent(),
|
|
|
|
|
Action: "clouddm.login",
|
|
|
|
|
ResourceType: "clouddm",
|
|
|
|
|
Decision: decision,
|
|
|
|
|
Reason: reason,
|
|
|
|
|
})
|
|
|
|
|
}
|