Esempio n. 1
0
 def from_passwd(uid_min=None, uid_max=None):
     """Create collection from locally discovered data, e.g. /etc/passwd."""
     import pwd
     users = Users(oktypes=User)
     passwd_list = pwd.getpwall()
     if not uid_min:
         uid_min = UID_MIN
     if not uid_max:
         uid_max = UID_MAX
     sudoers_entries = read_sudoers()
     for pwd_entry in passwd_list:
         if uid_min <= pwd_entry.pw_uid <= uid_max:
             user = User(name=text_type(pwd_entry.pw_name),
                         passwd=text_type(pwd_entry.pw_passwd),
                         uid=pwd_entry.pw_uid,
                         gid=pwd_entry.pw_gid,
                         gecos=text_type(pwd_entry.pw_gecos),
                         home_dir=text_type(pwd_entry.pw_dir),
                         shell=text_type(pwd_entry.pw_shell),
                         public_keys=read_authorized_keys(
                             username=pwd_entry.pw_name),
                         sudoers_entry=get_sudoers_entry(
                             username=pwd_entry.pw_name,
                             sudoers_entries=sudoers_entries))
             users.append(user)
     return users
Esempio n. 2
0
def remove_sudoers_entry(username=None):
    """Remove sudoers entry.

    args:
        user (User): Instance of User containing sudoers entry.

    returns:
        str: sudoers entry for the specified user.
    """
    sudoers_path = '/etc/sudoers'
    rnd_chars = random_string(length=RANDOM_FILE_EXT_LENGTH)
    tmp_sudoers_path = '/tmp/sudoers_{0}'.format(rnd_chars)
    execute_command(
        shlex.split(str('{0} cp {1} {2}'.format(sudo_check(), sudoers_path, tmp_sudoers_path))))
    execute_command(
        shlex.split(str('{0} chmod 777 {1}'.format(sudo_check(), tmp_sudoers_path))))
    with open(tmp_sudoers_path, mode=text_type('r')) as tmp_sudoers_file:
        sudoers_entries = tmp_sudoers_file.readlines()
    sudoers_output = list()
    for entry in sudoers_entries:
        if not entry.startswith(username):
            sudoers_output.append(entry)
    with open(tmp_sudoers_path, mode=text_type('w+')) as tmp_sudoers_file:
        tmp_sudoers_file.writelines(sudoers_output)
    execute_command(
        shlex.split(str('{0} cp {1} {2}'.format(sudo_check(), tmp_sudoers_path, sudoers_path))))
    execute_command(shlex.split(str('{0} chown root:root {1}'.format(sudo_check(), sudoers_path))))
    execute_command(shlex.split(str('{0} chmod 440 {1}'.format(sudo_check(), sudoers_path))))
    execute_command(shlex.split(str('{0} rm {1}'.format(sudo_check(), tmp_sudoers_path))))
Esempio n. 3
0
    def raw(self):
        """Return raw key.

        returns:
            str: raw key
        """
        if self._raw:
            return text_type(self._raw).strip("\r\n")
        else:
            return text_type(base64decode(self._b64encoded)).strip("\r\n")
Esempio n. 4
0
    def raw(self):
        """Return raw key.

        returns:
            str: raw key
        """
        if self._raw:
            return text_type(self._raw).strip("\r\n")
        else:
            return text_type(base64decode(self._b64encoded)).strip("\r\n")
