diff --git a/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/_validators.py b/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/_validators.py index 75be7e57989..02a5dc78591 100644 --- a/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/_validators.py +++ b/src/command_modules/azure-cli-vm/azure/cli/command_modules/vm/_validators.py @@ -523,15 +523,7 @@ def _validate_vm_create_auth(namespace): StorageProfile.SASpecializedOSDisk]: return - if len(namespace.admin_username) < 6 or namespace.admin_username.lower() == 'root': - # prompt for admin username if inadequate - from azure.cli.core.prompting import prompt, NoTTYException - try: - logger.warning("Cannot use admin username: %s. Admin username should be at " - "least 6 characters and cannot be 'root'", namespace.admin_username) - namespace.admin_username = prompt('Admin Username: ') - except NoTTYException: - raise CLIError('Please specify a valid admin username in non-interactive mode.') + _validate_admin_username(namespace.admin_username, namespace.os_type) if not namespace.os_type: raise CLIError("Unable to resolve OS type. Specify '--os-type' argument.") @@ -551,13 +543,16 @@ def _validate_vm_create_auth(namespace): "incorrect usage for authentication-type 'password': " "[--admin-username USERNAME] --admin-password PASSWORD") - if not namespace.admin_password: - # prompt for admin password if not supplied - from azure.cli.core.prompting import prompt_pass, NoTTYException - try: + from azure.cli.core.prompting import prompt_pass, NoTTYException + try: + if not namespace.admin_password: namespace.admin_password = prompt_pass('Admin Password: ', confirm=True) - except NoTTYException: - raise CLIError('Please specify both username and password in non-interactive mode.') + except NoTTYException: + raise CLIError('Please specify password in non-interactive mode.') + + # validate password + _validate_admin_password(namespace.admin_password, + namespace.os_type) elif namespace.authentication_type == 'ssh': @@ -571,6 +566,48 @@ def _validate_vm_create_auth(namespace): '/home/{}/.ssh/authorized_keys'.format(namespace.admin_username) +def _validate_admin_username(username, os_type): + if not username: + raise CLIError("admin user name can not be empty") + is_linux = (os_type.lower() == 'linux') + # pylint: disable=line-too-long + pattern = (r'[\\\/"\[\]:|<>+=;,?*@#()!A-Z]+' if is_linux else r'[\\\/"\[\]:|<>+=;,?*@]+') + linux_err = r'admin user name cannot contain upper case character A-Z, special characters \/"[]:|<>+=;,?*@#()! or start with $ or -' + win_err = r'admin user name cannot contain special characters \/"[]:|<>+=;,?*@# or ends with .' + if re.findall(pattern, username): + raise CLIError(linux_err if is_linux else win_err) + if is_linux and re.findall(r'^[$-]+', username): + raise CLIError(linux_err) + if not is_linux and username.endswith('.'): + raise CLIError(win_err) + disallowed_user_names = [ + "administrator", "admin", "user", "user1", "test", "user2", + "test1", "user3", "admin1", "1", "123", "a", "actuser", "adm", + "admin2", "aspnet", "backup", "console", "david", "guest", "john", + "owner", "root", "server", "sql", "support", "support_388945a0", + "sys", "test2", "test3", "user4", "user5"] + if username.lower() in disallowed_user_names: + raise CLIError("This user name '{}' meets the general requirements, but is specifically disallowed for this image. Please try a different value.".format(username)) + + +def _validate_admin_password(password, os_type): + is_linux = (os_type.lower() == 'linux') + max_length = 72 if is_linux else 123 + min_length = 12 + if len(password) not in range(min_length, max_length + 1): + raise CLIError('The pssword length must be between {} and {}'.format(min_length, + max_length)) + contains_lower = re.findall('[a-z]+', password) + contains_upper = re.findall('[A-Z]+', password) + contains_digit = re.findall('[0-9]+', password) + contains_special_char = re.findall(r'[ `~!@#$%^&*()=+_\[\]{}\|;:.\/\'\",<>?]+', password) + count = len([x for x in [contains_lower, contains_upper, + contains_digit, contains_special_char] if x]) + # pylint: disable=line-too-long + if count < 3: + raise CLIError('Password must have the 3 of the following: 1 lower case character, 1 upper case character, 1 number and 1 special character') + + def validate_ssh_key(namespace): string_or_file = (namespace.ssh_key_value or os.path.join(os.path.expanduser('~'), '.ssh/id_rsa.pub')) diff --git a/src/command_modules/azure-cli-vm/tests/test_vm_actions.py b/src/command_modules/azure-cli-vm/tests/test_vm_actions.py index a893c8470fb..8cc6ab7478c 100644 --- a/src/command_modules/azure-cli-vm/tests/test_vm_actions.py +++ b/src/command_modules/azure-cli-vm/tests/test_vm_actions.py @@ -12,7 +12,9 @@ from azure.cli.command_modules.vm._validators import (validate_ssh_key, _is_valid_ssh_rsa_public_key, - _figure_out_storage_source) + _figure_out_storage_source, + _validate_admin_username, + _validate_admin_password) class TestActions(unittest.TestCase): @@ -65,3 +67,67 @@ def test_figure_out_storage_source(self): self.assertFalse(src_disk) self.assertFalse(src_snapshot) self.assertEqual(src_blob_uri, test_data) + + def test_validate_admin_username_linux(self): + # pylint: disable=line-too-long + err_invalid_char = r'admin user name cannot contain upper case character A-Z, special characters \/"[]:|<>+=;,?*@#()! or start with $ or -' + + self._verify_username_with_ex('!@#', 'linux', err_invalid_char) + self._verify_username_with_ex('dav[', 'linux', err_invalid_char) + self._verify_username_with_ex('Adavid', 'linux', err_invalid_char) + self._verify_username_with_ex('-ddavid', 'linux', err_invalid_char) + self._verify_username_with_ex('', 'linux', 'admin user name can not be empty') + self._verify_username_with_ex('david', 'linux', + "This user name 'david' meets the general requirements, but is specifically disallowed for this image. Please try a different value.") + + _validate_admin_username('d-avid1', 'linux') + _validate_admin_username('david1', 'linux') + _validate_admin_username('david1.', 'linux') + + def test_validate_admin_username_windows(self): + # pylint: disable=line-too-long + err_invalid_char = r'admin user name cannot contain special characters \/"[]:|<>+=;,?*@# or ends with .' + + self._verify_username_with_ex('!@#', 'windows', err_invalid_char) + self._verify_username_with_ex('dav[', 'windows', err_invalid_char) + self._verify_username_with_ex('dddivid.', 'windows', err_invalid_char) + self._verify_username_with_ex('john', 'windows', + "This user name 'john' meets the general requirements, but is specifically disallowed for this image. Please try a different value.") + + _validate_admin_username('ADAVID', 'windows') + _validate_admin_username('d-avid1', 'windows') + _validate_admin_username('david1', 'windows') + + def test_validate_admin_password_linux(self): + # pylint: disable=line-too-long + err_length = 'The pssword length must be between 12 and 72' + err_variety = 'Password must have the 3 of the following: 1 lower case character, 1 upper case character, 1 number and 1 special character' + + self._verify_password_with_ex('te', 'linux', err_length) + self._verify_password_with_ex('P12' + '3' * 70, 'linux', err_length) + self._verify_password_with_ex('te12312312321', 'linux', err_variety) + + _validate_admin_password('Password22345', 'linux') + _validate_admin_password('Password12!@#', 'linux') + + def test_validate_admin_password_windows(self): + # pylint: disable=line-too-long + err_length = 'The pssword length must be between 12 and 123' + err_variety = 'Password must have the 3 of the following: 1 lower case character, 1 upper case character, 1 number and 1 special character' + + self._verify_password_with_ex('P1', 'windows', err_length) + self._verify_password_with_ex('te14' + '3' * 120, 'windows', err_length) + self._verify_password_with_ex('te12345678997', 'windows', err_variety) + + _validate_admin_password('Password22!!!', 'windows') + _validate_admin_password('Pas' + '1' * 70, 'windows') + + def _verify_username_with_ex(self, admin_username, is_linux, expected_err): + with self.assertRaises(CLIError) as context: + _validate_admin_username(admin_username, is_linux) + self.assertTrue(expected_err in str(context.exception)) + + def _verify_password_with_ex(self, admin_password, is_linux, expected_err): + with self.assertRaises(CLIError) as context: + _validate_admin_password(admin_password, is_linux) + self.assertTrue(expected_err in str(context.exception)) diff --git a/src/command_modules/azure-cli-vm/tests/test_vm_commands.py b/src/command_modules/azure-cli-vm/tests/test_vm_commands.py index 237ae93652f..9ae05618cd2 100644 --- a/src/command_modules/azure-cli-vm/tests/test_vm_commands.py +++ b/src/command_modules/azure-cli-vm/tests/test_vm_commands.py @@ -497,7 +497,7 @@ def test_vm_create_no_wait(self): self.execute() def body(self): - self.cmd('vm create -g {} -n {} --admin-username user12 --admin-password VerySecret! --authentication-type password --image UbuntuLTS --no-wait'.format(self.resource_group, self.name), checks=NoneCheck()) + self.cmd('vm create -g {} -n {} --admin-username user12 --admin-password testPassword0 --authentication-type password --image UbuntuLTS --no-wait'.format(self.resource_group, self.name), checks=NoneCheck()) self.cmd('vm wait -g {} -n {} --custom "{}"'.format(self.resource_group, self.name, "instanceView.statuses[?code=='PowerState/running']"), checks=NoneCheck()) self.cmd('vm get-instance-view -g {} -n {}'.format(self.resource_group, self.name), checks=[ JMESPathCheck("length(instanceView.statuses[?code=='PowerState/running'])", 1) @@ -547,7 +547,7 @@ def __init__(self, test_method): def set_up(self): super(VMExtensionScenarioTest, self).set_up() - self.cmd('vm create -n {} -g {} --image UbuntuLTS --authentication-type password --admin-username user11 --admin-password TestPass1@'.format(self.vm_name, self.resource_group)) + self.cmd('vm create -n {} -g {} --image UbuntuLTS --authentication-type password --admin-username user11 --admin-password testPassword0'.format(self.vm_name, self.resource_group)) def test_vm_extension(self): self.execute() @@ -764,7 +764,7 @@ def test_vm_boot_diagnostics(self): def set_up(self): super(VMBootDiagnostics, self).set_up() self.cmd('storage account create -g {} -n {} --sku Standard_LRS -l westus'.format(self.resource_group, self.storage_name)) - self.cmd('vm create -n {} -g {} --image UbuntuLTS --authentication-type password --admin-username user11 --admin-password TestPass1@ --use-unmanaged-disk'.format(self.vm_name, self.resource_group)) + self.cmd('vm create -n {} -g {} --image UbuntuLTS --authentication-type password --admin-username user11 --admin-password testPassword0 --use-unmanaged-disk'.format(self.vm_name, self.resource_group)) def body(self): storage_uri = 'https://{}.blob.core.windows.net/'.format(self.storage_name) @@ -789,7 +789,7 @@ def __init__(self, test_method): def set_up(self): super(VMSSExtensionInstallTest, self).set_up() - self.cmd('vmss create -n {} -g {} --image UbuntuLTS --authentication-type password --admin-username admin123 --admin-password TestPass1@'.format(self.vmss_name, self.resource_group)) + self.cmd('vmss create -n {} -g {} --image UbuntuLTS --authentication-type password --admin-username admin123 --admin-password testPassword0'.format(self.vmss_name, self.resource_group)) def test_vmss_extension(self): self.execute() @@ -1369,7 +1369,7 @@ def body(self): instance_count = 5 new_instance_count = 4 - self.cmd('vmss create --admin-password Test1234@! --name {} -g {} --admin-username myadmin --image Win2012R2Datacenter --instance-count {}' + self.cmd('vmss create --admin-password testPassword0 --name {} -g {} --admin-username myadmin --image Win2012R2Datacenter --instance-count {}' .format(vmss_name, self.resource_group, instance_count)) self.cmd('vmss show --name {} -g {}'.format(vmss_name, self.resource_group), @@ -1441,7 +1441,7 @@ def body(self): caching = 'ReadWrite' upgrade_policy = 'automatic' - self.cmd('vmss create --image Debian --admin-password Test1234@! -l westus' + self.cmd('vmss create --image Debian --admin-password testPassword0 -l westus' ' -g {} -n {} --disable-overprovision --instance-count {}' ' --storage-caching {} --upgrade-policy-mode {}' ' --authentication-type password --admin-username myadmin --public-ip-address {}' diff --git a/src/command_modules/azure-cli-vm/tests/test_vm_defaults.py b/src/command_modules/azure-cli-vm/tests/test_vm_defaults.py index 57e027f6f61..9543ccd5fa0 100644 --- a/src/command_modules/azure-cli-vm/tests/test_vm_defaults.py +++ b/src/command_modules/azure-cli-vm/tests/test_vm_defaults.py @@ -263,7 +263,7 @@ def test_linux_with_password(self): ns.os_type = "LINux" ns.authentication_type = 'password' ns.admin_username = 'user12345' - ns.admin_password = 'verySecret!' + ns.admin_password = 'verySecret!!!' _validate_vm_create_auth(ns) # still has 'password' self.assertEqual(ns.authentication_type, 'password')