I recently had the opportunity to use PowerShell to update AzureAD user attributes. This is different from what I normally do as we still leverage an on-prem AD setup.
I’d never used the command before but I know PowerShell and I’m fairly confident with the AD PowerShell commands.
The mission at hand was this: Update AzureAD user attributes so that the Marketing department had new address information
The first and rather dirty method I put together as a proof-of-concept is below:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
I know right, it’s ugly. It’s lacking any form of error checking, there’s no host output and it’s hard to read.
What I did next was put my code behind a few checks. You can see the improved code below:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This was looking much better, it handles error nicely but there is still room for improvement…
I want to implement splatting and also look into ways to speed the script up!
I wanted to take a look into speed first. I know there are subtle different between using the -filter parameter and piping the results into a Where-Object commandlet. Lets run some tests!
I ran the below commands 5 times to get an average using Measure-Command and outputted in total miliseconds:
Crazy! Switching from the -filter parameter to using the pipeline more than halved the time it took for the command to run!
Next was to build the hashtable for splatting in the Set-AzureADUser parameters before building the final version. This was simple done by using the below code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This now means I can simplify the Set-AzureADUser command.
You can find the full and finished script below:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
New and improved LAPS WinForm because the original one, found here, was kind of crap. It didn’t handle exceptions very well and I don’t think the group policy update worked at all after some further debugging.
I am please to present the new GUI for LAPS:
The best place to download this from would be my TechNet gallery
This is a little WinForm I created that would output the group membership for a domain user or FBA (Forms-Based Authentication) user on SharePoint.
This is what the form looks like, it gives the option for a domain or FBA user and also checked if the user exists before trying to get the relevant information:
The form first checks if CredSSP is configured on your machine to delegate your credentials to the SharePoint server. The form then loads, waits for your input, validates your input and finally collects the group information for your input.
And finally, this is the code for the Winform. I’ve removed some details as they need to be filled in by you. Enjoy!
#CHECKING CREDSSP SETTINGS
if ((Get-Item WSMan:\localhost\Client\Auth\CredSSP).value -eq $false){
#CREDSSP NOT CONFIGURED, EXITING
Write-Host @"
CredSSP is not configured!
Please open an elavated PowerShell prompt and run:
Enable-WSManCredSSP -Role client -DelegateComputer sandsharepointf
"@
Exit
}else{}
#LOADING ASSEMBLIES
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
#ICON FOR THE FORM
[string]$icon64=@"
#base64data
"@
#CONVERTING BASE 64 ICON INTO GRAPHIC
$iconstream = [System.IO.MemoryStream][System.Convert]::FromBase64String($icon64)
$iconbmp = [System.Drawing.Bitmap][System.Drawing.Image]::FromStream($iconstream)
$iconhandle = $iconbmp.GetHicon()
$icon = [System.Drawing.Icon]::FromHandle($iconhandle)
#FORM
$SharePoint_Membership_Form = New-Object system.Windows.Forms.Form
$SharePoint_Membership_Form.ClientSize = '400,278'
$SharePoint_Membership_Form.text = "SharePoint Membership"
$SharePoint_Membership_Form.TopMost = $false
$SharePoint_Membership_Form.StartPosition = "centerscreen"
$SharePoint_Membership_Form.Icon = $icon
$SharePoint_Membership_Form.FormBorderStyle = "Fixed3D"
#USERNAME LABEL
$Username_Label = New-Object system.Windows.Forms.Label
$Username_Label.text = "Enter a username:"
$Username_Label.AutoSize = $true
$Username_Label.width = 25
$Username_Label.height = 10
$Username_Label.location = New-Object System.Drawing.Point(146,12)
#USERNAME TEXTBOX
$Username_Textbox = New-Object system.Windows.Forms.TextBox
$Username_Textbox.multiline = $false
$Username_Textbox.width = 175
$Username_Textbox.height = 20
$Username_Textbox.location = New-Object System.Drawing.Point(115,33)
#VARIABLE FOR KEYDOWN
$Username_Textbox_keydown = {}
#KEYDOWN ASSIGNED
$Username_Textbox_keydown = [System.Windows.Forms.KeyEventHandler]{
if ($_.keycode -eq 'Enter'){
$Search_Button.PerformClick()
}
}
#REGISTER KEYDOWN HANDLER TO USERNAME TEXTBOX
$Username_Textbox.add_keydown($Username_Textbox_keydown)
#DOMAIN RADIO BUTTON
$Domain_User_RB = New-Object system.Windows.Forms.RadioButton
$Domain_User_RB.text = "Domain User"
$Domain_User_RB.AutoSize = $true
$Domain_User_RB.width = 104
$Domain_User_RB.height = 20
$Domain_User_RB.location = New-Object System.Drawing.Point(120,64)
$Domain_User_RB.Checked = $true
#SHAREPOINT FBA USER RADIO BUTTON
$FBA_User_RB = New-Object system.Windows.Forms.RadioButton
$FBA_User_RB.text = "FBA User"
$FBA_User_RB.AutoSize = $true
$FBA_User_RB.width = 104
$FBA_User_RB.height = 20
$FBA_User_RB.location = New-Object System.Drawing.Point(215,64)
#SEARCH BUTTON
$Search_Button = New-Object system.Windows.Forms.Button
$Search_Button.text = "Search"
$Search_Button.width = 60
$Search_Button.height = 30
$Search_Button.location = New-Object System.Drawing.Point(171,89)
#SEPERATOR LINE
$Seperator_Label = New-Object system.Windows.Forms.Label
$Seperator_Label.text = ""
$Seperator_Label.AutoSize = $false
$Seperator_Label.BorderStyle = "Fixed3D"
$Seperator_Label.width = 390
$Seperator_Label.height = 2
$Seperator_Label.location = New-Object System.Drawing.Point(5,124)
#OUTPUT TEXTBOX
$Output_Textbox = New-Object System.Windows.Forms.TextBox
$Output_Textbox.Multiline = $true
$Output_Textbox.Width = 390
$Output_Textbox.Height = 142
$Output_Textbox.Location = New-Object System.Drawing.Point(5,131)
$Output_Textbox.ReadOnly = $true
$Output_Textbox.ScrollBars = "vertical"
#ADDING CONTROLS TO FORM
$SharePoint_Membership_Form.controls.AddRange(@($Domain_User_RB,$FBA_User_RB,$Seperator_Label,$Username_Label,$Username_Textbox,$Search_Button,$Output_Textbox))
$Search_Button.add_click({
$Output_Textbox.Text = ""
#DATE FOR OUTPUT
$date = Get-Date
$username_value = $Username_Textbox.Text
$Username_Prefix = $null
$location = #base location
#SETTING SEARCH VALUES BACK TO FALSE
$Search_On_AD_User = $false
$Search_On_FBA_User = $false
#CHECKING IF USERNAME TEXTBOX IS EMPTY
if ($Username_Textbox.Text.Length -le 0){
#IF EMPTY, VARIABLE IS FALSE
$Output_Textbox.AppendText("$date - $Username cannot be empty! `n")
$Username_Not_Empty = $false
}else{
$Username_Not_Empty = $true
$Output_Textbox.Text = ""
}
#RUNS IF DOMAIN USER RADIO BUTTON IS CHECKED
if ($Domain_User_RB.Checked -and $Username_Not_Empty){
try{
$Output_Textbox.AppendText("$date - Searching for $username_value `n")
Get-ADUser -Identity $username_value
$Output_Textbox.AppendText("$date - Found user! `n")
$Search_On_AD_User = $true
$Search_On_FBA_User = $false
$Username_Found = $true
}catch{
$Output_Textbox.AppendText("$date - Cannot find domain user `n")
$Username_Found = $false
}
}
#RUNS IF FBA USER RADIO BUTTON IS CHECKED
if ($FBA_User_RB.Checked -and $Username_Not_Empty){
$SPAdmin = "sharepoint_admin_user"
$credential = New-Object System.Management.Automation.PSCredential $SPAdmin, (Get-Content "$location\sharepoint_admin_user_encrypted_password.txt" | ConvertTo-SecureString )
$sb = {
$username = $args[0]
Add-PSSnapin microsoft.sharepoint.PowerShell
$user = Get-SPUser -Limit All -Web http://SHAREPOINTSERVER |
Where-Object {$_.loginname -like "i:0#.f|fbamembershipprovider|$username"}
return $user
}
$Output_Textbox.AppendText("$date - Trying to find $username_value... `n")
$invokeoutputfbasearch = Invoke-Command -ScriptBlock $sb -ComputerName SHAREPOINTSERVER -Authentication Credssp -Credential $credential -ArgumentList $username_value
if ($invokeoutputfbasearch){
#FOUND USER
$Username_Found = $true
$Search_On_FBA_User = $true
$Search_On_AD_User = $false
$Output_Textbox.AppendText("$date - Found FBA user!`n")
}else{
#NOT FOUND USER
$Username_Found = $false
$Output_Textbox.AppendText("$date - Cannot find FBA user `n")
}
}
#ONLY RUNS IF BELOW CONDITIONS ARE MET
if ($Username_Found -and $Username_Not_Empty){
#ASSIGNING THE RIGHT USERNAME FORMAT
if ($Search_On_AD_User){
$Username_Prefix = "*|DOMAIN_NAME\"
}else{
$Username_Prefix = "i:0#.f|fbamembershipprovider|"
}
$SPAdmin = "sharepoint_admin_user"
$credential = New-Object System.Management.Automation.PSCredential $SPAdmin, (Get-Content "$location\sharepoint_admin_user_encrypted_password.txt" | ConvertTo-SecureString )
$sb = {
$groups = $null
$prefix = $args[0]
$username = $args[1]
Add-PSSnapin Microsoft.SharePoint.PowerShell
$user = get-SPUser -limit all -web http://SHAREPOINTSERVER |
Where-Object { $_.loginname -like "$prefix$username" }
$SPGroups = get-spsite -limit all |
Select-Object -ExpandProperty rootweb |
Select-Object -ExpandProperty siteusers |
Where-Object { $user.userlogin -eq $_.loginname } |
Select-Object -ExpandProperty groups |
Select-Object -ExpandProperty name
foreach ($i in $SPGroups){
$groups = $groups + " - $i `r`n"
}
return $groups
}
$Output_Textbox.AppendText("$date - Collecting group info on $username_value... `n")
$InvokeOutputfinal = Invoke-Command -ScriptBlock $sb -ComputerName SHAREPOINTSERVER -Authentication Credssp -Credential $credential -ArgumentList $Username_Prefix,$username_value
$Output_Textbox.AppendText("`n")
$Output_Textbox.AppendText("$InvokeOutputfinal")
}else{#THIS SERVES ONLY AS A TRAP TO STOP ANYTHING RUNNING
}
})
#DISPLAYING FORM
[void]$SharePoint_Membership_Form.ShowDialog()
This is a nice little trick I learnt whilst automating domain user creation with PowerShell, I found generating passwords in PowerShell was always ugly. Just see the example below from a previous post I’d made:
This would generate a password like “cDUtxlvM5” which is just about as ugly as the code used to create it.
So I decided to use DinoPass instead since it created better looking passwords without the faff of generating them in PowerShell. This is a the code I used:
Which would give me a much nicer, but still secure, password like “poorJump62”. Then to use it when automating domain user creation, I would use the below and put the whole thing into a variable that I would set the password to:
I have created the *final* iteration of this WPF form which can be found here
*UPDATE*
I didn’t like having to remote desktop into my domain controller and couldn’t figure out if there was a LAPS tool included in RSAT tools so I decided just to make my own and to add some extra features.
I wanted the GUI to look pretty much identity to the actual LAPS GUI. You can see the difference below:
My custom GUI
Default GUI
You might be able to see that I changed the “Set” button to say “Set and Update”. This was because I wanted the form to also attempt to update the group policy settings on the computer so that it would get a new password a lot quicker than the original GUI.
There’s not much else I can say, I will leave the entire script below for you to copy and paste. You will need to add the domain controller for your environment in the $domaincontroller variable at the top of the script. I have converted this to an EXE and run whenever I need it, never skips a beat. Let me know how you get on with it. Enjoy!
Running a little low on content this last few months, plus I’ve been busy with other work stuff.
I had the requirement to create a PowerShell script that would get the uptime of a server and then decide whether or not the server needed rebooting.
I also wanted the script to randomize the reboot of the servers, that way if there are multiple servers that need rebooting at once, they don’t cause a power spike or resource issues on the hosts. I did this by creating a random number between 1 and 5 and then if the number equals 5, the server is rebooted. If not then the server isn’t rebooted.
This is the script that I ended up with and what is currently being tested:
$loglocation = "C:\scripts\reboot\log"
$dateforfile = Get-Date
#GETS UPTIME IN DAYS
$lastbootuptime = Get-WmiObject win32_operatingsystem
$uptime = (Get-Date) - ($lastbootuptime.converttodatetime($lastbootuptime.lastbootuptime))
$uptimeindays = $uptime.days
#GETS RANDOM NUMBER
$randomnumber = Get-Random -Minimum 1 -Maximum 6
if ($uptimeindays -ge "14"){
Add-Content -Path "$loglocation\$env:COMPUTERNAME.txt" -Value @"
=====================================================================================
Server restarted at:
$dateforfile
This was an immediate shutdown as the server had been up for $uptimeindays days
"@
Restart-Computer -Force
}elseif ($uptimeindays -lt "14" -and $uptimeindays -ge "7"){
if ($randomnumber -eq "5"){
Add-Content -Path "$loglocation\$env:COMPUTERNAME.txt" -Value @"
=====================================================================================
Server restarted at :
$dateforfile
This was a random restart as uptime was only $uptimeindays days
"@
Restart-Computer -Force
}else{
Add-Content -Path "$loglocation\$env:COMPUTERNAME.txt" -Value @"
=====================================================================================
Server NOT restarted
$dateforfile
This was not randomly restarted. Uptime is currently $uptimeindays days. Random number was $randomnumber
"@
}
}else{
Add-Content -Path "loglocation\$env:COMPUTERNAME.txt" -Value @"
=====================================================================================
No restart required
$dateforfile
No restart required since uptime is only $uptimeindays days
"@
}
The first time I created this script and set it up as a scheduled task, nothing happened. Turns out that I needed the -Force parameter in order for the server to be rebooted.
This will later be used in a group policy without the log creating as that is only necessary in the testing stage.
This is something that I have recently created so that when a script asks for a credentials and there is an error, it doesn’t display a big, ugly and often intimidating error message for any poor soul trying to run my scripts.
That’s why I have recently (as in yesterday) “created” a “fool proof” way of entering and validating credentials against a domain.
This was a problem because whenever someone ran my script and did ANYTHING other than enter perfectly correct credentials, it would throw and error and exit the script. Or even carry on with the script WITHOUT THE CREDENTIALS, which obviously wouldn’t work. I know, I know. Amateur hour! But it was a crap system I must admit.
That’d why I spent and hour or so creating this beauty! It captures any errors, such as null credentials and incorrect credentials and only continues if a user exists with the same samaccountname as the one entered at the credentials prompt and if the user is in the domain admins group. Just for added “security”. Really I just want the appropriate people to be using the script.
This is the code I use!
#PROMPTING FOR CREDENTIALS
$cred = $host.UI.PromptForCredential("Need credentials", "Please enter your username and password.", "", "")
if ($cred -ne $null -and $cred -ne ''){
#CHECKING IF THE CREDENTIAL USERNAME EXISTS
$check = $(try {Get-ADUser -Identity $cred.UserName} catch {$null})
if ($check -ne $null){
#GETTING CREDENTIAL USERNAME GROUPMEMBERSHIP
$checkadmin = Get-ADPrincipalGroupMembership -Identity $cred.UserName
$checkadminrefined = $checkadmin.SamAccountName
#PUTTING GROUP MEMBERSHIP INTO AN ARRAY
$array = $checkadminrefined
#CHECKING IF USER GROUP LIST CONTAINS DOMAIN ADMINS
if ($array -contains "Domain Admins"){
Write-Host "Credentials are GOOD! - Continuing with script" -ForegroundColor Green
Start-Sleep -Seconds 1
}else{
#RESULT IF USER IS NOT DOMAIN ADMINS
Clear-Host
Write-Host "Credentials are not a domain admin! - Close and start again" -ForegroundColor Red ;
$x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
Exit
}
}else{
#RESULT IF NO USER CAN BE FOUND FROM CREDENTIAL USERNAME
Clear-Host
Write-Host "Check is empty - No user found matching credentials supplied! - Close and start again" -ForegroundColor Red
$x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
Exit
}
}else{
#RESULT IF PROMPT IS CLOSED / NO CREDENTIALS SUPPLIED
Clear-Host
write-host "No credentials supplied - Close and start again" -ForegroundColor Red
$x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
Exit
}
You should be able to read the script and see which each part does. I left comments in the script which I don’t normally do since it might be easier for you to see what its doing with pointers at each stage.
This is just a neat little “tactic” I use when I need to connect to the same machine over and over again but don’t want to drive myself insane with having to constantly enter the same username and password. For example, when testing a script.
First you need to enter your password, in plain text, into this script so that it can get the password. This a perfectly safe as it will only be at this point where the password is in plain text.
This gets the password that you just entered and encrypts it and also puts it into the variable “password”
Now you need to convert the password to an encrypted string of characters using the below command:
$Password2 = $password | ConvertFrom-SecureString | Out-File "PATH TO TEXT FILE TO STORE PASSWORD"
This puts the encrypted password into the text file for later use.
Now, whenever you need to connect to a machine, you can put this into a variable along with the username. Then put them together into a credential and away you go:
$Username = "DOMAIN\username"
$EcryptedPassword = Get-Content "LOCATION TO TEXT FILE" | ConvertTo-SecureString
$Credential = New-Object System.Management.Automation.PSCredential($Username, $EncryptedPassword)
This builds the credential which you can now use with something similar to below:
This sort of thing is useful if you have a bunch of users that have passwords which are set to not expired, but then you decide that they do need to expire. But if you simply untick the “Password doesn’t expired” attribute then it will instantly make them change their password because the “pwdLastSet” date will be from when the user was first set-up.
This trick will set the “pwdLastSet” date to today so that they have some warning before being told to reset their password.
First of all, make sure that you have “Advanced Features” turned on from the “View” menu.
Now find the user that you want to reset the value for and edit their properties. Navigate to the “Attribute Editor” tab and scroll down until you see the “pwdLastSet” attribute.
Edit the value to be “0“, this means that the value has never been set. See screenshot below.
Now click okay on all of the boxes until the users properties window has closed. Now reopen the users window, go back to the attributes editor and change pwdLastSet to “-1. See screenshot below:
Now press okay to all the boxes until the users properties window has closed. Now when you check for the pwdLastSet attribute it will be set to the current date.