diff --git a/src/serviceconnector-passwordless/HISTORY.rst b/src/serviceconnector-passwordless/HISTORY.rst index b363a7e3a96..9d10b1e5f7a 100644 --- a/src/serviceconnector-passwordless/HISTORY.rst +++ b/src/serviceconnector-passwordless/HISTORY.rst @@ -2,6 +2,10 @@ Release History =============== +0.3.1 +++++++ +* Support User-Assigned Managed Identity and Service Principal. + 0.3.0 ++++++ * Add extension information in API request. diff --git a/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_credential_free.py b/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_credential_free.py index b26e533d74b..7dc443bc9fb 100644 --- a/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_credential_free.py +++ b/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_credential_free.py @@ -11,7 +11,8 @@ from azure.cli.core.azclierror import ( AzureConnectionError, ValidationError, - CLIInternalError + CLIInternalError, + ResourceNotFoundError ) from azure.cli.core.extension.operations import _install_deps_for_psycopg2, _run_pip from azure.cli.core._profile import Profile @@ -34,7 +35,9 @@ AUTHTYPES = { AUTH_TYPE.SystemIdentity: 'systemAssignedIdentity', - AUTH_TYPE.UserAccount: 'userAccount' + AUTH_TYPE.UserIdentity: 'userAssignedIdentity', + AUTH_TYPE.ServicePrincipalSecret: 'servicePrincipalSecret', + AUTH_TYPE.UserAccount: 'userAccount', } IP_ADDRESS_CHECKER = 'https://api.ipify.org' OPEN_ALL_IP_MESSAGE = 'Do you want to enable access for all IPs to allow local environment connecting to database?' @@ -47,7 +50,11 @@ def get_enable_mi_for_db_linker_func(yes=False): def enable_mi_for_db_linker(cmd, source_id, target_id, auth_info, client_type, connection_name): # return if connection is not for db mi - if auth_info['auth_type'] not in {AUTHTYPES[AUTH_TYPE.SystemIdentity], AUTHTYPES[AUTH_TYPE.UserAccount]}: + if auth_info['auth_type'] not in [AUTHTYPES[AUTH_TYPE.SystemIdentity], + AUTHTYPES[AUTH_TYPE.UserIdentity], + AUTHTYPES[AUTH_TYPE.UserAccount], + AUTHTYPES[AUTH_TYPE.ServicePrincipalSecret] + ]: return None source_type = get_source_resource_name(cmd) @@ -56,7 +63,7 @@ def enable_mi_for_db_linker(cmd, source_id, target_id, auth_info, client_type, c if source_handler is None: return None target_handler = getTargetHandler( - cmd, target_id, target_type, auth_info['auth_type'], client_type, connection_name, skip_prompt=yes) + cmd, target_id, target_type, auth_info, client_type, connection_name, skip_prompt=yes) if target_handler is None: return None @@ -66,23 +73,56 @@ def enable_mi_for_db_linker(cmd, source_id, target_id, auth_info, client_type, c if user_object_id is None: raise Exception( - "No object id for user {}".format(target_handler.login_username)) + "No object id for current user {}".format(target_handler.login_username)) target_handler.user_object_id = user_object_id if source_type != RESOURCE.Local: - # enable source mi - source_object_id = source_handler.get_identity_pid() - target_handler.identity_object_id = source_object_id - try: - identity_info = run_cli_cmd( - 'az ad sp show --id {}'.format(source_object_id), 15, 10) - target_handler.identity_client_id = identity_info.get('appId') - target_handler.identity_name = identity_info.get('displayName') - except CLIInternalError as e: - if 'AADSTS530003' in e.error_msg: - logger.warning( - 'Please ask your IT department for help to join this device to Azure Active Directory.') - raise e + source_object_id = None + if auth_info['auth_type'] == AUTHTYPES[AUTH_TYPE.SystemIdentity]: + # enable source mi + source_object_id = source_handler.get_identity_pid() + target_handler.identity_object_id = source_object_id + try: + identity_info = run_cli_cmd( + 'az ad sp show --id {}'.format(source_object_id), 15, 10) + target_handler.identity_client_id = identity_info.get( + 'appId') + target_handler.identity_name = identity_info.get( + 'displayName') + except CLIInternalError as e: + if 'AADSTS530003' in e.error_msg: + logger.warning( + 'Please ask your IT department for help to join this device to Azure Active Directory.') + raise e + elif auth_info['auth_type'] == AUTHTYPES[AUTH_TYPE.UserIdentity]: + mi_client_id = auth_info.get('client_id') + mi_sub_id = auth_info.get('subscription_id') + umi_info = run_cli_cmd( + f'az identity list --subscription {mi_sub_id} --query "[?clientId==\'{mi_client_id}\']"') + if umi_info is None or len(umi_info) == 0: + raise ResourceNotFoundError( + "No identity found for client id {}".format(mi_client_id)) + source_object_id = umi_info[0].get('principalId') + target_handler.identity_object_id = source_object_id + target_handler.identity_client_id = mi_client_id + target_handler.identity_name = umi_info[0].get('name') + elif auth_info['auth_type'] == AUTHTYPES[AUTH_TYPE.ServicePrincipalSecret]: + sp_client_id = auth_info.get('client_id') + sp_object_id = auth_info.get('principal_id') + try: + sp_info = run_cli_cmd( + 'az ad sp show --id {}'.format(sp_client_id)) + if sp_info is None: + raise ResourceNotFoundError( + "Not found the service principal with client id {}".format(sp_client_id)) + target_handler.identity_object_id = sp_object_id + target_handler.identity_client_id = sp_client_id + target_handler.identity_name = sp_info.get('displayName') + except CLIInternalError as e: + if 'AADSTS530003' in e.error_msg: + logger.warning( + 'Please ask your IT department for help to join this device to Azure Active Directory.') + raise e # enable target aad authentication and set login user as db aad admin target_handler.enable_target_aad_auth() @@ -97,21 +137,22 @@ def enable_mi_for_db_linker(cmd, source_id, target_id, auth_info, client_type, c # pylint: disable=no-self-use, unused-argument, too-many-instance-attributes -def getTargetHandler(cmd, target_id, target_type, auth_type, client_type, connection_name, skip_prompt): +def getTargetHandler(cmd, target_id, target_type, auth_info, client_type, connection_name, skip_prompt): if target_type in {RESOURCE.Sql}: - return SqlHandler(cmd, target_id, target_type, auth_type, connection_name, skip_prompt) + return SqlHandler(cmd, target_id, target_type, auth_info, connection_name, skip_prompt) if target_type in {RESOURCE.Postgres}: - return PostgresSingleHandler(cmd, target_id, target_type, auth_type, connection_name, skip_prompt) + return PostgresSingleHandler(cmd, target_id, target_type, auth_info, connection_name, skip_prompt) if target_type in {RESOURCE.PostgresFlexible}: - return PostgresFlexHandler(cmd, target_id, target_type, auth_type, connection_name, skip_prompt) + return PostgresFlexHandler(cmd, target_id, target_type, auth_info, connection_name, skip_prompt) if target_type in {RESOURCE.MysqlFlexible}: - return MysqlFlexibleHandler(cmd, target_id, target_type, auth_type, connection_name, skip_prompt) + return MysqlFlexibleHandler(cmd, target_id, target_type, auth_info, connection_name, skip_prompt) return None class TargetHandler: cmd = None auth_type = "" + auth_info = None tenant_id = "" subscription = "" @@ -133,7 +174,7 @@ class TargetHandler: skip_prompt = False - def __init__(self, cmd, target_id, target_type, auth_type, connection_name, skip_prompt): + def __init__(self, cmd, target_id, target_type, auth_info, connection_name, skip_prompt): self.cmd = cmd self.target_id = target_id self.target_type = target_type @@ -142,7 +183,8 @@ def __init__(self, cmd, target_id, target_type, auth_type, connection_name, skip target_segments = parse_resource_id(target_id) self.subscription = target_segments.get('subscription') self.resource_group = target_segments.get('resource_group') - self.auth_type = auth_type + self.auth_type = auth_info['auth_type'] + self.auth_info = auth_info self.login_username = run_cli_cmd( 'az account show').get("user").get("name") self.login_usertype = run_cli_cmd( @@ -166,6 +208,17 @@ def set_target_firewall(self, is_add, ip_name, start_ip=None, end_ip=None): def create_aad_user(self): return + def get_auth_flag(self): + if self.auth_type == AUTHTYPES[AUTH_TYPE.UserAccount]: + return '--user-account' + if self.auth_type == AUTHTYPES[AUTH_TYPE.SystemIdentity]: + return '--system-identity' + if self.auth_type == AUTHTYPES[AUTH_TYPE.UserIdentity]: + return '--user-identity' + if self.auth_type == AUTHTYPES[AUTH_TYPE.ServicePrincipalSecret]: + return '--service-principal' + return None + def get_auth_config(self, user_object_id): if self.auth_type == AUTHTYPES[AUTH_TYPE.UserAccount]: return { @@ -178,6 +231,21 @@ def get_auth_config(self, user_object_id): 'auth_type': self.auth_type, 'username': self.aad_username, } + if self.auth_type == AUTHTYPES[AUTH_TYPE.UserIdentity]: + return { + 'auth_type': self.auth_type, + 'username': self.aad_username, + 'client_id': self.identity_client_id, + 'subscription_id': self.auth_info['subscription_id'], + } + if self.auth_type == AUTHTYPES[AUTH_TYPE.ServicePrincipalSecret]: + return { + 'auth_type': self.auth_type, + 'username': self.aad_username, + 'principal_id': self.identity_object_id, + 'client_id': self.identity_client_id, + 'secret': self.auth_info['secret'], + } return None @@ -186,8 +254,9 @@ class MysqlFlexibleHandler(TargetHandler): server = "" dbname = "" - def __init__(self, cmd, target_id, target_type, auth_type, connection_name, skip_prompt): - super().__init__(cmd, target_id, target_type, auth_type, connection_name, skip_prompt) + def __init__(self, cmd, target_id, target_type, auth_info, connection_name, skip_prompt): + super().__init__(cmd, target_id, target_type, + auth_info, connection_name, skip_prompt) self.endpoint = cmd.cli_ctx.cloud.suffixes.mysql_server_endpoint target_segments = parse_resource_id(target_id) self.server = target_segments.get('name') @@ -207,8 +276,8 @@ def set_user_admin(self, user_object_id, **kwargs): # set user as AAD admin if mysql_identity_id is None: raise ValidationError( - "Provide '{} mysql-identity-id=xx' to set {} as AAD administrator.".format( - '--system-identity' if self.auth_type == AUTHTYPES[AUTH_TYPE.SystemIdentity] else '--user-account', self.login_username)) + "Provide '{} mysql-identity-id=' to update AAD authentication.".format( + self.get_auth_flag())) mysql_umi = run_cli_cmd( 'az mysql flexible-server identity list -g {} -s {} --subscription {}'.format(self.resource_group, self.server, self.subscription)) if (not mysql_umi) or (not mysql_umi.get("userAssignedIdentities")) or mysql_identity_id not in mysql_umi.get("userAssignedIdentities"): @@ -244,7 +313,8 @@ def create_aad_user(self): raise CLIInternalError( 'Unable to prompt for confirmation as no tty available. Use --yes.') from e # allow public access - self.set_target_firewall(True, ip_name, '0.0.0.0', '255.255.255.255') + self.set_target_firewall( + True, ip_name, '0.0.0.0', '255.255.255.255') # create again self.create_aad_user_in_mysql(connection_kwargs, query_list) finally: @@ -255,7 +325,8 @@ def set_target_firewall(self, is_add, ip_name, start_ip=None, end_ip=None): target = run_cli_cmd( 'az mysql flexible-server show --ids {}'.format(self.target_id)) if target.get('network').get('publicNetworkAccess') == "Disabled": - raise AzureConnectionError("The target resource doesn't allow public access. Connection can't be created.") + raise AzureConnectionError( + "The target resource doesn't allow public access. Connection can't be created.") logger.warning("Add firewall rule %s %s - %s...%s", ip_name, start_ip, end_ip, ('(it will be removed after connection is created)' if self.auth_type != AUTHTYPES[ AUTH_TYPE.UserAccount] else '(Please delete it manually if it has security risk.)')) @@ -267,7 +338,8 @@ def set_target_firewall(self, is_add, ip_name, start_ip=None, end_ip=None): else: if self.auth_type == AUTHTYPES[AUTH_TYPE.UserAccount]: return - logger.warning("Remove database server firewall rule %s to recover...", ip_name) + logger.warning( + "Remove database server firewall rule %s to recover...", ip_name) try: run_cli_cmd( 'az mysql flexible-server firewall-rule delete --resource-group {0} --name {1} --rule-name {2} ' @@ -275,7 +347,8 @@ def set_target_firewall(self, is_add, ip_name, start_ip=None, end_ip=None): self.resource_group, self.server, ip_name, self.subscription) ) except CLIInternalError as e: - logger.warning("Can't remove firewall rule %s. Please manually delete it to avoid security issue. %s", ip_name, str(e)) + logger.warning( + "Can't remove firewall rule %s. Please manually delete it to avoid security issue. %s", ip_name, str(e)) def create_aad_user_in_mysql(self, connection_kwargs, query_list): if not is_packaged_installed('pymysql'): @@ -291,7 +364,8 @@ def create_aad_user_in_mysql(self, connection_kwargs, query_list): connection_kwargs['client_flag'] = CLIENT.MULTI_STATEMENTS try: connection = pymysql.connect(**connection_kwargs) - logger.warning("Adding new AAD user %s to database...", self.aad_username) + logger.warning( + "Adding new AAD user %s to database...", self.aad_username) cursor = connection.cursor() for q in query_list: if q: @@ -302,12 +376,14 @@ def create_aad_user_in_mysql(self, connection_kwargs, query_list): logger.warning( "Query %s, error: %s", q, str(e)) except pymysql.Error as e: - raise AzureConnectionError("Fail to connect mysql. " + str(e)) from e + raise AzureConnectionError( + "Fail to connect mysql. " + str(e)) from e if cursor is not None: try: cursor.close() except Exception as e: # pylint: disable=broad-except - raise CLIInternalError("connection close failed." + str(e)) from e + raise CLIInternalError( + "Connection close failed." + str(e)) from e def get_connection_string(self): password = run_cli_cmd( @@ -343,8 +419,9 @@ class SqlHandler(TargetHandler): dbname = "" ip = "" - def __init__(self, cmd, target_id, target_type, auth_type, connection_name, skip_prompt): - super().__init__(cmd, target_id, target_type, auth_type, connection_name, skip_prompt) + def __init__(self, cmd, target_id, target_type, auth_info, connection_name, skip_prompt): + super().__init__(cmd, target_id, target_type, + auth_info, connection_name, skip_prompt) self.endpoint = cmd.cli_ctx.cloud.suffixes.sql_server_hostname target_segments = parse_resource_id(target_id) self.server = target_segments.get('name') @@ -392,7 +469,8 @@ def set_target_firewall(self, is_add, ip_name, start_ip=None, end_ip=None): 'az sql server show --ids {}'.format(self.target_id)) # logger.warning("Update database server firewall rule to connect...") if target.get('publicNetworkAccess') == "Disabled": - raise AzureConnectionError("The target resource doesn't allow public access. Please enable it manually and try again.") + raise AzureConnectionError( + "The target resource doesn't allow public access. Please enable it manually and try again.") logger.warning("Add firewall rule %s %s - %s...%s", ip_name, start_ip, end_ip, ('(it will be removed after connection is created)' if self.auth_type != AUTHTYPES[ AUTH_TYPE.UserAccount] else '(Please delete it manually if it has security risk.)')) @@ -404,14 +482,16 @@ def set_target_firewall(self, is_add, ip_name, start_ip=None, end_ip=None): else: if self.auth_type == AUTHTYPES[AUTH_TYPE.UserAccount]: return - logger.warning("Remove database server firewall rule %s to recover...", ip_name) + logger.warning( + "Remove database server firewall rule %s to recover...", ip_name) try: run_cli_cmd( 'az sql server firewall-rule delete -g {0} -s {1} -n {2} --subscription {3}'.format( self.resource_group, self.server, ip_name, self.subscription) ) except CLIInternalError as e: - logger.warning("Can't remove firewall rule %s. Please manually delete it to avoid security issue. %s", ip_name, str(e)) + logger.warning( + "Can't remove firewall rule %s. Please manually delete it to avoid security issue. %s", ip_name, str(e)) def create_aad_user_in_sql(self, connection_args, query_list): @@ -432,7 +512,8 @@ def create_aad_user_in_sql(self, connection_args, query_list): try: with pyodbc.connect(connection_args.get("connection_string").format(driver=drivers[0]), attrs_before=connection_args.get("attrs_before")) as conn: with conn.cursor() as cursor: - logger.warning("Adding new AAD user %s to database...", self.aad_username) + logger.warning( + "Adding new AAD user %s to database...", self.aad_username) for execution_query in query_list: try: logger.debug(execution_query) @@ -462,7 +543,7 @@ def get_connection_string(self): return {'connection_string': conn_string, 'attrs_before': {SQL_COPT_SS_ACCESS_TOKEN: token_struct}} def get_create_query(self): - if self.auth_type == AUTHTYPES[AUTH_TYPE.SystemIdentity]: + if self.auth_type in [AUTHTYPES[AUTH_TYPE.SystemIdentity], AUTHTYPES[AUTH_TYPE.UserIdentity], AUTHTYPES[AUTH_TYPE.ServicePrincipalSecret]]: self.aad_username = self.identity_name if self.auth_type == AUTHTYPES[AUTH_TYPE.UserAccount]: self.aad_username = self.login_username @@ -481,8 +562,9 @@ class PostgresFlexHandler(TargetHandler): dbname = "" ip = "" - def __init__(self, cmd, target_id, target_type, auth_type, connection_name, skip_prompt): - super().__init__(cmd, target_id, target_type, auth_type, connection_name, skip_prompt) + def __init__(self, cmd, target_id, target_type, auth_info, connection_name, skip_prompt): + super().__init__(cmd, target_id, target_type, + auth_info, connection_name, skip_prompt) self.endpoint = cmd.cli_ctx.cloud.suffixes.postgresql_server_endpoint target_segments = parse_resource_id(target_id) self.db_server = target_segments.get('name') @@ -531,7 +613,8 @@ def create_aad_user(self): except NoTTYException as e: raise CLIInternalError( 'Unable to prompt for confirmation as no tty available. Use --yes.') from e - self.set_target_firewall(True, ip_name, '0.0.0.0', '255.255.255.255') + self.set_target_firewall( + True, ip_name, '0.0.0.0', '255.255.255.255') # create again self.create_aad_user_in_pg(connection_string, query_list) finally: @@ -542,7 +625,8 @@ def set_target_firewall(self, is_add, ip_name, start_ip=None, end_ip=None): target = run_cli_cmd( 'az postgres flexible-server show --ids {}'.format(self.target_id)) if target.get('network').get('publicNetworkAccess') == "Disabled": - raise AzureConnectionError("The target resource doesn't allow public access. Connection can't be created.") + raise AzureConnectionError( + "The target resource doesn't allow public access. Connection can't be created.") logger.warning("Add firewall rule %s %s - %s...%s", ip_name, start_ip, end_ip, ('(it will be removed after connection is created)' if self.auth_type != AUTHTYPES[ AUTH_TYPE.UserAccount] else '(Please delete it manually if it has security risk.)')) @@ -554,7 +638,8 @@ def set_target_firewall(self, is_add, ip_name, start_ip=None, end_ip=None): else: if self.auth_type == AUTHTYPES[AUTH_TYPE.UserAccount]: return - logger.warning("Remove database server firewall rule %s to recover...", ip_name) + logger.warning( + "Remove database server firewall rule %s to recover...", ip_name) try: run_cli_cmd( 'az postgres flexible-server firewall-rule delete --resource-group {0} --name {1} --rule-name {2} ' @@ -562,7 +647,8 @@ def set_target_firewall(self, is_add, ip_name, start_ip=None, end_ip=None): self.resource_group, self.db_server, ip_name, self.subscription) ) except CLIInternalError as e: - logger.warning("Can't remove firewall rule %s. Please manually delete it to avoid security issue. %s", ip_name, str(e)) + logger.warning( + "Can't remove firewall rule %s. Please manually delete it to avoid security issue. %s", ip_name, str(e)) def create_aad_user_in_pg(self, conn_string, query_list): if not is_packaged_installed('psycopg2'): @@ -675,11 +761,14 @@ def set_target_firewall(self, is_add, ip_name, start_ip=None, end_ip=None): else: if self.auth_type == AUTHTYPES[AUTH_TYPE.UserAccount]: return - logger.warning("Remove database server firewall rule %s to recover...", ip_name) + logger.warning( + "Remove database server firewall rule %s to recover...", ip_name) try: - run_cli_cmd('az postgres server firewall-rule delete -g {0} -s {1} -n {2} -y'.format(rg, server, ip_name)) + run_cli_cmd( + 'az postgres server firewall-rule delete -g {0} -s {1} -n {2} -y'.format(rg, server, ip_name)) except CLIInternalError as e: - logger.warning("Can't remove firewall rule %s. Please manually delete it to avoid security issue. %s", ip_name, str(e)) + logger.warning( + "Can't remove firewall rule %s. Please manually delete it to avoid security issue. %s", ip_name, str(e)) def get_connection_string(self): password = run_cli_cmd( diff --git a/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_help.py b/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_help.py index ffce84eadce..e59944801ea 100644 --- a/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_help.py +++ b/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_help.py @@ -5,4 +5,635 @@ # -------------------------------------------------------------------------------------------- from knack.help_files import helps # pylint: disable=unused-import -# skip because the CLI module provides the command help +from azure.cli.command_modules.serviceconnector._resource_config import ( + SOURCE_RESOURCES, + SOURCE_RESOURCES_PARAMS, + TARGET_RESOURCES, + LOCAL_CONNECTION_PARAMS +) +from azure.cli.command_modules.serviceconnector._utils import should_load_source +from azure.cli.command_modules.serviceconnector._addon_factory import AddonFactory +from ._resource_config import ( + AUTH_TYPE, + RESOURCE, + TARGET_RESOURCES_PARAMS, + SUPPORTED_AUTH_TYPE, +) + + +# pylint: disable=consider-using-f-string +def get_source_resource_params(resource): + if resource == RESOURCE.Local: + params = LOCAL_CONNECTION_PARAMS + else: + params = SOURCE_RESOURCES_PARAMS.get(resource).values() + + param_str = '' + for param in params: + option = param.get('options')[-1] + placeholder = param.get('placeholder') + param_str += '{} {} '.format(option, placeholder) + + return param_str.strip() + + +def get_target_resource_params(resource): + params = TARGET_RESOURCES_PARAMS.get(resource).values() + + param_str = '' + for param in params: + option = param.get('options')[-1] + placeholder = param.get('placeholder') + param_str += '{} {} '.format(option, placeholder) + + return param_str.strip() + + +def get_auth_info_params(auth_type): + auth_params_map = { + AUTH_TYPE.Secret: '--secret name=XX secret=XX', + AUTH_TYPE.SecretAuto: '--secret', + AUTH_TYPE.SystemIdentity: '--system-identity', + AUTH_TYPE.ServicePrincipalSecret: '--service-principal client-id=XX object-id=XX secret=XX', + AUTH_TYPE.UserIdentity: '--user-identity client-id=XX subs-id=XX', + AUTH_TYPE.UserAccount: '--user-account', + } + + return auth_params_map.get(auth_type) + + +def get_source_display_name(sourcename): + display_name = sourcename + if sourcename == RESOURCE.SpringCloud.value: + display_name = 'spring app' + return display_name + + +for source in SOURCE_RESOURCES: + if not should_load_source(source): + continue + + source_id = SOURCE_RESOURCES.get(source) + source_params = get_source_resource_params(source) + connection_id = ('/subscriptions/{subscription}/resourceGroups/{source_resource_group}/providers/' + 'Microsoft.Web/sites/{site}/providers/Microsoft.ServiceLinker/linkers/{linker}') + source_display_name = get_source_display_name(source.value) + + helps['{source} connection'.format(source=source.value)] = """ + type: group + short-summary: Commands to manage {source_display_name} connections + """.format(source_display_name=source_display_name) + + helps['{source} connection list-support-types'.format(source=source.value)] = """ + type: command + short-summary: List client types and auth types supported by {source_display_name} connections. + examples: + - name: List all {source_display_name} supported target resource types and auth types + text: |- + az {source} connection list-support-types -o table + - name: List {source_display_name} supported auth types for a specific target resource type + text: |- + az {source} connection list-support-types --target-type storage-blob -o table + """.format(source=source.value, source_display_name=source_display_name) + + helps['{source} connection list'.format(source=source.value)] = """ + type: command + short-summary: List connections of a {source_display_name}. + examples: + - name: List {source_display_name} connections interactively + text: |- + az {source} connection list + - name: List {source_display_name} connections by source resource name + text: |- + az {source} connection list {source_params} + - name: List {source_display_name} connections by source resource id + text: |- + az {source} connection list --source-id {source_id} + """.format( + source=source.value, + source_params=source_params, + source_id=source_id, + source_display_name=source_display_name) + + helps['{source} connection delete'.format(source=source.value)] = """ + type: command + short-summary: Delete a {source_display_name} connection. + examples: + - name: Delete a {source_display_name} connection interactively + text: |- + az {source} connection delete + - name: Delete a {source_display_name} connection by connection name + text: |- + az {source} connection delete {source_params} --connection MyConnection + - name: Delete a {source_display_name} connection by connection id + text: |- + az {source} connection delete --id {connection_id} + """.format( + source=source.value, + source_params=source_params, + connection_id=connection_id, + source_display_name=source_display_name) + + helps['{source} connection list-configuration'.format(source=source.value)] = """ + type: command + short-summary: List source configurations of a {source_display_name} connection. + examples: + - name: List a connection's source configurations interactively + text: |- + az {source} connection list-configuration + - name: List a connection's source configurations by connection name + text: |- + az {source} connection list-configuration {source_params} --connection MyConnection + - name: List a connection's source configurations by connection id + text: |- + az {source} connection list-configuration --id {connection_id} + """.format( + source=source.value, + source_params=source_params, + connection_id=connection_id, + source_display_name=source_display_name) + + helps['{source} connection validate'.format(source=source.value)] = """ + type: command + short-summary: Validate a {source_display_name} connection. + examples: + - name: Validate a connection interactively + text: |- + az {source} connection validate + - name: Validate a connection by connection name + text: |- + az {source} connection validate {source_params} --connection MyConnection + - name: Validate a connection by connection id + text: |- + az {source} connection validate --id {connection_id} + """.format( + source=source.value, + source_params=source_params, + connection_id=connection_id, + source_display_name=source_display_name) + + helps['{source} connection wait'.format(source=source.value)] = """ + type: command + short-summary: Place the CLI in a waiting state until a condition of the connection is met. + examples: + - name: Wait until the connection is successfully created. + text: |- + az {source} connection wait --id {connection_id} --created + """.format(source=source.value, connection_id=connection_id) + + helps['{source} connection show'.format(source=source.value)] = """ + type: command + short-summary: Get the details of a {source_display_name} connection. + examples: + - name: Get a connection interactively + text: |- + az {source} connection show + - name: Get a connection by connection name + text: |- + az {source} connection show {source_params} --connection MyConnection + - name: Get a connection by connection id + text: |- + az {source} connection show --id {connection_id} + """.format( + source=source.value, + source_params=source_params, + connection_id=connection_id, + source_display_name=source_display_name) + + helps['{source} connection create'.format(source=source.value)] = """ + type: group + short-summary: Create a connection between a {source_display_name} and a target resource + """.format(source_display_name=source_display_name) + + helps['{source} connection update'.format(source=source.value)] = """ + type: group + short-summary: Update a {source_display_name} connection + """.format(source_display_name=source_display_name) + + # use SUPPORTED_AUTH_TYPE to decide target resource, as some + # target resources are not avialable for certain source resource + supported_target_resources = list(SUPPORTED_AUTH_TYPE.get(source).keys()) + for target in supported_target_resources: + target_id = TARGET_RESOURCES.get(target) + + # target resource params + target_params = get_target_resource_params(target) + + # auth info params + auth_types = SUPPORTED_AUTH_TYPE.get(source).get(target) + auth_params = get_auth_info_params(auth_types[0]) + + # auth info params in help message + secret_param = ''' + - name: --secret + short-summary: The secret auth info + long-summary: | + Usage: --secret name=XX secret=XX + --secret name=XX secret-uri=XX + --secret name=XX secret-name=XX + + name : Required. Username or account name for secret auth. + secret : One of is required. Password or account key for secret auth. + secret-uri : One of is required. Keyvault secret uri which stores password. + secret-name : One of is required. Keyvault secret name which stores password. It's for AKS only. + ''' if AUTH_TYPE.Secret in auth_types else '' + secret_auto_param = ''' + - name: --secret + short-summary: The secret auth info + long-summary: | + Usage: --secret + + ''' if AUTH_TYPE.SecretAuto in auth_types else '' + system_identity_param = '' + if AUTH_TYPE.SystemIdentity in auth_types: + if target in {RESOURCE.MysqlFlexible}: + system_identity_param = ''' + - name: --system-identity + short-summary: The system assigned identity auth info + long-summary: | + Usage: --system-identity mysql-identity-id=xx + + mysql-identity-id : Optional. ID of identity used for MySQL flexible server AAD Authentication. Ignore it if you are the server AAD administrator. + ''' + else: + system_identity_param = ''' + - name: --system-identity + short-summary: The system assigned identity auth info + long-summary: | + Usage: --system-identity + + ''' + user_identity_param = '' + if AUTH_TYPE.UserIdentity in auth_types: + if target in {RESOURCE.MysqlFlexible}: + user_identity_param = ''' + - name: --user-identity + short-summary: The user assigned identity auth info + long-summary: | + Usage: --user-identity client-id=XX subs-id=XX + + client-id : Required. Client id of the user assigned identity. + subs-id : Required. Subscription id of the user assigned identity. + mysql-identity-id : Optional. ID of identity used for MySQL flexible server AAD Authentication. Ignore it if you are the server AAD administrator. + ''' + else: + user_identity_param = ''' + - name: --user-identity + short-summary: The user assigned identity auth info + long-summary: | + Usage: --user-identity client-id=XX subs-id=XX + + client-id : Required. Client id of the user assigned identity. + subs-id : Required. Subscription id of the user assigned identity. + ''' + service_principal_param = '' + if AUTH_TYPE.ServicePrincipalSecret in auth_types: + if target in {RESOURCE.MysqlFlexible}: + service_principal_param = ''' + - name: --service-principal + short-summary: The service principal auth info + long-summary: | + Usage: --service-principal client-id=XX secret=XX + + client-id : Required. Client id of the service principal. + object-id : Optional. Object id of the service principal (Enterprise Application). + secret : Required. Secret of the service principal. + mysql-identity-id : Optional. ID of identity used for MySQL flexible server AAD Authentication. Ignore it if you are the server AAD administrator. + ''' + else: + service_principal_param = ''' + - name: --service-principal + short-summary: The service principal auth info + long-summary: | + Usage: --service-principal client-id=XX secret=XX + + client-id : Required. Client id of the service principal. + object-id : Optional. Object id of the service principal (Enterprise Application). + secret : Required. Secret of the service principal. + ''' + + # create with `--new` examples + provision_example = ''' + - name: Create a new {target} and connect {source_display_name} to it interactively + text: |- + az {source} connection create {target} --new + - name: Create a new {target} and connect {source_display_name} to it + text: |- + az {source} connection create {target} --source-id {source_id} --new + '''.format( + source=source.value, + target=target.value, + source_id=source_id, + source_display_name=source_display_name) if target in AddonFactory else '' + + helps['{source} connection create {target}'.format(source=source.value, target=target.value)] = """ + type: command + short-summary: Create a {source_display_name} connection to {target}. + parameters: + {secret_param} + {secret_auto_param} + {system_identity_param} + {user_identity_param} + {service_principal_param} + examples: + - name: Create a connection between {source_display_name} and {target} interactively + text: |- + az {source} connection create {target} + - name: Create a connection between {source_display_name} and {target} with resource name + text: |- + az {source} connection create {target} {source_params} {target_params} {auth_params} + - name: Create a connection between {source_display_name} and {target} with resource id + text: |- + az {source} connection create {target} --source-id {source_id} --target-id {target_id} {auth_params} + {provision_example} + """.format( + source=source.value, + target=target.value, + source_id=source_id, + target_id=target_id, + secret_param=secret_param, + secret_auto_param=secret_auto_param, + system_identity_param=system_identity_param, + user_identity_param=user_identity_param, + service_principal_param=service_principal_param, + source_params=source_params, + target_params=target_params, + auth_params=auth_params, + provision_example=provision_example, + source_display_name=source_display_name) + + helps['{source} connection update {target}'.format(source=source.value, target=target.value)] = """ + type: command + short-summary: Update a {source_display_name} to {target} connection. + parameters: + {secret_param} + {secret_auto_param} + {system_identity_param} + {user_identity_param} + {service_principal_param} + examples: + - name: Update the client type of a connection with resource name + text: |- + az {source} connection update {target} {source_params} --connection MyConnection --client-type dotnet + - name: Update the client type of a connection with resource id + text: |- + az {source} connection update {target} --id {connection_id} --client-type dotnet + """.format( + source=source.value, + target=target.value, + secret_param=secret_param, + secret_auto_param=secret_auto_param, + system_identity_param=system_identity_param, + user_identity_param=user_identity_param, + service_principal_param=service_principal_param, + source_params=source_params, + connection_id=connection_id, + source_display_name=source_display_name) + + +source = RESOURCE.Local +connection_id = ( + '/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/' + 'providers/Microsoft.ServiceLinker/locations/{location}/connectors/{connectorName}') +source_display_name = 'Service Connector' +helps['connection'] = """ + type: group + short-summary: Commands to manage {} local connections which allow local environment to connect Azure Resource. If you want to manage connection for compute service, please run 'az webapp/containerapp/spring connection' +""".format(source_display_name) + +helps['connection list-support-types'] = """ + type: command + short-summary: List client types and auth types supported by local connections. + examples: + - name: List all supported target resource types and auth types + text: |- + az connection list-support-types -o table + - name: List supported auth types for a specific target resource type + text: |- + az connection list-support-types --target-type storage-blob -o table +""" + +helps['connection list'] = """ + type: command + short-summary: List local connections of {source_display_name}. + examples: + - name: List connections by resource group + text: |- + az connection list -g resource_group + - name: List connections by source resource group and location + text: |- + az connection list -g resource_group --location eastus +""".format( + source_display_name=source_display_name) + +helps['connection delete'] = """ + type: command + short-summary: Delete a {source_display_name} local connection. + examples: + - name: Delete a local connection interactively + text: |- + az connection delete + - name: Delete a local connection by connection name + text: |- + az connection delete -g resourceGroup --connection MyConnection + - name: Delete a local connection by connection id + text: |- + az connection delete --id {connection_id} +""".format( + connection_id=connection_id, + source_display_name=source_display_name) + +helps['connection generate-configuration'] = """ + type: command + short-summary: Generate configurations of a {source_display_name} local connection. The result should be put to application configuration file or set as environment variables. + examples: + - name: Generate a connection's local configurations by connection name + text: |- + az connection generate-configuration -g resource_group --connection MyConnection + - name: Generate a connection's local configurations by connection id + text: |- + az connection generate-configuration --id {connection_id} +""".format( + connection_id=connection_id, + source_display_name=source_display_name) + +helps['connection validate'] = """ + type: command + short-summary: Validate a {source_display_name} local connection. + examples: + - name: Validate a connection interactively + text: |- + az connection validate + - name: Validate a connection by connection name + text: |- + az connection validate -g resourceGroup --connection MyConnection + - name: Validate a connection by connection id + text: |- + az connection validate --id {connection_id} +""".format( + connection_id=connection_id, + source_display_name=source_display_name) + +helps['connection wait'] = """ + type: command + short-summary: Place the CLI in a waiting state until a condition of the connection is met. + examples: + - name: Wait until the connection is successfully created. + text: |- + az connection wait --id {connection_id} --created +""".format(connection_id=connection_id) + +helps['connection show'] = """ + type: command + short-summary: Get the details of a {source_display_name} local connection. + examples: + - name: Get a connection interactively + text: |- + az connection show + - name: Get a connection by connection name + text: |- + az connection show -g resourceGroup --connection MyConnection + - name: Get a connection by connection id + text: |- + az connection show --id {connection_id} +""".format( + connection_id=connection_id, + source_display_name=source_display_name) + +helps['connection create'] = """ + type: group + short-summary: Create a connection from local to a target resource +""" + +helps['connection update'] = """ + type: group + short-summary: Update a {source_display_name} local connection +""".format(source_display_name=source_display_name) + +helps['connection preview-configuration'] = """ + type: group + short-summary: Preview the expected configurations of local connection. +""" + + +# use SUPPORTED_AUTH_TYPE to decide target resource, as some +# target resources are not avialable for certain source resource +supported_target_resources = list(SUPPORTED_AUTH_TYPE.get(source).keys()) +for target in supported_target_resources: + target_id = TARGET_RESOURCES.get(target) + + # target resource params + target_params = get_target_resource_params(target) + + # auth info params + auth_types = SUPPORTED_AUTH_TYPE.get(source).get(target) + auth_params = get_auth_info_params(auth_types[0]) + + # auth info params in help message + secret_param = ''' + - name: --secret + short-summary: The secret auth info + long-summary: | + Usage: --secret name=XX secret=XX + --secret name=XX secret-uri=XX + --secret name=XX secret-name=XX + + name : Required. Username or account name for secret auth. + secret : Required. Password or account key for secret auth. + ''' if AUTH_TYPE.Secret in auth_types else '' + secret_auto_param = ''' + - name: --secret + short-summary: The secret auth info + long-summary: | + Usage: --secret + + ''' if AUTH_TYPE.SecretAuto in auth_types else '' + user_account_param = '' + if AUTH_TYPE.UserAccount in auth_types: + if target in {RESOURCE.MysqlFlexible}: + user_account_param = ''' + - name: --user-account + short-summary: The user account auth info + long-summary: | + Usage: --user-account mysql-identity-id=xx object-id=XX + + object-id : Optional. Object id of current login user. It will be set automatically if not provided. + mysql-identity-id : Optional. ID of identity used for MySQL flexible server AAD Authentication. Ignore it if you are the server AAD administrator. + ''' + else: + user_account_param = ''' + - name: --user-account + short-summary: The user account auth info + long-summary: | + Usage: --user-account object-id=XX + + object-id : Optional. Object id of current login user. It will be set automatically if not provided. + ''' + service_principal_param = ''' + - name: --service-principal + short-summary: The service principal auth info + long-summary: | + Usage: --service-principal client-id=XX secret=XX + + client-id : Required. Client id of the service principal. + object-id : Optional. Object id of the service principal (Enterprise Application). + secret : Required. Secret of the service principal. + ''' if AUTH_TYPE.ServicePrincipalSecret in auth_types else '' + + helps['connection create {target}'.format(target=target.value)] = """ + type: command + short-summary: Create a {source_display_name} local connection to {target}. + parameters: + {secret_param} + {secret_auto_param} + {user_account_param} + {service_principal_param} + examples: + - name: Create a connection from local to {target} interactively + text: |- + az connection create {target} -g resourceGroup + - name: Create a connection from local to {target} with resource name + text: |- + az connection create {target} -g resourceGroup {target_params} {auth_params} + - name: Create a connection from local to {target} with resource id + text: |- + az connection create {target} -g resourceGroup --target-id {target_id} {auth_params} + """.format( + target=target.value, + target_id=target_id, + secret_param=secret_param, + secret_auto_param=secret_auto_param, + user_account_param=user_account_param, + service_principal_param=service_principal_param, + target_params=target_params, + auth_params=auth_params, + source_display_name=source_display_name) + + helps['connection update {target}'.format(target=target.value)] = """ + type: command + short-summary: Update a local to {target} connection. + parameters: + {secret_param} + {secret_auto_param} + {user_account_param} + {service_principal_param} + examples: + - name: Update the client type of a connection with resource name + text: |- + az connection update {target} -g resourceGroup --connection MyConnection --client-type dotnet + - name: Update the client type of a connection with resource id + text: |- + az connection update {target} --id {connection_id} --client-type dotnet + """.format( + target=target.value, + secret_param=secret_param, + secret_auto_param=secret_auto_param, + user_account_param=user_account_param, + service_principal_param=service_principal_param, + connection_id=connection_id) + + helps['connection preview-configuration {target}'.format(target=target.value)] = """ + type: command + short-summary: Preview the expected configurations of local connection to {target}. + """.format( + target=target.value) diff --git a/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_params.py b/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_params.py index ac8ae367175..c7835b045ef 100644 --- a/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_params.py +++ b/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_params.py @@ -21,11 +21,11 @@ ) from azure.cli.command_modules.serviceconnector._resource_config import ( SOURCE_RESOURCES_PARAMS, - AUTH_TYPE_PARAMS, AUTH_TYPE, RESOURCE ) from ._resource_config import ( + AUTH_TYPE_PARAMS, SUPPORTED_AUTH_TYPE, TARGET_RESOURCES_PARAMS, ) diff --git a/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_resource_config.py b/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_resource_config.py index afc0b3dd7fe..de0634667cd 100644 --- a/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_resource_config.py +++ b/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/_resource_config.py @@ -5,15 +5,27 @@ from azure.cli.command_modules.serviceconnector._resource_config import ( RESOURCE, - AUTH_TYPE + AUTH_TYPE, ) -passwordless_target_resources = [ +from azure.cli.command_modules.serviceconnector.action import ( + AddSecretAuthInfo, + AddSecretAuthInfoAuto, +) +from .action import ( + AddSystemAssignedIdentityAuthInfo, + AddUserAssignedIdentityAuthInfo, + AddServicePrincipalAuthInfo, + AddUserAccountAuthInfo, +) + +PASSWORDLESS_TARGET_RESOURCES = [ RESOURCE.Postgres, RESOURCE.PostgresFlexible, RESOURCE.MysqlFlexible, RESOURCE.Sql ] +# pylint: disable=line-too-long SUPPORTED_AUTH_TYPE = { RESOURCE.Local: { RESOURCE.Postgres: [AUTH_TYPE.Secret, AUTH_TYPE.UserAccount], @@ -22,16 +34,16 @@ RESOURCE.Sql: [AUTH_TYPE.Secret, AUTH_TYPE.UserAccount], }, RESOURCE.WebApp: { - RESOURCE.Postgres: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity], - RESOURCE.PostgresFlexible: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity], - RESOURCE.MysqlFlexible: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity], - RESOURCE.Sql: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity], + RESOURCE.Postgres: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity, AUTH_TYPE.ServicePrincipalSecret], + RESOURCE.PostgresFlexible: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity, AUTH_TYPE.ServicePrincipalSecret], + RESOURCE.MysqlFlexible: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity, AUTH_TYPE.ServicePrincipalSecret], + RESOURCE.Sql: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity, AUTH_TYPE.ServicePrincipalSecret], }, RESOURCE.SpringCloud: { - RESOURCE.Postgres: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity], - RESOURCE.PostgresFlexible: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity], - RESOURCE.MysqlFlexible: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity], - RESOURCE.Sql: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity], + RESOURCE.Postgres: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity, AUTH_TYPE.ServicePrincipalSecret], + RESOURCE.PostgresFlexible: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity, AUTH_TYPE.ServicePrincipalSecret], + RESOURCE.MysqlFlexible: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity, AUTH_TYPE.ServicePrincipalSecret], + RESOURCE.Sql: [AUTH_TYPE.Secret, AUTH_TYPE.SystemIdentity, AUTH_TYPE.UserIdentity, AUTH_TYPE.ServicePrincipalSecret], }, RESOURCE.KubernetesCluster: { RESOURCE.Postgres: [AUTH_TYPE.Secret], @@ -115,3 +127,48 @@ } }, } + +AUTH_TYPE_PARAMS = { + AUTH_TYPE.Secret: { + 'secret_auth_info': { + 'options': ['--secret'], + 'help': 'The secret auth info', + 'action': AddSecretAuthInfo + } + }, + AUTH_TYPE.SecretAuto: { + 'secret_auth_info_auto': { + 'options': ['--secret'], + 'help': 'The secret auth info', + 'action': AddSecretAuthInfoAuto + } + }, + AUTH_TYPE.SystemIdentity: { + 'system_identity_auth_info': { + 'options': ['--system-identity'], + 'help': 'The system assigned identity auth info', + 'action': AddSystemAssignedIdentityAuthInfo + } + }, + AUTH_TYPE.UserIdentity: { + 'user_identity_auth_info': { + 'options': ['--user-identity'], + 'help': 'The user assigned identity auth info', + 'action': AddUserAssignedIdentityAuthInfo + } + }, + AUTH_TYPE.ServicePrincipalSecret: { + 'service_principal_auth_info_secret': { + 'options': ['--service-principal'], + 'help': 'The service principal auth info', + 'action': AddServicePrincipalAuthInfo + } + }, + AUTH_TYPE.UserAccount: { + 'user_account_auth_info': { + 'options': ['--user-account'], + 'help': 'The local user account auth info', + 'action': AddUserAccountAuthInfo + } + } +} diff --git a/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/action.py b/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/action.py new file mode 100644 index 00000000000..73fc6c96ab2 --- /dev/null +++ b/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/action.py @@ -0,0 +1,157 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import argparse +from collections import defaultdict +from azure.cli.core.azclierror import ValidationError +from azure.cli.command_modules.serviceconnector._resource_config import ( + RESOURCE +) + + +# pylint: disable=consider-using-f-string, raise-missing-from +def is_mysql_target(command_name): + target_name = command_name.split(' ')[-1] + return target_name.lower() == RESOURCE.MysqlFlexible.value.lower() + + +class AddUserAssignedIdentityAuthInfo(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + action = self.get_action(values, option_string, namespace.command) + namespace.user_identity_auth_info = action + + def get_action(self, values, option_string, command_name): # pylint: disable=no-self-use + try: + properties = defaultdict(list) + for (k, v) in (x.split('=', 1) for x in values): + properties[k].append(v) + properties = dict(properties) + except ValueError: + raise ValidationError( + 'usage error: {} [KEY=VALUE ...]'.format(option_string)) + d = {} + for k in properties: + kl = k.lower() + v = properties[k] + if kl == 'client-id': + d['client_id'] = v[0] + elif kl == 'subs-id': + d['subscription_id'] = v[0] + elif is_mysql_target(command_name) and kl == 'mysql-identity-id': + d['mysql-identity-id'] = v[0] + else: + raise ValidationError('Unsupported Key {} is provided for parameter --user-identity. All ' + 'possible keys are: client-id, subs-id{}'.format( + k, ', mysql-identity-id' if is_mysql_target(command_name) else '')) + if 'client_id' not in d or 'subscription_id' not in d: + raise ValidationError( + 'Required keys missing for parameter --user-identity: client-id, subs-id') + d['auth_type'] = 'userAssignedIdentity' + return d + + +class AddSystemAssignedIdentityAuthInfo(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + action = self.get_action(values, option_string, namespace.command) + namespace.system_identity_auth_info = action + + def get_action(self, values, option_string, command_name): # pylint: disable=no-self-use + try: + properties = defaultdict(list) + for (k, v) in (x.split('=', 1) for x in values): + properties[k].append(v) + properties = dict(properties) + except ValueError: + raise ValidationError( + 'Usage error: {} [KEY=VALUE ...]'.format(option_string)) + d = {} + for k in properties: + v = properties[k] + if is_mysql_target(command_name) and k.lower() == 'mysql-identity-id': + d['mysql-identity-id'] = v[0] + else: + raise ValidationError( + 'Unsupported Key {} is provided for parameter --system-identity') + d['auth_type'] = 'systemAssignedIdentity' + return d + + +class AddUserAccountAuthInfo(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + action = self.get_action(values, option_string, namespace.command) + namespace.user_account_auth_info = action + + def get_action(self, values, option_string, command_name): # pylint: disable=no-self-use + try: + properties = defaultdict(list) + for (k, v) in (x.split('=', 1) for x in values): + properties[k].append(v) + properties = dict(properties) + except ValueError: + raise ValidationError( + 'usage error: {} [KEY=VALUE ...]'.format(option_string)) + d = {} + for k in properties: + kl = k.lower() + v = properties[k] + if kl == 'object-id': + d['principal_id'] = v[0] + elif is_mysql_target(command_name) and kl == 'mysql-identity-id': + d['mysql-identity-id'] = v[0] + else: + raise ValidationError('Unsupported Key {} is provided for parameter --user-account. All ' + 'possible keys are: principal-id{}'.format( + k, ', mysql-identity-id' if is_mysql_target(command_name) else '')) + d['auth_type'] = 'userAccount' + return d + + +class AddServicePrincipalAuthInfo(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + action = self.get_action(values, option_string, namespace.command) + namespace.service_principal_auth_info_secret = action + + def get_action(self, values, option_string, command_name): # pylint: disable=no-self-use + try: + properties = defaultdict(list) + for (k, v) in (x.split('=', 1) for x in values): + properties[k].append(v) + properties = dict(properties) + except ValueError: + raise ValidationError( + 'Usage error: {} [KEY=VALUE ...]'.format(option_string)) + d = {} + for k in properties: + kl = k.lower() + v = properties[k] + if kl == 'client-id': + d['client_id'] = v[0] + elif kl == 'object-id': + d['principal_id'] = v[0] + elif kl == 'secret': + d['secret'] = v[0] + elif is_mysql_target(command_name) and kl == 'mysql-identity-id': + d['mysql-identity-id'] = v[0] + else: + raise ValidationError('Unsupported Key {} is provided for parameter --service-principal. Possible ' + 'keys are: client-id, object-id, secret{}'.format( + k, ', mysql-identity-id' if is_mysql_target(command_name) else '')) + if 'client_id' not in d or 'secret' not in d: + raise ValidationError('Required keys missing for parameter --service-principal. ' + 'Required keys are: client-id, secret') + if 'principal_id' not in d: + from azure.cli.command_modules.serviceconnector._utils import run_cli_cmd + output = run_cli_cmd( + 'az ad sp show --id {}'.format(d['client_id'])) + if output: + d['principal_id'] = output.get('id') + else: + raise ValidationError('Could not resolve object-id from the given client-id: {}. Please ' + 'confirm the client-id and provide the object-id (Enterprise Application) ' + 'of the service principal, by using --service-principal client-id=XX ' + 'object-id=XX secret=XX'.format(d['client_id'])) + + d['auth_type'] = 'servicePrincipalSecret' + return d diff --git a/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/commands.py b/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/commands.py index 1372ab263c7..d9773d38558 100644 --- a/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/commands.py +++ b/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/commands.py @@ -14,7 +14,7 @@ ) from azure.cli.command_modules.serviceconnector._utils import should_load_source -from ._resource_config import passwordless_target_resources +from ._resource_config import PASSWORDLESS_TARGET_RESOURCES from ._client_factory import ( cf_linker, cf_connector @@ -29,7 +29,7 @@ def load_command_table(self, _): operations_tmpl='azure.mgmt.servicelinker.operations._connector_operations#ConnectorOperations.{}', client_factory=cf_connector) - for target in passwordless_target_resources: + for target in PASSWORDLESS_TARGET_RESOURCES: with self.command_group('connection create', local_connection_type, client_factory=cf_connector) as ig: ig.custom_command(target.value, 'local_connection_create_ext', @@ -39,7 +39,7 @@ def load_command_table(self, _): # if source resource is released as an extension, load our command groups # only when the extension is installed if should_load_source(source): - for target in passwordless_target_resources: + for target in PASSWORDLESS_TARGET_RESOURCES: with self.command_group(f'{source.value} connection create', connection_type, client_factory=cf_linker) as ig: ig.custom_command(target.value, 'connection_create_ext', diff --git a/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/config.py b/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/config.py index 24abd4ccb6e..b680e839b74 100644 --- a/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/config.py +++ b/src/serviceconnector-passwordless/azext_serviceconnector_passwordless/config.py @@ -4,5 +4,5 @@ # -------------------------------------------------------------------------------------------- -VERSION = '0.3.0' +VERSION = '0.3.1' NAME = 'serviceconnector-passwordless' diff --git a/src/serviceconnector-passwordless/setup.py b/src/serviceconnector-passwordless/setup.py index a1d95a5750c..4fb921adc4f 100644 --- a/src/serviceconnector-passwordless/setup.py +++ b/src/serviceconnector-passwordless/setup.py @@ -15,7 +15,7 @@ logger.warn("Wheel is not available, disabling bdist_wheel hook") -VERSION = '0.3.0' +VERSION = '0.3.1' try: from azext_serviceconnector_passwordless.config import VERSION except ImportError: