diff --git a/linter_exclusions.yml b/linter_exclusions.yml index 4566954cf36..ec6d7b829a7 100644 --- a/linter_exclusions.yml +++ b/linter_exclusions.yml @@ -321,6 +321,18 @@ aks create: assign_kubelet_identity: rule_exclusions: - option_length_too_long + enable_azure_keyvault_kms: + rule_exclusions: + - option_length_too_long + azure_keyvault_kms_key_id: + rule_exclusions: + - option_length_too_long + azure_keyvault_kms_key_vault_network_access: + rule_exclusions: + - option_length_too_long + azure_keyvault_kms_key_vault_resource_id: + rule_exclusions: + - option_length_too_long aks enable-addons: parameters: workspace_resource_id: @@ -371,6 +383,21 @@ aks update: enable_managed_identity: rule_exclusions: - option_length_too_long + enable_azure_keyvault_kms: + rule_exclusions: + - option_length_too_long + disable_azure_keyvault_kms: + rule_exclusions: + - option_length_too_long + azure_keyvault_kms_key_id: + rule_exclusions: + - option_length_too_long + azure_keyvault_kms_key_vault_network_access: + rule_exclusions: + - option_length_too_long + azure_keyvault_kms_key_vault_resource_id: + rule_exclusions: + - option_length_too_long aks update-credentials: parameters: aad_server_app_secret: diff --git a/src/azure-cli/azure/cli/command_modules/acs/_consts.py b/src/azure-cli/azure/cli/command_modules/acs/_consts.py index ec7ad51beff..0389abd775f 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_consts.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_consts.py @@ -113,6 +113,10 @@ CONST_SECRET_ROTATION_ENABLED = "enableSecretRotation" CONST_ROTATION_POLL_INTERVAL = "rotationPollInterval" +# azure keyvault kms +CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PUBLIC = "Public" +CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE = "Private" + # all supported addons ADDONS = { 'http_application_routing': CONST_HTTP_APPLICATION_ROUTING_ADDON_NAME, diff --git a/src/azure-cli/azure/cli/command_modules/acs/_help.py b/src/azure-cli/azure/cli/command_modules/acs/_help.py index c87adbfce55..a2bc75019a0 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_help.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_help.py @@ -531,6 +531,20 @@ - name: --host-group-id type: string short-summary: The fully qualified dedicated host group id used to provision agent node pool. + - name: --enable-azure-keyvault-kms + type: bool + short-summary: Enable Azure KeyVault Key Management Service. + - name: --azure-keyvault-kms-key-id + type: string + short-summary: Identifier of Azure Key Vault key. + - name: --azure-keyvault-kms-key-vault-network-access + type: string + short-summary: Network Access of Azure Key Vault. + long-summary: Allowed values are "Public", "Private". If not set, defaults to type "Public". Requires --azure-keyvault-kms-key-id to be used. + - name: --azure-keyvault-kms-key-vault-resource-id + type: string + short-summary: Resource ID of Azure Key Vault. + examples: - name: Create a Kubernetes cluster with an existing SSH public key. text: az aks create -g MyResourceGroup -n MyManagedCluster --ssh-key-value /path/to/publickey @@ -758,6 +772,22 @@ - name: --defender-config type: string short-summary: Path to JSON file containing Microsoft Defender profile configurations. + - name: --enable-azure-keyvault-kms + type: bool + short-summary: Enable Azure KeyVault Key Management Service. + - name: --disable-azure-keyvault-kms + type: bool + short-summary: Disable Azure KeyVault Key Management Service. + - name: --azure-keyvault-kms-key-id + type: string + short-summary: Identifier of Azure Key Vault key. + - name: --azure-keyvault-kms-key-vault-network-access + type: string + short-summary: Network Access of Azure Key Vault. + long-summary: Allowed values are "Public", "Private". If not set, defaults to type "Public". Requires --azure-keyvault-kms-key-id to be used. + - name: --azure-keyvault-kms-key-vault-resource-id + type: string + short-summary: Resource ID of Azure Key Vault. examples: - name: Update a kubernetes cluster with standard SKU load balancer to use two AKS created IPs for the load balancer outbound connection usage. diff --git a/src/azure-cli/azure/cli/command_modules/acs/_params.py b/src/azure-cli/azure/cli/command_modules/acs/_params.py index 5386ed6b77b..b027731638a 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_params.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_params.py @@ -23,7 +23,8 @@ CONST_RAPID_UPGRADE_CHANNEL, CONST_SCALE_DOWN_MODE_DEALLOCATE, CONST_SCALE_DOWN_MODE_DELETE, CONST_SCALE_SET_PRIORITY_REGULAR, CONST_SCALE_SET_PRIORITY_SPOT, CONST_SPOT_EVICTION_POLICY_DEALLOCATE, - CONST_SPOT_EVICTION_POLICY_DELETE, CONST_STABLE_UPGRADE_CHANNEL) + CONST_SPOT_EVICTION_POLICY_DELETE, CONST_STABLE_UPGRADE_CHANNEL, + CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PUBLIC, CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE) from azure.cli.command_modules.acs._validators import ( validate_acr, validate_assign_identity, validate_assign_kubelet_identity, validate_create_parameters, validate_credential_format, @@ -43,7 +44,8 @@ validate_vm_set_type, validate_vnet_subnet_id, validate_keyvault_secrets_provider_disable_and_enable_parameters, validate_defender_disable_and_enable_parameters, validate_defender_config_parameter, - validate_host_group_id) + validate_host_group_id, + validate_azure_keyvault_kms_key_id, validate_azure_keyvault_kms_key_vault_resource_id) from azure.cli.core.commands.parameters import ( edge_zone_type, file_type, get_enum_type, get_resource_name_completion_list, get_three_state_flag, name_type, @@ -111,6 +113,8 @@ dev_space_endpoint_types = ['Public', 'Private', 'None'] +keyvault_network_access_types = [CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PUBLIC, CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE] + def load_arguments(self, _): @@ -274,6 +278,10 @@ def load_arguments(self, _): c.argument('node_resource_group') c.argument('enable_defender', action='store_true') c.argument('defender_config', validator=validate_defender_config_parameter) + c.argument('enable_azure_keyvault_kms', action='store_true') + c.argument('azure_keyvault_kms_key_id', validator=validate_azure_keyvault_kms_key_id) + c.argument('azure_keyvault_kms_key_vault_network_access', arg_type=get_enum_type(keyvault_network_access_types)) + c.argument('azure_keyvault_kms_key_vault_resource_id', validator=validate_azure_keyvault_kms_key_vault_resource_id) # addons c.argument('enable_addons', options_list=['--enable-addons', '-a']) c.argument('workspace_resource_id') @@ -356,6 +364,11 @@ def load_arguments(self, _): c.argument('disable_defender', action='store_true', validator=validate_defender_disable_and_enable_parameters) c.argument('enable_defender', action='store_true') c.argument('defender_config', validator=validate_defender_config_parameter) + c.argument('enable_azure_keyvault_kms', action='store_true') + c.argument('disable_azure_keyvault_kms', action='store_true') + c.argument('azure_keyvault_kms_key_id', validator=validate_azure_keyvault_kms_key_id) + c.argument('azure_keyvault_kms_key_vault_network_access', arg_type=get_enum_type(keyvault_network_access_types)) + c.argument('azure_keyvault_kms_key_vault_resource_id', validator=validate_azure_keyvault_kms_key_vault_resource_id) # addons c.argument('enable_secret_rotation', action='store_true') c.argument('disable_secret_rotation', action='store_true', validator=validate_keyvault_secrets_provider_disable_and_enable_parameters) diff --git a/src/azure-cli/azure/cli/command_modules/acs/_validators.py b/src/azure-cli/azure/cli/command_modules/acs/_validators.py index 395fc8ef4ea..f05b3a932ac 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_validators.py @@ -537,3 +537,27 @@ def validate_defender_config_parameter(namespace): def validate_defender_disable_and_enable_parameters(namespace): if namespace.disable_defender and namespace.enable_defender: raise ArgumentUsageError('Providing both --disable-defender and --enable-defender flags is invalid') + + +def validate_azure_keyvault_kms_key_id(namespace): + key_id = namespace.azure_keyvault_kms_key_id + if key_id: + # pylint:disable=line-too-long + err_msg = '--azure-keyvault-kms-key-id is not a valid Key Vault key ID. See https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name' + + https_prefix = "https://" + if not key_id.startswith(https_prefix): + raise InvalidArgumentValueError(err_msg) + + segments = key_id[len(https_prefix):].split("/") + if len(segments) != 4 or segments[1] != "keys": + raise InvalidArgumentValueError(err_msg) + + +def validate_azure_keyvault_kms_key_vault_resource_id(namespace): + key_vault_resource_id = namespace.azure_keyvault_kms_key_vault_resource_id + if key_vault_resource_id is None or key_vault_resource_id == '': + return + from msrestazure.tools import is_valid_resource_id + if not is_valid_resource_id(key_vault_resource_id): + raise InvalidArgumentValueError("--azure-keyvault-kms-key-vault-resource-id is not a valid Azure resource ID.") diff --git a/src/azure-cli/azure/cli/command_modules/acs/custom.py b/src/azure-cli/azure/cli/command_modules/acs/custom.py index 7530963e467..1c4ed9d4008 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/custom.py +++ b/src/azure-cli/azure/cli/command_modules/acs/custom.py @@ -1533,6 +1533,10 @@ def aks_create( node_resource_group=None, enable_defender=False, defender_config=None, + enable_azure_keyvault_kms=False, + azure_keyvault_kms_key_id=None, + azure_keyvault_kms_key_vault_network_access=None, + azure_keyvault_kms_key_vault_resource_id=None, # addons enable_addons=None, workspace_resource_id=None, @@ -1639,6 +1643,11 @@ def aks_update( enable_defender=False, disable_defender=False, defender_config=None, + enable_azure_keyvault_kms=False, + disable_azure_keyvault_kms=False, + azure_keyvault_kms_key_id=None, + azure_keyvault_kms_key_vault_network_access=None, + azure_keyvault_kms_key_vault_resource_id=None, # addons enable_secret_rotation=False, disable_secret_rotation=False, diff --git a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py index 8e757376a50..6fd47edfa58 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py @@ -18,6 +18,8 @@ CONST_OUTBOUND_TYPE_USER_DEFINED_ROUTING, CONST_PRIVATE_DNS_ZONE_NONE, CONST_PRIVATE_DNS_ZONE_SYSTEM, + CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE, + CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PUBLIC, AgentPoolDecoratorMode, DecoratorEarlyExitException, DecoratorMode, @@ -64,6 +66,7 @@ from azure.cli.core import AzCommandsLoader from azure.cli.core._profile import Profile from azure.cli.core.azclierror import ( + ArgumentUsageError, AzCLIError, CLIInternalError, InvalidArgumentValueError, @@ -3683,6 +3686,258 @@ def get_defender_config(self) -> Union[ManagedClusterSecurityProfileDefender, No ) return azure_defender + def _get_enable_azure_keyvault_kms(self, enable_validation: bool = False) -> bool: + """Internal function to obtain the value of enable_azure_keyvault_kms. + + This function supports the option of enable_validation. When enabled, if azure_keyvault_kms_key_id is empty, + raise a RequiredArgumentMissingError. + + :return: bool + """ + # read the original value passed by the command + enable_azure_keyvault_kms = self.raw_param.get("enable_azure_keyvault_kms") + # In create mode, try to read the property value corresponding to the parameter from the `mc` object. + if self.decorator_mode == DecoratorMode.CREATE: + if ( + self.mc and + hasattr(self.mc, "security_profile") and # backward compatibility + self.mc.security_profile and + self.mc.security_profile.azure_key_vault_kms + ): + enable_azure_keyvault_kms = self.mc.security_profile.azure_key_vault_kms.enabled + + # this parameter does not need dynamic completion + # validation + if enable_validation: + if bool(enable_azure_keyvault_kms) != bool(self._get_azure_keyvault_kms_key_id(enable_validation=False)): + raise RequiredArgumentMissingError( + 'You must set "--enable-azure-keyvault-kms" and "--azure-keyvault-kms-key-id" at the same time.' + ) + + return enable_azure_keyvault_kms + + def get_enable_azure_keyvault_kms(self) -> bool: + """Obtain the value of enable_azure_keyvault_kms. + + This function will verify the parameter by default. When enabled, if azure_keyvault_kms_key_id is empty, + raise a RequiredArgumentMissingError. + + :return: bool + """ + return self._get_enable_azure_keyvault_kms(enable_validation=True) + + def _get_disable_azure_keyvault_kms(self, enable_validation: bool = False) -> bool: + """Internal function to obtain the value of disable_azure_keyvault_kms. + + This function supports the option of enable_validation. When enabled, + if both enable_azure_keyvault_kms and disable_azure_keyvault_kms are + specified, raise a MutuallyExclusiveArgumentError. + + :return: bool + """ + # Read the original value passed by the command. + disable_azure_keyvault_kms = self.raw_param.get("disable_azure_keyvault_kms") + + # This option is not supported in create mode, hence we do not read the property value from the `mc` object. + # This parameter does not need dynamic completion. + if enable_validation: + if disable_azure_keyvault_kms and self._get_enable_azure_keyvault_kms(enable_validation=False): + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-azure-keyvault-kms and --disable-azure-keyvault-kms at the same time." + ) + + return disable_azure_keyvault_kms + + def get_disable_azure_keyvault_kms(self) -> bool: + """Obtain the value of disable_azure_keyvault_kms. + + This function will verify the parameter by default. If both enable_azure_keyvault_kms and + disable_azure_keyvault_kms are specified, raise a MutuallyExclusiveArgumentError. + + :return: bool + """ + return self._get_disable_azure_keyvault_kms(enable_validation=True) + + def _get_azure_keyvault_kms_key_id(self, enable_validation: bool = False) -> Union[str, None]: + """Internal function to obtain the value of azure_keyvault_kms_key_id according to the context. + + This function supports the option of enable_validation. When enabled, it will check if + azure_keyvault_kms_key_id is assigned but enable_azure_keyvault_kms is not specified, + if so, raise a RequiredArgumentMissingError. + + :return: string or None + """ + # read the original value passed by the command + azure_keyvault_kms_key_id = self.raw_param.get("azure_keyvault_kms_key_id") + # In create mode, try to read the property value corresponding to the parameter from the `mc` object. + if self.decorator_mode == DecoratorMode.CREATE: + if ( + self.mc and + hasattr(self.mc, "security_profile") and # backward compatibility + self.mc.security_profile and + self.mc.security_profile.azure_key_vault_kms and + self.mc.security_profile.azure_key_vault_kms.key_id is not None + ): + azure_keyvault_kms_key_id = self.mc.security_profile.azure_key_vault_kms.key_id + + if enable_validation: + enable_azure_keyvault_kms = self._get_enable_azure_keyvault_kms( + enable_validation=False) + if ( + azure_keyvault_kms_key_id and + ( + enable_azure_keyvault_kms is None or + enable_azure_keyvault_kms is False + ) + ): + raise RequiredArgumentMissingError( + '"--azure-keyvault-kms-key-id" requires "--enable-azure-keyvault-kms".') + + return azure_keyvault_kms_key_id + + def get_azure_keyvault_kms_key_id(self) -> Union[str, None]: + """Obtain the value of azure_keyvault_kms_key_id. + + This function will verify the parameter by default. When enabled, if enable_azure_keyvault_kms is False, + raise a RequiredArgumentMissingError. + + :return: bool + """ + return self._get_azure_keyvault_kms_key_id(enable_validation=True) + + def _get_azure_keyvault_kms_key_vault_network_access(self, enable_validation: bool = False) -> Union[str, None]: + """Internal function to obtain the value of azure_keyvault_kms_key_vault_network_access according to the + context. + + This function supports the option of enable_validation. When enabled, it will check if + azure_keyvault_kms_key_vault_network_access is assigned but enable_azure_keyvault_kms is not specified, if so, + raise a RequiredArgumentMissingError. + + :return: string or None + """ + # read the original value passed by the command + azure_keyvault_kms_key_vault_network_access = self.raw_param.get( + "azure_keyvault_kms_key_vault_network_access" + ) + + # validation + if enable_validation: + enable_azure_keyvault_kms = self._get_enable_azure_keyvault_kms( + enable_validation=False) + if azure_keyvault_kms_key_vault_network_access is None: + raise RequiredArgumentMissingError( + '"--azure-keyvault-kms-key-vault-network-access" is required.') + + if ( + azure_keyvault_kms_key_vault_network_access and + ( + enable_azure_keyvault_kms is None or + enable_azure_keyvault_kms is False + ) + ): + raise RequiredArgumentMissingError( + '"--azure-keyvault-kms-key-vault-network-access" requires "--enable-azure-keyvault-kms".') + + if azure_keyvault_kms_key_vault_network_access == CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE: + key_vault_resource_id = self._get_azure_keyvault_kms_key_vault_resource_id( + enable_validation=False) + if ( + key_vault_resource_id is None or + key_vault_resource_id == "" + ): + raise RequiredArgumentMissingError( + '"--azure-keyvault-kms-key-vault-resource-id" is required ' + 'when "--azure-keyvault-kms-key-vault-network-access" is Private.' + ) + + return azure_keyvault_kms_key_vault_network_access + + def get_azure_keyvault_kms_key_vault_network_access(self) -> Union[str, None]: + """Obtain the value of azure_keyvault_kms_key_vault_network_access. + + This function will verify the parameter by default. When enabled, if enable_azure_keyvault_kms is False, + raise a RequiredArgumentMissingError. + + :return: bool + """ + return self._get_azure_keyvault_kms_key_vault_network_access(enable_validation=True) + + def _get_azure_keyvault_kms_key_vault_resource_id(self, enable_validation: bool = False) -> Union[str, None]: + """Internal function to obtain the value of azure_keyvault_kms_key_vault_resource_id according to the context. + + This function supports the option of enable_validation. When enabled, it will do validation, and raise a + RequiredArgumentMissingError. + + :return: string or None + """ + # read the original value passed by the command + azure_keyvault_kms_key_vault_resource_id = self.raw_param.get( + "azure_keyvault_kms_key_vault_resource_id" + ) + if self.decorator_mode == DecoratorMode.CREATE: + if ( + self.mc and + hasattr(self.mc, "security_profile") and # backward compatibility + self.mc.security_profile and + self.mc.security_profile.azure_key_vault_kms and + self.mc.security_profile.azure_key_vault_kms.key_vault_resource_id is not None + ): + azure_keyvault_kms_key_vault_resource_id = ( + self.mc.security_profile.azure_key_vault_kms.key_vault_resource_id + ) + + # validation + if enable_validation: + enable_azure_keyvault_kms = self._get_enable_azure_keyvault_kms( + enable_validation=False) + if ( + azure_keyvault_kms_key_vault_resource_id and + ( + enable_azure_keyvault_kms is None or + enable_azure_keyvault_kms is False + ) + ): + raise RequiredArgumentMissingError( + '"--azure-keyvault-kms-key-vault-resource-id" requires "--enable-azure-keyvault-kms".' + ) + + key_vault_network_access = self._get_azure_keyvault_kms_key_vault_network_access( + enable_validation=False) + if ( + key_vault_network_access == CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE and + ( + azure_keyvault_kms_key_vault_resource_id is None or + azure_keyvault_kms_key_vault_resource_id == "" + ) + ): + raise ArgumentUsageError( + '"--azure-keyvault-kms-key-vault-resource-id" can not be empty if ' + '"--azure-keyvault-kms-key-vault-network-access" is "Private".' + ) + if ( + key_vault_network_access == CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PUBLIC and + ( + azure_keyvault_kms_key_vault_resource_id is not None and + azure_keyvault_kms_key_vault_resource_id != "" + ) + ): + raise ArgumentUsageError( + '"--azure-keyvault-kms-key-vault-resource-id" must be empty if ' + '"--azure-keyvault-kms-key-vault-network-access" is "Public".' + ) + + return azure_keyvault_kms_key_vault_resource_id + + def get_azure_keyvault_kms_key_vault_resource_id(self) -> Union[str, None]: + """Obtain the value of azure_keyvault_kms_key_vault_resource_id. + + This function will verify the parameter by default. When enabled, if enable_azure_keyvault_kms is False, + raise a RequiredArgumentMissingError. + + :return: bool + """ + return self._get_azure_keyvault_kms_key_vault_resource_id(enable_validation=True) + def _get_disable_local_accounts(self, enable_validation: bool = False) -> bool: """Internal function to obtain the value of disable_local_accounts. @@ -3962,6 +4217,31 @@ def set_up_defender(self, mc: ManagedCluster) -> ManagedCluster: return mc + def set_up_azure_keyvault_kms(self, mc: ManagedCluster) -> ManagedCluster: + """Set up security profile azureKeyVaultKms for the ManagedCluster object. + + :return: the ManagedCluster object + """ + self._ensure_mc(mc) + + if self.context.get_enable_azure_keyvault_kms(): + key_id = self.context.get_azure_keyvault_kms_key_id() + if key_id: + if mc.security_profile is None: + mc.security_profile = self.models.ManagedClusterSecurityProfile() + mc.security_profile.azure_key_vault_kms = self.models.AzureKeyVaultKms( + enabled=True, + key_id=key_id, + ) + key_vault_network_access = self.context.get_azure_keyvault_kms_key_vault_network_access() + mc.security_profile.azure_key_vault_kms.key_vault_network_access = key_vault_network_access + if key_vault_network_access == CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE: + mc.security_profile.azure_key_vault_kms.key_vault_resource_id = ( + self.context.get_azure_keyvault_kms_key_vault_resource_id() + ) + + return mc + def init_mc(self) -> ManagedCluster: """Initialize a ManagedCluster object with required parameter location and attach it to internal context. @@ -4789,6 +5069,8 @@ def construct_mc_profile_default(self, bypass_restore_defaults: bool = False) -> mc = self.set_up_node_resource_group(mc) # set up defender mc = self.set_up_defender(mc) + # set up azure keyvalut kms + mc = self.set_up_azure_keyvault_kms(mc) # DO NOT MOVE: keep this at the bottom, restore defaults if not bypass_restore_defaults: @@ -5604,6 +5886,53 @@ def update_defender(self, mc: ManagedCluster) -> ManagedCluster: return mc + def update_azure_keyvault_kms(self, mc: ManagedCluster) -> ManagedCluster: + """Update security profile azureKeyvaultKms for the ManagedCluster object. + + :return: the ManagedCluster object + """ + self._ensure_mc(mc) + + if self.context.get_enable_azure_keyvault_kms(): + # get kms profile + if mc.security_profile is None: + mc.security_profile = self.models.ManagedClusterSecurityProfile() + azure_key_vault_kms_profile = mc.security_profile.azure_key_vault_kms + if azure_key_vault_kms_profile is None: + azure_key_vault_kms_profile = self.models.AzureKeyVaultKms() + mc.security_profile.azure_key_vault_kms = azure_key_vault_kms_profile + + # set enabled + azure_key_vault_kms_profile.enabled = True + # set key id + azure_key_vault_kms_profile.key_id = self.context.get_azure_keyvault_kms_key_id() + # set network access, should never be None for now, can be safely assigned, temp fix for rp + # the value is obtained from user input or backfilled from existing mc or to default value + azure_key_vault_kms_profile.key_vault_network_access = ( + self.context.get_azure_keyvault_kms_key_vault_network_access() + ) + # set key vault resource id + if azure_key_vault_kms_profile.key_vault_network_access == CONST_AZURE_KEYVAULT_NETWORK_ACCESS_PRIVATE: + azure_key_vault_kms_profile.key_vault_resource_id = ( + self.context.get_azure_keyvault_kms_key_vault_resource_id() + ) + else: + azure_key_vault_kms_profile.key_vault_resource_id = "" + + if self.context.get_disable_azure_keyvault_kms(): + # get kms profile + if mc.security_profile is None: + mc.security_profile = self.models.ManagedClusterSecurityProfile() + azure_key_vault_kms_profile = mc.security_profile.azure_key_vault_kms + if azure_key_vault_kms_profile is None: + azure_key_vault_kms_profile = self.models.AzureKeyVaultKms() + mc.security_profile.azure_key_vault_kms = azure_key_vault_kms_profile + + # set enabled to False + azure_key_vault_kms_profile.enabled = False + + return mc + def update_identity_profile(self, mc: ManagedCluster) -> ManagedCluster: """Update identity profile for the ManagedCluster object. @@ -5675,6 +6004,8 @@ def update_mc_profile_default(self) -> ManagedCluster: mc = self.update_addon_profiles(mc) # update defender mc = self.update_defender(mc) + # update azure keyvalut kms + mc = self.update_azure_keyvault_kms(mc) # update identity mc = self.update_identity_profile(mc) return mc diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py index 81ea6d0e057..896b58f385e 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py @@ -6848,3 +6848,573 @@ def test_aks_create_node_resource_group(self, resource_group, resource_group_loc # delete self.cmd('aks delete -g {resource_group} -n {name} --yes --no-wait', checks=[self.is_empty()]) + + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='centraluseuap') + def test_aks_create_with_azurekeyvaultkms_public_key_vault(self, resource_group, resource_group_location): + aks_name = self.create_random_name('cliakstest', 16) + kv_name = self.create_random_name('cliakstestkv', 16) + identity_name = self.create_random_name('cliakstestidentity', 24) + self.kwargs.update({ + 'resource_group': resource_group, + 'name': aks_name, + "kv_name": kv_name, + "identity_name": identity_name, + 'ssh_key_value': self.generate_ssh_keys() + }) + + # create user-assigned identity + create_identity = 'identity create --resource-group={resource_group} --name={identity_name} -o json' + identity = self.cmd(create_identity).get_output_in_json() + identity_id = identity['id'] + identity_object_id = identity['principalId'] + assert identity_id is not None + assert identity_object_id is not None + self.kwargs.update({ + 'identity_id': identity_id, + 'identity_object_id': identity_object_id, + }) + + # create key vault and key + create_keyvault = 'keyvault create --resource-group={resource_group} --name={kv_name} -o json' + kv = self.cmd(create_keyvault, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + create_key = 'keyvault key create -n kms --vault-name {kv_name} -o json' + key = self.cmd(create_key, checks=[ + self.check('attributes.enabled', True) + ]).get_output_in_json() + key_id_0 = key['key']['kid'] + assert key_id_0 is not None + self.kwargs.update({ + 'key_id': key_id_0, + }) + + # assign access policy + set_policy = 'keyvault set-policy --resource-group={resource_group} --name={kv_name} ' \ + '--object-id {identity_object_id} --key-permissions encrypt decrypt -o json' + policy = self.cmd(set_policy, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + create_cmd = 'aks create --resource-group={resource_group} --name={name} ' \ + '--assign-identity {identity_id} ' \ + '--enable-azure-keyvault-kms --azure-keyvault-kms-key-id={key_id} --azure-keyvault-kms-key-vault-network-access=Public ' \ + '--ssh-key-value={ssh_key_value} -o json' + self.cmd(create_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.azureKeyVaultKms.enabled', True), + self.check('securityProfile.azureKeyVaultKms.keyId', key_id_0), + self.check('securityProfile.azureKeyVaultKms.keyVaultNetworkAccess', 'Public') + ]) + + key = self.cmd(create_key, checks=[ + self.check('attributes.enabled', True) + ]).get_output_in_json() + key_id_1 = key['key']['kid'] + assert key_id_1 is not None + self.kwargs.update({ + 'key_id': key_id_1, + }) + + # Rotate key + update_cmd = 'aks update --resource-group={resource_group} --name={name} ' \ + '--enable-azure-keyvault-kms --azure-keyvault-kms-key-id={key_id} --azure-keyvault-kms-key-vault-network-access=Public ' \ + '-o json' + self.cmd(update_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.azureKeyVaultKms.enabled', True), + self.check('securityProfile.azureKeyVaultKms.keyId', key_id_1), + self.check('securityProfile.azureKeyVaultKms.keyVaultNetworkAccess', 'Public') + ]) + + # delete + cmd = 'aks delete --resource-group={resource_group} --name={name} --yes --no-wait' + self.cmd(cmd, checks=[ + self.is_empty(), + ]) + + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='centraluseuap') + def test_aks_update_with_azurekeyvaultkms_public_key_vault(self, resource_group, resource_group_location): + aks_name = self.create_random_name('cliakstest', 16) + kv_name = self.create_random_name('cliakstestkv', 16) + identity_name = self.create_random_name('cliakstestidentity', 24) + self.kwargs.update({ + 'resource_group': resource_group, + 'name': aks_name, + "kv_name": kv_name, + "identity_name": identity_name, + 'ssh_key_value': self.generate_ssh_keys() + }) + + # create user-assigned identity + create_identity = 'identity create --resource-group={resource_group} --name={identity_name} -o json' + identity = self.cmd(create_identity).get_output_in_json() + identity_id = identity['id'] + identity_object_id = identity['principalId'] + assert identity_id is not None + assert identity_object_id is not None + self.kwargs.update({ + 'identity_id': identity_id, + 'identity_object_id': identity_object_id, + }) + + # create key vault and key + create_keyvault = 'keyvault create --resource-group={resource_group} --name={kv_name} -o json' + kv = self.cmd(create_keyvault, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + create_key = 'keyvault key create -n kms --vault-name {kv_name} -o json' + key = self.cmd(create_key, checks=[ + self.check('attributes.enabled', True) + ]).get_output_in_json() + key_id = key['key']['kid'] + assert key_id is not None + self.kwargs.update({ + 'key_id': key_id, + }) + + # assign access policy + set_policy = 'keyvault set-policy --resource-group={resource_group} --name={kv_name} ' \ + '--object-id {identity_object_id} --key-permissions encrypt decrypt -o json' + policy = self.cmd(set_policy, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + create_cmd = 'aks create --resource-group={resource_group} --name={name} ' \ + '--assign-identity {identity_id} ' \ + '--ssh-key-value={ssh_key_value} -o json' + self.cmd(create_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.not_exists('securityProfile.azureKeyVaultKms') + ]) + + update_cmd = 'aks update --resource-group={resource_group} --name={name} ' \ + '--enable-azure-keyvault-kms --azure-keyvault-kms-key-id={key_id} --azure-keyvault-kms-key-vault-network-access=Public ' \ + '-o json' + self.cmd(update_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.azureKeyVaultKms.enabled', True), + self.check('securityProfile.azureKeyVaultKms.keyId', key_id), + self.check('securityProfile.azureKeyVaultKms.keyVaultNetworkAccess', 'Public') + ]) + + # delete + cmd = 'aks delete --resource-group={resource_group} --name={name} --yes --no-wait' + self.cmd(cmd, checks=[ + self.is_empty(), + ]) + + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='centraluseuap', preserve_default_location=True) + def test_aks_create_with_azurekeyvaultkms_private_key_vault(self, resource_group, resource_group_location): + aks_name = self.create_random_name('cliakstest', 16) + kv_name = self.create_random_name('cliakstestkv', 16) + identity_name = self.create_random_name('cliakstestidentity', 24) + self.kwargs.update({ + 'resource_group': resource_group, + 'name': aks_name, + "kv_name": kv_name, + "identity_name": identity_name, + 'ssh_key_value': self.generate_ssh_keys() + }) + + # create user-assigned identity + create_identity = 'identity create --resource-group={resource_group} --name={identity_name} -o json' + identity = self.cmd(create_identity).get_output_in_json() + identity_id = identity['id'] + identity_object_id = identity['principalId'] + assert identity_id is not None + assert identity_object_id is not None + self.kwargs.update({ + 'identity_id': identity_id, + 'identity_object_id': identity_object_id, + }) + + # create key vault and key + create_keyvault = 'keyvault create --resource-group={resource_group} --name={kv_name} -o json' + kv = self.cmd(create_keyvault, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + kv_resource_id = kv['id'] + assert kv_resource_id is not None + self.kwargs.update({ + 'kv_resource_id': kv_resource_id, + }) + + create_key = 'keyvault key create -n kms --vault-name {kv_name} -o json' + key = self.cmd(create_key, checks=[ + self.check('attributes.enabled', True) + ]).get_output_in_json() + key_id_0 = key['key']['kid'] + assert key_id_0 is not None + self.kwargs.update({ + 'key_id': key_id_0, + }) + + # assign access policy + set_policy = 'keyvault set-policy --resource-group={resource_group} --name={kv_name} ' \ + '--object-id {identity_object_id} --key-permissions encrypt decrypt -o json' + policy = self.cmd(set_policy, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + # allow the identity approve private endpoint connection (Microsoft.KeyVault/vaults/privateEndpointConnectionsApproval/action) + create_role_assignment = 'role assignment create --role f25e0fa2-a7c8-4377-a976-54943a77a395 ' \ + '--assignee-object-id {identity_object_id} --assignee-principal-type "ServicePrincipal" ' \ + '--scope {kv_resource_id}' + role_assignment = self.cmd(create_role_assignment).get_output_in_json() + + # disable public network access + disable_public_network_access = 'keyvault update --resource-group={resource_group} --name={kv_name} --public-network-access "Disabled" -o json' + kv = self.cmd(disable_public_network_access, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + create_cmd = 'aks create --resource-group={resource_group} --name={name} ' \ + '--assign-identity {identity_id} ' \ + '--enable-azure-keyvault-kms --azure-keyvault-kms-key-id={key_id} ' \ + '--azure-keyvault-kms-key-vault-network-access=Private --azure-keyvault-kms-key-vault-resource-id {kv_resource_id} ' \ + '--ssh-key-value={ssh_key_value} -o json' + self.cmd(create_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.azureKeyVaultKms.enabled', True), + self.check('securityProfile.azureKeyVaultKms.keyId', key_id_0), + self.check('securityProfile.azureKeyVaultKms.keyVaultNetworkAccess', "Private"), + self.check('securityProfile.azureKeyVaultKms.keyVaultResourceId', kv_resource_id) + ]) + + # enable public network access + enable_public_network_access = 'keyvault update --resource-group={resource_group} --name={kv_name} --public-network-access "Enabled" -o json' + kv = self.cmd(enable_public_network_access, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + key = self.cmd(create_key, checks=[ + self.check('attributes.enabled', True) + ]).get_output_in_json() + key_id_1 = key['key']['kid'] + assert key_id_1 is not None + self.kwargs.update({ + 'key_id': key_id_1, + }) + + # disable public network access + disable_public_network_access = 'keyvault update --resource-group={resource_group} --name={kv_name} --public-network-access "Disabled" -o json' + kv = self.cmd(disable_public_network_access, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + # Rotate key + update_cmd = 'aks update --resource-group={resource_group} --name={name} ' \ + '--enable-azure-keyvault-kms --azure-keyvault-kms-key-id={key_id} ' \ + '--azure-keyvault-kms-key-vault-network-access=Private --azure-keyvault-kms-key-vault-resource-id {kv_resource_id} ' \ + '-o json' + self.cmd(update_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.azureKeyVaultKms.enabled', True), + self.check('securityProfile.azureKeyVaultKms.keyId', key_id_1), + self.check('securityProfile.azureKeyVaultKms.keyVaultNetworkAccess', "Private"), + self.check('securityProfile.azureKeyVaultKms.keyVaultResourceId', kv_resource_id) + ]) + + # delete + cmd = 'aks delete --resource-group={resource_group} --name={name} --yes --no-wait' + self.cmd(cmd, checks=[ + self.is_empty(), + ]) + + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='centraluseuap', preserve_default_location=True) + def test_aks_update_with_azurekeyvaultkms_private_key_vault(self, resource_group, resource_group_location): + aks_name = self.create_random_name('cliakstest', 16) + kv_name = self.create_random_name('cliakstestkv', 16) + identity_name = self.create_random_name('cliakstestidentity', 24) + self.kwargs.update({ + 'resource_group': resource_group, + 'name': aks_name, + "kv_name": kv_name, + "identity_name": identity_name, + 'ssh_key_value': self.generate_ssh_keys() + }) + + # create user-assigned identity + create_identity = 'identity create --resource-group={resource_group} --name={identity_name} -o json' + identity = self.cmd(create_identity).get_output_in_json() + identity_id = identity['id'] + identity_object_id = identity['principalId'] + assert identity_id is not None + assert identity_object_id is not None + self.kwargs.update({ + 'identity_id': identity_id, + 'identity_object_id': identity_object_id, + }) + + # create key vault and key + create_keyvault = 'keyvault create --resource-group={resource_group} --name={kv_name} -o json' + kv = self.cmd(create_keyvault, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + kv_resource_id = kv['id'] + assert kv_resource_id is not None + self.kwargs.update({ + 'kv_resource_id': kv_resource_id, + }) + + create_key = 'keyvault key create -n kms --vault-name {kv_name} -o json' + key = self.cmd(create_key, checks=[ + self.check('attributes.enabled', True) + ]).get_output_in_json() + key_id = key['key']['kid'] + assert key_id is not None + self.kwargs.update({ + 'key_id': key_id, + }) + + # assign access policy + set_policy = 'keyvault set-policy --resource-group={resource_group} --name={kv_name} ' \ + '--object-id {identity_object_id} --key-permissions encrypt decrypt -o json' + policy = self.cmd(set_policy, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + # allow the identity approve private endpoint connection (Microsoft.KeyVault/vaults/privateEndpointConnectionsApproval/action) + create_role_assignment = 'role assignment create --role f25e0fa2-a7c8-4377-a976-54943a77a395 ' \ + '--assignee-object-id {identity_object_id} --assignee-principal-type "ServicePrincipal" ' \ + '--scope {kv_resource_id}' + role_assignment = self.cmd(create_role_assignment).get_output_in_json() + + # disable public network access + disable_public_network_access = 'keyvault update --resource-group={resource_group} --name={kv_name} --public-network-access "Disabled" -o json' + kv = self.cmd(disable_public_network_access, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + create_cmd = 'aks create --resource-group={resource_group} --name={name} ' \ + '--assign-identity {identity_id} ' \ + '--ssh-key-value={ssh_key_value} -o json' + self.cmd(create_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.not_exists('securityProfile.azureKeyVaultKms') + ]) + + update_cmd = 'aks update --resource-group={resource_group} --name={name} ' \ + '--enable-azure-keyvault-kms --azure-keyvault-kms-key-id={key_id} ' \ + '--azure-keyvault-kms-key-vault-network-access=Private --azure-keyvault-kms-key-vault-resource-id {kv_resource_id} ' \ + '-o json' + self.cmd(update_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.azureKeyVaultKms.enabled', True), + self.check('securityProfile.azureKeyVaultKms.keyId', key_id), + self.check('securityProfile.azureKeyVaultKms.keyVaultNetworkAccess', "Private"), + self.check('securityProfile.azureKeyVaultKms.keyVaultResourceId', kv_resource_id) + ]) + + # delete + cmd = 'aks delete --resource-group={resource_group} --name={name} --yes --no-wait' + self.cmd(cmd, checks=[ + self.is_empty(), + ]) + + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='centraluseuap', preserve_default_location=True) + def test_aks_create_with_azurekeyvaultkms_private_cluster_v1_private_key_vault(self, resource_group, resource_group_location): + aks_name = self.create_random_name('cliakstest', 16) + kv_name = self.create_random_name('cliakstestkv', 16) + identity_name = self.create_random_name('cliakstestidentity', 24) + self.kwargs.update({ + 'resource_group': resource_group, + 'name': aks_name, + "kv_name": kv_name, + "identity_name": identity_name, + 'ssh_key_value': self.generate_ssh_keys() + }) + + # create user-assigned identity + create_identity = 'identity create --resource-group={resource_group} --name={identity_name} -o json' + identity = self.cmd(create_identity).get_output_in_json() + identity_id = identity['id'] + identity_object_id = identity['principalId'] + assert identity_id is not None + assert identity_object_id is not None + self.kwargs.update({ + 'identity_id': identity_id, + 'identity_object_id': identity_object_id, + }) + + # create key vault and key + create_keyvault = 'keyvault create --resource-group={resource_group} --name={kv_name} -o json' + kv = self.cmd(create_keyvault, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + kv_resource_id = kv['id'] + assert kv_resource_id is not None + self.kwargs.update({ + 'kv_resource_id': kv_resource_id, + }) + + create_key = 'keyvault key create -n kms --vault-name {kv_name} -o json' + key = self.cmd(create_key, checks=[ + self.check('attributes.enabled', True) + ]).get_output_in_json() + key_id_0 = key['key']['kid'] + assert key_id_0 is not None + self.kwargs.update({ + 'key_id': key_id_0, + }) + + # assign access policy + set_policy = 'keyvault set-policy --resource-group={resource_group} --name={kv_name} ' \ + '--object-id {identity_object_id} --key-permissions encrypt decrypt -o json' + policy = self.cmd(set_policy, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + # allow the identity approve private endpoint connection (Microsoft.KeyVault/vaults/privateEndpointConnectionsApproval/action) + create_role_assignment = 'role assignment create --role f25e0fa2-a7c8-4377-a976-54943a77a395 ' \ + '--assignee-object-id {identity_object_id} --assignee-principal-type "ServicePrincipal" ' \ + '--scope {kv_resource_id}' + role_assignment = self.cmd(create_role_assignment).get_output_in_json() + + # disable public network access + disable_public_network_access = 'keyvault update --resource-group={resource_group} --name={kv_name} --public-network-access "Disabled" -o json' + kv = self.cmd(disable_public_network_access, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + create_cmd = 'aks create --resource-group={resource_group} --name={name} ' \ + '--assign-identity {identity_id} --enable-private-cluster ' \ + '--enable-azure-keyvault-kms --azure-keyvault-kms-key-id={key_id} ' \ + '--azure-keyvault-kms-key-vault-network-access=Private --azure-keyvault-kms-key-vault-resource-id {kv_resource_id} ' \ + '--ssh-key-value={ssh_key_value} -o json' + self.cmd(create_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('apiServerAccessProfile.enablePrivateCluster', 'True'), + self.check('securityProfile.azureKeyVaultKms.enabled', True), + self.check('securityProfile.azureKeyVaultKms.keyId', key_id_0), + self.check('securityProfile.azureKeyVaultKms.keyVaultNetworkAccess', "Private"), + self.check('securityProfile.azureKeyVaultKms.keyVaultResourceId', kv_resource_id) + ]) + + # enable public network access + enable_public_network_access = 'keyvault update --resource-group={resource_group} --name={kv_name} --public-network-access "Enabled" -o json' + kv = self.cmd(enable_public_network_access, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + key = self.cmd(create_key, checks=[ + self.check('attributes.enabled', True) + ]).get_output_in_json() + key_id_1 = key['key']['kid'] + assert key_id_1 is not None + self.kwargs.update({ + 'key_id': key_id_1, + }) + + # disable public network access + disable_public_network_access = 'keyvault update --resource-group={resource_group} --name={kv_name} --public-network-access "Disabled" -o json' + kv = self.cmd(disable_public_network_access, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + # Rotate key + update_cmd = 'aks update --resource-group={resource_group} --name={name} ' \ + '--enable-azure-keyvault-kms --azure-keyvault-kms-key-id={key_id} ' \ + '--azure-keyvault-kms-key-vault-network-access=Private --azure-keyvault-kms-key-vault-resource-id {kv_resource_id} ' \ + '-o json' + self.cmd(update_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.azureKeyVaultKms.enabled', True), + self.check('securityProfile.azureKeyVaultKms.keyId', key_id_1), + self.check('securityProfile.azureKeyVaultKms.keyVaultNetworkAccess', "Private"), + self.check('securityProfile.azureKeyVaultKms.keyVaultResourceId', kv_resource_id) + ]) + + # delete + cmd = 'aks delete --resource-group={resource_group} --name={name} --yes --no-wait' + self.cmd(cmd, checks=[ + self.is_empty(), + ]) + + @live_only() + @AllowLargeResponse() + @AKSCustomResourceGroupPreparer(random_name_length=17, name_prefix='clitest', location='centraluseuap') + def test_aks_disable_azurekeyvaultkms(self, resource_group, resource_group_location): + aks_name = self.create_random_name('cliakstest', 16) + kv_name = self.create_random_name('cliakstestkv', 16) + identity_name = self.create_random_name('cliakstestidentity', 24) + self.kwargs.update({ + 'resource_group': resource_group, + 'name': aks_name, + "kv_name": kv_name, + "identity_name": identity_name, + 'ssh_key_value': self.generate_ssh_keys() + }) + + # create user-assigned identity + create_identity = 'identity create --resource-group={resource_group} --name={identity_name} -o json' + identity = self.cmd(create_identity).get_output_in_json() + identity_id = identity['id'] + identity_object_id = identity['principalId'] + assert identity_id is not None + assert identity_object_id is not None + self.kwargs.update({ + 'identity_id': identity_id, + 'identity_object_id': identity_object_id, + }) + + # create key vault and key + create_keyvault = 'keyvault create --resource-group={resource_group} --name={kv_name} -o json' + kv = self.cmd(create_keyvault, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + create_key = 'keyvault key create -n kms --vault-name {kv_name} -o json' + key = self.cmd(create_key, checks=[ + self.check('attributes.enabled', True) + ]).get_output_in_json() + key_id = key['key']['kid'] + assert key_id is not None + self.kwargs.update({ + 'key_id': key_id, + }) + + # assign access policy + set_policy = 'keyvault set-policy --resource-group={resource_group} --name={kv_name} ' \ + '--object-id {identity_object_id} --key-permissions encrypt decrypt -o json' + policy = self.cmd(set_policy, checks=[ + self.check('properties.provisioningState', 'Succeeded') + ]).get_output_in_json() + + create_cmd = 'aks create --resource-group={resource_group} --name={name} ' \ + '--assign-identity {identity_id} ' \ + '--enable-azure-keyvault-kms --azure-keyvault-kms-key-id={key_id} --azure-keyvault-kms-key-vault-network-access=Public ' \ + '--ssh-key-value={ssh_key_value} -o json' + self.cmd(create_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.azureKeyVaultKms.enabled', True), + self.check('securityProfile.azureKeyVaultKms.keyId', key_id), + self.check('securityProfile.azureKeyVaultKms.keyVaultNetworkAccess', "Public") + ]) + + update_cmd = 'aks update --resource-group={resource_group} --name={name} ' \ + '--disable-azure-keyvault-kms ' \ + '-o json' + self.cmd(update_cmd, checks=[ + self.check('provisioningState', 'Succeeded'), + self.check('securityProfile.azureKeyVaultKms.enabled', False), + ]) + + # delete + cmd = 'aks delete --resource-group={resource_group} --name={name} --yes --no-wait' + self.cmd(cmd, checks=[ + self.is_empty(), + ]) diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py index 9e25c0a8af2..a2894825c17 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py @@ -57,6 +57,7 @@ ) from azure.cli.command_modules.acs.tests.latest.utils import get_test_data_file_path from azure.cli.core.azclierror import ( + ArgumentUsageError, AzureInternalError, AzCLIError, CLIInternalError, @@ -3913,6 +3914,477 @@ def test_get_defender_config(self): ) self.assertEqual(defender_config_4, ground_truth_defender_config_4) + def test_get_enable_azure_keyvault_kms(self): + ctx_0 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({}), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertIsNone(ctx_0.get_enable_azure_keyvault_kms()) + + ctx_1 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_azure_keyvault_kms": False, + } + ), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_1.get_enable_azure_keyvault_kms(), False) + + key_id_1 = "https://fakekeyvault.vault.azure.net/secrets/fakekeyname/fakekeyversion" + ctx_2 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_azure_keyvault_kms": False, + } + ), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + security_profile = self.models.ManagedClusterSecurityProfile() + security_profile.azure_key_vault_kms = self.models.AzureKeyVaultKms( + enabled=True, + key_id=key_id_1, + ) + mc = self.models.ManagedCluster( + location="test_location", + security_profile=security_profile, + ) + ctx_2.attach_mc(mc) + self.assertEqual(ctx_2.get_enable_azure_keyvault_kms(), True) + + ctx_3 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_azure_keyvault_kms": False, + } + ), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + security_profile = self.models.ManagedClusterSecurityProfile() + security_profile.azure_key_vault_kms = self.models.AzureKeyVaultKms( + enabled=True, + key_id=key_id_1, + ) + mc = self.models.ManagedCluster( + location="test_location", + security_profile=security_profile, + ) + ctx_3.attach_mc(mc) + self.assertEqual(ctx_3.get_enable_azure_keyvault_kms(), False) + + ctx_4 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_azure_keyvault_kms": True, + } + ), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + with self.assertRaises(RequiredArgumentMissingError): + ctx_4.get_enable_azure_keyvault_kms() + + ctx_5 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "azure_keyvault_kms_key_id": "test_azure_keyvault_kms_key_id", + } + ), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + with self.assertRaises(RequiredArgumentMissingError): + ctx_5.get_enable_azure_keyvault_kms() + + def test_get_disable_azure_keyvault_kms(self): + ctx_0 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({}), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + self.assertIsNone(ctx_0.get_enable_azure_keyvault_kms()) + + ctx_1 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "disable_azure_keyvault_kms": True, + } + ), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + self.assertEqual(ctx_1.get_disable_azure_keyvault_kms(), True) + + ctx_2 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "disable_azure_keyvault_kms": False, + } + ), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + self.assertEqual(ctx_2.get_disable_azure_keyvault_kms(), False) + + ctx_3 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_azure_keyvault_kms": True, + "disable_azure_keyvault_kms": True, + } + ), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + with self.assertRaises(MutuallyExclusiveArgumentError): + ctx_3.get_disable_azure_keyvault_kms() + + def test_get_azure_keyvault_kms_key_id(self): + ctx_0 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({}), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertIsNone(ctx_0.get_azure_keyvault_kms_key_id()) + + key_id_1 = "https://fakekeyvault.vault.azure.net/secrets/fakekeyname/fakekeyversion" + ctx_1 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_id": key_id_1, + } + ), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_1.get_azure_keyvault_kms_key_id(), key_id_1) + + ctx_2 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_id": key_id_1, + } + ), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + key_id_2 = "https://fakekeyvault2.vault.azure.net/secrets/fakekeyname2/fakekeyversion2" + security_profile = self.models.ManagedClusterSecurityProfile() + security_profile.azure_key_vault_kms = self.models.AzureKeyVaultKms( + enabled=True, + key_id=key_id_2, + ) + mc = self.models.ManagedCluster( + location="test_location", + security_profile=security_profile, + ) + ctx_2.attach_mc(mc) + self.assertEqual(ctx_2.get_azure_keyvault_kms_key_id(), key_id_2) + + ctx_3 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_id": key_id_1, + } + ), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + security_profile = self.models.ManagedClusterSecurityProfile() + security_profile.azure_key_vault_kms = self.models.AzureKeyVaultKms( + enabled=True, + key_id=key_id_2, + ) + mc = self.models.ManagedCluster( + location="test_location", + security_profile=security_profile, + ) + ctx_3.attach_mc(mc) + self.assertEqual(ctx_3.get_azure_keyvault_kms_key_id(), key_id_1) + + ctx_4 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "azure_keyvault_kms_key_id": key_id_1, + } + ), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + with self.assertRaises(RequiredArgumentMissingError): + ctx_4.get_azure_keyvault_kms_key_id() + + ctx_5 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict( + { + "enable_azure_keyvault_kms": False, + "azure_keyvault_kms_key_id": key_id_1, + } + ), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + with self.assertRaises(RequiredArgumentMissingError): + ctx_5.get_azure_keyvault_kms_key_id() + + def test_get_azure_keyvault_kms_key_vault_network_access(self): + key_vault_network_access_1 = "Public" + key_vault_network_access_2 = "Private" + + ctx_0 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({}), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + with self.assertRaises(RequiredArgumentMissingError): + ctx_0.get_azure_keyvault_kms_key_vault_network_access() + + ctx_1 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "azure_keyvault_kms_key_vault_network_access": key_vault_network_access_1, + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + with self.assertRaises(RequiredArgumentMissingError): + ctx_1.get_azure_keyvault_kms_key_vault_network_access() + + ctx_2 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": False, + "azure_keyvault_kms_key_vault_network_access": key_vault_network_access_1, + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + with self.assertRaises(RequiredArgumentMissingError): + ctx_2.get_azure_keyvault_kms_key_vault_network_access() + + ctx_3 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_vault_network_access": key_vault_network_access_1, + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_3.get_azure_keyvault_kms_key_vault_network_access(), key_vault_network_access_1) + + ctx_4 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_vault_network_access": key_vault_network_access_2, + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + with self.assertRaises(RequiredArgumentMissingError): + ctx_4.get_azure_keyvault_kms_key_vault_network_access() + + ctx_5 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_vault_network_access": key_vault_network_access_2, + "azure_keyvault_kms_key_vault_resource_id": "fake-resource-id", + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_5.get_azure_keyvault_kms_key_vault_network_access(), key_vault_network_access_2) + + ctx_6 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_vault_network_access": key_vault_network_access_2, + "azure_keyvault_kms_key_vault_resource_id": "fake-resource-id", + }), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + security_profile = self.models.ManagedClusterSecurityProfile() + security_profile.azure_key_vault_kms = self.models.AzureKeyVaultKms( + enabled=True, + key_vault_network_access=key_vault_network_access_1, + ) + mc = self.models.ManagedCluster( + location="test_location", + security_profile=security_profile, + ) + ctx_6.attach_mc(mc) + self.assertEqual(ctx_6.get_azure_keyvault_kms_key_vault_network_access(), key_vault_network_access_2) + + def test_get_azure_keyvault_kms_key_vault_resource_id(self): + key_vault_resource_id_1 = "/subscriptions/8ecadfc9-d1a3-4ea4-b844-0d9f87e4d7c8/resourceGroups/foo/providers/Microsoft.KeyVault/vaults/foo" + key_vault_resource_id_2 = "/subscriptions/8ecadfc9-d1a3-4ea4-b844-0d9f87e4d7c8/resourceGroups/bar/providers/Microsoft.KeyVault/vaults/bar" + + ctx_0 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({}), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertIsNone(ctx_0.get_azure_keyvault_kms_key_vault_resource_id()) + + ctx_1 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_vault_network_access": "Public", + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_1.get_azure_keyvault_kms_key_vault_resource_id(), None) + + ctx_2 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_vault_network_access": "Public", + "azure_keyvault_kms_key_vault_resource_id": "", + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_2.get_azure_keyvault_kms_key_vault_resource_id(), "") + + ctx_3 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_vault_network_access": "Private", + "azure_keyvault_kms_key_vault_resource_id": key_vault_resource_id_1, + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + self.assertEqual(ctx_3.get_azure_keyvault_kms_key_vault_resource_id(), key_vault_resource_id_1) + + ctx_4 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_vault_network_access": "Private", + "azure_keyvault_kms_key_vault_resource_id": key_vault_resource_id_1, + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + security_profile = self.models.ManagedClusterSecurityProfile() + security_profile.azure_key_vault_kms = self.models.AzureKeyVaultKms( + enabled=True, + key_vault_network_access="Private", + key_vault_resource_id=key_vault_resource_id_2, + ) + mc = self.models.ManagedCluster( + location="test_location", + security_profile=security_profile, + ) + ctx_4.attach_mc(mc) + self.assertEqual(ctx_4.get_azure_keyvault_kms_key_vault_resource_id(), key_vault_resource_id_2) + + ctx_5 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_vault_network_access": "Private", + "azure_keyvault_kms_key_vault_resource_id": key_vault_resource_id_2, + }), + self.models, + decorator_mode=DecoratorMode.UPDATE, + ) + security_profile = self.models.ManagedClusterSecurityProfile() + security_profile.azure_key_vault_kms = self.models.AzureKeyVaultKms( + enabled=True, + key_vault_network_access="Private", + key_vault_resource_id=key_vault_resource_id_1, + ) + mc = self.models.ManagedCluster( + location="test_location", + security_profile=security_profile, + ) + ctx_5.attach_mc(mc) + self.assertEqual(ctx_5.get_azure_keyvault_kms_key_vault_resource_id(), key_vault_resource_id_2) + + ctx_6 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "azure_keyvault_kms_key_vault_resource_id": key_vault_resource_id_1, + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + with self.assertRaises(RequiredArgumentMissingError): + ctx_6.get_azure_keyvault_kms_key_vault_resource_id() + + ctx_7 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": False, + "azure_keyvault_kms_key_vault_resource_id": key_vault_resource_id_1, + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + with self.assertRaises(RequiredArgumentMissingError): + ctx_7.get_azure_keyvault_kms_key_vault_resource_id() + + ctx_8 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_vault_network_access": "Public", + "azure_keyvault_kms_key_vault_resource_id": key_vault_resource_id_1, + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + with self.assertRaises(ArgumentUsageError): + ctx_8.get_azure_keyvault_kms_key_vault_resource_id() + + ctx_9 = AKSManagedClusterContext( + self.cmd, + AKSManagedClusterParamDict({ + "enable_azure_keyvault_kms": True, + "azure_keyvault_kms_key_vault_network_access": "Private", + "azure_keyvault_kms_key_vault_resource_id": "", + }), + self.models, + decorator_mode=DecoratorMode.CREATE, + ) + with self.assertRaises(ArgumentUsageError): + ctx_9.get_azure_keyvault_kms_key_vault_resource_id() + def test_get_yes(self): # default ctx_1 = AKSManagedClusterContext( diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py index 01d36008632..35b11609ee4 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py @@ -490,5 +490,65 @@ def test_invalid_host_group_id(self): self.assertEqual(str(cm.exception), err) +class AzureKeyVaultKmsKeyIdNamespace: + + def __init__(self, azure_keyvault_kms_key_id): + self.azure_keyvault_kms_key_id = azure_keyvault_kms_key_id + +class TestValidateAzureKeyVaultKmsKeyId(unittest.TestCase): + def test_invalid_azure_keyvault_kms_key_id_without_https(self): + invalid_azure_keyvault_kms_key_id = "dummy key id" + namespace = AzureKeyVaultKmsKeyIdNamespace(azure_keyvault_kms_key_id=invalid_azure_keyvault_kms_key_id) + err = '--azure-keyvault-kms-key-id is not a valid Key Vault key ID. ' \ + 'See https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name' + + with self.assertRaises(CLIError) as cm: + validators.validate_azure_keyvault_kms_key_id(namespace) + self.assertEqual(str(cm.exception), err) + + def test_invalid_azure_keyvault_kms_key_id_without_key_version(self): + invalid_azure_keyvault_kms_key_id = "https://fakekeyvault.vault.azure.net/keys/fakekeyname" + namespace = AzureKeyVaultKmsKeyIdNamespace(azure_keyvault_kms_key_id=invalid_azure_keyvault_kms_key_id) + err = '--azure-keyvault-kms-key-id is not a valid Key Vault key ID. ' \ + 'See https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name' + + with self.assertRaises(CLIError) as cm: + validators.validate_azure_keyvault_kms_key_id(namespace) + self.assertEqual(str(cm.exception), err) + + def test_invalid_azure_keyvault_kms_key_id_with_wrong_object_type(self): + invalid_azure_keyvault_kms_key_id = "https://fakekeyvault.vault.azure.net/secrets/fakesecretname/fakesecretversion" + namespace = AzureKeyVaultKmsKeyIdNamespace(azure_keyvault_kms_key_id=invalid_azure_keyvault_kms_key_id) + err = '--azure-keyvault-kms-key-id is not a valid Key Vault key ID. ' \ + 'See https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name' + + with self.assertRaises(CLIError) as cm: + validators.validate_azure_keyvault_kms_key_id(namespace) + self.assertEqual(str(cm.exception), err) + + +class AzureKeyVaultKmsKeyVaultResourceIdNamespace: + + def __init__(self, azure_keyvault_kms_key_vault_resource_id): + self.azure_keyvault_kms_key_vault_resource_id = azure_keyvault_kms_key_vault_resource_id + + +class TestValidateAzureKeyVaultKmsKeyVaultResourceId(unittest.TestCase): + def test_invalid_azure_keyvault_kms_key_vault_resource_id(self): + invalid_azure_keyvault_kms_key_vault_resource_id = "invalid" + namespace = AzureKeyVaultKmsKeyVaultResourceIdNamespace(azure_keyvault_kms_key_vault_resource_id=invalid_azure_keyvault_kms_key_vault_resource_id) + err = '--azure-keyvault-kms-key-vault-resource-id is not a valid Azure resource ID.' + + with self.assertRaises(InvalidArgumentValueError) as cm: + validators.validate_azure_keyvault_kms_key_vault_resource_id(namespace) + self.assertEqual(str(cm.exception), err) + + def test_valid_azure_keyvault_kms_key_vault_resource_id(self): + valid_azure_keyvault_kms_key_vault_resource_id = "/subscriptions/8ecadfc9-d1a3-4ea4-b844-0d9f87e4d7c8/resourceGroups/foo/providers/Microsoft.KeyVault/vaults/foo" + namespace = AzureKeyVaultKmsKeyVaultResourceIdNamespace(azure_keyvault_kms_key_vault_resource_id=valid_azure_keyvault_kms_key_vault_resource_id) + + validators.validate_azure_keyvault_kms_key_vault_resource_id(namespace) + + if __name__ == "__main__": unittest.main()