Esempio n. 5
0
def write_sudoers_entry(username=None, sudoers_entry=None):
    """Write sudoers entry.

    args:
        user (User): Instance of User containing sudoers entry.

    returns:
        str: sudoers entry for the specified user.
    """

    sudoers_path = '/etc/sudoers'
    rnd_chars = random_string(length=RANDOM_FILE_EXT_LENGTH)
    tmp_sudoers_path = '/tmp/sudoers_{0}'.format(rnd_chars)
    execute_command(
        shlex.split(
            str('{0} cp {1} {2}'.format(sudo_check(), sudoers_path,
                                        tmp_sudoers_path))))
    execute_command(
        shlex.split(
            str('{0} chmod 777 {1}'.format(sudo_check(), tmp_sudoers_path))))
    with open(tmp_sudoers_path, mode=text_type('r')) as tmp_sudoers_file:
        sudoers_entries = tmp_sudoers_file.readlines()
    sudoers_output = list()
    for entry in sudoers_entries:
        if entry and not entry.startswith(username):
            sudoers_output.append(entry)
    if sudoers_entry:
        sudoers_output.append('{0} {1}'.format(username, sudoers_entry))
        sudoers_output.append('\n')
    with open(tmp_sudoers_path, mode=text_type('w+')) as tmp_sudoers_file:
        tmp_sudoers_file.writelines(sudoers_output)
    sudoers_check_result = execute_command(
        shlex.split(
            str('{0} {1} -cf {2}'.format(sudo_check(), LINUX_CMD_VISUDO,
                                         tmp_sudoers_path))))
    if sudoers_check_result[1] > 0:
        raise ValueError(sudoers_check_result[0][1])
    execute_command(
        shlex.split(
            str('{0} cp {1} {2}'.format(sudo_check(), tmp_sudoers_path,
                                        sudoers_path))))
    execute_command(
        shlex.split(
            str('{0} chown root:root {1}'.format(sudo_check(), sudoers_path))))
    execute_command(
        shlex.split(str('{0} chmod 440 {1}'.format(sudo_check(),
                                                   sudoers_path))))
    execute_command(
        shlex.split(str('{0} rm {1}'.format(sudo_check(), tmp_sudoers_path))))
Esempio n. 6
0
    def gecos(self):
        """Force double quoted gecos.

        returns:
            str: The double quoted gecos.
        """
        if not self._gecos:
            return None
        if self._gecos.startswith(text_type('\'')) and self._gecos.endswith(text_type('\'')):
            self._gecos = '\"{0}\"'.format(self._gecos[1:-1])
            return self._gecos
        elif self._gecos.startswith(text_type('\"')) and self._gecos.endswith(text_type('\"')):
            return self._gecos
        else:
            return '\"{0}\"'.format(self._gecos)
Esempio n. 7
0
def write_authorized_keys(user=None):
    """Write public keys back to authorized_keys file. Create keys directory if it doesn't already exist.

    args:
        user (User): Instance of User containing keys.

    returns:
        list: Authorised keys for the specified user.
    """
    authorized_keys = list()
    authorized_keys_dir = '{0}/.ssh'.format(os.path.expanduser('~{0}'.format(user.name)))
    rnd_chars = random_string(length=RANDOM_FILE_EXT_LENGTH)
    authorized_keys_path = '{0}/authorized_keys'.format(authorized_keys_dir)
    tmp_authorized_keys_path = '/tmp/authorized_keys_{0}_{1}'.format(user.name, rnd_chars)

    if not os.path.isdir(authorized_keys_dir):
        execute_command(shlex.split(str('{0} mkdir -p {1}'.format(sudo_check(), authorized_keys_dir))))
    for key in user.public_keys:
        authorized_keys.append('{0}\n'.format(key.raw))
    with open(tmp_authorized_keys_path, mode=text_type('w+')) as keys_file:
        keys_file.writelines(authorized_keys)
    execute_command(
        shlex.split(str('{0} cp {1} {2}'.format(sudo_check(), tmp_authorized_keys_path, authorized_keys_path))))
    execute_command(shlex.split(str('{0} chown -R {1} {2}'.format(sudo_check(), user.name, authorized_keys_dir))))
    execute_command(shlex.split(str('{0} chmod 700 {1}'.format(sudo_check(), authorized_keys_dir))))
    execute_command(shlex.split(str('{0} chmod 600 {1}'.format(sudo_check(), authorized_keys_path))))
    execute_command(shlex.split(str('{0} rm {1}'.format(sudo_check(), tmp_authorized_keys_path))))
Esempio n. 8
0
    def gecos(self):
        """Force double quoted gecos.

        returns:
            str: The double quoted gecos.
        """
        if not self._gecos:
            return None
        if self._gecos.startswith(text_type('\'')) and self._gecos.endswith(
                text_type('\'')):
            self._gecos = '\"{0}\"'.format(self._gecos[1:-1])
            return self._gecos
        elif self._gecos.startswith(text_type('\"')) and self._gecos.endswith(
                text_type('\"')):
            return self._gecos
        else:
            return '\"{0}\"'.format(self._gecos)
