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:
| Dimension | Azure RBAC Role Assignment | AWS 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) tuple | One policy can attach to many identities; one identity can have many policies |
| Authority source | The assignment itself | The policy document's Statement array |
Mapping IAM Policy to type="role" causes two problems:
- 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. - Materialiser assumption breakage — the existing materialiser path
identity → HAS_ROLE → roleassumesroleobjects represent scoped grants. IAM Policies are not scoped; scope is embedded in the policy statement'sResourcefield. If the materialiser traversesHAS_POLICY → IAM Policy → GRANTSusing the same logic asHAS_ROLE → Role → GRANTS, it will conflate the two and produce incorrectvia_rolesvalues 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
| Entity | Type |
|---|---|
| AWS IAM Managed Policy | permission_set |
| AWS IAM Inline Policy | permission_set (embedded; modelled as a virtual entity derived from the identity's embedded policy) |
| AWS IAM Permission Boundary | permission_set (ceiling variant — subtype: permission_boundary) |
| AWS SCP | permission_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:
- Add
permission_setto theNormalizedNodeTypeenum insv0-platform - Update the materialiser to traverse
HAS_PERMISSION_SETedges for AWS-sourced paths - Update the authority path API to include
via_role_sourcein path responses - Run a backfill migration to retype any provisional
roleentities 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_TOedges onpermission_setentities - The
via_rolesfield in authority paths correctly surfaces the IAM Policy name;via_role_sourcetells 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_setsource gracefully
Related
- ADR-006: Entity Type Reclassification — established the 9-type model this ADR extends
- AWS Connector Research — context and provisional mapping
- Access Paths —
via_rolesandvia_role_idsfields affected