Skip to main content

Generate and Populate an Entra Lab

Use this walkthrough when you want a cloud-first identity environment centered on Entra, Microsoft 365-style collaboration, B2B guests, and tenant governance.

DataGen does not replace Microsoft Graph or Entra administration. It gives you the realistic source model and normalized artifacts; Graph PowerShell turns the selected parts of that model into tenant objects.

:::warning Use a disposable tenant Run this against a dedicated developer or lab tenant. Do not run bulk user, group, or guest creation against a production tenant until you have reviewed the exact object set and added your own naming, licensing, and cleanup controls. :::

Requirements

  • DataGen module imported
  • catalog content available
  • Microsoft Graph PowerShell SDK installed
  • tenant permissions to create users, groups, and group memberships
  • a naming prefix or other lab marker that makes cleanup unambiguous

Reference assets

1. Generate and export the DataGen world

Test-SEScenario -Path .\entra-lab.json

$world = New-SEEnterpriseWorld -ScenarioPath .\entra-lab.json -Seed 2307
$world | Get-SEWorldSummary

$world | Save-SEEnterpriseWorld -Path .\out\entra-lab\entra-lab.seworld -Compress
$world | Export-SEEnterpriseWorld `
-OutputPath .\out\entra-lab\normalized `
-Format Json `
-Profile Normalized `
-IncludeManifest `
-IncludeSummary `
-Overwrite

The tenant population examples use these artifacts:

  • entities/accounts.json
  • entities/groups.json
  • links/group_memberships.json
  • entities/cloud_tenants.json
  • entities/policies.json
  • links/policy_target_links.json

2. Inspect what will become tenant objects

$exportRoot = Resolve-Path .\out\entra-lab\normalized

$accounts = Get-Content "$exportRoot\entities\accounts.json" | ConvertFrom-Json
$groups = Get-Content "$exportRoot\entities\groups.json" | ConvertFrom-Json
$memberships = Get-Content "$exportRoot\links\group_memberships.json" | ConvertFrom-Json
$tenants = Get-Content "$exportRoot\entities\cloud_tenants.json" | ConvertFrom-Json

$accounts | Group-Object user_type, identity_provider | Select-Object Name, Count
$groups | Select-Object name, group_type, mail_enabled, purpose | Format-Table -AutoSize
$tenants | Format-Table -AutoSize

Decide which generated objects you actually want to materialize. For many labs, creating a representative slice of users, groups, and memberships is more useful than importing every generated account.

3. Connect to Microsoft Graph

Install-Module Microsoft.Graph -Scope CurrentUser
Import-Module Microsoft.Graph.Users
Import-Module Microsoft.Graph.Groups
Import-Module Microsoft.Graph.Identity.SignIns

Connect-MgGraph -Scopes 'User.ReadWrite.All','Group.ReadWrite.All','Directory.ReadWrite.All'
Get-MgContext

Use delegated access for interactive lab work. For repeatable CI-style tenant refreshes, use an application identity with the least privileges required for the objects you create.

4. Create cloud-only users

The example below creates enabled member users and tags them with a lab prefix through the display name. Keep a small limit until your tenant cleanup story is proven.

$labPrefix = 'DG-LAB'
$passwordProfile = @{
password = 'ChangeMe-For-Lab-Only-001!'
forceChangePasswordNextSignIn = $true
}

$createdUsersBySourceId = @{}