Esempio n. 9
0
 def from_json(cls, file_path=None):
     """Create collection from a JSON file."""
     with io.open(file_path, encoding=text_type('utf-8')) as stream:
         try:
             users_json = json.load(stream)
         except ValueError:
             raise ValueError('No JSON object could be decoded')
         return cls.construct_user_list(raw_users=users_json.get('users'))
Esempio n. 10
0
 def from_json(cls, file_path=None):
     """Create collection from a JSON file."""
     with io.open(file_path, encoding=text_type('utf-8')) as stream:
         try:
             users_json = json.load(stream)
         except ValueError:
             raise ValueError('No JSON object could be decoded')
         return cls.construct_user_list(raw_users=users_json.get('users'))
Esempio n. 11
0
 def export(self, file_path=None, export_format=None):
     """ Write the users to a file. """
     with io.open(file_path, mode='w', encoding="utf-8") as export_file:
         if export_format == 'yaml':
             import yaml
             yaml.safe_dump(self.to_dict(), export_file, default_flow_style=False)
         elif export_format == 'json':
             export_file.write(text_type(json.dumps(self.to_dict(), ensure_ascii=False)))
         return True
Esempio n. 12
0
    def b64encoded(self):
        """Return a base64 encoding of the key.

        returns:
            str: base64 encoding of the public key
        """
        if self._b64encoded:
            return text_type(self._b64encoded).strip("\r\n")
        else:
            return base64encode(self.raw)
Esempio n. 13
0
    def b64encoded(self):
        """Return a base64 encoding of the key.

        returns:
            str: base64 encoding of the public key
        """
        if self._b64encoded:
            return text_type(self._b64encoded).strip("\r\n")
        else:
            return base64encode(self.raw)
Esempio n. 14
0
 def export(self, file_path=None, export_format=None):
     """ Write the users to a file. """
     with io.open(file_path, mode='w', encoding="utf-8") as export_file:
         if export_format == 'yaml':
             import yaml
             yaml.safe_dump(self.to_dict(),
                            export_file,
                            default_flow_style=False)
         elif export_format == 'json':
             export_file.write(
                 text_type(json.dumps(self.to_dict(), ensure_ascii=False)))
         return True
Esempio n. 15
0
def remove_sudoers_entry(username=None):
    """Remove sudoers entry.

    args:
        user (User): Instance of User containing sudoers entry.

    returns:
        str: sudoers entry for the specified user.
    """
    sudoers_path = '/etc/sudoers'
    rnd_chars = random_string(length=RANDOM_FILE_EXT_LENGTH)
    tmp_sudoers_path = '/tmp/sudoers_{0}'.format(rnd_chars)
    execute_command(
        shlex.split(
            str('{0} cp {1} {2}'.format(sudo_check(), sudoers_path,
                                        tmp_sudoers_path))))
    execute_command(
        shlex.split(
            str('{0} chmod 777 {1}'.format(sudo_check(), tmp_sudoers_path))))
    with open(tmp_sudoers_path, mode=text_type('r')) as tmp_sudoers_file:
        sudoers_entries = tmp_sudoers_file.readlines()
    sudoers_output = list()
    for entry in sudoers_entries:
        if not entry.startswith(username):
            sudoers_output.append(entry)
    with open(tmp_sudoers_path, mode=text_type('w+')) as tmp_sudoers_file:
        tmp_sudoers_file.writelines(sudoers_output)
    execute_command(
        shlex.split(
            str('{0} cp {1} {2}'.format(sudo_check(), tmp_sudoers_path,
                                        sudoers_path))))
    execute_command(
        shlex.split(
            str('{0} chown root:root {1}'.format(sudo_check(), sudoers_path))))
    execute_command(
        shlex.split(str('{0} chmod 440 {1}'.format(sudo_check(),
                                                   sudoers_path))))
    execute_command(
        shlex.split(str('{0} rm {1}'.format(sudo_check(), tmp_sudoers_path))))
