Skip to content

[Enh]: Implement role inheritance for entity permissions #3163

@JerryNixon

Description

@JerryNixon

Problem

Today, a developer is required to repeat permissions across all possible roles.

Today's lack of permissions inheritance can lead to very verbose configs and unexpected denials.

Desired Behavior

Introduce role inheritance that let's unlisted roles inherit from roles with fewer permissions.

Specific-role -(not found)-> Authenticated -(not found)-> Anonymous -(not found)-> None

Rules

  1. When any role is configured in permissions, that role always gets its that configuration.
  2. When authenticated is not configured, authenticated inherits the permissions of anonymous, if present.
  3. When named-role is not configured, it inherits the permissions of authenticated, if present.
  4. When named-role is not configured and neither is authenticated, it inherits the permissions of anonymous, if present.
  5. When named-role is not configured and neither is authenticated or anonymous, it inherits nothing.
  6. Permissions inheritance includes actions, policiesandfields`.
  7. It is still Data API builder's permission model that the requestor is only ONE role at a time.

Command line

We need to ensure the developer always has a way to know and understand inheritance.

dab configure --show-effective-permissions <role-name>.

Note: In this release, this feature does not work with auto-entities.

Output

Entity         Effective Role    Actions         Policy
─────────────  ────────────────  ──────────────  ──────────────
Employees      anonymous         read            (none)
Products       authenticated     read, update    @item.active
Inventory      special-role      *               (none)

Example Matrix

Note: none of the examples include execute below, but the behavior for stored procedures would be the same.

1. All roles configured:

{
  "permissions": {
    "anonymous": [ "read" ],
    "authenticated": [ "update" ],
    "special-role": [ "delete" ]
  }
}
anonymous authenticated special-role
read update delete

2. special-role missing

{
  "permissions": {
    "anonymous": [ "read" ],
    "authenticated": [ "update" ]
  }
}
anonymous authenticated special-role
read update update

3. authenticated and special-role missing

{
  "permissions": {
    "anonymous": [ "read" ]
  }
}
anonymous authenticated special-role
read read read

4. Only a custom role defined

{
  "permissions": {
    "jerry-role": [ "read" ]
  }
}
anonymous authenticated special-role jerry-role
none none none read

Coding considerations

The implementation of [CopyOverPermissionsFromAnonymousToAuthenticatedRole](https://github.com/Azure/data-api-builder/blob/29b0e6eee594027e0787b3ce9c9aace015128f49/src/Core/Authorization/AuthorizationResolver.cs#L398-L427) already exists. This is a nice start, but not the complete story. It has a bug: This is a reference assignment, not a deep copy. Both authenticated and anonymous share the same RoleMetadata object. If any downstream code ever mutates the inherited permissions for one role (e.g., appending an action), it silently mutates the other. Extending this pattern to named roles creates a three-way shared reference chain, a subtle and dangerous source of bugs. We want to fix this and not repeat it.

The method GetRolesForEntity(string entityName) would return the wrong result. This is used by GraphQL to build @authorize directives on object types. With inheritance, you'd need to materialize all possible roles (including those that aren't explicitly configured but would inherit), which is unbounded, DAB can't know what named roles a JWT might carry ahead of time. This is fundamentally different from today, where every role that can access an entity is explicitly listed. The GraphQL schema generation would break or become incomplete.

  • Option A: GraphQL @authorize directives only list explicitly-configured roles (status quo). A named role that inherits at runtime would pass authorization checks but wouldn't appear in the schema's directive. This is functionally correct but the schema is "incomplete."
  • Option B: Add a synthetic authenticated entry to @authorize directives when inheritance is active, since any authenticated named role would inherit from authenticated anyway. This is a closer approximation.

The method AreRoleAndOperationDefinedForEntity() would need to implement the fallback chain (named-role → authenticated → anonymous). But if you materialize everything at startup (like the current anonymous→authenticated copy), then named roles you don't know about at config time won't benefit. If you do it lazily at request time, you need the fallback in every authorization check point (AreRoleAndOperationDefinedForEntity, AreColumnsAllowedForOperation, GetDBPolicyForRequest, GetAllowedExposedColumns, GetRolesForField, IsStoredProcedureExecutionPermitted). That's a significant surface area to update and test.

Metadata

Metadata

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions