Omada IGA Crawler — Data Model Reference¶
Overview¶
The Omada IGA crawler pulls data directly from Omada's OData 4.0 REST API (/odata/dataobjects and /odata/builtin) and pushes it into Identity Atlas via the Ingest API.
Key characteristics:
- Always performs a full sync (Omada has no delta API; SyncMode is accepted for dispatcher compatibility but ignored)
- Registers every Omada connected system as a separate Identity Atlas System
- Uses configurable type mappings, context object types, and resource category mapping
- Supports on-premise and Omada Cloud deployments
Authentication Methods¶
Configure authMethod in the crawler config. All methods work for both on-premise and cloud.
| Method | Required fields | Description |
|---|---|---|
BasicAuth |
username, password |
HTTP Basic Auth — most common for on-premise |
FormCookie |
username, password |
POST to /api/authenticate, capture session cookie |
OAuth2CC |
clientId, clientSecret, tokenEndpoint |
OAuth2 client credentials (app registration) |
OAuth2ROPC |
username, password, clientId, clientSecret, tokenEndpoint |
OAuth2 resource owner password credentials |
ApiToken |
apiToken |
Static Bearer token (no refresh) |
CookieString |
cookieString |
Pre-built cookie string — use for Omada Cloud when only browser/PS session cookies are available |
CookieString (cloud): Paste the oisauthtoken value from your browser DevTools or from OmadaWeb.PS. A bare token value is automatically prefixed with oisauthtoken=. A valid name=value cookie string is sent as-is. Multi-cookie strings (ASP.NET_SessionId=x; Auth=y) are also supported unchanged.
URL and Service Root¶
Configure baseUrl as either:
- The OData service root: https://tenant.omada.cloud/odata/dataobjects (explicit)
- The server root: https://tenant.omada.cloud/ (auto-appended with /odata/dataobjects)
The crawler uses System.Uri to normalise the URL and derives the Builtin service URL automatically:
DataObjects: https://tenant.omada.cloud/odata/dataobjects
Builtin: https://tenant.omada.cloud/odata/builtin
The $metadata endpoint (used for entity-set discovery) is fetched at startup as a non-blocking diagnostic. If it fails (e.g. HTTP 500 on some cloud instances), all phases still attempt to run.
OData Endpoints Used¶
| Service | Base URL | Entity sets used |
|---|---|---|
| DataObjects | {baseUrl} |
System, Orgunit, Identity, User, Resource, Resourceassignment, Contextassignment, Employment, Usergroup, Country, Job_titles, + configured context types |
| Builtin | {builtinBaseUrl} (derived from DataObjects URL) |
CalculatedAssignments |
Pagination: $top=N&$skip=M offset paging. Stops on empty page (not short page — Builtin returns variable-size pages). Default page size: 100 (Builtin CRA: 1000). CRA pages are streamed one-at-a-time to avoid OOM on large datasets.
Configurable Settings¶
contextObjectTypes (array)¶
Which Omada entity sets to sync as Identity Atlas Contexts. Each entry:
| Field | Required | Description |
|---|---|---|
entitySet |
Yes | Omada entity set name (case-sensitive, from $metadata) |
contextType |
No | Identity Atlas contextType label (defaults to entitySet) |
identityField |
No | Field on Identity entity that references this context type, used to create ContextMembers automatically |
Default:
Well-known identity→context field mappings (implicit fallback):
| Identity field | Context entity set |
|---|---|
| OUREF | Orgunit |
| COUNTRY | Country |
| BUILDING | Building |
| BUSINESSUNIT | Businessunit |
| COSTCENTER | Costcenter |
| DIVISION | Division |
| JOBTITLE_REF | Job_titles |
| LOCATION | Location |
| SUBAREA | Subarea |
resourceCategoryMapping (array)¶
Maps Omada ROLECATEGORY to Identity Atlas resourceType. First matching entry wins; leave category blank for the default/catch-all (must be last).
Default:
| ROLECATEGORY | Identity Atlas resourceType |
|---|---|
| Role | BusinessRole |
| Permission | Resource |
| (blank = default) | Resource |
typeMappings (object, overrides defaults)¶
| Key | Maps | Default examples |
|---|---|---|
identityTypeToIdentityAtlas |
IDENTITYTYPE.Value → principalType | Employee→User, Contractor→ExternalUser, Machine→ServicePrincipal |
contextTypeToIdentityAtlas |
OUTYPE.Value → contextType | OrgUnit→OrgUnit, Department→Department |
identityTypesForIdentityTable |
Which IDENTITYTYPE values go into Identities table | [Employee, Primary, Person] |
Sync Phases¶
Phase 0: Systems¶
Every System entity in Omada is registered as a separate Identity Atlas System. This drives per-system scoping of resources and assignments.
| OData source | /System?$filter=Deleted eq false |
| IA destination | ingest/systems (full sync) |
Mapping:
| Omada field | Identity Atlas field |
|---|---|
| UId | tenantId |
| DisplayName | displayName |
| (constant) | systemType = 'Omada' |
Omada Identity system (main IGA platform) → used as systemId for Contexts, Identities, Principals.
Phase 1: Contexts¶
Syncs every configured contextObjectType. Orgunit uses topological sort (parents before children via PARENTOU); all other types are flat.
| OData source | /{entitySet}?$filter=Deleted eq false (per configured type) |
| IA destination | ingest/contexts (full sync, scoped by { variant:'synced', contextType }) |
Mapping:
| Omada field | Identity Atlas field |
|---|---|
| UId | id, externalId |
| NAME or DisplayName | displayName |
| OUTYPE.Value (mapped) | contextType (Orgunit only; others use config value) |
| PARENTOU.UId | parentContextId (Orgunit only) |
| (constant) | variant = 'synced', targetType = 'Identity' |
Phase 2: Identities¶
Only person-type identities (IDENTITYTYPE in identityTypesForIdentityTable) are stored in the Identities table.
| OData source | /Identity?$filter=Deleted eq false |
| IA destination | ingest/identities (full sync) |
Column mapping:
| Omada field | Identities column |
|---|---|
| UId | id, externalId |
| FIRSTNAME + LASTNAME | displayName, givenName, surname |
| EMPLOYEEID | employeeId |
| JOBTITLE | jobTitle |
| COMPANY.DisplayName | companyName |
| CITY | city |
| COUNTRY.DisplayName | country |
extendedAttributes JSON:
| Key | Source |
|---|---|
| identityType | IDENTITYTYPE.Value |
| identityCategory | IDENTITYCATEGORY.Value |
| identityStatus | IDENTITYSTATUS.Value |
| identityId | IDENTITYID |
| oisId | OISID |
| email2 | EMAIL2 |
| zipCode | ZIPCODE |
| validFrom / validTo | VALIDFROM / VALIDTO |
| riskScore | RISKSCORE |
| riskLevel | RISKLEVEL.DisplayName |
| manager | MANAGER[].DisplayName (joined '; ') |
| identityOwner | IDENTITYOWNER.DisplayName |
| explicitOwners | EXPLICITOWNER[].DisplayName (joined '; ') |
| ouRefId / ouRefName | OUREF.UId / .DisplayName |
| countryId / countryName | COUNTRY.UId / .DisplayName |
| locationId | LOCATION.UId |
| buildingId | BUILDING.UId |
| businessUnitId | BUSINESSUNIT.UId |
| costCenterId | COSTCENTER.UId |
| divisionId | DIVISION.UId |
| subAreaId | SUBAREA.UId |
| jobTitleRefId / jobTitleRef | JOBTITLE_REF.UId / .DisplayName |
| company | COMPANY.DisplayName |
Phase 3: Accounts (Principals)¶
Omada User entities (accounts) become Identity Atlas Principals. principalType is resolved from the linked Identity's IDENTITYTYPE.
| OData source | /User?$filter=Deleted eq false |
| Join key | User.IDENTITYREF.IDENTITYID = Identity.IDENTITYID |
| IA destination | ingest/principals (full sync, batched per principalType) |
Mapping:
| Omada field | Principals column / extendedAttributes |
|---|---|
| UId | id, externalId |
| FIRSTNAME + LASTNAME | displayName |
| JOBTITLE | jobTitle |
| (from linked Identity) | principalType (mapped via typeMappings) |
| (constant) | accountEnabled = true |
| UserName | extendedAttributes.userName |
Phase 4: IdentityMembers¶
Links each Omada User account to its Identity. Only person-type identities (FK guard).
| IA destination | ingest/identity-members (full sync) |
| Source | Target field |
|---|---|
| Identity.UId | identityId |
| User.UId | principalId |
| (constant) | accountType = 'Primary' |
Phase 5: Context Members¶
Three sources combined, deduplicated on (contextId, memberId). Stored with memberType='Identity' and memberId=Identity.UId so the context detail page query (WHERE memberType = context.targetType) finds members correctly.
| IA destination | ingest/context-members (full sync, deduped) |
Source 1 — Contextassignment entity:
Source 2 — Direct Identity reference fields (OUREF, COUNTRY, JOBTITLE_REF, …):
Source 3 — Employment entity (IDENTITYREF → OUREF, if entity set available):
All records: memberType='Identity', addedBy='sync'. FK guard: memberId must be in Identities table.
Phase 6: Resources¶
Resources grouped by their connected system (SYSTEMREF) for correct per-system scoped-delete.
| OData source | /Resource?$filter=Deleted eq false |
| Pre-fetch | /Usergroup (for USERGROUPREF name lookup) |
| IA destination | ingest/resources (full sync, per-system batches) |
| Type mapping | ROLECATEGORY.Value → resourceType via resourceCategoryMapping |
Column mapping:
| Omada field | Resources column |
|---|---|
| UId | id, externalId |
| NAME or DisplayName | displayName |
| DESCRIPTION | description |
| ROLECATEGORY (mapped) | resourceType |
| RESOURCESTATUS | enabled (false if Inactive/Disabled/Deleted) |
extendedAttributes JSON:
| Key | Source |
|---|---|
| resourceCategory | ROLECATEGORY.Value |
| resourceType | ROLETYPEREF.DisplayName |
| roleFolder | ROLEFOLDER.DisplayName |
| skipProvisioning | SKIPPROVISIONING (bool) |
| userGroupName | USERGROUPREF → Usergroup.DisplayName lookup |
| explicitOwner | EXPLICITOWNER[].DisplayName (joined '; ') |
| manualOwner | MANUALOWNER[].DisplayName (joined '; ') |
| omadaSystem | SYSTEMREF.DisplayName |
Phase 7: Entitlements¶
Role nesting extracted from Resource.CHILDROLES — no separate endpoint exists.
| Source | $AllResources (pre-fetched in Phase 6), Resource.CHILDROLES collection |
| IA destination | ingest/resource-relationships (full sync, scope {relationshipType:'Contains'}) |
| Field | Value |
|---|---|
| parentResourceId | Resource.UId |
| childResourceId | CHILDROLES[n].UId |
| relationshipType | 'Contains' |
Phase 8: Assignments¶
Two sources combined for comprehensive coverage:
Source A — Resourceassignment (role/permission assignments, IGA-governed)¶
| OData source | /Resourceassignment?$filter=Deleted eq false |
| Filter | ROLEASSNSTATUS.Value in ['Active', 'Pending'] |
| Fan-out | Each identity assignment → one record per linked User account (via $IdentityUidToUserUids) |
| IA destination | ingest/resource-assignments (full sync, per-system, assignmentType='Governed') |
| Deduplication | YES — on principalId|resourceId per system |
| Omada field | ResourceAssignments column / extendedAttributes |
|---|---|
| ROLEREF.UId | resourceId |
| (fanned via Identity.UId) | principalId (User.UId) |
| (constant) | assignmentType = 'Governed' |
| VALIDFROM / VALIDTO | extendedAttributes.validFrom / validTo |
Source B — CalculatedAssignments (account provisioning, all systems)¶
| OData source | /odata/builtin/CalculatedAssignments?$filter=Status eq true&$expand=Identity,Resource,System,ResourceType |
| Page size | 1000 (server default; stops on empty page) |
| IA destination | ingest/principals (delta) + ingest/identity-members (delta) + ingest/resource-assignments (full sync, per-system) |
Two sub-tracks based on which Omada system the account belongs to:
Omada Identity accounts (AccountName → $UserNameToUid lookup):
principalId = existing User.UId
assignmentType = Governed (IsManaged=true) or Direct (IsManaged=false)
Connected-system accounts (Salesforce, AD, etc. — AccountKey as new Principal id):
New Principal:
AccountKey → id
AccountName → externalId
FIRSTNAME/LASTNAME → displayName
EMAIL → email
(constant) → principalType = 'User'
Status → accountEnabled
New IdentityMember:
Identity.UId → identityId (FK guard: person types only)
AccountKey → principalId
ResourceType.DisplayName → accountType
ResourceAssignment:
Resource.UId → resourceId
AccountKey → principalId
IsManaged=true → assignmentType = 'Governed'
IsManaged=false → assignmentType = 'Direct'
extendedAttributes on CRA assignments:
| Key | Source |
|---|---|
| validFrom / validTo | ValidFrom / ValidTo |
| status | 'Enabled' (Status=true) or 'Disabled' |
| reasons | Reasons[].Description (joined '; ') |
| accountType | ResourceType.DisplayName |
| accountName | AccountName |
Entity Relationship Summary¶
Systems (58 connected systems, 1 per Omada System entity)
└─ Resources (grouped by systemId)
└─ Principals (grouped by systemId — Omada accounts + CRA-derived accounts)
└─ ResourceAssignments (grouped by systemId)
Identities (person-type identities only: Employee, Primary, Person)
└─ IdentityMembers → Principals (Omada User accounts)
└─ IdentityMembers → Principals (connected-system accounts, from CRA)
└─ ContextMembers → Contexts (memberType='Identity')
Contexts (OrgUnit hierarchy + configured flat types e.g. JOB_TITLES)
└─ ContextMembers (from Contextassignment + direct refs + Employment)
Resources → ResourceRelationships (Contains, from CHILDROLES)
Principals → ResourceAssignments → Resources
Shared State Variables (inter-phase dependencies)¶
| Variable | Built in phase | Used in phases |
|---|---|---|
$AllOmadaSystems |
Systems | Contexts, Resources, Assignments (label lookups) |
$OmadaSystemMap (UId→systemId) |
Systems | Resources, Assignments |
$OmadaIdentitySystemUId |
Systems | Assignments (distinguishes Omada vs connected-system accounts) |
$SyncedContextIds |
Contexts | ContextMembers (filter out unknown CA_CONTEXT refs) |
$AllIdentities |
Identities | Accounts (principalType lookup), ContextMembers (direct refs) |
$IdentityLookup (IDENTITYID→{uid,type}) |
Identities | Accounts |
$IdentityUidInIdentitiesTable |
Identities | ContextMembers, Assignments (FK guard) |
$AllAccounts |
Accounts | IdentityMembers, ContextMembers, Assignments |
$UserNameToUid (UserName→UId) |
Accounts | Assignments (CRA Omada-account lookup) |
$IdentityUidToUserUids (IdentityUId→[UserUIds]) |
Accounts | ContextMembers, Assignments (fan-out) |
$UserGroupMap (UId→DisplayName) |
Resources (pre-fetch) | Resources |
$AllResources |
Resources | Entitlements |
Error Handling¶
| Scenario | Behaviour |
|---|---|
| Ingest HTTP 429 / 5xx | Retry with exponential backoff: 2s, 4s, 8s, 16s, 32s (max 5 retries) |
| Ingest HTTP 409 (job cancelled) | Throw immediately — aborts crawl |
| Ingest HTTP 4xx (non-429) | Throw immediately |
OData entity set not in $metadata |
Skip with yellow warning; continue |
| CRA endpoint HTTP 404/400/501 | Skip gracefully (module not installed) |
| Build-FGContexts 404 | Not called for Omada (Omada syncs its own contexts directly) |
JOBTITLE_REF contextId not in $SyncedContextIds |
Skip ContextMember silently |
Identity not in $IdentityUidInIdentitiesTable |
Skip IdentityMember / ContextMember (FK guard) |
Post-Sync Hooks¶
After all phases complete:
- Phase results →
POST /api/crawlers/jobs/:id/phases(per-phase timing + status) - View refresh →
POST /api/ingest/refresh-views— refreshesvw_ResourceUserPermissionAssignmentsmaterialized view, recalculatesdirectMemberCount/totalMemberCounton all Contexts, updateslastSyncDateTimeon all Systems that have synced data
Test Environments¶
On-Premise (masterdemo)¶
| Property | Value |
|---|---|
| Hostname | omada-server.example.com (→ masterdemo.example.com → 192.0.2.28) |
| Auth method | BasicAuth (DOMAIN\demouser) |
| API version | v15.0 |
| Entity sets | 25 (DataObjects) + CalculatedAssignments (Builtin) |
| Systems | 58 connected systems |
| Identities | 332 (321 person-type) |
| Contexts | 322 OrgUnits + 28 Job Titles |
| Resources | 13,220 across all systems |
| CRA records | ~37,961 active (Status=true) |
DNS: AD DNS at 192.0.2.25/192.0.2.28 resolves example.com zone. Persisted via extra_hosts in docker-compose.yml.
Cloud (yourtenant.omada.cloud)¶
| Property | Value |
|---|---|
| Base URL | https://yourtenant.omada.cloud/ (auto-normalised to /odata/dataobjects) |
| Auth method | CookieString (oisauthtoken=<token>) |
| Entity sets | 18 available (subset of on-premise; $metadata may return 500 — gracefully skipped) |
| Systems | 41 connected systems |
| Identities | 4,279 |
| Contexts | 403 OrgUnits + 277 Job Titles |
| Resources | 30,355+ across all systems |
| CRA records | 12,421+ active |
Cloud notes: The $metadata endpoint on cloud may return HTTP 500 — this is handled gracefully (all phases still run). CookieString auth sends the cookie in a Cookie request header (not via WebSession), which is required for cloud HTTPS URLs where WebSession cookie-domain matching is unreliable.