Fixing Multi-Entitlement Provisioning Issues in SailPoint Identity Security Cloud (Web Service Connector)

If you’ve ever faced a situation where your Web Service Connector in SailPoint Identity Security Cloud (ISC) only assigns the first entitlement during a Create Account operation—this guide is for you.

🚨 The Problem

You’ve defined an entitlement attribute such as accessGroup in your account schema, marked it as:

  • Entitlement: ✅
  • Multi-valued: ✅

And your Create Account JSON body looks like this:

{
  "firstName": "$plan.firstName$",
  "lastName": "$plan.lastName$",
  "authorised": "$plan.authorised$",
  "division": { "href": "https://edu.au:804/api/divisions/773" },
  "@Email": "$plan.email$",
  "accessGroups": [
    {
      "accessGroup": {
        "href": "$plan.accessGroup$"
      }
    }
  ]
}

When provisioning a user with one entitlement, everything works perfectly.
But when you try multiple entitlements, only the first one is processed—or worse, you get a Bad Request error.

🧠 Why It Happens

SailPoint ISC’s Web Service Connector does not automatically “fan out” multi-valued attributes when they’re nested inside an object.

So, when $plan.accessGroup$ contains multiple values, it doesn’t generate multiple objects under accessGroups[]. Instead, it attempts to stuff all values into the same node, leading to malformed JSON or ignored entries.

✅ The Correct Approach

You need to intercept the request before it’s sent and dynamically build the JSON payload.

That’s exactly what the Web Service Before Operation Rule is designed for.

⚙️ The WebServiceBeforeOperationRule Solution

Below is a fully compliant ISC rule that:

  • Reads all accessGroup values from the provisioning plan.
  • Builds a clean, nested accessGroups array.
  • Ensures no duplicate or blank entries.
  • Returns the updated requestEndPoint with proper jsonBody.

