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
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
|
|
}
|
|
|