Esempio n. 16
0
def login_defs():
    """Discover the minimum and maximum UID number."""
    uid_min = None
    uid_max = None
    login_defs_path = '/etc/login.defs'
    if os.path.exists(login_defs_path):
        with io.open(text_type(login_defs_path), encoding=text_type('utf-8')) as log_defs_file:
            login_data = log_defs_file.readlines()
        for line in login_data:
            if PY3:  # pragma: no cover
                line = str(line)
            if PY2:  # pragma: no cover
                line = line.encode(text_type('utf8'))
            if line[:7] == text_type('UID_MIN'):
                uid_min = int(line.split()[1].strip())
            if line[:7] == text_type('UID_MAX'):
                uid_max = int(line.split()[1].strip())
    if not uid_min:  # pragma: no cover
        uid_min = DEFAULT_UID_MIN
    if not uid_max:  # pragma: no cover
        uid_max = DEFAULT_UID_MAX
    return uid_min, uid_max
Esempio n. 17
0
def login_defs():
    """Discover the minimum and maximum UID number."""
    uid_min = None
    uid_max = None
    login_defs_path = '/etc/login.defs'
    if os.path.exists(login_defs_path):
        with io.open(text_type(login_defs_path),
                     encoding=text_type('utf-8')) as log_defs_file:
            login_data = log_defs_file.readlines()
        for line in login_data:
            if PY3:  # pragma: no cover
                line = str(line)
            if PY2:  # pragma: no cover
                line = line.encode(text_type('utf8'))
            if line[:7] == text_type('UID_MIN'):
                uid_min = int(line.split()[1].strip())
            if line[:7] == text_type('UID_MAX'):
                uid_max = int(line.split()[1].strip())
    if not uid_min:  # pragma: no cover
        uid_min = DEFAULT_UID_MIN
    if not uid_max:  # pragma: no cover
        uid_max = DEFAULT_UID_MAX
    return uid_min, uid_max
Esempio n. 18
0
def write_sudoers_entry(username=None, sudoers_entry=None):
    """Write sudoers entry.

    args:
        user (User): Instance of User containing sudoers entry.

    returns:
        str: sudoers entry for the specified user.
    """

    sudoers_path = '/etc/sudoers'
    rnd_chars = random_string(length=RANDOM_FILE_EXT_LENGTH)
    tmp_sudoers_path = '/tmp/sudoers_{0}'.format(rnd_chars)
    execute_command(
        shlex.split(str('{0} cp {1} {2}'.format(sudo_check(), sudoers_path, tmp_sudoers_path))))
    execute_command(
        shlex.split(str('{0} chmod 777 {1}'.format(sudo_check(), tmp_sudoers_path))))
    with open(tmp_sudoers_path, mode=text_type('r')) as tmp_sudoers_file:
        sudoers_entries = tmp_sudoers_file.readlines()
    sudoers_output = list()
    for entry in sudoers_entries:
        if entry and not entry.startswith(username):
            sudoers_output.append(entry)
    if sudoers_entry:
        sudoers_output.append('{0} {1}'.format(username, sudoers_entry))
        sudoers_output.append('\n')
    with open(tmp_sudoers_path, mode=text_type('w+')) as tmp_sudoers_file:
        tmp_sudoers_file.writelines(sudoers_output)
    sudoers_check_result = execute_command(
        shlex.split(str('{0} {1} -cf {2}'.format(sudo_check(), LINUX_CMD_VISUDO, tmp_sudoers_path))))
    if sudoers_check_result[1] > 0:
        raise ValueError(sudoers_check_result[0][1])
    execute_command(
        shlex.split(str('{0} cp {1} {2}'.format(sudo_check(), tmp_sudoers_path, sudoers_path))))
    execute_command(shlex.split(str('{0} chown root:root {1}'.format(sudo_check(), sudoers_path))))
    execute_command(shlex.split(str('{0} chmod 440 {1}'.format(sudo_check(), sudoers_path))))
    execute_command(shlex.split(str('{0} rm {1}'.format(sudo_check(), tmp_sudoers_path))))
