<# .SYNOPSIS Creates the Active Directory objects for testing the self-service portal. .DESCRIPTION Creates, under a parent OU you choose: - a test OU with test users and two test groups, - a separate OU with a service account that is NOT inside the test OU, - the delegated rights the portal needs on user objects in the test OU, and nothing else. Delegated rights (inherited to user objects in the test OU only): write givenName, sn, displayName, mail, telephoneNumber, mobile; the "Change Password" right. Reading is already allowed for authenticated users. The account cannot create or delete objects, cannot reset passwords (unless -AllowResetPassword), and cannot write anything else. Run on a domain controller, or a machine with the AD module, as a domain administrator. Run it once with -WhatIf first. Nothing outside the two new OUs is changed. Not tested by the author against a live directory: read the -WhatIf output before you run it. .PARAMETER ParentDn Distinguished name of an existing OU or container that will hold the two new OUs. .EXAMPLE .\New-PortalTestEnvironment.ps1 -ParentDn "OU=Organisation,DC=example,DC=local" -WhatIf #> [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)][string]$ParentDn, [string]$TestOuName = 'PortalTest', [string]$ServiceOuName = 'PortalTestService', [string]$ServiceAccountName = 'svc-portal-test', [string]$GroupPrefix = 'PortalTest', [string]$MailDomain = 'example.test', [switch]$AllowResetPassword ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' Import-Module ActiveDirectory # --- helpers ----------------------------------------------------------------- $script:rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider function Get-RandomIndex([int]$Max) { $bytes = New-Object byte[] 4 $script:rng.GetBytes($bytes) return [int]([BitConverter]::ToUInt32($bytes, 0) % $Max) } function New-RandomPassword([int]$Length = 24) { $sets = @('ABCDEFGHJKLMNPQRSTUVWXYZ', 'abcdefghijkmnopqrstuvwxyz', '23456789', '!#%&*+-=?@^_') $all = -join $sets $chars = New-Object System.Collections.Generic.List[char] foreach ($s in $sets) { $chars.Add($s[(Get-RandomIndex $s.Length)]) } while ($chars.Count -lt $Length) { $chars.Add($all[(Get-RandomIndex $all.Length)]) } for ($i = $chars.Count - 1; $i -gt 0; $i--) { $j = Get-RandomIndex ($i + 1) $t = $chars[$i]; $chars[$i] = $chars[$j]; $chars[$j] = $t } return -join $chars } function Test-Dn([string]$Dn) { try { Get-ADObject -Identity $Dn | Out-Null; return $true } catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] { return $false } } function Test-UserExists([string]$Sam, [string]$SearchBase) { if (-not (Test-Dn $SearchBase)) { return $false } return [bool](Get-ADUser -Filter "SamAccountName -eq '$Sam'" -SearchBase $SearchBase) } # --- checks ------------------------------------------------------------------ if (-not (Test-Dn $ParentDn)) { throw "ParentDn does not exist: $ParentDn" } $domain = Get-ADDomain $netbios = $domain.NetBIOSName $testOuDn = "OU=$TestOuName,$ParentDn" $svcOuDn = "OU=$ServiceOuName,$ParentDn" $real = -not $WhatIfPreference $secrets = [ordered]@{} $groups = @{} $users = @() # Everything below is wrapped so that passwords generated so far are always shown, even if a # later step fails. Existing objects are detected with read-only calls, also under -WhatIf. try { # --- OUs --------------------------------------------------------------------- foreach ($ou in @(@($TestOuName, $testOuDn), @($ServiceOuName, $svcOuDn))) { if (Test-Dn $ou[1]) { Write-Host "OU exists: $($ou[1])"; continue } if ($PSCmdlet.ShouldProcess($ou[1], 'Create OU')) { New-ADOrganizationalUnit -Name $ou[0] -Path $ParentDn -ProtectedFromAccidentalDeletion $false } } # --- service account (outside the test OU on purpose) ------------------------ $svcDn = "CN=$ServiceAccountName,$svcOuDn" if (Test-Dn $svcDn) { Write-Host "Service account exists: $svcDn (password unchanged)" } elseif ($PSCmdlet.ShouldProcess($svcDn, 'Create service account')) { $pw = New-RandomPassword 32 New-ADUser -Name $ServiceAccountName -SamAccountName $ServiceAccountName ` -UserPrincipalName "$ServiceAccountName@$($domain.DNSRoot)" -Path $svcOuDn ` -AccountPassword (ConvertTo-SecureString $pw -AsPlainText -Force) -Enabled $true ` -PasswordNeverExpires $true -CannotChangePassword $true ` -Description 'Self-service portal TEST service account' $secrets[$ServiceAccountName] = $pw } # --- groups ------------------------------------------------------------------ foreach ($g in @('Publishers', 'Admins')) { $name = "$GroupPrefix-$g" $groups[$g] = "CN=$name,$testOuDn" if (Test-Dn $groups[$g]) { Write-Host "Group exists: $name"; continue } if ($PSCmdlet.ShouldProcess($groups[$g], 'Create group')) { New-ADGroup -Name $name -SamAccountName $name -GroupScope Global -GroupCategory Security -Path $testOuDn } } # --- test users -------------------------------------------------------------- $users = @( @{ Sam = 'ptest-user1'; Given = 'Test'; Sur = 'UserOne'; Group = $null; Enabled = $true; Expired = $false }, @{ Sam = 'ptest-user2'; Given = 'Test'; Sur = 'UserTwo'; Group = $null; Enabled = $true; Expired = $false }, @{ Sam = 'ptest-publisher'; Given = 'Test'; Sur = 'Publisher'; Group = 'Publishers'; Enabled = $true; Expired = $false }, @{ Sam = 'ptest-admin'; Given = 'Test'; Sur = 'Admin'; Group = 'Admins'; Enabled = $true; Expired = $false }, @{ Sam = 'ptest-disabled'; Given = 'Test'; Sur = 'Disabled'; Group = $null; Enabled = $false; Expired = $false }, @{ Sam = 'ptest-expired'; Given = 'Test'; Sur = 'MustChange'; Group = $null; Enabled = $true; Expired = $true } ) foreach ($u in $users) { $dn = "CN=$($u.Given) $($u.Sur),$testOuDn" if (Test-UserExists $u.Sam $testOuDn) { Write-Host "User exists: $($u.Sam) (password unchanged)"; continue } if ($PSCmdlet.ShouldProcess($dn, "Create test user $($u.Sam)")) { $pw = New-RandomPassword 24 New-ADUser -Name "$($u.Given) $($u.Sur)" -SamAccountName $u.Sam -GivenName $u.Given -Surname $u.Sur ` -DisplayName "$($u.Given) $($u.Sur)" -EmailAddress "$($u.Sam)@$MailDomain" ` -UserPrincipalName "$($u.Sam)@$($domain.DNSRoot)" -Path $testOuDn ` -AccountPassword (ConvertTo-SecureString $pw -AsPlainText -Force) -Enabled $true ` -ChangePasswordAtLogon $u.Expired if (-not $u.Enabled) { Disable-ADAccount -Identity $u.Sam } if ($u.Group) { Add-ADGroupMember -Identity $groups[$u.Group] -Members $u.Sam } $secrets[$u.Sam] = $pw } } # --- delegation on the test OU (user objects only, inherited to children) ------- $grants = @( 'WP;givenName;user', 'WP;sn;user', 'WP;displayName;user', 'WP;mail;user', 'WP;telephoneNumber;user', 'WP;mobile;user', 'CA;Change Password;user' ) if ($AllowResetPassword) { $grants += 'CA;Reset Password;user' } foreach ($g in $grants) { if ($PSCmdlet.ShouldProcess($testOuDn, "Grant $g to $netbios\$ServiceAccountName")) { $out = & dsacls.exe $testOuDn /I:S /G "$netbios\$($ServiceAccountName):$g" 2>&1 if ($LASTEXITCODE -ne 0) { throw "dsacls failed for '$g': $out" } } } } finally { # --- summary (always shown) ---------------------------------------------------- Write-Host '' Write-Host '=== Values to write down (not secrets) ===' Write-Host "Domain DNS name : $($domain.DNSRoot)" Write-Host "Directory server (FQDN): $($domain.PDCEmulator)" Write-Host "Test OU (base DN) : $testOuDn" Write-Host "Service account : $ServiceAccountName@$($domain.DNSRoot) ($svcDn)" Write-Host "Publisher group DN : $($groups['Publishers'])" Write-Host "Admin group DN : $($groups['Admins'])" Write-Host "Test users : $(($users | ForEach-Object { $_.Sam }) -join ', ')" if ($secrets.Count -gt 0) { Write-Host '' Write-Host '=== PASSWORDS (shown once; store them in your password manager now) ===' -ForegroundColor Yellow foreach ($k in $secrets.Keys) { Write-Host ('{0,-18} {1}' -f $k, $secrets[$k]) } } if (-not $real) { Write-Host "`n-WhatIf was used: nothing was changed." } }