🧾 Rule: Build Multi-Entitlement JSON for Create Account

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Rule name="Modify Create Body - Expand Multi AccessGroups" type="WebServiceBeforeOperationRule">
    <Description>
        Modify Create Account jsonBody in-place to expand multi-valued 'accessGroup'
        into the nested array structure required by the API.
        (Beanshell style: no generics; mirrors the coding pattern from the provided example.)
    </Description>
    <Source><![CDATA[
        import java.util.ArrayList;
        import java.util.Collection;
        import java.util.HashMap;
        import java.util.LinkedHashMap;
        import java.util.LinkedHashSet;
        import java.util.List;
        import java.util.Map;
        import java.util.Set;

        import connector.common.JsonUtil;
        import connector.common.Util;

        import sailpoint.connector.webservices.EndPoint;
        import sailpoint.connector.webservices.WebServicesClient;
        import sailpoint.object.Application;
        import sailpoint.object.ProvisioningPlan;
        import sailpoint.object.ProvisioningPlan.AccountRequest;

            Map body = requestEndPoint.getBody();
            String jsonBody = null;
            if (body != null) {
                jsonBody = (String) body.get("jsonBody");
            } else {
                body = new HashMap();
            }
            log.info("Rule - Modify Create Body: start");

            // We'll parse whatever is currently in jsonBody (could be from the action template)
            try {

                Map jsonMap = null;
                if (jsonBody != null && jsonBody.trim().length() > 0) {
                    jsonMap = JsonUtil.toMap(jsonBody);
                    log.info("Rule - Modify Create Body: parsed existing jsonBody");
                }
                if (jsonMap == null) {
                    jsonMap = new LinkedHashMap();
                    log.info("Rule - Modify Create Body: initialized empty json map");
                }

                // -----------------------------
                // Read values from provisioningPlan (Create op)
                // -----------------------------
                String firstName = "";
                String lastName = "";
                String authorisedRaw = "";
                String email = "";
                String auCardholderId = "";
                String programCode = "";
                String academicPlanCode = "";
                String faculty = "";
                String school = "";
                String schoolCode = "";
                String courseCode = "";
                String eduPersonAffiliation = "";

                // Collect multi-valued accessGroup hrefs
                List hrefs = new ArrayList();

                if (provisioningPlan != null) {
                    log.info("Rule - Modify Create Body: provisioningPlan present");

                    for (AccountRequest accReq : Util.iterate(provisioningPlan.getAccountRequests())) {
                        if (accReq.getOperation().equals(AccountRequest.Operation.Create)) {
                            log.info("Rule - Modify Create Body: processing CREATE account request");

                            for (ProvisioningPlan.AttributeRequest attReq : Util.iterate(accReq.getAttributeRequests())) {
                                if (attReq == null) continue;

                                String attrName = attReq.getName();
                                Object valObj = attReq.getValue();
                                String valStr = (valObj == null) ? null : String.valueOf(valObj).trim();

                                if (attrName == null) continue;

                                // single-valued fields
                                if ("firstName".equalsIgnoreCase(attrName)) firstName = (valStr == null ? "" : valStr);
                                else if ("lastName".equalsIgnoreCase(attrName))
                                    lastName = (valStr == null ? "" : valStr);
                                else if ("authorised".equalsIgnoreCase(attrName))
                                    authorisedRaw = (valStr == null ? "" : valStr);
                                else if ("email".equalsIgnoreCase(attrName)) email = (valStr == null ? "" : valStr);
                                else if ("AU_Cardholder_ID".equalsIgnoreCase(attrName))
                                    auCardholderId = (valStr == null ? "" : valStr);
                                else if ("Program_Code".equalsIgnoreCase(attrName))
                                    programCode = (valStr == null ? "" : valStr);
                                else if ("academic_plan_code".equalsIgnoreCase(attrName))
                                    academicPlanCode = (valStr == null ? "" : valStr);
                                else if ("faculty".equalsIgnoreCase(attrName)) faculty = (valStr == null ? "" : valStr);
                                else if ("school".equalsIgnoreCase(attrName)) school = (valStr == null ? "" : valStr);
                                else if ("school_Code".equalsIgnoreCase(attrName))
                                    schoolCode = (valStr == null ? "" : valStr);
                                else if ("course_Code".equalsIgnoreCase(attrName))
                                    courseCode = (valStr == null ? "" : valStr);
                                else if ("eduPersonAffiliation".equalsIgnoreCase(attrName))
                                    eduPersonAffiliation = (valStr == null ? "" : valStr);

                                    // multi-valued entitlement attribute
                                else if ("accessGroup".equalsIgnoreCase(attrName)) {
                                    if (valObj instanceof Collection) {
                                        for (Object o : (Collection) valObj) {
                                            if (o != null) {
                                                String s = String.valueOf(o).trim();
                                                if (s.length() > 0) hrefs.add(s);
                                            }
                                        }
                                    } else if (valStr != null && valStr.length() > 0) {
                                        hrefs.add(valStr);
                                    }
                                }
                            }
                        }
                    }
                } else {
                    log.info("Rule - Modify Create Body: provisioningPlan is null");
                }

                // -----------------------------
                // Mutate/normalize jsonMap in-place like your example style
                // -----------------------------

                // Ensure core attributes overwrite/appear (if template had placeholders, this sets concrete values)
                if (firstName != null && firstName.length() > 0) jsonMap.put("firstName", firstName);
                if (lastName != null && lastName.length() > 0) jsonMap.put("lastName", lastName);

                // authorised -> boolean normalization only if present
                if (authorisedRaw != null && authorisedRaw.length() > 0) {
                    boolean auth = "true".equalsIgnoreCase(authorisedRaw) || "1".equals(authorisedRaw);
                    jsonMap.put("authorised", new Boolean(auth));
                }

                if (email != null && email.length() > 0) jsonMap.put("@Email", email);
                if (auCardholderId != null && auCardholderId.length() > 0)
                    jsonMap.put("@AU Cardholder ID", auCardholderId);
                if (programCode != null && programCode.length() > 0) jsonMap.put("@Program Code", programCode);
                if (academicPlanCode != null && academicPlanCode.length() > 0)
                    jsonMap.put("@Academic Plan Code", academicPlanCode);
                if (faculty != null && faculty.length() > 0) jsonMap.put("@Faculty", faculty);
                if (school != null && school.length() > 0) jsonMap.put("@School", school);
                if (schoolCode != null && schoolCode.length() > 0) jsonMap.put("@School Code", schoolCode);
                if (courseCode != null && courseCode.length() > 0) jsonMap.put("@Course Code", courseCode);
                if (eduPersonAffiliation != null && eduPersonAffiliation.length() > 0)
                    jsonMap.put("@EDU Person Affiliation", eduPersonAffiliation);

                // division.href — enforce/normalize if present in template; else set the known static value you shared
                Object divisionObj = jsonMap.get("division");
                Map divisionMap = null;
                if (divisionObj instanceof Map) {
                    divisionMap = (Map) divisionObj;
                } else {
                    divisionMap = new LinkedHashMap();
                    jsonMap.put("division", divisionMap);
                }
                // Your sample had a leading space in the URL; trim to be safe
                String divHref = "https://edu.au:804/api/divisions/773";
                divisionMap.put("href", divHref);

                // -------- accessGroups build (remove any existing then rebuild) --------
                if (jsonMap.containsKey("accessGroups")) {
                    jsonMap.remove("accessGroups");
                }

                // De-duplicate while preserving order
                Set seen = new LinkedHashSet(hrefs);
                List ag = new ArrayList();
                for (Object o : seen) {
                    if (o == null) continue;
                    String href = String.valueOf(o).trim();
                    if (href.length() == 0) continue;

                    Map inner = new LinkedHashMap();
                    inner.put("href", href);

                    Map wrapper = new LinkedHashMap();
                    wrapper.put("accessGroup", inner);

                    ag.add(wrapper);
                }

                if (!ag.isEmpty()) {
                    jsonMap.put("accessGroups", ag);
                    log.info("Rule - Modify Create Body: accessGroups size = " + ag.size());
                } else {
                    log.info("Rule - Modify Create Body: no accessGroups to set");
                }

                // -----------------------------
                // Render JSON back to body and return endpoint
                // -----------------------------
                String finalBody = JsonUtil.render(jsonMap);
                body.put("jsonBody", finalBody);
                requestEndPoint.setBody(body);

                // Clean accidental "&&" patterns if any templates concatenated params
                requestEndPoint.setFullUrl(requestEndPoint.getFullUrl().replaceAll("&&", "&"));

                log.info("Rule - Modify Create Body: complete");

            } catch (Exception ex) {
                log.error("Rule - Modify Create Body: exception - " + ex);
            }

            return requestEndPoint;
        ]]></Source>
