Repository: Azure/azure-cli (Python)

Issue:
Use `roleName` filter to limit the results returned by `az role definition list --name`
**Related command**

Any role definition list operation that filters by name, e.g.:

`az role definition list --name "Contributor"`

**Is your feature request related to a problem? Please describe.**

The call to the resource manager API fetches *all* the IDs:

```text
GET https://management.azure.com/subscriptions/<id>/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview 
HTTP/1.1
```

Whilst the works, it seems unnecessarily costly given a specific name has been supplied.

**Describe the solution you'd like**

Consider specifying the role name if supplied, using the target query parameter:

```text
GET https://management.azure.com/subscriptions/<id>/providers/Microsoft.Authorization/roleDefinitions?api-version=2022-05-01-preview&$filter=roleName%20eq%20'Contributor' 
HTTP/1.1
```

**Describe alternatives you've considered**
n/a

**Additional context**
https://learn.microsoft.com/en-us/rest/api/authorization/role-definitions/list?view=rest-authorization-2022-04-01&tabs=HTTP#uri-parameters


Discussion: <!-- thank-you-comment: do not delete/edit this line -->
Thank you for opening this issue, we will look into it.
I guess downloading it once for all the role types might be more efficient if you take into account caching.  Was just a curiosity i spotted when doing some testing.
@kewalaka, you have sharp eyes!

The role definition filtering is indeed performed on the client side: 

https://github.com/Azure/azure-cli/blob/e8aa309e947d175f1b88fd1abed1cf717e2d4740/src/azure-cli/azure/cli/command_modules/role/custom.py#L140-L150

This is because, according to L145, `--name` matches both `name` and `roleName`. For example, you may retrieve `Reader` role definition with either command:

```
az role definition list --name Reader
az role definition list --name acdd72a7-3385-48ef-bd42-f606fba81ae7
```

And the result is the same:

```
[
  {
    "assignableScopes": [
      "/"
    ],
    "createdBy": null,
    "createdOn": "2015-02-02T21:55:09.880642+00:00",
    "description": "View all resources, but does not allow you to make any changes.",
    "id": "/subscriptions/xxx/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7",
    "name": "acdd72a7-3385-48ef-bd42-f606fba81ae7",
    "permissions": [
      {
        "actions": [
          "*/read"
        ],
        "condition": null,
        "conditionVersion": null,
        "dataActions": [],
        "notActions": [],
        "notDataActions": []
      }
    ],
    "roleName": "Reader",
    "roleType": "BuiltInRole",
    "type": "Microsoft.Authorization/roleDefinitions",
    "updatedBy": null,
    "updatedOn": "2021-11-11T20:13:47.862868+00:00"
  }
]
```

But I think you made a very good point. We may consider adding `--role-name` for a more efficient search.

> I guess downloading it once for all the role types might be more efficient if you take into account caching. Was just a curiosity i spotted when doing some testing.

Most Azure CLI commands by far have no caching mechanism, as it is impossible to know when the data returned by the ARM service expires.

Take https://learn.microsoft.com/en-us/rest/api/authorization/role-definitions/list?view=rest-authorization-2022-04-01&tabs=HTTP as an example, if a new role is created, CLI will not know that. Quering ARM service again will defeat the purpose of caching.

> Most Azure CLI commands by far have no caching mechanism, as it is impossible to know when the data returned by the ARM service expires.
> 
> Take https://learn.microsoft.com/en-us/rest/api/authorization/role-definitions/list?view=rest-authorization-2022-04-01&tabs=HTTP as an example, if a new role is created, CLI will not know that. Quering ARM service again will defeat the purpose of caching.

Good point!  I was thinking in the context of built in roles but of course that is too limiting.
I did a quick test in https://github.com/Azure/azure-cli/pull/30587 and the number of returned role definitions reduced from 727 to 1, making `az role definition list` much faster.

However, `roleName eq '{role_name}'` filter is not documented in any official documents:

- https://learn.microsoft.com/en-us/azure/role-based-access-control/role-definitions-list
- https://learn.microsoft.com/en-us/rest/api/authorization/role-definitions/list?view=rest-authorization-2022-04-01&tabs=HTTP

Azure CLI has been using `roleName` as a filter since 2016 (https://github.com/Azure/azure-cli/pull/508):

https://github.com/Azure/azure-cli/blob/a43f7f918f600f45c31793a5f4c2351e435e299c/src/azure-cli/azure/cli/command_modules/role/custom.py#L620

I'll internally discuss with RBAC team on this.




Fix (patch):
diff --git a/src/azure-cli/azure/cli/command_modules/role/_params.py b/src/azure-cli/azure/cli/command_modules/role/_params.py
index 8097a588d74..7f97b2ba2d7 100644
--- a/src/azure-cli/azure/cli/command_modules/role/_params.py
+++ b/src/azure-cli/azure/cli/command_modules/role/_params.py
@@ -372,4 +372,5 @@ class PrincipalType(str, Enum):
         c.argument('role_definition_id', options_list=['--name', '-n'], help='the role definition name')
         c.argument('custom_role_only', arg_type=get_three_state_flag(), help='custom roles only(vs. build-in ones)')
         c.argument('role_definition', help="json formatted content which defines the new role.")
-        c.argument('name', arg_type=name_arg_type, completer=get_role_definition_name_completion_list, help="the role's name")
+        c.argument('name', arg_type=name_arg_type, completer=get_role_definition_name_completion_list,
+                   help="Matches the role definition's name (GUID) or roleName (e.g. 'Reader') property.")
diff --git a/src/azure-cli/azure/cli/command_modules/role/custom.py b/src/azure-cli/azure/cli/command_modules/role/custom.py
index fab9aa9fb37..f7731268fe7 100644
--- a/src/azure-cli/azure/cli/command_modules/role/custom.py
+++ b/src/azure-cli/azure/cli/command_modules/role/custom.py
@@ -141,7 +141,11 @@ def delete_role_definition(cmd, name, resource_group_name=None, scope=None,
 
 def _search_role_definitions(cli_ctx, definitions_client, name, scopes, custom_role_only=False):
     for scope in scopes:
-        roles = list(definitions_client.list(scope))
+        # name argument matches the role definition's name (GUID) or roleName (e.g. 'Reader') property.
+        # Only roleName can be used as a filter in Role Definitions - List API.
+        # If name is a GUID, the filtering is performed on the client side.
+        filter_query = f"roleName eq '{name}'" if name and not is_guid(name) else None
+        roles = list(definitions_client.list(scope, filter=filter_query))
         worker = MultiAPIAdaptor(cli_ctx)
         if name:
             roles = [r for r in roles if r.name == name or worker.get_role_property(r, 'role_name') == name]