You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
137 lines
3.9 KiB
137 lines
3.9 KiB
package console
|
|
|
|
/*
|
|
账号数据作用域(后端强制隔离)。
|
|
|
|
登录时把账号绑定(apps/products/regions,CSV)写进 JWT,tokenValid 解析后放进 gin.Context。
|
|
每个数据接口用 scopeOf(c) 取出作用域,对查询/结果强制按绑定收敛——无论前端传什么、是否传 X-App-Id,
|
|
代理(3)/运营(4) 都只能拿到自己绑定范围内的数据;超管(1)/管理员(2) 不受限。
|
|
|
|
语义(与前端既有行为一致):某维度绑定为空 = 该维度不限制;非空 = 仅限列表内。
|
|
*/
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"yunyan/pb"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type acctScope struct {
|
|
unlimited bool // 超管/管理员:不受任何限制
|
|
apps []string // 绑定应用名(空=应用维度不限)
|
|
products []uint32 // 绑定产品 id(空=产品维度不限)
|
|
regions []string // 绑定区域代码(空=区域维度不限)
|
|
}
|
|
|
|
func ctxStr(c *gin.Context, k string) string {
|
|
if v, ok := c.Get(k); ok {
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func splitCSV(s string) []string {
|
|
out := make([]string, 0)
|
|
for _, p := range strings.Split(s, ",") {
|
|
if p = strings.TrimSpace(p); p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func splitCSVU32(s string) []uint32 {
|
|
out := make([]uint32, 0)
|
|
for _, p := range splitCSV(s) {
|
|
if n, err := strconv.ParseUint(p, 10, 32); err == nil {
|
|
out = append(out, uint32(n))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func containsStr(list []string, v string) bool {
|
|
for _, x := range list {
|
|
if x == v {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// scopeOf 从已鉴权的请求上下文解析当前账号的数据作用域。
|
|
func scopeOf(c *gin.Context) acctScope {
|
|
switch currentIdentity(c) {
|
|
case pb.Identity_Admin, pb.Identity_Manager:
|
|
return acctScope{unlimited: true} // 超管/管理员不受工厂/产品/应用限制
|
|
default:
|
|
return acctScope{
|
|
apps: splitCSV(ctxStr(c, "apps")),
|
|
products: splitCSVU32(ctxStr(c, "products")),
|
|
regions: splitCSV(ctxStr(c, "regions")),
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s acctScope) allowApp(name string) bool {
|
|
return s.unlimited || len(s.apps) == 0 || containsStr(s.apps, name)
|
|
}
|
|
|
|
func (s acctScope) allowProduct(id uint32) bool {
|
|
if s.unlimited || len(s.products) == 0 {
|
|
return true
|
|
}
|
|
for _, p := range s.products {
|
|
if p == id {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// filterProducts 把产品列表收敛到绑定范围内。
|
|
func (s acctScope) filterProducts(in []*pb.DBProduct) []*pb.DBProduct {
|
|
if s.unlimited || len(s.products) == 0 {
|
|
return in
|
|
}
|
|
out := make([]*pb.DBProduct, 0, len(in))
|
|
for _, p := range in {
|
|
if s.allowProduct(p.Id) {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// statsClause 返回账号作用域的统计过滤子句(与前端下钻条件 AND,强制收敛、防越权)。
|
|
//
|
|
// 口径:按"最具体的绑定"收敛——有产品绑定就**只按产品**,否则按应用。这样对绑定产品的代理,
|
|
// 「全部产品」(不传产品) 与「选该产品」(传 product_id) 落到同一过滤 product_id IN(绑定),结果一致。
|
|
// 注意:登录/用户/订单等"应用级"指标记在 product_id=0,按产品收敛后对代理显示 0(它们不挂产品);
|
|
// apps 绑定改为只用于"用户查询"页(X-App-Id 校验),不参与仪表盘统计。
|
|
// 区域再 AND 上(含未归属 region='',让产品级空区域行能通过)。超管/管理员或完全无绑定 → 返回空。
|
|
func (s acctScope) statsClause() (string, []interface{}) {
|
|
if s.unlimited {
|
|
return "", nil
|
|
}
|
|
conds := make([]string, 0, 2)
|
|
args := make([]interface{}, 0, 2)
|
|
|
|
if len(s.products) > 0 {
|
|
conds = append(conds, "product_id IN ?")
|
|
args = append(args, s.products)
|
|
} else if len(s.apps) > 0 {
|
|
conds = append(conds, "app_id IN ?")
|
|
args = append(args, s.apps)
|
|
}
|
|
if len(s.regions) > 0 {
|
|
conds = append(conds, "(region IN ? OR region = '')")
|
|
args = append(args, s.regions)
|
|
}
|
|
return strings.Join(conds, " AND "), args
|
|
}
|
|
|