蓝鲸 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
- 批量鉴权:AuthorizeBatch 与 AuthorizeAnyBatch 批量下发 IAM 请求
- 创建者授权:RegisterResourceCreatorAction 向 IAM 注册资源创建者的编辑权限
了解
- SyncIAMSysInstances 的三轮差分同步机制(删除/新增/更新)
- 动态分组(BizCustomQuery)在 IAM 中的特殊处理
目录
- 一、IAM 在 CMDB 架构中的定位
- 二、权限总开关:EnableAuthorize() 的全局控制
- 三、注册链路:Register() 的 11 步依赖顺序
- 四、适配器链路:AdaptAuthOptions 的三层翻译
- 五、资源类型映射:ccIamResTypeMap 完整解析
- 六、动作映射:ConvertResourceAction 的四种分支
- 七、批量鉴权:AuthorizeBatch / AuthorizeAnyBatch
- 八、创建者授权:RegisterResourceCreatorAction
- 九、动态分组与 IAM:BizCustomQuery 特殊处理
- FAQ(常见 20 问)
- Roadmap 后续预告
一、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 做跨产品授权
读 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 策略后再开启,实现平滑过渡。
读 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
}
这里有几个关键设计点:
- 配置文件驱动:EnableAuth 从配置文件中读取字符串,用 strconv.ParseBool 转成布尔值。配置值可以是 "true"、"false"、"1"、"0" 等。
- once.Do 保证只设置一次:setEnableAuth 用 sync.Once 保证 enableAuth 只在首次调用时被赋值,后续调用直接忽略。这防止了运行中重复设置开关导致的状态不一致。
- 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),直接改名会导致中间状态失败。源码用加下划线 "_" 作为中间变量,分两步完成改名。
读 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。
读 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
}
三层翻译的分工非常清晰:
- 第一层:动作翻译(ConvertResourceAction):把 CMDB 的动作枚举(meta.Create/Update/Delete/Find)翻译成 IAM 的 ActionID(如 "edit_business_host")。这里会考虑 BusinessID 的正负决定具体映射到业务动作还是资源池动作。
- 第二层:类型翻译(ConvertResourceType):把 CMDB 的资源类型枚举翻译成 IAM 的 TypeID。如果 CMDB 资源类型在 ccIamResTypeMap 中找不到,就检查是否是系统实例(IsCMDBSysInstance),是的话直接用类型名字符串作为 IAM TypeID。
- 第三层:资源实例生成(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 枚举,名字偏向业务语义(如 Business、HostInstance、DynamicGrouping)。IAM 的 TypeID 偏向系统标识(如 "biz"、"host"、"biz_custom_query")。这张映射表建立了两个命名空间之间的对应关系。
问题二:部分资源不需要鉴权
有些 CMDB 内部资源类型(如 meta.HostFavorite、meta.ModelInstanceTopology)不需要在 IAM 注册,它们映射到 SkipType。
问题三:动态模型实例有特殊处理
CMDB 支持用户自定义模型(Custom Object),这些模型的实例在 IAM 中用系统实例(sysinst_<modelID>)来注册。IsCMDBSysInstance 判断资源类型是否以 "sysinst_" 开头,如果是则直接用类型名作为 IAM TypeID。
读 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 的动作枚举有 CreateMany、FindMany、UpdateMany、DeleteMany,但 IAM 通常只注册了单数形式(Create、Find 等)。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"。
读 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:SkipAction → Skip(直接放行)
- 分支 1:批量动作归一化(FindMany → Find)
- 分支 2:硬编码特殊处理(HostInstance 按 businessID 分流)
- 分支 3 + 4:映射表 + 动态系统实例
七、批量鉴权:AuthorizeBatch / AuthorizeAnyBatch
What — 批量鉴权在做什么?
AuthorizeBatch 和 AuthorizeAnyBatch 定义在 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。
读 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(任一过就过)
- 跳过逻辑:SkipAction 和 Skip 类型直接放行
- 结果合并:跳过资源保持原位标 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 负责把这些策略应用到本地缓存。
以动态分组为例,看 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
}
这段代码在 CreateDynamicGroup 的 autoRunTxnFunc 中调用(第 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 的 CreateBusinessCustomQuery、EditBusinessCustomQuery、DeleteBusinessCustomQuery 三个 ActionID。
Why — 为什么动态分组需要特殊的 IAM 处理?
问题一:动态分组是业务维度的资源
动态分组按 bk_biz_id 隔离,不同业务下的分组互不可见。在 IAM 中,BizCustomQuery 资源类型挂靠在业务(biz)下,权限继承业务的访问范围。
问题二:动态分组的执行需要单独鉴权
执行动态分组(meta.Execute)是一个特殊动作,它在 IAM 中映射到 ViewBusinessResource,而不是 EditBusinessCustomQuery。这是因为执行动态分组本质上是"查询符合条件的主机列表",属于查看操作而非编辑操作。
问题三:动态分组的权限和业务权限联动
如果用户对某个业务没有访问权限,他既看不到该业务下的主机,也无法使用该业务下的动态分组。动态分组的执行结果受业务权限的约束。
读 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 个要点
- 资源类型:DynamicGrouping → BizCustomQuery
- Execute 特殊映射:meta.Execute → ViewBusinessResource(不是编辑权限)
- 创建者自动授权:RegisterResourceCreatorAction → EditBusinessCustomQuery
十、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/。这两种路径格式完全不同,需要不同的生成逻辑。
问题三:某些动作不需要资源实例
比如 CreateBusiness、ViewCloudArea 等动作,创建和查看时不需要传实例 ID,只需要 IAM 知道"哪个系统"在请求即可。
读 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-Code 和 X-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。
所有端点基于 src/ac/iam/client.go 和 src/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.go 的 ActionID 类型别名和 ActionIDNameMap 映射表中。这些 ActionID 对应 IAM 侧"用户在蓝鲸权限中心看到的权限项名称"。
Why — 为什么需要这么细粒度的动作拆分?
问题一:权限最小化原则
蓝鲸 IAM 的核心原则是"最小权限":每个操作都应该有独立的权限项。如果"查看业务主机"和"编辑业务主机"共用同一个 ActionID,管理员就无法只授权"查看"而不授权"编辑"。
问题二:K8s 资源纳管增加了大量容器管理动作
v3.14.6 版本中,K8s 相关动作占新增 ActionID 的 40% 以上,包括 create_container_cluster、edit_container_node、delete_container_namespace 等,覆盖了 Pod/Deployment/StatefulSet/DaemonSet/Job/CronJob 等 8 种 Workload 类型。
业务维度动作(以 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_query、edit_business_host)
- ResourcePool 开头:资源池维度资源(view_resource_pool_host、edit_resource_pool_host)
- Container 开头:K8s 容器管理资源(create_container_cluster ~ delete_container_pod)
- Sys 开头:系统级资源(create_sys_model、delete_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_host 的 RelatedActions 包含 view_business_resource,意味着"如果用户有编辑主机权限,他必定有查看业务资源权限"。AuthServer 在本地完成这个依赖链的展开,不需要每次都查 IAM。
问题三:策略匹配支持 AND/OR 组合
IAM 的 Policy 表达式支持复杂的条件组合(AND、OR、NOT),AuthServer 的 calculatePolicy 函数(src/scene_server/auth_server/sdk/auth/authorize.go)负责解析这些表达式并与请求资源进行匹配。
读 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
这里有两个值得深入的设计:
- 动作去重优化:如果一次请求 100 个主机但都是同一个动作,只查一次策略再复用到 100 个资源上,减少 IAM 调用次数
- 50 并发上限:pipe := make(chan struct{}, 50) 控制最大并发数,防止突发流量压垮 IAM 服务
- 策略映射:通过 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_host → view_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 个布尔开关(RegisterModuleEnabled、RegisterSetEnabled、SkipReadAuthorization、RegisterAuditCategoryEnabled),是 CMDB 权限体系对外暴露的统一入口。
Why — 为什么需要 AuthManager 而不是直接用 IAM?
问题一:分层解耦
如果各服务直接引用 IAM 结构体,IAM 模块的内部变更(改 Client 结构、换 HTTP 实现)会直接影响所有服务消费者。AuthManager 作为门面(Facade),隔离了底层实现,Consumer 只需依赖接口而非具体类型。
问题二:多实例差异配置
CMDB 各 Server(host_server、event_server 等)可能有不同的鉴权策略。AuthManager 在每个 Server 初始化时通过 SetConfig 单独配置,实现差异化授权。
问题三:SetAuthorizer 与 SetViewer 的组合设计
SetAuthorizer 和 SetViewer 允许在运行时替换授权器和查看器实现,这对测试和降级场景非常重要。
读 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.AuthorizeInterface 和 Viewer 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
每个服务都调用 initModules(src/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 个开关:RegisterModuleEnabled、RegisterSetEnabled、SkipReadAuthorization(默认 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 映射到 EditBusinessHost,businessID == 0 映射到 EditResourcePoolHost。
读 src/ac/iam/adaptor.go 第 170-185 行,这个判断是硬编码在 ConvertResourceAction 中的,不走映射表。Find 动作同理:businessID > 0 → ViewBusinessResource,businessID == 0 → ViewResourcePoolHost。
Q8. AuthorizeBatch 和 AuthorizeAnyBatch 的区别是什么?
一句话结论:AuthorizeBatch 要求所有资源都通过才通过;AuthorizeAnyBatch 只要求至少一个通过。
读 src/ac/iam/iam.go 第 1198-1208 行,两者都调用同一个 authorizeBatch,通过 exact 参数区分。exact=true 时调用 AuthorizeBatch,exact=false 时调用 AuthorizeAnyBatch。
Q9. 哪些动作不需要走 IAM 鉴权?
一句话结论:SkipAction 和映射到 Skip/SkipType 的动作。
读 src/ac/iam/adaptor.go 第 144-146 行,SkipAction 直接返回 Skip。src/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 行,actionType 是 Create/Edit/Delete/View 之一,modelID 是模型 ID。IAMSysInstTypePrefix = "sysinst_"。
Q17. parseAttributesToBatchOptions 在什么情况下返回 nil opts?
一句话结论:当所有资源都是 SkipAction 或 Skip 类型时。
读 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 行,AuthorizeBatch 和 AuthorizeAnyBatch 在 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 模板与数据校验

浙公网安备 33010602011771号