$accounts |
Where-Object { $_.account_type -eq 'User' -and $_.user_type -ne 'Guest' } |
Select-Object -First 50 |
ForEach-Object {
$displayName = "$labPrefix $($_.display_name)"
$upn = $_.user_principal_name

$existing = Get-MgUser -Filter "userPrincipalName eq '$upn'" -ErrorAction SilentlyContinue
if ($existing) {
$createdUsersBySourceId[$_.id] = $existing.Id
return
}

$user = New-MgUser `
-AccountEnabled ([bool]$_.enabled) `
-DisplayName $displayName `
-MailNickname $_.sam_account_name `
-UserPrincipalName $upn `
-PasswordProfile $passwordProfile

$createdUsersBySourceId[$_.id] = $user.Id
}

If your generated UPN suffix is not verified in the lab tenant, remap it before calling New-MgUser.

5. Invite B2B guests when the scenario includes them

Guest users are usually created through invitation rather than ordinary member-user creation. Review external domains first, then invite only the slice you intend to test.

$guestCandidates = $accounts |
Where-Object { $_.user_type -eq 'Guest' -and $_.mail } |
Select-Object -First 10

$guestCandidates | Select-Object display_name, mail, invited_organization_id, access_expires_at

# Example invitation shape:
# foreach ($guest in $guestCandidates) {
# New-MgInvitation `
# -InvitedUserDisplayName "$labPrefix $($guest.display_name)" `
# -InvitedUserEmailAddress $guest.mail `
# -InviteRedirectUrl 'https://myapps.microsoft.com' `
# -SendInvitationMessage:$false
# }

Keeping invitations commented by default is intentional. Guest creation can reach real external mailboxes if you enable invitation messages.

6. Create groups

$createdGroupsBySourceId = @{}

$groups |
Where-Object { $_.group_type -in @('Security','M365','Microsoft365') } |
Select-Object -First 40 |
ForEach-Object {
$mailNickname = ($_.name -replace '[^A-Za-z0-9]', '').ToLowerInvariant()
if ([string]::IsNullOrWhiteSpace($mailNickname)) {
$mailNickname = "group$($_.id)"
}

$displayName = "$labPrefix $($_.name)"
$existing = Get-MgGroup -Filter "displayName eq '$displayName'" -ErrorAction SilentlyContinue
if ($existing) {
$createdGroupsBySourceId[$_.id] = $existing.Id
return
}

$isUnified = $_.group_type -in @('M365','Microsoft365')
$groupTypes = if ($isUnified) { @('Unified') } else { @() }

$group = New-MgGroup `
-DisplayName $displayName `
-MailEnabled ([bool]$_.mail_enabled) `
-MailNickname $mailNickname `
-SecurityEnabled (-not [bool]$_.mail_enabled -or -not $isUnified) `
-GroupTypes $groupTypes `
-Description $_.purpose

$createdGroupsBySourceId[$_.id] = $group.Id
}

Microsoft 365 groups, dynamic groups, mail-enabled security groups, and Teams-backed collaboration all have additional service rules. Treat this as the identity shell, then layer Exchange, Teams, SharePoint, or Intune behavior intentionally.

7. Apply group memberships

foreach ($membership in $memberships) {
$groupObjectId = $createdGroupsBySourceId[$membership.group_id]
if (-not $groupObjectId) {
continue
}

$memberObjectId = $createdUsersBySourceId[$membership.member_object_id]
if (-not $memberObjectId) {
continue
}

New-MgGroupMemberByRef `
-GroupId $groupObjectId `
-BodyParameter @{
'@odata.id' = "https://graph.microsoft.com/v1.0/directoryObjects/$memberObjectId"
}
}

For larger labs, add retry handling for throttling and keep a checkpoint file that maps DataGen IDs to Graph object IDs.

8. Use policies as a configuration checklist

DataGen can emit Entra, conditional-access, access-review, and tenant-governance style policy records. Some policy surfaces are not safe to blindly materialize from a generic walkthrough, so start by reviewing them.

$policies = Get-Content "$exportRoot\entities\policies.json" | ConvertFrom-Json
$policyTargets = Get-Content "$exportRoot\links\policy_target_links.json" | ConvertFrom-Json

$policies |
Where-Object { $_.platform -match 'Entra|Azure|Microsoft' } |
Select-Object name, policy_type, category, status, description |
Format-Table -AutoSize

$policyTargets | Group-Object target_type, assignment_mode | Select-Object Name, Count

Use this output to configure Conditional Access, identity governance, or Intune policies by hand or in a tenant-specific adapter once you have confirmed the effect. The walkthrough should make the next action obvious, but it should not silently create high-impact access controls.

9. Validate the tenant

Get-MgUser -Filter "startswith(displayName,'$labPrefix')" -All |
Select-Object -First 10 DisplayName, UserPrincipalName, AccountEnabled

Get-MgGroup -Filter "startswith(displayName,'$labPrefix')" -All |
Select-Object -First 10 DisplayName, MailEnabled, SecurityEnabled, GroupTypes

$sampleGroup = Get-MgGroup -Filter "startswith(displayName,'$labPrefix')" -Top 1
if ($sampleGroup) {
Get-MgGroupMember -GroupId $sampleGroup.Id -All
}

10. Clean up or rerun

Keep a lab prefix, export the DataGen-to-Graph ID map, and delete only objects with that prefix when refreshing the tenant. For long-running demos, prefer restoring from a known tenant snapshot or rerunning the same scenario and seed into a fresh tenant.

For deterministic generation and comparison patterns, continue to Repeatable Lab Runs.