Updating Object Ownership in Bulk Using the Powershell SDK

Quite frequently, mover and leaver actions require a transfer of ownership for the following objects

  • Sources
  • Roles
  • Access Profiles
  • Entitlements

If the mover/leaver in question owns a lot of these objects, manually updating those in the UI can be frustrating and time-consuming. Thankfully, the API has operations to partially update (PATCH) those objects. Furthermore, the Powershell SDK makes scripting out this operation even easier.

Prerequisites

  1. SailPoint Powershell SDK

  2. Powershell 6.2 or higher (required by the SDK)

  3. A token with ORG_ADMIN

Running the Script

Calling this script from the command line looks like this

.\sailpoint_set_new_owner_by_user.ps1 -CurrentOwnerUid mcheek -NewOwnerUid jwolf

Script Walkthrough

This script assumes the person running it knows the Identity attribute UID of both the old and new owners. I chose this attribute because it’s typically easier for people to either type that out or remember it (as opposed to the Identity ID).

The first thing the script does is grab the Identity Id of both users using the Search API method

$current_owner = Get-IdentityId -Username $CurrentOwnerUid
$new_owner = Get-IdentityId -Username $NewOwnerUid

The Get-IdentityId function is below. It’s querying for the identity based on attributes.uid matching the $Username parameter. It returns the first result (there should only be one in this case) and includes, the id, name, and owns object.

function Get-IdentityId{
    param(
        [Parameter(Mandatory=$true)]
        [string]$Username
    )

    $search_body = @"
    {
        "indices": [
            "identities"
        ],
        "query": {
            "query": "attributes.uid:$Username"
        },
        "queryResultFilter":{
            "includes":[
                "id"
                ,"name"
                ,"owns"
            ]
        }
    }
"@

    $Search = ConvertFrom-JsonToSearch -Json $search_body
    try{
        $search_result = Search-Post -Search $Search
        return $search_result[0]
    } catch {
        Write-Host ("Exception occurred when calling Search-Post: {0}" -f ($_.ErrorDetails | ConvertFrom-Json))
        Write-Host ("Response headers: {0}" -f ($_.Exception.Response.Headers | ConvertTo-Json))
    }
}

The owns object is eventually what we’re going to loop through to make the updates. An example of what it looks like:

"owns": {
            "sources": [
                {
                    "id": "29b324f7140d40a199edce61f281506f",
                    "name": "SOX Infrastructure - Database"
                },
                {
                    "id": "2c91808783c7d7fd0183e635ba652128",
                    "name": "ServiceNow - chkdev"
                },
                {
                    "id": "8206ebe16ae74ae2b805ac90bb23b796",
                    "name": "SOX Infrastructure - Server"
                }
            ],
            "accessProfiles": [
                {
                    "id": "0e135c32608b4012aa1c7d4222934e88",
                    "name": "SecNetMotion"
                },
                {
                    "id": "27c4f4b6d253408ca61b20ca95409fde",
                    "name": "secCheekTest108"
                },
                {
                    "id": "2c91808470cfba910170f40418db253a",
                    "name": "Contractor Access Profile"
                },
                {
                    "id": "2c9180847c7a072c017c7afcf7a104c5",
                    "name": "AD Security Group - secCheekTestGroup_2"
                },
                {
                    "id": "2c9180857c570edf017c759894da47f8",
                    "name": "AD Security Group - secCheekTestGroup"
                },
                {
                    "id": "2c9180857c7a0724017c7b1515200539",
                    "name": "AP - secCheekTestGroup_6"
                },
                {
                    "id": "2c9180867f224b41017f27d0b69f12cb",
                    "name": "AP - secCheekTestGroup_6 (ADM ACCOUNTS ONLY)"
                },
                {
                    "id": "2c91808970cfba930170f3fe80e32510",
                    "name": "Employee Access Profile"
                },
                {
                    "id": "304487df4885421a93b119f1a46aa7e2",
                    "name": "secCheekTesting2"
                },
                {
                    "id": "456e92f3969f4e769c7316f23833bf7d",
                    "name": "secCheekTest109"
                },
                {
                    "id": "4bab51075f08492ba2b4c4f5d276f7a6",
                    "name": "secCheekTest104"
                },
                {
                    "id": "4c4f72f27e4b44a5a4c74c2473c2757c",
                    "name": "secCheekTest111"
                },
                {
                    "id": "61ee3ce4151a4590bea7e8abfa332746",
                    "name": "secCheekTest106"
                },
                {
                    "id": "7c9a36c017d24ab997ff6614eed03b1d",
                    "name": "RemotePDI5Live"
                },
                {
                    "id": "9769abf477c343f7b8149fc02ee8e91d",
                    "name": "secCheekTest105"
                },
                {
                    "id": "9a6b2f85a79e40a9ba33d2048e1deec2",
                    "name": "secCheekTest107"
                },
                {
                    "id": "9f316ce3eda14ff9a67e31064a400812",
                    "name": "RemoteDesktop"
                },
                {
                    "id": "b24bcc913deb489db9f556afe9aa2b86",
                    "name": "secActiveSyncUser"
                },
                {
                    "id": "c72c714a06b14be5a1ed4a4f4b150c3c",
                    "name": "secCheekTest103"
                },
                {
                    "id": "caad82bb40e24d0ea79cb06aefa5f631",
                    "name": "RemoteCygnet"
                },
                {
                    "id": "cf128c2aee06474d997d3f3e2135994e",
                    "name": "secDMRV"
                },
                {
                    "id": "e6ab4f21ff894ada8d8ca918f24b2547",
                    "name": "secCheekTest112"
                },
                {
                    "id": "e959bcf81cb34a4d9aa292215d753749",
                    "name": "secCheekTest110"
                },
                {
                    "id": "f8dd5a700ee244bcb2957205484eae35",
                    "name": "SAP Users"
                }
            ],
            "roles": [
                {
                    "id": "2c9180857cc7c342017cc81aacf901aa",
                    "name": "CHEEK TEST"
                }
            ],
            "apps": [
                {
                    "id": "2c9180867c2cb638017c51c30faf5d1a",
                    "name": "Well Readiness"
                }
            ]
        }

