> 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/synchronize-and-import-data/import-roles-groups-and-memberships.md).

# Import roles, groups, organizations and entitlement memberships

Load roles, groups, organizations and resources from CSV, then grant memberships — in the fixed order OpenIAM requires.

## What this is, and when you need it

**OpenIAM does not import users and their entitlements in a single process.** It is two imports, in a fixed order, and getting the order wrong is the usual cause of a run that creates every user and grants nothing.

The entitlements themselves — roles, groups, organizations and resources — load through four bulk-upload configurations that ship with the product. Memberships load through a fifth synchronization whose transformation adds one named role or group per row.

This page is the order and the file shapes. The general configuration procedure is [Set up a synchronization](/administration-guide/lifecycle/synchronize-and-import-data.md), and a plain user file is [Import users from a CSV file](/administration-guide/lifecycle/synchronize-and-import-data/import-users-from-csv.md).

## At a glance

|                          |                                                                                                                                                                                                                                 |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **You will end up with** | The roles, groups, organizations or resources in the file created or updated and memberships added to existing users — the named role and group visible on the test user, and a run report with no `F` rows. Nothing is revoked |
| **Where**                | **Provisioning → Synchronization**, `/webconsole/synchronizations`; **Administration → Groovy Manager** to copy `sync/user/csv/CsvUserEntitlementsTransformationScript_1.0.groovy`                                              |
| **You need first**       | Entitlement names that are unique — a name resolving to two roles loses the grant silently — the users in the membership file already present, and Groovy Manager access to copy the transformation script                      |
| **Takes**                | 30–45 minutes for the two-row test; the full load scales with the number of rows                                                                                                                                                |
| **Reversible**           | Partly — memberships a run added are ordinary assignments you can remove, but deleting an entitlement a bulk upload created removes it from everyone who holds it                                                               |

## Before you begin

* The entitlement CSV, and the membership CSV with the four columns below.
* Confirmation that no two roles and no two groups share a name on the same managed system.
* The users in the membership file already present in OpenIAM, matched by the same immutable identifier the membership feed will use.

## Load the entitlements, then the people, then the memberships

{% stepper %}
{% step %}

### Import the entitlements first

