Skip to main content

ADR-014: AWS IAM Policy Entity Type — Introduce permission_set

Status

Accepted (2026-03-11)


Context

The AWS connector research (2026-03-11) mapped IAM Managed Policy to Entity(type="role") as a prototype convenience. During peer review (Delta, 2026-03-11), this mapping was flagged as semantically ambiguous and requiring an explicit ADR decision before Phase 1 ships.

What type="role" means today

ADR-006 established 9 internal entity types. type="role" was carried over from the OAA-aligned data model to represent permission-granting constructs that sit between an identity and an access grant:

Identity → HAS_ROLE → Role → GRANTS → Permission → APPLIES_TO → Resource

In the existing Entra ID and ServiceNow connectors, role maps to:

  • Azure RBAC Role Assignment (e.g., Storage Blob Data Reader)
  • ServiceNow OAuth Scope (e.g., read:incidents)

Both are named grants — they are specific, labelled authorizations attached to an identity at a particular scope.

Why IAM Policy ≠ Role

An AWS IAM Managed Policy does not behave like an Azure RBAC role assignment:

DimensionAzure RBAC Role AssignmentAWS IAM Managed Policy
Scoped to a resource?Yes — assignment includes scope (subscription, RG, resource)No — policy is a standalone document; scope comes from the resource ARN in the statement
Human-readable name?Yes — always (e.g., "Reader", "Contributor")Usually — but inline policies have no name; wildcard-heavy policies have misleading names
Many-to-many?One assignment per (identity, role, scope) tupleOne policy can attach to many identities; one identity can have many policies
Authority sourceThe assignment itselfThe policy document's Statement array

Mapping IAM Policy to type="role" causes two problems:

  1. UI confusion — the Roles tab in the authority path detail page would surface IAM Policy documents, not role names. A path showing via_roles: ["AmazonS3ReadOnlyAccess"] is technically correct but looks like an Azure/ServiceNow role name, misleading operators who manage both environments.
  2. Materialiser assumption breakage — the existing materialiser path identity → HAS_ROLE → role assumes role objects represent scoped grants. IAM Policies are not scoped; scope is embedded in the policy statement's Resource field. If the materialiser traverses HAS_POLICY → IAM Policy → GRANTS using the same logic as HAS_ROLE → Role → GRANTS, it will conflate the two and produce incorrect via_roles values in authority path documents.

Decision

Introduce type="permission_set" as the 10th entity type in the SecurityV0 data model, specifically to represent policy-document entities that grant permissions via embedded statements rather than via scoped named assignments.

Definition

permission_set — an entity that contains one or more permission statements granting actions on resources. Unlike role, a permission_set is not scoped at the assignment level; scope is encoded within the entity itself (in the policy document's Resource ARN fields).

Scope of the new type

EntityType
AWS IAM Managed Policypermission_set
AWS IAM Inline Policypermission_set (embedded; modelled as a virtual entity derived from the identity's embedded policy)
AWS IAM Permission Boundarypermission_set (ceiling variant — subtype: permission_boundary)
AWS SCPpermission_set (ceiling variant — subtype: scp, attached to tenant entities)

Existing role entities (Azure RBAC assignments, ServiceNow OAuth scopes) are not affected — they remain type="role".

Graph edges

IAM Role → HAS_PERMISSION_SET → permission_set (IAM Policy)
permission_set → GRANTS → Permission (action + resource ARN)
Permission → APPLIES_TO → Resource

IAM Role → SUBJECT_TO → permission_set (permission_boundary, subtype)
Account → SUBJECT_TO → permission_set (scp, subtype)

The materialiser traverses HAS_PERMISSION_SET instead of HAS_ROLE for AWS entities. Authority path documents populate via_roles with the policy name for backward compatibility, but the source field via_role_source: "permission_set" disambiguates in the API response.

Migration

The AWS connector prototype may ship with type="role" and _type_provisional: true on all IAM Policy entities while the platform-side materialiser changes are implemented. Before Phase 1 goes to production, the platform team must:

  1. Add permission_set to the NormalizedNodeType enum in sv0-platform
  2. Update the materialiser to traverse HAS_PERMISSION_SET edges for AWS-sourced paths
  3. Update the authority path API to include via_role_source in path responses
  4. Run a backfill migration to retype any provisional role entities created during development

Consequences

Positive:

  • IAM Policies are modelled accurately — no semantic conflation with Azure RBAC role assignments
  • The materialiser can apply ceiling semantics (Permission Boundary, SCP) cleanly via SUBJECT_TO edges on permission_set entities
  • The via_roles field in authority paths correctly surfaces the IAM Policy name; via_role_source tells the UI which system the "role" came from
  • Opens a clean extension point for future policy-as-document systems (GCP IAM Bindings, Okta Policy Rules)

Negative / Trade-offs:

  • One additional entity type to maintain in the data model; documentation and schema validation must be updated
  • Materialiser changes required before Phase 1 ships — this is a blocker for the AWS connector
  • UI components that render "Roles" on authority path detail pages need to handle permission_set source gracefully