Workday Tenant Password Reset via PowerShell

As part of my job, I am responsible for security in our Workday environment.  We needed an automated way to reset the passwords when our sandbox tenant was refreshed on a weekly basis.  I have gradually improved my solution over a few years.

The first thing to start with is that I got tired of modifying the code when we needed to change different settings, so I created a config file.

<?xml version="1.0"?>
<Settings>
    <MailFrom>user1@tenant.com</MailFrom>
    <MailTo>user1@tenant,user2@tenant</MailTo>
    <SMTP>smtp.server.com</SMTP>
    <TenantName>tenant</TenantName>
    <URL>https://wd5-impl-services1.workday.com/ccx/service/tenant/Human_Resources/v27.0</URL>
    <UserName>user1@tenant</UserName>
    <NewPassword>Sandbox123!</NewPassword>
    <Password>Unecrypted Password</Password>
    <users>
        <user>
            <employeeid>1234</employeeid>
            <workday_account>jane.doe</workday_account>
            <IsEmployee>false</IsEmployee>
        </user>
        <user>
            <employeeid>6789</employeeid>
            <workday_account>john.doe</workday_account>
            <IsEmployee>true</IsEmployee>
        </user>
    </users>
</Settings>

Due to my security concerns, the config file will start with an un-encrypted password and on the first run, it will encrypt it.

So, let’s get into the script.

The first thing that happens is that the script finds the name of the script file and then looks for a config file with the same name. This is helpful because we have multiple Workday tenants and I don’t have to hard code the config filename in the script. I just rename the script and the config file.

$baseName = ([io.fileinfo]$MyInvocation.InvocationName.ToString()).FullName.Replace(([io.fileinfo]$MyInvocation.InvocationName.ToString()).Extension,'')
$ConfigFileName = $baseName + ".config"

Then, we read the config file and test to see if the password is encrypted. If it isn’t, we encrypt it, save the config file, and then re-read it.

[xml]$ConfigFile = Get-Content $ConfigFileName
try 
{
    $SecurePassword = ConvertTo-SecureString $ConfigFile.Settings.Password -ErrorAction Stop
} 
Catch 
{
    $secpassword = $ConfigFile.Settings.Password | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString
    $ConfigFile.Settings.Password = $secpassword.ToString()
    $ConfigFile.save($ConfigFileName)
    [xml]$ConfigFile = Get-Content $ConfigFileName
}

Once we know that the config file has an encrypted password, we can continue reading in the variables and create the login credentials.

$ErrorRecipients = $ConfigFile.Settings.MailTo.Split(",")
$wd_username = $ConfigFile.Settings.UserName
$ErrorFrom = $ConfigFile.Settings.MailFrom
$uri = $ConfigFile.Settings.URL
$tenant = $ConfigFile.Settings.TenantName
$smtp_server = $ConfigFile.Settings.SMTP
$newPassword = $ConfigFile.Settings.NewPassword
$SecurePassword = ConvertTo-SecureString $ConfigFile.Settings.Password

$credentials = New-Object System.Management.Automation.PSCredential ($wd_username, $SecurePassword)

$Message = ''

$wd_username = $credentials.UserName
$wd_password = $credentials.GetNetworkCredential().password

The core of the script is next. The first thing I had to do was to set a counter variable to catch an error on the first password reset. I initially was using my own account and our passwords expire every 90 days. I would lock my account out by accident when the script ran automatically.

The next step is to loop through all the users in the config file and reset their passwords individually.   After the for loop, I will be sending success or error messages using settings in the config file.


$counter = 0
foreach ($d in $ConfigFile.Settings.Users.user)
{
...
}
Send-MailMessage -To $ErrorRecipients -From $ErrorFrom -Subject "$Tenant Password Reset Results" -Body $Message -SMTP $smtp_server

The XML of the SOAP call comes next.  The script is written to use a default password for all accounts.  To simplify the script, I just concatenated the password into the XML at the beginning.  I have included both the employee and contingent worker reference sections of the API.  I found it was easier to delete a section than dynamically add one.

    [xml]$xml = '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:bsvc="urn:com.workday/bsvc">
    <soapenv:Header>
        <wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
            <wsse:UsernameToken>
                <wsse:Username></wsse:Username>
                <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"></wsse:Password>
            </wsse:UsernameToken>
        </wsse:Security>
    </soapenv:Header>
    <soapenv:Body>
        <bsvc:Workday_Account_for_Worker_Update>
            <bsvc:Worker_Reference>
                <bsvc:Employee_Reference>
                    <bsvc:Integration_ID_Reference>
                        <bsvc:ID bsvc:System_ID="WD-EMPLID"></bsvc:ID>
                    </bsvc:Integration_ID_Reference>
                </bsvc:Employee_Reference>
                <bsvc:Contingent_Worker_Reference>
                    <bsvc:Integration_ID_Reference>
                        <bsvc:ID bsvc:System_ID="WD-EMPLID"></bsvc:ID>
                    </bsvc:Integration_ID_Reference>
                </bsvc:Contingent_Worker_Reference>
            </bsvc:Worker_Reference>
            <bsvc:Workday_Account_for_Worker_Data>
                <bsvc:User_Name></bsvc:User_Name>
                <bsvc:Password>' + $newPassword + '</bsvc:Password>
            </bsvc:Workday_Account_for_Worker_Data>
        </bsvc:Workday_Account_for_Worker_Update>
    </soapenv:Body>
