-
Notifications
You must be signed in to change notification settings - Fork 314
Description
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
- When any role is configured in
permissions, that role always gets its that configuration. - When
authenticatedis not configured,authenticatedinherits the permissions ofanonymous, if present. - When
named-roleis not configured, it inherits the permissions ofauthenticated, if present. - When
named-roleis not configured and neither isauthenticated, it inherits the permissions ofanonymous, if present. - When
named-roleis not configured and neither isauthenticatedoranonymous, it inherits nothing. - Permissions inheritance includes
actions,policiesandfields`. - 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.