fix: 企业账号只能访问授权给本企业的资产
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m25s

/assets/:identifier/* 共用 AssetService.Resolve 解析资产,而店铺数据范围 ApplyShopFilter 对企业账号不加条件,
企业账号可以查询并操作任意卡或设备(资产详情、订单、套餐、停复机、停用、改套餐有效期与已用量、开关轮询、改实名状态)。

- Resolve 对企业账号校验卡或设备当前授权给本企业,未授权与不存在同样返回未找到
- 改套餐有效期、改已用量、停用、开关轮询、改实名状态对企业账号返回无权限;停复机按授权范围放行

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
邹进
2026-10-26 15:05:05 +09:00
parent 7963e5dadd
commit 82a2fcc306
4 changed files with 84 additions and 6 deletions

View File

@@ -391,6 +391,9 @@ func (h *AssetHandler) CurrentPackage(c *fiber.Ctx) error {
// UpdatePackageExpiresAt 修改资产套餐过期时间
// PATCH /api/admin/assets/:identifier/packages/:package_usage_id/expires-at
func (h *AssetHandler) UpdatePackageExpiresAt(c *fiber.Ctx) error {
if err := rejectEnterpriseAssetAdmin(c); err != nil {
return err
}
asset, packageUsageID, err := h.resolveAssetPackageUsagePath(c)
if err != nil {
@@ -417,6 +420,9 @@ func (h *AssetHandler) UpdatePackageExpiresAt(c *fiber.Ctx) error {
// UpdatePackageUsage 修改资产套餐已用量
// PATCH /api/admin/assets/:identifier/packages/:package_usage_id/used-data
func (h *AssetHandler) UpdatePackageUsage(c *fiber.Ctx) error {
if err := rejectEnterpriseAssetAdmin(c); err != nil {
return err
}
asset, packageUsageID, err := h.resolveAssetPackageUsagePath(c)
if err != nil {
return err
@@ -496,6 +502,9 @@ func (h *AssetHandler) Start(c *fiber.Ctx) error {
// Deactivate 停用资产
// PATCH /api/admin/assets/:identifier/deactivate
func (h *AssetHandler) Deactivate(c *fiber.Ctx) error {
if err := rejectEnterpriseAssetAdmin(c); err != nil {
return err
}
if h.assetLifecycleService == nil {
return errors.New(errors.CodeInternalError, "资产生命周期服务未配置")
}
@@ -590,6 +599,9 @@ func (h *AssetHandler) OperationLogs(c *fiber.Ctx) error {
// UpdatePollingStatus 更新资产轮询状态
// PATCH /api/admin/assets/:identifier/polling-status
func (h *AssetHandler) UpdatePollingStatus(c *fiber.Ctx) error {
if err := rejectEnterpriseAssetAdmin(c); err != nil {
return err
}
identifier := c.Params("identifier")
if identifier == "" {
return errors.New(errors.CodeInvalidParam)
@@ -678,6 +690,9 @@ func (h *AssetHandler) UpdateRealnamePolicy(c *fiber.Ctx) error {
// UpdateRealnameStatus 手动更新资产实名状态
// PATCH /api/admin/assets/:identifier/realname-status
func (h *AssetHandler) UpdateRealnameStatus(c *fiber.Ctx) error {
if err := rejectEnterpriseAssetAdmin(c); err != nil {
return err
}
identifier := c.Params("identifier")
if identifier == "" {
return errors.New(errors.CodeInvalidParam)
@@ -770,3 +785,12 @@ func assetIdentifierForAudit(asset *dto.AssetResolveResponse) string {
}
return asset.Identifier
}
// rejectEnterpriseAssetAdmin 拒绝企业账号执行平台管理类资产写操作(改套餐有效期与已用量、停用、开关轮询、改实名状态)。
// 企业账号即使对已授权资产也不能执行这些操作;停复机仍按企业授权范围放行(由 Resolve 约束)。
func rejectEnterpriseAssetAdmin(c *fiber.Ctx) error {
if middleware.GetUserTypeFromContext(c.UserContext()) == constants.UserTypeEnterprise {
return errors.New(errors.CodeForbidden, "企业账号无权执行该资产操作")
}
return nil
}

View File

@@ -14,7 +14,7 @@ func registerAssetRoutes(router fiber.Router, handler *admin.AssetHandler, walle
Register(assets, doc, groupPath, "GET", "/resolve/:identifier", handler.Resolve, RouteSpec{
Summary: "解析资产",
Description: "通过虚拟号/ICCID/IMEI/SN/MSISDN 解析设备或卡的完整详情。exchange_trace 始终存在,previous_asset/next_asset 无关系时为 null;关联项仅在 can_view=true 且 asset_id 非空时允许跳转。企业账号禁止调用。",
Description: "通过虚拟号/ICCID/IMEI/SN/MSISDN 解析设备或卡的完整详情。exchange_trace 始终存在,previous_asset/next_asset 无关系时为 null;关联项仅在 can_view=true 且 asset_id 非空时允许跳转。企业账号禁止调用。企业账号只能解析当前授权给本企业的卡或设备,未授权与不存在同样返回未找到;/assets/:identifier/* 下其余接口同此范围。",
Tags: []string{"资产管理"},
Input: new(dto.AssetResolveRequest),
Output: new(dto.AssetResolveResponse),
@@ -58,7 +58,7 @@ func registerAssetRoutes(router fiber.Router, handler *admin.AssetHandler, walle
Register(assets, doc, groupPath, "PATCH", "/:identifier/packages/:package_usage_id/expires-at", handler.UpdatePackageExpiresAt, RouteSpec{
Summary: "修改资产套餐过期时间",
Description: "修改指定资产下某条套餐使用记录的过期时间。仅允许已认证账号调用,并保留资产与套餐记录归属校验。",
Description: "修改指定资产下某条套餐使用记录的过期时间。仅允许已认证账号调用,并保留资产与套餐记录归属校验。企业账号无权调用。",
Tags: []string{"资产管理"},
Input: new(dto.UpdateAssetPackageExpiresAtRequest),
Output: new(dto.AssetPackageResponse),
@@ -67,7 +67,7 @@ func registerAssetRoutes(router fiber.Router, handler *admin.AssetHandler, walle
Register(assets, doc, groupPath, "PATCH", "/:identifier/packages/:package_usage_id/used-data", handler.UpdatePackageUsage, RouteSpec{
Summary: "修改资产套餐已用量",
Description: "修改指定资产下某条套餐使用记录的真实已用量(MB)。仅允许已认证账号调用,并保留资产与套餐记录归属校验。",
Description: "修改指定资产下某条套餐使用记录的真实已用量(MB)。仅允许已认证账号调用,并保留资产与套餐记录归属校验。企业账号无权调用。",
Tags: []string{"资产管理"},
Input: new(dto.UpdateAssetPackageUsageRequest),
Output: new(dto.AssetPackageResponse),
@@ -94,7 +94,7 @@ func registerAssetRoutes(router fiber.Router, handler *admin.AssetHandler, walle
Register(assets, doc, groupPath, "PATCH", "/:identifier/deactivate", handler.Deactivate, RouteSpec{
Summary: "停用资产",
Description: "将资产状态设置为停用(不可逆)。仅支持库存中或已售出的资产。",
Description: "将资产状态设置为停用(不可逆)。仅支持库存中或已售出的资产。企业账号无权调用。",
Tags: []string{"资产管理"},
Input: new(dto.AssetIdentifierRequest),
Output: nil,
@@ -139,7 +139,7 @@ func registerAssetRoutes(router fiber.Router, handler *admin.AssetHandler, walle
Register(assets, doc, groupPath, "PATCH", "/:identifier/polling-status", handler.UpdatePollingStatus, RouteSpec{
Summary: "更新资产轮询状态",
Description: "启用或禁用指定资产(IoT卡或设备)的定期状态轮询;同事务写不轮询清单(来源单资产接口)与审计。网关扩展状态为风险停机或已销户的卡不能开启轮询,返回 403。设备开关级联到设备下各卡(开启时跳过风险停机、已销户的卡)。影响说明见 GET /polling-status/impact。",
Description: "启用或禁用指定资产(IoT卡或设备)的定期状态轮询;同事务写不轮询清单(来源单资产接口)与审计。网关扩展状态为风险停机或已销户的卡不能开启轮询,返回 403。设备开关级联到设备下各卡(开启时跳过风险停机、已销户的卡)。影响说明见 GET /polling-status/impact。企业账号无权调用。",
Tags: []string{"资产管理"},
Input: new(dto.UpdateAssetPollingStatusRequest),
Output: new(dto.UpdateAssetPollingStatusResponse),
@@ -157,7 +157,7 @@ func registerAssetRoutes(router fiber.Router, handler *admin.AssetHandler, walle
Register(assets, doc, groupPath, "PATCH", "/:identifier/realname-status", handler.UpdateRealnameStatus, RouteSpec{
Summary: "手动更新卡实名状态",
Description: "手动将指定卡资产标记为已实名或未实名。状态更新后会触发停复机评估,并在标记为已实名时触发待实名套餐激活。",
Description: "手动将指定卡资产标记为已实名或未实名。状态更新后会触发停复机评估,并在标记为已实名时触发待实名套餐激活。企业账号无权调用。",
Tags: []string{"资产管理"},
Input: new(dto.UpdateAssetRealnameStatusRequest),
Output: new(dto.UpdateAssetRealnameStatusResponse),

View File

@@ -0,0 +1,42 @@
package asset
import (
"context"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
)
// ensureEnterpriseAssetVisible 校验企业账号只能访问当前授权给本企业的卡或设备。
// 非企业账号直接放行(由原有店铺数据范围约束)。未授权与不存在返回同一错误,不暴露资产是否存在。
// 设备授权时会同步为设备内的卡写入卡授权记录,因此卡只看卡授权表。
func (s *Service) ensureEnterpriseAssetVisible(ctx context.Context, assetType string, assetID uint) error {
if middleware.GetUserTypeFromContext(ctx) != constants.UserTypeEnterprise {
return nil
}
notFound := errors.New(errors.CodeNotFound, "未找到匹配的资产")
enterpriseID := middleware.GetEnterpriseIDFromContext(ctx)
if enterpriseID == 0 || assetID == 0 {
return notFound
}
table, column := "", ""
switch assetType {
case "card", constants.AssetTypeIotCard:
table, column = "tb_enterprise_card_authorization", "card_id"
case constants.AssetTypeDevice:
table, column = "tb_enterprise_device_authorization", "device_id"
default:
return notFound
}
var count int64
if err := s.db.WithContext(ctx).Table(table).
Where(column+" = ? AND enterprise_id = ? AND revoked_at IS NULL AND deleted_at IS NULL", assetID, enterpriseID).
Count(&count).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "校验企业资产授权失败")
}
if count == 0 {
return notFound
}
return nil
}

View File

@@ -118,7 +118,19 @@ func (s *Service) SetAccessAudit(writer *infraAudit.Writer) {
// 主路径:查注册表(精确匹配 ICCID 或 VirtualNo)
// Fallback:原有跨表 OR 查询(处理 IMEI/SN/MSISDN 等非注册标识符)
// includeUsageSummary 为 true 时额外填充当前世代流量汇总字段
// 企业账号只能解析当前授权给本企业的卡或设备,其余一律按"未找到"处理(/assets/:identifier/* 共用此入口)。
func (s *Service) Resolve(ctx context.Context, identifier string, includeUsageSummary bool) (*dto.AssetResolveResponse, error) {
resp, err := s.resolveAsset(ctx, identifier, includeUsageSummary)
if err != nil || resp == nil {
return resp, err
}
if err := s.ensureEnterpriseAssetVisible(ctx, resp.AssetType, resp.AssetID); err != nil {
return nil, err
}
return resp, nil
}
func (s *Service) resolveAsset(ctx context.Context, identifier string, includeUsageSummary bool) (*dto.AssetResolveResponse, error) {
if s.assetIdentifierStore != nil {
regRecord, regErr := s.assetIdentifierStore.FindByIdentifier(ctx, identifier)
if regErr == nil && regRecord != nil {