Esempio n. 19
0
 def from_passwd(uid_min=None, uid_max=None):
     """Create collection from locally discovered data, e.g. /etc/passwd."""
     import pwd
     users = Users(oktypes=User)
     passwd_list = pwd.getpwall()
     if not uid_min:
         uid_min = UID_MIN
     if not uid_max:
         uid_max = UID_MAX
     sudoers_entries = read_sudoers()
     for pwd_entry in passwd_list:
         if uid_min <= pwd_entry.pw_uid <= uid_max:
             user = User(name=text_type(pwd_entry.pw_name),
                         passwd=text_type(pwd_entry.pw_passwd),
                         uid=pwd_entry.pw_uid,
                         gid=pwd_entry.pw_gid,
                         gecos=text_type(pwd_entry.pw_gecos),
                         home_dir=text_type(pwd_entry.pw_dir),
                         shell=text_type(pwd_entry.pw_shell),
                         public_keys=read_authorized_keys(username=pwd_entry.pw_name),
                         sudoers_entry=get_sudoers_entry(username=pwd_entry.pw_name,
                                                         sudoers_entries=sudoers_entries))
             users.append(user)
     return users
Esempio n. 20
0
def test_create_and_execute_plan_to_create_new_user_with_sudo_all():
    """ Test creation of a user instance with sudo all and then write """
    delete_test_user_and_group()
    create_test_group()
    current_users = Users.from_passwd()
    provided_users = Users()

    public_keys = [PublicKey(b64encoded=PUBLIC_KEYS[0]['encoded'])]
    provided_users.append(
        User(name='testuserx1234',
             home_dir='/home/testuserx1234',
             shell='/bin/false',
             gid=59999,
             uid=59999,
             gecos='test user gecos',
             public_keys=public_keys,
             sudoers_entry='ALL=(ALL)\tNOPASSWD:ALL'))
    plan = create_plan(existing_users=current_users,
                       proposed_users=provided_users,
                       purge_undefined=True,
                       protected_users=[
                           'travis', 'couchdb', 'ubuntu', 'vagrant',
                           CURRENT_USER
                       ])
    assert plan[0]['state'] == 'missing'
    assert plan[0]['proposed_user'].name == "testuserx1234"
    assert plan[0]['proposed_user'].home_dir == "/home/testuserx1234"
    assert plan[0]['proposed_user'].uid == 59999
    assert plan[0]['proposed_user'].gid == 59999
    assert plan[0]['proposed_user'].gecos == '\"test user gecos\"'
    assert plan[0]['proposed_user'].shell == '/bin/false'
    assert plan[0]['proposed_user'].sudoers_entry == 'ALL=(ALL)\tNOPASSWD:ALL'
    assert type(plan[0]['proposed_user'].public_keys[0].raw) == text_type
    assert plan[0]['proposed_user'].public_keys[0].raw == text_type(
        PUBLIC_KEYS[0]['raw'])
    execute_plan(plan=plan)
    current_users = Users.from_passwd()
    created_user = current_users.describe_users(users_filter=dict(
        name='testuserx1234'))
    assert created_user[0].sudoers_entry == 'ALL=(ALL)\tNOPASSWD:ALL'
    plan = create_plan(existing_users=current_users,
                       proposed_users=provided_users,
                       purge_undefined=True,
                       protected_users=[
                           'travis', 'couchdb', 'ubuntu', 'nginx', 'vagrant',
                           CURRENT_USER
                       ])
    assert not plan
