Site icon Windows Active Directory

Copy, update Active Directory user attributes with PowerShell

What “replicating user attributes” really means in AD

Active Directory doesn’t have a special “copy attributes” feature for users—the directory stores an object (the user) with a set of schema-defined attributes, and your changes are just LDAP modify operations against those attributes.

PowerShell “replication” in this context usually means one of these operator tasks:

  1. Add / update attribute values for an existing user (single user or bulk)
  2. Copy a chosen set of attributes from a “template” user to another user (or many users)
  3. Copy multi-valued attributes (like proxyAddresses) safely
  4. Copy group membership (which is not the same as copying the user’s memberOf attribute)

This doc is written as a reference you can keep beside your console while you work.

 


 

Prereqs and safety rails (don’t skip in production)

Prereqs

Safety rails

 


 

Fast: view user attributes (and learn the real attribute names you must set)

By default, Get-ADUser returns only a subset of properties. To retrieve more (or all), use -Properties (including *). (Microsoft Learn)

# Commonly used view
Get-ADUser jdoe -Server $Server -Properties Department,Title,Company,Office,proxyAddresses |
  Select-Object SamAccountName,DisplayName,Department,Title,Company,Office,proxyAddresses

# Everything (heavy)
Get-ADUser jdoe -Server $Server -Properties * | Format-List *

Tip: In PowerShell you must use LDAP display names when using generic setters like -Replace / -Add. Microsoft’s Set-ADUser docs describe the -Add hashtable format and also the operation order when combining -Remove, -Add, -Replace, -Clear. (Microsoft Learn)

 


 

Core update patterns (single user)

Pattern A — Set common attributes directly

Set-ADUser exposes many common attributes as parameters (Department, Title, Office, etc.).

