Skip to content

Commit

Permalink
VM/VMSS: incorporate credentials validation logic used by portal (#2537)
Browse files Browse the repository at this point in the history
* remove the length limitation of admin-username as shorter name is accpeted by portal and vm

* update tests to use legit password

* make validation routine throw

* address review feedback
  • Loading branch information
yugangw-msft committed Mar 17, 2017
1 parent ffce175 commit 9a31807
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 23 deletions.
Expand Up @@ -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.")
Expand All @@ -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':

Expand All @@ -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'))
Expand Down
68 changes: 67 additions & 1 deletion src/command_modules/azure-cli-vm/tests/test_vm_actions.py
Expand Up @@ -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):
Expand Down Expand Up @@ -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))
12 changes: 6 additions & 6 deletions src/command_modules/azure-cli-vm/tests/test_vm_commands.py
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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 {}'
Expand Down
2 changes: 1 addition & 1 deletion src/command_modules/azure-cli-vm/tests/test_vm_defaults.py
Expand Up @@ -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')
Expand Down

0 comments on commit 9a31807

Please sign in to comment.