The Ontology, Visually¶
Identity Atlas keeps its data in PostgreSQL tables. The ontology describes what that data means: which kinds of things exist (an account, a group, a person, a team), how they may be connected, and which values a field may hold. It is the grammar of the data model, written in a standard format that other tools can read.
This page shows what that looks like: first the overall picture, then one small team as real data, then a few lines of the schema file itself.
Download the schema file
The ontology is one file, core.ttl
(view on GitHub). It is
written in Turtle, the readable text format for RDF, and uses the OWL 2 vocabulary for
classes and properties. Protégé, WebVOWL, Apache Jena, rdflib and any other RDF/OWL tool can open
it; see Open it in a tool.
The worked example on this page is a file too:
examples/team.ttl.
The big picture¶
Everything in Identity Atlas is one of five kinds of node, connected by five kinds of edge.
A solid arrow is an edge table: a row in it connects two nodes, and carries fields of its own (an
assignment has an assignmentType, for example). A dotted arrow is a plain reference from one node
to another (a column such as managerId).
flowchart LR
Context["Context<br/><small>Contexts</small>"]:::Context
Identity["Identity<br/><small>Identities</small>"]:::Identity
Principal["Principal<br/><small>Principals</small>"]:::Principal
Resource["Resource<br/><small>Resources</small>"]:::Resource
System["System<br/><small>Systems</small>"]:::System
Context -->|"ContextMembers"| Identity
Context -->|"ContextMembers"| Resource
Context -->|"ContextMembers"| Principal
Context -->|"ContextMembers"| System
Context -->|"ContextRelationships<br/><small>SameUnitAs</small>"| Context
Context -->|"ContextRoles"| Principal
Context -->|"ContextRoles"| Identity
Identity -->|"IdentityMembers"| Principal
Principal -->|"PrincipalRelationships<br/><small>Owner, Sponsor</small>"| Principal
Principal -->|"ResourceAssignments"| Resource
Identity -->|"ResourceAssignments"| Resource
Resource -->|"ResourceRelationships<br/><small>Contains, DelegatesScope, GrantsAccessTo, HasAppOwnership, HasAppRole, HasApplicationPermission, HasOwnership</small>"| Resource
Context -.->|"parentContextId"| Context
Context -.->|"scopeSystemId"| System
Identity -.->|"managerIdentityId"| Identity
Identity -.->|"primaryPrincipalId"| Principal
Principal -.->|"managerId"| Principal
Principal -.->|"systemId"| System
Resource -.->|"systemId"| System
System -.->|"directorySystemId"| System
classDef Context fill:#d97706,stroke:#b45309,color:#fff
classDef Identity fill:#7c3aed,stroke:#5b21b6,color:#fff
classDef Principal fill:#2563eb,stroke:#1e40af,color:#fff
classDef Resource fill:#059669,stroke:#047857,color:#fff
classDef System fill:#475569,stroke:#334155,color:#fff
| Node | What it is |
|---|---|
| System | A source the data came from: Entra ID, Omada, a SQL database, a CSV file. |
| Principal | An account in one system: a user, a service principal, a managed identity, an AI agent, a guest. |
| Resource | Anything that gives access: a group, an app role, a directory role, a permission — and also a business role. |
| Identity | A real person, who may have accounts in several systems. |
| Context | A grouping of one kind of node: a team, a department, an OU, a tag, a cluster. Contexts form trees. |
Each kind has types below it. A Principal is a User, an AIAgent, a ServicePrincipal …; a
Resource is a Group, an AppRole, a BusinessRole …; a Context is a ManagerHierarchy node, a
Department, a Tag …. The next section shows them all.
The types¶
Each kind of node has types: a value in its type column (principalType, resourceType,
contextType) that says what sort of account, resource or grouping a row is. Above the types sit
14 templates: three kinds of account (Person, Software, Agent), seven kinds of resource (Group,
TechnicalRole, Permission, Application, Scope, BusinessRole, Other) and four kinds of grouping
(OrganizationalUnit, LogicalApplication, Tag, Segment). The diagrams below
show every type the ontology defines, grouped by its template: the level at which Identity Atlas
reasons about it. The type is the source's own word; the template says what the product does with
it. The small text under a type names the crawlers and plugins that write it.
Account types (Principal)¶
principalType is a closed list: the ingest accepts only these values. Crawlers that map types
from their source (Omada, midPoint, SCIM, CSV, SQL) map onto this list. The template says what kind
of actor an account is. Who answers for it is a second axis, accountability: Personal, Shared or
Autonomous, a property of each account with a default per type. An AI agent can be any of the three
(a personal assistant, a team assistant, an agent on its own permissions), so its default is unknown
until the source, an Owner or Sponsor link, or a person settles it.
flowchart LR
root["Principal<br/><small>principalType</small>"]:::Principal
subgraph g_Agent["Agent"]
direction LR
t_AIAgent["AIAgent<br/><small>demo, entra-id</small>"]:::Principal
end
subgraph g_Person["Person"]
direction LR
t_ExternalUser["ExternalUser<br/><small>demo, omada</small>"]:::Principal
t_SharedMailbox["SharedMailbox<br/><small>demo</small>"]:::Principal
t_User["User<br/><small>azure-rm, csv, demo, entra-id, midpoint, mssql, omada, scim</small>"]:::Principal
t_ExternalUser ~~~ t_SharedMailbox ~~~ t_User
end
subgraph g_Software["Software"]
direction LR
t_ManagedIdentity["ManagedIdentity<br/><small>demo, entra-id</small>"]:::Principal
t_ServicePrincipal["ServicePrincipal<br/><small>demo, entra-id, omada</small>"]:::Principal
t_WorkloadIdentity["WorkloadIdentity"]:::Principal
t_ManagedIdentity ~~~ t_ServicePrincipal ~~~ t_WorkloadIdentity
end
root --> g_Agent
root --> g_Person
root --> g_Software
classDef Principal fill:#2563eb,stroke:#1e40af,color:#fff
Resource types (Resource)¶
resourceType is an open list: any value is accepted, because every system names its own kinds
of access. Defined here are the values Identity Atlas itself writes or relies on. The dotted arrow
names the crawlers that also pass values through from their source data or configuration.
Owning a group is a right in the target system, so it is a Permission of the control kind: a
right over another object, which it names through the HasOwnership link from that object. Holding
it never makes the holder a member of what it controls. That is how owner and member of one group
stay two rows of the same group in the matrix, each held Direct, Indirect or Eligible.
flowchart LR
root["Resource<br/><small>resourceType</small>"]:::Resource
subgraph g_Application["Application"]
direction LR
t_AppRegistration["AppRegistration<br/><small>entra-id</small>"]:::Resource
t_Application["Application<br/><small>demo, entra-id</small>"]:::Resource
t_Service["Service<br/><small>midpoint</small>"]:::Resource
t_AppRegistration ~~~ t_Application ~~~ t_Service
end
subgraph g_BusinessRole["Business role"]
direction LR
t_BusinessRole["BusinessRole<br/><small>csv, demo, entra-id, midpoint, mssql, omada</small>"]:::Resource
end
subgraph g_Group["Group"]
direction LR
t_Group["Group<br/><small>demo, entra-id, scim</small>"]:::Resource
end
subgraph g_Permission["Permission"]
direction LR
t_ApplicationOwnership["ApplicationOwnership<br/><small>demo, entra-id</small>"]:::Resource
t_ApplicationPermission["ApplicationPermission<br/><small>entra-id</small>"]:::Resource
t_DelegatedPermission["DelegatedPermission<br/><small>demo, entra-id</small>"]:::Resource
t_Entitlement["Entitlement<br/><small>midpoint, mssql</small>"]:::Resource
t_GroupOwnership["GroupOwnership<br/><small>demo, entra-id</small>"]:::Resource
t_Resource["Resource<br/><small>omada</small>"]:::Resource
t_ResourceOwnership["ResourceOwnership<br/><small>mssql</small>"]:::Resource
t_ServicePrincipalOwnership["ServicePrincipalOwnership<br/><small>demo, entra-id</small>"]:::Resource
t_ApplicationOwnership ~~~ t_ApplicationPermission ~~~ t_DelegatedPermission ~~~ t_Entitlement
t_GroupOwnership ~~~ t_Resource ~~~ t_ResourceOwnership ~~~ t_ServicePrincipalOwnership
end
subgraph g_Scope["Scope"]
direction LR
t_AzureManagementGroup["AzureManagementGroup<br/><small>azure-rm</small>"]:::Resource
t_AzureResource["AzureResource<br/><small>azure-rm, demo</small>"]:::Resource
t_AzureResourceGroup["AzureResourceGroup<br/><small>azure-rm, demo</small>"]:::Resource
t_AzureScope["AzureScope<br/><small>azure-rm, demo</small>"]:::Resource
t_AzureSubscription["AzureSubscription<br/><small>azure-rm</small>"]:::Resource
t_AzureManagementGroup ~~~ t_AzureResource ~~~ t_AzureResourceGroup ~~~ t_AzureScope
end
subgraph g_TechnicalRole["Technical role"]
direction LR
t_AppRole["AppRole<br/><small>demo, entra-id</small>"]:::Resource
t_AzureRoleAssignment["AzureRoleAssignment<br/><small>azure-rm, demo</small>"]:::Resource
t_CRMRole["CRMRole<br/><small>demo</small>"]:::Resource
t_EntraDirectoryRole["EntraDirectoryRole<br/><small>demo, entra-id</small>"]:::Resource
t_SAPRole["SAPRole<br/><small>demo</small>"]:::Resource
t_AppRole ~~~ t_AzureRoleAssignment ~~~ t_CRMRole ~~~ t_EntraDirectoryRole
end
root --> g_Application
root --> g_BusinessRole
root --> g_Group
root --> g_Permission
root --> g_Scope
root --> g_TechnicalRole
other["any other value<br/><small>from csv, custom-connector, midpoint, mssql, omada</small>"]
root -.-> other
classDef Resource fill:#059669,stroke:#047857,color:#fff
Grouping types (Context)¶
contextType is an open list too, and since 2.1.0 every row also carries its template in
contextCategory. A context groups one kind of node; the
Core Model Reference lists which kind each type groups.
OrgUnit (synced from an IGA or HR source, grouping persons) and OrganisationalUnit (derived by a
plugin from accounts' AD distinguished names) are two values under the same template. A tree's root
is simply a context without a parent, not a type of its own.
An organizational unit or a logical application can say who answers for it: one Accountable
(its manager or owner) and any number of Responsible people, each an account or a person. That is a
link from the grouping to the person (ContextRoles), never access: nobody gains anything by being
accountable for a team. Two org units can be two sides of one unit, the persons and the resources
that belong to them, linked as two variants of one unit by SameUnitAs.
flowchart LR
root["Context<br/><small>contextType</small>"]:::Context
subgraph g_LogicalApplicationContext["Logical application"]
direction LR
t_ContextRecipe["ContextRecipe<br/><small>context-recipe</small>"]:::Context
t_LogicalApplication["LogicalApplication<br/><small>demo, mssql</small>"]:::Context
t_ContextRecipe ~~~ t_LogicalApplication
end
subgraph g_OrganizationalUnit["Organizational unit"]
direction LR
t_AdministrativeUnit["AdministrativeUnit<br/><small>demo</small>"]:::Context
t_CostCenter["CostCenter<br/><small>omada</small>"]:::Context
t_Department["Department<br/><small>department-from-principal, demo, omada</small>"]:::Context
t_ManagerHierarchy["ManagerHierarchy<br/><small>manager-hierarchy</small>"]:::Context
t_OrgUnit["OrgUnit<br/><small>csv, midpoint, omada</small>"]:::Context
t_OrganisationalUnit["OrganisationalUnit<br/><small>ad-ou-from-dn</small>"]:::Context
t_Team["Team<br/><small>demo</small>"]:::Context
t_AdministrativeUnit ~~~ t_CostCenter ~~~ t_Department ~~~ t_ManagerHierarchy
t_OrgUnit ~~~ t_OrganisationalUnit ~~~ t_Team
end
subgraph g_Segment["Segment"]
direction LR
t_EntraGroupCategory["EntraGroupCategory<br/><small>entra-group-category-tree</small>"]:::Context
t_Location["Location<br/><small>omada</small>"]:::Context
t_OrphanedAccounts["OrphanedAccounts<br/><small>orphaned-accounts</small>"]:::Context
t_PrincipalType["PrincipalType<br/><small>principal-type-tree</small>"]:::Context
t_ResourceCluster["ResourceCluster<br/><small>resource-cluster</small>"]:::Context
t_ResourcePlane["ResourcePlane<br/><small>resource-type-tree</small>"]:::Context
t_ResourceRole["ResourceRole<br/><small>resource-type-tree</small>"]:::Context
t_ResourceType["ResourceType<br/><small>resource-type-tree</small>"]:::Context
t_RiskyAppConsent["RiskyAppConsent<br/><small>risky-consent</small>"]:::Context
t_RiskyConsent["RiskyConsent<br/><small>risky-consent</small>"]:::Context
t_Scope["Scope<br/><small>scope-hierarchy</small>"]:::Context
t_SystemMembership["SystemMembership<br/><small>system-membership-principals, system-membership-resources</small>"]:::Context
t_EntraGroupCategory ~~~ t_Location ~~~ t_OrphanedAccounts ~~~ t_PrincipalType
t_ResourceCluster ~~~ t_ResourcePlane ~~~ t_ResourceRole ~~~ t_ResourceType
t_RiskyAppConsent ~~~ t_RiskyConsent ~~~ t_Scope ~~~ t_SystemMembership
end
subgraph g_TagContext["Tag"]
direction LR
t_Activity["Activity"]:::Context
t_Duty["Duty"]:::Context
t_Expertise["Expertise"]:::Context
t_Tag["Tag"]:::Context
t_Activity ~~~ t_Duty ~~~ t_Expertise ~~~ t_Tag
end
root --> g_LogicalApplicationContext
root --> g_OrganizationalUnit
root --> g_Segment
root --> g_TagContext
other["any other value<br/><small>from csv, custom-connector, midpoint, mssql, omada</small>"]
root -.-> other
classDef Context fill:#d97706,stroke:#b45309,color:#fff
Every type, with its description, is in the tables of the Core Model Reference.
One team, as data¶
The picture below is a small, fictional team: Anna works for Piet. Anna has an account in Entra ID, holds a business role, and through a group has access to an app role. An AI agent holds that same app role directly. The manager-hierarchy plugin has put Anna in Piet's team.
flowchart LR
anna["Anna de Vries<br/><small>Identity</small>"]:::Identity
subgraph entra["Entra ID · System"]
annaAccount["anna.devries@example.org<br/><small>User</small>"]:::Principal
berthApp["Berth Planning System<br/><small>Application</small>"]:::Resource
berthPlanner["Planner<br/><small>App role</small>"]:::Resource
pietAccount["piet.jansen@example.org<br/><small>User</small>"]:::Principal
plannerBot["Planning assistant<br/><small>AI agent</small>"]:::Principal
plannerRole["Berth planner<br/><small>Business role</small>"]:::Resource
planningGroup["SG-Berth-Planning<br/><small>Group</small>"]:::Resource
end
orgRoot["Organisation<br/><small>Manager hierarchy node</small>"]:::Context
pietsTeam["Berth Planning (Piet Jansen)<br/><small>Manager hierarchy node</small>"]:::Context
annaAccount -->|"Indirect"| berthPlanner
annaAccount -->|"Direct, governed"| plannerRole
annaAccount -->|"Direct"| planningGroup
pietsTeam -->|"context member"| annaAccount
anna -->|"identity member"| annaAccount
plannerBot -->|"Direct"| berthPlanner
pietsTeam -->|"Accountable"| pietAccount
annaAccount -.->|"managerId"| pietAccount
berthApp ==>|"HasAppRole"| berthPlanner
pietsTeam -.->|"parentContextId"| orgRoot
plannerRole ==>|"Contains"| planningGroup
classDef Identity fill:#7c3aed,stroke:#5b21b6,color:#fff
classDef Principal fill:#2563eb,stroke:#1e40af,color:#fff
classDef Resource fill:#059669,stroke:#047857,color:#fff
classDef Context fill:#d97706,stroke:#b45309,color:#fff
classDef System fill:#475569,stroke:#334155,color:#fff
How to read it:
- Colour = kind of node: purple a person (Identity), blue an account (Principal), green something that gives access (Resource), orange a grouping (Context). The grey box is the system the accounts and resources come from. The small text under each name is its type.
- A solid arrow labelled
Direct/Indirectis an assignment: who holds what, and how. Anna holds the business role Direct and governed (an approved request); she has the app role Indirect, because it comes through her group. - A thick arrow is a relationship between two resources: the business role
Containsthe group, the applicationHasAppRolethe app role. - A dotted arrow is a reference: Anna's account names Piet's as its
managerId; Piet's team sits below the organisation root (parentContextId). - Anna (the person) and anna.devries@example.org (the account) are two different things. The
person can have more accounts in other systems; the
identity memberarrow links them. - Piet answers for the team. The
Accountablearrow is a context role: the manager-hierarchy plugin states who is accountable for each node, and an analyst can add who is responsible. It is a link from the team to the person, not access; see Contexts and the manager hierarchy.
The same team, as the file says it¶
This is the complete example file. Every word after ia: is a term from the ontology. Each block
describes one thing: its type after a, then its fields.
# Worked example: one small team, described with the Identity Atlas core ontology.
#
# Fictional people and systems. Every resource below is an instance of a class
# from ontology/core.ttl, and every property is one of its terms — this is
# what the data in an Identity Atlas database looks like when it is written as
# RDF. `npm run ontology:check` (app/api) validates this file against the
# ontology, and the picture on docs/concepts/ontology.md is generated from it,
# so neither can drift from the model.
#
# Rows of the edge tables (assignments, identity and context memberships) are
# resources of their own, because they carry fields. A resource-to-resource
# relationship (Contains, HasAppRole …) is written as the direct shortcut
# property between the two resources.
@prefix ia: <https://identityatlas.io/ontology#> .
@prefix ex: <https://identityatlas.io/example#> .
# ── A source system ──────────────────────────────────────────────────────────
ex:entra a ia:System ;
ia:displayName "Entra ID" .
# ── Two people and their accounts ────────────────────────────────────────────
ex:anna a ia:Identity ;
ia:displayName "Anna de Vries" .
ex:annaAccount a ia:User ;
ia:displayName "anna.devries@example.org" ;
ia:systemId ex:entra ;
ia:managerId ex:pietAccount .
ex:pietAccount a ia:User ;
ia:displayName "piet.jansen@example.org" ;
ia:systemId ex:entra .
ex:annaIsAnna a ia:IdentityMember ; # account correlation: this account is Anna
ia:identityId ex:anna ;
ia:principalId ex:annaAccount .
# ── An AI agent that holds access as its own account ─────────────────────────
ex:plannerBot a ia:AIAgent ;
ia:displayName "Planning assistant" ;
ia:systemId ex:entra .
# ── What can be held ─────────────────────────────────────────────────────────
ex:planningGroup a ia:Group ;
ia:displayName "SG-Berth-Planning" ;
ia:systemId ex:entra .
ex:plannerRole a ia:BusinessRole ; # a governance construct (access package)
ia:displayName "Berth planner" ;
ia:systemId ex:entra ;
ia:contains ex:planningGroup .
ex:berthApp a ia:Application ;
ia:displayName "Berth Planning System" ;
ia:systemId ex:entra ;
ia:hasAppRole ex:berthPlanner .
ex:berthPlanner a ia:AppRole ;
ia:displayName "Planner" ;
ia:systemId ex:entra .
# ── Who holds what (rows of ResourceAssignments) ─────────────────────────────
ex:annaHoldsRole a ia:ResourceAssignment ;
ia:principalId ex:annaAccount ;
ia:resourceId ex:plannerRole ;
ia:assignmentType "Direct" ;
ia:governed true ;
ia:origin "Requested" .
ex:annaInGroup a ia:ResourceAssignment ;
ia:principalId ex:annaAccount ;
ia:resourceId ex:planningGroup ;
ia:assignmentType "Direct" .
ex:annaHasAppRole a ia:ResourceAssignment ; # through the group: Indirect
ia:principalId ex:annaAccount ;
ia:resourceId ex:berthPlanner ;
ia:assignmentType "Indirect" .
ex:botHasAppRole a ia:ResourceAssignment ;
ia:principalId ex:plannerBot ;
ia:resourceId ex:berthPlanner ;
ia:assignmentType "Direct" .
# ── The team, as the manager-hierarchy plugin builds it ──────────────────────
ex:orgRoot a ia:ManagerHierarchyContext ;
ia:displayName "Organisation" ;
ia:targetType "Principal" ;
ia:contextCategory "OrganizationalUnit" ;
ia:variant "generated" ;
ia:externalId "root" .
ex:pietsTeam a ia:ManagerHierarchyContext ;
ia:displayName "Berth Planning (Piet Jansen)" ;
ia:targetType "Principal" ;
ia:contextCategory "OrganizationalUnit" ;
ia:variant "generated" ;
ia:externalId "pietAccount" ;
ia:parentContextId ex:orgRoot .
ex:pietAnswersForTeam a ia:ContextRole ; # who answers for the team: a link, not access
ia:contextId ex:pietsTeam ;
ia:principalId ex:pietAccount ;
ia:role "Accountable" ;
ia:addedBy "algorithm" .
ex:annaInPietsTeam a ia:ContextMember ;
ia:contextId ex:pietsTeam ;
ia:memberId ex:annaAccount ;
ia:memberType "Principal" ;
ia:addedBy "algorithm" .
What the schema file says¶
The ontology itself is written the same way, but it describes the kinds of things instead of the
things. A few excerpts, quoted straight from core.ttl:
A kind of node. ia:Principal is a class. It is stored in the Principals table, and its
principalType column says which subtype a row is:
ia:Principal a owl:Class ; rdfs:subClassOf ia:Node ;
rdfs:label "Principal" ; rdfs:isDefinedBy <https://identityatlas.io/ontology> ;
rdfs:comment "An account in a source system: a user, service principal, managed identity, AI agent, guest, shared mailbox …" ;
ia:table "Principals" ; ia:identifierColumn "id" ; ia:identifierSqlType "uuid" ; ia:ingestEntity "principals" ;
ia:discriminator "principalType" ;
ia:passthroughFrom "csv", "custom-connector", "midpoint", "mssql", "omada", "scim" .
A type below it. An AI agent is a Principal whose principalType is AIAgent:
ia:AIAgent a owl:Class ; rdfs:subClassOf ia:Agent ; ia:typeValue "AIAgent" ;
ia:emittedBy "demo", "entra-id" ;
rdfs:label "AI agent" ; rdfs:isDefinedBy <https://identityatlas.io/ontology> ;
rdfs:comment "An AI agent (Copilot Studio, Azure OpenAI, a custom bot) that holds access as its own account." .
A context type, with the plugin that makes it and the kind of node it groups:
ia:ManagerHierarchyContext a owl:Class ; rdfs:subClassOf ia:OrganizationalUnit ; ia:typeValue "ManagerHierarchy" ;
ia:producedBy "manager-hierarchy" ; ia:allowedTargetType ia:EntityKind_Principal ;
rdfs:label "Manager hierarchy node" ; rdfs:isDefinedBy <https://identityatlas.io/ontology> ;
rdfs:comment "One generated context per manager of one system's accounts, plus a synthetic root (externalId 'root'). The manager is recorded only in externalId, which holds the manager's Principal id — there is no manager column or member role. Members are the manager's direct reports (Principals.managerId, overridden per account by ManagerHierarchyOverrides); the manager is a member of their own manager's node, not of their own. parentContextId is the node of the manager's own manager, else the root. Accounts with no manager that manage nobody are members of the root; a top-level manager is a member of no node. A managerId that points at no account of the system still becomes a node, named 'Unknown'. A reporting cycle, a self-reference included, leaves a node without a parent: the runner refuses the link that would close the cycle." .
A relationship type between two resources, and which classes it connects:
ia:hasAppRole a owl:ObjectProperty ; ia:typeValue "HasAppRole" ; ia:storedIn ia:ResourceRelationship ;
rdfs:domain ia:Application ; rdfs:range ia:AppRole ;
rdfs:label "has app role" ; rdfs:isDefinedBy <https://identityatlas.io/ontology> ;
rdfs:comment "An application and one of its app roles." .
A closed list of values. An assignment is always one of exactly three:
ia:AssignmentType a owl:Class ;
rdfs:label "Assignment type" ; rdfs:isDefinedBy <https://identityatlas.io/ontology> ;
rdfs:comment "How an assignment is held. Only these three values exist; ownership, governance and source detail are modelled elsewhere." .
ia:AssignmentType_Direct a owl:NamedIndividual, ia:AssignmentType ; ia:typeValue "Direct" ;
rdfs:label "Direct" ; rdfs:isDefinedBy <https://identityatlas.io/ontology> ;
rdfs:comment "Held directly: a membership, a directly assigned app role, an OAuth2 grant, an ownership." .
ia:AssignmentType_Indirect a owl:NamedIndividual, ia:AssignmentType ; ia:typeValue "Indirect" ;
rdfs:label "Indirect" ; rdfs:isDefinedBy <https://identityatlas.io/ontology> ;
rdfs:comment "Inherited through another resource (an app role held via a group, a nested group). Stored, not inferred." .
ia:AssignmentType_Eligible a owl:NamedIndividual, ia:AssignmentType ; ia:typeValue "Eligible" ;
rdfs:label "Eligible" ; rdfs:isDefinedBy <https://identityatlas.io/ontology> ;
rdfs:comment "Granted but not active (PIM-eligible): can be activated." .
Every term carries a label and a description; the Core Model Reference lists them all as tables, generated from this same file.
One way of holding¶
The model separates what exists (the types and templates above), how things relate (the relationship types) and who holds what. The first two may grow. The third never does: an account reaches a resource only through an assignment, held Direct, Indirect or Eligible. A new kind of right, such as owning a group, is a new resource plus a relationship to the object it is about, never a new way of holding. That rule is what keeps every matrix cell to three values while the ontology grows; Core Ontology has the full table of concepts and where each one lives.
How it stays true¶
A description is only useful if it matches the application. Three things keep it that way:
- CI compares the ontology with the code. If a developer adds a relationship type, a principal type, a column, a context type or an allowed value without describing it, the build fails. It also fails the other way round, when the ontology describes something the code does not do.
- CI compares it with the database. A database migrated from scratch must have exactly the columns and allowed values the ontology describes.
- The pictures on this page and the reference page are generated from the ontology, and the example file is validated against it. Nobody draws them by hand, so they cannot drift.
The details, and what is deliberately not checked, are on Core Ontology. How to change the model: Maintaining the Core Ontology.
Open it in a tool¶
- Protégé (free desktop editor, protege.stanford.edu): File →
Open from URL, paste the
core.ttllink above. The Entities tab shows the class tree (Node → Principal → AIAgent …) and every property with its domain and range. The OntoGraf tab draws it. - WebVOWL (in the browser, service.tib.eu/webvowl): download
core.ttl, then Ontology → Select ontology file to see an interactive graph of the classes. -
Python (rdflib):
from rdflib import Graph g = Graph().parse("https://raw.githubusercontent.com/Fortigi/IdentityAtlas/main/ontology/core.ttl") for cls in g.query(""" PREFIX ia: <https://identityatlas.io/ontology#> PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> SELECT ?type WHERE { ?c rdfs:subClassOf ia:Principal ; ia:typeValue ?type }"""): print(cls.type) # User, ServicePrincipal, AIAgent, …
The namespace https://identityatlas.io/ontology# is an identifier, not a web page: opening it in a
browser does not return the file. Use the download links above.