Wednesday, December 16, 2020

Delete Inactive Accounts From Your Okta Org

Until a few months ago (as of this writing), I had an Automation Workflow in Okta that would delete Okta-mastered accounts that had not logged in for 2 years. That may sound like a long time to leave inactive accounts laying around, but we maintain accounts for former employees so they can get into Workday to retrieve their tax documents. Many of them will only sign in one time each year, but if an account goes unused for two years, they should be safe to delete without inconveniencing anyone.

Unfortunately, Okta recently changed the behavior of the Delete action in the automation workflow. Instead of setting the account status to DELETED, it deactivates them, turning the status to DEPROVISIONED, and the workflow is limited to just one Delete action so there's no way to actually delete the deactivated accounts in the workflow. To make matters worse, I have a powershell script that reactivates all deactivated accounts, so after the workflow deactivated the accounts, the powershell script turned around and reactivated them, even going so far as to sending activation emails to the accounts with legitimate email addresses. And THAT prompted calls and emails to the HR department, demanding to know why we continued to send them unsolicited emails. Needless to say, I deactivated that workflow as soon as I was made aware of the problem. I was quite busy at the time and so I didn't get around to finding another solution, until today. Well, yesterday, actually.

The solution has two parts. The first part involves the automation workflow. Instead of setting the account status to DELETED (DEPROVISIONED, actually), I modified it to set the status to SUSPENDED. In 5-6 years, I've used that status very rarely, so this seemed to be a great way to allow the workflow to identify dormant accounts and get them into some sort of container that I could then - and this is the second part - modify a copy of my reactivation powershell script to query Okta for all the users in the SUSPENDED status, and then delete them with an API call (two API calls, actually, since the first merely deactivates them, just as in the workflow).

Part 1: The Automation Workflow

The reactivation powershell script not only reactivates the accounts of former employees, but it also puts them into a special Okta group that assigns them to the Workday integration. The Automation Workflow also uses that Okta group to limit itself to just the former employees. The workflow is set to run at 5pm every day and look for any user that has been inactive for 730 days. If any former employee accounts meets that criteria, the account status is changed to SUSPENDED.

Part 2: The PowerShell Script

The powershell script will use the Okta API to query our org and return all accounts with a SUSPENDED status. It then loops through that array of accounts and makes two API calls to delete that account. As the script loops, information about each account is logged to a file, and when all the accounts have been processed, the script emails the log file to the designated email address. This gives us a record of which accounts were deleted, and when.

<#
.SYNOPSIS
    Okta_Purge_Inactive_Former_Associates.ps1 - Deletes suspended Okta accounts for former associates
    Created by Mike Koch on December 16, 2020
.DESCRIPTION
    Queries Okta for all SUSPENDED accounts, then deletes them
    Send email with all logged events/actions
.NOTES
    TO DO
    1. 
#>
[CmdletBinding()]
Param()

$api_token = "put_your_org_token_here"
$uri = "https://yourcompany.okta.com/api/v1/users?filter=status%20eq%20%22SUSPENDED%22"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

$allusers = @()
$logfile = "c:\temp\Okta-Purge.log"
if (Test-Path -Path $logfile) {
    Remove-Item -Path $logfile -Force
}

function LogWrite {
    param ([string]$logstring)
    Add-Content $logfile -Value $logstring
}

# Query Okta using the URI specified above, page through the results to get all matching accounts
Do {
    $webrequest = Invoke-WebRequest -Headers @{"Authorization" = "SSWS $api_token"} -Method GET -Uri $uri
    $link = $webrequest.Headers.Link.Split("<").Split(">")
    $uri = $link[3]
    $json = $webrequest | ConvertFrom-Json
    $allusers += $json
} while ($webrequest.Headers.Link.EndsWith('rel="next"'))

if ($allusers.count -gt 0) {
    foreach ($usr in $allusers) {
        LogWrite "Deleting suspended user: $($usr.profile.login), $($usr.profile.displayname)"
        Write-Output "Deleting suspended user: $($usr.profile.login), $($usr.profile.displayname)"
# the first DELETE only DEACTIVATEs the account
        Invoke-WebRequest -Headers @{"Authorization" = "SSWS $api_token"} -Method Delete -Uri "https://yourcompany.okta.com/api/v1/users/$($usr.id)"
# the second DELETE actually DELETEs the account
        Invoke-WebRequest -Headers @{"Authorization" = "SSWS $api_token"} -Method Delete -Uri "https://yourcompany.okta.com/api/v1/users/$($usr.id)"
    }
    $MailMessage = @{
        To         = "SomeoneWhoCares@youremaildomain.com"
        From       = "OktaMaintenanceBot@youremaildomain.com"
        Subject    = "Report: Okta Former Employee Account Deletions"
        Body       = Get-Content $logfile -Raw
        BodyAsHtml = $false
        SmtpServer = "your.smtp.server"
    }
    Send-MailMessage @MailMessage
}

It's been a couple of months since the automation workflow broke. I opened a case with Okta and they acknowledged that there had been an unintended behavior change. The issue was escalated to the developers and the support case was closed. I'm sure they'll fix it eventually, but as a global retailer with thousands of employees, we have a lot of turnover so I need to keep up with the dormant account deletions. If for no other reason than to ensure that we have accurate counts the next time our contract is up for renewal. Even dormant accounts cost money.

Today's initial run of SUSPENDED users came to well over 14000. That got us caught up from the last couple of months, so subsequent runs should be much more reasonable, and finish much more quickly.

Tuesday, March 31, 2020

Don't Use O365 Portals To Set Permissions, Part 1 - FullAccess

I'm finding the O365 and Exchange Admin portals to be quite unreliable when it comes to viewing, setting, and changing permissions on Exchange objects. Retrieval times are slow and timeouts frequently occur. It's particularly frustrating when the current permissions finally appear and I quickly realize that they're not right, that some entries are missing. Our multi-geo environment undoubtedly makes this worse. It's just quicker and easier to use PowerShell, so I'm going to share the various commands and one-liners that I use on a regular basis to get the job done.

In almost all instances, a mailbox or a user can be referenced in a command parameter using their userprincipalname, email address or even their full name enclosed in double quotes. For example, you can use "jdirt@redneck.org", "JoeDirt@redneck.org" or "Joe Dirt" and Exchange will quickly and correctly locate and use the right object. There are always exceptions, but I haven't run into one yet. In the examples below, wherever you see "<mailboxaddress>" or "<useraddress>", you can substitute one of these IDs.

Mailbox Permissions - this is usually just a case of either adding or removing the FullAccess. Since completing our migration to Exchange Online, I'm finding that most of our shared mailboxes have a lot of stale permissions, be it unresolved SIDs or deleted O365 accounts.

The first step is to review the current permissions:

Get-MailboxPermission <mailboxaddress>

That works fine, but the output includes a lot of extra information you don't necessarily need or care about, and some of the stuff you do care about gets truncated. Here's what I use to get just the output I'm interested in.

Get-MailboxPermission <mailboxaddress> | where {$_.isinherited -eq $false -AND $_.user -notlike "NT AUTHORITY*"} | select user, accessrights | sort user

This removes the default and inherited permissions, and in most cases, the output doesn't get truncated. That final Sort helps when I need to copy multiple user strings to the clipboard so I can use Get-Clipboard to pull them into a variable.

The following commands handle the permissions changes.

Add-MailboxPermission <mailboxaddress> -AccessRights FullAccess -User <useraddress>

Remove-MailboxPermission <mailboxaddress> -AccessRights FullAccess -User <useraddress> -Confirm:$false

Next time, I'll cover the Send-As and Send On Behalf Of permissions.