-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Subscription RP] create subscription command #119
Changes from 5 commits
b04b404
857a4f6
6110bb9
dd9d483
d910a35
4c499ec
ad1e07b
74c7979
e65df5d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,16 +3,86 @@ | |
# Licensed under the MIT License. See License.txt in the project root for license information. | ||
# -------------------------------------------------------------------------------------------- | ||
|
||
from azext_subscription.subscription.models import SubscriptionDefinition | ||
from knack.log import get_logger | ||
from knack.util import CLIError | ||
from azext_subscription.subscription.models import (SubscriptionCreationParameters, AdPrincipal) | ||
|
||
logger = get_logger(__name__) | ||
|
||
def cli_subscription_create_subscription_definition(client, subscription_definition_name, | ||
offer_type, subscription_display_name=None): | ||
if subscription_display_name is None: | ||
subscription_display_name = subscription_definition_name | ||
|
||
new_def = SubscriptionDefinition( | ||
subscription_display_name=subscription_display_name, | ||
offer_type=offer_type) | ||
def _get_object_id_by_spn(graph_client, spn): | ||
accounts = list(graph_client.service_principals.list( | ||
filter="servicePrincipalNames/any(c:c eq '{}')".format(spn))) | ||
if not accounts: | ||
logger.warning("Unable to find user with spn '%s'", spn) | ||
return None | ||
if len(accounts) > 1: | ||
logger.warning("Multiple service principals found with spn '%s'. " | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code is copied from KeyVault's policy handling. The parameter names (object_id/spn/upn) also reflect what both the KeyVault and AuthZ modules are doing. Ideally there'd be a separate change to address this feedback across all modules. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good to me |
||
"You can avoid this by specifying object id.", spn) | ||
return None | ||
return accounts[0].object_id | ||
|
||
return client.create(subscription_definition_name, new_def) | ||
|
||
def _get_object_id_by_upn(graph_client, upn): | ||
accounts = list(graph_client.users.list(filter="userPrincipalName eq '{}'".format(upn))) | ||
if not accounts: | ||
logger.warning("Unable to find user with upn '%s'", upn) | ||
return None | ||
if len(accounts) > 1: | ||
logger.warning("Multiple users principals found with upn '%s'. " | ||
"You can avoid this by specifying object id.", upn) | ||
return None | ||
return accounts[0].object_id | ||
|
||
|
||
def _get_object_id_from_subscription(graph_client, subscription): | ||
if subscription['user']: | ||
if subscription['user']['type'] == 'user': | ||
return _get_object_id_by_upn(graph_client, subscription['user']['name']) | ||
elif subscription['user']['type'] == 'servicePrincipal': | ||
return _get_object_id_by_spn(graph_client, subscription['user']['name']) | ||
else: | ||
logger.warning("Unknown user type '%s'", subscription['user']['type']) | ||
else: | ||
logger.warning('Current credentials are not from a user or service principal. ' | ||
'Azure Key Vault does not work with certificate credentials.') | ||
return None | ||
|
||
|
||
def _get_object_id(graph_client, subscription=None, spn=None, upn=None): | ||
if spn: | ||
return _get_object_id_by_spn(graph_client, spn) | ||
if upn: | ||
return _get_object_id_by_upn(graph_client, upn) | ||
return _get_object_id_from_subscription(graph_client, subscription) | ||
|
||
|
||
def _object_id_args_helper(cli_ctx, object_id=None, spn=None, upn=None): | ||
if not object_id: | ||
from azure.cli.core._profile import Profile | ||
from azure.graphrbac import GraphRbacManagementClient | ||
|
||
profile = Profile(cli_ctx=cli_ctx) | ||
cred, _, tenant_id = profile.get_login_credentials( | ||
resource=cli_ctx.cloud.endpoints.active_directory_graph_resource_id) | ||
graph_client = GraphRbacManagementClient(cred, | ||
tenant_id, | ||
base_url=cli_ctx.cloud.endpoints.active_directory_graph_resource_id) | ||
object_id = _get_object_id(graph_client, spn=spn, upn=upn) | ||
if not object_id: | ||
raise CLIError('Unable to get object id from principal name.') | ||
return object_id | ||
|
||
|
||
def cli_subscription_create(cmd, client, enrollment_account_name, offer_type, | ||
display_name=None, object_id="", spn="", upn=""): | ||
owners = [_object_id_args_helper(cmd.cli_ctx, object_id=x) for x | ||
in object_id.split(',') if object_id] + \ | ||
[_object_id_args_helper(cmd.cli_ctx, spn=x) for x in spn.split(',') if spn] + \ | ||
[_object_id_args_helper(cmd.cli_ctx, upn=x) for x in upn.split(',') if upn] | ||
creation_parameters = SubscriptionCreationParameters( | ||
display_name=display_name, | ||
offer_type=offer_type, | ||
owners=[AdPrincipal(object_id=x) for x in owners]) | ||
|
||
return client.create_subscription_in_enrollment_account(enrollment_account_name, creation_parameters) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# coding=utf-8 | ||
# -------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for | ||
# license information. | ||
# | ||
# Code generated by Microsoft (R) AutoRest Code Generator. | ||
# Changes may cause incorrect behavior and will be lost if the code is | ||
# regenerated. | ||
# -------------------------------------------------------------------------- | ||
|
||
from msrest.serialization import Model | ||
|
||
|
||
class AdPrincipal(Model): | ||
"""Active Directory Principal for subscription creation delegated permission. | ||
|
||
All required parameters must be populated in order to send to Azure. | ||
|
||
:param object_id: Required. Object id of the Principal | ||
:type object_id: str | ||
""" | ||
|
||
_validation = { | ||
'object_id': {'required': True}, | ||
} | ||
|
||
_attribute_map = { | ||
'object_id': {'key': 'objectId', 'type': 'str'}, | ||
} | ||
|
||
def __init__(self, **kwargs): | ||
super(AdPrincipal, self).__init__(**kwargs) | ||
self.object_id = kwargs.get('object_id', None) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
# coding=utf-8 | ||
# -------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for | ||
# license information. | ||
# | ||
# Code generated by Microsoft (R) AutoRest Code Generator. | ||
# Changes may cause incorrect behavior and will be lost if the code is | ||
# regenerated. | ||
# -------------------------------------------------------------------------- | ||
|
||
from msrest.serialization import Model | ||
|
||
|
||
class AdPrincipal(Model): | ||
"""Active Directory Principal for subscription creation delegated permission. | ||
|
||
All required parameters must be populated in order to send to Azure. | ||
|
||
:param object_id: Required. Object id of the Principal | ||
:type object_id: str | ||
""" | ||
|
||
_validation = { | ||
'object_id': {'required': True}, | ||
} | ||
|
||
_attribute_map = { | ||
'object_id': {'key': 'objectId', 'type': 'str'}, | ||
} | ||
|
||
def __init__(self, *, object_id: str, **kwargs) -> None: | ||
super(AdPrincipal, self).__init__(**kwargs) | ||
self.object_id = object_id |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# coding=utf-8 | ||
# -------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for | ||
# license information. | ||
# | ||
# Code generated by Microsoft (R) AutoRest Code Generator. | ||
# Changes may cause incorrect behavior and will be lost if the code is | ||
# regenerated. | ||
# -------------------------------------------------------------------------- | ||
|
||
from msrest.serialization import Model | ||
from msrest.exceptions import HttpOperationError | ||
|
||
|
||
class ErrorResponse(Model): | ||
"""Describes the format of Error response. | ||
|
||
:param code: Error code | ||
:type code: str | ||
:param message: Error message indicating why the operation failed. | ||
:type message: str | ||
""" | ||
|
||
_attribute_map = { | ||
'code': {'key': 'code', 'type': 'str'}, | ||
'message': {'key': 'message', 'type': 'str'}, | ||
} | ||
|
||
def __init__(self, *, code: str=None, message: str=None, **kwargs) -> None: | ||
super(ErrorResponse, self).__init__(**kwargs) | ||
self.code = code | ||
self.message = message | ||
|
||
|
||
class ErrorResponseException(HttpOperationError): | ||
"""Server responsed with exception of type: 'ErrorResponse'. | ||
|
||
:param deserialize: A deserializer | ||
:param response: Server response to be deserialized. | ||
""" | ||
|
||
def __init__(self, deserialize, response, *args): | ||
|
||
super(ErrorResponseException, self).__init__(deserialize, response, 'ErrorResponse', *args) |
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.
There is no help text for
--enrollment-account-name
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
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 is fine to be like this since it is an extension command, but if you plan to convert the extension to a core command module, I would suggest fold all 3 into a single
--owners
argument and your command do the hard work by making the graph call to figure out what the real type it is. Per usage feedback, lots of users have a hard time to figure out whose graph objects, nor they have any passions to learn those terms. By just seeing thespn
,upn
, they might already get lost.