3 changes: 3 additions & 0 deletions .fixtures.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ fixtures:
stdlib:
repo: 'https://github.com/puppetlabs/puppetlabs-stdlib.git'
ref: '4.6.0'
concat:
repo: 'https://github.com/puppetlabs/puppetlabs-concat.git'
ref: '2.2.1'
common:
repo: 'https://github.com/ghoneycutt/puppet-module-common.git'
ref: 'v1.4.1'
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### v3.55.0 - 2017-09-26
* Add `ssh::config_entry` defined type to manage `~/.ssh/config`
* Add `config_entries` parameter to ssh class to allow specifying a
hash of multiple entries for `ssh::config_entry`.

### v3.54.0 - 2017-07-24
* Allow sshd_config_hostcertificate to be an array. This fixes a bug
where you could have specified one cert and multiple HostKey's since
Expand Down
53 changes: 51 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ ssh_key_ensure and purge_keys.

This module may be used with a simple `include ::ssh`

The `ssh::config_entry` defined type may be used directly and is used to manage
Host entries in a personal `~/.ssh/config` file.

===

### Table of Contents
Expand Down Expand Up @@ -54,8 +57,9 @@ A value of `'USE_DEFAULTS'` will use the defaults specified by the module.

hiera_merge
-----------
Boolean to merges all found instances of ssh::keys in Hiera. This is useful for specifying
SSH keys at different levels of the hierarchy and having them all included in the catalog.
Boolean to merges all found instances of ssh::keys and ssh::config_entries in Hiera.
This is useful for specifying SSH keys at different levels of the hierarchy and having
them all included in the catalog.

This will default to 'true' in future versions.

Expand Down Expand Up @@ -616,6 +620,24 @@ See `sshd_config(5)` for more details

- *Default*: undefined

config_entries
--------------
Hash of config entries for a specific user's ~/.ssh/config. Please check the docs for ssd::config_entry for a list and details of the parameters usable here.
Setting hiera_merge to true will activate merging entries through all levels of hiera.

- *Hiera example*:

``` yaml
ssh::config_entries:
'root':
owner: 'root'
group: 'root'
path: '/root/.ssh/config'
host: 'host.example.local'
```

- *Default*: {}

keys
----
Hash of keys for user's ~/.ssh/authorized_keys
Expand Down Expand Up @@ -852,3 +874,30 @@ ssh::keys:
ensure: absent
user: root
```

Manage config entries in a personal ssh/config file.

```
Ssh::Config_entry {
ensure => present,
path => '/home/jenkins/.ssh/config',
owner => 'jenkins',
group => 'jenkins',
}


ssh::config_entry { 'jenkins *':
host => '*',
lines => [
' ForwardX11 no',
' StrictHostKeyChecking no',
],
order => '10',
}

ssh::config_entry { 'jenkins github.com':
host => 'github.com',
lines => [" IdentityFile /home/jenkins/.ssh/jenkins-gihub.key"],
order => '20',
}
```
36 changes: 36 additions & 0 deletions manifests/config_entry.pp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# == Define: ssh::config_entry
#
# Manage an entry in ~/.ssh/config for a particular user. Lines model the lines
# in each Host block.
define ssh::config_entry (
$owner,
$group,
$path,
$host,
$order = '10',
$ensure = 'present',
$lines = [],
) {

# All lines including the host line. This will be joined with "\n " for
# indentation.
$entry = concat(["Host ${host}"], $lines)
$content = join($entry, "\n")

if ! defined(Concat[$path]) {
concat { $path:
ensure => present,
owner => $owner,
group => $group,
mode => '0644',
ensure_newline => true,
}
}

concat::fragment { "${path} Host ${host}":
target => $path,
content => $content,
order => $order,
tag => "${owner}_ssh_config",
}
}
9 changes: 8 additions & 1 deletion manifests/init.pp
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
$ssh_config_global_known_hosts_group = 'root',
$ssh_config_global_known_hosts_mode = '0644',
$ssh_config_user_known_hosts_file = undef,
$config_entries = {},
$keys = undef,
$manage_root_ssh_config = false,
$root_ssh_config_content = "# This file is being maintained by Puppet.\n# DO NOT EDIT\n",
Expand Down Expand Up @@ -802,18 +803,21 @@
$supported_loglevel_vals=['QUIET', 'FATAL', 'ERROR', 'INFO', 'VERBOSE']
validate_re($sshd_config_loglevel, $supported_loglevel_vals)

#enable hiera merging for groups and users
#enable hiera merging for groups, users, and config_entries
if $hiera_merge_real == true {
$sshd_config_allowgroups_real = hiera_array('ssh::sshd_config_allowgroups',[])
$sshd_config_allowusers_real = hiera_array('ssh::sshd_config_allowusers',[])
$sshd_config_denygroups_real = hiera_array('ssh::sshd_config_denygroups',[])
$sshd_config_denyusers_real = hiera_array('ssh::sshd_config_denyusers',[])
$config_entries_real = hiera_hash('ssh::config_entries',{})
} else {
$sshd_config_allowgroups_real = $sshd_config_allowgroups
$sshd_config_allowusers_real = $sshd_config_allowusers
$sshd_config_denygroups_real = $sshd_config_denygroups
$sshd_config_denyusers_real = $sshd_config_denyusers
$config_entries_real = $config_entries
}
validate_hash($config_entries_real)

if $sshd_config_denyusers_real != [] {
validate_array($sshd_config_denyusers_real)
Expand Down Expand Up @@ -973,6 +977,9 @@
purge => $purge_keys_real,
}

# manage users' ssh config entries if present
create_resources('ssh::config_entry',$config_entries_real)

# manage users' ssh authorized keys if present
if $keys != undef {
if $hiera_merge_real == true {
Expand Down
3 changes: 2 additions & 1 deletion metadata.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ghoneycutt-ssh",
"version": "3.54.0",
"version": "3.55.0",
"author": "ghoneycutt",
"summary": "Manages SSH",
"license": "Apache-2.0",
Expand Down Expand Up @@ -88,6 +88,7 @@
"description": "Manage SSH",
"dependencies": [
{"name":"puppetlabs/stdlib","version_requirement":">= 4.6.0 < 6.0.0"},
{"name":"puppetlabs/concat","version_requirement":">= 2.0.0 < 3.0.0"},
{"name":"ghoneycutt/common","version_requirement":">= 1.4.1 < 2.0.0"},
{"name":"puppetlabs/firewall","version_requirement":">= 1.9.0 < 2.0.0"}
]
Expand Down
84 changes: 75 additions & 9 deletions spec/classes/init_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@
'purge' => 'true',
})
}

it { should have_ssh__config_entry_resource_count(0) }
end
end

Expand Down Expand Up @@ -1345,6 +1347,71 @@
}
end

context 'with config_entries defined on valid osfamily' do
let(:params) do
{
:config_entries => {
'root' => {
'owner' => 'root',
'group' => 'root',
'path' => '/root/.ssh/config',
'host' => 'test_host1',
},
'user' => {
'owner' => 'user',
'group' => 'group',
'path' => '/home/user/.ssh/config',
'host' => 'test_host2',
'order' => '242',
'lines' => [ 'ForwardX11 no', 'StrictHostKeyChecking no' ],
},
}
}
end

it { should compile.with_all_deps }
it { should have_ssh__config_entry_resource_count(2) }
it do
should contain_ssh__config_entry('root').with({
'owner' => 'root',
'group' => 'root',
'path' => '/root/.ssh/config',
'host' => 'test_host1',
})
end
it do
should contain_ssh__config_entry('user').with({
'owner' => 'user',
'group' => 'group',
'path' => '/home/user/.ssh/config',
'host' => 'test_host2',
'order' => '242',
'lines' => [ 'ForwardX11 no', 'StrictHostKeyChecking no' ],
})
end
end

describe 'with hiera providing data from multiple levels' do
let(:facts) do
default_facts.merge({
:fqdn => 'hieramerge.example.com',
:specific => 'test_hiera_merge',
})
end

context 'with defaults for all parameters' do
it { should have_ssh__config_entry_resource_count(1) }
it { should contain_ssh__config_entry('user_from_fqdn') }
end

context 'with hiera_merge set to valid <true>' do
let(:params) { { :hiera_merge => true } }
it { should have_ssh__config_entry_resource_count(2) }
it { should contain_ssh__config_entry('user_from_fqdn') }
it { should contain_ssh__config_entry('user_from_fact') }
end
end

context 'with keys defined on valid osfamily' do
let(:params) { { :keys => {
'root_for_userX' => {
Expand Down Expand Up @@ -2514,14 +2581,15 @@
end

describe 'variable type and content validations' do
# set needed custom facts and variables
let(:mandatory_params) do
{
#:param => 'value',
}
end
mandatory_params = {} if mandatory_params.nil?

validations = {
'hash' => {
:name => %w[config_entries],
:valid => [], # valid hashes are to complex to block test them here. types::mount should have its own spec tests anyway.
:invalid => ['string', %w[array], 3, 2.42, true],
:message => 'is not a Hash',
},
'regex (yes|no|unset)' => {
:name => %w(ssh_config_use_roaming),
:valid => ['yes', 'no', 'unset'],
Expand All @@ -2543,9 +2611,7 @@
var[:invalid].each do |invalid|
context "when #{var_name} (#{type}) is set to invalid #{invalid} (as #{invalid.class})" do
let(:params) { [mandatory_params, var[:params], { :"#{var_name}" => invalid, }].reduce(:merge) }
it 'should fail' do
expect { should contain_class(subject) }.to raise_error(Puppet::Error, /#{var[:message]}/)
end
it { is_expected.to compile.and_raise_error(/#{var[:message]}/) }
end
end
end # var[:name].each
Expand Down
83 changes: 83 additions & 0 deletions spec/defines/config_entry_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
require 'spec_helper'
describe 'ssh::config_entry' do
mandatory_params = {
:owner => 'test_owner',
:group => 'test_group',
:path => '/test/path',
:host => 'test_host',
}

let(:title) { 'example' }
let(:params) { mandatory_params }

context 'with no paramater is provided' do
let(:params) { {} }
it 'should fail' do
expect do
should contain_define(subject)
end.to raise_error(Puppet::Error, /(Must pass|expects a value for parameter)/) # Puppet4/5
end
end

context 'with mandatory params set' do
let(:params) { mandatory_params }
it { should compile.with_all_deps }

it do
should contain_concat('/test/path').with({
'ensure' => 'present',
'owner' => 'test_owner',
'group' => 'test_group',
'mode' => '0644',
'ensure_newline' => true,
})
end

it do
should contain_concat__fragment('/test/path Host test_host').with({
'target' => '/test/path',
'content' => 'Host test_host',
'order' => '10',
'tag' => 'test_owner_ssh_config',
})
end
end

context 'with owner set to valid string <other_owner>' do
let(:params) { mandatory_params.merge({ :owner => 'other_owner' }) }
it { should contain_concat('/test/path').with_owner('other_owner') }
it { should contain_concat__fragment('/test/path Host test_host').with_tag('other_owner_ssh_config') }
end

context 'with group set to valid string <other_group>' do
let(:params) { mandatory_params.merge({ :group => 'other_group' }) }
it { should contain_concat('/test/path').with_group('other_group') }
end

context 'with path set to valid string </other/path>' do
let(:params) { mandatory_params.merge({ :path => '/other/path' }) }
it { should contain_concat('/other/path') }
it { should contain_concat__fragment('/other/path Host test_host') }
end

context 'with host set to valid string <other_host>' do
let(:params) { mandatory_params.merge({ :host => 'other_host' }) }
it { should contain_concat__fragment('/test/path Host other_host').with_content('Host other_host') }
end

context 'with order set to valid string <242>' do
let(:params) { mandatory_params.merge({ :order => '242' }) }
it { should contain_concat__fragment('/test/path Host test_host').with_order('242') }
end

# /!\ no functionality for $ensure implemented yet
# context 'with ensure set to valid string <absent>' do
# let(:params) { mandatory_params.merge({ :ensure => 'absent' }) }
# it { should contain_concat('/test/path').with_ensure('absent') }
# end

context 'with lines set to valid array [ <ForwardX11 no>, <StrictHostKeyChecking no> ]' do
let(:params) { mandatory_params.merge({ :lines => ['ForwardX11 no', 'StrictHostKeyChecking no'] }) }
it { should contain_concat__fragment('/test/path Host test_host').with_content("Host test_host\nForwardX11 no\nStrictHostKeyChecking no") }
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ ssh::sshd_config_denygroups:
- denygroup_from_fqdn
ssh::sshd_config_denyusers:
- denyuser_from_fqdn
ssh::config_entries:
'user_from_fqdn':
owner: 'fqdn_user'
group: 'fqdn_user'
path: '/home/fqdn_user/.ssh/config'
host: 'fqdn_host.example.local'
Loading