pspete / psPAS

PowerShell module for CyberArk Privileged Access Security REST API
https://pspas.pspete.dev
MIT License
286 stars 90 forks source link

Set-PASUser - Unix time error on ExpiryDate #536

Closed anycard94 closed 1 month ago

anycard94 commented 1 month ago

Set-PASUser in psPAS 6.4.80 using CyberArk PAM 12.6 on accounts without expiryDate shows the following error:

"ConvertTo-UnixTime : The input object cannot be bound to any parameters for the command either because the command does not take pipeline input
or the input and its properties do not match any of the parameters that take pipeline input.
At line:44 char:65
+ ... ct['ExpiryDate'] = $UserProperties['ExpiryDate'] | ConvertTo-UnixTime
+                                                        ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (-62135578800:Int64) [ConvertTo-UnixTime], ParameterBindingException
    + FullyQualifiedErrorId : InputObjectNotBound,ConvertTo-UnixTime"

This error doesn't prevent the PAS user changes but is thrown for parameter changed using Set-PASUser.

Environment:

The accounts in question all have expiryDate of -62135578800 which is a nullable date. If we set an account with a valid expiration date it does not throw this error.

I was able to modify Format-PASUserObject.ps1 to do a check of negative numbers to remove this error:

'ExpiryDate' {
                #Include date string in required format
                if ($UserProperties['ExpiryDate'] -le -1){
                    $UserObject['ExpiryDate'] = $null
                } else {
                    $UserObject['ExpiryDate'] = UserProperties['ExpiryDate'] | ConvertTo-UnixTime 
                }

            }

This removes the error and updates the account. However not sure if this is a proper fix but should be a good starting point.

I tried other fixes such as changing from piping to ConvertTo-UnixTime to specifying ConvertTo-UnixTime -Date UserProperties['ExpiryDate'] but that throws an error about DateTime.MinValue.Ticks / Max.Ticks and doesn't update PAS user.

pspete commented 1 month ago

Hi @anycard94 - thanks for the report. This is the first time hearing of the value -62135578800 in relation to the expiry date for a user account in CyberArk. To remove an expiry date one would usually provide a UnixTime value of 0 (or a datetime of 1/1/1970).

anycard94 commented 1 month ago

If you create a local PAS User using New-PASUser without specifying expiry date it creates user and has that has the value -62135578800.

So, without any changes to Format-PASUserObject.ps1 it sends expiry date as pipeline input to ConvertTo-UnixTime.ps1. Which, then just returns value as $null but with error described.

It appears the -62135578800 value comes back from API when sending $null as expiry date??

Tying to specify with -expirydate of 0 gives an error -> Invoke-PASRestMethod : [500] Value was either too large or too small for an Int32.

Results down below:

PS C:\temp> New-PASUser -UserName pas.test -userType epvuser -authenticationMethod AuthTypeRADIUS -ExpiryDate 0
Invoke-PASRestMethod : [500] Value was either too large or too small for an Int32.
At line:474 char:14
+ ...     $result = Invoke-PASRestMethod -Uri $URI -Method POST -Body $Body
+                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: ({"ErrorCode":"C...for an Int32."}:ErrorRecord) [Invoke-PASRestMethod], Ex
   ception
    + FullyQualifiedErrorId : **CAWS00001E,Invoke-PASRestMethod**
PS C:\temp> New-PASUser -UserName pas.test -userType epvuser -authenticationMethod AuthTypeRADIUS

ID   UserName Source   UserType Suspended enableUser ExpiryDate Location
--   -------- ------   -------- --------- ---------- ---------- --------
201 pas.test CyberArk EPVUser  False     True                  \

PS C:\temp> get-pasuser -id 201 | select *

enableUser              : True
changePassOnNextLogon   : True
expiryDate              : -62135578800
suspended               : False
lastSuccessfulLoginDate : 1715173918
unAuthorizedInterfaces  : {}
authenticationMethod    : {AuthTypeRadius}
passwordNeverExpires    : False
distinguishedName       :
description             :
businessAddress         : @{workStreet=; workCity=; workState=; workZip=; workCountry=}
internet                : @{homePage=; homeEmail=; businessEmail=; otherEmail=}
phones                  : @{homeNumber=; businessNumber=; cellularNumber=; faxNumber=; pagerNumber=}
personalDetails         : @{street=; city=; state=; zip=; country=; title=; organization=; department=; profession=;
                          firstName=; middleName=; lastName=}
id                      : 201
username                : pas.test
source                  : CyberArk
userType                : EPVUser
componentUser           : False
groupsMembership        : {}
vaultAuthorization      : {}
location                : \
pspete commented 1 month ago

its not something we see when creating a user nor are able to replicate image Expiry date is not set on creation when no value is specified.

ExpiryDate parameter of New-PASUser accepts a DateTime object type, and sets the expected value when specified image

Providing a DateTime of 1/1/1970 clears the expirydate when specified for Set-PASUser image psPAS converts the date to unix time and provides a value of 0 in the request

anycard94 commented 1 month ago

I must have something different going on with date time on my end. If I set expiry date to 1/1/1970 it returns value 18000 instead of 0.

Creating user with expire date of tomorrow image

Changing expiry date to 1/1/1970 image image

anycard94 commented 1 month ago

The time I'm getting is UTC - I wrote a function long ago to convert:

image

pspete commented 1 month ago

Maybe something to do with timezone offset then - presume you are UTC-5?

pspete commented 1 month ago

Potentially due to the ToUniversalTime() conversion here.... https://github.com/pspete/psPAS/blob/b8fba0fbf3c1acab2e958f275f39a73647a4b129/psPAS/Private/ConvertTo-UnixTime.ps1#L37