Esempio n. 21
0
def write_authorized_keys(user=None):
    """Write public keys back to authorized_keys file. Create keys directory if it doesn't already exist.

    args:
        user (User): Instance of User containing keys.

    returns:
        list: Authorised keys for the specified user.
    """
    authorized_keys = list()
    authorized_keys_dir = '{0}/.ssh'.format(
        os.path.expanduser('~{0}'.format(user.name)))
    rnd_chars = random_string(length=RANDOM_FILE_EXT_LENGTH)
    authorized_keys_path = '{0}/authorized_keys'.format(authorized_keys_dir)
    tmp_authorized_keys_path = '/tmp/authorized_keys_{0}_{1}'.format(
        user.name, rnd_chars)

    if not os.path.isdir(authorized_keys_dir):
        execute_command(
            shlex.split(
                str('{0} mkdir -p {1}'.format(sudo_check(),
                                              authorized_keys_dir))))
    for key in user.public_keys:
        authorized_keys.append('{0}\n'.format(key.raw))
    with open(tmp_authorized_keys_path, mode=text_type('w+')) as keys_file:
        keys_file.writelines(authorized_keys)
    execute_command(
        shlex.split(
            str('{0} cp {1} {2}'.format(sudo_check(), tmp_authorized_keys_path,
                                        authorized_keys_path))))
    execute_command(
        shlex.split(
            str('{0} chown -R {1} {2}'.format(sudo_check(), user.name,
                                              authorized_keys_dir))))
    execute_command(
        shlex.split(
            str('{0} chmod 700 {1}'.format(sudo_check(),
                                           authorized_keys_dir))))
    execute_command(
        shlex.split(
            str('{0} chmod 600 {1}'.format(sudo_check(),
                                           authorized_keys_path))))
    execute_command(
        shlex.split(
            str('{0} rm {1}'.format(sudo_check(), tmp_authorized_keys_path))))
Esempio n. 22
0
    def from_yaml(cls, file_path=None):
        """Create collection from a YAML file."""
        try:
            import yaml
        except ImportError:  # pragma: no cover
            yaml = None
        if not yaml:
            import sys
            sys.exit('PyYAML is not installed, but is required in order to parse YAML files.'
                     '\nTo install, run:\n$ pip install PyYAML\nor visit'
                     ' http://pyyaml.org/wiki/PyYAML for instructions.')

        with io.open(file_path, encoding=text_type('utf-8')) as stream:
            users_yaml = yaml.safe_load(stream)
            if isinstance(users_yaml, dict):
                return cls.construct_user_list(raw_users=users_yaml.get('users'))
            else:
                raise ValueError('No YAML object could be decoded')
Esempio n. 23
0
    def from_yaml(cls, file_path=None):
        """Create collection from a YAML file."""
        try:
            import yaml
        except ImportError:  # pragma: no cover
            yaml = None
        if not yaml:
            import sys
            sys.exit(
                'PyYAML is not installed, but is required in order to parse YAML files.'
                '\nTo install, run:\n$ pip install PyYAML\nor visit'
                ' http://pyyaml.org/wiki/PyYAML for instructions.')

        with io.open(file_path, encoding=text_type('utf-8')) as stream:
            users_yaml = yaml.safe_load(stream)
            if isinstance(users_yaml, dict):
                return cls.construct_user_list(
                    raw_users=users_yaml.get('users'))
            else:
                raise ValueError('No YAML object could be decoded')
Esempio n. 24
0
def test_execute_plan_to_update_existing_user():
    """ Create a new user and then attempt to create another user with existing id """

    delete_test_user_and_group()
    create_test_user()
    raw_public_key_2 = PUBLIC_KEYS[1].get('raw')
    public_key_2 = PublicKey(raw=raw_public_key_2)
    current_users = Users.from_passwd()
    provided_users = Users()
    provided_users.append(
        User(name='testuserx1234',
             uid=59998,
             gid=1,
             gecos='test user gecos update',
             shell='/bin/false',
             public_keys=[public_key_2],
             sudoers_entry='ALL=(ALL:ALL) ALL'))
    plan = create_plan(existing_users=current_users,
                       proposed_users=provided_users,
                       protected_users=[
                           'travis', 'couchdb', 'ubuntu', 'nginx', 'hadfielj',
                           'vagrant', CURRENT_USER
                       ])
    assert plan[0]['proposed_user'].gecos == '\"test user gecos update\"'
    execute_plan(plan=plan)
    updated_users = Users.from_passwd()
    updated_user = updated_users.describe_users(users_filter=dict(
        name='testuserx1234'))
    assert len(updated_user) == 1
    assert updated_user[0].name == 'testuserx1234'
    assert updated_user[0].uid == 59998
    assert updated_user[0].gid == 1
    assert updated_user[0].gecos == '\"test user gecos update\"'
    assert updated_user[0].shell == '/bin/false'
    assert updated_user[0].public_keys[0].raw == text_type(
        PUBLIC_KEYS[1]['raw'])
    assert updated_user[0].sudoers_entry == 'ALL=(ALL:ALL) ALL'
    delete_test_user_and_group()