Now that we have the information of what the old owner owns, we need to loop through and send update requests for each one:

foreach($source in $current_owner.owns.sources){
    Update-ObjectOwnership -Id $source.id -NewOwnerId $new_owner.id -Type "SOURCE"
}
foreach($role in $current_owner.owns.roles){
    Update-ObjectOwnership -Id $role.id -NewOwnerId $new_owner.id -Type "ROLE"
}
foreach($access_profile in $current_owner.owns.accessProfiles){
    Update-ObjectOwnership -Id $access_profile.id -NewOwnerId $new_owner.id -Type "ACCESSPROFILE"
}
foreach($entitlement in $current_owner.owns.entitlements){
    Update-ObjectOwnership -Id $entitlement.id -NewOwnerId $new_owner.id -Type "ENTITLEMENT"
}

These function calls pass in the id of the object being updated, the id and name of the new owner, and the type of object being updated. The last one is important because it determines which API operation we call.

Here is what the Update-ObjectOwnership function looks like:

function Update-ObjectOwnership{
    param(
        [Parameter(Mandatory=$true)]
        [string]$Id,
        [Parameter(Mandatory=$true)]
        [string]$Type,
        [Parameter(Mandatory=$true)]
        [string]$NewOwnerId
    )
    $request_body = @"
    {
        "op": "replace",
        "path": "/owner",
        "value":
            {
                "id":"$NewOwnerId",
                "type":"IDENTITY"
            }
    }
"@

    try {
        if($Type -eq "SOURCE"){
            Update-Source -Id $Id -JsonPatchOperation ($request_body | ConvertFrom-Json)
        }
        if($Type -eq "ROLE"){
            Update-Role -Id $Id -JsonPatchOperation ($request_body | ConvertFrom-Json)
        }
        if($Type -eq "ACCESSPROFILE"){
            Update-AccessProfile -Id $Id -JsonPatchOperation ($request_body | ConvertFrom-Json)
        }
        if($Type -eq "ENTITLEMENT"){
            Update-BetaEntitlement -Id $Id -JsonPatchOperation ($request_body | ConvertFrom-Json)
        }
    } catch {
        Write-Host ("Exception occurred when calling Update-ObjectOwnership for {0} {1}: {2}" -f ($Type,$Id,$_.ErrorDetails))
    }
}

A successful update operation will return the new object attributes to the console:

Have any questions or feedback? Be sure to ask below. Attached below is the full script.

sailpoint_set_new_owner_by_user.ps1 (2.7 KB)

3 Likes

Hi, I’m trying to follow your instructions to implement this script. When running it, i get the error:

Invoke-ApiClient: C:\Users\woodsma\OneDrive - Park National Bank\Documents\PowerShell\Modules\PSSailPoint\1.2.1\v3\src\PSSailpoint\Api\AccessProfilesApi.ps1:684:27
Line |
684 | $LocalVarResult = Invoke-ApiClient -Method ‘PATCH’ `
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Cannot index into a null array.

Do you know what i might be doing wrong here?

I have also tried this as well, and gotten an error:

PS C:\Users\woodsma\Downloads\powershell_module_identitynow-master\powershell_module_identitynow-master> .\sailpoint_set_new_ap_owner_by_userTest.ps1 -CurrentOwnerUid “105470” -NewOwnerUid “106425”
ConvertFrom-Json: C:\Users\woodsma\Downloads\powershell_module_identitynow-master\powershell_module_identitynow-master\sailpoint_set_new_ap_owner_by_userTest.ps1:32:94
Line |
32 | … en calling Search-Post: {0}" -f ($.ErrorDetails | ConvertFrom-Json))
| ~~~~~~~~~~~~~~~~
| Cannot bind argument to parameter ‘InputObject’ because it is null.
Exception occurred when calling Search-Post:
Response headers: null
ConvertFrom-Json: C:\Users\woodsma\Downloads\powershell_module_identitynow-master\powershell_module_identitynow-master\sailpoint_set_new_ap_owner_by_userTest.ps1:32:94
Line |
32 | … en calling Search-Post: {0}" -f ($
.ErrorDetails | ConvertFrom-Json))
| ~~~~~~~~~~~~~~~~
| Cannot bind argument to parameter ‘InputObject’ because it is null.
Exception occurred when calling Search-Post:
Response headers: null