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.
 
 
 
 
 
 

157 lines
5.7 KiB

package console
import (
"sort"
"strings"
"yunyan/pb"
"github.com/gin-gonic/gin"
)
// 「产品 → 应用」归属,以及由它推导出的账号可见应用集合。
//
// 背景:产品(DBProduct,硬件产品)与品牌商同在 console 主库,与「应用」(app_product/app_registry)
// 原本没有关联,导致品牌商/渠道商账号登录后拿不到任何应用(appbinds 只从账号手工勾选的 apps 推导),
// 用户查询页「无可用应用」。现在产品可绑定一到多个应用名(DBProduct.Appnames,CSV),于是:
//
// 品牌商账号可见应用 = 本品牌名下全部产品绑定的应用名并集
// 渠道商账号可见应用 = 其所属品牌商的同一集合(渠道商卖的就是该品牌的产品)
//
// 沿用全局的「非空才限制」语义:并集为空(产品都没绑应用,如存量数据)时不做限制,
// 免得升级后老账号突然一个应用都看不到。
// normalizeAppNames 归一「应用名 CSV」:去空白、去重、保序。空串原样返回。
func normalizeAppNames(csv string) string {
seen := make(map[string]bool, 4)
out := make([]string, 0, 4)
for _, n := range splitCSV(csv) {
if seen[n] {
continue
}
seen[n] = true
out = append(out, n)
}
return strings.Join(out, ",")
}
// checkAppNames 校验应用名都在应用中心(app_product)里;返回错误说明(空串=通过)。
// 查不到应用表时放行(不能因为注册表读失败就挡住产品保存)。
func (this *serverComp) checkAppNames(csv string) string {
names := splitCSV(csv)
if len(names) == 0 {
return ""
}
all, err := this.module.model.listProducts()
if err != nil || len(all) == 0 {
return ""
}
known := make(map[string]bool, len(all))
for _, p := range all {
known[p.Name] = true
}
for _, n := range names {
if !known[n] {
return "应用不存在: " + n
}
}
return ""
}
// brandAppNames 取某品牌商名下全部产品绑定的应用名并集(走产品缓存,避免每次查库)。
// 返回空切片 = 该品牌的产品都没绑应用,调用方按「不限制」处理。
func (this *serverComp) brandAppNames(brandId uint32) []string {
products, err := this.module.deviceCache.GetProducts()
if err != nil {
return nil
}
seen := make(map[string]bool, 4)
out := make([]string, 0, 4)
for _, p := range products {
if p.Brandid != brandId {
continue
}
for _, n := range splitCSV(p.Appnames) {
if !seen[n] {
seen[n] = true
out = append(out, n)
}
}
}
sort.Strings(out)
return out
}
// accountAppNames 当前账号在「应用」维度的绑定名单(空=该维度不限制):
// 品牌商/渠道商账号按名下产品的应用归属推导,其余角色沿用账号手工勾选的 apps。
func (this *serverComp) accountAppNames(c *gin.Context) []string {
s := scopeOf(c)
if s.unlimited {
return nil
}
if s.brandId != 0 {
return this.brandAppNames(s.brandId)
}
return s.apps
}
// getMyApps 返回当前账号可选的应用部署列表([{id,name,app_name,region}],只含已启用的)。
// 前端的应用下拉统一调它:超管/管理员拿到全部,代理/运营按账号绑定,品牌商/渠道商按产品绑定的应用。
// 与登录响应里的 appbinds 同源,但登录态不会随产品绑定变化而更新,故页面按需再拉一次。
func (this *serverComp) getMyApps(c *gin.Context) {
writeOK(c, gin.H{"apps": this.visibleAppBinds(this.accountAppNames(c), scopeOf(c).unlimited)})
}
// visibleAppBinds 把「应用名绑定」展开成已启用的部署行(一个应用的国内/海外各一行)。
// 绑定值可能是部署注册名(历史数据)或应用名(后台按应用勾选),两者都算命中。
// unlimited(超管/管理员)→ 全部已启用部署;否则严格按 names 过滤,names 为空即返回空列表
// (下拉只呈现「确实绑定了的应用」;接口层的越权拦截另见 requireAppScope,那里沿用「空=不限制」)。
func (this *serverComp) visibleAppBinds(names []string, unlimited bool) []gin.H {
out := make([]gin.H, 0, 4)
all, err := this.module.model.listApps()
if err != nil {
return out
}
want := make(map[string]bool, len(names))
for _, n := range names {
want[n] = true
}
for _, a := range all {
if !a.Enabled {
continue
}
if !unlimited && !want[a.Name] && !(a.AppName != "" && want[a.AppName]) {
continue
}
out = append(out, gin.H{
"id": a.Id, "name": a.Name, "region": a.Region, "app_name": a.AppName,
})
}
return out
}
// requireAppNameScope 校验「按应用名作用域」的写操作在当前账号权限内(agent 等按 app_name 隔离的配置用)。
// 与 requireAppScope(按 X-App-Id)同一套口径,只是这里的作用域键是应用名:
// - 超管/管理员(unlimited) 或 账号没有应用维度绑定 → 放行(沿用全局「非空才限制」语义);
// - 有绑定的账号只能写自己名下的应用,且不能写「全局默认」(app_name=”)——
// 那是所有应用的兜底,改一处影响所有人,只该由超管/管理员维护。
//
// 只用于写操作:读(列表)仍允许看全局默认,否则应用级账号看不到自己继承的那份配置。
func (this *serverComp) requireAppNameScope(c *gin.Context, appName string) bool {
if scopeOf(c).unlimited {
return true
}
allowed := this.accountAppNames(c)
if len(allowed) == 0 {
return true
}
if strings.TrimSpace(appName) == "" {
writeErr(c, pb.ErrorCode_InsufficientPermissions, "无权修改「全局默认」作用域,请先选择具体应用")
return false
}
if !containsStr(allowed, appName) {
writeErr(c, pb.ErrorCode_InsufficientPermissions, "无权访问该应用: "+appName)
return false
}
return true
}