</soapenv:Envelope>'

Here is where I manipulate the XML to set the parameters in the for loop and from the config file. I use SelectSingleNode to select the section of the XML that I want to delete and then use RemoveChild to delete it. Then I set the actual ID and the Workday account I want the password reset on.

    $ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
    $ns.AddNamespace("bsvc", "urn:com.workday/bsvc")

    $xml.Envelope.Header.Security.UsernameToken.Username = $wd_username
    $xml.Envelope.Header.Security.UsernameToken.Password.InnerText = $wd_password

    if ($d.IsEmployee -eq "true") {
        $node = $xml.SelectSingleNode("//bsvc:Contingent_Worker_Reference", $ns)
        $removed = $xml.Envelope.Body.Workday_Account_for_Worker_Update.Worker_Reference.RemoveChild($node)
        $xml.Envelope.Body.Workday_Account_for_Worker_Update.Worker_Reference.Employee_Reference.Integration_ID_Reference.ID.InnerText = $d.employeeid
    } else {
        $node = $xml.SelectSingleNode("//bsvc:Employee_Reference", $ns)
        $removed = $xml.Envelope.Body.Workday_Account_for_Worker_Update.Worker_Reference.RemoveChild($node)
        $xml.Envelope.Body.Workday_Account_for_Worker_Update.Worker_Reference.Contingent_Worker_Reference.Integration_ID_Reference.ID.InnerText = $d.employeeid
    }
    $xml.Envelope.Body.Workday_Account_for_Worker_Update.Workday_Account_for_Worker_Data.User_Name = $d.workday_account.ToString()

I am using a try catch when posting the call to Workday.  Besides capturing errors during each update, it prevents my account from being locked out as I mentioned below.  I do a check to see if the error occurred on the first loop and abort the whole process if it does.  Other than that, I just record the error and continue.  The last step in the for loop is to send a combined list of results via email.

    try {
        $counter = $counter + 1
        $post = Invoke-WebRequest -Uri $uri -Method Post -Body $xml -ContentType "application/xml"
        $Message = $Message + "Changed password for $($d.employeeid) $($d.workday_account)`r`n"
    }
    catch {
        $Message = $Message + "Couldn't Change password for $($d.employeeid) $($d.workday_account)`r`n"
        if($_.Exception.Response) {
            $result = $_.Exception.Response.GetResponseStream()
            $reader = New-Object System.IO.StreamReader($result)
            [xml]$responseBody = $reader.ReadToEnd();
            $errorMsg = "Error: $($responseBody.Envelope.Body.Fault.faultstring)`r`n"
            $Message = $Message +  $errorMsg
            Write-Host $errorMsg
        }
        $Message = $Message + "$($_.Exception)`r`n"
        if ( $counter -le 1 ) {
            $Message = $Message + "Error occurred on first password change.`r`nAborting Process`r`n"
            break
        }
    }

 

Here is the full script.

#Get Execution FileName and Path and use to create paths for additional processing files
$baseName = ([io.fileinfo]$MyInvocation.InvocationName.ToString()).FullName.Replace(([io.fileinfo]$MyInvocation.InvocationName.ToString()).Extension,'')
$ConfigFileName = $baseName + ".config"

#Check to see if password is encrypted.  Encrypt it if not
[xml]$ConfigFile = Get-Content $ConfigFileName
try 
{
    $SecurePassword = ConvertTo-SecureString $ConfigFile.Settings.Password -ErrorAction Stop
} 
Catch 
{
    $secpassword = $ConfigFile.Settings.Password | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString
    $ConfigFile.Settings.Password = $secpassword.ToString()
    $ConfigFile.save($ConfigFileName)
    [xml]$ConfigFile = Get-Content $ConfigFileName
}

