From 68dd872428091d6ed02b38fca97ab2af0394a092 Mon Sep 17 00:00:00 2001 From: Jeff McCune Date: Fri, 15 Sep 2017 13:33:58 -0400 Subject: [PATCH 1/4] Add ssh::config_entry defined type This patch adds a mechanism to manage ~/.ssh/config entries using the concat module. --- README.md | 30 ++++++++++++++++++++++++++++++ manifests/config_entry.pp | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 manifests/config_entry.pp diff --git a/README.md b/README.md index 605785c..3829376 100644 --- a/README.md +++ b/README.md @@ -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 @@ -852,3 +855,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', +} +``` diff --git a/manifests/config_entry.pp b/manifests/config_entry.pp new file mode 100644 index 0000000..3b368db --- /dev/null +++ b/manifests/config_entry.pp @@ -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", + } +} From a2b6ba47996e36ec9619d032bd6b390875875c86 Mon Sep 17 00:00:00 2001 From: Phil Friderici Date: Thu, 21 Sep 2017 08:55:30 +0000 Subject: [PATCH 2/4] Add concat as new dependency --- .fixtures.yml | 3 +++ metadata.json | 1 + 2 files changed, 4 insertions(+) diff --git a/.fixtures.yml b/.fixtures.yml index d0ec0d9..e8a1576 100644 --- a/.fixtures.yml +++ b/.fixtures.yml @@ -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' diff --git a/metadata.json b/metadata.json index e7cf559..0364a5b 100644 --- a/metadata.json +++ b/metadata.json @@ -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"} ] From 4dbbdf5685919ee6c53e2787f6c53f85cf0c3360 Mon Sep 17 00:00:00 2001 From: Phil Friderici Date: Thu, 21 Sep 2017 11:18:57 +0000 Subject: [PATCH 3/4] Add spec tests for ssh::config_entry define --- spec/defines/config_entry_spec.rb | 83 +++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 spec/defines/config_entry_spec.rb diff --git a/spec/defines/config_entry_spec.rb b/spec/defines/config_entry_spec.rb new file mode 100644 index 0000000..eee757e --- /dev/null +++ b/spec/defines/config_entry_spec.rb @@ -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 ' 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 ' 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 ' 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 ' 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 ' 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 [ , ]' 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 From 1cc9edea3a7401b458f22457af24b266ba2ee7b5 Mon Sep 17 00:00:00 2001 From: Phil Friderici Date: Thu, 21 Sep 2017 13:17:52 +0000 Subject: [PATCH 4/4] Add config_entries parameter Uses create_resources() to create ssh::config_entry resources for the given hash. Does respect hiera_merge parameter accordingly. --- README.md | 23 ++++- manifests/init.pp | 9 +- spec/classes/init_spec.rb | 84 +++++++++++++++++-- .../fqdn/hieramerge.example.com.yaml | 6 ++ .../hieradata/specific/test_hiera_merge.yaml | 6 ++ 5 files changed, 116 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3829376..0f6589f 100644 --- a/README.md +++ b/README.md @@ -57,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. @@ -619,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 diff --git a/manifests/init.pp b/manifests/init.pp index 5f6a972..eb1deeb 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -109,6 +109,7 @@ class ssh ( $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", @@ -802,18 +803,21 @@ class ssh ( $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) @@ -973,6 +977,9 @@ class ssh ( 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 { diff --git a/spec/classes/init_spec.rb b/spec/classes/init_spec.rb index 256d52c..43563f0 100644 --- a/spec/classes/init_spec.rb +++ b/spec/classes/init_spec.rb @@ -295,6 +295,8 @@ describe 'ssh' do 'purge' => 'true', }) } + + it { should have_ssh__config_entry_resource_count(0) } end end @@ -1345,6 +1347,71 @@ describe 'sshd_config_print_last_log param' do } 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 ' 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' => { @@ -2514,14 +2581,15 @@ describe 'sshd_config_print_last_log param' do 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'], @@ -2543,9 +2611,7 @@ describe 'sshd_config_print_last_log param' do 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 diff --git a/spec/fixtures/hiera/hieradata/fqdn/hieramerge.example.com.yaml b/spec/fixtures/hiera/hieradata/fqdn/hieramerge.example.com.yaml index e8d0fc4..7748213 100644 --- a/spec/fixtures/hiera/hieradata/fqdn/hieramerge.example.com.yaml +++ b/spec/fixtures/hiera/hieradata/fqdn/hieramerge.example.com.yaml @@ -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' diff --git a/spec/fixtures/hiera/hieradata/specific/test_hiera_merge.yaml b/spec/fixtures/hiera/hieradata/specific/test_hiera_merge.yaml index 7f7b51f..ea22373 100644 --- a/spec/fixtures/hiera/hieradata/specific/test_hiera_merge.yaml +++ b/spec/fixtures/hiera/hieradata/specific/test_hiera_merge.yaml @@ -7,3 +7,9 @@ ssh::sshd_config_denygroups: - denygroup_from_fact ssh::sshd_config_denyusers: - denyuser_from_fact +ssh::config_entries: + 'user_from_fact': + owner: 'fact_user' + group: 'fact_user' + path: '/home/fact_user/.ssh/config' + host: 'fact_host.example.local'