请求排查Azure DevOps非活跃用户删除脚本无报错但未生效问题
我编写了一款PowerShell脚本,用于通过Azure DevOps对应API删除组织内的非活跃用户,同时在删除时排除管理员组中的非活跃用户。脚本执行过程无报错,但目标用户并未被删除。希望了解是否在API调用或必要参数配置方面存在遗漏,以下是相关脚本:
# Define required variables $Organizations = @( "OrganizationName" ) $AdminGroups = @( "ADgroup" ) $LogFile = "$(Get-Location)\AzDO_Cleanup_Log_$(Get-Date -Format 'yyyy-MM-dd').txt" $CsvFilePath = "$(System.DefaultWorkingDirectory)/deletedUsers.csv" $AzureDevOpsPAT = $token $DaysInactiveThreshold = 10 # Users inactive for more than this will be deleted # Function to log messages to console and file function Write-Log { param ([string]$Message) $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $LogEntry = "$Timestamp - $Message" Write-Host $LogEntry Add-Content -Path $LogFile -Value $LogEntry } # Function to call Azure DevOps REST API function Invoke-AzDOApi { param ( [string]$Uri, [string]$Method ) $Headers = @{Authorization = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$AzureDevOpsPAT")) } try { return Invoke-RestMethod -Uri $Uri -Headers $Headers -Method $Method -ContentType "application/json" } catch { Write-Log "Error calling API $Uri - $_" return $null } } # Get the cutoff date for inactivity $CutoffDate = (Get-Date).AddDays(-$DaysInactiveThreshold) # Loop through each organization $DeletedUsers = @() foreach ($Org in $Organizations) { Write-Log "Processing organization: $Org" # Step 1: Retrieve all users $UsersUri = "https://vsaex.dev.azure.com/$Org/_apis/memberentitlements?api-version=7.1-preview.2" $Users = Invoke-AzDOApi -Uri $UsersUri -Method "GET" if (-not $Users -or -not $Users.items) { Write-Log "No users found or API call failed for $Org. Skipping..." continue } # Step 2: Retrieve admin group descriptors $AdminDescriptors = @() $GroupsUri = "https://vssps.dev.azure.com/$Org/_apis/graph/groups?api-version=7.1-preview.1" $GroupsData = Invoke-AzDOApi -Uri $GroupsUri -Method "GET" foreach ($Group in $AdminGroups) { $GroupDescriptor = ($GroupsData.value | Where-Object { $_.displayName -eq $Group }).descriptor if ($GroupDescriptor) { $AdminDescriptors += $GroupDescriptor } } # Step 3: Filter inactive users foreach ($User in $Users.items) { $displayName = $User.member.displayName $email = $User.member.mailAddress $status = $User.accessLevel.status $lastAccess = $User.lastAccessedDate $userDescriptor = $User.member.descriptor Write-Log "Checking user: $displayName ($email)" # Skip active users if ($status -eq "active") { Write-Log "User $displayName is active. Skipping..." continue } # Convert last access date to DateTime object if ($lastAccess) { $LastAccessedDate = [DateTime]$lastAccess } else { Write-Log "User $displayName has no recorded login activity. Treating as inactive." $LastAccessedDate = (Get-Date).AddYears(-10) # Consider users with no activity as very old } # Check if user is inactive (last login > 10 days ago) if ($LastAccessedDate -gt $CutoffDate) { Write-Log "User $displayName logged in within the last $DaysInactiveThreshold days. Skipping..." continue } # Step 4: Check if user belongs to any admin group $MembershipUri = "https://vssps.dev.azure.com/$Org/_apis/graph/memberships/$userDescriptor?api-version=7.1-preview.1" $Memberships = Invoke-AzDOApi -Uri $MembershipUri -Method "GET" $IsAdmin = $false foreach ($Membership in $Memberships.value) { if ($Membership.containerDescriptor -in $AdminDescriptors) { $IsAdmin = $true break } } # Step 5: Delete non-admin inactive users if (-not $IsAdmin) { Write-Log "Deleting inactive user: $displayName ($email) - Last Login: $LastAccessedDate" $DeleteUri = "https://vsaex.dev.azure.com/$Org/_apis/userentitlements/$userDescriptor?api-version=7.1" Invoke-AzDOApi -Uri $DeleteUri -Method "DELETE" # Log deletion $DeletedUsers += [PSCustomObject]@{ Organization = $Org DisplayName = $displayName Email = $email Status = $status LastAccessed = $LastAccessedDate DeletedOn = Get-Date } } else { Write-Log "Skipping admin user: $displayName ($email)" } } Write-Log "Finished processing organization: $Org" Write-Log "----------------------------------------" } # Step 6: Export to CSV if any users were deleted if ($DeletedUsers.Count -gt 0) { if (Test-Path $CsvFilePath) { $DeletedUsers | Export-Csv -Path $CsvFilePath -NoTypeInformation -Append } else { $DeletedUsers | Export-Csv -Path $CsvFilePath -NoTypeInformation } Write-Log "Deleted users logged to $CsvFilePath" } else { Write-Log "No inactive users deleted, skipping CSV creation." }
排查与修复建议
1. 删除API端点错误
脚本中使用的删除端点https://vsaex.dev.azure.com/$Org/_apis/userentitlements/$userDescriptor?api-version=7.1不符合Azure DevOps API规范。正确的删除用户授权接口应为成员授权(Member Entitlements),需要使用memberEntitlementId而非用户descriptor:
# 替换原DeleteUri为: $DeleteUri = "https://vsaex.dev.azure.com/$Org/_apis/memberentitlements/$($User.id)?api-version=7.1"
说明:$User.id是从memberentitlements接口返回的items.id字段,这才是删除用户授权所需的唯一标识。
2. 验证PAT权限
确保你的PAT拥有以下核心权限,否则会出现调用无报错但无实际操作的情况:
- Member Entitlement Management:勾选「读写」权限
- Graph:勾选「读写」权限
3. 管理员组成员检查逻辑缺陷
当前脚本仅检查用户的直接组成员关系,若用户通过嵌套组继承管理员权限,会被误判为非管理员;同时组名匹配可能因大小写/空格问题失败:
修复点:
- 改用支持嵌套组查询的API:
# 替换原MembershipUri为: $MembershipUri = "https://vssps.dev.azure.com/$Org/_apis/graph/memberships/$userDescriptor/transitive?api-version=7.1-preview.1"
- 组名匹配添加不区分大小写规则:
$GroupDescriptor = ($GroupsData.value | Where-Object { $_.displayName -ieq $Group }).descriptor
4. 日期转换与比较的时区偏差
API返回的lastAccessedDate是UTC格式字符串,直接转换为[DateTime]可能因本地时区导致判断错误:
修复点:
# 带时区的日期转换 if ($lastAccess) { $LastAccessedDate = [DateTime]::Parse($lastAccess, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal) } # 同时将CutoffDate转换为UTC时间 $CutoffDate = (Get-Date).ToUniversalTime().AddDays(-$DaysInactiveThreshold)
5. 增强API调用的错误验证
当前函数对DELETE请求的成功判断不足,DELETE请求成功时Invoke-RestMethod无返回内容,导致调用失败(如403、404)被忽略。修改函数:
function Invoke-AzDOApi { param ( [string]$Uri, [string]$Method ) $Headers = @{Authorization = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$AzureDevOpsPAT")) } try { if ($Method -eq "DELETE") { $response = Invoke-RestMethod -Uri $Uri -Headers $Headers -Method $Method -ContentType "application/json" -StatusCodeVariable statusCode if ($statusCode -ne 204) { Write-Log "DELETE request to $Uri returned unexpected status code: $statusCode" return $null } return $response } else { return Invoke-RestMethod -Uri $Uri -Headers $Headers -Method $Method -ContentType "application/json" } } catch { Write-Log "Error calling API $Uri - Status Code: $($_.Exception.Response.StatusCode.value__), Message: $($_.Exception.Message)" return $null } }
6. 用户状态判断的完整性
脚本仅判断$status -eq "active",但Azure DevOps用户授权状态还有pending等其他活跃类状态,建议扩展判断:
# 替换原跳过活跃用户的逻辑: if ($status -in "active", "pending") { Write-Log "User $displayName is in active/pending status. Skipping..." continue }
内容的提问来源于stack exchange,提问作者user25518631