Set-ADUser jdoe -Server $Server `
  -Department "IT" `
  -Title "Systems Administrator" `
  -Office "Chennai"

Pattern B — Set any attribute via -Replace (works for custom/extension attributes)

-Replace takes a hashtable of ldapDisplayName = value.

Set-ADUser jdoe -Server $Server -Replace @{
  employeeType       = "Contractor"
  extensionAttribute1 = "CostCenter-431"
}

Pattern C — Clear attributes (wipe all values)

Use -Clear with one or more attribute ldapDisplayNames.

Set-ADUser jdoe -Server $Server -Clear @(
  "extensionAttribute1",
  "telephoneNumber"
)

 


 

Multi-valued attributes (proxyAddresses, otherTelephone, servicePrincipalName, …)

Multi-valued attributes need special handling: you either add/remove specific values or replace the whole set.

Add a value (keep existing values)

Set-ADUser jdoe -Server $Server -Add @{
  proxyAddresses = "smtp:jdoe@contoso.com"
}

This is the same idea shown in Microsoft community guidance for adding proxyAddresses via -Add. (Microsoft Learn)

Remove a specific value

Set-ADUser jdoe -Server $Server -Remove @{
  proxyAddresses = "smtp:oldalias@contoso.com"
}

Replace the entire attribute with a known-good list (array)

Microsoft’s scripting guidance shows using -Replace with an array for multi-valued attributes like proxyAddresses. (Microsoft for Developers)

$addresses = @(
  "SMTP:jdoe@contoso.com",    # primary SMTP in Exchange conventions
  "smtp:j.doe@contoso.com"
)

Set-ADUser jdoe -Server $Server -Replace @{ proxyAddresses = $addresses }

Important nuance: Some AD properties come back as collections (not plain arrays). Casting to an array (e.g., @($value) or [array]$value) avoids “type mismatch” surprises when you feed them back into -Replace. (Microsoft for Developers)

 


 

Copy attributes from one user to another (single + multi-valued + guardrails)

Step 1 — Decide what you will copy (don’t copy “everything”)

Copying all attributes is a common mistake. Many attributes are:

Instead, define an explicit allow-list (you can expand it over time).

Example allow-list (typical HR/profile stamping):

Step 2 — Use a copy function that handles multi-valued values correctly

function Copy-ADUserAttributes {
  [CmdletBinding(SupportsShouldProcess)]
  param(
    [Parameter(Mandatory)] [string] $Source,
    [Parameter(Mandatory)] [string] $Target,
    [Parameter(Mandatory)] [string[]] $Attributes,
    [string] $Server = $env:LOGONSERVER.TrimStart("\"),
    [switch] $ClearWhenSourceEmpty
  )

  # Pull only what we need
  $src = Get-ADUser $Source -Server $Server -Properties $Attributes
  $dst = Get-ADUser $Target -Server $Server -Properties $Attributes

  $replace = @{}
  $clear   = New-Object System.Collections.Generic.List[string]

  foreach ($attr in $Attributes) {
    $val = $src.$attr

    if ($null -eq $val -or ($val -is [string] -and [string]::IsNullOrWhiteSpace($val))) {
      if ($ClearWhenSourceEmpty) { $clear.Add($attr) | Out-Null }
      continue
    }

    # Normalize multi-valued collections to a plain array
    if ($val -is [System.Collections.IEnumerable] -and -not ($val -is [string])) {
      $replace[$attr] = @($val)
    } else {
      $replace[$attr] = $val
    }
  }

  if ($PSCmdlet.ShouldProcess($Target, "Set attributes from $Source")) {
    if ($replace.Count -gt 0 -and $clear.Count -gt 0) {
      # Order matters if you combine ops (Remove > Add > Replace > Clear). :contentReference[oaicite:9]{index=9}
      Set-ADUser $Target -Server $Server -Replace $replace -Clear $clear.ToArray()
    }
    elseif ($replace.Count -gt 0) {
      Set-ADUser $Target -Server $Server -Replace $replace
    }
    elseif ($clear.Count -gt 0) {
      Set-ADUser $Target -Server $Server -Clear $clear.ToArray()
    }
  }
}

Run it:

$attrs = @(
  "department","title","company","physicalDeliveryOfficeName",
  "telephoneNumber","mobile","manager",
  "extensionAttribute1","extensionAttribute2",
  "proxyAddresses"
)

Copy-ADUserAttributes -Source "template.user" -Target "new.user" -Attributes $attrs -Server $Server -WhatIf
# then remove -WhatIf

Why this pattern works:

 


 

Copy group memberships (don’t try to set memberOf)

memberOf is not a normal “editable” attribute. It’s a backlink/constructed view of group membership; you change membership by modifying the group’s member attribute (i.e., add the user to groups). In practice: use group cmdlets.

Example (copy direct memberships except “Domain Users”):

$sourceUser = "template.user"
$targetUser = "new.user"

$groups = Get-ADPrincipalGroupMembership $sourceUser -Server $Server |
  Where-Object { $_.SamAccountName -ne "Domain Users" }

foreach ($g in $groups) {
  Add-ADGroupMember -Identity $g -Members $targetUser -Server $Server -WhatIf
}

 


 

Bulk updates from CSV (update attributes at scale)

Option 1 — CSV columns map to AD attribute ldapDisplayNames

Example CSV (users.csv):

SamAccountName,department,title,extensionAttribute1,proxyAddresses
jdoe,IT,Systems Administrator,CostCenter-431,"SMTP:jdoe@contoso.com;smtp:j.doe@contoso.com"
asmith,Finance,Analyst,CostCenter-210,

Importer (supports single + multi-valued + optional clear when blank):

$Server = "dc01.contoso.com"
$MultiValued = @("proxyAddresses","otherTelephone","servicePrincipalName")

Import-Csv .\users.csv | ForEach-Object {
  $sam = $_.SamAccountName

  $replace = @{}
  $clear   = @()

  foreach ($prop in $_.PSObject.Properties.Name) {
    if ($prop -eq "SamAccountName") { continue }

    $raw = $_.$prop

    if ([string]::IsNullOrWhiteSpace($raw)) {
      # Decide your policy: skip vs clear
      # $clear += $prop
      continue
    }

    if ($MultiValued -contains $prop) {
      $replace[$prop] = $raw.Split(";") | ForEach-Object { $_.Trim() } | Where-Object { $_ }
    } else {
      $replace[$prop] = $raw
    }
  }

  if ($replace.Count -gt 0 -and $clear.Count -gt 0) {
    Set-ADUser $sam -Server $Server -Replace $replace -Clear $clear
  } elseif ($replace.Count -gt 0) {
    Set-ADUser $sam -Server $Server -Replace $replace
  } elseif ($clear.Count -gt 0) {
    Set-ADUser $sam -Server $Server -Clear $clear
  }
}

Remember: if you combine ops, AD cmdlets apply Remove → Add → Replace → Clear.

 


 

Troubleshooting: common Set-ADUser errors you’ll actually see

“Set-ADUser : The attribute cannot be modified because it is owned by the system”

This usually happens for one of these reasons:

  1. You tried to rename the user’s CN / Name using Set-ADUser. Use Rename-ADObject for the name attribute (and related rename semantics). Microsoft explicitly documents that Rename-ADObject sets the LDAP name attribute and that other name parts are handled via Set-ADUser.
    Get-ADUser jdoe -Server $Server | Rename-ADObject -NewName "John Doe"
    
  2. The attribute is schema-marked “systemOnly” (system-owned). Protocol documentation notes that the value of a system-only attribute cannot be modified by normal LDAP modify requests—only by the system.

    Quick check against schema:

    $schemaNC = (Get-ADRootDSE -Server $Server).schemaNamingContext
    Get-ADObject -Server $Server -SearchBase $schemaNC `
      -LDAPFilter "(lDAPDisplayName=instanceType)" -Properties systemOnly |
      Select-Object lDAPDisplayName,systemOnly
    
  3. You tried to modify a backlink / constructed attribute (like memberOf). Copy memberships using group cmdlets instead (see earlier section).

“This cmdlet does not work with a read-only domain controller”

Set-ADUser notes it won’t work with an RODC—ensure you’re targeting a writable DC (or omit -Server so it picks a suitable one).

“I changed it but ADUC doesn’t show it”

Often a visibility/timing issue:

 


 

Practical “do not copy” list (keep yourself out of trouble)

Avoid copying these categories unless you fully understand the consequences:

 


 

If your requirement is “delegate bulk attribute edits to helpdesk with approval workflows,” GUI tools can help. ManageEngine’s AD-focused tooling is one common example, but the PowerShell patterns above remain valuable even when you operationalize via a GUI (because you still need correct attribute semantics and safe allow-lists).


 

Exit mobile version