{
  "name": "Access control",
  "nodes": [
    {
      "id": "n_user",
      "name": "User",
      "labels": ["User", "Principal"],
      "shape": "circle",
      "description": "A person or automated agent that can authenticate. Holds identity and nothing else: a user has no permissions of their own in this model, only roles that carry them. That separation is the whole point.",
      "properties": [
        { "name": "userId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier that never changes. Usernames and email addresses both get reused after someone leaves, which is why neither can be the key." },
        { "name": "username", "type": "string", "required": true, "unique": true, "indexed": true, "description": "Login name. Unique among active accounts, but not stable enough across a lifetime to identify the person." },
        { "name": "displayName", "type": "string", "required": true, "description": "Name shown in interfaces and audit trails." },
        { "name": "email", "type": "string", "unique": true, "indexed": true, "description": "Contact address, and the usual route for access-review notifications." },
        { "name": "isEnabled", "type": "boolean", "required": true, "default": "true", "description": "False when the account is suspended or the person has left. Disabling here revokes everything at once, which is only possible because permissions were never attached to the user directly." },
        { "name": "lastAuthenticatedAt", "type": "datetime", "description": "When the user last signed in successfully. The field dormant-account reviews run on." }
      ]
    },
    {
      "id": "n_group",
      "name": "Group",
      "labels": ["Group", "Principal"],
      "shape": "circle",
      "description": "A collection of users administered together, usually mastered in a directory rather than here. Not part of the NIST model, but present in almost every real deployment, and worth modelling explicitly rather than pretending role assignment is always direct.",
      "properties": [
        { "name": "groupId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier, stable across renames in the source directory." },
        { "name": "name", "type": "string", "required": true, "unique": true, "description": "Group name as administrators see it." },
        { "name": "purpose", "type": "string", "description": "What the group is for, in a sentence. The field that decides whether a membership is still appropriate at review time." },
        { "name": "source", "type": "enum", "required": true, "default": "local", "description": "Where membership is mastered: local, ldap, scim or oidc. An externally mastered group cannot be edited here, which changes who can fix a wrong assignment." },
        { "name": "distinguishedName", "type": "string", "description": "Full directory path for an externally mastered group. Blank for a local one." }
      ]
    },
    {
      "id": "n_role",
      "name": "Role",
      "labels": ["Role"],
      "shape": "circle",
      "description": "A named job function that permissions are attached to. This is the node the model exists to argue for: it sits between users and permissions so that the reason someone has access is recorded once, in a place that can be reviewed, rather than implied by thousands of individual grants.",
      "properties": [
        { "name": "roleId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier, stable across renames." },
        { "name": "name", "type": "string", "required": true, "unique": true, "indexed": true, "description": "Role name, such as invoice_approver. Names a job, not a capability — a role called can_delete is a permission wearing a costume." },
        { "name": "purpose", "type": "string", "description": "The job function this role exists to support. Answers 'should this person have it?', which no list of permissions ever does." },
        { "name": "riskLevel", "type": "enum", "required": true, "default": "low", "description": "Assessed risk of holding the role: low, medium, high or critical. Drives how often the assignment must be recertified." },
        { "name": "requiresApproval", "type": "boolean", "default": "false", "description": "Whether assigning the role needs an approver rather than being self-service." },
        { "name": "maximumAssignmentDays", "type": "integer", "description": "Longest an assignment of this role may last before it must be renewed. Blank for a permanent role." }
      ]
    },
    {
      "id": "n_permission",
      "name": "Permission",
      "labels": ["Permission"],
      "shape": "diamond",
      "description": "Approval to perform one operation on one class of resource. A reified relationship: in the NIST model a permission *is* an operation-object pair, made into a thing in its own right so that roles can be attached to it and so that the same approval can be reused across many roles.",
      "properties": [
        { "name": "permissionId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier for the approval." },
        { "name": "name", "type": "string", "required": true, "unique": true, "indexed": true, "description": "Readable name, conventionally resource:operation — invoice:approve. Derived from the two relationships below rather than authoritative, but indispensable in logs." },
        { "name": "effect", "type": "enum", "required": true, "default": "allow", "description": "Whether the permission grants or withholds: allow or deny. The NIST model is allow-only; deny is a common extension and is included here because real systems have it, but it makes evaluation order significant and should be used sparingly." },
        { "name": "isDelegatable", "type": "boolean", "default": "false", "description": "Whether a holder may pass this permission to someone else for a period, as during leave cover." },
        { "name": "riskLevel", "type": "enum", "required": true, "default": "low", "description": "Assessed risk of the approval itself: low, medium, high or critical. A role's risk is driven by the riskiest permission it grants." }
      ]
    },
    {
      "id": "n_operation",
      "name": "Operation",
      "labels": ["Operation"],
      "shape": "square",
      "description": "Something that can be done to a resource — read, create, approve, export. Reference data defined by the application that implements it, and deliberately small: a long list of operations usually means resource types have been conflated.",
      "properties": [
        { "name": "operationCode", "type": "string", "key": true, "required": true, "unique": true, "indexed": true, "description": "Short stable code such as read or approve. Appears in every permission name and every audit record." },
        { "name": "name", "type": "string", "required": true, "description": "Readable name of the operation." },
        { "name": "isMutating", "type": "boolean", "required": true, "default": "false", "description": "Whether the operation changes state. Separates the operations that need an audit record and an approval path from those that only need a check." },
        { "name": "requiresMfa", "type": "boolean", "default": "false", "description": "Whether performing the operation demands a second factor at the time, regardless of how the session was established." }
      ]
    },
    {
      "id": "n_resource_type",
      "name": "ResourceType",
      "labels": ["ResourceType"],
      "shape": "square",
      "description": "A class of protected thing — invoice, customer record, deployment pipeline. Permissions are filed against the type rather than the instance, because a permission per row does not survive contact with a real dataset.",
      "properties": [
        { "name": "typeCode", "type": "string", "key": true, "required": true, "unique": true, "indexed": true, "description": "Stable code for the class, such as invoice." },
        { "name": "name", "type": "string", "required": true, "description": "Readable name of the resource class." },
        { "name": "dataClassification", "type": "enum", "required": true, "default": "internal", "description": "Sensitivity band: public, internal, confidential or restricted. Sets the floor for what any permission on this type may be granted to." },
        { "name": "retentionDays", "type": "integer", "description": "How long instances of this type are kept. Relevant to access because expired data often keeps live permissions pointing at it." }
      ]
    },
    {
      "id": "n_resource",
      "name": "Resource",
      "labels": ["Resource"],
      "shape": "circle",
      "description": "A single protected instance — one invoice, one repository. Present so that a permission can be narrowed to a specific object where it genuinely must be, without forcing every permission down to instance level.",
      "properties": [
        { "name": "resourceId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier used by the access system, distinct from whatever the owning application calls it." },
        { "name": "name", "type": "string", "required": true, "description": "Readable name of the instance." },
        { "name": "externalIdentifier", "type": "string", "indexed": true, "description": "The identifier the owning application uses. The join back to the system that actually holds the data." },
        { "name": "isSensitive", "type": "boolean", "default": "false", "description": "True for an instance needing tighter handling than its type implies — a board pack among ordinary documents." },
        { "name": "createdAt", "type": "datetime", "description": "When the resource came into existence, which bounds when any access to it can have been legitimate." }
      ]
    },
    {
      "id": "n_application",
      "name": "Application",
      "labels": ["Application", "System"],
      "shape": "circle",
      "description": "A system that owns resources and defines the operations available on them. Scopes everything below it: the same role name means different things in two applications, and treating them as one is a common and expensive mistake.",
      "properties": [
        { "name": "applicationId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier for the application." },
        { "name": "name", "type": "string", "required": true, "unique": true, "description": "Name of the application as its owners know it." },
        { "name": "vendor", "type": "string", "description": "Supplier of the application, or blank when built in house." },
        { "name": "environment", "type": "enum", "required": true, "default": "production", "description": "Which deployment this is: development, staging or production. Access granted in one must never be assumed to hold in another." },
        { "name": "ownerEmail", "type": "string", "description": "Accountable owner, who signs off access reviews for everything the application holds." }
      ]
    },
    {
      "id": "n_session",
      "name": "Session",
      "labels": ["Session"],
      "shape": "diamond",
      "description": "One authenticated period in which a user has activated some of their roles. Reified because it has a lifecycle and facts of its own, and because it carries the model's second big idea: a user activates a *subset* of what they hold, so what someone can do right now is narrower than what they have been granted.",
      "properties": [
        { "name": "sessionId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier for the session." },
        { "name": "startedAt", "type": "datetime", "required": true, "description": "When authentication succeeded and the session opened." },
        { "name": "endedAt", "type": "datetime", "description": "When the session closed, by sign-out or expiry. Blank while it is live." },
        { "name": "sourceIpAddress", "type": "string", "description": "Where the session was established from, kept for investigation rather than for authorisation." },
        { "name": "authenticationMethod", "type": "enum", "required": true, "default": "password", "description": "How identity was proven: password, mfa, sso, certificate or service_account. Some permissions require a stronger method than others." },
        { "name": "isElevated", "type": "boolean", "default": "false", "description": "True while a time-boxed privilege elevation is active, which is how standing high-risk access is avoided." }
      ]
    },
    {
      "id": "n_separation_of_duty",
      "name": "SeparationOfDuty",
      "labels": ["SeparationOfDuty", "Policy"],
      "shape": "circle",
      "description": "A rule that a set of roles must not accumulate in one pair of hands — the person who raises a payment must not be the person who approves it. Modelled as a node because the rule is a thing the business owns and audits, not a property of any one role.",
      "properties": [
        { "name": "constraintId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier for the rule." },
        { "name": "name", "type": "string", "required": true, "unique": true, "description": "Readable name of the rule, such as payment_raise_vs_approve." },
        { "name": "enforcement", "type": "enum", "required": true, "default": "static", "description": "When the rule is applied: static blocks the assignment outright, dynamic allows both assignments but forbids activating them in one session. The distinction NIST draws between SSD and DSD, and the one most implementations get wrong." },
        { "name": "maximumRolesAllowed", "type": "integer", "required": true, "default": "2", "description": "How many roles from the restricted set one user may hold before the rule is breached — NIST calls this the constraint's cardinality. Two means any two of them together are forbidden." },
        { "name": "rationale", "type": "string", "description": "Why the rule exists, usually a control reference. Without it, a blocked assignment looks like a bug." },
        { "name": "isActive", "type": "boolean", "default": "true", "description": "False for a rule kept for history but no longer enforced." }
      ]
    }
  ],
  "relationships": [
    {
      "type": "ASSIGNED_ROLE", "from": "n_user", "to": "n_role", "cardinality": "n-n",
      "description": "Grants a role to a user directly — the NIST user-assignment relation. Many-to-many in both directions, and the facts below sit on the assignment rather than on the user or the role, because when it was granted and when it lapses are true of this pairing alone.",
      "properties": [
        { "name": "grantedOn", "type": "date", "required": true, "description": "When the assignment took effect. The starting point for every recertification cycle." },
        { "name": "expiresOn", "type": "date", "description": "When it lapses without renewal. Blank for standing access, which should be rare above low risk." },
        { "name": "grantedBy", "type": "string", "description": "Who approved the assignment. The single most useful field during an access review, and the one most often left empty." },
        { "name": "justification", "type": "string", "description": "Why this person needs this role. Recorded at grant time because it is unreconstructable afterwards." }
      ]
    },
    {
      "type": "ASSIGNED_ROLE", "from": "n_group", "to": "n_role", "cardinality": "n-n",
      "description": "Grants a role to every member of a group. The same relation as direct assignment, from a different kind of principal — which is exactly why access reviews must walk both paths. Access held only through a group is the access people forget they have.",
      "properties": [
        { "name": "grantedOn", "type": "date", "required": true, "description": "When the group was given this role." },
        { "name": "expiresOn", "type": "date", "description": "When the grant lapses without renewal. Blank for a standing grant." },
        { "name": "grantedBy", "type": "string", "description": "Who approved giving the role to the whole group." },
        { "name": "justification", "type": "string", "description": "Why the group needs the role. Weaker evidence than a per-user justification, and worth saying so at review time." }
      ]
    },
    {
      "type": "MEMBER_OF", "from": "n_user", "to": "n_group", "cardinality": "n-n",
      "description": "Membership of a group, and therefore an indirect route to every role the group carries. Frequently mastered in an external directory, which means access can change here without anyone in this system doing anything."
    },
    {
      "type": "GRANTS", "from": "n_role", "to": "n_permission", "cardinality": "n-n",
      "description": "Attaches a permission to a role — the NIST permission-assignment relation. Together with role assignment this forms the indirection the model is built around: users reach permissions only by this two-step path, never directly."
    },
    {
      "type": "INHERITS", "from": "n_role", "to": "n_role", "cardinality": "n-n",
      "description": "Role hierarchy: the senior role on the `from` side acquires every permission of the junior role on the `to` side. The direction is the thing people reverse, so it is worth stating plainly — an edge points from the more powerful role to the less powerful one, and permissions flow back along it. Self-referencing, so seniority composes to any depth: a manager inherits from a supervisor who inherits from an operator.",
      "properties": [
        { "name": "isLimited", "type": "boolean", "default": "false", "description": "True where the hierarchy is restricted to a tree, so a role has at most one immediate senior. NIST distinguishes general hierarchies, which form a lattice, from limited ones; the limited form is easier to reason about and easier to review." },
        { "name": "establishedOn", "type": "date", "description": "When the inheritance was put in place. Hierarchies accumulate silently, and this is what makes an unexpected path explicable." }
      ]
    },
    {
      "type": "PERMITS", "from": "n_permission", "to": "n_operation", "cardinality": "n-1",
      "description": "The operation a permission approves. One half of the operation-object pair that a permission reifies."
    },
    {
      "type": "APPLIES_TO", "from": "n_permission", "to": "n_resource_type", "cardinality": "n-1",
      "description": "The class of resource a permission covers. The other half of the pair, and the level most permissions should be written at — one permission per row does not scale and cannot be reviewed."
    },
    {
      "type": "SCOPED_TO", "from": "n_permission", "to": "n_resource", "cardinality": "n-1",
      "description": "Narrows a permission to a single named instance, where the type-level grant would be too broad. Deliberately optional: most permissions have no such edge, and a model where most do has usually lost the distinction between access control and data filtering."
    },
    {
      "type": "OF_TYPE", "from": "n_resource", "to": "n_resource_type", "cardinality": "n-1",
      "description": "The class a resource instance belongs to, and therefore which type-level permissions reach it."
    },
    {
      "type": "OWNS", "from": "n_application", "to": "n_resource", "cardinality": "1-n",
      "description": "The application that holds a resource and is accountable for access to it."
    },
    {
      "type": "DEFINES", "from": "n_application", "to": "n_operation", "cardinality": "1-n",
      "description": "The operations an application implements. Scoping operations to an application prevents the quiet assumption that read means the same thing everywhere."
    },
    {
      "type": "FOR_USER", "from": "n_session", "to": "n_user", "cardinality": "n-1",
      "description": "The user a session authenticates. One user accumulates many sessions; a session belongs to exactly one user."
    },
    {
      "type": "ACTIVATES", "from": "n_session", "to": "n_role", "cardinality": "n-n",
      "description": "The roles a user has actually switched on for this session, which is a subset of those assigned to them. This is where least privilege becomes real rather than aspirational, and where dynamic separation of duty is enforced.",
      "properties": [
        { "name": "activatedAt", "type": "datetime", "required": true, "description": "When the role was switched on within the session, which can be later than the session start." },
        { "name": "deactivatedAt", "type": "datetime", "description": "When the role was dropped, if it was released before the session ended." }
      ]
    },
    {
      "type": "RESTRICTS", "from": "n_separation_of_duty", "to": "n_role", "cardinality": "n-n",
      "description": "The set of roles a separation-of-duty rule governs. Many-to-many because a rule names several roles by definition, and one role can appear in several rules."
    }
  ]
}
