I'm reading in a list of users from a Excel spreadsheet one row at a time. Once I get that I'm attempting to get the user object in Active Directory that matches the username I pulled from the Excel file. Unfortunately, it gets the first user but then every user after that it says that it can't find them. Here's what I'm doing:
do
{
# Get the user's login name
$userPrincipalName = $objWorksheet.Cells.Item($intRow, 1).Value()
# Get the user description
$description = $objWorksheet.Cells.Item($intRow, 2).Value()
$intRow++
$user = Get-ADUser -Filter "userPrincipalName -eq '$userPrincipalName'" -Properties Description
if ($user)
{
if (!($user.Description))
{
$user | Set-ADUser -Description $description
Write-Host "User" $userPrincipalName "was altered."
$num_of_users_altered++
}
else
{
Write-Host "User" $userPrincipalName "already has a description."
}
}
else
{
Write-Host "User" $userPrincipalName "was not found."
$num_of_users_not_altered++
}
}
while ($objWorksheet.Cells.Item($intRow, 1).Value() -ne $null)
Now the first user (the one that is found) is in a different OU from the others. I've tried removing that user from the spreadsheet to see if the issue with them being in different OU's but it just didn't find any of them. Any ideas what I might be doing wrong?
Your loop looks like it should work.
Are you sure that you have UPNs (user1#mydomain.com) for all users, and not samAccountName (user1)?
Do you get any results if you manually run Get-ADUser -Filter "userPrincipalName -eq '$userPrincipalName'" -Properties Description (replace $userPrincipalName with one of the values you get in a "not found"-message?
What happends if you use this ($userPrincipalName = $objWorksheet.Cells.Item($intRow, 1).Value().Trim())?
Does it make a difference if convert your file to CSV? Personally, I would always recommend using a CSV-file with PowerShell. CSV-files are alot easier to work with.
sample:
Import-CSV -Path "c:\mycsvfile.csv" | ForEach-Object {
#Modify to match your column names
$upn = $_.userPrincipalName.Trim()
$desc = $_.description.Trim()
$user = Get-ADUser -Filter "userPrincipalName -eq '$upn'" -Properties Description
if ($user)
{
if (!($user.Description))
{
$user | Set-ADUser -Description $desc
Write-Host "User" $upn "was altered."
$num_of_users_altered++
}
else
{
Write-Host "User" $upn "already has a description."
}
}
else
{
Write-Host "User" $upn "was not found."
$num_of_users_not_altered++
}
}
Related
I'm trying to execute a powershell script from within another powershell script. My current script runs fine, but I'd like to try and speed it up if possible.
What my script does is import a list of contacts into Each users Contacts folder through EWS.
In my powershell script the script that handles the importing I call it like this (ImportContacts is a Function without any arguments):
. $rootPath\ImportContacts\ImportContacts.ps1
ImportContacts
When I run it normally, as I mentioned above everything works fine, I just would like to speed it up. I tried following some examples of implementing runspacepool in Powershell to take advantage of using multiple threads, but it doesn't seem to be working properly for me. I'm sure it's a silly syntax error, but I have this currently:
Measure-Command{
$MaxThreads = 5
$RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads)
$RunspacePool.Open()
$Jobs = #()
ImportContacts | Foreach-Object {
$PowerShell = [powershell]::Create()
$PowerShell.RunspacePool = $RunspacePool
$PowerShell.AddScript({ImportContacts})
$Jobs += $PowerShell.BeginInvoke()
}
while ($Jobs.IsCompleted -contains $false)
{
Start-Sleep 1
}}
This seems to do the job, but I can't tell a difference with the speed.
EDIT 5-15-21
To better assist with the question, here is how I retrieve the user data. I have a file I call called "ExportedContacts.ps1"
It's called like so:
$Users = & $rootPath\ExportContacts\ExportContacts.ps1
The contents of the file is this
$Users = Get-ADUser -Filter * -Properties extensionAttribute2, middlename, mobile, OfficePhone, GivenName, Surname, DisplayName, EmailAddress, Title, Company, Department, thumbnailPhoto | Where-Object {($_.extensionAttribute2 -like "DynamicDistro") -AND (($_.Mobile -ne $NULL) -OR ($_.OfficePhone -ne $NULL))} | Select-Object #{Name="First Name";Expression={$_.GivenName}},#{Name="Last Name";Expression={$_.Surname}},#{Name="Display Name";Expression={$_.DisplayName}},#{Name="Job Title";Expression={$_.Title}},#{Name="Company";Expression={$_.Company}},#{Name="Department";Expression={$_.Department}},#{Name="Mobile Phone";Expression={$_.Mobile}},#{Name="Home Phone";Expression={$_.OfficePhone}}, #{Name="Middle Name";Expression={$_.MiddleName}}, #{Name="E-mail Address";Expression={$_.EmailAddress}}, thumbnailPhoto | Sort-Object "Last Name"
return $Users
Then I import those contacts, similarly to how I mentioned above. The content of the import is as follows:
Function ImportContacts
{
Write-Host "Importing Contacts. This can take several minutes."
#******************************************************************
foreach ($ContactItem in $Users)
{
$service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress);
$ExchangeContact = New-Object Microsoft.Exchange.WebServices.Data.Contact($service);
$ExchangeContact.NickName = ('{0} {1}' -f $ContactItem."First Name", $ContactItem."Last Name"). Trim()
$ExchangeContact.DisplayName = $ExchangeContact.NickName;
$ExchangeContact.FileAs = $ExchangeContact.NickName;
$ExchangeContact.EmailAddresses[[Microsoft.Exchange.WebServices.Data.EmailAddressKey]::EmailAddress1] = $ContactItem."E-mail Address";
$ExchangeContact.PhoneNumbers[[Microsoft.Exchange.WebServices.Data.PhoneNumberKey]::HomePhone] = $ContactItem."Home Phone";
$ExchangeContact.PhoneNumbers[[Microsoft.Exchange.WebServices.Data.PhoneNumberKey]::MobilePhone] = $ContactItem."Mobile Phone";
$ExchangeContact.Department = $ContactItem."Department";
$ExchangeContact.CompanyName = $ContactItem."Company";
$ExchangeContact.JobTitle = $ContactItem."Job Title";
$ExchangeContact.MiddleName = $ContactItem."Middle Name";
# Save the contact
$ExchangeContact.Save($ContactsFolder.Id);
}
}
I am also including the File that creates the contacts folder I specify and also deletes existing contacts (if the folder already exists), so that each import is a clean and updated import. I'd be curious if there is a faster way to clean the existing items?
Function CreateContactsFolder
{
Write-Host "Creating Contacts Folder and Cleaning up stale items. This can take a couple minutes."
Try
{
$service.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $EmailAddress);
$RootFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,[Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot)
$RootFolder.Load()
#Check to see if they have a contacts folder that we want
$FolderView = new-Object Microsoft.Exchange.WebServices.Data.FolderView(1000)
$ContactsFolderSearch = $RootFolder.FindFolders($FolderView) | Where-Object {$_.DisplayName -eq $FolderName}
if($ContactsFolderSearch)
{
$ContactsFolder = [Microsoft.Exchange.WebServices.Data.ContactsFolder]::Bind($service,$ContactsFolderSearch.Id);
#If folder exists, connect to it. Clear existing Contacts, and reupload new (UPDATED) Contact Info
Write-Host "Folder alreads exists. We will remove all contacts under this folder."
# Attempt to empty the target folder up to 10 times.
$tries = 0
$max_tries = 0
while ($tries -lt 2)
{
try
{
$tries++
$ErrorActionPreference='Stop'
$ContactsFolder.Empty([Microsoft.Exchange.WebServices.Data.DeleteMode]::HardDelete, $true)
$tries++
}
catch
{
$ErrorActionPreference='SilentlyContinue'
$rnd = Get-Random -Minimum 1 -Maximum 10
Start-Sleep -Seconds $rnd
$tries = $tries - 1
$max_tries++
if ($max_tries -gt 100)
{
Write-Host "Error; Cannot empty the target folder; `t$EmailAddress"
}
}
}
}
else
{
#Contact Folder doesn't exist. Let's create it
try
{
Write-Host "Creating new Contacts Folder called $FolderName"
$ContactsFolder = New-Object Microsoft.Exchange.WebServices.Data.ContactsFolder($service);
$ContactsFolder.DisplayName = $FolderName
$ContactsFolder.Save([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot)
}
catch
{
Write-Host "Error; Cannot create the target folder; `t$EmailAddress"
}
}
return $ContactsFolder
}
Catch
{
Write-Host "Couldn't connect to the user's mailbox. Make sure the admin account you're using to connect to has App Impersonization permissions"
Write-Host "Check this link for more info: https://help.bittitan.com/hc/en-us/articles/115008098447-The-account-does-not-have-permission-to-impersonate-the-requested-user"
}
}
Hi I'm trying to export a list of AD users based on "Last Logon"
I've scripted using base powershell however I'd be interested if anyone can find a solution using "AzureAD to Powershell" commands.
I've gotten as far as getting the list however I cannot export it to any file type because of how it generates through the loop.
End result I'm looking for is to be able to organize the data to see which users have been inactive?
Import-Module ActiveDirectory
function Get-ADUserLastLogon([string]$userName) {
$dcs = Get-ADDomainController -Filter {Name -like "*"}
$time = 0
foreach($dc in $dcs) {
$hostname = $dc.HostName
$user = Get-ADUser $userName | Get-ADObject -Properties lastLogon
if($user.LastLogon -gt $time) {
$time = $user.LastLogon
}
}
$dt = [DateTime]::FromFileTime($time)
Write-Host $username "last logged on at:" $dt
}
$unames = Get-ADUser -Filter 'ObjectClass -eq "User"' | Select -Expand SamAccountName
foreach ($uname in $unames) { Get-ADUserLastLogon($uname); }
In Azure AD, we can get all user Sign-ins records on Azure Portal or using Azure AD PowerShell.
If you are looking for a way by PowerShell to export Azure AD users last login list with user account status (enabled or not), just try the code below:
Connect-AzureAD
$AllUsers = Get-AzureADUser -All $true
$AllSiginLogs = Get-AzureADAuditSignInLogs -All $true
$results = #()
foreach($user in $AllUsers){
$LoginRecord = $AllSiginLogs | Where-Object{ $_.UserId -eq $user.ObjectId } | Sort-Object CreatedDateTime -Descending
if($LoginRecord.Count -gt 0){
$lastLogin = $LoginRecord[0].CreatedDateTime
}else{
$lastLogin = 'no login record'
}
$item = #{
userUPN=$user.UserPrincipalName
userDisplayName = $user.DisplayName
lastLogin = $lastLogin
accountEnabled = $user.AccountEnabled
}
$results += New-Object PSObject -Property $item
}
$results | export-csv -Path d:\result.csv -NoTypeInformation
export to .csv file Result:
There is one thing that you should know, for different Azure AD service tier, the time that Azure AD keep these data is different, details see here.
I am getting this error from the following code. It's coming from $Context.Load($RecycleBinItems). Any idea what's wrong with the code? I am attempting to restore all recyclebin items.
Add-Type -Path "C:\Program Files\WindowsPowerShell\Modules\SharePointPnPPowerShellOnline\3.17.2001.2\Microsoft.SharePoint.Client.dll"
Add-Type -Path "C:\Program Files\WindowsPowerShell\Modules\SharePointPnPPowerShellOnline\3.17.2001.2\Microsoft.SharePoint.Client.Runtime.dll"
Import-Module 'Microsoft.PowerShell.Security'
#Get the Site Owners Credentials to connect the SharePoint
$SiteUrl = "https://phaselinknet.sharepoint.com"
$UserName = Read-host "Enter the Email ID"
$Password = Read-host - assecurestring "Enter Password for $AdminUserName"
$Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($UserName, $Password)
# Once Connected, get the Site information using current Context objects
Try {
$Context = New-Object Microsoft.SharePoint.Client.ClientContext($SiteUrl)
$Context.Credentials = $Credentials
$Site = $Context.Site
$RecycleBinItems = $Site.RecycleBin
$Context.Load($Site)
$Context.Load($RecycleBinItems)
$Context.ExecuteQuery()
Write-Host "Total Number of Files found in Recycle Bin:" $RecycleBinItems.Count
}
catch {
write - host "Error: $($_.Exception.Message)" - foregroundcolor Red
}
# using for loop to restore the item one by one
Try {
if($RecycleBinItems)
{
foreach($Item in $RecycleBinItems)
{
$Site.RecycleBin.restore($Item.ID)
#Write-Host "Item restored:"$Item.Title
}
}
}
catch {
write-host "Error: $($_.Exception.Message)" -foregroundcolor Red
}
The error message is giving you you answer. There is not a version of the method Restore that takes 1 parameter.
You need to load up a list of items simular to this
$Item = $RecycleBin | Where{$_.Title -eq $ItemName}
Then call restore for the items.
if($Item -ne $null)
{
$Item.Restore()
}
Thanks for the tip. So I load up the first 10 items in the recyclebin, and Write-Host does write out the correct files, but the $Item.Restore() does noting as the files are still not restored:
$itemsToRestore = #()
for ($i = 0; $i -lt 10; $i++)
{
$Item = $RecycleBinItems[$i]
$itemsToRestore += $Item
}
Write-Host "Total Number of Files to Restore:" $itemsToRestore.Count
foreach($item in $itemsToRestore)
{
Write-Host "Item:" $Item.Title
$item.Restore()
}
I found the problem. I missed $Context.ExecuteQuery() after $Item.Restore(). It works now.
I am attempting to change all the files in a library from one content type to another. This is in SharePoint Online so I'm using the CSOM. I am new to this so I'm stuck at where to go from what I have below.
I think my biggest issue is getting the values of the content types. That and I'm so used to SP On Premise I'm having trouble grasping this CSOM stuff. Much thanks to anyone that can help point me in the right direction!
#Load necessary module to connect to SPOService
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.Runtime") | Out-Null
#Login Information for script
$User = "user"
$Pass = "password"
$WebUrl = "SiteURL"
#Connect to SharePoint Online service
Write-Host "Logging into SharePoint online service." -ForegroundColor Green
$Context = New-Object Microsoft.SharePoint.Client.ClientContext($WebUrl)
$Context.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($User, (ConvertTo-SecureString $Pass -AsPlainText -Force))
#Get the Necessary List
Write-Host "Getting the required list." -ForegroundColor Green
$List = $Context.Web.Lists.GetByTitle("TestLibrary")
Write-Host "Getting the Content Types." -ForegroundColor Green
$oldCT = $list.ContentTypes("OldCTName")
$newCT = $list.ContentTypes("NewCTName")
$newCTID = $newCT.ID
$Query = [Microsoft.SharePoint.Client.CamlQuery]::CreateAllItemsQuery(1000);
$Items = $List.GetItems($Query);
$Context.Load($Items);
$Context.ExecuteQuery();
#Check if the values specified for the content types actually exist on the list
if (($oldCT -ne $null) -and ($newCT -ne $null))
{
#Go through each item in the list
#Edit existing list items
foreach($item in $Items)
{
#Check if the item content type currently equals the old content type specified
if ($_.ContentType.Name -eq $oldCT.Name)
{
#Check the check out status of the file
if ($_.File.CheckOutType -eq "None")
{
#Change the content type association for the item
$_.File.CheckOut()
write-host "Resetting content type for file" $_.Name "from" $oldCT.Name "to" $newCT.Name
$_["ContentTypeId"] = $newCTID
$_.Update()
$_.File.CheckIn("Content type changed to " + $newCT.Name, 1)
}
else
{
write-host "File" $_.Name "is checked out to" $_.File.CheckedOutByUser.ToString() "and cannot be modified"
}
}
else
{
write-host "File" $_.Name "is associated with the content type" $_.ContentType.Name "and shall not be modified"
}
}
}
else
{
write-host "One of the content types specified has not been attached to the list"$list.Title
}
$Context.ExecuteQuery();
}
I have an Application pool that has a lot of applications been assigned to it, it won't let me rename.
Beside delete and creating a new application pool, is there anyway to get a new name for my application pool? I don't want to go and reassign every application in it.
Assign applications to another pool, rename the one you wanted renamed. Re-assign applications back to your pool.
IIS doesn't support other options
This was the simplest way that I could work it out, although I can't believe this isn't easier.
Import-Module WebAdministration
$oldName = "OldAppPool";
$newName = "NewAppPool";
if(-not (Test-Path IIS:\AppPools\TempPool)){
New-WebAppPool TempPool
}
$tempAppPool = Get-Item IIS:\AppPools\TempPool
foreach($site in Get-ChildItem IIS:\Sites){
$apps = $site | Get-ChildItem | Where-Object { $_.ApplicationPool -eq $oldName }
foreach($app in $apps){
$path = ("IIS:\Sites\{0}\{1}" -f $site.name, $app.name)
$path
Set-ItemProperty $path applicationPool TempPool
}
}
Set-ItemProperty "IIS:\AppPools\$oldName" -Name name -Value $newName
foreach($site in Get-ChildItem IIS:\Sites){
$apps = $site | Get-ChildItem | Where-Object { $_.ApplicationPool -eq "TempPool" }
foreach($app in $apps){
$path = ("IIS:\Sites\{0}\{1}" -f $site.name, $app.name)
$path
Set-ItemProperty $path applicationPool $newName
}
}
Remove-WebAppPool TempPool
No, there isn't.
Either put up with the name, or create a new App Pool and assign the applications one-by-one.
If you need to repeat it on multiple servers, you can even automate it with ADSI and JavaScript or VBScript:
http://msdn.microsoft.com/en-us/library/ms525389(v=vs.90).aspx
I've created similar script to automate this job.
It is a bit different from the other answer here:
It works for WebSites in addition to WebApplications;
It works for all pools: with and without assigned applications;
Powershell script:
Import-Module WebAdministration
Function Rename-AppPool([String]$oldName="", [String]$newName="") {
if ($oldName -eq "") {
Write-Warning "Parameter 'oldName' was not provided."
return
}
if ($newName -eq "") {
Write-Warning "Parameter 'newName' was not provided."
return
}
if(-not (Test-Path "IIS:\AppPools\$oldName")){
Write-Warning "There is no pool with name '$oldName' to rename. Operation stopped."
return
}
if(Test-Path "IIS:\AppPools\$newName"){
Write-Warning "Pool with name '$newName' already exists. Operation stopped."
return
}
Write-Output "Renaming app pool '$oldName' to '$newName'"
$pathsOfPools = New-Object System.Collections.ArrayList
$listOfSites = Get-ChildItem "IIS:\Sites"
foreach ($site in $listOfSites) {
if ($site.applicationPool -eq $oldName) {
$path = ("IIS:\Sites\{0}" -f $site.name)
$pathsOfPools.Add($path) | Out-Null
}
$apps = $site | Get-ChildItem
foreach ($app in $apps) {
if ($app.applicationPool -eq $oldName) {
$path = ("IIS:\Sites\{0}\{1}" -f $site.name, $app.name)
$pathsOfPools.Add($path) | Out-Null
}
}
}
$tempGuid = [Guid]::NewGuid()
$tempName = $tempGuid.Guid
if ($pathsOfPools.Count -gt 0) {
$pathsOfPools
New-WebAppPool $tempName | Out-Null
Write-Output "Temp app pool '$tempName' has been created"
Write-Output "Changing apps to Temp pool"
foreach ($path in $pathsOfPools) {
Set-ItemProperty $path applicationPool $tempName
}
}
Set-ItemProperty "IIS:\AppPools\$oldName" -Name name -Value $newName
Write-Output "Application pool name has been changed"
if ($pathsOfPools.Count -gt 0) {
Write-Output "Changing apps to New pool"
foreach ($path in $pathsOfPools) {
Set-ItemProperty $path applicationPool $newName
}
Remove-WebAppPool $tempName
Write-Output "Temp pool has been removed"
}
}
Rename-AppPool "OldName" "NewBetterName"
Yes, there is an option. Create a dummy app pool or make use of DefaultApppool. Associate the existing site to the defaultapppool . Now go to the original app pool, Stop the app pool and rename.
Associate back the url to the renamed appool.