A while ago, a new parameter was added to the NERM Profiles API that allows for a very fast GET of all profiles in a tenant or for a particular Profile Type. In this post, I wanted to provide some code examples for how one can utilize this effectively
Relevant API docs: get-profiles | SailPoint Developer Community
When sending the initial request, you can leave the parameter blank. Having it present will tell our system to use that endpoint and with a limit of 500, we generally see responses in under 1s.
The response will look pretty much like any other Profiles API response but with two key differences:
- We include an “Archived” flag as using this parameter will return all profiles, archived and not. This allows you to filter out - or only keep - archived profiles.
- The _metadata object will include the next ID to use as an offset
{
"profiles": [
{
"id": "000c1ce2-7073-460b-a1b7-f146ef907afb",
"uid": "fec66e24cc89496fa3729757275562c1",
"name": "Test 137",
"profile_type_id": "047f1d6f-a231-4a7d-808f-35622794dbad",
"status": "Active",
"id_proofing_status": "pending",
"archived": false,
"updated_at": "2024-12-05T14:57:11.420-05:00",
"created_at": "2024-08-16T11:31:46.271-04:00",
"attributes": {
"first_name": "Test 137"
}
}
],
"_metadata": {
"limit": 1,
"total": 649,
"next": "/profiles?after_id=000c1ce2-7073-460b-a1b7-f146ef907afb&limit=1",
"after_id": "000c1ce2-7073-460b-a1b7-f146ef907afb"
}
}
So, the pseudocode for using after_id would be:
- In a loop, send a GET request for /profiles that includes:
- A limit of 500
- The metadata parameter
- The after_id parameter
- (Optional) The Profile Type ID parameter
- Parse the response, storing the Profiles and tracking the next after_id value
- Continue looping until there are no more profiles to get
- You can do this by checking for a status code other than 200 or tracking the number of total profiles available against how many you have collected so far
Ruby - Here, I loop until I see a non-200 response. When that happens, I break the loop. Inside the loop, I make my GET request for Profiles. First, I pass in a blank string for after_id - but for every request after that, I use the after_id value from the metadata:
require 'json'
require 'net/http'
require 'uri'
require 'net/https'
def make_request(after_id)
profile_type_id= "12345678-1234-1234-1234-123456789012"
url = URI("https://acmeco.nonemployee.com/api/profiles?metadata=true&limit=500&profile_type_id=#{profile_type_id}&after_id=#{after_id}")
https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Accept"] = "application/json"
request["Authorization"] = "Bearer <TOKEN>"
response = https.request(request)
return response.read_body, response.code
end
profiles = Array.new
after_id = ""
loop do
response_body,response_code=make_request(after_id)
break if response_code != '200'
after_id = JSON.parse(response_body)["_metadata"]["after_id"]
JSON.parse(response_body)["profiles"].each do |profile|
profiles << profile
end
end
puts profiles
Go - Similar to the Ruby code, but golang is a bit more verbose:
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
)
type ProfileResponse struct {
Profiles []struct {
ID string `json:"id"`
UID string `json:"uid"`
Name string `json:"name"`
ProfileTypeID string `json:"profile_type_id"`
Status string `json:"status"`
IDProofingStatus string `json:"id_proofing_status"`
Archived bool `json:"archived"`
UpdatedAt string `json:"updated_at"`
CreatedAt string `json:"created_at"`
Attributes map[string]string `json:"attributes"`
} `json:"profiles"`
}
type ResponseMetaData struct {
Metadata struct {
Limit int `json:"limit"`
Offset int `json:"offset"`
Total int `json:"total"`
Next string `json:"next"`
AfterID string `json:"after_id"`
} `json:"_metadata"`
}
func CheckError(err error) {
if err != nil {
log.Fatal(err)
}
}
func MakeGetRequest(params string) ([]byte,int) {
url := "https://acmeco.nonemployee.com/api/profiles?"+ params
req, err := http.NewRequest(http.MethodGet, url, nil)
CheckError(err)
req.Header.Add("Authorization", "Bearer <TOKEN>")
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
CheckError(err)
respBody, err := io.ReadAll(resp.Body)
CheckError(err)
defer resp.Body.Close()
return respBody, resp.StatusCode
}
func main() {
var profiles ProfileResponse
params := url.Values{}
params.Add("metadata", "true")
params.Add("profile_type_id", "12345678-1234-1234-1234-123456789012")
params.Add("limit", "500")
params.Add("after_id", "")
for {
var api_response ProfileResponse
var respMetaData ResponseMetaData
resp, status_code := MakeGetRequest(params.Encode())
if status_code != 200{ break }
CheckError(json.Unmarshal(resp, &api_response))
CheckError(json.Unmarshal(resp, &respMetaData))
for _, rec := range api_response.Profiles {
profiles.Profiles = append(profiles.Profiles, rec)
}
params.Add("after_id", respMetaData.Metadata.AfterID)
}
fmt.Println("Profiles\n",profiles)
}