Files
opsx 0427b87412 fix(instance): access_mode 值 read/readonly 不一致导致「读」实例被过滤
现象:instance-groups 里给 MySQL 实例授予「只读查询」权限后,
     /queries 页面仍然选不到该实例,admin 也无法访问。

根因(枚举值前后端不一致):
  前端 CreateInstanceForm「读」按钮写入 access_mode = "read"
  后端 AccessibleInstances 只匹配 ["readwrite", "readonly"]
  → "read" 永远不命中,选了「读」的实例(MySQL)被过滤掉;
    Redis 实例是 "readwrite" 所以恰好能查,掩盖了该 bug。

修复:后端 readonly 过滤增加 "read",兼容两种历史写法:
  []string{"readwrite", "readonly", "read"}
  写过滤仍为 ["readwrite", "write"]("read" 实例正确地不能写)

验证(admin):
  GET /instances/accessible?access_type=readonly → 5 个(含 MySQL id=5, access_mode=read)
  GET /instances/accessible?access_type=write    → 4 个(MySQL 正确排除)

镜像:lghost/opsxsql:latest + v0.1 = sha256:6b1a7121da71
2026-10-26 18:30:47 +00:00

133 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"net/http"
"sql-platform/internal/handler/resp"
"sql-platform/internal/middleware"
"sql-platform/internal/model"
"github.com/gin-gonic/gin"
)
// GET /api/v1/instances/accessible?access_type=readonly|write
func AccessibleInstances(c *gin.Context) {
userID, _, role := middleware.GetCurrentUser(c)
accessType := c.DefaultQuery("access_type", "readonly")
var instances []model.Instance
if role == "admin" || role == "dba" {
// admin/dba see all instances, filter by access_mode if needed
query := model.DB.Order("id")
// 注意:access_mode 存在两种历史写法——
// 前端「读」按钮写入 "read"(CreateInstanceForm),
// 另有 "readonly" 的历史值。两者都必须命中,否则选了「读」的实例
// (尤其 MySQL)会被过滤掉,连 admin 都查不到。
if accessType == "readonly" {
query = query.Where("access_mode IN ? OR access_mode IS NULL", []string{"readwrite", "readonly", "read"})
} else {
query = query.Where("access_mode IN ? OR access_mode IS NULL", []string{"readwrite", "write"})
}
query.Find(&instances)
} else {
// developer/auditor: only instances in their groups
var groupIDs []int
model.DB.Model(&model.UserGroup{}).Where("user_id = ?", userID).Pluck("group_id", &groupIDs)
if len(groupIDs) == 0 {
c.JSON(http.StatusOK, []model.Instance{})
return
}
var instanceIDs []int
// Query for the requested access_type OR readwrite (readwrite grants both)
model.DB.Model(&model.InstanceGroupMember{}).
Where("group_id IN ? AND (access_type = ? OR access_type = ?)", groupIDs, accessType, "readwrite").
Pluck("instance_id", &instanceIDs)
if len(instanceIDs) == 0 {
c.JSON(http.StatusOK, []model.Instance{})
return
}
model.DB.Where("id IN ?", instanceIDs).Order("id").Find(&instances)
}
// mask passwords
for i := range instances {
instances[i].Password = "******"
}
c.JSON(http.StatusOK, instances)
}
// GET /api/v1/instances/:id/databases
func ListDatabases(c *gin.Context) {
id := c.Param("id")
var instance model.Instance
if err := model.DB.First(&instance, id).Error; err != nil {
resp.NotFound(c, "Instance not found")
return
}
databases, err := listDatabases(&instance)
if err != nil {
c.JSON(http.StatusOK, []string{})
return
}
if databases == nil {
databases = []string{}
}
c.JSON(http.StatusOK, databases)
}
// GET /api/v1/instances/:id/databases/:db/tables
func ListTables(c *gin.Context) {
id := c.Param("id")
dbName := c.Param("db")
var instance model.Instance
if err := model.DB.First(&instance, id).Error; err != nil {
resp.NotFound(c, "Instance not found")
return
}
tables, err := listTables(&instance, dbName)
if err != nil {
c.JSON(http.StatusOK, []interface{}{})
return
}
if tables == nil {
tables = []map[string]interface{}{}
}
c.JSON(http.StatusOK, tables)
}
// POST /api/v1/instances/test-connection (new instance, not saved yet)
func TestNewConnection(c *gin.Context) {
var req struct {
DBType string `json:"db_type"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Database string `json:"database"`
}
if err := c.ShouldBindJSON(&req); err != nil {
resp.BadRequest(c, "Invalid request")
return
}
instance := &model.Instance{
DBType: req.DBType,
Host: req.Host,
Port: req.Port,
Username: req.Username,
Password: req.Password,
}
if req.Database != "" {
instance.Database = &req.Database
}
result := testDBConnectionFull(instance)
c.JSON(http.StatusOK, gin.H{
"success": result.Success,
"message": result.Message,
"version": result.Version,
"cluster_mode": result.ClusterMode,
})
}