SSO、SCIM 与 RBAC:把入转离变成可验证的身份流水线
周一早上,新同事能用企业账号登录代码托管平台,却看不到项目;下午管理员把他手工加进团队,问题暂时消失。两个月后他转到另一个部门,旧团队权限没有撤回;离职当天,身份提供方已经禁用账号,平台里的会话、个人访问令牌和外部集成却仍能访问仓库。三个时刻分别暴露了三条链路:SSO 只完成认证入口,SCIM 负责用户与群组的配置生命周期,RBAC 才决定主体对资源能做什么。把三者画成一个“已接入”的勾选框,入职会缺权限,转组会叠权限,离职会留残权。
可运营的身份体系不以“用户能登录”为验收终点,而以状态收敛为目标:权威目录中的人员状态发生变化后,目标工具中的 User、Group、成员关系和角色映射在约定时间内达到预期;旧会话、独立 token、机器人授权和本地缓存另有明确回收动作;每一步能找到主体、对象、时间、结果和关联请求。这样团队排查的不是“为什么又没同步”,而是能指出故障停在认证、配置、授权还是凭据回收。
先把三个控制面拆开
SSO(Single Sign-On)解决“谁正在登录”。常见企业入口通过 SAML 或 OpenID Connect 把认证交给身份提供方,目标工具消费断言或 token,并根据 issuer、audience、签名、时间窗口和用户标识建立会话。SSO 可以集中 MFA、登录策略和账号禁用入口,但不会自动创建所有目标账号,也不会天然决定仓库、项目或管理权限。
SAML assertion 和 OIDC ID Token 都是建立登录会话的证据,不应被当作研发工具 API 的长期访问令牌。目标工具建立自己的浏览器 session 后,session 的空闲超时、绝对寿命和撤销能力由目标工具控制;OAuth access token、refresh token、PAT 与 SSH key 又有各自的签发者和撤销面。身份提供方禁用用户只能阻止后续认证,不足以推出这些存量能力已经失效。
SCIM(System for Cross-domain Identity Management)解决“目标工具里应该存在哪些身份对象”。RFC 7643 定义 JSON 表示的 User、Group、公共属性和企业用户扩展;RFC 7644 定义基于 HTTP 的查询、创建、替换、局部更新和删除协议。SCIM 客户端通常是身份平台,服务提供方是研发工具。客户端把权威目录中的人员和群组变化推到目标工具,但目标产品可以限制属性可变性、过滤能力、嵌套群组和停用语义。
RBAC(Role-Based Access Control)解决“已经存在的主体可以对什么对象执行什么动作”。角色可能位于企业、组织、项目、仓库、空间或环境层级;群组成员关系只是角色绑定的输入。RFC 7643 明确 Group 可表达常见群组关系,但不定义授权模型,群组产生的权限语义由服务提供方决定。因此,SCIM 返回 200 或 204 只能证明配置请求被接受,不能证明最终角色正确。
| 控制面 | 核心对象 | 最小成功证据 | 常见误判 |
|---|---|---|---|
| SSO | IdP 用户、断言、会话 | 受控入口登录成功,失败登录被策略拒绝 | 能登录就等于有正确权限 |
| SCIM | User、Group、成员关系 | 目标对象及属性与权威目录收敛 | 创建用户就等于完成授权 |
| RBAC | 角色、资源范围、绑定 | 正向动作允许,越权动作明确拒绝 | 群组名与角色名相同就会自动映射 |
| 凭据回收 | session、PAT、SSH key、App grant | 旧凭据调用失败且有审计事件 | 禁用 SSO 后所有 token 都立即失效 |
登录成功之后还有一条会话链
SAML 2.0 Profiles 定义 Single Logout profile,OIDC 则分别定义 RP-Initiated、Front-Channel 和 Back-Channel Logout。它们解决的是身份提供方与依赖方之间如何传播注销,不代表每个 SaaS 都完整实现,也不等于 OAuth token 撤销。OIDC Back-Channel Logout由身份提供方向已登记的依赖方端点发送带 logout_token 的服务端请求;前台注销依赖浏览器,容易受到标签页、第三方 Cookie 和网络中断影响。接入时必须从产品元数据、管理页和隔离实验确认支持哪一种,而不是看到“支持 OIDC”就假定支持全局注销。
先为每种能力登记签发者与撤销动作:
revocation_matrix:
browser_session:
issuer: engineering-tool
revoke: terminate user sessions in tool admin API
evidence: old session cookie rejected on private resource
oidc_login:
issuer: enterprise-idp
revoke: disable subject and propagate supported logout
evidence: new authorization request denied
oauth_grant:
issuer: engineering-tool
revoke: revoke grant or refresh token
evidence: refresh and API calls rejected
personal_access_token:
issuer: engineering-tool
revoke: revoke token object
evidence: authenticated identity endpoint rejected
ssh_key:
issuer: engineering-tool
revoke: remove key association
evidence: private repository handshake rejected正向实验先让合成用户登录,确认目标 session 能访问一个私有只读资源,并在审计中记录 session 主体。反向实验禁用该用户并触发产品支持的注销:新登录必须失败,原浏览器 session、另一个浏览器中的 session、测试 PAT 与测试 SSH key 分别重放。只有目标产品明确终止的会话才应立即失败;仍成功的路径要按矩阵调用对应撤销接口。若 OAuth 服务提供 RFC 7009 Token Revocation,调用撤销端点后还要验证 access token 与同一授权派生的 refresh token 的实际行为,因为服务端对相关 token 的级联处理取决于实现。最终证据是每条原授权路径都在需要认证的私有资源上失败,而不是注销页显示“成功”。
建立权威源、稳定键和角色映射
身份流水线首先要确定唯一权威源。HR 系统可以决定在职状态和部门,企业目录维护登录身份和群组,目标工具消费用户、群组与角色。不要允许目标工具中的手工成员关系与目录同步长期并存而没有优先级,否则下一次同步可能覆盖人工修复,也可能让人工添加永久绕过离职回收。
用户关联要使用稳定且不可重分配的键。邮箱可读,但会因改名、域名迁移和别名变化而改变;短用户名也可能在离职后被复用。SCIM 的 id 是服务提供方生成的资源标识,externalId 可保存客户端侧稳定标识,userName 则常用于登录或匹配。工程台账至少记录 IdP 对象 ID、SCIM externalId、目标 id、主登录名和当前状态,排障时才不会把同名账户误认为同一主体。
下面的合成映射展示了配置对象与授权对象之间的显式关系。真实租户的组 ID、角色键和域名应存入受控配置,不把显示名称当稳定主键。
identity_source: idp.example.invalid
provisioning:
user_key: immutable_directory_object_id
scim_external_id: immutable_directory_object_id
login_name: primary_email
groups:
- source_id: grp-platform-readers-001
target_team: platform-readers
bindings:
- resource: org/demo-platform
role: read
- source_id: grp-platform-maintainers-002
target_team: platform-maintainers
bindings:
- resource: org/demo-platform
role: maintain
break_glass:
excluded_from_scim: true
owner: security-duty
review_interval_days: 30角色映射要同时回答“授予什么”和“没有授予什么”。例如 maintain 可以管理议题与合并,但不能修改企业 SSO 或导出审计;生产环境审批与仓库维护也不应塞进同一个角色。目标产品支持自定义角色时,先从任务所需动作反推权限;只支持固定角色时,记录多出来的能力以及补偿控制。映射变更必须像代码一样评审,因为给一个 IdP 群组绑定高权限角色,影响的是群组当前和未来全部成员。
用发现端点认识目标 SCIM 实现
在接通配置同步前,先把 SSO 登录入口放进隔离租户。SAML 配置通常涉及 IdP entity ID、目标服务的 ACS URL、受众、NameID/用户匹配属性、签名证书和时钟;OIDC 配置通常涉及 issuer、client ID、redirect URI、client secret 或私钥、scope 与 claims。字段名称随产品变化,但判断逻辑相同:issuer/entity ID 标识可信签发者,audience/client ID 防止发给其他应用的凭证被复用,ACS/redirect URI 把认证响应限制到登记回调,稳定用户属性负责关联既有账号。
sso_application: engineering-tool-demo
protocol: saml
idp_entity_id: https://idp.example.invalid/metadata
service_provider:
entity_id: https://tool.example.invalid/saml/metadata
acs_url: https://tool.example.invalid/saml/acs
user_match:
source_attribute: immutable_directory_object_id
fallback_email_match: false
signing:
require_signed_assertion: true
accepted_certificate_ids:
- cert-next
clock_skew_seconds: 120配置前从目标工具自己的管理页复制 entity ID 与回调地址,不从工单旧截图转录。保存后使用一个没有管理权限的合成用户发起 IdP-initiated 和 service-provider-initiated 登录,预期两种入口都进入同一个目标 User;再故意把 audience 改成未登记值或使用未授权用户,预期认证被拒绝并留下不含断言正文的错误事件。若错误是“用户不存在”,先判断产品要求预创建、JIT 还是 SCIM;不要通过打开全员 JIT 绕过同步作用域。
证书和 client secret 不能到期当天替换。目标支持多证书时,先加入 cert-next,用测试入口确认新签名被接受,再让 IdP 切换签名材料;观察登录成功率和错误类型后,移除 cert-old,最后用旧签名断言确认拒绝。只允许单证书的产品需要明确变更窗口、已登录管理员会话和本地恢复入口。OIDC secret 也采用 current/next 重叠或产品支持的双 secret 机制,切换前后验证 redirect URI、audience 与 token exchange,而不是只看配置页保存成功。
标准兼容不代表每个产品实现完全相同。RFC 7644 定义 /ServiceProviderConfig、/ResourceTypes 和 /Schemas 等发现入口,可用于确认过滤、PATCH、批处理、认证方案和资源属性。先在合成或隔离租户读取能力,不要一开始就向生产目录推送全量人员。
export SCIM_BASE='https://scim.example.invalid/v2'
export SCIM_TOKEN='<ephemeral-scim-token>'
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${SCIM_TOKEN}" \
-H 'Accept: application/scim+json' \
"${SCIM_BASE}/ServiceProviderConfig"
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${SCIM_TOKEN}" \
-H 'Accept: application/scim+json' \
"${SCIM_BASE}/ResourceTypes"预期结果不是某个固定 JSON,而是能确认 patch.supported、filter.supported、认证方案和 User/Group endpoint。401 表示 token 无效或认证头不符;403 表示凭据存在但缺少配置权限;404 常见于 base URL 或协议版本错误;429 说明需要退避与限速。若发现端点不可用,必须以目标产品文档给出的能力为准,并在集成测试中显式验证,不能猜测支持 PATCH 或复杂 filter。
查询用户时对 filter 进行 URL 编码,并把“没有结果”与“请求失败”分开。RFC 7644 允许服务提供方选择是否支持过滤;不支持的操作应返回带 scimType: invalidFilter 的 400,而不是由客户端悄悄退化为拉取全量用户。
curl --fail-with-body --get \
-H "Authorization: Bearer ${SCIM_TOKEN}" \
-H 'Accept: application/scim+json' \
--data-urlencode 'filter=externalId eq "usr-demo-1042"' \
--data-urlencode 'attributes=id,userName,active,externalId,meta' \
"${SCIM_BASE}/Users"预期 totalResults 为 1,且 externalId 与权威目录一致。返回 0 要检查同步作用域、属性映射和目标租户;返回多个结果说明稳定键策略已破坏,暂停自动更新,先去重和确定保留对象。不要用邮箱模糊搜索后取第一条,那会把身份错误变成权限错误。
从隔离租户跑通新增用户
入职验证必须覆盖“创建、登录、入组、授权和拒绝”五个结果。先创建一个合成用户和低权限群组,SCIM payload 只携带目标所需属性。企业扩展中的部门、成本中心、经理关系属于个人数据,只有确有授权用途时才同步。
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
],
"externalId": "usr-demo-1042",
"userName": "lin.tester@example.invalid",
"active": true,
"name": {
"givenName": "Lin",
"familyName": "Tester"
},
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"department": "Platform Lab",
"employeeNumber": "DEMO-1042"
}
}POST 成功通常返回 201 Created、服务端生成的 id 和 meta.location。随后把这个 id 加入目标 Group.members,再验证目标团队成员关系。部分产品不允许客户端直接修改用户的 groups 属性,而要求对 Group 资源做 PATCH;这是 RFC 模型中的正常差异,应按发现信息和产品文档实现。
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [{"value": "<target-user-id>"}]
}
]
}Group.members 是可增删的多值复杂属性,成员的 value 应引用目标 User 或 Group 的 SCIM id。这也决定了移除操作不能只写一个含糊的 path: members:按照 RFC 7644,remove 一个未带过滤器的多值属性会移除全部成员;移除单个成员应使用 members[value eq "<target-user-id>"]。若该用户本来就不在组中,服务端可以不改变资源并返回成功,因此 200 或 204 不能证明“刚刚删掉了一个残留成员”。客户端必须在 PATCH 后重新读取 Group,比较期望成员集合与实际成员集合,并再做资源访问反向实验。
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "remove",
"path": "members[value eq \"<target-user-id>\"]"
}
]
}正向实验以普通用户登录,读取 org/demo-platform 中允许的仓库;反向实验尝试修改 SSO 配置、读取未授权仓库或变更生产环境。成功标准必须同时包含“应允许的动作成功”和“应拒绝的动作返回 403 或产品定义的拒绝页”。只有正向成功不能发现角色过宽。审计中应能关联 SSO 登录主体、SCIM 变更主体、群组绑定和资源访问主体,但配置 token 的值本身不能进入日志。
转组不是添加新角色,而是权限集合替换
人员从开发组转到质量组时,最危险的实现是先添加新群组、旧群组靠人工稍后删除。只要删除失败,权限就会叠加。转组状态应表达为期望集合,例如 desiredGroups = {quality-readers},协调器计算 add = desired - actual 与 remove = actual - desired,并对高权限移除设定更短的收敛目标。
权威目录: groups = [quality-readers]
目标工具: groups = [platform-maintainers]
差异:
add = [quality-readers]
remove = [platform-maintainers]
验收:
quality 项目可读
platform 项目写入被拒绝
旧团队成员列表不再包含该用户
审计事件可关联同一次转组请求反向实验可以故意把目标角色映射键拼错,让 SCIM 群组同步成功而 RBAC 绑定失败。预期证据是:用户和 Group membership 已存在,登录也成功,但访问资源被拒绝;这说明故障位于“群组到角色”而不是 SSO 或 SCIM。另一类反向实验是模拟移除请求返回 429,确认客户端采用带抖动的指数退避、保留幂等关联 ID,并在超过收敛目标后告警,而不是无限重试淹没接口。
嵌套群组尤其需要验证。RFC 7643 允许 Group member 指向 User 或 Group,但服务提供方未必支持嵌套,也未必以相同方式展开间接成员。不要根据目录中“父组包含子组”推断目标权限已经继承;用一个只存在于子组的合成用户做正反访问测试。如果产品只支持直接成员,应该在身份侧扁平化并监控展开规模,而不是在目标端留下手工补丁。
离职回收要穿过五道状态门
离职不是把 active 改成 false 后结束。第一道门是 IdP 禁用与新 SSO 登录拒绝;第二道门是 SCIM 用户停用或删除;第三道门是 Group 和 RBAC 绑定移除;第四道门是目标工具中的 session、PAT、SSH key、OAuth grant、App installation 和个人 runner 等独立能力;第五道门是外部系统、缓存、导出数据和所有权交接。
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "replace",
"path": "active",
"value": false
}
]
}停用与 DELETE 的效果取决于产品。停用通常更利于保留内容归属和审计关联,DELETE 可能触发匿名化、转移或不可逆清理。先确认产品语义,再选择动作。GitHub 的 Enterprise Managed Users 团队管理说明 IdP 群组可驱动团队成员关系;GitLab SCIM 配置也列出了其配置与角色行为。产品能力和订阅边界会变化,实施时应在目标租户的当前文档与设置页确认,而不是把一个平台的停用语义复制到另一个平台。
离职反向实验要在隔离租户预先登记一个低权限测试 session、一个测试 token、一把测试 SSH key 和一个应用授权:先禁用 IdP 用户,确认新登录失败;再逐一重放旧访问路径。若任一路径仍成功,不要宣称 SSO 失效,而要按 session、PAT、SSH key、OAuth grant 或 App installation 标记独立回收缺口,并执行产品相应撤销动作。随后重新查询 User active、所有相关 Group 的 members、直接角色绑定和应用安装范围,避免“账号停用但旧群组或直授仍在”。最终成功证据是私有资源上的旧访问路径全部失败、恢复测试用户时不会自动带回旧高权限、内容 owner 已转移、审计能解释每次撤销,且没有误删需要保留的项目历史。
重入必须证明是同一个人、一个新授权周期
重新入职或误停用恢复不能简单执行 active=true。先判断是同一主体恢复还是新人员复用了邮箱、用户名或工号;只有权威目录中不可重分配的对象 ID 与原 externalId 一致,才允许关联原目标 id。邮箱相同但目录对象 ID 不同,应创建新主体并走冲突处置,不能为了消除重复账号而继承旧身份。
恢复流程先恢复 User 的可用状态,再根据当前岗位重新计算期望群组和角色;旧的直接授权、管理员角色、PAT、SSH key、OAuth grant、App installation 与 session 不应自动恢复。目标产品如果在软停用时保留这些对象,协调器要把“保留用于审计”和“重新启用访问”拆成两步。GitHub 的 SCIM 取消配置与恢复说明也区分软取消配置、恢复和不同触发动作;具体保留与恢复语义必须以当前部署形态实测。
恢复前:User inactive,期望群组为空,旧 token 已撤销
身份核对:directory_object_id 与 externalId 精确相等
恢复用户:active=true,但暂不授予业务角色
重新计算:仅加入当前岗位的 desiredGroups
正向验证:当前项目的只读动作成功
反向验证:旧项目写入、管理员动作、旧 token 和旧 session 失败
审计验收:恢复事件与新的审批单关联,旧授权周期保持关闭反例是把同邮箱的新员工关联到旧 id,或恢复用户后目标产品自动复原历史团队。应在隔离租户故意让目录创建一个同邮箱、不同稳定 ID 的对象,预期同步器进入冲突队列且不创建角色绑定;再对原测试用户执行恢复,预期只有当前期望群组出现。若旧高权限随恢复重现,应暂停自动恢复,清理产品中的直授与保留绑定,并把“恢复后权限差异为零”设为硬门禁。
把同步故障变成可定位的证据
身份问题可以沿链路逐层判断。登录页循环或 audience 错误属于 SSO;用户不存在或属性不更新属于 SCIM;成员关系正确但资源拒绝属于 RBAC 映射;用户已停用但 API 仍成功属于 token 或应用授权;只有某个客户端继续访问则还要检查本地 session 和缓存。
| 现象 | 首查证据 | 常见原因 | 修复后的再验证 |
|---|---|---|---|
| SSO 成功但目标无用户 | SCIM 请求日志、filter 查询 | 未进入同步作用域、稳定键冲突 | User 唯一且状态正确 |
| User 存在但不在团队 | Group resource、PATCH 响应 | 修改了只读 groups、组映射错误 | 直接成员与目标团队一致 |
| 团队正确但权限过大 | 角色绑定、资源层继承 | 高层角色继承、手工直授 | 越权动作稳定返回拒绝 |
| 转组后旧权限仍在 | 期望/实际群组差异 | 只 add 未 remove、重试积压 | 旧资源写入失败 |
| 离职后私有 API 仍成功 | token 清单、App grant、session、SSH key | 独立凭据未撤销或组织授权仍有效 | 旧路径在需要认证的资源上被拒绝 |
大量同步返回 429 | 限流头、重试队列年龄 | 全量轮询、无退避 | 增量恢复且队列收敛 |
SCIM 写入应具备幂等性。网络超时不代表服务端没有执行,客户端不能立即无条件 POST 创建第二个用户;应先用稳定键查询,再决定重试。更新时可利用 meta.version 或服务提供方支持的 ETag 条件请求避免覆盖并发修改。RFC 7644 要求同一 PATCH 请求内的操作按顺序计算并整体原子化:任一操作失败时恢复原资源并返回错误。客户端仍要在失败和超时后重新读取资源,既避免误重试,也能发现目标产品实现偏差。批量任务要记录每个资源的关联 ID、尝试次数、最终状态和脱敏错误,不在日志中保存 Bearer token 或完整个人属性。
会话、凭据与紧急账号必须另行治理
SSO 和 SCIM 的凭据本身是高价值能力。SCIM token 应属于受控非人身份,只有目标租户的配置权限,存入密钥系统,通过短期注入交给同步器;设定到期、轮换 owner 和上次使用监控。不要使用管理员个人 PAT 作为 SCIM bearer token,也不要把 token 放进 IaC state、命令历史、工单或截图。
紧急账号不能被普通 SCIM 流程意外删除,否则身份平台故障时团队失去恢复入口;也不能成为无人监管的永久后门。它应与日常账号隔离,使用强认证和双人取用,触发即告警,每次使用后轮换凭据并复核审计。演练内容是“身份提供方不可用时能否恢复”,不是绕过正常权限审批。
SSO 证书或 OIDC client secret 轮换也要采用重叠窗口:先在目标工具加入新材料,验证新签名或新 secret,再切换 IdP,观察失败率,最后移除旧材料。只替换一端会造成全员登录中断。元数据 URL、issuer、ACS/redirect URI、entity ID、audience、时钟偏差和证书链都应作为配置对象登记,而不是散落在个人笔记。
从试点扩展到多工具身份架构
小团队可以为单个工具维护一条 IdP 到 SaaS 的连接;工具增多后,架构重点转向一致对象模型和故障隔离。每个目标工具仍有独立的 enterprise application、SCIM 凭据、映射和告警,避免一个全局 token 能配置所有系统。共用的是稳定人员键、群组命名规范、角色分级和入转离事件,而不是共用高权限凭据。
同步模式通常有三种取舍:即时推送收敛快,但依赖 IdP 与目标 API 可用;周期对账能修复漂移,但会产生延迟和限流压力;事件推送加周期对账能兼顾速度与最终一致,却需要维护队列、幂等和死信处理。核心工具可采用事件驱动加短周期增量对账,低风险工具使用较长周期;无论哪种,都必须量化“身份事件年龄”和“高权限残留时长”。
架构指标不应只看同步成功率。至少包括:待处理事件最老年龄、创建/停用 P95 收敛时间、稳定键冲突数、孤儿账号数、手工直授数、无映射群组数、离职后有效 token 数、管理员角色人数和紧急账号使用次数。成功率 99.9% 仍可能掩盖唯一一名高权限离职者没有回收,因此高风险失败要按主体和权限等级告警。
用固定节奏保持身份闭环
每次接入新工具时,建立稳定键、用户与群组映射、角色矩阵、SCIM 凭据 owner、收敛目标和退出语义;用一个合成用户跑新增、转组、停用,再加入真实团队。每次角色或群组规则变更时,比较期望与实际权限,执行至少一个应允许和一个应拒绝动作。每次身份连接升级或证书轮换时,保留重叠材料和回退入口。
日常运行中,身份团队负责权威人员与群组,工具 owner 负责目标映射和产品行为,资源 owner 负责角色是否符合业务任务,安全团队复查高权限和紧急路径。离职事件必须同时触发 SSO、SCIM、RBAC、独立凭据和所有权交接;其中任何一步没有证据,都不能把工单标记为完成。
最终检查可以压缩成一条可重放链:合成新人只能访问被授予资源;转组后新权限出现且旧权限消失;停用后新登录、旧会话和测试 token 均失败;恢复测试用户时不会意外恢复旧高权限;审计记录能从人员事件关联到配置、授权和撤销。做到这里,SSO、SCIM 与 RBAC 才不再是三个采购功能,而是一条能够证明“谁在何时拥有什么权限”的工程流水线。