The roles and groups have to exist before anything can be a member of them. Load them from their own CSV formats — `ent_csv_ex2` for roles and groups, `application_csv_ex3` for applications — or from the application through its connector. See [Role engineering and maintenance](/administration-guide/access/role-engineering.md#build-load-the-catalog-do-not-hand-type-it) for the entitlement header.

A membership row naming a role that does not yet exist does not fail the run: the helper simply finds nothing to add. `addUserRoleByName` logs `Role collection was empty or contained more then one element: %s` and `addUserGroupByName` logs `Group collection was empty or contained more then one element: %s`, then returns without adding anything; the shipped entitlement script prints `Role with name <name> was not found`. Note the *or more than one* half of that message: a name that resolves to **two** roles is dropped just as silently as a name that resolves to none, so duplicate role or group names lose grants without a failed record. That is why this step is not optional and not reorderable, and why entitlement names have to be unique before the membership feed runs.
{% endstep %}

{% step %}

### Make sure the people exist

A membership row for a person OpenIAM does not have cannot be repaired later in the same run. The shipped entitlement transformation returns `SKIP` for an unmatched row, which counts as a **failure**, so an assignment file run against an unknown population fails every row. Load the users first — [Import users from a CSV file](/administration-guide/lifecycle/synchronize-and-import-data/import-users-from-csv.md) — and confirm the count before going on. The full reasoning is [Users first, then assignments](#users-first-then-assignments).
{% endstep %}

{% step %}

### Import the users with their memberships

A second synchronization, **Synchronize Object = `USER`**, whose transformation both builds the user profile and creates the memberships named on each row. **Synchronization Source** is `CSV` for a file or the connector for a directory source.

Test with one test user and two membership rows first, on an inactive config with **Records per Batch** `2`.

{% hint style="warning" %}
These imports grant access, and a membership row naming a missing or duplicated entitlement is dropped without a failed record — so a run report with no `F` rows does not prove every grant landed. Count grants added against rows in the file.
{% endhint %}
{% endstep %}
{% endstepper %}

For the CSV case the file is four columns, and — as always — the header has to match what the transformation script reads:

{% code overflow="wrap" %}

```csv
USER_NAME,APPLICATION,TYPE,ENTITLEMENT_NAME
test.user,ManagedSystemName,GROUP,GroupName
test.user,ManagedSystemName,ROLE,RoleName
```

{% endcode %}

| Column             | What it carries                                                                                                                                                                                                                                                                                    |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `USER_NAME`        | The user this row is about. This is the value the match rule correlates on, so it has to be the identifier you approved as immutable — see [Match before transform](/administration-guide/lifecycle/synchronize-and-import-data/synchronization-matching-and-processing.md#match-before-transform) |
| `APPLICATION`      | The managed system the entitlement belongs to. It is what disambiguates two roles with the same name on different managed systems                                                                                                                                                                  |
| `TYPE`             | Which kind of entitlement this row grants — `GROUP` or `ROLE` in the example above                                                                                                                                                                                                                 |
| `ENTITLEMENT_NAME` | The name of the role or group to add the user to. It must already exist, from step one                                                                                                                                                                                                             |

Two structural points follow from the example, and both are easy to get wrong:

* **One row per membership, not one row per user.** `test.user` appears twice above — once for the group and once for the role. The transformation therefore runs several times for the same person, and each run must match that same existing user and add only the entitlement its own row names. Everything in [One synchronization processes the whole record](/administration-guide/lifecycle/synchronize-and-import-data/synchronization-matching-and-processing.md#one-synchronization-processes-the-whole-record) applies. The shipped script that implements exactly this shape is `sync/user/csv/CsvUserEntitlementsTransformationScript_1.0.groovy`: it reads `APPLICATION`, `TYPE` and `ENTITLEMENT_NAME` from `columnMap`, resolves the managed system by name and adds the one group or role that row names. Copy that, not the consolidated user script.
* **This grants; it does not revoke.** A membership that disappears from the file is not removed by the import. Decide separately, and deliberately, what owns removal — a business rule, a certification campaign or reconciliation — because an add-only feed asserts no authority over it.

{% hint style="info" %}
**"Bulk upload" is a synchronization, not a separate feature.** The role, group, organization and resource CSV bulk imports are four ordinary `SYNCH_CONFIG` rows — `ROLE_BULK_UPLOAD_SYNC`, `GROUP_BULK_UPLOAD_SYNC`, `ORG_BULK_UPLOAD_SYNC` and `RES_BULK_UPLOAD_SYNC`, all `SYNCH_SRC = 'CSV'`, `SYNCH_TYPE = 'FULL'`, `MATCH_FIELD_NAME = 'NAME'` and `IS_PROVISION = 'N'`, shipped since 4.0.0.0 and visible on `/webconsole/synchronizations` like any other config. So everything in [Set up a synchronization](/administration-guide/lifecycle/synchronize-and-import-data.md) — match field, batch size, return codes, the audit tree, the run report — applies to a bulk upload unchanged. If a bulk role import silently drops rows, read it as a synchronization and start with the match field.
{% endhint %}

## Verification

Open the test user and confirm the named role and group are on it, on the managed system you expected. Then count rows in the membership file against grants added: a run that reports no failures has still dropped every row whose entitlement name did not resolve, so the count is the check, not the failure total. The rest of the evidence is [the same as any synchronization](/administration-guide/lifecycle/synchronize-and-import-data.md#verification).

## When it goes wrong

| Symptom                                                                           | Cause                                                                                                                                                                                                                                                                                                               | Fix                                                                                                                        |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Every row of an entitlement feed fails, log shows `User not found in the system!` | The shipped `CsvUserEntitlementsTransformationScript_1.0.groovy` returns `SKIP` when `isNewUser` is true, and `SKIP` counts as a failure                                                                                                                                                                            | Run the users feed first. See [Users first, then assignments](#users-first-then-assignments).                              |
| A named role or group is silently never granted                                   | The name resolved to zero **or more than one** object. The log carries `Role collection was empty or contained more then one element: %s` or `Group collection was empty or contained more then one element: %s`, nothing is added, and the shipped entitlement script prints `Role with name <name> was not found` | Load the entitlements first, and make role and group names unique. A duplicate name loses the grant with no failed record. |
| Duplicate people created from a feed with several rows per person                 | A new user has no id to serialise saves on, so concurrent creates of the same person both proceed — and each unmatched row already had its own empty user object                                                                                                                                                    | Do not rely on script-side aggregation. Consolidate the feed, or ensure the users exist before the multi-row feed runs.    |

## Rollback

Untick **Active** on the membership config before changing anything. Memberships added by a run are ordinary role and group assignments: remove them from the user, or from the role's member list. Entitlements created by a bulk upload are ordinary roles, groups, organizations and resources, and deleting one removes it from everyone who holds it — confirm who holds it before you delete.

## Consolidated feed or one row per assignment

Take a records application called `Ledger` and a CSV export of user-to-role assignments from it. The export has one row per assignment, so a person holding four roles appears four times. The first question support is asked is whether the file has to be reshaped into one row per user. It does not — but the two shapes are not interchangeable, and the difference is about revocation, not about granting.

**A single consolidated record per user is a convention of the shipped user feed, not a framework requirement.** `CSVSampleTransformationScript_1.0.groovy` expects one row per person and splits `;`-separated `ROLES` and `GROUPS` columns inside that row. The shipped *entitlement* feed is deliberately the other shape: `CsvUserEntitlementsTransformationScript_1.0.groovy` handles exactly one `APPLICATION` / `TYPE` / `ENTITLEMENT_NAME` triple per row. Both ship, both work.

Multiple rows for one person accumulate rather than overwrite, which is why the per-assignment shape works at all: adding a role or group merges into the existing collection with operation `ADD`, and persistence is delta-driven — OpenIAM removes a membership only for an entry explicitly marked `DELETE`. Absence from the feed is not a delete.

|                                      | Consolidated: one row per user                                                           | One row per assignment                                     |
| ------------------------------------ | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| Grants                               | Whole intended set in one row                                                            | Accumulate across rows                                     |
| Revocation                           | Expressible — the row states the complete set, so anything absent can be marked `DELETE` | Not expressible — no row knows what the other rows contain |
| Users may be created by the same run | Yes, if the transformation builds the user                                               | No, see the ordering rule below                            |
| Shipped example                      | `CSVSampleTransformationScript_1.0.groovy`                                               | `CsvUserEntitlementsTransformationScript_1.0.groovy`       |

**Synchronization never revokes implicitly.** Nothing in the framework infers "this assignment is gone because it is absent from the file". That is the real reason a consolidated view matters: only a row that carries the person's complete intended set can be turned into a revocation, by marking the memberships that are no longer present with operation `DELETE`. With a per-assignment feed, removal has to be owned by something else entirely — a business rule, a certification campaign or reconciliation. Decide that before the first run, not after the first audit finding.

### Users first, then assignments

If the population may contain people OpenIAM does not have yet, a per-assignment feed has a hard ordering constraint. The shipped entitlement script returns `SKIP` for a row whose user did not match, printing `User not found in the system!`; `isNewUser` is true exactly when the match found nothing; and `SKIP` is counted as a **failure**. So a first run of a `Ledger` assignment file against an unknown population fails every single row and grants nothing — and the failure count in the run report is your confirmation of that, not a defect.

The order is therefore: entitlements exist, then users exist, then assignments. One run is enough **only** when the feed is consolidated *and* its transformation creates the user. With one row per assignment a single run cannot be made reliable, because every unmatched row is handed its own fresh empty user object — four rows for one new person are four candidate people, not one.

Three ways to consolidate are supported:

| Approach                          | How                                                                                                                                                | When                                                           |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| **`CUSTOM` source adapter**       | A Groovy adapter selected in **Custom Adapter Script** returns the list of rows itself, so it can read the raw export and emit one row per person  | The file cannot be changed and the source is not a database    |
| **`RDBMS` query that aggregates** | Put the `GROUP BY` and string aggregation in **SQL Query**; OpenIAM builds one row object per result row, so the query's shape is the feed's shape | The source is a database, or you can stage the export into one |
| **Reshape the file**              | Produce one row per user with a multi-value column, and split it in the transformation as the shipped user script does                             | You control the export                                         |

For the `Ledger` example: if you own the export, emit one row per user with a `ROLES` column and follow `CSVSampleTransformationScript_1.0.groovy`. If you do not, run the users feed first and the per-assignment feed second, and accept that the second feed grants only — then name what owns revocation.

## Related

* [Set up a synchronization](/administration-guide/lifecycle/synchronize-and-import-data.md) — the configuration procedure, the run evidence and the full failure catalogue.
* [How a synchronization matches and processes a record](/administration-guide/lifecycle/synchronize-and-import-data/synchronization-matching-and-processing.md) — why script-side aggregation cannot merge a per-assignment feed.
* [Role engineering and maintenance](/administration-guide/access/role-engineering.md) — the entitlement catalogue these files load.
* [Import users from a CSV file](/administration-guide/lifecycle/synchronize-and-import-data/import-users-from-csv.md) — the feed that has to run first.

*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/synchronize-and-import-data/import-roles-groups-and-memberships.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.