$ErrorRecipients = $ConfigFile.Settings.MailTo.Split(",")
$wd_username = $ConfigFile.Settings.UserName
$ErrorFrom = $ConfigFile.Settings.MailFrom
$uri = $ConfigFile.Settings.URL
$tenant = $ConfigFile.Settings.TenantName
$smtp_server = $ConfigFile.Settings.SMTP
$newPassword = $ConfigFile.Settings.NewPassword
$SecurePassword = ConvertTo-SecureString $ConfigFile.Settings.Password

$credentials = New-Object System.Management.Automation.PSCredential ($wd_username, $SecurePassword)

$Message = ''

$wd_username = $credentials.UserName
$wd_password = $credentials.GetNetworkCredential().password

$counter = 0

foreach ($d in $ConfigFile.Settings.Users.user)
{
    [xml]$xml = '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:bsvc="urn:com.workday/bsvc">
    <soapenv:Header>
        <wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
            <wsse:UsernameToken>
                <wsse:Username></wsse:Username>
                <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"></wsse:Password>
            </wsse:UsernameToken>
        </wsse:Security>
    </soapenv:Header>
    <soapenv:Body>
        <bsvc:Workday_Account_for_Worker_Update>
            <bsvc:Worker_Reference>
                <bsvc:Employee_Reference>
                    <bsvc:Integration_ID_Reference>
                        <bsvc:ID bsvc:System_ID="WD-EMPLID"></bsvc:ID>
                    </bsvc:Integration_ID_Reference>
                </bsvc:Employee_Reference>
                <bsvc:Contingent_Worker_Reference>
                    <bsvc:Integration_ID_Reference>
                        <bsvc:ID bsvc:System_ID="WD-EMPLID"></bsvc:ID>
                    </bsvc:Integration_ID_Reference>
                </bsvc:Contingent_Worker_Reference>
            </bsvc:Worker_Reference>
            <bsvc:Workday_Account_for_Worker_Data>
                <bsvc:User_Name></bsvc:User_Name>
                <bsvc:Password>' + $newPassword + '</bsvc:Password>
            </bsvc:Workday_Account_for_Worker_Data>
        </bsvc:Workday_Account_for_Worker_Update>
    </soapenv:Body>
</soapenv:Envelope>'

    $ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
    $ns.AddNamespace("bsvc", "urn:com.workday/bsvc")

    $xml.Envelope.Header.Security.UsernameToken.Username = $wd_username
    $xml.Envelope.Header.Security.UsernameToken.Password.InnerText = $wd_password

    if ($d.IsEmployee -eq "true") {
        $node = $xml.SelectSingleNode("//bsvc:Contingent_Worker_Reference", $ns)
        $removed = $xml.Envelope.Body.Workday_Account_for_Worker_Update.Worker_Reference.RemoveChild($node)
        $xml.Envelope.Body.Workday_Account_for_Worker_Update.Worker_Reference.Employee_Reference.Integration_ID_Reference.ID.InnerText = $d.employeeid
    } else {
        $node = $xml.SelectSingleNode("//bsvc:Employee_Reference", $ns)
        $removed = $xml.Envelope.Body.Workday_Account_for_Worker_Update.Worker_Reference.RemoveChild($node)
        $xml.Envelope.Body.Workday_Account_for_Worker_Update.Worker_Reference.Contingent_Worker_Reference.Integration_ID_Reference.ID.InnerText = $d.employeeid
    }
    $xml.Envelope.Body.Workday_Account_for_Worker_Update.Workday_Account_for_Worker_Data.User_Name = $d.workday_account.ToString()
    try {
        $counter = $counter + 1
        $post = Invoke-WebRequest -Uri $uri -Method Post -Body $xml -ContentType "application/xml"
        $Message = $Message + "Changed password for $($d.employeeid) $($d.workday_account)`r`n"
    }
    catch {
        $Message = $Message + "Couldn't Change password for $($d.employeeid) $($d.workday_account)`r`n"
        if($_.Exception.Response) {
            $result = $_.Exception.Response.GetResponseStream()
            $reader = New-Object System.IO.StreamReader($result)
            [xml]$responseBody = $reader.ReadToEnd();
            $errorMsg = "Error: $($responseBody.Envelope.Body.Fault.faultstring)`r`n"
            $Message = $Message +  $errorMsg
            Write-Host $errorMsg
        }
        $Message = $Message + "$($_.Exception)`r`n"
        if ( $counter -le 1 ) {
            $Message = $Message + "Error occurred on first password change.`r`nAborting Process`r`n"
            break
        }
    }
}

Send-MailMessage -To $ErrorRecipients -From $ErrorFrom -Subject "$Tenant Password Reset Results" -Body $Message -SMTP $smtp_server
Posted in Workday

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow SQL Uber Geek on WordPress.com
Try Audible and Get Two Free Audiobooks
%d bloggers like this: