Most of the Active Directory object properties can be easily accessed by using the Get-
or Set-
such as Get-ADUser or Set-ADUser, but it is not that easy regarding LogonHours.
Multiple steps and challenges required to build the value, and in this tutorial, you will learn all the tips and tricks needed to set the LogonHours hours via Windows PowerShell.
You can download the script from my GitHub Repo over here
Table of Contents
- Understanding Logon Hours (LogonHours) Basics.
- Understanding LogonHours Property
- Parameters for the PowerShell Script.
- Example How to Use Set-LogonHours
- LogonHours PowerShell Script Explination.
- Timezone Challenge and Bits Shifting.
- Building the Final Byte array and setting the values
- All the PowerShell Script.
- What’s Next
Understanding Logon Hours (LogonHours) Basics.
First, Let’s try to understand how the logon hours work and how it works by using the GUI of the Logon Hours window.
Open any AD user account property —> Account Tab —> click on Logon Hours.
The Blue block means a permitted logon.
If there is any white block, it means a logon denied on that selected time/day slot.
In the example below, the user cannot log in on Sunday or Saturday but can log in from Monday through Friday.
Let’s go deeper and see how these values represented in the Attribute Editor.
Ensuring that the Advanced Features in Active Directory Management Console is checked to see the Attribute Editor.
Start by opening any Active Directory user property —> Attribute Editor tab.
From the Attributes Editor, scroll to LogonHours, double click on the LogonHours. A new window popped up that looks similar to the one below. Change the Value Format to Binary.
Here is a basic explanation of what these values mean, don’t worry if you don’t understand it now; an in-depth explanation is on the way.
Additional information about LogonHours attribute can be found on the Microsoft.com site
Understanding LogonHours Property
In the LogonHours attribute (Binary view), the information represents each hour of the week. The value of each 1 means allowed to log in and the value of 0 means not allowed to log in.
The list below explains more about the Bits, Bytes, and each group represents.
- One Bit represents one hour.
- Each Byte (or a group of 8 bits) represents eight hours.
- Each three Byte represents One Day (24 Hours).
- 21 Byte represents one week.
- 168 Bit represents one week in hours, which also means 168 hours per week.
All the results in the property are based on UTC.
Changing any value from 1 to 0 or 0 to 1 reflected in the Logon Hours GUI.
Let’s give it a try. In the first block, change the first bit and make it 0, so the first byte looks like 01111111.
Click on OK to save and close the LogonHours attribute, and then click on OK to close the user property window. Refresh the view by clicking on the Refresh icon in the Active Directory Users and Computers.
Reopen the same user, navigate to the Account tab and open the Logon Hours.
You find a single white block, which means logon denied on that time slot.
Result After Changing one bit
The result on your side may look different as these values are affected by the timezone, which is covered in a later section.
The LogonHours property that you saw in the Attribute Editor uses UTC, and Logon Hours GUI uses the local timezone. The GUI manages the time shifting based on the timezone to provide you with an accurate view.
Parameters for the PowerShell Script.
To make things easy to use for the user, I wrote a function Set-LogonHours to set the AD user logon hours and respect the timezone bias.
This function accepts the following parameters.
- Identity: The Username of the target principal (String, Position=0).
- TimeIn24Format: Array of the hours access with a permitted logon (Array, ValidateRange (0- 23).
- Sunday: Apply the TimeIn24Format parameter this day (Switch).
- Monday: Apply the TimeIn24Format parameter to this day (Switch).
- Tuesday: Apply the TimeIn24Format parameter to this day (Switch).
- Wednesday: Apply the TimeIn24Format parameter to this day (Switch).
- Thursday: Apply the TimeIn24Format parameter to this day (Switch).
- Friday: Apply the TimeIn24Format parameterto this day (Switch).
- Saturday: Apply the TimeIn24Format parameter to this day (Switch).
- NonSelectedDaysAre: This option for the non-selected weeks. What the default value should be? is it a permit or denied (ValidateSet =”WorkingDays” or “NonWorkingDays”).
Example How to Use Set-LogonHours
The following are some quick examples of how to execute the script function.
The example below sets the LogonHours for MyTestUser to allow login at 8,9,10,11,12 o’clock on the following days Monday, Tuesday, Wednesday, and Thursday. All the non-selected days are considered as permitted days to logon.
Set-LogonHours -identity "MyTestUser" -TimeIn24Format @(8,9,10,11,12) -Monday -Tuesday -Wednesday -Thursday -NonSelectedDaysare WorkingDays
To set the LogonHours for users in a single OU, use the following example.
The Set-LogonHours function accepts values from the pipeline, so you can pipeline the result from the Get-ADUser cmdlet and pass the result to the Set-LogonHours function.
Get-ADUser -SearchBase 'OU=Test,OU=DevUsers,DC=Dev-Test,DC=local' -Filter *| Set-LogonHours -TimeIn24Format @(8,9,10,11,12,13,14,15,16) -Monday -Tuesday -Wednesday -Thursday -NonSelectedDaysare WorkingDays
LogonHours PowerShell Script Explination.
First, create a Byte array to store the final result.
The array length is 21, which represents a week.
One thing to note is that the LogonHours AD attribute accepts a Byte array, not an integer array.
$FullByte=New-Object "byte[]" 21
Then create a Hashtable that represents each hour of the day.
The value of the hashtable items is set to One based on the user input
The hashtable values are based on the user input, so if the user sets the value of TimeIn24Format to @(8,9,10,11,12,13,14,15), the respected value in the Hashtable should be set to 1.
$FullDay=[ordered]@{}
0..23 | foreach{$FullDay.Add($_,"0")}
$TimeIn24Format.ForEach({$FullDay[$_]=1})
Then join all the hashtable values together and store the result in a variable named $Working
.
$Working= -join ($FullDay.values)
The user selects the working days via switch parameters, but what about the non-selected days?
What is the default value for the non-selected days? Permit login or denied login?
Whether the user considered all the non-selected days a permitted login or denied login, the first Switch set the default value for the weekdays.
The Second Switch statement will fill the user’s added weekdays parameter value with the allowed logon hours.
Switch ($PSBoundParameters["NonSelectedDaysare"])
{
'NonWorkingDays' {$SundayValue=$MondayValue=$TuesdayValue=$WednesdayValue=$ThursdayValue=$FridayValue=$SaturdayValue="000000000000000000000000"}
'WorkingDays' {$SundayValue=$MondayValue=$TuesdayValue=$WednesdayValue=$ThursdayValue=$FridayValue=$SaturdayValue="111111111111111111111111"} }
Switch ($PSBoundParameters.Keys)
{
'Sunday' {$SundayValue=$Working}
'Monday' {$MondayValue=$Working}
'Tuesday' {$TuesdayValue=$Working}
'Wednesday' {$WednesdayValue=$Working}
'Thursday' {$ThursdayValue=$Working}
'Friday' {$FridayValue=$Working}
'Saturday' {$SaturdayValue=$Working}
}
The next line is to build up the full week string by combining all the values in one row.
This includes the hashtable values and the NonSelectedDaysare, which is required for fixing the time zone offset.
This makes the script can give the same result regardless of the time zone.
$AllTheWeek="{0}{1}{2}{3}{4}{5}{6}" -f $SundayValue,$MondayValue,$TuesdayValue,$WednesdayValue,$ThursdayValue,$FridayValue,$SaturdayValue
Timezone Challenge and Bits Shifting.
The LogonHours value is based on UTC format, and this led to the following possible scenarios:
- Scenarios 1: The user is located in a minus time zone range like the US and Canada with -8 Hours UTC.
- Scenarios 2: The user is located in the zero time zone range, such as Dublin, London +00 UTC.
- Scenarios 3: The User is located in a plus time zone such as Bangkok, Hanoi, Jakarta +7 UTC.
To see and understand the challenge, let’s see it first in action to know how to build the solution.
Set the time zone to any time zone with 0 UTC, such as London, Dublin. 0 UTC.
Then change the Logon Hours for a test user to only allow a login for one hour as follows.
This helps see how this bit is moving after changing the time zone.
Change the Timezone to -2 or -3, such as (UTC – 03:00) Salvador. Open the same user Logon Hours GUI interface again, and see where the permitted login slot has now shifted.
The bit shifted 3 bits to the left.
The same thing happens if the selected timezone is (UTC +). The permit login slot is shifted to the right.
Timezone bias change should be reflected in the order of the bits, so the result is always correct.
In PowerShell, using the Get-Timezone
cmdlet will read the current time zone information.
The Get-Timezone
contains a property that shows the Timezone bias (Get-TimeZone).baseutcoffset.hours
.
It’s possible to change the order of the bits based on the Timezone bais.
Timezone in UTC -
If ((Get-TimeZone).baseutcoffset.hours -lt 0){
$TimeZoneOffset = $AllTheWeek.Substring(0,168+ ((Get-TimeZone).baseutcoffset.hours))
$TimeZoneOffset1 = $AllTheWeek.SubString(168 + ((Get-TimeZone).baseutcoffset.hours))
$FixedTimeZoneOffSet="$TimeZoneOffset1$TimeZoneOffset"
}
Timezone is UTC +
If ((Get-TimeZone).baseutcoffset.hours -gt 0){
$TimeZoneOffset = $AllTheWeek.Substring(0,((Get-TimeZone).baseutcoffset.hours))
$TimeZoneOffset1 = $AllTheWeek.SubString(((Get-TimeZone).baseutcoffset.hours))
$FixedTimeZoneOffSet="$TimeZoneOffset1$TimeZoneOffset"
}
Timezone is UTC 0
if ((Get-TimeZone).baseutcoffset.hours -eq 0){
$FixedTimeZoneOffSet=$AllTheWeek
}
Building the Final Byte array and setting the values
Now the hours are aligned with the time zone bias, and it’s time to build the array and convert the string to a byte array.
The result is stored in a variable named $FixedTimeZoneOffset
, and it should be split into a group of 8 characters for converting it to Byte later.
Another challenge is each 8 bits group needs to have a reverse order, so the 1’s and 0’s order reflects the correct binary number bits order, then update the result in the $FullByte
variable, which will be used to update the AD user information by using Set-ADUser cmdlet.
$BinaryResult=$FixedTimeZoneOffSet -split '(\d{8})' | where {$_ -match '(\d{8})'}
Foreach($singleByte in $BinaryResult){
$Tempvar=$singleByte.tochararray()
[array]::Reverse($Tempvar)
$Tempvar= -join $Tempvar
$Byte = [Convert]::ToByte($Tempvar, 2)
$FullByte[$i]=$Byte
$i++
}
Set-ADUser -Identity $Identity -Replace @{logonhours = $FullByte}
All the PowerShell Script.
Function Set-LogonHours{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[ValidateRange(0,23)]
$TimeIn24Format,
[Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True,
Position=0)]$Identity,
[parameter(mandatory=$False)]
[ValidateSet("WorkingDays", "NonWorkingDays")]$NonSelectedDaysare="NonWorkingDays",
[parameter(mandatory=$false)][switch]$Sunday,
[parameter(mandatory=$false)][switch]$Monday,
[parameter(mandatory=$false)][switch]$Tuesday,
[parameter(mandatory=$false)][switch]$Wednesday,
[parameter(mandatory=$false)][switch]$Thursday,
[parameter(mandatory=$false)][switch]$Friday,
[parameter(mandatory=$false)][switch]$Saturday
)
Process{
$FullByte=New-Object "byte[]" 21
$FullDay=[ordered]@{}
0..23 | foreach{$FullDay.Add($_,"0")}
$TimeIn24Format.ForEach({$FullDay[$_]=1})
$Working= -join ($FullDay.values)
Switch ($PSBoundParameters["NonSelectedDaysare"])
{
'NonWorkingDays' {$SundayValue=$MondayValue=$TuesdayValue=$WednesdayValue=$ThursdayValue=$FridayValue=$SaturdayValue="000000000000000000000000"}
'WorkingDays' {$SundayValue=$MondayValue=$TuesdayValue=$WednesdayValue=$ThursdayValue=$FridayValue=$SaturdayValue="111111111111111111111111"}
}
Switch ($PSBoundParameters.Keys)
{
'Sunday' {$SundayValue=$Working}
'Monday' {$MondayValue=$Working}
'Tuesday' {$TuesdayValue=$Working}
'Wednesday' {$WednesdayValue=$Working}
'Thursday' {$ThursdayValue=$Working}
'Friday' {$FridayValue=$Working}
'Saturday' {$SaturdayValue=$Working}
}
$AllTheWeek="{0}{1}{2}{3}{4}{5}{6}" -f $SundayValue,$MondayValue,$TuesdayValue,$WednesdayValue,$ThursdayValue,$FridayValue,$SaturdayValue
# Timezone Check
if ((Get-TimeZone).baseutcoffset.hours -lt 0){
$TimeZoneOffset = $AllTheWeek.Substring(0,168+ ((Get-TimeZone).baseutcoffset.hours))
$TimeZoneOffset1 = $AllTheWeek.SubString(168 + ((Get-TimeZone).baseutcoffset.hours))
$FixedTimeZoneOffSet="$TimeZoneOffset1$TimeZoneOffset"
}
if ((Get-TimeZone).baseutcoffset.hours -gt 0){
$TimeZoneOffset = $AllTheWeek.Substring(0,((Get-TimeZone).baseutcoffset.hours))
$TimeZoneOffset1 = $AllTheWeek.SubString(((Get-TimeZone).baseutcoffset.hours))
$FixedTimeZoneOffSet="$TimeZoneOffset1$TimeZoneOffset"
}
if ((Get-TimeZone).baseutcoffset.hours -eq 0){
$FixedTimeZoneOffSet=$AllTheWeek
}
$i=0
$BinaryResult=$FixedTimeZoneOffSet -split '(\d{8})' | Where {$_ -match '(\d{8})'}
Foreach($singleByte in $BinaryResult){
$Tempvar=$singleByte.tochararray()
[array]::Reverse($Tempvar)
$Tempvar= -join $Tempvar
$Byte = [Convert]::ToByte($Tempvar, 2)
$FullByte[$i]=$Byte
$i++
}
Set-ADUser -Identity $Identity -Replace @{logonhours = $FullByte}
}
end{
Write-Output "All Done :)"
}
}
# Change the LogonHours for all the users in the Test OI
Get-ADUser -SearchBase "OU=Test,DC=test,DC=local" -Filter *| Set-LogonHours -TimeIn24Format @(8,9,10,11,12,13,14,15,16) -Monday -Tuesday -Wednesday -Thursday -NonSelectedDaysare WorkingDays
# Change the LogonHours for a single user
Set-LogonHours -Identity Jack.Ripper -TimeIn24Format @(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,0) -Monday -Tuesday -Wednesday -Thursday -Friday -NonSelectedDaysare NonWorkingDays # Allow Access during weekday
Thank you for the detailed explanation of the rather mind-warping way those bits are stored. However, your full script is missing the “[array]::Reverse($Tempvar)” line, so I was thoroughly lost for a bit. 🙂
Nice catch, thanks for the comment.
I updated it and tried to rewrite most of the explanation to make it easier to understand.
@Faris
How can i use your script with a Servicenow request?
I will add an extra section to give more examples on how to use the script
for example
Set-LogonHours -identity "MyTestUser" -TimeIn24Format @(8,9,10,11,12) -Monday -Tuesday -Wednesday -Thursday -NonSelectedDaysare WorkingDays
I did not use ServiceNow, but I guess you will pass some parameters from ServiceNow to the PowerShell Script. ServiceNow should convert the value you are passing and supply it to the PowerShell Script.
Make sure that the parameters are with the supported Type for example the TimeIn24Format is an array, not a string. Here are a list of parameters and their type:
Identity: The Username of the target principal (String, Position=0).
TimeIn24Format: Array of the hours access is permitted (Array, ValidateRange (0- 23).
Sunday: Apply the TimeIn24Format to this day (Switch).
Monday: Apply the TimeIn24Format to this day (Switch).
Tuesday: Apply the TimeIn24Format to this day (Switch).
Wednesday: Apply the TimeIn24Format to this day (Switch).
Thursday: Apply the TimeIn24Format to this day (Switch).
Friday: Apply the TimeIn24Format to this day (Switch).
Saturday: Apply the TimeIn24Format to this day (Switch).
NonSelectedDaysAre: This option for the weekdays that are not selected, what the default value should be?, is it a permit or denied (ValidateSet =”WorkingDays” or “NonWorkingDays”)
Let me know if I missed any points to make it more clear.
Note: You may get an email with different content, I thought the comment for another post 🙂
Another note: English is my secondary lang, sooooooooo good luck 🙂
Great script,
just having issue to powershell to exclude Saturday, Sunday or set individual day and time for set users.
Sorry sorted,
Set-LogonHours -Identity Username -TimeIn24Format @(9,10,11,12,13,14,15,16) -Monday -Tuesday -Wednesday -Thursday -Friday -NonSelectedDaysare NonWorkingDays
Bonjour, j’ai mis en place votre script pour des raisons de sécurité sur certains comptes, j’ai changé le get-zone par ([TimeZoneInfo]::Local).BaseUtcOffset.Hours) car la version powershell n’est pas la même. J’ai également écris la fonction dans un fichier .psm1 que j’appelle dans mon script.
Par contre, si vous pouvez m’aider, ça fonctionnait bien avant que je parte en vacances, et aujourd’hui cela enlève toutes les autorisations sur mon user test alors que je met les horaires 8/19h. Auriez-vous une solution?
i’m trying to use this script with a csv file, so i replaced the line “Set-LogonHours -Identity …” for:
import-Csv $base | foreach {
Set-LogonHours -Identity $_.username -TimeIn24Format $_.hour
}
on the csv, there is only two columns, column A “username” and column B “hour”, on the column B i’m setting the parameter as it’s on the original script “@(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,0) -Monday -Tuesday -Wednesday -Thursday -Friday -NonSelectedDaysare NonWorkingDays” but i’m stuck with the error Set-LogonHours : Cannot validate argument on parameter ‘TimeIn24Format’. The argument cannot be validated because its type “String”
is not the same type (Int32) as the maximum and minimum limits of the parameter. Make sure the argument is of type Int32 and then
try the command again.
Can you help me with this or suggest another way to automate using a csv file.
I’m trying to automate this script using a csv file with the username and the -TimeIn24Format attribute, but i’m stuck with an error:
Set-LogonHours : Cannot validate argument on parameter ‘TimeIn24Format’. The argument cannot be validated because its type “String”
is not the same type (Int32) as the maximum and minimum limits of the parameter. Make sure the argument is of type Int32 and then
try the command again.
in the script i replaced the last line “Set-LogonHours -Identity Jack.Ripper -TimeIn24Format …” for this:
$users = “C:\Scripts\LogonHour\csv\users.csv”
import-Csv $users | foreach {
Set-LogonHours -Identity $_.username -TimeIn24Format $_.time # Allow Access during weekday
}
Here’s an example from my csv file:
username,time
user1,” @(10,11,12,13,14,15,16,17,18) -Monday -Tuesday -Wednesday -Thursday -Friday -NonSelectedDaysare NonWorkingDays”
user2,” @(10,11,12,13,14,15) -Tuesday -Wednesday -Thursday -Friday -NonSelectedDaysare NonWorkingDays”
user3,” @(13,14,15,16,17,18) -Monday -Tuesday -Wednesday -Thursday -NonSelectedDaysare NonWorkingDays”
This is kind of in the real of what I am looking for. The difference for me is that I do not need to set logonHours. I simply want to be able to extract the logonHours for each user in a human readable format like the following:
Sunday: 10:00 – 21:00
Monday: 09:00 – 22:00
Tuesday: 09:00 – 22:00
Wednesday: 09:00 – 22:00
Thursday: 09:00 – 22:00
Friday: 09:00 – 22:00
Saturday: 11:00 – 21:00
If you can give me any insight it would be most appreciated
I’m trying to get a logins from a csv and change their access, but I can’t make it to work, the powershell wont recognize it.
Great explanation, but blew my mind with the function creation. I an not that advanced in function creation, so I took a simpler approach. Using a source account that already has the times set, then coping them to the target.
Remove-Variable * -ErrorAction SilentlyContinue
$SourceIdentity = “Source_Test_Account”
$TargetIdentity = “Target_Test_Account”
$logonHours = Get-ADUser $SourceIdentity -Property logonhours | Select-Object -ExpandProperty logonhours
[byte[]]$hours = @($logonHours)
set-aduser -identity $TargetIdentity -replace @{logonhours = $hours}