> For the complete documentation index, see [llms.txt](https://docs.openiam.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.openiam.com/administration-guide/lifecycle/policy-map/policy-map-reference.md).

# Policy map reference

How OpenIAM evaluates a policy-map row, the script contract, the worked Active Directory map, and the connector behaviour behind a row that does not arrive.

The behaviour behind every row on a policy map: how a row is stored and dispatched, what a Groovy source script may assume, a complete worked Active Directory user map, and the group, enablement and null-handling rules that decide what the target actually receives.

Build the map itself on [Build a policy map](/administration-guide/lifecycle/policy-map.md). Come here when a row does not behave the way the editor suggests it should.

## The data model

A managed system has zero or more policy maps (`MNG_SYS_POLICY`), each typed by a metadata type in grouping `PROV_OBJECT`. Three have shipped since 4.0.0.0: `USER_OBJECT` ("Provision User"), `GROUP_OBJECT` ("Provision Group") and `COMPUTER_OBJECT` ("Provision Computer"). Which type user provisioning looks for is itself a system property, `org.openiam.idm.managed.sys.policy.user.type`, shipping as `USER_OBJECT`.

Each policy map owns rows in `ATTRIBUTE_MAP`, one per target attribute. Every row has a mandatory one-to-one `RECON_RES_ATTR_MAP` child that carries the source — either a Groovy script ID or an IDM field name.

### Row columns

| Editor column   | Persisted as                                                                                            | Meaning                                                                                                                    |
| --------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Object type     | `ATTRIBUTE_MAP.MAP_FOR_OBJECT_TYPE`                                                                     | Which part of the connector object this row fills. Drives special handling — see below.                                    |
| Attribute name  | `ATTRIBUTE_MAP.ATTRIBUTE_NAME` (max 50 chars)                                                           | The name sent to the connector. This is the target attribute, despite the column that is *called* `TARGET_ATTRIBUTE_NAME`. |
| Type            | derived from which source column is filled — see the source-type table                                  | Groovy / IDM value / Static value.                                                                                         |
| Attribute Value | `RECON_RES_ATTR_MAP.GROOVY_SCRIPT_ID`, `RECON_RES_ATTR_MAP.IDM_FIELD`, or `ATTRIBUTE_MAP.DEFAULT_VALUE` | The source, depending on Type.                                                                                             |
| Status          | `ATTRIBUTE_MAP.IS_ACTIVE` (`Y`/`N`)                                                                     | Inactive rows are skipped with "Attribute Map is inactive or name is empty!".                                              |

{% hint style="warning" %}
`TARGET_ATTRIBUTE_NAME` is a legacy column and is *not* what the connector receives. The value on the wire is `ATTRIBUTE_MAP.ATTRIBUTE_NAME`, which is what the editor's **Attribute name** field writes; `TARGET_ATTRIBUTE_NAME` is stored separately and read by nothing. If you are querying the map under break-glass, read `ATTRIBUTE_NAME`, and confirm what actually went out from the operation's `provisionconnectorrequest` document.
{% endhint %}

`ATTRIBUTE_MAP.DATA_TYPE` persists a `PolicyMapDataTypeOptions` value (`string`, `date`, `int`, `long`, `timestamp`, `memberOf`, `manager`, `byteArray`). The current editor does not expose a Data type column, and provisioning never reads it — it is saved and read back and nothing else. Treat it as inert unless your connector release documents otherwise.

### Object types

The persisted enum is `PolicyMapObjectTypeOptions`: `USER`, `PRINCIPAL`, `PASSWORD`, `GROUP`, `GROUP_PRINCIPAL`, `ROLE`, `ROLE_PRINCIPAL` . The Webconsole dropdown offers a slightly different set — `USER`, `PRINCIPAL`, `GROUP`, `GROUP_PRINCIPAL`, `COMPUTER`, `COMPUTER_PRINCIPAL`, `PASSWORD` . Three of them get special treatment in the engine:

| Object type     | What the engine does with it                                                                                                                                                                                                                                                                                                    |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PRINCIPAL`     | Not evaluated as an ordinary attribute. The engine reads the already-built login for this managed system and calls `mergeIdentity()`, which sets `identityName` (your attribute name) and `identityValue` on the connector object, and on a rename also sets `originalIdentityValue`. Exactly one `PRINCIPAL` row per user map. |
| `PASSWORD`      | Evaluated only when there is a password to send: either `isChangePasswordOperation()` is true for this managed system, or the identity has never been successfully provisioned (`isProcessingAllFields()`). On an ordinary modify of an already-provisioned account, the `PASSWORD` row produces nothing.                       |
| everything else | Evaluated as a normal attribute, and only when the operation is evaluating policy-map attributes at all.                                                                                                                                                                                                                        |

`isProcessingAllFields()` returns `false` — meaning "send only what changed" — once the identity's `ProvLoginStatusEnum` is one of `SAVED`, `ENABLED`, `CREATED`, `DISABLED`, `UPDATED` . Before that, every row is forced to evaluate.

### The three source types

The **Type** drop-down offers three choices. The label you see and the string that is persisted differ, which matters the moment you read the database:

| Label in the UI | Persisted value | Where the value lives                        | What runs                                                                                   |
| --------------- | --------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Groovy          | `POLICY`        | `RECON_RES_ATTR_MAP.GROOVY_SCRIPT_ID`        | The script is instantiated, then its `isPerform()` and `perform()` are called in that order |
| IDM value       | `DEFAULT_IDM`   | `RECON_RES_ATTR_MAP.IDM_FIELD`               | The named field is read straight off the diff                                               |
| Static value    | `STATIC_VALUE`  | `ATTRIBUTE_MAP.DEFAULT_VALUE` (max 32 chars) | The literal, added with operation `ADD`                                                     |

Saving a row clears the other two sources, so a row can only ever have one. At run time the type is re-derived from whichever source is populated, in this order — Groovy wins, then IDM field, then static. A row with none of the three is **skipped silently**.

`DEFAULT_VALUE` does double duty: on a Groovy or IDM row it is the **fallback** used when the source produced no values at all, which is a genuinely useful way to give `objectClass`-style attributes a floor without writing a script for it.

### Evaluation order

For one provisioning operation on one managed system, in this order:

1. The operation builds a diff between the incoming user and the stored user. **Every row's decision is made against that diff, not against the user.**
2. The managed system's policy maps of the user type are selected. **One connector object is built per matching map** — two maps of the same type means two objects sent for the same operation.
3. Whether *all* fields are being processed is decided once per object. When it is, every row is forced to evaluate whatever its `isPerform` says.
4. Each `ATTRIBUTE_MAP` row is dispatched on `MAP_FOR_OBJECT_TYPE`: `PRINCIPAL` and `GROUP_PRINCIPAL` go to identity merging, `PASSWORD` to the password branch, everything else to the ordinary attribute path.
5. The row's source type is derived, and then the Groovy script is run (`isPerform`, then `perform`), or the IDM field is read off the diff, or the static `DEFAULT_VALUE` is used with operation `ADD`.
6. **A row that throws is caught, logged at debug level, and skipped.** Any other failure is logged at error level as `Internal error`. Neither aborts the object.
7. The assembled attributes go to the connector.

The observable check for the whole sequence is the `provisionconnectorrequest` document for the operation, or a simulation capture: it holds exactly the attributes the map produced, and comparing it against the rows is the only way to see which rows were skipped.

## Creating a map: the three routes

| Route                                | How                                                                                                                                                                  | What you get                                                                                                                                                                                                                                                                                                         |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| From scratch                         | **Add** on `/webconsole/policyMap?id=<MANAGED_SYSTEM_ID>`, pick a `PROV_OBJECT` type → `/webconsole/policyMapEdit?managedSysId=<MANAGED_SYSTEM_ID>&type=USER_OBJECT` | An empty map. Every row is yours to add.                                                                                                                                                                                                                                                                             |
| From the connector's policy template | **Create Default Policies** on `/webconsole/policyMap?id=<MANAGED_SYSTEM_ID>`                                                                                        | One new policy map per template on the connector, with every attribute row copied as a genuinely new row — editing the copy never touches the template. Requires a connector on the managed system.                                                                                                                  |
| By cloning a managed system          | **Clone Managed System** on the managed-system screen, with **Clone Policy Map** ticked in the dialog                                                                | A full copy of the source system's maps onto the new system. The dialog needs a **New Name** and independently offers **Clone Managed System Credentials**, **Clone Managed System Attributes** and **Clone Managed System Object Match** — tick only what you want carried over. Audited as `CLONE_MANAGED_SYSTEM`. |

Connector templates live at `/webconsole/policyMapTemplate?id=<connectorId>` and are edited with the same screen (`/webconsole/policyMapEdit?connectorId=...&type=USER_OBJECT`). Editing a template does **not** propagate to managed systems already created from it — the copy is taken once, at create-defaults time.

## A complete Active Directory user policy map

`provision/ad/` ships 33 per-attribute scripts, plus `group/` (5), `fileshare/` (6) and `powershell/` (32 — a near-parallel set for the PowerShell AD connector, which adds `enabled_1.0.groovy` and drops `objectclass`, `userAccountControl` and `employeeType`). The rows below are what an admin actually creates for the LDAP-mode AD connector; every script named here exists in that directory.

| Object type | Attribute name       | Type   | Attribute Value                              |
| ----------- | -------------------- | ------ | -------------------------------------------- |
| `PRINCIPAL` | `cn`                 | Groovy | `provision/ad/cn_1.0.groovy`                 |
| `PASSWORD`  | `unicodePwd`         | Groovy | `provision/ad/unicodePwd_1.0.groovy`         |
| `USER`      | `objectClass`        | Groovy | `provision/ad/objectclass_1.0.groovy`        |
| `USER`      | `sAMAccountName`     | Groovy | `provision/ad/sAMAccountName_1.0.groovy`     |
| `USER`      | `userPrincipalName`  | Groovy | `provision/ad/userPrincipalName_1.0.groovy`  |
| `USER`      | `givenName`          | Groovy | `provision/ad/givenName_1.0.groovy`          |
| `USER`      | `sn`                 | Groovy | `provision/ad/sn_1.0.groovy`                 |
| `USER`      | `initials`           | Groovy | `provision/ad/initials_1.0.groovy`           |
| `USER`      | `displayName`        | Groovy | `provision/ad/displayName_1.0.groovy`        |
| `USER`      | `mail`               | Groovy | `provision/ad/mail_1.0.groovy`               |
| `USER`      | `title`              | Groovy | `provision/ad/title_1.0.groovy`              |
| `USER`      | `department`         | Groovy | `provision/ad/department_1.0.groovy`         |
| `USER`      | `division`           | Groovy | `provision/ad/division_1.0.groovy`           |
| `USER`      | `company`            | Groovy | `provision/ad/company_1.0.groovy`            |
| `USER`      | `employeeId`         | Groovy | `provision/ad/employeeId_1.0.groovy`         |
| `USER`      | `employeeNumber`     | Groovy | `provision/ad/employeeNumber_1.0.groovy`     |
| `USER`      | `employeeType`       | Groovy | `provision/ad/employeeType_1.0.groovy`       |
| `USER`      | `manager`            | Groovy | `provision/ad/manager_1.0.groovy`            |
| `USER`      | `telephoneNumber`    | Groovy | `provision/ad/telephoneNumber_1.0.groovy`    |
| `USER`      | `mobile`             | Groovy | `provision/ad/mobile_1.0.groovy`             |
| `USER`      | `streetAddress`      | Groovy | `provision/ad/streetAddress_1.0.groovy`      |
| `USER`      | `l`                  | Groovy | `provision/ad/l_1.0.groovy`                  |
| `USER`      | `st`                 | Groovy | `provision/ad/st_1.0.groovy`                 |
| `USER`      | `postalCode`         | Groovy | `provision/ad/postalCode_1.0.groovy`         |
| `USER`      | `c`                  | Groovy | `provision/ad/c_1.0.groovy`                  |
| `USER`      | `co`                 | Groovy | `provision/ad/country_1.0.groovy`            |
| `USER`      | `homeDirectory`      | Groovy | `provision/ad/homeDirectory_1.0.groovy`      |
| `USER`      | `homeDrive`          | Groovy | `provision/ad/homeDrive_1.0.groovy`          |
| `USER`      | `thumbnailPhoto`     | Groovy | `provision/ad/ThumbnailPhoto_1.0.groovy`     |
| `USER`      | `userAccountControl` | Groovy | `provision/ad/userAccountControl_1.0.groovy` |
| `USER`      | `accountExpires`     | Groovy | `provision/ad/accountExpires_1.0.groovy`     |
| `USER`      | `memberOf`           | Groovy | `provision/ad/memberOf_1.0.groovy`           |
| `USER`      | `ou`                 | Groovy | `provision/ad/ou_1.0.groovy`                 |

Notes that will bite you if you copy this blind:

* **`cn` is the `PRINCIPAL` row, not a `USER` row.** `cn_1.0.groovy` extends `AbstractPrincipalGenerator`, which generates `<firstName>.<lastName>` (transliterated) and appends a counter until the login is unique in that managed system. Because the row is `PRINCIPAL`, the engine ignores the script's output at provision time and uses the login already built for the identity — the script matters for *generating* the login and for reconciliation's `getActualValue()`.
* **`ou_1.0.groovy` ships with a placeholder ID you must change.** It reads `private final static String OU_METADATA_TYPE_ID = "8a808899679a0d9001679ddd707002a3"` and its own comment says "please fill this. it can be different. you need auto-generated id." It resolves the user's role of that metadata type and emits `ou=<roleName>`. Left as shipped, the row emits nothing on a system where that type ID does not exist.
* **`ou_1.0.groovy` returns `true` from `isPerform` unconditionally** — "in term of LDAP connector OU must be perform permanently" — so it is recalculated on every modify.
* **`accountExpires` sends an empty string when the user has no end date**, converting `lastDateDiff` to Windows FILETIME otherwise (`(millis + 11644473600000) * 10000`). Its `isPerform` short-circuits on `diffObject.getValue().getOperation()` — the operation on the user object, not `getStatus()` — then falls back to `lastDateDiff.isDirty()`.
* **`userAccountControl`** computes the account's enabled state from the user's two statuses, and only fires when one of them changed — see [Status changes and account enablement](#status-changes-and-account-enablement).
* There is **no `sAMAccountName` uniqueness logic** in `sAMAccountName_1.0.groovy`. It emits `diffObject.getFirstNameDiff()?.getValue() + " " + diffObject.getLastNameDiff()?.getValue()` — with a space, and with no collision handling. Its own class comment points you at `AbstractPrincipalGenerator` instead. On most AD deployments you will replace it.
* **`c_1.0.groovy` hard-codes `"US"`** and returns `true` from `isPerform` unconditionally, so it overwrites `c` on every operation with a literal. `country_1.0.groovy` (class `Co`) is the one that reads a real value — `diffObject.getValue().getDefaultAddress().getCountry()` — and is the script for the `co` row. Change or drop the `c` row before you go live.
* **`objectclass_1.0.groovy`'s `isPerform` has no else branch.** It returns `true` on `ADD` and falls off the end otherwise, which Groovy returns as `null` and the engine treats as false. That is the intended behaviour (objectClass is set once) but it will surprise you if you copy the pattern.
* **`ThumbnailPhoto_1.0.groovy` emits a data URL**, not raw bytes: it loads the profile picture through `userMQService.getProfilePictureByUserId` and formats `data:image/<format>;base64,<body>`. Whether your connector unwraps that into AD's octet-string `thumbnailPhoto` is connector behaviour — check the value that lands in the directory before trusting this row.

### Rows the Active Directory connector drops

A row can be correct in OpenIAM, present in the connector request, and still never reach the directory, because the PowerShell Active Directory connector binds only attribute names it recognises. `Add-User` and `Update-User` build their allow-list from `$defaultExtraAttributes` — `adminDisplayName`, `EmployeeType`, `ExtensionAttribute1` through `ExtensionAttribute15`, `Info`, `Co`, `CountryCode`, `mailNickname`, `physicalDeliveryOfficeName` — concatenated with whatever the managed-system attribute `ExtraUserAttributes` holds as a comma-separated string. Group operations use a fixed list of `ExtensionAttribute1` through `ExtensionAttribute15` only, and **never consult `ExtraUserAttributes`** — so an extra attribute you add for groups cannot be allowed through this way at all.

Anything outside the list for that object type is discarded, and the discard is logged only at debug:

{% code overflow="wrap" %}

```
Ignoring 'myCustomAttr' attribute for modifying user because no special rule defined for it and isUnknownAttrBindAllowed is not set as well
```

{% endcode %}

Two managed-system attributes change this, both added on the **Provisioning → Managed System** attributes table:

| Managed-system attribute   | Value                           | Effect                                                                                                                                                                                                                 |
| -------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ExtraUserAttributes`      | Comma-separated attribute names | Adds exactly those names to the allow-list for user objects. Prefer this: it is explicit and it changes nothing about any other attribute.                                                                             |
| `isUnknownAttrBindAllowed` | `True`                          | The connector attempts to bind *every* unrecognised attribute it receives, logging `isUnknownAttrBindAllowed flag is set - appending 'myCustomAttr' attribute with 'somevalue' value to additional object attributes`. |

{% hint style="warning" %}
`isUnknownAttrBindAllowed` applies to everything the connector does not recognise, including a value you did not intend to send. An attribute that arrives with an empty name is appended with an empty name, and the operation then fails on a null reference inside the connector. If you turn the flag on, read the whole request body back from the `provisionconnectorrequest` index and confirm that every attribute in it has a name.
{% endhint %}

The connector considers an extra attribute at all only when its operation is `ADD` or `REPLACE` and its value is not blank. A `NO_CHANGE` value is skipped before the allow-list is even consulted, which is why a row that emits `NO_CHANGE` looks present in the captured request and absent in the directory. Compare the operation on each value in the simulation capture against what the directory shows.

## The policy-map script contract

Every Groovy source script extends one class. Copy the declaration exactly — the generic parameter is what makes `perform` receive a typed diff.

{% code overflow="wrap" expandable="true" %}

```java
public abstract class AbstractIPolicyMapGroovy<DIFF extends AbstractProvisionObjectDiff> {

    public final void init(String managedSysId);

    public abstract boolean isPerform(DIFF diffObject);

    public abstract void perform(ConnectorAttribute attribute, DIFF diffObject)
            throws BasicDataServiceException;

    public void getActualValue(ConnectorAttribute attribute, DIFF diffObject)
            throws BasicDataServiceException;   // default: delegates to perform()
}
```

{% endcode %}

Your script extends `AbstractIPolicyMapGroovy`, imported as `org.openiam.api.connector.groovy.AbstractIPolicyMapGroovy`. For a user map the type parameter is `ProvisionUserObjectDiff`; for a group map, `ProvisionGroupObjectDiff`.

* `init(String managedSysId)` is `final` and OpenIAM calls it before every evaluation. It stores the managed system ID and autowires your instance — which is why `@Autowired` fields in your script get populated. Do not call it yourself and do not try to override it.
* `isPerform(diff)` decides whether the row runs at all on a modify. It is skipped entirely when the operation is processing all fields, so a row can run even when your `isPerform` would have said no. Return `true` for an `ADD` and otherwise test the specific diffs your `perform` reads — that is the whole convention the shipped scripts follow.
* `perform(attribute, diff)` adds values to the passed-in `ConnectorAttribute`. It returns nothing; adding no values means "send no value for this attribute".
* `getActualValue(attribute, diff)` is the reconciliation-side read. The default implementation just calls `perform`. Override it when computing the *desired* value and reading the *current* value are different operations — `AbstractPrincipalGenerator` does exactly this, generating a new unique login in `perform` but returning the existing login with operation `NO_CHANGE` in `getActualValue`.

### Injected fields

Seven protected members are available without declaring anything. Six are `@Autowired` beans; the seventh is set by `init`.

| Field                    | Type                                             | Use                                                                                                                         |
| ------------------------ | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `ctx`                    | `org.springframework.context.ApplicationContext` | Escape hatch for any other bean.                                                                                            |
| `counterRabbitMQService` | `CounterRabbitMQService`                         | Atomic named counters — `incrementAndGet(name)`. This is how login-uniqueness suffixes are generated.                       |
| `loginMQService`         | `LoginMQService`                                 | Identity lookups and `loginExists(login, managedSysId)`; also password decryption.                                          |
| `propertyValueService`   | `PropertyValueCache`                             | Cached system properties, including `getDefaultManagedSysId()`.                                                             |
| `mapper`                 | `CustomJacksonMapper`                            | JSON serialisation with OpenIAM's configured modules.                                                                       |
| `userMQService`          | `UserRabbitMQService`                            | Loading users and their collections when the diff does not carry what you need.                                             |
| `managedSysId`           | `String`                                         | The managed system this evaluation is for. Essential for any row that must filter by system — group DNs, per-system logins. |

For a `PRINCIPAL` row, extend `AbstractPrincipalGenerator` instead. It adds `getManageSystemId()` (falls back to the default managed system), `count(String)` and `protected boolean loginExistsV4211(String)` — the odd name is deliberate, kept so pre-4.2.1.1 scripts that declared their own `private loginExists` still compile.

{% hint style="warning" %}
The base class is annotated `@Slf4j` on the processor side but not here, and no logger is inherited. Declare your own — every shipped script does, with `private static final Log log = LogFactory.getLog("MyScript")` — or use `println`.
{% endhint %}

### Instance lifecycle: one cached instance per script

OpenIAM does **not** create a fresh script object per operation. Each policy-map script is compiled once and the resulting instance is cached and reused. The cache lives on a single long-lived service, so **one instance of each script serves every user, every managed system and every thread on that node.**

Two consequences that are not visible in any shipped script:

* **A field on your script is shared state.** Assign a user ID, a login, a DN or any other per-user value to a field in `isPerform` and read it back in `perform`, and two provisioning operations running at the same time will read each other's values. The symptom is one user's value appearing on a different user, only under concurrent load, with a correct audit diff and no error anywhere. Keep per-user data in local variables and read it from the `diffObject` parameter. A field is safe only for a constant — an attribute name, a metadata type ID, a logger.
* **`managedSysId` is written on that shared instance too**, immediately before each evaluation. Read it inside `isPerform` and `perform`; never copy it into another field and never use it to memoise anything.

{% hint style="warning" %}
The cache is evicted on a timer, not on save. The interval is `org.openiam.idm.cache.evict.rate`, which ships as `180000` — three minutes. **You cannot force the eviction or observe it from the Webconsole.** After editing a policy-map script, allow up to three minutes per IDM node before testing, and do not conclude that the edit had no effect until that window has passed. If you need the interval changed, it is a deployment property — ask the deployment owner, and see the [Installation Guide](/installation/installation.md).
{% endhint %}

### Which diff fields can be null

`ProvisionUserObjectDiff` builds every scalar diff unconditionally in its constructor, so `getStatusDiff()`, `getSecondaryStatusDiff()`, `getFirstNameDiff()` and every other `StringDiff`, `DateDiff` or enum diff are always non-null — a null check on them is dead code. Three collections behave differently :

| Field                   | When it is `null`                                                                                                                      |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `getAttributeDiffs()`   | The incoming user has no user attributes at all. It is assigned only inside `if (MapUtils.isNotEmpty(getLeft().getUserAttributes()))`. |
| `getSupervisorsDiff()`  | The incoming user has no supervisors.                                                                                                  |
| `getSubordinatesDiff()` | The incoming user has no subordinates.                                                                                                 |

Everything else — `getPhoneDiffs()`, `getAddressDiffs()`, `getEmailDiffs()`, `getLoginDiffs()`, `getUserRoleDiffs()`, `getUserGroupDiffs()`, `getUserResourceDiffs()`, `getUserAffiliationDiffs()` — is an empty set, never `null`, when the incoming collection is empty.

A row that keys off a custom attribute therefore has to use safe navigation:

{% code overflow="wrap" %}

```groovy
@Override
boolean isPerform(ProvisionUserObjectDiff diffObject) {
    return diffObject.getStatusDiff().isDirty() ||
            diffObject.getAttributeDiffs()?.any({
                "employmentType".equalsIgnoreCase(it.getValue()?.getName()) && it.isDirty()
            })
}
```

{% endcode %}

Without the `?.`, that `isPerform` throws `NullPointerException` for any user who happens to have no attributes. The engine catches it at `log.error` as `Internal error`, the row is skipped, and the attribute is silently absent from that one user's request while every other user works.

Two more properties of the diff change how `isPerform` has to be written: every collection diff is built from the **incoming** object only, so a removal by omission is invisible to a policy-map script, and an element whose operation is `null` never reaches the diff at all. Both are set out in [Entitlements: the diff only sees what you send](/administration-guide/lifecycle/provisioning-processors/provisioning-processor-reference.md#entitlements-the-diff-only-sees-what-you-send).

## A complete password-generation script

This is `provision/primary/primaryRandomPassword_1.0.groovy`, verbatim. Point a `PASSWORD` row at it when the target should get a random password that satisfies the OpenIAM password policy rather than a password the requester chose.

{% code overflow="wrap" lineNumbers="true" expandable="true" %}

```groovy
package org.openiam

import org.openiam.api.connector.groovy.AbstractIPolicyMapGroovy
import org.openiam.api.connector.model.ConnectorAttribute
import org.openiam.api.connector.model.StringOperationalConnectorValue
import org.openiam.base.AttributeOperationEnum
import org.openiam.base.response.list.PolicyListResponse
import org.openiam.common.beans.mq.PolicyRabbitMQService
import org.openiam.idm.provisioning.diff.model.user.ProvisionUserObjectDiff
import org.openiam.idm.searchbeans.PolicySearchBean
import org.openiam.idm.srvc.pswd.service.PasswordGenerator
import org.springframework.beans.factory.annotation.Autowired

/**
 * Script generates random password using default password policy
 */
class UserPassword extends AbstractIPolicyMapGroovy<ProvisionUserObjectDiff> {

    private static final String DEFAULT_POLICY_ID = "4000"


    @Autowired
    private PolicyRabbitMQService policyRabbitMQService


    @Override
    boolean isPerform(ProvisionUserObjectDiff diffObject) {
        return AttributeOperationEnum.ADD.equals(diffObject.getStatus())
    }


    @Override
    void perform(ConnectorAttribute attribute, ProvisionUserObjectDiff diffObject) {
        PolicySearchBean psb = new PolicySearchBean()
        psb.addKey(DEFAULT_POLICY_ID)
        PolicyListResponse response = policyRabbitMQService.findBeans(psb, 0, 1)
        String password = PasswordGenerator.generatePassword(response.getList().get(0))
        attribute.addValue(new StringOperationalConnectorValue(password, diffObject.getStatus()))

    }
}
```

{% endcode %}

Three things to understand before you reuse it:

* **`"4000"` is the shipped default password policy.** `POLICY_ID` `4000` is seeded as `Default Pswd Policy` / "Default Password Policy" against policy definition `100` , and it is the policy every later migration attaches new password parameters to. The script hard-codes it because it wants *a* generator configuration that is guaranteed to exist, not the policy that applies to this particular user. If your managed system has its own password policy, replace the constant with that policy's ID; the generator takes whichever password policy you hand it.
* **`isPerform` returns true only on `ADD`.** The generated password is written once, at account creation. Subsequent modifies send no password, which is what you want: a random password regenerated on every modify would lock the account out of anything that cached it.
* The AD variant `provision/ad/unicodePwd_1.0.groovy` is the same class with a wider `isPerform`: it also fires when the login diff for `managedSysId` is dirty, so a newly added AD identity on an existing user still gets a password.

{% hint style="info" %}
The password generated here never reaches the user. If the account needs a communicated password, that is the `sendPasswordToUser` path on the provisioning request, not the policy map.
{% endhint %}

### Every value on the wire is a string

Exactly one kind of connector attribute value ships: the string one, `StringConnectorAttribute` and `StringOperationalConnectorValue`, which is what your `perform` is handed. There is no byte-array connector value, and a policy-map script cannot introduce one.

So a binary target attribute is always carried as a string and decoded by the connector, never by your script:

* **Send a password as plain text.** For Active Directory over LDAP, `unicodePwd` is a binary attribute, but the policy map's job is to hand the connector the password as a string. `unicodePwd_1.0.groovy` does exactly that — `attribute.addValue(new StringOperationalConnectorValue(password, diffObject.getStatus()))`. Base64-encoding it, or encoding it to UTF-16LE, in the script produces a value the connector then encodes a second time, and the directory rejects the result with `LdapErr: DSID-0C091211, comment: Error in attribute conversion operation, data 0, v3839`.
* **Binary payloads that really are data use a documented string form.** `ThumbnailPhoto_1.0.groovy` emits `data:image/<format>;base64,<body>`, and the PowerShell connector decodes it with `[System.Convert]::FromBase64String` before binding it. Do not decode it in the policy-map script — hand over the `data:` string as it comes.

### The seeded field-name properties

Three managed-system attributes tell a directory connector which attribute of the object it received is the password, the membership list and the manager. They ship on the `LDAP` and `ACTIVE DIRECTORY` managed systems as of 4.1.9.0, on every supported database. Read them back from the managed system's **Attributes** table:

| Managed-system attribute | Shipped value on the LDAP system | Shipped value on the Active Directory system |
| ------------------------ | -------------------------------- | -------------------------------------------- |
| `PASSWORD_FIELD_NAME`    | `userPassword`                   | `unicodePwd`                                 |
| `MEMBER_FIELD_NAME`      | `member`                         | `member`                                     |
| `MANAGER_FIELD_NAME`     | `manager`                        | `manager`                                    |

Nothing in the IDM service reads them — they exist for the connector, so what happens when they disagree with your policy map is connector-specific. Keep the `PASSWORD` row's Attribute name equal to `PASSWORD_FIELD_NAME`, and the group-membership row's Attribute name equal to `MEMBER_FIELD_NAME`, and you never depend on a connector's fallback behaviour.

{% hint style="info" %}
Support's field guidance is that the LDAP connector looks for the password attribute by the name in `PASSWORD_FIELD_NAME` first and, failing that, falls back to a name derived from the managed system's `TARGET_SYSTEM_TYPE`. That fallback is not verified on a running system: confirm it against your connector release before relying on it.
{% endhint %}

### When a `PASSWORD` row produces nothing

"There is a password to send" does not mean "the caller asked for a password operation". On a user map, OpenIAM compares the stored and the incoming encrypted password on the identity for this managed system, and sends a password only when both are present and different, or when the stored one was absent and the incoming one is not. When it returns `false`, the `PASSWORD` row is evaluated only if `isProcessingAllFields()` is true — that is, only while the identity has never been successfully provisioned.

On an already-provisioned identity, then, a modify sends no `PASSWORD` attribute at all unless the encrypted password on that identity actually changed, and that includes a modify triggered by a password re-synchronisation. Before concluding that OpenIAM sent an *empty* password, read the request in the `provisionconnectorrequest` index: an absent password attribute and a password attribute present with an empty value have different causes, and only the second can make a connector overwrite a stored credential.

## Groups: `memberOf` and `MembershipDiff`

Group membership is the one row where the *operation* on each value matters more than the value. `provision/ad/memberOf_1.0.groovy` (class `MemberOf`) iterates `diffObject.getUserGroupDiffs()`, a `Set<MembershipDiff<UserToGroupMembershipXref>>`, and for each entry:

1. Loads the group via `GroupRabbitMQService.findById(diff.getValue().entityId)`.
2. Skips it unless the group has attributes **and** `group.getManagedSysId()` equals the ID of the managed system resolved from `managedSysId` — group rows for other systems must not leak into this request.
3. Walks the group's attributes for one named `dn` (case-insensitively) and uses its first value as the membership value. **No `dn` group attribute means no membership is sent**, and the script logs `there is no DN for group` at info — once for every attribute that is not `dn`, so the message appears even for groups that do have a DN. This is the single most common cause of "OpenIAM says the user is in the group and AD disagrees".
4. Chooses the per-value operation: if `diff.getValue().getEndDate()` is non-null and in the past, the value is added with operation `DELETE`; otherwise with `diff.getStatus()`.

`isPerform` returns `true` on an `ADD`, and otherwise `diffObject.userGroupDiffs.any({ it.isDirty() })` — so on a modify with an unchanged membership set the row produces no `memberOf` attribute at all and the target keeps whatever it has.

The shipped script wraps its whole `perform` body in a `catch (Exception e)` that only logs, so a failure inside it never reaches OpenIAM's own row-level handling: it is swallowed inside the script and the attribute is sent empty. If memberships are silently wrong, raise the `MemberOf` logger before anything else, and if you clone this script, consider narrowing that `catch` so a real failure is visible.

A membership diff exposes `startDateDiff`, `endDateDiff`, `accessRightDiffs`, `entityIdDiff` and `memberEntityId`, and each of those appears in the diff token on the audit record — which is where you confirm what a membership change actually carried. A membership counts as modified only when its operation is `REPLACE`, and that operation comes straight from the incoming request.

If you use the **IDM value** source type for memberships instead of a script, OpenIAM takes a different route: it resolves group, role, resource or organization *names* — not DNs — and joins them with commas into a single string value. That is rarely what a directory wants.

## Status changes and account enablement

Every OpenIAM user carries two statuses, and which one an action changes decides whether the action reaches the managed system at all.

| Status    | Field on `ProvisionUser` | Shown in the Webconsole as | Changed by                                                |
| --------- | ------------------------ | -------------------------- | --------------------------------------------------------- |
| Primary   | `status`                 | **User Status**            | Editing the user, a joiner/leaver rule, a synchronization |
| Secondary | `secondaryStatus`        | **Account Status**         | The Enable and Disable provisioning operations            |

**User Status** shows the primary status and **Account Status** the secondary one; both draw their values from the same set, and **that set has no `ENABLED` value.** An account that has been enabled therefore never reads "Enabled" anywhere — see the next section for what it does read.

### What Enable and Disable actually change

Enable and Disable reload the stored user, set the **secondary** status, and only then build the diff:

| Operation | Secondary status set | Resulting secondary status |
| --------- | -------------------- | -------------------------- |
| Disable   | `DISABLED`           | `DISABLED`                 |
| Enable    | `null`               | cleared                    |

Neither touches the primary status. That is also why an enabled account shows an **empty** Account Status: Enable clears the secondary status to `null`, and there is no enum value meaning "enabled".

### Why the target does not follow

`provision/ad/userAccountControl_1.0.groovy` returns `diffObject.getStatusDiff().isDirty() || diffObject.getSecondaryStatusDiff().isDirty()` from `isPerform`. In `perform` it emits `514` (`UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE`) whenever the secondary status is `DISABLED`, or the primary status is anything other than `ACTIVE` or `PENDING_INITIAL_LOGIN`, and `512` otherwise. It logs its own output at info as `[userAccountControl]: 514`.

{% hint style="danger" %}
**The PowerShell variant does not react to Disable at all.** `provision/ad/powershell/enabled_1.0.groovy` (class `Enabled`) has the same `isPerform` — `statusDiff.isDirty() || secondaryStatusDiff.isDirty()` — but its `perform` reads *only* the primary status, and only for five values: it emits `"false"` for `DECEASED`, `TERMINATED`, `LEAVE_OF_ABSENCE`, `LEAVE_WITH_PAY` and `LEAVE`, and `"true"` for everything else. Secondary status is never consulted. So on a managed system using the PowerShell connector, the Disable operation sets secondary status to `DISABLED`, the row fires because that diff is dirty, and the script sends `enabled = "true"` — the account stays enabled. The script prints its decision as `AD user Status: true ====`. If you need the Disable operation to disable a PowerShell-connected account, replace this script with one that tests `getSecondaryStatusDiff().getValue()` for `DISABLED`, as `userAccountControl_1.0.groovy` does.
{% endhint %}

Two more failure modes follow directly, and both look like "the admin action did nothing". Setting the user's status back to `ACTIVE` while the secondary status is still `DISABLED` makes `statusDiff` dirty, so the row fires, takes the `DISABLED` branch again and sends `514` a second time — only the **Enable** operation clears secondary status. And disabling a user whose secondary status is already `DISABLED` leaves both status diffs clean, so `isPerform` returns `false`, no value is produced, and the operation still reports success. Grep the IDM log for `[userAccountControl]:` to tell "no value was computed" from "a value was computed and the target rejected it"; the two need completely different fixes.

{% hint style="danger" %}
Widening the enablement row's `isPerform` makes unrelated edits disable accounts. A row whose `isPerform` also tests, say, `getAttributeDiffs()` for a rehire flag will recompute `userAccountControl` whenever an unrelated operation happens to touch that attribute — and `perform` has no memory of what the target currently holds, so it recomputes purely from the two statuses and overwrites the directory. Keep this row's `isPerform` to `statusDiff` and `secondaryStatusDiff`, and put any extra condition in `perform` where it can only change the value, not whether the row runs.
{% endhint %}

If you want **Account Status** to read `Active` rather than being blank, the value has to be set explicitly on the incoming user — in practice a pre-processor that sets the secondary status to `ACTIVE`; see [Mutating the ProvisionUser safely](/administration-guide/lifecycle/provisioning-processors/provisioning-processor-reference.md#mutating-the-provisionuser-safely). `ACTIVE` is not `DISABLED`, so `userAccountControl_1.0.groovy` still emits `512`, but any custom script that tests `getSecondaryStatus() == null` as a proxy for "enabled" stops working. Audit your own scripts for that test before making the change.

## Null and multi-value semantics

Multi-value is native: `ConnectorAttribute` holds a list, and `addValue` may be called repeatedly. `objectclass_1.0.groovy` is the clearest example — four `addValue` calls producing `top`, `person`, `user`, `organizationalPerson`. Each value carries its own operation, which is how one `memberOf` attribute can add two groups and delete a third in the same request.

Clearing a value on modify is where the three source types diverge, and the difference is not configurable:

| Source type  | Source value cleared in OpenIAM                                                                                                                                                                                                                     | What the connector receives                                           |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| Groovy       | Whatever the script does. The shipped `title_1.0.groovy` and `telephoneNumber_1.0.groovy` explicitly `addValue(new StringOperationalConnectorValue("", diffObject.getStatus()))` in their else branch — an empty value with the modify's operation. | The attribute, with an empty value.                                   |
| IDM value    | The row is abandoned with `value is null or hasn't changed - nothing to put`, logged at debug level in the provisioning service log.                                                                                                                | Nothing. The attribute is omitted and the target keeps its old value. |
| Static value | Not applicable — the value does not come from the user.                                                                                                                                                                                             | The literal, always, with operation `ADD`.                            |

So: **an IDM-value row can never clear a target attribute.** If clearing matters — a leaver whose `telephoneNumber` must disappear from the directory — the row has to be Groovy, and the script has to emit the empty value deliberately. The omission is logged only at debug, as `Could not put value for attribute map <attribute name>. Cause:<message>`.

An empty-valued `ConnectorAttribute` from a Groovy script is *not* the same as no attribute: if `perform` added nothing and the row has a `DEFAULT_VALUE`, the engine substitutes the default; if it added nothing and there is no default, `processGroovy` returns an attribute with an empty value list and `getConnectorAttribute` lets it through. Whether the target interprets an empty value as "delete" is connector- and schema-specific — for LDAP-family systems it is, for others confirm against your connector's release notes.

## Advanced symptoms

These are the failures that come from the behaviour on this page rather than from the map's own configuration. The everyday ones are on [Build a policy map](/administration-guide/lifecycle/policy-map.md#when-it-goes-wrong).

| Symptom                                                                                                                                                                                                                                  | Cause                                                                                                                                                                         | Fix                                                                                                                                                        |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ou` never sent                                                                                                                                                                                                                          | `ou_1.0.groovy` still holds the shipped placeholder `OU_METADATA_TYPE_ID`.                                                                                                    | Replace the constant with your OU role metadata type ID.                                                                                                   |
| One user's value appears on a different user, only under concurrent load, with a correct audit diff                                                                                                                                      | Per-user state held in a field of a policy-map script. One instance of each script is cached and shared across all users, managed systems and threads.                        | Move per-user data into local variables and read it from `diffObject`.                                                                                     |
| `Internal error` at error level for one user only; the attribute is missing from that user's request and present for everyone else                                                                                                       | `isPerform` or `perform` dereferenced `getAttributeDiffs()`, `getSupervisorsDiff()` or `getSubordinatesDiff()`, which are `null` when the incoming user has none of them.     | Use safe navigation. See [Which diff fields can be null](#which-diff-fields-can-be-null).                                                                  |
| The same script is on two systems' maps and produces a value on one, nothing on the other                                                                                                                                                | The row's **Object type** differs between the maps. A `PRINCIPAL` row never runs its script — the engine takes the value from the identity's login and calls `mergeIdentity`. | Give the row the same Object type on both maps, or move it off `PRINCIPAL`.                                                                                |
| Admin action **Disable** reports success and the target account stays enabled                                                                                                                                                            | Either secondary status was already `DISABLED`, so no status diff is dirty, or the enablement row is `enabled_1.0.groovy`, which never reads secondary status.                | See [Status changes and account enablement](#status-changes-and-account-enablement).                                                                       |
| Setting the user's status back to `ACTIVE` leaves the target account disabled                                                                                                                                                            | The primary status changed but the secondary status is still `DISABLED`, and `userAccountControl_1.0.groovy` reads the secondary status.                                      | Run the **Enable** operation, which is the only thing that clears secondary status.                                                                        |
| An account is disabled in the target after a change that had nothing to do with status                                                                                                                                                   | The enablement row's `isPerform` tests a diff other than `statusDiff`/`secondaryStatusDiff`, so an unrelated edit makes it recompute from status and overwrite the directory. | Narrow `isPerform` to the two status diffs.                                                                                                                |
| Custom attribute is in the connector request and absent from Active Directory; debug shows `Ignoring 'myCustomAttr' attribute for modifying user because no special rule defined for it and isUnknownAttrBindAllowed is not set as well` | The name is not on the connector's allow-list for that object type.                                                                                                           | Add it to the managed-system attribute `ExtraUserAttributes`. See [Rows the Active Directory connector drops](#rows-the-active-directory-connector-drops). |

## Related

* [Build a policy map](/administration-guide/lifecycle/policy-map.md) — the task, and the everyday failures.
* [Attach a provisioning processor](/administration-guide/lifecycle/provisioning-processors.md) — the hooks that mutate the `ProvisionUser` a policy map then reads.
* [Configure provisioning](/administration-guide/lifecycle/configure-provisioning.md) — the managed-system settings that decide whether the object built here is ever sent.
* [Configure reconciliation](/administration-guide/lifecycle/configure-reconciliation.md) — the same kind of attribute rows read in the opposite direction.
* [Shipped Groovy scripts](/administration-guide/reference/shipped-groovy-scripts.md) — the full inventory of scripts to start from.
* [Enumerated values](/administration-guide/reference/enums.md) — the managed-system attribute names and the user statuses the enablement row tests.
* [Simulation mode](/administration-guide/lifecycle/simulation-mode.md) — holding the generated object for review before it is sent.

*Applies to OpenIAM 2026.8.1. Last reviewed 7 August 2026.*


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.openiam.com/administration-guide/lifecycle/policy-map/policy-map-reference.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
