蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】— CMDB #13:IAM 权限模型:资源层级权限控制与 RBAC 集成

蓝鲸 CMDB 3.14.6 源码专题【左扬精讲】— CMDB #13:IAM 权限模型:资源层级权限控制与 RBAC 集成

SRE 日常运维里最常见的权限事故,往往不是 "没有权限控制",而是 "权限控制得太粗":一个运维账号既能删主机、又能改业务拓扑、还能归档项目,一失误就是大面积数据污染。

蓝鲸 CMDB 把这块能力交给 IAM(Identity and Access Management)来做,核心目的就是让 "谁、在什么范围、能对什么资源、做什么操作" 都变成可配置、可审计的细粒度策略。

这篇博文我们不谈抽象的 RBAC 概念,而是直接看 CMDB 3.14.6 里 IAM 集成的真实源码:EnableAuthorize() 总开关如何切断整条鉴权链路、CMDB 资源类型如何映射到 IAM 资源类型、批量鉴权请求如何组装与下发、以及创建者权限如何通过 RegisterResourceCreatorAction 自动注册。读完你会发现,CMDB 的权限模型本质上是一层 "翻译器 + 批量调度器"

    src/ac/iam/iam.go                        ← IAM 主模块:EnableAuthorize / Register / AuthorizeBatch
        src/ac/iam/adaptor.go                    ← CMDB ↔ IAM 资源/动作映射适配器
        src/ac/iam/types.go                      ← IAM 系统标识、ResourceType、ActionID 等常量定义
        src/common/auth/auth.go                  ← 权限总开关:EnableAuthorize()
        src/ac/meta/meta.go                      ← ResourceType / Action / ResourceAttribute 元数据
        src/scene_server/host_server/service/dynamic_grouping.go  ← IAM 注册示例:registerActionToIAM

IAM AuthorizeBatch AdaptAuthOptions ccIamResTypeMap RegisterResourceCreatorAction v3.14.6

必掌握

  • 权限总开关EnableAuthorize() 如何一键关闭/开启整套鉴权链路
  • 资源类型映射ccIamResTypeMap 把 CMDB 的 meta.ResourceType 翻译成 IAM 的 TypeID
  • 动作映射ConvertResourceAction 把 CMDB 的 meta.Action 翻译成 IAM 的 ActionID
  • 批量鉴权AuthorizeBatchAuthorizeAnyBatch 批量下发 IAM 请求
  • 创建者授权RegisterResourceCreatorAction 向 IAM 注册资源创建者的编辑权限

了解

  • SyncIAMSysInstances 的三轮差分同步机制(删除/新增/更新)
  • 动态分组(BizCustomQuery)在 IAM 中的特殊处理

一、IAM 在 CMDB 架构中的定位

What — IAM 在 CMDB 里扮演什么角色?

蓝鲸 IAM(Identity and Access Management)是蓝鲸体系的统一权限中心,所有蓝鲸系产品都通过它管理资源权限。CMDB 接入 IAM 的方式不是把权限数据同步过去,而是注册一套"资源类型 + 动作"定义,然后在每次资源操作时向 IAM 发起鉴权请求。IAM 本身不存储 CMDB 的资源数据,只存储"谁对什么资源有什么操作权限"这条策略。

Why — 为什么 CMDB 需要接入 IAM?解决了什么问题?

问题一:跨系统的统一权限管控

蓝鲸体系中可能有多个产品(CMDB、作业平台、监控等)都涉及主机、业务等资源的操作。如果每个产品各自维护一套权限体系,SRE 要在多个地方配权限,而且无法保证一致性。IAM 提供了一个集中的权限策略管理界面,CMDB 只需要注册资源类型和接受鉴权结果。

问题二:细粒度的资源层级权限

CMDB 的资源有层级关系:业务(Biz)→ 集群(Set)→ 模块(Module)→ 主机(Host)。如果只控制到"业务"级别,一个运维能删整个业务下的所有主机,风险太大。IAM 支持从业务级到实例级的逐层继承与覆盖,让权限粒度可控。

问题三:权限变更的实时生效

在 IAM 后台修改某个用户的权限后,CMDB 下次请求鉴权时就能感知到变化。无需重启 CMDB 服务,也无需同步权限数据。

没有 IAM 会发生什么?

  • 没有跨产品统一权限:每个产品各搞一套,权限分散、难以审计
  • 没有细粒度控制:只能控制到业务级别,无法精确到主机或实例
  • 没有统一审计:权限变更历史分散在各产品中,无法统一追溯
  • 无法与蓝鲸体系集成:作业平台、监控等依赖 IAM 做跨产品授权
How — 从源码看 IAM 集成的三层架构

src/ac/iam/iam.go 第 45-47 行,IAM 结构体非常简洁:

type IAM struct {
            Client iamClientInterface
        }

整个 IAM 模块的核心逻辑就两个:注册(Register)鉴权(Authorize)Client 是 IAM 的 HTTP 客户端,负责与 IAM 服务通信。NewIAM 在第 50-93 行初始化客户端,如果 !auth.EnableAuthorize(),则返回一个空的 IAM,所有鉴权直接放行。

src/ac/meta/meta.go,CMDB 侧定义了 ResourceType 枚举(所有支持的资源类型)和 Action 枚举(所有操作类型)。这些枚举值在 src/ac/iam/adaptor.go 中被翻译成 IAM 侧的对应值。

三层架构总结:

  • 1 层:CMDB API 层(api server)接收请求,调用 AuthManager.Authorizer.AuthorizeBatch
  • 2 层:适配器层(src/ac/iam/adaptor.go)把 CMDB 资源翻译成 IAM 资源格式
  • 3 层:IAM Client 层(src/ac/iam/client.go)发起 HTTP 请求到蓝鲸权限中心

本节总结:IAM 是 CMDB 的"权限代理"

  • CMDB 负责注册资源类型到 IAM,并接收 IAM 的鉴权结果
  • 权限数据存储在 IAM,不在 CMDB
  • EnableAuthorize() 开关可以让 CMDB 完全绕过 IAM

二、权限总开关:EnableAuthorize() 的全局控制

What — EnableAuthorize() 在做什么?

EnableAuthorize() 定义在 src/common/auth/auth.go 第 68-71 行,是 CMDB 权限体系的全局总开关。它读取配置文件中的 enableAuth 布尔值,返回 true 表示开启鉴权,false 表示跳过所有鉴权。

Why — 为什么需要全局开关?

场景一:开发/测试环境

在没有部署 IAM 服务的开发测试环境里,如果 CMDB 强制要求 IAM 鉴权,所有请求都会被拒绝,导致服务完全不可用。开启 EnableAuthorize=false 后,CMDB 所有鉴权直接放行,开发调试不受影响。

场景二:IAM 服务故障时的降级

如果 IAM 服务宕机,CMDB 可以通过关闭鉴权开关实现降级,保证核心业务不受影响。但要注意:关闭鉴权后所有操作不再有权限控制,属于高危操作。

场景三:权限迁移过渡期

从旧版权限体系迁移到 IAM 时,可以先关闭开关,配置好 IAM 策略后再开启,实现平滑过渡。

How — EnableAuthorize 的实现细节

src/common/auth/auth.go 第 23-71 行完整源码:

var EnableAuth = "true"
        var enableAuth = true
        var EnableAuthFlag *authValue
        var once = sync.Once{}
        
        type authValue struct{}
        
        func (a *authValue) Set(s string) error {
            v, err := strconv.ParseBool(s)
            if err != nil {
                return err
            }
            setEnableAuth(v)
            return nil
        }
        
        func setEnableAuth(enable bool) {
            once.Do(func() {
                enableAuth = enable
            })
        }
        
        func EnableAuthorize() bool {
            return enableAuth
        }

这里有几个关键设计点:

  1. 配置文件驱动EnableAuth 从配置文件中读取字符串,用 strconv.ParseBool 转成布尔值。配置值可以是 "true""false""1""0" 等。
  2. once.Do 保证只设置一次setEnableAuthsync.Once 保证 enableAuth 只在首次调用时被赋值,后续调用直接忽略。这防止了运行中重复设置开关导致的状态不一致。
  3. nil client 直接放行:在 src/ac/iam/iam.go 第 52-53 行,如果 !auth.EnableAuthorize(),则返回空的 &IAM{}Client 为 nil,所有后续调用都不发请求。

src/ac/iam/iam.go 第 1261-1269 行,parseAttributesToBatchOptions 在入口处就做了开关判断:

func parseAttributesToBatchOptions(rid string, user meta.UserInfo,
            resources ...meta.ResourceAttribute) (*types.AuthBatchOptions, []types.Decision, error) {
            if !auth.EnableAuthorize() {
                decisions := make([]types.Decision, len(resources))
                for i := range decisions {
                    decisions[i].Authorized = true
                }
                return nil, decisions, nil
            }
            // ... 正常鉴权逻辑
        }

开关关闭时,直接把所有资源的鉴权结果设为 Authorized=true,跳过所有 IAM 请求。

本节总结:EnableAuthorize() 的 3 个关键特性

  • 1 个配置文件驱动:EnableAuth 字段从配置读取
  • 1 次性赋值:sync.Once 保证运行中只设置一次
  • 1 个短路逻辑:关闭时所有鉴权直接放行,零 IAM 调用

三、注册链路:Register() 的 11 步依赖顺序

What — Register() 在做什么?

Register 定义在 src/ac/iam/iam.go 第 142-226 行,是 CMDB 启动时将自身资源类型和动作注册到 IAM 的入口函数。它不是每次请求都调用,而是在 CMDB 初始化或资源配置变更时才触发。

Why — 为什么注册顺序如此重要?

问题一:资源间有依赖关系

IAM 中资源类型的依赖关系是:ResourceType → InstanceSelection → Action。一个 Action 要关联到某个 ResourceType,必须先注册该 ResourceType 和它所依赖的 InstanceSelection。如果先注册 Action,后注册 ResourceType,IAM 会报依赖缺失错误。

问题二:删除和新建的顺序不同

删除时:先删 Action,再删 InstanceSelection,最后删 ResourceType(因为 Action 依赖前两者)。新建时:先建 ResourceType,再建 InstanceSelection,最后建 Action(因为 Action 依赖前两者)。

问题三:同名冲突需要中间变量

如果两个资源类型的名称互相冲突(一个从 A 改成 B,一个从 B 改成 A),直接改名会导致中间状态失败。源码用加下划线 "_" 作为中间变量,分两步完成改名。

