-
Notifications
You must be signed in to change notification settings - Fork 3.1k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Add new az ad app permissions grant and list commands for OAuth2 permissions for AAD registered apps #6975
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shanepeckham, thanks for the contribution. Good improvement! I left a few comments, mostly to help get your change integrate well with the existing commands
short-summary: Grant the app OAuth2 permissions for the respective apps | ||
examples: | ||
- name: update a native application with OAuth2 permissions with space separated clientIds for AAD apps with TTL of 1 year | ||
text: az ad app update --id e042ec79-34cd-498f-9d9f-1234234 --appid a0322f79-57df-498f-9d9f-12678 --expires 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the command should be grant
, instead of update
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to grant and reworked for new command ad app permission grant
@@ -84,6 +84,7 @@ def load_command_table(self, _): | |||
g.custom_command('delete', 'delete_application') | |||
g.custom_command('list', 'list_apps') | |||
g.custom_show_command('show', 'show_application') | |||
g.custom_command('grant', 'grant_application') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does AAD graph support "permission revoke"? If yes, I suggest you author it like az ad app permission grant
so we can add revoke
and list
later
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to g.custom_command('permission grant', 'grant_application')
Also added g.custom_command('permission list', 'list_granted_application')
_get_role_property(a, 'scope').lower() == scope.lower() | ||
not scope or | ||
include_inherited and re.match(_get_role_property(a, 'scope'), scope, re.I) or | ||
_get_role_property(a, 'scope').lower() == scope.lower() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest you back out the formatting change and only submit the command change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed formatting - accidental change
@@ -667,6 +668,62 @@ def create_application(client, display_name, homepage=None, identifier_uris=None | |||
return result | |||
|
|||
|
|||
def grant_application(cmd, identifier, appid, expires='1'): | |||
if not appid: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unnecessary, for not optional argument, CLI command parser will ensure users supply the values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed optional argument validation
if appid == '00000002-0000-0000-c000-000000000000': # This is AD Read | ||
scope = 'User.Read' | ||
else: | ||
scope = "user_impersonation" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It this a right assumption? If you like to focus on common permission, I suggest add a elif
by comparing with the real id of impersonation
and then keep the guid for the rest. Also move the code block to end since this is only for display.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We pass the scope into the payload and is required to grant permissions, so we can't move it. This is a common well know AAD GUID and as far as I know this scope needs to be set differently as per the code block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Want to get on the same page with you. The 00000002-0000-0000-c000-000000000000
, to me, is the resource id of AAD graph. Scope
in this context, means delegated permissions. Right?
If yes, the code here means if the resource is AAD graph, the delegated permission will be READ
. For native apps, this permission is way too limited, can we also use user_impersonation
, or expose as an argument if we can't make a reasonable default here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed this for now and we can re-implement if required
|
||
if not sps: | ||
raise CLIError("'--id'" + identifier + " does not exist on this directory") | ||
client_spn = sps[0].object_id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest name it sp_object_id
, as object id is not a service principal name. SPN is either related application's id, or app id uri.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented as client_sp_object_id
# Get the Service principal ObjectId for the client app | ||
|
||
sps = list(graph_client.service_principals.list( | ||
filter="appId eq '{}'".format(identifier))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest you just call list_sps
in the same file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extended list_sps
to include search by appid/clientid
def list_sps(client, spn=None, display_name=None, query_filter=None, appid=None):
sub_filters = []
if query_filter:
sub_filters.append(query_filter)
if spn:
sub_filters.append("servicePrincipalNames/any(c:c eq '{}')".format(spn))
if display_name:
sub_filters.append("startswith(displayName,'{}')".format(display_name))
if appid:
sub_filters.append("appId eq '{}'".format(appid))
return client.list(filter=(' and '.join(sub_filters)))
@@ -42,6 +42,11 @@ def load_arguments(self, _): | |||
help="resource scopes and roles the application requires access to. Should be in manifest json format. See examples below for details") | |||
c.argument('native_app', arg_type=get_three_state_flag(), help="an application which can be installed on a user's device or computer") | |||
|
|||
with self.argument_context('ad app grant') as c: | |||
c.argument('identifier', options_list=['--id'], help='clientId of the app you want to grant permissions to') | |||
c.argument('appid', help='clientId of the app you want to grant permissions for') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be --app-id
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented
@@ -42,6 +42,11 @@ def load_arguments(self, _): | |||
help="resource scopes and roles the application requires access to. Should be in manifest json format. See examples below for details") | |||
c.argument('native_app', arg_type=get_three_state_flag(), help="an application which can be installed on a user's device or computer") | |||
|
|||
with self.argument_context('ad app grant') as c: | |||
c.argument('identifier', options_list=['--id'], help='clientId of the app you want to grant permissions to') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest do not redefine, rather let the default one take over
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented
@@ -91,8 +91,7 @@ def load_command_table(self, _): | |||
|
|||
compute_vm_run_sdk = CliCommandType( | |||
operations_tmpl='azure.mgmt.compute.operations.virtual_machine_run_commands_operations#VirtualMachineRunCommandsOperations.{}', | |||
client_factory=cf_run_commands, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rebase and get rid of the change which have been merged into the public repo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rebased
1a34dd9
to
a0ae68d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more suggestions. Please also fix linter error reported by CI
graph_client = _graph_client_factory(cmd.cli_ctx) | ||
|
||
# Get the Service principal ObjectId for the client app | ||
sps = list(list_sps(graph_client.service_principals, appid=identifier)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just use _resolve_service_principal
which returns the object id you need.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented and extended to query by appId
@@ -556,14 +557,16 @@ def list_apps(client, app_id=None, display_name=None, identifier_uri=None, query | |||
return client.list(filter=(' and '.join(sub_filters))) | |||
|
|||
|
|||
def list_sps(client, spn=None, display_name=None, query_filter=None): | |||
def list_sps(client, spn=None, display_name=None, query_filter=None, appid=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please check out my comments below. If you use _resolve_service_principal
, then this change can be reverted
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverted
if appid == '00000002-0000-0000-c000-000000000000': # This is AD Read | ||
scope = 'User.Read' | ||
else: | ||
scope = "user_impersonation" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Want to get on the same page with you. The 00000002-0000-0000-c000-000000000000
, to me, is the resource id of AAD graph. Scope
in this context, means delegated permissions. Right?
If yes, the code here means if the resource is AAD graph, the delegated permission will be READ
. For native apps, this permission is way too limited, can we also use user_impersonation
, or expose as an argument if we can't make a reasonable default here?
@@ -1076,7 +1141,8 @@ def _get_keyvault_client(cli_ctx): | |||
from azure.keyvault import KeyVaultClient, KeyVaultAuthentication | |||
|
|||
def _get_token(server, resource, scope): # pylint: disable=unused-argument | |||
return Profile(cli_ctx=cli_ctx).get_login_credentials(resource)[0]._token_retriever() # pylint: disable=protected-access | |||
return Profile(cli_ctx=cli_ctx).get_login_credentials(resource)[ | |||
0]._token_retriever() # pylint: disable=protected-access |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, please back out the format change here and below. The change here is not correct anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Backed out
5a2acc1
to
2466c9b
Compare
2466c9b
to
4445dc3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One last suggestion hope to help you write less code. Also please get CI passing by doing 2 things:
a. Add group help to ad app permission
b. fix lint error(you can just suppress them using #pylint: disable=no-member)
Once this finalized, I will coordinate to get you spec pr and sdk published
@@ -884,9 +931,13 @@ def delete_service_principal_credential(cmd, identifier, key_id, cert=False): | |||
key_id, identifier)) | |||
|
|||
|
|||
def _resolve_service_principal(client, identifier): | |||
def _resolve_service_principal(client, identifier=None, appid=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please double check this is needed? appid and app id uri are both service principal names which the existing query below should cover it. I did a quick check just now for reference, but let me know if you do need it.
(env) D:\sdk\azure-cli>az ad app show --id e96b494b-61a2-4a42-a33c-e11ab1f1a6b6
{
"appId": "e96b494b-61a2-4a42-a33c-e11ab1f1a6b6",
<omit irrelevant fields>
}
(env) D:\sdk\azure-cli>az ad sp show --id e96b494b-61a2-4a42-a33c-e11ab1f1a6b6
{
"appDisplayName": "yugangw-op2",
"appId": "e96b494b-61a2-4a42-a33c-e11ab1f1a6b6",
"appOwnerTenantId": "54826b22-38d6-4fb2-bad9-b7b93a3e9c5a",
"objectId": "e0a58f57-dcae-48ab-98b6-ee173a6097fa",
"objectType": "ServicePrincipal",
servicePrincipalNames": [
"http://yugangw-op2",
"e96b494b-61a2-4a42-a33c-e11ab1f1a6b6"
],
"servicePrincipalType": "Application",
<omit irrelevant fields>
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yugangw-msft I can confirm that the appId can be the same as the objectId, but it is not always. In all of my test cases, like for example creating an app in the portal, the SP associated with the app will be autogenerated and needs to be searched for by the appId. The code is definitely needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
object id is irrelevant here which is different thing from the app id. I am talking about the app id of the application. The service principal provisioned in the tenant for the application should have the app id as part of the servicePrincipalNames
array. If you see the appid is not in the servicePrincipalNames
of the SP then your change is definitely needed. To get my comment clear, I am talking about the following array in the SP object
servicePrincipalNames": [
"http://yugangw-op2",
"e96b494b-61a2-4a42-a33c-e11ab1f1a6b6" <---this one
],
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yugangw-msft Not sure if I am missing something here, this is the flow:
- I have the appId/clientId of my app
- I need to get the objectId of the SP associated with my app
- The app array does not contain the objectId of the SP
- I cannot query the SP by the objectId as I do not have the objectId, I only have the appId
- I query the SP by the appId which gives me the SP ObjectId
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The flow is right. My comment is on step 5 that you don't need to update _resolve_service_principal
to take a new argument of appid
, rather just pass the app id to the existing argument of identifier
which should work.
The reason is the _resolve_service_principal
queries for SP using servicePrincipalNames
, and appid
is one of service principal name, so the query should just work.
If you have tried but not working, please let me know
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yugangw-msft , thank you, in my tests this was not working for me. Will try again
just pass the app id to the existing argument of identifier which should work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks. Is it easy for you to share a repro for me to try out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yugangw-msft , you are right - my tests were bad, will remove appId from query and rebase
All are taken care through #7611 |
This is needed so that we can programmatically grant and list OAuth2 consent to an application in Azure Active Directory. This is the supported and correct way to do this respecting customer approval workflows. Example use case includes Azure Kubernetes Service RBAC integration with Azure Active Directory.
This PR has the following dependencies upstream that need to be merged first:
New command has the following help format:
Usage:
This checklist is used to make sure that common guidelines for a pull request are followed.