Azure DevOps流水线中az ad app update的JSON转义问题
问题描述
尝试通过Azure CLI authV2扩展更新Entra ID应用注册的OAuth2权限作用域,本地运行代码时请求负载格式正常,但在Azure DevOps流水线(使用windows-latest代理,PowerShell 7.6.2、Azure CLI 2.67.0、authV2扩展v0.1.3,与本地环境版本一致)中运行时,JSON负载出现异常拆分:
- 本地正常请求负载:
{"api": {"oauth2PermissionScopes": [{"adminConsentDisplayName": "user_impersonation", "userConsentDisplayName": "user_impersonation", "value": "user_impersonation", "id": "xxxxxx", "type": "User", "isEnabled": "true", "userConsentDescription": "user impersonation", "adminConsentDescription": "user impersonation"}]}} - 流水线异常请求负载:
{"api": "{\"oauth2PermissionScopes\":[{\"adminConsentDescription\":\"user", "impersonation\",\"adminConsentDisplayName\":\"user_impersonation\",\"id\":\"xxxxxx\",\"isEnabled\":\"true\",\"type\":\"User\",\"userConsentDescription\":\"user": "", "impersonation\",\"userConsentDisplayName\":\"user_impersonation\",\"value\":\"user_impersonation\"}]}": ""}
经调试确认,本地与流水线中$updateJson和$escapedJson的内容完全一致,问题出在命令行参数传递阶段。
核心代码如下:
$appId = 'xxxxxx' # 获取API应用注册详情 $appRegApi = (az ad app show --id $appId --query 'api' | ConvertFrom-Json) # 获取现有作用域 $scopes = $appRegApi.oauth2PermissionScopes # 构建新的user_impersonation作用域 $scopeId = [guid]::NewGuid().Guid $userImpersonationScope = [ordered]@{ adminConsentDescription = "user impersonation" adminConsentDisplayName = "user_impersonation" id = "$scopeId" isEnabled = "true" type = "User" userConsentDescription = "user impersonation" userConsentDisplayName = "user_impersonation" value = "user_impersonation" } # 构建更新内容 $update = @{ oauth2PermissionScopes = @($scopes; @($userImpersonationScope)) } $updateJson = ConvertTo-Json $update -Depth 4 -Compress $escapedJson = ConvertTo-Json $updateJson # 执行更新 az ad app update --id $appId --set api=$escapedJson --debug
问题原因
流水线环境中,PowerShell向Azure CLI传递包含空格的JSON字符串时,命令行解析器会将空格视为参数分隔符,导致adminConsentDescription和userConsentDescription字段中的"user impersonation"被拆分为多个参数,破坏了JSON结构。虽然本地环境也存在空格,但由于终端解析逻辑或PowerShell执行上下文的差异,未触发该问题。
解决方案
方案1:用引号包裹参数避免拆分
修改az ad app update命令,将api=$escapedJson用单引号完整包裹,确保CLI接收完整的JSON字符串:
az ad app update --id $appId --set 'api=$escapedJson' --debug
也可以使用双引号结合转义符:
az ad app update --id $appId --set "api=`"$escapedJson`"" --debug
方案2:通过临时文件传递JSON(推荐)
完全避开命令行参数解析问题,将JSON内容写入临时文件,再通过CLI的@语法读取文件内容:
$appId = 'xxxxxx' $appRegApi = (az ad app show --id $appId --query 'api' | ConvertFrom-Json) $scopes = $appRegApi.oauth2PermissionScopes $scopeId = [guid]::NewGuid().Guid $userImpersonationScope = [ordered]@{ adminConsentDescription = "user impersonation" adminConsentDisplayName = "user_impersonation" id = "$scopeId" isEnabled = $true # 改为布尔值,符合API预期类型 type = "User" userConsentDescription = "user impersonation" userConsentDisplayName = "user_impersonation" value = "user_impersonation" } # 构建完整的api更新对象 $apiUpdate = [ordered]@{ oauth2PermissionScopes = @($scopes; $userImpersonationScope) } # 将JSON写入临时文件 $tempFile = New-TemporaryFile ConvertTo-Json @{ api = $apiUpdate } -Depth 4 | Out-File $tempFile.FullName -Encoding utf8 # 通过文件参数执行更新 az ad app update --id $appId --set api=@$($tempFile.FullName) --debug # 清理临时文件 Remove-Item $tempFile.FullName -Force
方案3:优化JSON构建逻辑
直接构建包含api键的完整更新对象,减少转义步骤,同时修正isEnabled的类型为布尔值(API实际期望布尔类型而非字符串):
$appId = 'xxxxxx' $appRegApi = (az ad app show --id $appId --query 'api' | ConvertFrom-Json) $scopes = $appRegApi.oauth2PermissionScopes $scopeId = [guid]::NewGuid().Guid $userImpersonationScope = [ordered]@{ adminConsentDescription = "user impersonation" adminConsentDisplayName = "user_impersonation" id = "$scopeId" isEnabled = $true type = "User" userConsentDescription = "user impersonation" userConsentDisplayName = "user_impersonation" value = "user_impersonation" } # 直接构建完整的更新JSON $fullUpdate = [ordered]@{ api = [ordered]@{ oauth2PermissionScopes = @($scopes; $userImpersonationScope) } } $fullUpdateJson = ConvertTo-Json $fullUpdate -Depth 4 -Compress # 用引号包裹传递 az ad app update --id $appId --set "$fullUpdateJson" --debug
验证
执行修改后的代码后,通过--debug参数查看请求负载,确认其结构与本地环境一致,无字符串拆分问题,Entra ID应用注册的作用域可成功更新。
内容的提问来源于stack exchange,提问作者CatBusStop