Esempio n. 25
0
def test_execute_plan_to_update_existing_user_with_multiple_keys():
    """ Create a new user with 2 keys and then replace with a new one """
    create_test_user()
    raw_public_key_1 = PUBLIC_KEYS[0].get('raw')
    public_key_1 = PublicKey(raw=raw_public_key_1)
    raw_public_key_2 = PUBLIC_KEYS[1].get('raw')
    public_key_2 = PublicKey(raw=raw_public_key_2)
    raw_public_key_3 = PUBLIC_KEYS[2].get('raw')
    public_key_3 = PublicKey(raw=raw_public_key_3)
    raw_public_key_4 = PUBLIC_KEYS[3].get('raw')
    public_key_4 = PublicKey(raw=raw_public_key_4)
    current_users = Users.from_passwd()
    provided_users_2 = Users()
    provided_users_2.append(
        User(name='testuserx1234',
             uid=59998,
             gid=1,
             gecos='test user gecos update',
             shell='/bin/false',
             public_keys=[public_key_1, public_key_2]))
    plan = create_plan(existing_users=current_users,
                       proposed_users=provided_users_2,
                       protected_users=[
                           'travis', 'couchdb', 'ubuntu', 'nginx', 'hadfielj',
                           'vagrant', CURRENT_USER
                       ])
    execute_plan(plan=plan)
    updated_users = Users.from_passwd()
    updated_user = updated_users.describe_users(users_filter=dict(
        name='testuserx1234'))
    assert updated_user[0].public_keys[0].raw == text_type(
        PUBLIC_KEYS[0]['raw'])
    assert updated_user[0].public_keys[1].raw == text_type(
        PUBLIC_KEYS[1]['raw'])
    # Replace both keys
    current_users = Users.from_passwd()
    provided_users_3 = Users()
    provided_users_3.append(
        User(name='testuserx1234',
             uid=59998,
             gid=1,
             gecos='test user gecos update',
             shell='/bin/false',
             public_keys=[public_key_3, public_key_4]))
    plan = create_plan(existing_users=current_users,
                       proposed_users=provided_users_3,
                       protected_users=[
                           'travis', 'couchdb', 'ubuntu', 'nginx', 'hadfielj',
                           'vagrant', CURRENT_USER
                       ])
    execute_plan(plan=plan)
    updated_users = Users.from_passwd()
    updated_user = updated_users.describe_users(users_filter=dict(
        name='testuserx1234'))
    assert updated_user[0].public_keys[0].raw == text_type(
        PUBLIC_KEYS[2]['raw'])
    assert updated_user[0].public_keys[1].raw == text_type(
        PUBLIC_KEYS[3]['raw'])
    # Replace one key
    current_users = Users.from_passwd()
    provided_users_4 = Users()
    provided_users_4.append(
        User(name='testuserx1234',
             uid=59998,
             gid=1,
             gecos='test user gecos update',
             shell='/bin/false',
             public_keys=[public_key_2, public_key_4]))
    plan = create_plan(existing_users=current_users,
                       proposed_users=provided_users_4,
                       protected_users=[
                           'travis', 'couchdb', 'ubuntu', 'nginx', 'hadfielj',
                           'vagrant', CURRENT_USER
                       ])
    execute_plan(plan=plan)
    updated_users = Users.from_passwd()
    updated_user = updated_users.describe_users(users_filter=dict(
        name='testuserx1234'))
    assert updated_user[0].public_keys[0].raw == text_type(
        PUBLIC_KEYS[1]['raw'])
    assert updated_user[0].public_keys[1].raw == text_type(
        PUBLIC_KEYS[3]['raw'])
    delete_test_user_and_group()