How — Register() 的 11 步执行顺序

src/ac/iam/iam.go 第 122-139 行,注释里清楚记录了 11 步执行顺序:

/**
        1. 注册cc系统信息
        2. 删除Action。该操作无依赖
        3. 更新ResourceType,先更新名字冲突的为中间值,再更新其它
        4. 新增ResourceType。依赖于第3步中同名ResourceType均已更新
        5. 更新InstanceSelection,先更新名字冲突的为中间值,再更新其它
        6. 新增InstanceSelection。依赖于第5步+第4步
        7. 更新ResourceAction,先更新名字冲突的为中间值,再更新其它
        8. 新增ResourceAction。依赖于第7步+第6步
        9. 删除InstanceSelection。依赖于第2步和第7步
        10. 删除ResourceType。依赖于第5步和第9步
        11. 注册ActionGroup、ResCreatorAction、CommonAction
        **/

src/ac/iam/iam.go 第 142-226 行,Register 的核心逻辑:

func (i IAM) Register(ctx context.Context, redisCli redis.Client,
            opt *RegisterIamOptions, rid string) error {
            if !auth.EnableAuthorize() {
                return nil
            }
        
            locker, err := tryLockRegister(redisCli, rid)
            if err != nil {
                return err
            }
            defer locker.Unlock()
        
            // 1. 注册系统信息
            registeredInfo, err := i.registerSystem(ctx, opt.Host)
        
            // 对比三方差异:新增/更新/删除
            newResTypes, updateResTypes, removedResTypeIDs := i.crossCompareResTypes(...)
            newInstSelections, updateInstSelections, removedInstSelectionIDs := i.crossCompareInstSelections(...)
            newResActions, updateResActions, removedResActionIDs := i.crossCompareResActions(...)
        
            // 2. 删除Action(无依赖)
            i.removeResActions(ctx, removedResActionIDs, rid)
        
            // 3-4. 更新+新增 ResourceType
            for _, rt := range updateResTypes { i.Client.UpdateResourcesType(ctx, rt) }
            i.Client.RegisterResourcesTypes(ctx, newResTypes)
        
            // 5-6. 更新+新增 InstanceSelection
            for _, sel := range updateInstSelections { i.Client.UpdateInstanceSelection(ctx, sel) }
            i.Client.RegisterInstanceSelections(ctx, newInstSelections)
        
            // 7-8. 更新+新增 ResourceAction
            for _, act := range updateResActions { i.Client.UpdateAction(ctx, act) }
            i.Client.RegisterActions(ctx, newResActions)
        
            // 9-10. 删除 InstanceSelection 和 ResourceType
            i.Client.DeleteInstanceSelections(ctx, removedInstSelectionIDs)
            i.Client.DeleteResourcesTypes(ctx, removedResTypeIDs)
        
            // 11. 注册 ActionGroup / ResCreatorAction / CommonAction
            i.registerActionGroups(ctx, registeredInfo, opt.Objects, rid)
            i.registerResCreatorActions(ctx, registeredInfo, rid)
            i.registerCommonActions(ctx, registeredInfo, rid)
            return nil
        }

这里最值得注意的设计是 Redis 分布式锁tryLockRegister)。在第 96-114 行,tryLockRegister 通过 Redis SETNX 获取锁,重试 3 次,每次间隔 5 秒。如果 CMDB 多个实例同时启动,只有拿到锁的实例才执行注册,其他实例等待并复用已有结果。

src/ac/iam/iam.go 第 285-375 行,crossCompareResTypes 做了三轮比较:先找出已有和新定义的 ResourceType 的差异,然后分类为新增/更新/删除。特别处理了"先改名成中间变量再加正确名字"的两次更新场景,防止同名冲突。

本节总结:Register() 的 3 个关键机制

  • 1 个 Redis 分布式锁:tryLockRegister 保证只有一个实例执行注册
  • 3 轮差分比较:ResourceType / InstanceSelection / ResourceAction 各做一次新增/更新/删除
  • 11 步严格顺序:删除顺序和新建顺序相反,防止依赖缺失

四、适配器链路:AdaptAuthOptions 的三层翻译

What — AdaptAuthOptions 在做什么?

AdaptAuthOptions 定义在 src/ac/iam/adaptor.go 第 29-51 行,是 CMDB 资源对象翻译成 IAM 鉴权请求的核心函数。每次用户对 CMDB 资源执行操作时,CMDB 会调用这个函数把 meta.ResourceAttribute 转换成 IAM 的 ActionID + []types.Resource

Why — 为什么需要翻译层?

问题一:CMDB 和 IAM 的资源类型体系不同

CMDB 的资源类型枚举(meta.ResourceType)和 IAM 的资源类型 ID(TypeID)名字不同、粒度不同。比如 CMDB 的 meta.HostInstance 在 IAM 侧就是 "host";CMDB 的 meta.DynamicGrouping 在 IAM 侧是 "biz_custom_query"

问题二:同一 CMDB 动作在不同业务下映射到不同 IAM 动作

比如主机编辑操作,如果是在业务下(businessID > 0),映射到 EditBusinessHost;如果在资源池下(businessID = 0),映射到 EditResourcePoolHost。这个业务上下文在翻译层处理。

问题三:部分操作不需要鉴权

有些 CMDB 内部操作(如 meta.SkipAction)不需要走 IAM,翻译层直接返回 Skip

How — AdaptAuthOptions 的三层翻译

src/ac/iam/adaptor.go 第 29-51 行,核心翻译函数:

func AdaptAuthOptions(a *meta.ResourceAttribute) (ActionID, []types.Resource, error) {
            // 第一层:翻译动作
            action, err := ConvertResourceAction(a.Type, a.Action, a.BusinessID)
            if err != nil {
                return "", nil, err
            }
        
            // 第二层:翻译资源类型
            rscType, err := ConvertResourceType(a.Type, a.BusinessID)
            if err != nil {
                return "", nil, err
            }
        
            // 第三层:生成资源实例
            resource, err := GenIamResource(action, *rscType, a)
            if err != nil {
                return "", nil, err
            }
        
            return action, resource, nil
        }

三层翻译的分工非常清晰:

  1. 第一层:动作翻译ConvertResourceAction):把 CMDB 的动作枚举(meta.Create/Update/Delete/Find)翻译成 IAM 的 ActionID(如 "edit_business_host")。这里会考虑 BusinessID 的正负决定具体映射到业务动作还是资源池动作。
  2. 第二层:类型翻译ConvertResourceType):把 CMDB 的资源类型枚举翻译成 IAM 的 TypeID。如果 CMDB 资源类型在 ccIamResTypeMap 中找不到,就检查是否是系统实例(IsCMDBSysInstance),是的话直接用类型名字符串作为 IAM TypeID。
  3. 第三层:资源实例生成GenIamResource):根据翻译后的 ActionID 和 TypeID,构建 IAM 的资源实例对象,包括资源 ID、父级链路(path)等信息。

本节总结:AdaptAuthOptions 的 3 层翻译

  • 第 1 层:ConvertResourceAction → CMDB 动作 → IAM ActionID
  • 第 2 层:ConvertResourceType → CMDB 资源类型 → IAM TypeID
  • 第 3 层:GenIamResource → 生成 IAM 资源实例(含路径父子关系)

五、资源类型映射:ccIamResTypeMap 完整解析

What — ccIamResTypeMap 是什么?

ccIamResTypeMap 定义在 src/ac/iam/adaptor.go 第 53-111 行,是一个从 CMDB 资源类型枚举到 IAM TypeID 的静态映射表。CMDB 中所有定义的资源类型都通过这张表找到在 IAM 侧对应的 TypeID。

Why — 为什么需要静态映射表?

问题一:两套命名体系不同

CMDB 的资源类型用 meta.ResourceType 枚举,名字偏向业务语义(如 BusinessHostInstanceDynamicGrouping)。IAM 的 TypeID 偏向系统标识(如 "biz""host""biz_custom_query")。这张映射表建立了两个命名空间之间的对应关系。

问题二:部分资源不需要鉴权

有些 CMDB 内部资源类型(如 meta.HostFavoritemeta.ModelInstanceTopology)不需要在 IAM 注册,它们映射到 SkipType

问题三:动态模型实例有特殊处理

CMDB 支持用户自定义模型(Custom Object),这些模型的实例在 IAM 中用系统实例(sysinst_<modelID>)来注册。IsCMDBSysInstance 判断资源类型是否以 "sysinst_" 开头,如果是则直接用类型名作为 IAM TypeID。

How — ccIamResTypeMap 源码解读

src/ac/iam/adaptor.go 第 53-111 行,完整映射表:

var ccIamResTypeMap = map[meta.ResourceType]TypeID{
            meta.Business:                 Business,        // "biz"
            meta.BizSet:                   BizSet,          // "business_set"
            meta.Project:                   Project,         // "project"
            meta.Model:                     SysModel,        // "sys_model"
            meta.ModelUnique:              SysModel,
            meta.ModelAttributeGroup:      SysModel,
            meta.ModelModule:              BizTopology,     // "biz_topology"
            meta.ModelSet:                 BizTopology,
            meta.MainlineInstance:         BizTopology,
            meta.MainlineInstanceTopology: BizTopology,
            meta.ModelClassification:      SysModelGroup,   // "sys_model_group"
            meta.AssociationType:          SysAssociationType,
            meta.ModelAssociation:         SysModel,
            meta.CloudAreaInstance:        SysCloudArea,    // "sys_cloud_area"
            meta.HostInstance:             Host,            // "host"
            meta.HostFavorite:             SkipType,        // 不需要鉴权
            meta.Process:                  BizProcessServiceInstance,
            meta.DynamicGrouping:          BizCustomQuery,  // "biz_custom_query"
            meta.AuditLog:                 SysAuditLog,     // "sys_audit_log"
            meta.UserCustom:               UserCustom,      // "usercustom"
            meta.ProcessServiceTemplate:   BizProcessServiceTemplate,
            meta.ProcessServiceCategory:   BizProcessServiceCategory,
            meta.ProcessServiceInstance:   BizProcessServiceInstance,
            meta.BizTopology:              BizTopology,
            meta.SetTemplate:              BizSetTemplate,
            meta.OperationStatistic:       SysOperationStatistic,
            meta.HostApply:                BizHostApply,
            meta.ResourcePoolDirectory:    SysResourcePoolDirectory,
            meta.CloudAccount:             SysCloudAccount,
            meta.CloudResourceTask:        SysCloudResourceTask,
            meta.EventWatch:               SysEventWatch,
            meta.KubeCluster:              TypeID(""),
            meta.KubeNode:                 TypeID(""),
            meta.KubePod:                  TypeID(""),
            meta.KubeDeployment:           TypeID(""),
            // ... 更多 k8s 类型映射到具体 TypeID
            meta.FieldTemplate:            FieldGroupingTemplate,
        }

