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.
 
 
 
 
 
 

278 lines
8.7 KiB

package api
import (
"strings"
"yunyan/comm"
"yunyan/lego/core"
"yunyan/lego/core/cbase"
"yunyan/lego/sys/log"
"yunyan/pb"
)
// 接口授权语义
const (
scopeNone = "*none" // 无需登录
scopeAdmin = "*admin" // 仅站长
scopeAny = "*any" // 已登录任意身份均可(工具类接口,无对应页面)
scopeAgent = "*agent" // 仅代理身份(identity=3)
)
// apiPageMap 接口 -> 后台页面 id(与 console/admin.html 中 scopedPages 对齐)
// 取值含义:
// - 页面 id(dashboard/factory/...):Admin/Manager 直通;Agent/Operator 需在 access 中勾选
// - scopeNone:完全开放
// - scopeAdmin:仅 Admin
// - scopeAny:登录后任意身份可访问(无页面归属的工具类接口)
var apiPageMap = map[string]string{
// === 无需登录 ===
"api_login": scopeNone,
"api_getsiteinfo": scopeNone,
// === 仅站长(账号管理) ===
"api_addadminuser": scopeAdmin,
"api_deladminuser": scopeAdmin,
"api_updateadminuser": scopeAdmin,
"api_getadminusers": scopeAdmin,
"api_grantadminresource": scopeAdmin, // 超管→后台账号 资源点赠送
// === dashboard 仪表盘 ===
"api_getglobalstats": "dashboard",
"api_rebuildstats": scopeAdmin, // 全量重建统计(清空+全库重算,仅超管)
// === factory 品牌商管理 ===
// === channel 渠道商管理(挂在品牌商下) ===
// 生产授权码要选渠道商,故 device 页也需读渠道列表;读接口归 channel 页,
// 审批(api_auditchannel)另在接口内限制为超管/管理员。
"api_getchannel": "channel",
"api_getchannels": "channel",
"api_addchannel": "channel",
"api_updatechannel": "channel",
"api_delchannel": "channel",
"api_auditchannel": "channel",
// === product 产品管理 ===
"api_getproduct": "product",
"api_getproducts": "product",
"api_getproductversions": "product",
"api_addproduct": "product",
"api_updateproduct": "product",
"api_delproduct": "product",
"api_addproductversion": "product",
"api_delproductversion": "product",
// === device 生产管理 ===
"api_getauthcode": "device",
"api_getauthcodes": "device",
"api_createauthcodes": "device",
"api_disableauthcodes": "device",
"api_restfactorydevics": "device",
"api_resetlicensestatus": "device",
"api_getproductionbatches": "device",
"api_revokeproductionbatch": scopeAdmin, // 撤销生产批次,仅超管
// === goods 商品管理 ===
"api_getgoods": "goods",
"api_getgoodss": "goods",
"api_addgoods": "goods",
"api_updategoods": "goods",
"api_delgoods": "goods",
// === order 订单查询 ===
"api_getpayorders": "order",
"api_getpaystats": "order",
// === user 用户查询 ===
"api_getuser": "user",
"api_updateuser": "user",
"api_getuseruselog": "user",
"api_getuserechomeetrecords": "userechomeet",
"api_dispatchresource": "user",
// === transaction 流水 ===
// 代理在此接口内被强制只看自己派发的流水(handler 内做隔离),其它身份可自由按类型/UID过滤
"api_getuseruselogspaged": scopeAny,
"api_getadminresourcelogs": scopeAny, // 超管查全部,其他账号只能查自己;handler 内做隔离
// === config 服务配置 ===
"api_getconfig": "config",
"api_addconfig": "config",
"api_updateconfig": "config",
"api_delconfig": "config",
// === mcp MCP配置 ===
"api_getmcpserver": "mcp",
"api_getmcpservers": "mcp",
"api_addmcpservers": "mcp",
"api_updatemcpserver": "mcp",
"api_delmcpservers": "mcp",
// === agent Agent管理 ===
"api_getagent": "agent",
"api_getagents": "agent",
"api_addagent": "agent",
"api_updateagent": "agent",
"api_delagent": "agent",
// === template 模板管理 ===
"api_gettemplates": "template",
"api_gettemplate": "template",
"api_addtemplates": "template",
"api_updatetemplates": "template",
"api_deltemplate": "template",
// === channelapp 渠道应用 ===
"api_getchannelapp": "channelapp",
"api_getchannelapps": "channelapp",
"api_addchannelapp": "channelapp",
"api_uploadchannelapp": "channelapp",
"api_delchannelapp": "channelapp",
// === wakeup 唤醒词管理 ===
"api_getwakeupvoice": "wakeup",
"api_getwakeupvoices": "wakeup",
"api_addwakeupvoice": "wakeup",
"api_updatewakeupvoice": "wakeup",
"api_delwakeupvoice": "wakeup",
// === sysconfig 系统配置 ===
// === audit 操作日志 ===
"api_getauditlogs": "audit",
"api_getconsolelogs": "audit",
"api_getconsolelogfilters": "audit",
// === agentdashboard 代理仪表盘 ===
"api_getagentdashboard": scopeAgent,
// === 工具类接口(无页面归属,登录即可) ===
"api_sendsmtpemail": scopeAny,
"api_translate": scopeAny,
"api_getcostoken": scopeAny,
"api_getuserrank": scopeAny, // 用户使用量排行 dashboard / agentdashboard 共用
"api_getmyadminpool": scopeAny, // 当前账号资源点池查询(派发界面余额展示)
}
// permissionInterceptorComp 权限拦截器组件
type permissionInterceptorComp struct {
cbase.ModuleCompBase
service core.IService
module *API
}
func (this *permissionInterceptorComp) Init(service core.IService, module core.IModule, comp core.IModuleComp, opt core.IModuleOptions) (err error) {
this.ModuleCompBase.Init(service, module, comp, opt)
this.service = service
this.module = module.(*API)
return
}
func (this *permissionInterceptorComp) Start() (err error) {
if err = this.ModuleCompBase.Start(); err != nil {
return
}
var scomp core.IServiceComp
if scomp, err = this.service.GetComp(comm.SC_ServiceHttpRouteComp); err != nil {
return
}
scomp.(comm.ISC_HttpRouteComp).AddInterceptor(this.checkPermission)
return
}
// checkPermission 权限校验拦截器
func (this *permissionInterceptorComp) checkPermission(apiName string, session comm.IUserSession, msg interface{}) *pb.ErrorData {
// console 运维接口(重载/重置业务配置)不走后台管理员登录——调用方是 console 而非人,没有 identity。
// 它们的身份由**网关**用 ${FIELD_ENCRYPT_KEY} 的 HMAC 签名验过(见 comm.IsConsoleOnlyRoute),
// 且网关已禁止它们从不验签的 /web 入口进入,故此处放行是安全的。
//
// 用 comm.IsConsoleOnlyRoute 而不是往 apiPageMap 里加 scopeNone:与网关共用同一份清单,
// 免得两边各维护一份、哪天漂移成"网关验签、拦截器却放行任意来源"。
if comm.IsConsoleOnlyRoute(apiName) {
return nil
}
scope, exists := apiPageMap[apiName]
if !exists {
// 未配置的接口默认仅 Admin 可访问,避免新接口意外暴露
scope = scopeAdmin
}
if scope == scopeNone {
return nil
}
identityStr := session.GetMateToString("identity")
if identityStr == "" {
return &pb.ErrorData{
Code: pb.ErrorCode_NoLogin,
Message: "未登录,请先登录",
}
}
identity := pb.Identity(session.GetMateToInt32("identity"))
if identity <= 0 {
return &pb.ErrorData{
Code: pb.ErrorCode_NoLogin,
Message: "登录信息异常",
}
}
switch scope {
case scopeAdmin:
if identity != pb.Identity_Admin {
return permDenied()
}
return nil
case scopeAny:
return nil
case scopeAgent:
if identity != pb.Identity_Agent {
return permDenied()
}
return nil
}
// 按页面授权:Admin/Manager 直通;Agent/Operator 需在 access 中勾选
if identity == pb.Identity_Admin || identity == pb.Identity_Manager {
return nil
}
if identity != pb.Identity_Agent && identity != pb.Identity_Operator {
return permDenied()
}
account := session.GetMateToString(comm.SessionMeta_UserId)
if account == "" {
log.Errorf("[perm] api=%s identity=%d account=empty", apiName, identity)
return permDenied()
}
user, err := this.module.model.findforaccount(account)
if err != nil || user == nil {
log.Errorf("[perm] api=%s identity=%d account=%s findforaccount err=%v user=%v", apiName, identity, account, err, user)
return permDenied()
}
if !accessHasPage(user.Access, scope) {
log.Errorf("[perm] api=%s identity=%d account=%s page=%s access=%q -> denied", apiName, identity, account, scope, user.Access)
return permDenied()
}
log.Debugf("[perm] api=%s identity=%d account=%s page=%s access=%q -> allow", apiName, identity, account, scope, user.Access)
return nil
}
func accessHasPage(access, page string) bool {
if access == "" || page == "" {
return false
}
for _, p := range strings.Split(access, ",") {
if strings.TrimSpace(p) == page {
return true
}
}
return false
}
func permDenied() *pb.ErrorData {
return &pb.ErrorData{
Code: pb.ErrorCode_InsufficientPermissions,
Message: "权限不足,需要更高级别权限",
}
}