Skip to content

Identity Mapping — Operator Guide

See also: MULTI-PROVIDER-IDP.md For browsing IDP user/group directories from the people picker, see MULTI-PROVIDER-IDP.md and PEOPLE-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)

  1. 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
}
  1. Restart Cesivi Server to load the script into the runspace pool.

  2. 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

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