src/ac/iam/types.go 第 239-266 行,IAM TypeID 常量定义:

const (
            SystemIDCMDB       = "bk_cmdb"
            SystemNameCMDB     = "配置平台"
            SystemNameCMDBEn   = "cmdb"
        
            // 业务相关资源类型
            BizSet              TypeID = "business_set"
            Business            TypeID = "biz"
            BizCustomQuery      TypeID = "biz_custom_query"
            BizTopology         TypeID = "biz_topology"
            BizCustomField      TypeID = "biz_custom_field"
            BizProcessServiceTemplate   TypeID = "biz_process_service_template"
            BizProcessServiceCategory   TypeID = "biz_process_service_category"
            BizProcessServiceInstance   TypeID = "biz_process_service_instance"
            BizSetTemplate      TypeID = "biz_set_template"
            BizHostApply        TypeID = "biz_host_apply"
        
            // 系统级资源类型
            SysModel           TypeID = "sys_model"
            SysModelGroup      TypeID = "sys_model_group"
            SysAssociationType TypeID = "sys_association_type"
            SysAuditLog        TypeID = "sys_audit_log"
            SysOperationStatistic TypeID = "sys_operation_statistic"
            SysResourcePoolDirectory TypeID = "sys_resource_pool_directory"
            SysCloudArea       TypeID = "sys_cloud_area"
            SysCloudAccount    TypeID = "sys_cloud_account"
            SysCloudResourceTask TypeID = "sys_cloud_resource_task"
        
            // 通用资源类型
            Host               TypeID = "host"
            Set                TypeID = "set"
            Module             TypeID = "module"
            UserCustom         TypeID = "usercustom"
            SkipType           TypeID = "skip_type"
        )

映射表的命名规律:

  • Biz* 前缀:业务维度资源,权限与业务 ID 绑定
  • Sys* 前缀:系统级资源,跨业务共享
  • Kube*:K8s 相关资源,TypeID 为空字符串表示通过动态模型方式注册

本节总结:ccIamResTypeMap 的 3 种映射类型

  • Biz* 类型:业务维度资源,权限按业务隔离
  • Sys* 类型:系统级资源,跨业务共享
  • SkipType:不需要 IAM 鉴权的内部资源

六、动作映射:ConvertResourceAction 的四种分支

What — ConvertResourceAction 在做什么?

ConvertResourceAction 定义在 src/ac/iam/adaptor.go 第 142-199 行,把 CMDB 的动作枚举(meta.Create/Update/Delete/Find)翻译成 IAM 的具体 ActionID。它处理了四种分支:批量动作归一化、特殊动作按业务 ID 分流、资源动作映射表、系统实例动作。

Why — 为什么动作映射如此复杂?

问题一:CMDB 有批量动作

CMDB 的动作枚举有 CreateManyFindManyUpdateManyDeleteMany,但 IAM 通常只注册了单数形式(CreateFind 等)。ConvertResourceAction 在开头做归一化处理,把 FindMany → Find

问题二:同一动作在不同上下文映射不同

主机编辑(meta.Update)在业务下映射到 EditBusinessHost,在资源池下映射到 EditResourcePoolHost。这个 BusinessID > 0 vs BusinessID == 0 的判断在第 170-185 行硬编码处理。

问题三:动态模型的动作是动态生成的

对于用户自定义模型(IsCMDBSysInstance),CMDB 用 ConvertSysInstanceActionID 动态生成 ActionID,格式为 "<action_type>_sysinst_<modelID>",如 "view_sysinst_123"

How — ConvertResourceAction 的四种分支

src/ac/iam/adaptor.go 第 142-199 行,核心逻辑:

func ConvertResourceAction(resourceType meta.ResourceType,
            action meta.Action, businessID int64) (ActionID, error) {
        
            // 分支0:跳过动作直接返回 Skip
            if action == meta.SkipAction {
                return Skip, nil
            }
        
            // 分支1:批量动作归一化为单数
            switch action {
            case meta.CreateMany:  convertAction = meta.Create
            case meta.FindMany:     convertAction = meta.Find
            case meta.DeleteMany:  convertAction = meta.Delete
            case meta.UpdateMany:  convertAction = meta.Update
            }
        
            // 分支2:特殊动作按 businessID 分流(硬编码)
            switch resourceType {
            case meta.HostInstance:
                switch convertAction {
                case meta.Update:
                    if businessID > 0 { return EditBusinessHost, nil }
                    return EditResourcePoolHost, nil
                case meta.Find:
                    if businessID > 0 { return ViewBusinessResource, nil }
                    return ViewResourcePoolHost, nil
                }
            }
        
            // 分支3:从 resourceActionMap 映射表查找
            if actionID, ok := resourceActionMap[resourceType][convertAction]; ok && actionID != Unsupported {
                return actionID, nil
            }
        
            // 分支4:系统实例动态生成 ActionID
            if IsCMDBSysInstance(resourceType) {
                return ConvertSysInstanceActionID(resourceType, convertAction)
            }
        
            return Unsupported, fmt.Errorf("unsupported type %s action: %s", resourceType, action)
        }

src/ac/iam/adaptor.go 第 201-221 行,ConvertSysInstanceActionID 的实现:

func ConvertSysInstanceActionID(resourceType meta.ResourceType,
            action meta.Action) (ActionID, error) {
            var actionType ActionType
            switch action {
            case meta.Create: actionType = Create  // "create"
            case meta.Update: actionType = Edit    // "edit"
            case meta.Delete: actionType = Delete  // "delete"
            case meta.Find:   actionType = View    // "view"
            default: return Unsupported, fmt.Errorf("unsupported action: %s", action)
            }
            id := strings.TrimPrefix(string(resourceType), meta.CMDBSysInstTypePrefix)
            // meta.CMDBSysInstTypePrefix = "sysinst_"
            return ActionID(fmt.Sprintf("%s_%s%s", actionType, IAMSysInstTypePrefix, id)), nil
            // IAMSysInstTypePrefix = "sysinst_"
        }

src/ac/iam/adaptor.go 第 223-611 行,resourceActionMap 映射表覆盖了 CMDB 所有主要资源类型的动作映射。举几个关键映射:

var resourceActionMap = map[meta.ResourceType]map[meta.Action]ActionID{
            // 动态分组(DynamicGrouping)映射到 IAM 的 biz_custom_query 资源类型
            // Create/Update/Delete 分别对应 IAM 的创建/编辑/删除动态分组权限
            // Find/FindMany/Execute 统一映射到 ViewBusinessResource:查询和执行分组都只需要业务查看权
            meta.DynamicGrouping: {
                meta.Delete:   DeleteBusinessCustomQuery,   // 删除分组 → IAM 的删除动态分组权限
                meta.Update:   EditBusinessCustomQuery,     // 更新分组 → IAM 的编辑动态分组权限
                meta.Create:   CreateBusinessCustomQuery,   // 创建分组 → IAM 的创建动态分组权限
                meta.Find:     ViewBusinessResource,        // 查询分组 → IAM 的业务资源查看权限(与执行共用)
                meta.FindMany: ViewBusinessResource,       // 批量查询 → IAM 的业务资源查看权限
                meta.Execute:  ViewBusinessResource,        // 执行分组 → IAM 的业务资源查看权限(非编辑权限)
            },
            // 业务(Business)是最基础的 IAM 资源类型
            // Archive(归档)是业务特有的生命周期动作,区别于普通的 Delete
            meta.Business: {
                meta.Create:  CreateBusiness,    // 创建业务 → IAM 的创建业务权限
                meta.Update:  EditBusiness,      // 编辑业务 → IAM 的编辑业务权限
                meta.Find:    FindBusiness,      // 查询业务 → IAM 的查询业务列表权限
                meta.Archive: ArchiveBusiness,    // 归档业务 → IAM 的归档业务权限(区别于删除)
            },
            // 云区域(CloudAreaInstance)属于系统级资源,映射到 IAM 的 cloud_area 资源类型
            // 云区域不支持 Execute 动作(按理没有 FindMany)
            meta.CloudAreaInstance: {
                meta.Delete:   DeleteCloudArea,    // 删除云区域 → IAM 的删除云区域权限
                meta.Update:   EditCloudArea,       // 编辑云区域 → IAM 的编辑云区域权限
                meta.Create:   CreateCloudArea,    // 创建云区域 → IAM 的创建云区域权限
                meta.Find:     ViewCloudArea,       // 查询云区域 → IAM 的查看云区域权限
                meta.FindMany: ViewCloudArea,      // 批量查询 → IAM 的查看云区域权限
            },
            // K8s 集群(KubeCluster)映射到 IAM 的 container_cluster 资源类型
            // 注意:KubeCluster 没有 Update 动作(更新走内部 reconciliation),只有 Create/Delete/Find
            meta.KubeCluster: {
                meta.Create: CreateContainerCluster,   // 创建 K8s 集群 → IAM 的创建集群权限
                meta.Update: EditContainerCluster,     // 更新 K8s 集群 → IAM 的编辑集群权限
                meta.Delete: DeleteContainerCluster,   // 删除 K8s 集群 → IAM 的删除集群权限
                meta.Find:   ViewBusinessResource,      // 查询集群 → IAM 的业务资源查看权限(非集群专属查看)
            },
        }