but - removing this would impact times used/returned by other functions in the module.

does (Get-Date 1/1/1970).AddHours(-5) land you at 0 (when updating expirydate)?

anycard94 commented 1 month ago

I am UTC -5 and came across the following KB "PVWA - Requests REST API converts the request's timeframe value to LocalTime instead of UTC time". This article is marked as a known issue and no workaround.

If I do .AddHours(-5) it does get the expiry date to be 0 and no error. I could use this as workaround when I update other properties on the PAS User accounts.

image

But then when I get the user the value is converted back to -62135578800.

image

anycard94 commented 1 month ago

@pspete - I tried some modifications but probably not in the preferred way. It appears to give the correct results. Please feel free to copy / modify as needed.

Created a new file ConvertTo-UtcTime.ps1

Function ConvertTo-UtcTime {
    [CmdletBinding()]
    [OutputType('System.Integer')]
    Param(
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true
        )]
        [int64]$time
    )
    begin {
        $currentCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture
    }
    process {
        [System.Threading.Thread]::CurrentThread.CurrentCulture = 'en-US'

        #Convert EPOCH Time from local
        $UTCTime = (Get-Date 01/01/1970)+([System.TimeSpan]::fromseconds($time))
        $strCurrentTimeZone = (Get-WmiObject win32_timezone).StandardName
        $TZ = [System.TimeZoneInfo]::FindSystemTimeZoneById($strCurrentTimeZone)
        $UTCTimeFinal = [System.TimeZoneInfo]::ConvertTimeFromUtc($UTCTime, $TZ)
        #If the time is before 1/1/1970 set final UTC to 1/1/1970
        if($UTCTimeFinal -le (Get-Date 1/1/1970)){
            $UTCTimeFinal = (Get-Date 1/1/1970)
        }

        # Do UTC Offset
        $UtcOffSetHours = $TZ.BaseUtcOffset.Hours
        $UtcOffSetMinutes = $TZ.BaseUtcOffset.Minutes
                # Are we currently in Daylight Savings??
        If($tz.IsDaylightSavingTime((get-date))){$UtcOffSetHours += 1}

        # Return Value
        Return $UTCTimeFinal.AddHours($UtcOffSetHours).AddMinutes($UtcOffSetMinutes)
    }
    end {
        [System.Threading.Thread]::CurrentThread.CurrentCulture = $currentCulture
    }
}

Modified Format-PASUserObject.ps1 - I added the switch as my expire date of -62135578800 comes back as Int64 type instead of DateTime.

switch ($UserProperties.keys) {

            'ExpiryDate' {
                #Include date string in required format
                switch ($UserProperties['ExpiryDate'].GetType().Name){
                    'DateTime'{$UserObject['ExpiryDate'] = $UserProperties['ExpiryDate'] | ConvertTo-UnixTime}
                    'Int64' {$UserObject['ExpiryDate'] = ($UserProperties['ExpiryDate'] | ConvertTo-UtcTime) | ConvertTo-UnixTime}
                }
            }

Modified ConvertTo-UnixTime.ps1 to modify date on offset.

process {
        [System.Threading.Thread]::CurrentThread.CurrentCulture = 'en-US'

        $strCurrentTimeZone = (Get-WmiObject win32_timezone).StandardName
        $TZ = [System.TimeZoneInfo]::FindSystemTimeZoneById($strCurrentTimeZone)
        # Do UTC Offset
        $UtcOffSetHours = $TZ.BaseUtcOffset.Hours
        $UtcOffSetMinutes = $TZ.BaseUtcOffset.Minutes
        # Are we currently in Daylight Savings??
        If($tz.IsDaylightSavingTime((get-date))){$UtcOffSetHours += 1}

        # Return Value
        $Date = $Date.AddHours($UtcOffSetHours).AddMinutes($UtcOffSetMinutes)

        $UnixTime = [math]::Round($(Get-Date $Date.ToUniversalTime() -UFormat %s))

        If ($Milliseconds) {
            $UnixTime = $UnixTime * 1000
        }

        $UnixTime

Results

Before ConvertTo-UnixTime.ps1 fix for TZ

PS C:\> date

Tuesday, May 14, 2024 2:45:24 PM

PS C:\> Set-PASUser -id 144 -username cybtest -ExpiryDate (get-date).addhours(4)

ID  UserName Source   UserType Suspended enableUser ExpiryDate            Location
--  -------- ------   -------- --------- ---------- ----------            --------
144 CybTest  CyberArk EPVUser  False     True       5/14/2024 10:45:29 PM \

PS C:\> date

Tuesday, May 14, 2024 2:46:17 PM

After modifications ConvertTo-UnixTime.ps1 for TZ

PS C:\> date

Tuesday, May 14, 2024 3:10:08 PM

PS C:\> Set-PASUser -id 144 -username cybtest -description 'test' -ExpiryDate (Get-Date).AddHours(4)

ID  UserName Source   UserType Suspended enableUser ExpiryDate           Location
--  -------- ------   -------- --------- ---------- ----------           --------
144 CybTest  CyberArk EPVUser  False     True       5/14/2024 7:10:11 PM \

PS C:\> date

Tuesday, May 14, 2024 3:10:14 PM
pspete commented 1 month ago

Nice - will take a look next time I have some dev time 👍

pspete commented 1 month ago

Should now be resolved in 6.4.85. Set-PASUser now does not attempt to convert in-situ property values. Unix epoch time is also not converted to universal time if specified as an expirydate.