现象: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
133 lines
3.7 KiB
Go
133 lines
3.7 KiB
Go
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,
|
||
})
|
||
}
|