</Rule>

🧩 Where to Configure This

  1. Deploy the rule using VS code.
  2. Attach the rule to connector.

💡 Expected Outcome

Single Entitlement — works as before
Multiple Entitlements — all entitlements get correctly assigned in a single request
No Bad Request — payload is clean and API-compliant

Example final payload sent to API:

{
  "firstName": "John",
  "lastName": "Smith",
  "authorised": true,
  "division": { "href": "https://edu.au:804/api/divisions/773" },
  "@Email": "john.smith@edu.au",
  "accessGroups": [
    { "accessGroup": { "href": "https://edu.au/api/group/123" } },
    { "accessGroup": { "href": "https://edu.au/api/group/456" } }
  ]
}

🚀 Key Takeaways

  • $plan.accessGroup$ expands properly only at the top level, not inside an object.
  • The Before Operation Rule gives full control over request payloads.
  • Always de-duplicate and trim your entitlement values to avoid API rejections.
  • Keep static body sections minimal—build the dynamic parts via rules.

📘 Related Reading

🏁 Conclusion

This approach provides a scalable, clean, and API-compliant solution for complex provisioning payloads in SailPoint Identity Security Cloud.
No more half-assigned entitlements or mysterious Bad Request errors—your Web Service connector will now handle multi-entitlement creation like a pro.


✍️ About the Author

Amit Kumar Gupta is an IAM Architect and founder of IdentityClasses — a leading platform for hands-on training and consulting in SailPoint, Saviynt, Okta, and Oracle IAM.
He helps enterprises and engineers simplify Identity Governance through real-world solutions and practical design patterns.

If you found this post useful, share it with your team or IAM community and follow IdentityClasses on LinkedIn for more hands-on SailPoint tips!

Scroll to Top