Declare the service account DN before the OU loop and print the LDAP result code and server message when the password-change check fails. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
173 lines
8.2 KiB
PowerShell
173 lines
8.2 KiB
PowerShell
<#
|
|
.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"
|
|
$svcDn = "CN=$ServiceAccountName,$svcOuDn"
|
|
$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) ------------------------
|
|
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." }
|
|
}
|