user management
1 TopicAzure DevOps - How to collect all accounts from Organization using PowerShell
When you are managing an Azure DevOps instance, you can have one or many Organizations (depending of your isolation and distribution requirements). Each organization will have a specific Account list (with license associated), but there is no automatic cleanup to remove disable accounts or consolidate all account in one view, and the built-in CSV Export module is limited in usage. This PowerShell Script will connect to all organization you specified in setting (using a dedicated Token you have to use or dedicated per Organization) to get all declared accounts, and loop for each account the Project part of. If No project assigned, you will have "NO PROJECT" as value. Import-module AzureAD connect-azureAD #region -- Customized Script Settings (need to be reviewed) --- # ================== / Parameters to Adapt \ ================== [string]$SharePointTeamSite = "https://tenant.sharepoint.com/sites/YourCollection/" [string]$DocumentLibrary = "YourDocLib" [string]$GlobalPATForAllORganizations= "xxxyyyyxxxxxxyyyyy" #If you have a global PAT [string]$JSonFolderPath = "C:\JSONDEVOPS" [string]$DomainNameEmail = "@emaildomain.com" # ================== \ Parameters to Adapt / ================== #endregion #region -- Internal Script Settings --- $OrganizationList = @() [string]$PAT = "" [string]$OrganizationName = "" [string]$Organization = "" [string]$UserGroupJSonFilePath = "" [string]$UserListJSonFilePath = "" [string]$AccountValidInAzureAD = "" $DataRefreshDate = Get-Date -Format "yyyy-MM-dd" [string]$TempEmailAddress = "" $UserGroupsOveralOrganization = @() $UserGroupOveralOragnizationJSonFilePath = "$JSonFolderPath\DevOps-UserGroups.json" #endregion #region -- Each Az DevOps Organization -- #YourOrganization1 $PAT = $GlobalPATForAllORganizations #If specific https://dev.azure.com/YourOrganization1/_usersSettings/tokens $OrganizationName = "YourOrganization1" $OrganizationWithPAT = New-Object PSObject -property @{OrganizationName=$OrganizationName;OrganizationPAT=$PAT} $OrganizationList += $OrganizationWithPAT #YourOrganization2 $PAT = $GlobalPATForAllORganizations #If specific https://dev.azure.com/YourOrganization2/_usersSettings/tokens $OrganizationName = "YourOrganization2" $OrganizationWithPAT = New-Object PSObject -property @{OrganizationName=$OrganizationName;OrganizationPAT=$PAT} $OrganizationList += $OrganizationWithPAT #YourOrganization3 $PAT = $GlobalPATForAllORganizations #If specific https://dev.azure.com/YourOrganization3/_usersSettings/tokens $OrganizationName = "YourOrganization3" $OrganizationWithPAT = New-Object PSObject -property @{OrganizationName=$OrganizationName;OrganizationPAT=$PAT} $OrganizationList += $OrganizationWithPAT #endregion foreach($MyOrganization in $OrganizationList) { $Organization = "https://dev.azure.com/$($MyOrganization.OrganizationName)/" $UserGroupsPerOrganization = @() $UserGroupJSonFilePath = "$JSonFolderPath\$($MyOrganization.OrganizationName)-UserGroups.json" $UserListJSonFilePath = "$JSonFolderPath\$($MyOrganization.OrganizationName)-UsersList.json" echo $($MyOrganization.OrganizationPAT) | az devops login --org $Organization az devops configure --defaults organization=$Organization $allUsers = az devops user list --org $Organization --top 10000 | ConvertFrom-Json #write-host "All Users:", $allUsers $allUsers | ConvertTo-Json | Out-File -FilePath $UserListJSonFilePath foreach($au in $allUsers.members) { if($au.user.mailAddress.endswith($DomainNameEmail)) { $TempEmailAddress = $au.user.mailAddress -replace "'", "''" $MyAzureADUser = Get-AzureADUser -Filter "userPrincipalName eq '$($TempEmailAddress)'" if($MyAzureADUser) { if($MyAzureADUser.AccountEnabled) { $AccountValidInAzureAD = "ACCOUNT VALID IN AZURE AD" } else { $AccountValidInAzureAD = "ACCOUNT DISABLE IN AZURE AD" } } else { $AccountValidInAzureAD = "ACCOUNT NOT EXIST IN AZURE AD" } } else { $AccountValidInAzureAD = "ACCOUNT NOT INTERNAL EMAIL" } $activeUserGroups = az devops security group membership list --id $au.user.principalName --org $Organization --relationship memberof | ConvertFrom-Json [array]$groups = ($activeUserGroups | Get-Member -MemberType NoteProperty).Name if ($groups.count -gt 0) { foreach ($aug in $groups) { $UserGroupsPerOrganization += New-Object -TypeName PSObject -Property @{ DevOpsOrganizationName=$MyOrganization.OrganizationName principalName=$au.user.principalName displayName=$au.user.displayName mailAddress=$au.user.mailAddress UserID=$au.id AccountValidInAzureAD = $AccountValidInAzureAD LicenseType=$au.accessLevel.licenseDisplayName dateCreated=$au.dateCreated lastAccessedDate=$au.lastAccessedDate ProjectName= $($activeUserGroups.$aug.principalName.Split("\\"))[0] GroupName=$activeUserGroups.$aug.principalName DataRefreshDate = $DataRefreshDate } } } else { $UserGroupsPerOrganization += New-Object -TypeName PSObject -Property @{ DevOpsOrganizationName=$MyOrganization.OrganizationName principalName=$au.user.principalName displayName=$au.user.displayName mailAddress=$au.user.mailAddress UserID=$au.id AccountValidInAzureAD = $AccountValidInAzureAD LicenseType=$au.accessLevel.licenseDisplayName dateCreated=$au.dateCreated lastAccessedDate=$au.lastAccessedDate ProjectName= "[NO PROJECT]" GroupName="[NO PROJECT GROUP]" DataRefreshDate = $DataRefreshDate } } } $UserGroupsPerOrganization | ConvertTo-Json | Out-File -FilePath $UserGroupJSonFilePath $UserGroupsOveralOrganization += $UserGroupsPerOrganization } $UserGroupsOveralOrganization | ConvertTo-Json | Out-File -FilePath $UserGroupOveralOragnizationJSonFilePath Import-Module PnP.PowerShell -DisableNameChecking Connect-PnPOnline -Url $SharePointTeamSite -UseWebLogin $Files = Get-ChildItem -Path $JSonFolderPath -Force -Recurse #Upload All files from the directory ForEach ($File in $Files) { Write-host "Uploading $($File.Directory)\$($File.Name)" #upload a file to sharepoint online using powershell - Upload File and Set Metadata Add-PnPFile -Path "$($File.Directory)\$($File.Name)" -Folder $DocumentLibrary -Values @{"Title" = $($File.Name)} } The result will be a JSON file (saved in SharePoint), you can easily load in Power Bi to build your dedicated Report helping you to adapt Develop license with real usage like: Remove all accounts Disable in Azure AD Downgrade all account license from Basic+ to Stakeholder if no connection since more than 1 year Remove all accounts without any project Any other rules part of your Governance model That could also be adapted to use an Azure Engine to cleanup automatically your Organization. In any case, that part need to be integrated into your FinOps process to optimize your license costs as you really need and use. Fabrice Romelard References used to build that script: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-windows?view=azure-cli-latest&tabs=azure-powershell https://vinijmoura.medium.com/how-to-list-all-users-and-group-permissions-on-azure-devops-using-azure-devops-cli-54f73a20a4c7 Update: Script cleaned to be easily modified and used8.5KViews2likes3Comments