Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

master-port 50141 #54981

Merged
merged 8 commits into from
Dec 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/topics/releases/neon.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ The slot syntax has been updated to support parsing dictionary responses and to
Duration: 1.229 ms
Changes:


State Changes
=============

- Added new :py:func:`ssh_auth.manage <salt.states.ssh_auth.manage>` state to
ensure only the specified ssh keys are present for the specified user.


Deprecations
============

Expand Down
131 changes: 114 additions & 17 deletions salt/states/ssh_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@
- ssh-dss AAAAB3NzaCL0sQ9fJ5bYTEyY== user@domain
- option3="value3" ssh-dss AAAAB3NzaC1kcQ9J5bYTEyY== other@testdomain
- AAAAB3NzaC1kcQ9fJFF435bYTEyY== newcomment

sshkeys:
ssh_auth.manage:
- user: root
- enc: ssh-rsa
- options:
- option1="value1"
- option2="value2 flag2"
- comment: myuser
- ssh_keys:
- AAAAB3NzaC1kc3MAAACBAL0sQ9fJ5bYTEyY==
- ssh-dss AAAAB3NzaCL0sQ9fJ5bYTEyY== user@domain
- option3="value3" ssh-dss AAAAB3NzaC1kcQ9J5bYTEyY== other@testdomain
- AAAAB3NzaC1kcQ9fJFF435bYTEyY== newcomment
'''

# Import python libs
Expand Down Expand Up @@ -125,7 +139,7 @@ def _present_test(user, name, enc, comment, options, source, config, fingerprint
elif check == 'exists':
result = True
comment = ('The authorized host key {0} is already present '
'for user {1}'.format(name, user))
'for user {1}'.format(name, user))

return result, comment

Expand Down Expand Up @@ -251,14 +265,7 @@ def present(

fingerprint_hash_type
The public key fingerprint hash type that the public key fingerprint
was originally hashed with. This defaults to ``md5`` if not specified.

.. versionadded:: 2016.11.7

.. note::

The default value of the ``fingerprint_hash_type`` will change to
``sha256`` in Salt 2017.7.0.
was originally hashed with. This defaults to ``sha256`` if not specified.
'''
ret = {'name': name,
'changes': {},
Expand Down Expand Up @@ -325,7 +332,7 @@ def present(
saltenv=__env__,
fingerprint_hash_type=fingerprint_hash_type)
else:
# Split keyline to get key und comment
# Split keyline to get key and comment
keyline = keyline.split(' ')
key_type = keyline[0]
key_value = keyline[1]
Expand Down Expand Up @@ -423,15 +430,9 @@ def absent(name,

fingerprint_hash_type
The public key fingerprint hash type that the public key fingerprint
was originally hashed with. This defaults to ``md5`` if not specified.
was originally hashed with. This defaults to ``sha256`` if not specified.

.. versionadded:: 2016.11.7

.. note::

The default value of the ``fingerprint_hash_type`` will change to
``sha256`` in Salt 2017.7.0.

'''
ret = {'name': name,
'changes': {},
Expand Down Expand Up @@ -506,3 +507,99 @@ def absent(name,
ret['changes'][name] = 'Removed'

return ret


def manage(
name,
ssh_keys,
user,
enc='ssh-rsa',
comment='',
source='',
options=None,
config='.ssh/authorized_keys',
fingerprint_hash_type=None,
**kwargs):
'''
.. versionadded:: Neon

Ensures that only the specified ssh_keys are present for the specified user

ssh_keys
The SSH key to manage

user
The user who owns the SSH authorized keys file to modify

enc
Defines what type of key is being used; can be ed25519, ecdsa, ssh-rsa
or ssh-dss

comment
The comment to be placed with the SSH public key

source
The source file for the key(s). Can contain any number of public keys,
in standard "authorized_keys" format. If this is set, comment and enc
will be ignored.

.. note::
The source file must contain keys in the format ``<enc> <key>
<comment>``. If you have generated a keypair using PuTTYgen, then you
will need to do the following to retrieve an OpenSSH-compatible public
key.

1. In PuTTYgen, click ``Load``, and select the *private* key file (not
the public key), and click ``Open``.
2. Copy the public key from the box labeled ``Public key for pasting
into OpenSSH authorized_keys file``.
3. Paste it into a new file.

options
The options passed to the keys, pass a list object

config
The location of the authorized keys file relative to the user's home
directory, defaults to ".ssh/authorized_keys". Token expansion %u and
%h for username and home path supported.

fingerprint_hash_type
The public key fingerprint hash type that the public key fingerprint
was originally hashed with. This defaults to ``sha256`` if not specified.
'''
ret = {'name': '',
'changes': {},
'result': True,
'comment': ''}

all_potential_keys = []
for ssh_key in ssh_keys:
# gather list potential ssh keys for removal comparison
# options, enc, and comments could be in the mix
all_potential_keys.extend(ssh_key.split(' '))
existing_keys = __salt__['ssh.auth_keys'](user=user).keys()
remove_keys = set(existing_keys).difference(all_potential_keys)
for remove_key in remove_keys:
if __opts__['test']:
remove_comment = '{0} Key set for removal'.format(remove_key)
ret['comment'] = remove_comment
ret['result'] = None
else:
remove_comment = absent(remove_key, user)['comment']
ret['changes'][remove_key] = remove_comment

for ssh_key in ssh_keys:
run_return = present(ssh_key, user, enc, comment, source,
options, config, fingerprint_hash_type, **kwargs)
if run_return['changes']:
ret['changes'].update(run_return['changes'])
else:
ret['comment'] += '\n' + run_return['comment']
ret['comment'] = ret['comment'].strip()

if run_return['result'] is None:
ret['result'] = None
elif not run_return['result']:
ret['result'] = False

return ret
75 changes: 75 additions & 0 deletions tests/unit/states/test_ssh_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,78 @@ def test_absent(self):
ret.update({'comment': comt, 'result': True,
'changes': {name: 'Removed'}})
self.assertDictEqual(ssh_auth.absent(name, user, source), ret)

def test_manage(self):
'''
Test to verifies that the specified SSH key is absent.
'''
user = 'root'
ret = {'name': '',
'changes': {},
'result': None,
'comment': ''}

mock_rm = MagicMock(side_effect=['User authorized keys file not present',
'Key removed'])
mock_up = MagicMock(side_effect=['update', 'updated'])
mock_set = MagicMock(side_effect=['replace', 'new'])
mock_keys = MagicMock(return_value={'somekey': {
"enc": "ssh-rsa",
"comment": "user@host",
"options": [],
"fingerprint": "b7"}})
with patch.dict(ssh_auth.__salt__, {'ssh.rm_auth_key': mock_rm,
'ssh.set_auth_key': mock_set,
'ssh.check_key': mock_up,
'ssh.auth_keys': mock_keys}):
with patch('salt.states.ssh_auth.present') as call_mocked_present:
mock_present = {'comment': '',
'changes': {},
'result': None
}
call_mocked_present.return_value = mock_present
with patch.dict(ssh_auth.__opts__, {'test': True}):
# test: expected keys found. No chanages
self.assertDictEqual(ssh_auth.manage('sshid', ['somekey'], user), ret)

comt = ('somekey Key set for removal')
ret.update({'comment': comt})
# test: unexpected sshkey found. Should be removed.
self.assertDictEqual(ssh_auth.manage('sshid', [], user), ret)

with patch('salt.states.ssh_auth.present') as call_mocked_present:
mock_present = {'comment': '',
'changes': {},
'result': True
}
call_mocked_present.return_value = mock_present
with patch.dict(ssh_auth.__opts__, {'test': False}):
# expected keys found. No changes
ret = {'name': '',
'changes': {},
'result': True,
'comment': ''}
self.assertDictEqual(ssh_auth.manage('sshid', ['somekey'], user), ret)

with patch('salt.states.ssh_auth.absent') as call_mocked_absent:
mock_absent = {'comment': 'Key removed'}
call_mocked_absent.return_value = mock_absent
ret.update({'comment': '', 'result': True,
'changes': {'somekey': 'Key removed'}})
# unexpected sshkey found. Was removed.
self.assertDictEqual(ssh_auth.manage('sshid', ['addkey'], user), ret)

# add a key
with patch('salt.states.ssh_auth.present') as call_mocked_present:
mock_present = {'comment': 'The authorized host key newkey for user {} was added'.format(user),
'changes': {'newkey': 'New'},
'result': True
}
call_mocked_present.return_value = mock_present
with patch.dict(ssh_auth.__opts__, {'test': False}):
# added key newkey
ret = {'name': '',
'changes': {'newkey': 'New'},
'result': True,
'comment': ''}
self.assertDictEqual(ssh_auth.manage('sshid', ['newkey', 'somekey'], user), ret)