本节总结:ConvertResourceAction 的 4 种分支

  • 分支 0:SkipActionSkip(直接放行)
  • 分支 1:批量动作归一化(FindMany → Find
  • 分支 2:硬编码特殊处理(HostInstance 按 businessID 分流)
  • 分支 3 + 4:映射表 + 动态系统实例

七、批量鉴权:AuthorizeBatch / AuthorizeAnyBatch

What — 批量鉴权在做什么?

AuthorizeBatchAuthorizeAnyBatch 定义在 src/ac/iam/iam.go 第 1198-1259 行,是 IAM 模块的鉴权入口。它们都调用同一个底层函数 authorizeBatch,通过 exact 参数区分"全部通过才通过"(AuthorizeBatch)还是"任一通过就通过"(AuthorizeAnyBatch)。

Why — 为什么需要批量鉴权?

问题一:一次操作可能涉及多个资源

比如"将主机 A 和主机 B 从模块 X 移到模块 Y",这个操作涉及两台主机的鉴权。如果逐个发请求,延迟是 O(N)。AuthorizeBatch 把所有资源的鉴权请求打包成一次 HTTP 调用,延迟是 O(1)。

问题二:事务需要原子性

某些操作(如跨主机批量转移)要求所有资源都有权限才执行,用 AuthorizeBatch(exact=true)。如果只需要"至少有一个资源有权限"(如"查看任一一台主机的详情"),用 AuthorizeAnyBatch

问题三:部分资源可能跳过鉴权

CMDB 中有些内部资源(如 meta.HostFavorite)不需要鉴权,parseAttributesToBatchOptions 会在构建请求前就把这些资源的 Authorized 设为 true,只把真正需要鉴权的资源发给 IAM。

How — 批量鉴权的完整执行路径

src/ac/iam/iam.go 第 1198-1259 行:

// AuthorizeBatch:全部通过才通过(exact=true)
        func (a *authorizer) AuthorizeBatch(ctx context.Context, h http.Header, user meta.UserInfo,
            resources ...meta.ResourceAttribute) ([]types.Decision, error) {
            return a.authorizeBatch(ctx, h, true, user, resources...)
        }
        
        // AuthorizeAnyBatch:任一通过就通过(exact=false)
        func (a *authorizer) AuthorizeAnyBatch(ctx context.Context, h http.Header, user meta.UserInfo,
            resources ...meta.ResourceAttribute) ([]types.Decision, error) {
            return a.authorizeBatch(ctx, h, false, user, resources...)
        }
        
        func (a *authorizer) authorizeBatch(ctx context.Context, h http.Header,
            exact bool, user meta.UserInfo,
            resources ...meta.ResourceAttribute) ([]types.Decision, error) {
        
            rid := httpheader.GetRid(h)
        
            // 1. 翻译 CMDB 资源为 IAM 鉴权选项
            opts, decisions, err := parseAttributesToBatchOptions(rid, user, resources...)
            if err != nil {
                return nil, err
            }
        
            // 2. 所有资源都跳过时,直接返回
            if opts == nil {
                return decisions, nil
            }
        
            // 3. 发 HTTP 请求到 IAM
            var authDecisions []types.Decision
            if exact {
                authDecisions, err = a.authClientSet.AuthorizeBatch(ctx, h, opts)
            } else {
                authDecisions, err = a.authClientSet.AuthorizeAnyBatch(ctx, h, opts)
            }
            if err != nil {
                blog.ErrorJSON("authorize batch failed, err: %s, ops: %s, rid: %s", err, opts, rid)
                return nil, err
            }
        
            // 4. 合并 IAM 返回结果到 decisions 数组
            index := 0
            for _, decision := range authDecisions {
                for decisions[index].Authorized {
                    index++
                }
                decisions[index].Authorized = decision.Authorized
                index++
            }
        
            return decisions, nil
        }

src/ac/iam/iam.go 第 1261-1315 行,parseAttributesToBatchOptions 的关键逻辑:

func parseAttributesToBatchOptions(rid string, user meta.UserInfo,
            resources ...meta.ResourceAttribute) (*types.AuthBatchOptions, []types.Decision, error) {
        
            if !auth.EnableAuthorize() {
                // 关闭鉴权时,全部放行
                decisions := make([]types.Decision, len(resources))
                for i := range decisions { decisions[i].Authorized = true }
                return nil, decisions, nil
            }
        
            authBatchArr := make([]*types.AuthBatch, 0)
            decisions := make([]types.Decision, len(resources))
        
            for index, resource := range resources {
                // 跳过动作:直接授权
                if resource.Action == meta.SkipAction {
                    decisions[index].Authorized = true
                    continue
                }
        
                // 适配翻译
                action, resourcesOut, err := AdaptAuthOptions(&resource)
                if err != nil {
                    return nil, nil, err
                }
        
                // Skip 类型:直接授权
                if action == Skip {
                    decisions[index].Authorized = true
                    continue
                }
        
                // 打包到批量请求
                authBatchArr = append(authBatchArr, &types.AuthBatch{
                    Action:    types.Action{ID: string(action)},
                    Resources: resourcesOut,
                })
            }
        
            // 构建 AuthBatchOptions
            ops := &types.AuthBatchOptions{
                System: SystemIDCMDB,
                Subject: types.Subject{Type: "user", ID: user.UserName},
                Batch: authBatchArr,
            }
            return ops, decisions, nil
        }

一个关键细节:第 1296-1298 行的 AuthBatch.Action.ID 传入的是 string(action)(即 ActionID),而不是动作枚举值。ActionID 在 src/ac/iam/types.go 中定义,如 EditBusinessHost = "edit_business_host"

本节总结:批量鉴权的 4 个关键设计

  • 1 次 HTTP 请求:所有资源打包成 AuthBatchOptions,O(1) 延迟
  • 2 种模式:AuthorizeBatch(全过才过)vs AuthorizeAnyBatch(任一过就过)
  • 跳过逻辑:SkipActionSkip 类型直接放行
  • 结果合并:跳过资源保持原位标 Authorized=true,IAM 结果顺序填充

八、创建者授权:RegisterResourceCreatorAction

What — 创建者授权在做什么?

RegisterResourceCreatorAction 定义在 src/ac/iam/iam.go 第 1335-1341 行,是 IAM 模块中注册资源创建者权限的方法。当用户在 CMDB 创建某个资源时,CMDB 调用这个方法通知 IAM:"请自动给创建者授予该资源的完整操作权限",这样创建者无需额外申请就能操作自己创建的资源。

Why — 为什么需要创建者授权?

问题一:权限真空期

如果创建资源后不自动授权,创建者在 IAM 策略生效前无法操作自己刚创建的资源。典型的例子是动态分组:用户创建了一个"高配主机"分组,但如果没有创建者授权,创建者无法查看、执行或编辑这个分组。

问题二:集中授权 vs 分散授权

如果没有创建者授权机制,运维人员必须手动在 IAM 后台给每个资源的创建者配置权限,工作量大且容易遗漏。RegisterResourceCreatorAction 实现了"创建即授权"的自动化。

问题三:IAM 返回创建者默认权限策略

RegisterResourceCreatorAction 不是直接授权,而是向 IAM 注册"创建者"这个角色关系,IAM 会返回该资源类型对应的默认权限策略(IamCreatorActionPolicy),CMDB 负责把这些策略应用到本地缓存。

How — 动态分组的创建者授权实战

以动态分组为例,看 src/scene_server/host_server/service/dynamic_grouping.go 第 98-121 行:

func (s *Service) registerActionToIAM(kit *rest.Kit,
            dynamicGroup meta.DynamicGroup) error {
            bizID := strconv.FormatInt(dynamicGroup.AppID, 10)
        
            // 1. 先查询刚创建的分组(拿到服务端生成的 ID)
            resp, err := s.CoreAPI.CoreService().Host().GetDynamicGroup(
                kit.Ctx, bizID, dynamicGroup.ID, kit.Header)
            if err != nil {
                return err
            }
        
            // 2. 构造 IAM 实例对象
            iamInstance := meta.IamInstanceWithCreator{
                Type:    string(iam.BizCustomQuery),  // "biz_custom_query"
                ID:      resp.Data.ID,
                Name:    resp.Data.Name,
                Creator: kit.User,
            }
        
            // 3. 调用 IAM 注册创建者权限
            if _, err = s.AuthManager.Authorizer.RegisterResourceCreatorAction(
                kit.Ctx, kit.Header, iamInstance); err != nil {
                return err
            }
            return nil
        }

这段代码在 CreateDynamicGroupautoRunTxnFunc 中调用(第 80-85 行),处于事务内。如果 IAM 注册失败,整个创建事务回滚,保证"分组已创建"和"IAM 已授权"的一致性。

src/ac/iam/iam.go 第 1335-1341 行,RegisterResourceCreatorAction 的接口签名:

func (a *authorizer) RegisterResourceCreatorAction(ctx context.Context, h http.Header,
            input metadata.IamInstanceWithCreator) (
            []metadata.IamCreatorActionPolicy, error) {
            return a.authClientSet.RegisterResourceCreatorAction(ctx, h, input)
        }

返回值 []IamCreatorActionPolicy 是 IAM 返回的创建者默认权限策略列表,包含该资源类型(如 biz_custom_query)在创建时自动授予的 ActionID 列表。CMDB 可以将这些策略缓存到 Redis,避免每次都查询 IAM。

本节总结:创建者授权的 3 步流程

  • 1 步查询:GetDynamicGroup 获取服务端生成的资源 ID
  • 2 步构造:IamInstanceWithCreator{Type, ID, Name, Creator}
  • 3 步注册:RegisterResourceCreatorAction 向 IAM 注册创建者角色

九、动态分组与 IAM:BizCustomQuery 特殊处理

What — 动态分组在 IAM 中的特殊处理是什么?

动态分组(DynamicGrouping)在 CMDB 中是一种"保存查询条件的资源",对应 IAM 中的 BizCustomQuery 类型。这不是系统内置的资源类型,而是蓝鲸体系为 CMDB 动态分组专门注册的自定义资源类型。动态分组的 CRUD 操作分别映射到 IAM 的 CreateBusinessCustomQueryEditBusinessCustomQueryDeleteBusinessCustomQuery 三个 ActionID。

Why — 为什么动态分组需要特殊的 IAM 处理?

问题一:动态分组是业务维度的资源

动态分组按 bk_biz_id 隔离,不同业务下的分组互不可见。在 IAM 中,BizCustomQuery 资源类型挂靠在业务(biz)下,权限继承业务的访问范围。

问题二:动态分组的执行需要单独鉴权

执行动态分组(meta.Execute)是一个特殊动作,它在 IAM 中映射到 ViewBusinessResource,而不是 EditBusinessCustomQuery。这是因为执行动态分组本质上是"查询符合条件的主机列表",属于查看操作而非编辑操作。

问题三:动态分组的权限和业务权限联动

如果用户对某个业务没有访问权限,他既看不到该业务下的主机,也无法使用该业务下的动态分组。动态分组的执行结果受业务权限的约束。

How — resourceActionMap 中动态分组的动作映射

src/ac/iam/adaptor.go 第 252-259 行:

meta.DynamicGrouping: {
            meta.Delete:   DeleteBusinessCustomQuery,
            meta.Update:   EditBusinessCustomQuery,
            meta.Create:   CreateBusinessCustomQuery,
            meta.Find:     ViewBusinessResource,
            meta.FindMany: ViewBusinessResource,
            meta.Execute:  ViewBusinessResource,
        },

这里值得注意的设计:

  • meta.Find / FindMany(查询分组详情)映射到 ViewBusinessResource:查询分组信息属于业务查看权限
  • meta.Execute(执行分组获取成员)映射到 ViewBusinessResource:执行结果取决于用户对成员的访问权限
  • meta.Create / Update / Delete 分别映射到对应的 Create/Edit/DeleteBusinessCustomQuery

这意味着:一个用户如果只有业务查看权限(ViewBusinessResource),他可以查询和执行动态分组,但无法创建、编辑或删除分组。如果要管理动态分组,需要额外的 EditBusinessCustomQuery 权限。

一个关键设计:Execute 动作不在 resourceActionMap 的标准动作里

meta.Execute 是 CMDB 为动态分组专门定义的动作(定义在 src/ac/meta/meta.go),不在标准 CRUD 动作中。ConvertResourceAction 在查映射表前会先归一化批量动作,但 meta.Execute 没有归一化目标,直接走映射表查找。

本节总结:动态分组 IAM 映射的 3 个要点

  • 资源类型:DynamicGroupingBizCustomQuery
  • Execute 特殊映射:meta.ExecuteViewBusinessResource(不是编辑权限)
  • 创建者自动授权:RegisterResourceCreatorActionEditBusinessCustomQuery

十、GenIamResource 体系:CMDB 资源如何翻译成 IAM 资源实例

What — GenIamResource 在做什么?

GenIamResource 定义在 src/ac/iam/gen_id.go 第 65-105 行,是 IAM 适配器体系的第三层(紧接 AdaptAuthOptions 的动作翻译和类型翻译之后),负责把 meta.ResourceAttribute 转换成 IAM 鉴权请求的 []types.Resource。每一个资源类型都有专属的生成函数,通过 genIamResFuncMap 函数映射表动态分发。

Why — 为什么需要这么多生成函数?

问题一:不同资源类型的鉴权语义不同

业务(biz)创建时不需要关联具体实例 ID(因为业务尚未分配 ID),但业务编辑时需要带实例 ID;主机(host)始终需要通过拓扑链路(bk_biz_id)定位父级;动态分组(biz_custom_query)在创建和查看时只关联业务,编辑和删除时关联分组实例本身。

问题二:跨业务 vs 资源池的 ID 体系不同

资源池的主机没有 bk_biz_id,鉴权路径是 /host,id/;业务下的主机鉴权路径是 /biz,id/host,hostID/。这两种路径格式完全不同,需要不同的生成逻辑。

问题三:某些动作不需要资源实例

比如 CreateBusinessViewCloudArea 等动作,创建和查看时不需要传实例 ID,只需要 IAM 知道"哪个系统"在请求即可。

How — GenIamResource 的分发逻辑与主要生成函数

src/ac/iam/gen_id.go 第 65-105 行,核心分发函数:

func GenIamResource(act ActionID, rscType TypeID, a *meta.ResourceAttribute) ([]types.Resource, error) {
        // Skip 动作不需要关联资源
        if act == Skip {
            return genSkipResource(act, rscType, a)
        }
    
        // 按资源类型分派到专属生成函数
        switch a.Basic.Type {
        case meta.ModelAttributeGroup, meta.ModelAttribute:
            if a.BusinessID > 0 {
                return genBizModelAttributeResource(act, rscType, a)
            } else {
                return genModelRelatedResource(act, rscType, a)
            }
        case meta.SystemBase, meta.FulltextSearch, meta.IDRuleIncrID:
            return make([]types.Resource, 0), nil  // 不需要资源
        case meta.KubeCluster, meta.KubeNode, meta.KubeNamespace,
             meta.KubeWorkload, meta.KubeDeployment,
             meta.KubeStatefulSet, meta.KubeDaemonSet,
             meta.KubeGameStatefulSet, meta.KubeGameDeployment,
             meta.KubeCronJob, meta.KubeJob,
             meta.KubePodWorkload, meta.KubePod, meta.KubeContainer:
            return genKubeResource(act, rscType, a)
        }
    
        // 通过函数映射表分发
        genIamResourceFunc, exists := genIamResFuncMap[a.Basic.Type]
        if exists {
            return genIamResourceFunc(act, rscType, a)
        }
    
        // 动态系统实例走专用路径
        if IsCMDBSysInstance(a.Basic.Type) {
            return genSysInstanceResource(act, rscType, a)
        }
        return nil, fmt.Errorf("gen id failed: unsupported resource type: %s", a.Type)
    }

src/ac/iam/gen_id.go 第 171-200 行,genDynamicGroupingResource 的特殊设计:

func genDynamicGroupingResource(act ActionID, typ TypeID, att *meta.ResourceAttribute) ([]types.Resource, error) {
        r := types.Resource{
            System:    SystemIDCMDB,
            Attribute: nil,
        }
    
        // 必须在业务下(bk_biz_id > 0)
        if att.BusinessID <= 0 {
            return nil, errors.New("biz id can not be 0")
        }
    
        // 创建动态分组 && 查看动态分组:只关联业务,不关联分组实例
        if act == CreateBusinessCustomQuery || act == ViewBusinessResource {
            r.Type = types.ResourceType(Business)
            r.ID = strconv.FormatInt(att.BusinessID, 10)
            return []types.Resource{r}, nil
        }
    
        // 编辑 && 删除动态分组:关联分组实例本身
        r.Type = types.ResourceType(typ)
        if len(att.InstanceIDEx) > 0 {
            r.ID = att.InstanceIDEx
        }
    
        // 权限路径:/biz,{bk_biz_id}/
        r.Attribute = map[string]interface{}{
            types.IamPathKey: []string{fmt.Sprintf("/%s,%d/", Business, att.BusinessID)},
        }
        return []types.Resource{r}, nil
    }

这是动态分组的核心设计:

  • CreateBusinessCustomQuery / ViewBusinessResource(执行)→ 资源类型 = biz,ID = 业务 ID,路径为 /biz,{id}/
  • EditBusinessCustomQuery / DeleteBusinessCustomQuery → 资源类型 = biz_custom_query,ID = 分组 ID,路径 = /biz,{id}/

src/ac/iam/gen_id.go 第 446-469 行,genModelResource 的创建动作特殊处理:

func genModelResource(act ActionID, typ TypeID, att *meta.ResourceAttribute) ([]types.Resource, error) {
        r := types.Resource{
            System:    SystemIDCMDB,
            Type:      types.ResourceType(typ),
            Attribute: nil,
        }
    
        // 创建模型:按模型分组授权,不需要具体实例 ID
        if act == CreateSysModel {
            if len(att.Layers) > 0 {
                r.Type = types.ResourceType(SysModelGroup)  // 挂靠到模型分组
                r.ID = strconv.FormatInt(att.Layers[0].InstanceID, 10)
                return []types.Resource{r}, nil
            }
            return []types.Resource{r}, nil
        }
    
        if att.InstanceID > 0 {
            r.ID = strconv.FormatInt(att.InstanceID, 10)
        }
        return []types.Resource{r}, nil
    }

本节总结:GenIamResource 体系的 4 个设计要点

  • 1 个分发函数:按资源类型走 switch 或函数映射表 genIamResFuncMap
  • 1 个"空资源"约定:部分动作(SystemBase/FulltextSearch/IDRuleIncrID)返回空切片,IAM 用系统而非实例授权
  • 1 个动态实例路径:IsCMDBSysInstance 判断后走 genSysInstanceResource
  • 2 类动态分组语义:创建/查看 → 关联业务;编辑/删除 → 关联分组实例

十一、IAM Client HTTP 端点:完整路径与请求体格式

What — CMDB 通过哪些 HTTP 端点与 IAM 通信?

CMDB 的 IAM Client 定义在 src/ac/iam/client.go 中,所有方法都通过 REST Client 向蓝鲸 IAM 服务发起 HTTP 调用。HTTP 端点(SubResource)遵循 IAM 的 OpenAPI 规范,每次请求都需要在 Header 中携带 X-Bk-App-CodeX-Bk-App-Secret 鉴权。

Why — 为什么需要这么完整的端点映射?

问题一:IAM 采用"资源驱动"设计

IAM 的 API 不是按功能分组的,而是按"资源类型"组织的:/api/v1/model/systems/{system_id}/... 是资源注册;/authorize/batch 是批量鉴权;/register/resource_creator_action 是创建者授权。每类操作有独立的端点。

问题二:鉴权请求需要签名 Header

CMDB 的 IAM Client 在初始化时(第 80-84 行)设置了 X-Bk-App-Code(AppCode)和 X-Bk-App-Secret(AppSecret),这些 Header 在每个请求中透传到 IAM,IAM 用这两个值验证"哪个系统在发起请求"。

问题三:Delete 动作传 Body 而非 PathParam

src/ac/iam/client.go 第 475-507 行,DeleteInstanceSelections 使用 DELETE 方法但 Body 传 ID 数组,这是 IAM 的设计规范,不是 bug。

How — IAM Client 完整端点映射

所有端点基于 src/ac/iam/client.gosrc/apimachinery/authserver/api.go 源码:

操作类型HTTP 方法端点路径请求体关键逻辑
查询系统信息 GET /api/v1/model/systems/{system_id}/query?fields=... fields 参数控制返回哪些子资源类型
注册系统 POST /api/v1/model/systems System 首次启动时调用一次
更新系统配置 PUT /api/v1/model/systems/{system_id}/configs SysConfig 只更新 provider_config.host
批量鉴权 POST /authorize/batch AuthBatchOptions 精确模式:全部通过才通过
任意鉴权 POST /authorize/any/batch AuthBatchOptions 任一通过即通过
注册资源类型 POST /api/v1/model/systems/{system_id}/resource-types []ResourceType 批量新增,跳过已存在
更新资源类型 PUT /api/v1/model/systems/{system_id}/resource-types/{type_id} ResourceType 按 ID 更新名称/版本/描述
删除资源类型 DELETE /api/v1/model/systems/{system_id}/resource-types []{"id": TypeID} Delete + Body,非 PathParam
注册动作 POST /api/v1/model/systems/{system_id}/actions []ResourceAction 批量新增,跳过已存在
删除动作前:删除策略 DELETE /api/v1/model/systems/{system_id}/actions/{action_id}/policies 必须先删策略再删动作
删除动作 DELETE /api/v1/model/systems/{system_id}/actions []{"id": ActionID} 依赖策略已清空
注册实例选择 POST /api/v1/model/systems/{system_id}/instance-selections []InstanceSelection 挂载到资源类型上
更新动作组 PUT /api/v1/model/systems/{system_id}/configs/action_groups []ActionGroup 批量替换,非增量更新
注册创建者权限 POST /register/resource_creator_action IamInstanceWithCreator ESB 路径:/v2/iam/authorization/resource_creator_action/
获取无权限跳转 URL POST /find/no_auth_skip_url IamPermission 返回 IAM 申请页面 URL
获取需申请权限 POST /find/permission_to_apply []ResourceAttribute 鉴权失败时提示用户需要哪些权限
获取用户有权限的资源 POST /findmany/authorized_resource ListAuthorizedResourcesParam 返回用户有权限的具体资源 ID 列表

关键 Header 信息(读 src/ac/iam/iam.go 第 80-84 行):

header := http.Header{}
    header.Set("Content-Type", "application/json")
    header.Set("Accept", "application/json")
    header.Set(iamAppCodeHeader,   cfg.AppCode)    // X-Bk-App-Code
    header.Set(iamAppSecretHeader, cfg.AppSecret)   // X-Bk-App-Secret

所有请求的 Header 在 src/apimachinery/authserver/api.go 第 36-52 行通过 WithHeaders(h) 透传到 IAM 服务。

本节总结:IAM Client HTTP 端点的 3 个关键设计

  • 1 套认证 Header:X-Bk-App-Code + X-Bk-App-Secret 在 Client 初始化时设置,每个请求透传
  • 2 大类端点:/api/v1/model/systems/{id}/...(注册管理)+ /authorize/...(鉴权)
  • 2 个幂等设计:批量注册(POST)跳过已存在,删除(DELETE)先清策略再删动作

十二、ActionID 常量全集与动作语义分类

What — CMDB 向 IAM 注册了哪些 ActionID?

CMDB 定义了约 80+ 个 ActionID 常量,分布在 src/ac/iam/types.goActionID 类型别名和 ActionIDNameMap 映射表中。这些 ActionID 对应 IAM 侧"用户在蓝鲸权限中心看到的权限项名称"。

Why — 为什么需要这么细粒度的动作拆分?

问题一:权限最小化原则

蓝鲸 IAM 的核心原则是"最小权限":每个操作都应该有独立的权限项。如果"查看业务主机"和"编辑业务主机"共用同一个 ActionID,管理员就无法只授权"查看"而不授权"编辑"。

问题二:K8s 资源纳管增加了大量容器管理动作

v3.14.6 版本中,K8s 相关动作占新增 ActionID 的 40% 以上,包括 create_container_clusteredit_container_nodedelete_container_namespace 等,覆盖了 Pod/Deployment/StatefulSet/DaemonSet/Job/CronJob 等 8 种 Workload 类型。

How — ActionID 常量全集(基于 types.go L325-622)

业务维度动作(以 biz/biz_topology/biz_custom_query 开头)

ActionID 常量常量值英文名含义
CreateBusiness "create_business" Create Business 创建业务
EditBusiness "edit_business" Edit Business 编辑业务
ArchiveBusiness "archive_business" Archive Business 归档业务
FindBusiness "find_business" Find Business 查询业务列表
ViewBusinessResource "view_business_resource" View Business Resource 查看业务资源(含主机/分组等)
CreateBizSet "create_business_set" Create Business Set 创建业务集
EditBizSet "edit_business_set" Edit Business Set 编辑业务集
DeleteBizSet "delete_business_set" Delete Business Set 删除业务集
ViewBizSet "view_business_set" View Business Set 查看业务集
AccessBizSet "access_business_set" Access Business Set 访问业务集
CreateBusinessCustomQuery "create_business_custom_query" Create Dynamic Grouping 创建动态分组
EditBusinessCustomQuery "edit_business_custom_query" Edit Dynamic Grouping 编辑动态分组
DeleteBusinessCustomQuery "delete_business_custom_query" Delete Dynamic Grouping 删除动态分组
CreateBusinessTopology "create_business_topology" Create Business Topo 创建拓扑节点(集群/模块)
EditBusinessTopology "edit_business_topology" Edit Business Topo 编辑拓扑节点
DeleteBusinessTopology "delete_business_topology" Delete Business Topo 删除拓扑节点
EditBusinessHost "edit_business_host" Edit Business Hosts 编辑业务下主机
HostTransferAcrossBusiness "host_transfer_across_business" Assigned Host To Other Business 跨业务转移主机
ResourcePoolHostTransferToBusiness "resource_pool_host_transfer_to_business" Assigned Pool Hosts To Business 资源池主机转入业务
EditBusinessLayer "edit_business_layer" Edit Business Layer 编辑主线条模型(业务集层)
ViewModelTopo "view_model_topo" View Model Topo 查看模型拓扑
EditModelTopologyView "edit_model_topology_view" Edit Model Topology View 编辑模型拓扑视图

资源池维度动作(sys_host/sys_resource_pool 开头)

ActionID 常量常量值英文名含义
ViewResourcePoolHost "view_resource_pool_host" View Resource Pool Hosts 查看资源池主机
CreateResourcePoolHost "create_resource_pool_host" Create Pool Hosts 资源池新增主机
EditResourcePoolHost "edit_resource_pool_host" Edit Pool Hosts 编辑资源池主机
DeleteResourcePoolHost "delete_resource_pool_host" Delete Pool Hosts 删除资源池主机
ResourcePoolHostTransferToDirectory "resource_pool_host_transfer_to_directory" Assigned Pool Hosts To Directory 资源池内目录转移
ManageHostAgentID "manage_host_agent_id" Manage Host AgentID 管理主机 AgentID
CreateResourcePoolDirectory "create_resource_pool_directory" Create Pool Directory 创建资源池目录
EditResourcePoolDirectory "edit_resource_pool_directory" Edit Pool Directory 编辑资源池目录
DeleteResourcePoolDirectory "delete_resource_pool_directory" Delete Pool Directory 删除资源池目录

K8s 容器管理动作(create/edit/delete_container_* 开头)

ActionID 常量常量值英文名含义
CreateContainerCluster "create_container_cluster" Create Cluster 创建 K8s 集群
EditContainerCluster "edit_container_cluster" Edit Cluster 编辑 K8s 集群
DeleteContainerCluster "delete_container_cluster" Delete Cluster 删除 K8s 集群
CreateContainerNode "create_container_node" Create Node 创建 K8s 节点
EditContainerNode "edit_container_node" Edit Node 编辑 K8s 节点
DeleteContainerNode "delete_container_node" Delete Node 删除 K8s 节点
CreateContainerNamespace "create_container_namespace" Create Namespace 创建命名空间
EditContainerNamespace "edit_container_namespace" Edit Namespace 编辑命名空间
DeleteContainerNamespace "delete_container_namespace" Delete Namespace 删除命名空间
CreateContainerWorkload "create_container_workload" Create Workload 创建 Workload(Deployment/StatefulSet/DaemonSet 等)
EditContainerWorkload "edit_container_workload" Edit Workload 编辑 Workload
DeleteContainerWorkload "delete_container_workload" Delete Workload 删除 Workload
CreateContainerPod "create_container_pod" Create Pod 创建 Pod
DeleteContainerPod "delete_container_pod" Delete Pod 删除 Pod

系统级动作(sys_* 开头)

ActionID 常量常量值英文名含义
CreateSysModel "create_sys_model" Create Model 创建模型
ViewSysModel "view_sys_model" View Model 查看模型
EditSysModel "edit_sys_model" Edit Model 编辑模型
DeleteSysModel "delete_sys_model" Delete Model 删除模型
CreateModelGroup "create_model_group" Create Model Group 创建模型分组
EditModelGroup "edit_model_group" Edit Model Group 编辑模型分组
DeleteModelGroup "delete_model_group" Delete Model Group 删除模型分组
CreateAssociationType "create_association_type" Create Association Type 创建关联类型
EditAssociationType "edit_association_type" Edit Association Type 编辑关联类型
DeleteAssociationType "delete_association_type" Delete Association Type 删除关联类型
CreateCloudArea "create_cloud_area" Create Cloud Area 创建云区域
EditCloudArea "edit_cloud_area" Edit Cloud Area 编辑云区域
DeleteCloudArea "delete_cloud_area" Delete Cloud Area 删除云区域
ViewCloudArea "view_cloud_area" View Cloud Area 查看云区域
CreateCloudAccount "create_cloud_account" Create Cloud Account 创建云账户
EditCloudAccount "edit_cloud_account" Edit Cloud Account 编辑云账户
DeleteCloudAccount "delete_cloud_account" Delete Cloud Account 删除云账户
CreateCloudResourceTask "create_cloud_resource_task" Create Cloud Task 创建云资源任务
EditCloudResourceTask "edit_cloud_resource_task" Edit Cloud Task 编辑云资源任务
DeleteCloudResourceTask "delete_cloud_resource_task" Delete Cloud Task 删除云资源任务
CreateProject "create_project" Create Project 创建项目
EditProject "edit_project" Edit Project 编辑项目
DeleteProject "delete_project" Delete Project 删除项目
ViewProject "view_project" View Project 查看项目

本节总结:ActionID 的命名规律

  • Biz 开头:业务维度资源,权限与业务 ID 绑定(create_business_custom_queryedit_business_host
  • ResourcePool 开头:资源池维度资源(view_resource_pool_hostedit_resource_pool_host
  • Container 开头:K8s 容器管理资源(create_container_cluster ~ delete_container_pod
  • Sys 开头:系统级资源(create_sys_modeldelete_cloud_area

十三、AuthServer SDK 内部:Policy 计算与 RelatedActions 依赖链

What — AuthServer 内部的 Policy 计算逻辑是什么?

AuthServer 是 CMDB 内部的一个独立服务(运行在 src/scene_server/auth_server/),它接收 CMDB API Server 转发的 IAM 鉴权请求,负责实际的 Policy 计算与匹配。这个服务与 IAM 本身的 AuthorizeBatch 端点不同:IAM 是外部集中式服务,AuthServer 是 CMDB 内部的鉴权计算器。

Why — 为什么 CMDB 需要 AuthServer 而不是直接调 IAM?

问题一:减少跨进程网络开销

每次资源操作都直接调 IAM,会产生大量网络往返。AuthServer 实现了本地缓存和批量合并优化。

问题二:RelatedActions 依赖链的本地计算

ResourceAction.RelatedActions 定义了动作间的依赖关系。例如 edit_business_hostRelatedActions 包含 view_business_resource,意味着"如果用户有编辑主机权限,他必定有查看业务资源权限"。AuthServer 在本地完成这个依赖链的展开,不需要每次都查 IAM。

问题三:策略匹配支持 AND/OR 组合

IAM 的 Policy 表达式支持复杂的条件组合(ANDORNOT),AuthServer 的 calculatePolicy 函数(src/scene_server/auth_server/sdk/auth/authorize.go)负责解析这些表达式并与请求资源进行匹配。

How — AuthServer 的 Policy 计算链路

src/scene_server/auth_server/sdk/auth/authorize.go 第 35-60 行,Authorize 单请求鉴权:

func (a *Authorize) Authorize(ctx context.Context, opts *types.AuthOptions) (*types.Decision, error) {
        if err := opts.Validate(); err != nil {
            return nil, err
        }
    
        // 1. 从 IAM 获取用户的策略
        getOpt := types.GetPolicyOption{
            System:   opts.System,
            Subject:  opts.Subject,
            Action:   opts.Action,
            // 注意:Resources 为空,意味着获取用户该动作的全部策略(不限实例)
            Resources: make([]types.Resource, 0),
        }
        policy, err := a.iam.GetUserPolicy(ctx, &getOpt)
        if err != nil {
            return nil, err
        }
    
        // 2. 用策略条件与请求资源实例进行匹配
        authorized, err := a.calculatePolicy(ctx, opts.Resources, policy)
        if err != nil {
            return nil, fmt.Errorf("calculate user's auth policy failed, err: %v", err)
        }
    
        return &types.Decision{Authorized: authorized}, nil
    }

src/scene_server/auth_server/sdk/auth/authorize.go 第 62-126 行,AuthorizeBatch 批量鉴权(50 并发):

func (a *Authorize) AuthorizeBatch(ctx context.Context, opts *types.AuthBatchOptions) ([]*types.Decision, error) {
        return a.authorizeBatch(ctx, opts, true)
    }
    
    func (a *Authorize) authorizeBatch(ctx context.Context, opts *types.AuthBatchOptions,
        exact bool) ([]*types.Decision, error) {
    
        if err := opts.Validate(); err != nil { return nil, err }
        if len(opts.Batch) == 0 {
            return nil, errors.New("no resource instance need to authorize")
        }
    
        // 1. 对动作 ID 去重:相同动作只查一次策略
        actionIDMap := make(map[string]types.Action)
        for _, b := range opts.Batch {
            actionIDMap[b.Action.ID] = b.Action
        }
        actions := make([]types.Action, 0)
        for _, action := range actionIDMap {
            actions = append(actions, action)
        }
    
        // 2. 批量获取用户策略(一个请求获取多个动作的策略)
        listOpts := &types.ListPolicyOptions{
            System:   opts.System,
            Subject:  opts.Subject,
            Actions:  actions,
            Resources: nil,  // 获取所有实例的策略
        }
        policies, err := a.iam.ListUserPolicies(ctx, listOpts)
        if err != nil {
            return nil, fmt.Errorf("list user's policy failed, err: %s", err)
        }
    
        // 3. 构建 policy 映射:actionID -> policy
        policyMap := make(map[string]*operator.Policy)
        for _, p := range policies {
            policyMap[p.Action.ID] = p.Policy
        }
    
        // 4. 并发计算每个资源的鉴权结果(50 并发上限)
        decisions := make([]*types.Decision, len(opts.Batch))
        pipe := make(chan struct{}, 50)
        wg := sync.WaitGroup{}
        for idx, b := range opts.Batch {
            wg.Add(1)
            pipe 

这里有两个值得深入的设计:

  1. 动作去重优化:如果一次请求 100 个主机但都是同一个动作,只查一次策略再复用到 100 个资源上,减少 IAM 调用次数
  2. 50 并发上限pipe := make(chan struct{}, 50) 控制最大并发数,防止突发流量压垮 IAM 服务
  3. 策略映射:通过 policyMap[actionID] 将策略关联回每个请求资源

RelatedActions 依赖链的展开

src/ac/iam/initial_actions.go 中,每个 ResourceAction 结构体的 RelatedActions 字段定义了前置依赖。例如:

ResourceAction{
        ID:   EditBusinessHost,
        Name: "Edit Business Hosts",
        Type: Edit,
        RelatedResourceTypes: []RelateResourceType{...},
        RelatedActions: []ActionID{ViewBusinessResource},  // 编辑主机前必须先有查看权限
        Version: 1,
    }
    
    ResourceAction{
        ID:   CreateResourcePoolHost,
        Type: Create,
        RelatedResourceTypes: []RelateResourceType{resourcePoolDirResource},
        RelatedActions: nil,  // 创建资源池主机无前置依赖
        Version: 1,
    }

当用户在 IAM 后台配置"允许编辑业务主机"权限时,IAM 自动展开 RelatedActions 链,隐式授予"查看业务资源"权限,无需管理员单独配置。

本节总结:AuthServer SDK 的 3 个关键设计

  • 1 个策略去重优化:相同动作的多个资源只查一次 Policy,大幅减少 IAM 调用
  • 1 个 50 并发控制:sync.WaitGroup + pipe := make(chan struct{}, 50) 控制突发流量
  • 1 个 RelatedActions 隐式授权:edit_business_hostview_business_resource 自动展开

十四、NewAuthManager 架构:AuthManager 与 Authorizer 的初始化链路

What — AuthManager 在 CMDB 中扮演什么角色?

AuthManager 是 CMDB 各服务(host_server、event_server、operation_server 等)共享的权限管理单例,通过 NewAuthManager(clientSet, iamCli) 构造(src/ac/extensions/types.go 第 38-49 行)。它聚合了 Authorizer(IAM 鉴权,ac.AuthorizeInterface 接口)、Viewer(资源可见性过滤,ac.Viewer 接口)、clientSet(API 客户端)三个核心组件和 4 个布尔开关(RegisterModuleEnabledRegisterSetEnabledSkipReadAuthorizationRegisterAuditCategoryEnabled),是 CMDB 权限体系对外暴露的统一入口。

Why — 为什么需要 AuthManager 而不是直接用 IAM?

问题一:分层解耦

如果各服务直接引用 IAM 结构体,IAM 模块的内部变更(改 Client 结构、换 HTTP 实现)会直接影响所有服务消费者。AuthManager 作为门面(Facade),隔离了底层实现,Consumer 只需依赖接口而非具体类型。

问题二:多实例差异配置

CMDB 各 Server(host_server、event_server 等)可能有不同的鉴权策略。AuthManager 在每个 Server 初始化时通过 SetConfig 单独配置,实现差异化授权。

问题三:SetAuthorizer 与 SetViewer 的组合设计

SetAuthorizerSetViewer 允许在运行时替换授权器和查看器实现,这对测试和降级场景非常重要。

How — NewAuthManager 的完整初始化链路

src/ac/extensions/types.go 第 27-49 行,AuthManager 结构体与 NewAuthManager 构造函数(均为真实源码):

// AuthManager 是 CMDB 各服务的统一鉴权管理器门面
    // 聚合了 Authorizer(鉴权)、Viewer(资源可见性)和 4 个注册开关布尔值
    type AuthManager struct {
        clientSet  apimachinery.ClientSetInterface  // API 客户端(配置中心 / IAM Server 连接)
        Authorizer ac.AuthorizeInterface             // 鉴权接口(由 iam.NewAuthorizer 构造)
        Viewer     ac.Viewer                        // 资源可见性接口(由 iam.NewViewer 构造,支持资源过滤)

        RegisterModuleEnabled        bool  // 是否注册模型(Module)到 IAM,默认 false
        RegisterSetEnabled           bool  // 是否注册集合(Set)到 IAM,默认 false
        RegisterAuditCategoryEnabled bool  // 是否注册审计分类到 IAM,默认 false
        SkipReadAuthorization        bool  // 是否跳过读取操作的鉴权,默认 true(读操作不鉴权)
    }

    // NewAuthManager 的真实签名需要两个参数:clientSet + iamCli
    // clientSet 用于构造 Authorizer 和 Viewer
    // iamCli(*iam.IAM)用于 Viewer 的初始化
    func NewAuthManager(clientSet apimachinery.ClientSetInterface, iamCli *iam.IAM) *AuthManager {
        return &AuthManager{
            clientSet:                    clientSet,
            Authorizer:                   iam.NewAuthorizer(clientSet),   // 构造鉴权器
            Viewer:                       iam.NewViewer(clientSet, iamCli), // 构造资源可见性器(双参数)
            RegisterModuleEnabled:        false,   // 默认不注册模块
            RegisterSetEnabled:           false,   // 默认不注册集合
            SkipReadAuthorization:        true,    // 默认跳过读鉴权
            RegisterAuditCategoryEnabled: false,   // 默认不注册审计分类
        }
    }

AuthManager 本身没有内嵌(Embedding)Authorizer,而是显式声明了字段 Authorizer ac.AuthorizeInterfaceViewer ac.Viewer,通过 iam.NewAuthorizer(clientSet)iam.NewViewer(clientSet, iamCli) 分别构造。

AuthManager 在各服务中的初始化调用链(codegraph blast radius 揭示):

  • src/scene_server/datacollection/app/server.go:数据采集服务初始化 AuthManager
  • src/scene_server/event_server/app/server.go:事件服务初始化 AuthManager
  • src/scene_server/host_server/app/server.go:主机服务初始化 AuthManager(动态分组创建链路中使用)
  • src/scene_server/operation_server/app/server.go:运营服务初始化 AuthManager

每个服务都调用 initModulessrc/ac/extensions/types.go),其中包含 NewAuthManager 的构造。clientSet 通过配置中心的 NewFromConfig 初始化,包括 IAM Server 的地址和认证信息。

本节总结:AuthManager 的 4 个设计要点

  • 1 个门面模式:AuthManager 聚合 Authorizer + Viewer + 4 个开关布尔值,对外暴露统一权限 API
  • 2 个构造参数:NewAuthManager(clientSet, iamCli),需要同时传入 API 客户端和 IAM 实例
  • 3 个核心组件:Authorizer(鉴权)、Viewer(可见性过滤)、clientSet(HTTP 客户端)
  • 4 个开关:RegisterModuleEnabledRegisterSetEnabledSkipReadAuthorization(默认 true)、RegisterAuditCategoryEnabled

FAQ(常见 20 问)

Q1. CMDB 的权限总开关在哪里?

一句话结论:EnableAuthorize()src/common/auth/auth.go 第 68-71 行。

src/common/auth/auth.go 第 68-71 行,EnableAuthorize() 返回配置文件中的 enableAuth 布尔值。开关关闭时,所有鉴权直接放行,零 IAM 调用。

Q2. IAM 模块的核心结构体是什么?

一句话结论:IAM 结构体只有一个字段 Client iamClientInterface

src/ac/iam/iam.go 第 45-47 行,type IAM struct { Client iamClientInterface }。所有 IAM 操作都通过 Client 发起 HTTP 请求到蓝鲸权限中心。

Q3. Register() 的注册顺序为什么重要?

一句话结论:因为 IAM 中资源类型之间有依赖关系,删除顺序和新增顺序相反。

src/ac/iam/iam.go 第 122-139 行注释,删除顺序:Action → InstanceSelection → ResourceType;新增顺序:ResourceType → InstanceSelection → Action。违反顺序会导致依赖缺失错误。

Q4. 为什么注册要用 Redis 分布式锁?

一句话结论:防止 CMDB 多个实例同时注册造成冲突。

src/ac/iam/iam.go 第 96-114 行,tryLockRegister 通过 Redis SETNX 获取锁,锁键为 "register_iam_lock",锁超时 2 分钟,重试 3 次,每次间隔 5 秒。只有拿到锁的实例执行注册,其他实例等待并复用已有结果。

Q5. AdaptAuthOptions 的三层翻译分别是什么?

一句话结论:动作翻译 + 资源类型翻译 + 资源实例生成。

src/ac/iam/adaptor.go 第 29-51 行,ConvertResourceAction 翻译动作、ConvertResourceType 翻译类型、GenIamResource 生成实例。三层顺序不可颠倒,因为后一层依赖前一层的输出。

Q6. ccIamResTypeMap 中 DynamicGrouping 映射到哪个 TypeID?

一句话结论:BizCustomQuery(值为 "biz_custom_query")。

src/ac/iam/adaptor.go 第 75 行,meta.DynamicGrouping: BizCustomQuery。动态分组在 IAM 中注册为业务维度的自定义查询资源类型。

Q7. HostInstance 的 Update 动作如何按业务 ID 分流?

一句话结论:businessID > 0 映射到 EditBusinessHostbusinessID == 0 映射到 EditResourcePoolHost

src/ac/iam/adaptor.go 第 170-185 行,这个判断是硬编码在 ConvertResourceAction 中的,不走映射表。Find 动作同理:businessID > 0ViewBusinessResourcebusinessID == 0ViewResourcePoolHost

Q8. AuthorizeBatch 和 AuthorizeAnyBatch 的区别是什么?

一句话结论:AuthorizeBatch 要求所有资源都通过才通过;AuthorizeAnyBatch 只要求至少一个通过。

src/ac/iam/iam.go 第 1198-1208 行,两者都调用同一个 authorizeBatch,通过 exact 参数区分。exact=true 时调用 AuthorizeBatchexact=false 时调用 AuthorizeAnyBatch

Q9. 哪些动作不需要走 IAM 鉴权?

一句话结论:SkipAction 和映射到 Skip/SkipType 的动作。

src/ac/iam/adaptor.go 第 144-146 行,SkipAction 直接返回 Skipsrc/ac/iam/adaptor.go 第 1289-1293 行,Skip 类型的资源在 parseAttributesToBatchOptions 中直接设为 Authorized=true,不发 IAM 请求。

Q10. 批量鉴权的 HTTP 请求体格式是什么?

一句话结论:AuthBatchOptions{System, Subject, Batch[]}

src/ac/iam/iam.go 第 1306-1314 行,AuthBatchOptions 包含 System: SystemIDCMDB"bk_cmdb")、Subject{Type:"user", ID: user.UserName}Batch[](每个元素的 Action.ID 是 IAM ActionID 字符串)。

Q11. RegisterResourceCreatorAction 返回什么?

一句话结论:返回 []IamCreatorActionPolicy,即创建者对该资源类型的默认权限策略列表。

src/ac/iam/iam.go 第 1335-1341 行,RegisterResourceCreatorAction 的返回值包含该资源类型对应的所有 ActionID(如 EditBusinessCustomQuery),CMDB 可以缓存这些策略。

Q12. 动态分组的 Execute 动作映射到哪个 IAM ActionID?

一句话结论:ViewBusinessResource,不是编辑权限。

src/ac/iam/adaptor.go 第 258 行,meta.Execute: ViewBusinessResource。这意味着执行动态分组不需要编辑权限,只需要业务查看权限。但执行结果受业务权限约束。

Q13. SyncIAMSysInstances 和 Register 有什么区别?

一句话结论:Register 是启动时全量注册;SyncIAMSysInstances 是运行时差分同步。

src/ac/iam/iam.go 第 735-843 行,SyncIAMSysInstances 在 CMDB 运行中检测 IAM 侧资源与本地定义的差异,只同步新增和删除的部分。它先删后增,顺序和 Register 一致,但只操作有差异的资源。

Q14. 主机在业务下和资源池下的 Find 权限映射为什么不同?

一句话结论:因为业务下的主机和资源池的主机属于不同的权限命名空间。

src/ac/iam/adaptor.go 第 178-184 行,businessID > 0 时映射到 ViewBusinessResource,表示"查看业务资源";businessID == 0 时映射到 ViewResourcePoolHost,表示"查看资源池主机"。两个 ActionID 在 IAM 中对应不同的策略模板。

Q15. 资源类型映射中 SkipType 是什么意思?

一句话结论:不需要在 IAM 注册鉴权的内部资源类型。

src/ac/iam/adaptor.go 第 73 行,meta.HostFavorite: SkipType。像主机收藏这类纯用户本地数据,不涉及跨用户共享,不需要走 IAM 鉴权。

Q16. ConvertSysInstanceActionID 生成什么格式的 ActionID?

一句话结论:格式为 "<action_type>_sysinst_<modelID>",如 "view_sysinst_123"

src/ac/iam/adaptor.go 第 201-221 行,actionTypeCreate/Edit/Delete/View 之一,modelID 是模型 ID。IAMSysInstTypePrefix = "sysinst_"

Q17. parseAttributesToBatchOptions 在什么情况下返回 nil opts?

一句话结论:当所有资源都是 SkipActionSkip 类型时。

src/ac/iam/iam.go 第 1301-1304 行,authBatchArr 为空(没有需要鉴权的资源)时,直接返回 nil, decisions, nil,不走 HTTP 请求。

Q18. NewIAM 在什么情况下返回空的 &IAM{}?

一句话结论:当 EnableAuthorize() == false 时。

src/ac/iam/iam.go 第 52-53 行,if !auth.EnableAuthorize() { return new(IAM), nil }。此时 Client 为 nil,所有后续调用都走短路逻辑直接放行。

Q19. resourceActionMap 覆盖了哪些主要资源类型?

一句话结论:涵盖了 CMDB 几乎所有主要资源类型的动作映射。

src/ac/iam/adaptor.go 第 223-611 行,映射表包含 Business、BizSet、DynamicGrouping、HostInstance、ProcessServiceInstance、CloudArea、CloudAccount、KubeCluster、KubeNode、AuditLog、Project 等 40+ 种资源类型的动作映射。未在映射表中的资源类型,如果实现了 IsCMDBSysInstance(自定义模型),则走动态生成逻辑。

Q20. 如果 IAM 服务宕机,CMDB 会发生什么?

一句话结论:HTTP 请求超时或失败,CMDB 返回鉴权错误,用户无法操作受保护的资源。

src/ac/iam/iam.go 第 1231-1237 行和 1238-1245 行,AuthorizeBatchAuthorizeAnyBatch 在 HTTP 请求失败时直接返回错误,而不是返回 Authorized=false。如果 EnableAuthorize() == true 且 IAM 不可达,CMDB 所有需要鉴权的操作都会失败。运维可以关闭 EnableAuth 配置降级,但会丧失权限控制能力。

FAQ 全篇总纲

本 FAQ 覆盖了 CMDB 3.14.6 IAM 权限模型的 6 大维度:

  • 开关与初始化:Q1(EnableAuthorize)、Q3(Register 顺序)、Q4(Redis 锁)、Q18(NewIAM 空返回)
  • 适配器翻译:Q5(AdaptAuthOptions 三层)、Q6(ccIamResTypeMap)、Q7(按 businessID 分流)、Q16(动态 ActionID)
  • 批量鉴权:Q8(AuthorizeBatch vs Any)、Q9(跳过逻辑)、Q10(请求体格式)、Q17(nil opts 条件)
  • 创建者授权:Q11(返回值)、Q12(动态分组 Execute 映射)
  • 同步机制:Q13(SyncIAMSysInstances vs Register)、Q14(业务/资源池 Find 权限差异)
  • 特殊处理:Q15(SkipType)、Q19(resourceActionMap 覆盖范围)、Q20(IAM 宕机影响)

Roadmap 后续预告

下一篇 #14:动态分组 — 按条件自动归类主机

将带你深入 src/scene_server/host_server/service/dynamic_grouping.go,看动态分组的创建链路如何通过 AutoRunTxn 保证原子性,条件模型如何做类型安全校验,以及 variable_condition 如何实现运行时参数覆盖。

后续专题预告:

  • #15:云区域管理 — 多云/混合云账号与区域管理
  • #16:拓扑缓存加速 — 业务拓扑秒级查询的 Redis 缓存策略
  • #17:k8s 资源纳管 — Pod/Deployment/Service 统一管理
  • #18:容器拓扑关联 — Pod ↔ Host ↔ 机柜全链路映射
  • #19:主机锁定 — 并发控制与分布式锁
  • #20:批量导入导出 — Excel 模板与数据校验
posted @ 2026-07-06 19:58  左扬  阅读(37)  评论(0)    收藏  举报