Maintain control of your Microsoft 365 data
Post Reply
HavardBakke
Service Provider
Posts: 4
Liked: never
Joined: Sep 10, 2024 8:20 am
Full Name: Havard Bakke
Contact:

Powershell - List all users in job based on organization

Post by HavardBakke »

Hello,

We are trying to export a list of all users for an organization with two jobs (Exchange split into its own job).
We are finding that getting a list without duplicates is practically impossible without manually going through it and removing duplicates or connecting it to other sources. (unless i am missing something).
This is specifically for users that are deleted in M365, since the currently active users can be queried and correlated against M365.
Objects like displayname and email does not always match between to two jobs when we pull the data with Get-VBOEntityData.

Having an option to pull this information based on the organization would be really helpful.

Get-VBOBackupItem will not work, since in the job the entire organization is selected and exclude is used on the resources it should not contain.
MaartenA
Service Provider
Posts: 135
Liked: 51 times
Joined: Oct 31, 2021 7:03 am
Full Name: maarten
Contact:

Re: Powershell - List all users in job based on organization

Post by MaartenA »

Get-VBOOrganizationUser can help you i think?

See https://helpcenter.veeam.com/docs/vbo36 ... html?ver=8
HavardBakke
Service Provider
Posts: 4
Liked: never
Joined: Sep 10, 2024 8:20 am
Full Name: Havard Bakke
Contact:

Re: Powershell - List all users in job based on organization

Post by HavardBakke »

That command pulls the information from users that exist in the tennant from what i understand, it wont contain users deleted in the M365 tenant sadly.
Polina
Veeam Software
Posts: 3890
Liked: 977 times
Joined: Oct 21, 2011 11:22 am
Full Name: Polina Vasileva

Re: Powershell - List all users in job based on organization

Post by Polina »

Hi Havard,

Sorry, I read it two times but still unsure which users you would like to list - those that are currently included into your backup jobs, those that are already protected/backed up, or those that are available for protection in your tenant organization?
Also, what is the end goal? Am I correct that you want to ensure that your job scopes do not overlap?
HavardBakke
Service Provider
Posts: 4
Liked: never
Joined: Sep 10, 2024 8:20 am
Full Name: Havard Bakke
Contact:

Re: Powershell - List all users in job based on organization

Post by HavardBakke »

Hello Polina,

Our goal is to list out all users included in repos for organizations.
Exsisting users that are actively beeing backup up and users that have been deleted in M365 but are still retained in the backup.
For customers with only one repo this it works with just querying the repo, but for customers with split jobs we cannot find a way to do this without getting duplicates.
HavardBakke
Service Provider
Posts: 4
Liked: never
Joined: Sep 10, 2024 8:20 am
Full Name: Havard Bakke
Contact:

Re: Powershell - List all users in job based on organization

Post by HavardBakke »

For anyone with the same issue, this is what we have so far.
It does not remove all duplicates, but it gets close.

Code: Select all

$org = Get-VBOOrganization -Name ""
$jobs = Get-VBOJob | Where-Object { $org.Id -eq $_.Organization.Id }
$repos = $jobs | ForEach-Object { $_.Repository } | Sort-Object -Property Id -Unique


#Union-Find for identity resolution
$parent = @{}

function Find-Root {
    param([string]$x)
    if (-not $parent.ContainsKey($x)) { $parent[$x] = $x }
    while ($parent[$x] -ne $x) {
        $parent[$x] = $parent[$parent[$x]]  # path compression
        $x = $parent[$x]
    }
    return $x
}

function Union-Keys {
    param([string]$a, [string]$b)
    $rootA = Find-Root $a
    $rootB = Find-Root $b
    if ($rootA -ne $rootB) {
        # Prefer email-looking keys as root
        if ($rootB -match '@' -and $rootA -notmatch '@') {
            $parent[$rootA] = $rootB
        } else {
            $parent[$rootB] = $rootA
        }
    }
}

function Normalize {
    param([string]$value)
    if ([string]::IsNullOrWhiteSpace($value)) { return $null }
    return $value.Trim().ToLower()
}


#Collect all raw records with their identifiers
# Each raw record: list of identifiers + data
$rawRecords = [System.Collections.Generic.List[hashtable]]::new()

# --- Org users first ---
#Write-Host "Loading M365 organization users..." -ForegroundColor Cyan
$orgUsers = Get-VBOOrganizationUser -Organization $org
#Write-Host "Loaded $($orgUsers.Count) org users" -ForegroundColor Green

foreach ($ou in $orgUsers) {
    $ids = [System.Collections.Generic.List[string]]::new()

    $email = Normalize $ou.Email
    if (-not $email) { $email = Normalize $ou.UserName }
    $name = Normalize $ou.DisplayName

    if ($email) { $ids.Add($email) }
    if ($name -and $name -ne $email) { $ids.Add($name) }

    if ($ids.Count -eq 0) { continue }

    $rawRecords.Add(@{
        Identifiers = $ids
        DisplayName = if (-not [string]::IsNullOrWhiteSpace($ou.DisplayName)) { $ou.DisplayName.Trim() } else { $null }
        Email       = if ($email) { $email } else { $null }
        Source      = "M365"
        RepoName    = $null
        AccountType = $null
        IsMailboxBackedUp      = $false
        MailboxBackedUpTime    = $null
        IsArchiveBackedUp      = $false
        IsOneDriveBackedUp     = $false
        OneDriveBackedUpTime   = $null
        IsPersonalSiteBackedUp = $false
    })
}

