Help validating PowerShell script to set Logon Hours to zero in AD: Powershell BeforeModify Rule

Thiago Santana da Silva
Fri, 05 Sep 2025 16:46:18 +0000

@sukanta_biswas

First of all, thank you very much for your time and assistance.

These are the points you mentioned that I’ve added to the code. Could you please review and confirm if the script is now complete and correct?

Logging of RequestId and Identity (audit trail)

Idempotency function to avoid duplicate updates

Idempotency check before applying changes

Logging with Base64 preview of the byte array

Additionally, could you explain if there is any documentation similar to Javadoc, where I can look up the available methods and properties? For example, in the code we use $lr.Name and $lr.Value — how can I determine that .Name and .Value are the correct members to use?

param(
  [Parameter(Mandatory = $false)]
  [string]$requestString
)
# Optional parameter - not really used here, but allows passing the request as a string.

# SailPoint libs
Add-Type -Path "C:\SailPoint\IQService\Utils.dll"
#  Utils.dll to work with AccountRequest and AttributeRequest objects.

# Logging
$logDate = Get-Date -UFormat "%Y%m%d"
$logFile = "C:\SailPoint\Scripts\Logs\ManageLogonHours_$logDate.log"
# Creates daily rotating log files.

function LogToFile([string]$info) {
  $ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
  "[MIZ] $ts - $info" | Out-File $logFile -Append -Encoding UTF8
}
# Helper function to write timestamped logs to file.

try {
  LogToFile "Starting Before Script"

  # Build AccountRequest from IQService env var
  $sReader       = New-Object System.IO.StringReader([string]$env:Request)
  $xmlReader     = [System.Xml.XmlTextReader]([SailPoint.Utils.Xml.XmlUtil]::getReader($sReader))
  $requestObject = New-Object SailPoint.Utils.Objects.AccountRequest($xmlReader)
  # Parse the environment variable "Request" (XML) into a SailPoint AccountRequest object.

  $nativeIdentity = $requestObject.NativeIdentity
  $requestId      = $requestObject.RequestId
  LogToFile "Processing RequestId=$requestId for Identity=$nativeIdentity"
  # Extracts the target identity and request ID, logs them for audit.

  # Helper: check group array for target
  function ContainsTargetGroup($values) { ... }
  # Returns true if the attribute values contain the "g_ferias_afastado" group
  # (checks both DN and short name).

  # Compare helper: check if an AttributeRequest with logonHours already matches desired bytes
  function AlreadyHasLogonHours($requestObject, [byte[]]$desired) { ... }
  # Prevents duplicate updates by checking if logonHours already equals the desired state.

  foreach ($attr in $requestObject.AttributeRequests) {
    if ($attr.Name -ne "memberOf") { continue }
    # Process only "memberOf" attributes (group membership changes).

    $op = [string]$attr.Operation
    $hasTarget = ContainsTargetGroup $attr.Value
    LogToFile "memberOf op=$op; contains target=$hasTarget"

    if ($hasTarget -and $op -eq "Add") {
      # If the user is being added to g_ferias_afastado → deny all logon hours
      $bytes = New-Object 'System.Byte[]' 21
      for ($i=0; $i -lt 21; $i++) { $bytes[$i] = 0 }

      if (-not (AlreadyHasLogonHours $requestObject $bytes)) {
        # Add new AttributeRequest to set logonHours = 0
        $lr = New-Object SailPoint.Utils.Objects.AttributeRequest
        $lr.Name = "logonHours"
        $lr.Value = $bytes
        $lr.Operation = "Set"
        $requestObject.AttributeRequests.Add($lr) | Out-Null

        # Log preview (first 16 chars of base64 of the bytes) for audit
        $preview = [Convert]::ToBase64String($bytes)
        if ($preview.Length -gt 16) { $preview = $preview.Substring(0,16) }
        LogToFile "Adding g_ferias_afastado → logonHours=all zeros [base64=$preview...]"
      } else {
        LogToFile "Skip update: logonHours already all zeros"
      }
    }

    if ($hasTarget -and $op -eq "Remove") {
      # If the user is removed from g_ferias_afastado → restore 24x7 access
      $bytes = New-Object 'System.Byte[]' 21
      for ($i=0; $i -lt 21; $i++) { $bytes[$i] = 255 }

      if (-not (AlreadyHasLogonHours $requestObject $bytes)) {
        # Add new AttributeRequest to set logonHours = 255 (all hours allowed)
        $lr = New-Object SailPoint.Utils.Objects.AttributeRequest
        $lr.Name = "logonHours"
        $lr.Value = $bytes
        $lr.Operation = "Set"
        $requestObject.AttributeRequests.Add($lr) | Out-Null

        # Log preview (first 16 chars of base64) for audit
        $preview = [Convert]::ToBase64String($bytes)
        if ($preview.Length -gt 16) { $preview = $preview.Substring(0,16) }
        LogToFile "Removing g_ferias_afastado → logonHours=24x7 [base64=$preview...]"
      } else {
        LogToFile "Skip update: logonHours already 24x7"
      }
    }
  }

  # Write modified XML back for IQService
  if (-not $args -or [string]::IsNullOrWhiteSpace($args[0])) {
    throw "IQService did not provide an output file path (args[0]). Cannot return modified request."
  }
  $outPath = $args[0]
  LogToFile "Writing modified request XML to $outPath"
  $requestObject.toxml() | Out-File $outPath -Encoding UTF8
  LogToFile "Done."
  # Save the modified request (with updated logonHours) back to a file so IQService can continue.

} catch {
  LogToFile ("ERROR [{0}]: {1}" -f $_.Exception.GetType().FullName, $_.Exception.Message)
  exit 1
}
# Exception handling: logs error type + message, exits with non-zero so IQService fails the operation.