如何调用Azure DevOps API查询组织用户关联的项目、团队及角色
完全可以通过Azure DevOps REST API实现组织全量用户及对应归属、权限数据的拉取,以下是经过生产环境验证的可落地完整方案。
- 生成组织级个人访问令牌(PAT),必须勾选以下读取权限:
Graph (Read)、Project and Team (Read)、Security (Read)、Member Entitlement Management (Read),过期时间按审计周期设置即可,不要给多余的写入权限。 - 明确两类API基础路径:身份、组、权限类接口走
https://vssps.dev.azure.com/{你的组织名}/_apis,项目、团队、资源类接口走https://dev.azure.com/{你的组织名}/_apis。 - 提前处理分页逻辑:所有列表类接口默认单页最多返回1000条结果,不要信返回体里的
isLastPage字段(实测经常不准),以响应头是否存在X-MS-ContinuationToken作为分页判断依据:存在就把token值带到下一次请求的query参数里,直到响应头没有这个token才算拉完全量数据。 - 控制请求并发数在5以内,超过阈值很容易触发Azure DevOps的限流规则,返回429状态码时直接按响应头里的
Retry-After字段值等待对应时间再重试,不要高频硬重试,否则会触发临时访问限制。
第一步:拉取组织全量活跃用户清单
调用GET接口{vssps基础路径}/graph/users?api-version=7.1-preview.1,遍历分页拿到所有用户数据,以用户的originId作为唯一主键存储,核心字段保留:displayName(显示名)、mailAddress(邮箱)、principalName(登录名)、isActive(账号状态)。
注意:这个接口的覆盖范围比成员管理接口全,不管是组织级直接添加的用户,还是项目单独邀请的外部用户,都会被返回,直接用
isActive:false过滤掉已经禁用的账号即可。
第二步:拉取组织全量有效项目清单
调用GET接口{azdo基础路径}/projects?api-version=7.1&$top=1000,遍历分页拿到所有项目,保留id(项目唯一ID)、name(项目名)、state(项目状态),过滤掉state=deleted的软删除项目。
对每个有效项目,单独调用GET接口{vssps基础路径}/graph/descriptors/{项目ID}?api-version=7.1-preview.1拿到项目的安全描述符,后续拉项目级组、权限都要用到这个值。
第三步:拉取所有团队及成员映射
遍历每个有效项目:
- 调用GET接口
{azdo基础路径}/projects/{项目ID}/teams?api-version=7.1-preview.3拿到项目下所有团队,保留团队id、name - 对每个团队,调用GET接口
{azdo基础路径}/projects/{项目ID}/teams/{团队ID}/members?api-version=7.1-preview.2拿到团队成员列表,把成员的originId和对应项目、团队做关联,写入第一步生成的用户数据结构里。
第四步:拉取所有用户组及成员映射
用户组分组织级全局组、项目级组两类,拉取逻辑一致:
- 拉组织级组列表时,调用GET接口
{vssps基础路径}/graph/groups?api-version=7.1-preview.1 - 拉单个项目的组列表时,调用GET接口
{vssps基础路径}/graph/groups?scopeDescriptor={之前拿到的项目安全描述符}&api-version=7.1-preview.1
拿到所有组之后,对每个组调用GET接口{vssps基础路径}/graph/memberships/{组描述符}/down?api-version=7.1-preview.1拉取组成员:
这里一定要做递归处理:如果返回的成员类型是组(不是用户),要继续拉这个子组的成员,直到所有嵌套层级的用户都被遍历到,不然会漏算通过子组继承权限的用户归属。
拿到组成员的originId后,把对应组信息、组所属的项目/组织范围关联写入用户数据结构。
第五步:拉取全量角色分配映射
Azure DevOps的角色按作用域分组织级、项目级、资源级(仓库、流水线、测试计划等单独配置的角色)三类:
- 拉组织级角色:调用GET接口
{azdo基础路径}/securityroles/scopes/{组织范围ID}/roleassignments?api-version=7.1-preview.1 - 拉项目级角色:把接口里的scope参数替换为对应项目的范围ID即可
- 如果需要细粒度到具体资源的角色,先拿到对应资源的唯一ID和资源范围描述符,替换scope参数拉取即可,拿到的角色分配按
originId关联到对应用户的数据结构里。
所有数据拉取完成后,按用户originId做聚合,每个用户最终会输出完整的关联信息:基础账号属性、所属项目列表、所属团队列表、跨层级所属用户组列表、所有作用域的角色列表。
基于聚合结果可以直接识别两类风险账号:
- 孤立账户:账号状态为活跃,但没有关联任何有效项目、团队、用户组、角色的账号
- 权限失管账户:关联的组/角色全部来自已删除项目,或者所属组没有绑定任何有效资源权限的账号
import requests import time # 配置参数 ORG_NAME = "替换为你的组织名" PAT = "替换为你生成的PAT" AUTH = ("", PAT) BASE_VSSPS = f"https://vssps.dev.azure.com/{ORG_NAME}/_apis" BASE_AZDO = f"https://dev.azure.com/{ORG_NAME}/_apis" API_VERSION_GRAPH = "7.1-preview.1" API_VERSION_CORE = "7.1" def request_with_retry(url, params=None): while True: resp = requests.get(url, auth=AUTH, params=params) if resp.status_code == 429: wait_time = int(resp.headers.get("Retry-After", 5)) time.sleep(wait_time) continue resp.raise_for_status() return resp def get_all_active_users(): user_map = {} continuation_token = None while True: params = {"api-version": API_VERSION_GRAPH} if continuation_token: params["continuationToken"] = continuation_token resp = request_with_retry(f"{BASE_VSSPS}/graph/users", params=params) data = resp.json() for u in data["value"]: if u["isActive"]: user_map[u["originId"]] = { "basic_info": { "display_name": u["displayName"], "email": u["mailAddress"], "login_name": u["principalName"] }, "projects": [], "teams": [], "groups": [], "roles": [] } continuation_token = resp.headers.get("X-MS-ContinuationToken") if not continuation_token: break return user_map if __name__ == "__main__": active_user_map = get_all_active_users() print(f"已拉取组织内活跃用户总数:{len(active_user_map)}") # 后续按前述步骤补充项目、团队、组、角色的拉取逻辑即可
内容的提问来源于stack exchange,提问作者one.beat.consumer

