HeNeedsSomeMilk
HeNeedsSomeMilk

Reputation: 1

Active Directory get all user's logon hours(when they're allowed to log in) using powershell

Is it possible to get all the AD user's logon hours and output it into an excel spreadsheet. So something like:

Name Logon Hours
Bob 9am-6pm
Jane 8am-5pm
Chris 9am-6pm

So far I have Get-AdUser -Filter * -Properties LogonHours | ft Name, LogonHours. However the output is all in binary.

Upvotes: 0

Views: 2629

Answers (1)

Mathias R. Jessen
Mathias R. Jessen

Reputation: 174690

However the output is all in binary.

That happens to be the way AD stores it. The logonHours attribute value consists of 21 bytes, each byte covering an 8-hour window, starting from midnight on Sunday.

As TheMadTechnician notes, converting the byte values to binary/base-2 strings might be the simplest way of accessing each "hour".

With this in mind, we could make a nifty little helper class that can translate the byte array into meaningful logon hour information:

class LogonHourAdapter
{
  hidden
  [string[]]$_days

  LogonHourAdapter([byte[]]$logonHours)
  {
    if($logonHours.Count -ne 21){
      throw [System.ArgumentException]::new('logonHours', "Expected byte array of length 21.")
    }
    $this._days = [string[]]::new(7)
    for($i = 0; $i -lt 7; $i++){
      $offset = $i * 3
      $this._days[$i] = ($logonHours[$offset..($offset+2)]|ForEach-Object{[convert]::ToString($_,2).PadLeft(8, '0')})-join''
    }
  }

  [bool]
  IsAllowedToLogonDuring([DayOfWeek]$day, [int]$hour)
  {
    if($hour -ge 24){
      throw [System.ArgumentOutOfRangeException]::new('hour')
    }

    return $this._days[$day][$hour] -eq '1'
  }

  [int[]]
  GetLogonHoursOn([DayOfWeek]$day){
    $hours = 0..23 |? {$this._days[$day][$_] -eq '1'}
    return $hours -as [int[]]
  }
}

Now we don't need to worry about parsing the binary data, we just pass the attribute value to an instance of the class:

PS ~> $bob = Get-ADUser Bob -Properties logonHours
PS ~> $bobHours = [LogonHourAdapter]::new($bob.logonHours)
PS ~> $bobHours.IsAllowedToLogonDuring('Monday', 9) # Bob is allowed to logon after 9am
True
PS ~> $bobHours.IsAllowedToLogonDuring('Monday', 6) # But not earlier!
False

Next, we'll want to track "ranges" - contiguous periods throughout each day where the user is allowed to logon.

For each day, simply "walk" through each hour from 0 through 23 and start tracking a new range everytime there's a break:

# Prepare our logon hour adapter
$lha = [LogonHourAdapter]::new($bob.logonHours)

# Extract ranges for each day of the week
$ranges = foreach($day in -split 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday'){
  $currRange = $null
  0..23|%{
    if($lha.IsAllowedToLogonDuring($day, $_)){
      if(-not $currRange){
        $currRange = [pscustomobject]@{ Day = [DayOfWeek]$day; From = $_; To = $null}
      }
      $currRange.To = $_ + 1
    }else{
      if($currRange){
        $currRange
        $currRange = $null
      }
    }
  }
  if($currRange){
    $currRange
  }
}

Now we just need to group them together so we can express "8-16 (Monday, Tuesday, Friday)" instead of "8-16 (Monday), 8-16 (Tuesday), 8-16 (Wednesday) ... etc.":

$label = $ranges |Group-Object From,To |Sort Count -Descending |ForEach-Object {
  '{0:00}-{1:00} ({2})' -f $_.Group[0].From,$_.Group[0].To,(($_.Group.Day |Sort) -join', ')
}

And $label now contains a more human readable version of Bob's logonHours:

PS ~> $label
08-16 (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)

Upvotes: 1

Related Questions