fix: 企业账号只能访问授权给本企业的资产
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m25s
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
42
internal/service/asset/enterprise_scope.go
Normal file
42
internal/service/asset/enterprise_scope.go
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user