# --- Repo entities ---
foreach ($repo in $repos) {
    #Write-Host "Loading repo: $($repo.Name)" -ForegroundColor Cyan
    $entities = Get-VBOEntityData -Type User -Repository $repo

    foreach ($entity in $entities) {
        $ids = [System.Collections.Generic.List[string]]::new()

        $email = Normalize $entity.Email
        $name  = Normalize $entity.DisplayName

        if ($email) { $ids.Add($email) }
        if ($name -and $name -ne $email) { $ids.Add($name) }

        if ($ids.Count -eq 0) { continue }

        $rawRecords.Add(@{
            Identifiers = $ids
            DisplayName = if (-not [string]::IsNullOrWhiteSpace($entity.DisplayName)) { $entity.DisplayName.Trim() } else { $null }
            Email       = if ($email) { $email } else { $null }
            Source      = "Repo"
            RepoName    = $repo.Name
            AccountType = if (-not [string]::IsNullOrWhiteSpace($entity.AccountType)) { $entity.AccountType } else { $null }
            IsMailboxBackedUp      = [bool]$entity.IsMailboxBackedUp
            MailboxBackedUpTime    = $entity.MailboxBackedUpTime
            IsArchiveBackedUp      = [bool]$entity.IsArchiveBackedUp
            IsOneDriveBackedUp     = [bool]$entity.IsOneDriveBackedUp
            OneDriveBackedUpTime   = $entity.OneDriveBackedUpTime
            IsPersonalSiteBackedUp = [bool]$entity.IsPersonalSiteBackedUp
        })
    }
}

#Write-Host "`nTotal raw records collected: $($rawRecords.Count)" -ForegroundColor Gray


#Build union-find — link all identifiers per record
foreach ($record in $rawRecords) {
    $ids = $record.Identifiers
    # Ensure all ids for this record are in the same group
    for ($i = 1; $i -lt $ids.Count; $i++) {
        Union-Keys $ids[0] $ids[$i]
    }
}

#Write-Host "Union-find built with $($parent.Count) identifiers" -ForegroundColor Gray


#Merge records by their root identity
$userMap = @{}

foreach ($record in $rawRecords) {
    $root = Find-Root $record.Identifiers[0]

    if (-not $userMap.ContainsKey($root)) {
        $userMap[$root] = [PSCustomObject]@{
            DisplayName            = $null
            Email                  = $null
            AccountType            = $null
            InM365                 = $false
            IsMailboxBackedUp      = $false
            MailboxBackedUpTime    = $null
            IsArchiveBackedUp      = $false
            IsOneDriveBackedUp     = $false
            OneDriveBackedUpTime   = $null
            IsPersonalSiteBackedUp = $false
            Repos                  = [System.Collections.Generic.List[string]]::new()
        }
    }

    $entry = $userMap[$root]

    # Display name: prefer real name over email-as-name
    if ($record.DisplayName) {
        if (-not $entry.DisplayName -or
            ($entry.DisplayName -match '@' -and $record.DisplayName -notmatch '@')) {
            $entry.DisplayName = $record.DisplayName
        }
    }

    # Email: grab from any source that has it
    if ($record.Email -and $record.Email -match '@') {
        if (-not $entry.Email) {
            $entry.Email = $record.Email
        }
    }

    # Account type: prefer specific (SharedMailbox) over generic (User)
    if ($record.AccountType) {
        if (-not $entry.AccountType -or
            ($entry.AccountType -eq 'User' -and $record.AccountType -ne 'User')) {
            $entry.AccountType = $record.AccountType
        }
    }

    # M365 presence
    if ($record.Source -eq "M365") {
        $entry.InM365 = $true
    }

    # Backup states (OR across all sources)
    if ($record.IsMailboxBackedUp)      { $entry.IsMailboxBackedUp = $true; $entry.MailboxBackedUpTime = $record.MailboxBackedUpTime }
    if ($record.IsArchiveBackedUp)      { $entry.IsArchiveBackedUp = $true }
    if ($record.IsOneDriveBackedUp)     { $entry.IsOneDriveBackedUp = $true; $entry.OneDriveBackedUpTime = $record.OneDriveBackedUpTime }
    if ($record.IsPersonalSiteBackedUp) { $entry.IsPersonalSiteBackedUp = $true }

    # Repos
    if ($record.RepoName -and $entry.Repos -notcontains $record.RepoName) {
        $entry.Repos.Add($record.RepoName)
    }
}


#Results — only users that appear in at least one repo
$results = $userMap.Values |
    Where-Object { $_.Repos.Count -gt 0 } |
    ForEach-Object {
        $_ | Add-Member -NotePropertyName 'AccountState' -NotePropertyValue $(
            if ($_.InM365) { "Active" } else { "Deleted/Retained" }
        ) -PassThru
    } |
    Sort-Object AccountState, AccountType, DisplayName

# Summary
$active      = ($results | Where-Object { $_.AccountState -eq "Active" }).Count
$deleted     = ($results | Where-Object { $_.AccountState -eq "Deleted/Retained" }).Count
$userCount   = ($results | Where-Object { $_.AccountType -eq 'User' }).Count
$sharedCount = ($results | Where-Object { $_.AccountType -and $_.AccountType -ne 'User' }).Count

#Write-Host "`n===== $($org.Name) =====" -ForegroundColor Green
#Write-Host "Total unique backed-up entities: $($results.Count)"
#Write-Host "Active in M365:                  $active"
#Write-Host "Deleted/Retained:                $deleted"
#Write-Host "Users:                           $userCount"
#Write-Host "Shared Mailboxes:                $sharedCount"

$results | Format-Table DisplayName, Email, AccountType, AccountState,
    IsMailboxBackedUp, IsOneDriveBackedUp, IsPersonalSiteBackedUp,
    @{ Name = 'Repos'; Expression = { $_.Repos -join ', ' } } -AutoSize
Post Reply

Who is online

Users browsing this forum: Semrush [Bot], Yann.S and 13 guests