Find Accounts by Any Attribute (No SQL): Reusable Helper + Pre-Provisioning & Policy Examples
Problem
You need to search for existing accounts (Links) by an account attribute that is not the application’s nativeIdentity (e.g., sAMAccountName, mail, uid). You want to:
-
avoid direct SQL,
-
use it reliably in Before Provisioning checks (to prevent duplicates),
-
optionally enforce it via an Advanced Policy (governance trail + deny on violation).
Diagnosis
IIQ guarantees uniqueness only for nativeIdentity. Searching by other attributes is possible via the Object Model (Link, QueryOptions, Filter) without SQL, but teams often reinvent it ad-hoc, miss case handling, or forget to scope by application — causing slow or incorrect lookups.
Solution
Use a small, reusable helper to find a Link by <application, attribute, value>. Then plug it into:
-
a Before Provisioning rule (to veto duplicates right before create), and/or
-
an Advanced Policy Rule (to raise a PolicyViolation and block the request).
Below you’ll find:
-
a utility method you can paste into your rules,
-
a Before Provisioning example (cleanest enforcement),
-
an Advanced Policy example (governance + reporting).
Works on IIQ 8.4; adapt names (application, attributes) to your environment.
A) Reusable Helper (drop-in)
/** Utility: find first Link for appName where Link.attributes[attrName] equals candidateValue.
* - No raw SQL. Uses QueryOptions + Filter.
* - Case handling: optional normalized compare in-memory (safe & predictable).
* - For best perf, ensure the attribute is aggregated and (ideally) indexed in DB.
*/
import sailpoint.api.SailPointContext;
import sailpoint.object.Link;
import sailpoint.object.QueryOptions;
import sailpoint.object.Filter;
import sailpoint.tools.Util;
public static Link findLinkByAttr(SailPointContext context,
String appName,
String attrName,
String candidateValue,
boolean caseInsensitive) throws Exception {
if (Util.isEmpty(appName) || Util.isEmpty(attrName) || Util.isEmpty(candidateValue)) return null;
// Scope hard to application to keep queries tight
QueryOptions qo = new QueryOptions(Link.class);
qo.addFilter(Filter.eq("application.name", appName));
// Primary filter on attribute — exact match first (DB-side)
qo.addFilter(Filter.eq("attributes." + attrName, candidateValue));
// Quick hit
java.util.Iterator it = context.search(Link.class, qo);
if (it != null && it.hasNext()) {
return (Link) it.next();
}
// Optional: case-insensitive fallback (broader, then normalize in-memory)
if (caseInsensitive) {
QueryOptions qo2 = new QueryOptions(Link.class);
qo2.addFilter(Filter.eq("application.name", appName));
java.util.Iterator it2 = context.search(Link.class, qo2);
while (it2 != null && it2.hasNext()) {
Link link = (Link) it2.next();
Object v = (link.getAttributes() != null) ? link.getAttributes().get(attrName) : null;
if (v != null && candidateValue.equalsIgnoreCase(String.valueOf(v))) {
return link;
}
}
}
return null;
}
Notes
-
Keep the attribute aggregated into
Link.attributes(check your aggregation/correlation). -
For large apps, prefer an exact match and ensure proper indexing; use the case-insensitive fallback only if needed.
-
Always pass application name to avoid cross-app collisions (e.g.,
uidreused by multiple sources).
B) Before Provisioning (veto duplicates cleanly)
When to use: You’re about to create an account and need to guarantee no other account in the same application already has the same attribute (e.g., sAMAccountName).
Hook: Before Provisioning rule on the application (or a central pre-plan rule).
import sailpoint.object.ProvisioningPlan;
import sailpoint.object.ProvisioningPlan.AccountRequest;
import sailpoint.object.ProvisioningPlan.ObjectOperation;
import sailpoint.object.Attribute;
import sailpoint.tools.GeneralException;
import sailpoint.tools.Util;
// Inputs typically available: context, plan, application
String appName = application.getName();
String attrName = "sAMAccountName"; // <— adjust
boolean ignoreCase = true;
java.util.List accReqs = plan.getAccountRequests(appName);
if (!Util.isEmpty(accReqs)) {
for (Object o : accReqs) {
AccountRequest ar = (AccountRequest) o;
if (ar.getOp() == ObjectOperation.Create) {
// read candidate value from the plan (attribute requests)
String candidate = null;
if (!Util.isEmpty(ar.getAttributeRequests())) {
for (Object arqObj : ar.getAttributeRequests()) {
sailpoint.object.ProvisioningPlan.AttributeRequest arq =
(sailpoint.object.ProvisioningPlan.AttributeRequest) arqObj;
if (attrName.equals(arq.getName())) {
Object val = arq.getValue();
candidate = (val == null) ? null : String.valueOf(val);
break;
}
}
}
if (Util.isNotNullOrEmpty(candidate)) {
Link existing = findLinkByAttr(context, appName, attrName, candidate, ignoreCase);
if (existing != null) {
throw new GeneralException(
"Duplicate detected: " + attrName + " '" + candidate + "' already exists in " + appName);
}
}
}
}
}
Why this works
-
It runs right before provisioning, blocking duplicates regardless of UI/API path.
-
Uses IIQ’s object model (no SQL).
-
Gives a clear error to the requester/approver.
C) Advanced Policy (governance enforcement)
When to use: You also want a PolicyViolation (audit trail, reporting) if a request proposes a duplicate value. Configure the policy to Deny on violation.
Hook: Advanced Policy Rule attached to a PolicyDefinition.
import sailpoint.object.IdentityRequest;
import sailpoint.object.RequestItem;
import sailpoint.object.PolicyViolation;
import sailpoint.object.Application;
import sailpoint.tools.Util;
java.util.List violations = new java.util.ArrayList();
IdentityRequest req = (IdentityRequest) context.get("identityRequest");
if (req != null && !Util.isEmpty(req.getItems())) {
String appName = "Active Directory"; // <— adjust
String attrName = "sAMAccountName"; // <— adjust
boolean ignoreCase = true;
for (Object o : req.getItems()) {
RequestItem item = (RequestItem) o;
boolean isCreate = "Add".equalsIgnoreCase(item.getOperation());
boolean isTargetApp = appName.equalsIgnoreCase(item.getApplicationName());
if (isCreate && isTargetApp && item.getAttributes() != null) {
Object val = item.getAttributes().get(attrName);
String candidate = (val == null) ? null : String.valueOf(val);
if (Util.isNotNullOrEmpty(candidate)) {
Link existing = findLinkByAttr(context, appName, attrName, candidate, ignoreCase);
if (existing != null) {
sailpoint.object.PolicyViolation v = new PolicyViolation();
v.setPolicy(policy);
v.setOwner(req.getIdentity());
v.setDescription("Duplicate " + attrName + " detected in " + appName + ": " + candidate);
violations.add(v);
}
}
}
}
}
return violations;
Configure the policy
-
Create Policy → Type: Advanced.
-
Attach this rule; set Scope to your application.
-
Set On Violation: Deny and provide a friendly UI message.
Testing Checklist
-
Verify the attribute is aggregated into
Link.attributes. -
Test exact and case-insensitive collisions (e.g.,
jdoevsJDOE). -
Try UI, API, and bulk provisioning paths.
-
Confirm Advanced Policy shows a violation and blocks when enabled.
-
Load test against your largest application; if needed, consider storing a normalized copy (e.g.,
sAMAccountNameLower) to speed lookups.
Gotchas & Tips
-
Race conditions: Rare, but two concurrent creates can pass the check. If that risk matters, prefer mapping this attribute as
nativeIdentity(best), or add an authoritative-side uniqueness constraint. -
Performance: Always include
application.namein the filter. For very large datasets, add an index on the attribute column in your DB (work with your DBA). -
Error hygiene: Throw clear messages so helpdesk knows what to fix (“value already exists”).
-
Governance vs runtime: The Before Provisioning hook is authoritative (stops it). The Advanced Policy adds visibility and reporting.
Conclusion
You don’t need raw SQL to enforce uniqueness on non-nativeIdentity attributes. A tiny, reusable Link search helper plus a Before Provisioning check gives you safe, consistent enforcement. If you also want audit/reporting, layer an Advanced Policy on top.
Feel free to copy this into your KB; swap Active Directory / sAMAccountName for your app/attribute names.