Feb 10 2021
11:28 AM
- last edited on
Mar 05 2024
01:07 PM
by
TechCommunityAP
Feb 10 2021
11:28 AM
- last edited on
Mar 05 2024
01:07 PM
by
TechCommunityAP
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:
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:
Update:
Feb 10 2021 12:42 PM
@Fromelard thanks for mentioning my blog
Feb 10 2021 03:00 PM
Mar 08 2021 06:52 AM