Skip to content

Commit

Permalink
Merge pull request #54981 from mchugh19/port-50141
Browse files Browse the repository at this point in the history
master-port 50141
  • Loading branch information
dwoz authored Dec 11, 2019
2 parents 4ca509c + cb7adba commit 6892e85
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 17 deletions.
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)

0 comments on commit 6892e85

Please sign in to comment.