Identity Mapping — Operator Guide¶
See also: MULTI-PROVIDER-IDP.md For browsing IDP user/group directories from the people picker, see
MULTI-PROVIDER-IDP.mdandPEOPLE-PICKER-IDP.md. Identity Mapping (this doc) is for source-farm-identity → target-farm-identity translation during MIGRATION, which is a separate concern from runtime multi-provider authentication.
This guide helps SharePoint administrators decide whether they need the Cesivi Identity Mapping Bridge and, if so, how to set it up, verify it, and operate it day-to-day.
For full script-authoring API details, see IDENTITY-MAPPING-REFERENCE.md.
What It Is¶
The Identity Mapping Bridge translates source identities from a migrating SharePoint farm (Entra OIDs, AD SIDs, SP claims) into target identities in the identity system your Cesivi deployment actually uses (Keycloak users/groups, AD accounts, custom IDs).
You supply a PowerShell script. Cesivi calls it once per unique source identity during migration, caches the result, and uses the mapped identity for permission grants and user-stamped fields.
Do You Need It?¶
Probably yes if your Cesivi deployment:
| Scenario | Need bridge? |
|---|---|
| Source farm uses Entra ID / Azure AD, target uses Keycloak | Yes |
| Source farm uses on-premises AD, target uses a different AD domain | Yes (SID re-mapping) |
| Source farm users are local SP accounts, target is Keycloak | Yes |
| Source and target use the same AD domain (SIDs are identical) | No — pass-through |
| Single-tenant Entra with synced AD and matching UPNs in Keycloak | Likely no |
| You are not migrating permission grants or user fields | No |
When in doubt: run a test migration without the bridge. If user fields and permission entries look wrong in Cesivi, enable the bridge.
Quickstart (5 minutes)¶
- Copy the reference script that matches your identity topology:
# EntraID → Keycloak (reference implementation):
cp Cesivi.Examples.IdentityMapping.Keycloak/Map-Identity.ps1 \
<DataRoot>/IdentityMapping/Map-Identity.ps1
Or write your own minimal script:
# <DataRoot>/IdentityMapping/Map-Identity.ps1
function Map-Identity {
param([Parameter(Mandatory)] [Cesivi.Sdk.Identity.SourceRef] $Source)
# Example: map a known test OID to a local account
if ($Source.Value -eq '11111111-1111-1111-1111-111111111111') {
return [Cesivi.Sdk.Identity.MappedRef] @{
Target = [Cesivi.Sdk.Identity.TargetType]::KeycloakUser
TargetId = 'alice-uuid'
DisplayName = 'Alice Smith'
}
}
return $null # no mapping for everything else
}
-
Restart Cesivi Server to load the script into the runspace pool.
-
Verify it works:
bash Scripts/run-tools-smoke.sh --tool IdentityMapping
Expected output: IdentityMapping: PASS
Where the Script Goes¶
| Location | Description |
|---|---|
<DataRoot>/IdentityMapping/Map-Identity.ps1 |
Default. DataRoot is the value of Cesivi:DataRootPath in appsettings.json. |
| Any absolute path | Set Cesivi:IdentityMapping:ScriptPath in appsettings.json to an absolute path. |
{
"Cesivi": {
"IdentityMapping": {
"ScriptPath": "C:/scripts/my-Map-Identity.ps1"
}
}
}
The script is loaded once per runspace at pool startup — a server restart (or cache flush) is required to pick up changes.
Input/Output at a Glance¶
Your script receives a SourceRef and returns a MappedRef or $null:
SourceRef { Kind, Value, CustomKindTag?, Metadata? }
→ Map-Identity
→ MappedRef { Target, TargetId, DisplayName?, CustomTargetTag?, Annotations? }
OR $null (= "no mapping")
Common Kind values: EntraObjectId, EntraUserPrincipalName, ActiveDirectorySid,
GroupSid, Custom.
Common Target values: KeycloakUser, KeycloakGroup, ActiveDirectoryUser,
ActiveDirectoryGroup, EntraUser, EntraGroup, Custom.
For full type shapes and a complete authoring guide: see IDENTITY-MAPPING-REFERENCE.md.
Deployment Checklist¶
Before starting a migration run:
- [ ] Script file exists at
<DataRoot>/IdentityMapping/Map-Identity.ps1(or configured path) - [ ] Script file is readable by the Cesivi Server process user
- [ ] If the script makes network calls (e.g. Keycloak), those endpoints are reachable from the Cesivi host
- [ ] Environment variables required by the script are set in the server environment
(service unit, Windows service properties, or
appsettings.json) - [ ] Cesivi Server has been restarted after placing/editing the script
- [ ] Smoke test passes:
bash Scripts/run-tools-smoke.sh --tool IdentityMapping
Verifying It Works¶
Smoke test (recommended — no live identity provider needed)¶
bash Scripts/run-tools-smoke.sh --tool IdentityMapping
This exercises the bridge end-to-end with a stub script and confirms the runspace pool
starts, maps a known identity, caches it, and handles $null correctly — all without
contacting Keycloak or any other external system.
Consumer integration smoke (Phase δ.1)¶
For developers building or testing a MigrationTool consumer, a self-contained shim is available that fakes the MigrationTool call surface — no real MigrationTool or Keycloak required:
# Run the full identity-mapping smoke pipeline (δ.0 + δ.1, 12 tests total):
bash Scripts/tools-smoke/run-identitymapping-smoke.sh
The shim (MigrationToolImportShim in Cesivi.Server.IdentityMapping.Tests/Shim/) exercises:
- ClassifySourceRef — CSOM LoginName → SourceRef classifier (SP claim prefixes, GUID, UPN, AD SID)
- ImportUsersAsync — loop-path: one MapIdentityAsync call per user
- ImportPermissionsAsync — batch-path: one MapIdentitiesAsync call per permission list
- ImportRecycleBinDeletionAuthorsAsync — loop-path with cache flush verification
- Partial-failure non-caching (sentinel slot does not enter the cache)
- Cross-pass cache reuse (loop pass warms cache; batch pass hits it)
See CONTRACT.md §11 in _project/areas/identity-mapping/ for the full consumer surface contract.
Cache stats¶
After a real migration run, check that the bridge is being used:
curl http://localhost:5000/_admin/identity-mapping/cache-stats \
-H "Authorization: Bearer <admin-token>"
# { "hits": 8420, "misses": 311, "evictions": 0, "size": 311 }
A misses count > 0 means the script was called at least once. If hits and misses
are both 0 after a migration, either no identity-bearing content was migrated or the bridge
is not wired in.
Log lines to look for¶
Identity mapping: Map-Identity called for SourceRef { Kind=EntraObjectId, Value=... }
Identity mapping: cache hit for EntraObjectId|<value>
Identity mapping: cache miss — invoking runspace for EntraObjectId|<value>
Identity mapping: script returned null for EntraObjectId|<value> (no mapping)
Log level: Debug. Set Logging:LogLevel:Cesivi.Server.IdentityMapping to Debug in
appsettings.json to enable them.
Troubleshooting¶
Script not found¶
IdentityMappingException: script not found: <DataRoot>/IdentityMapping/Map-Identity.ps1
Check: file exists at the path, Cesivi has read permission, server was restarted after
placing the file. If you set ScriptPath manually, confirm the path is absolute.
Syntax error in script¶
The first MapIdentityAsync call after startup will throw:
IdentityMappingException: PS runspace initialization failed: ...
Test your script outside Cesivi first by dot-sourcing it in a PowerShell 7 session:
. './Map-Identity.ps1'
Type cast fails (cannot convert ...)¶
If your script returns a plain hashtable without the explicit [MappedRef] cast, Cesivi's
binder converts it automatically — as long as the key names match (Target, TargetId,
DisplayName). Check for typos in the hashtable keys.
Timeout¶
IdentityMappingTimeoutException: Map-Identity exceeded 5s for EntraObjectId|...
The script took longer than DefaultTimeoutSeconds (default 5 s). Either:
- The network call to your identity provider is slow → increase DefaultTimeoutSeconds.
- The identity provider is unreachable → check connectivity from the Cesivi host.
{
"Cesivi": {
"IdentityMapping": {
"DefaultTimeoutSeconds": 30
}
}
}
Keycloak returns 401¶
The admin token has expired. Set KEYCLOAK_CLIENT_ID + KEYCLOAK_CLIENT_SECRET env vars
so the reference script uses the client_credentials grant and auto-refreshes tokens, rather
than relying on a pre-fetched token in KEYCLOAK_ADMIN_TOKEN.
Script works manually but fails in Cesivi¶
The runspace sandbox blocks Start-Process, Invoke-Expression, Add-Type, Set-Content,
and Out-File. If your script uses any of these, it will fail with an IdentityMappingException
citing "command not found" or "access denied". Use Invoke-RestMethod and Invoke-WebRequest
for network calls; use Get-Content for file reads.
Day-2 Operations¶
After editing the script¶
The script is cached per runspace at startup. Editing the file does NOT take effect until you either restart the server or flush the runspace pool:
Flush cache (no restart):
curl -X POST http://localhost:5000/_admin/identity-mapping/flush-cache \
-H "Authorization: Bearer <admin-token>"
# { "flushed": 1234, "cacheSize": 0 }
The next call after a flush will re-initialize the runspace pool and re-load the script.
CLI equivalent:
./Cesivi identity-mapping flush-cache
Tuning the TTL¶
The cache TTL controls how long a mapped identity is trusted without re-calling the script:
{
"Cesivi": {
"IdentityMapping": {
"CacheTtlSeconds": 3600
}
}
}
Default: 3600 s (1 hour). For batch migrations (one-shot), 3600 s is usually fine — entries live for the duration of the run. For long-running sync scenarios, reduce to 300–600 s if your identity store changes frequently.
Sizing the cache¶
The LRU size cap prevents unbounded memory growth:
{
"Cesivi": {
"IdentityMapping": {
"CacheSizeLimit": 50000
}
}
}
Default: 50 000 entries (~10–50 MB depending on ID length). Increase for tenants with very
large user counts (100k+). Monitor evictions via cache-stats.
See Also¶
| Resource | Audience |
|---|---|
| IDENTITY-MAPPING-REFERENCE.md | Script authors — type shapes, sandbox model, full API |
_project/areas/identity-mapping/CONTRACT.md |
Developers integrating the bridge |
| MIGRATION_GUIDE.md | Migration overview |
Cesivi.Examples.IdentityMapping.Keycloak/ |
Reference EntraID → Keycloak script |
_docs_dev/IDENTITY-MAPPING-DEV.md |
Developers extending the bridge — runspace internals, cache architecture, extension points |