From 3907cf90f1b118b85fdc6475b5118879f12e08b8 Mon Sep 17 00:00:00 2001 From: Greg Swift Date: Sun, 14 Jul 2013 22:26:06 -0500 Subject: [PATCH] Add of pam type/provider with doc and unit tests --- README.md | 1 + docs/examples/pam.md | 65 ++++++ lib/puppet/provider/pam/augeas.rb | 129 ++++++++++++ lib/puppet/type/pam.rb | 99 +++++++++ .../unit/puppet/provider/pam/augeas/broken | 27 +++ .../unit/puppet/provider/pam/augeas/empty | 0 .../unit/puppet/provider/pam/augeas/full | 27 +++ spec/unit/puppet/provider/pam/augeas_spec.rb | 190 ++++++++++++++++++ 8 files changed, 538 insertions(+) create mode 100644 docs/examples/pam.md create mode 100644 lib/puppet/provider/pam/augeas.rb create mode 100644 lib/puppet/type/pam.rb create mode 100644 spec/fixtures/unit/puppet/provider/pam/augeas/broken create mode 100644 spec/fixtures/unit/puppet/provider/pam/augeas/empty create mode 100644 spec/fixtures/unit/puppet/provider/pam/augeas/full create mode 100755 spec/unit/puppet/provider/pam/augeas_spec.rb diff --git a/README.md b/README.md index 96c0416d..6d790b73 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ The module adds the following new types: * `apache_setenv` for updating SetEnv entries in Apache HTTP Server configs * `kernel_parameter` for adding kernel parameters to GRUB Legacy or GRUB 2 configs * `nrpe_command` for setting command entries in Nagios NRPE's `nrpe.cfg` +* `pam` for files inside /etc/pam.d/ * `pg_hba` for PostgreSQL's `pg_hba.conf` entries * `puppet_auth` for authentication rules in Puppet's `auth.conf` * `shellvar` for shell variables in `/etc/sysconfig` or `/etc/default` etc. diff --git a/docs/examples/pam.md b/docs/examples/pam.md new file mode 100644 index 00000000..4260600a --- /dev/null +++ b/docs/examples/pam.md @@ -0,0 +1,65 @@ +## pam provider + +This is a custom type and provider supplied by `augeasproviders`. + +### manage simple entry + + pam { "Set sss entry to system-auth auth": + ensure => present, + service => 'system-auth', + type => 'auth', + control => 'sufficient', + module => 'pam_sss.so', + arguments => 'use_first_pass', + position => 'before module pam_deny.so', + } + +### manage same entry but with Augeas xpath + + pam { "Set sss entry to system-auth auth": + ensure => present, + service => 'system-auth', + type => 'auth', + control => 'sufficient', + module => 'pam_sss.so', + arguments => 'use_first_pass', + position => 'before *[type="auth" and module="pam_deny.so"]', + } + +### delete entry + + pam { "Remove sss auth entry from system-auth": + ensure => absent, + service => 'system-auth', + type => 'auth', + module => 'pam_sss.so', + } + +### delete all references to module in file + + pam { "Remove all pam_sss.so from system-auth": + ensure => absent, + service => 'system-auth', + module => 'pam_sss.so', + } + +### manage entry in another pam service + + pam { "Set cracklib limits in password-auth": + ensure => present, + service => 'password-auth', + type => 'password', + module => 'pam_cracklib.so', + arguments => ['try_first_pass','retry=3', 'minlen=10'], + } + +### manage entry like previous but in classic pam.conf + + pam { "Set cracklib limits in password-auth": + ensure => present, + service => 'password-auth', + type => 'password', + module => 'pam_cracklib.so', + arguments => ['try_first_pass','retry=3', 'minlen=10'], + target => '/etc/pam.conf', + } diff --git a/lib/puppet/provider/pam/augeas.rb b/lib/puppet/provider/pam/augeas.rb new file mode 100644 index 00000000..0b09dff7 --- /dev/null +++ b/lib/puppet/provider/pam/augeas.rb @@ -0,0 +1,129 @@ +# Alternative Augeas-based providers for Puppet +# +# Copyright (c) 2012 Greg Swift +# Licensed under the Apache License, Version 2.0 + +require File.dirname(__FILE__) + '/../../../augeasproviders/provider' + +Puppet::Type.type(:pam).provide(:augeas) do + desc "Uses Augeas API to update an pam parameter" + + include AugeasProviders::Provider + + # Boolean is the key because they either do or do not provide a + # value for control to work against. Module doesn't work against + # control + PAM_POSITION_ALIASES = { + true => { 'first' => "*[type='%s' and control='%s'][1]", + 'last' => "*[type='%s' and control='%s'][last()]", + 'module' => "*[type='%s' and module='%s'][1]", }, + false => { 'first' => "*[type='%s'][1]", + 'last' => "*[type='%s'][last()]", }, + } + + confine :feature => :augeas + + default_file { '/etc/pam.d/system-auth' } + + def target(resource = nil) + if resource and resource[:service] + "/etc/pam.d/#{resource[:service]}".chomp('/') + else + super + end + end + + lens do |resource| + target(resource) == '/etc/pam.conf' ? 'pamconf.lns' : 'pam.lns' + end + + resource_path do |resource| + service = resource[:service] + type = resource[:type] + mod = resource[:module] + if target == '/etc/pam.conf' + "$target/*[service='#{service}' and type='#{type}' and module='#{mod}']" + else + "$target/*[type='#{type}' and module='#{mod}']" + end + end + + def self.instances + augopen do |aug| + resources = [] + aug.match("$target/*[label()!='#comment']").each do |spath| + optional = aug.match("#{spath}/optional").empty?.to_s.to_sym + type = aug.get("#{spath}/type") + control = aug.get("#{spath}/control") + mod = aug.get("#{spath}/module") + arguments = aug.match("#{spath}/argument").map { |p| aug.get(p) } + entry = {:ensure => :present, + :optional => optional, + :type => type, + :control => control, + :module => mod, + :arguments => arguments} + if target == '/etc/pam.conf' + entry[:service] = aug.get("#{spath}/service") + end + resources << new(entry) + end + resources + end + end + + define_aug_method!(:create) do |aug, resource| + path = '01' + entry_path = "$target/#{path}" + # we pull type, control, and position out because we actually + # work with those values, not just reference them in the set section + # type comes to us as a symbol, so needs to be converted to a string + type = resource[:type].to_s + control = resource[:control] + position = resource[:position] + placement, identifier, value = position.split(/ /) + key = !!value + if PAM_POSITION_ALIASES[key].has_key?(identifier) + expr = PAM_POSITION_ALIASES[key][identifier] + expr = key ? expr % [type, value] : expr % [type] + else + # if the identifier is not in the mapping + # we assume that its an xpath and so + # join everything after the placement + identifier = position.split(/ /)[1..-1].join(" ") + expr = identifier + end + aug.insert("$target/#{expr}", path, placement == 'before') + if resource[:optional] == :true + aug.touch("#{entry_path}/optional") + end + if target == '/etc/pam.conf' + aug.set("#{entry_path}/service", resource[:service]) + end + aug.set("#{entry_path}/type", type) + aug.set("#{entry_path}/control", control) + aug.set("#{entry_path}/module", resource[:module]) + resource[:arguments].each do |argument| + aug.set("#{entry_path}/argument[last()+1]", argument) + end + end + + define_aug_method(:optional) do |aug, resource| + aug.match("$resource/optional").empty?.to_s.to_sym + end + + define_aug_method!(:optional=) do |aug, resource, value| + if resource[:optional] == :true + if aug.match("$resource/optional").empty? + aug.clear("$resource/optional") + end + else + aug.rm("$resource/optional") + end + end + + attr_aug_accessor(:control) + + attr_aug_accessor(:arguments, :type => :array, :label => 'argument') + +end diff --git a/lib/puppet/type/pam.rb b/lib/puppet/type/pam.rb new file mode 100644 index 00000000..9c3028fd --- /dev/null +++ b/lib/puppet/type/pam.rb @@ -0,0 +1,99 @@ +# Manages settings in PAM service files +# +# Copyright (c) 2012 Greg Swift +# Licensed under the Apache License, Version 2.0 + +require File.dirname(__FILE__) + '/../../augeasproviders/type' + +Puppet::Type.newtype(:pam) do + @doc = "Manages settings in an PAM service files. + +The resource name is a descriptive string only due to the non-uniqueness of any single paramter." + + extend AugeasProviders::Type + + positionable + + def munge_boolean(value) + case value + when true, "true", :true + :true + when false, "false", :false + :false + else + fail("munge_boolean only takes booleans") + end + end + + newparam(:name) do + desc "The name of the resource, has no bearing on anything" + isnamevar + end + + newparam(:service) do + desc "The PAM service this entry will be placed in. Typically this is the same as the +filename under /etc/pam.d" + end + + newparam(:type) do + desc "The PAM service type of the setting: account, auth, password, session." + newvalues(:account, :auth, :password, :session) + end + + newparam(:module) do + desc "The name of the specific PAM module to load." + end + + newproperty(:optional, :boolean => true) do + desc "Whether failure to load the module will break things" + + newvalue(:true) + newvalue(:false) + + munge do |value| + @resource.munge_boolean(value) + end + end + + newproperty(:arguments, :array_matching => :all) do + desc "Arguments to assign for the module." + end + + newproperty(:control) do + desc "Simple or complex definition of the module's behavior on failure." + end + + newparam(:position) do + desc "A three part text field that providers the placement position of an entry. + +The field consists of `placement identifier value` + +Placement can be either `before` or `after` +Identifier can be either `first`, `last`, `module`, or an Augeas xpath +Value is matched as follows: + With `first` and `last` match `value` to the `control` field, can be blank for absolute positioning. + With `module` matches the `module` field of the associated line, can not be blank. + With an Augeas xpath this field will be ignored, and should be blank. +" + defaultto('before last') + validate do |value| + placement, identifier, val = value.split(/ /) + unless ['before', 'after'].include? placement + raise ArgumentError, "%s is not a valid placement in position" % placement + end +# Don't do validation of the second field because we are supporting xpath +# and thats hard to validate +# unless ['first', 'last', 'module'].include? identifier or identifier =~ // +# raise ArgumentError, "%s is not a valid identifier in position" % indentifier +# end + if val.nil? and identifier == 'module' + raise ArgumentError, "Value must be set if you are matching on module" + end + end + end + + newparam(:target) do + desc "The file in which to store the settings, defaults to `/etc/pam.d/{service}`." + end + +end diff --git a/spec/fixtures/unit/puppet/provider/pam/augeas/broken b/spec/fixtures/unit/puppet/provider/pam/augeas/broken new file mode 100644 index 00000000..ddb41f8f --- /dev/null +++ b/spec/fixtures/unit/puppet/provider/pam/augeas/broken @@ -0,0 +1,27 @@ +#%PAM-1.0 +# This file is auto-generated. +# User changes will be destroyed the next time authconfig is run. +auth +auth sufficient pam_unix.so nullok try_first_pass +auth requisite pam_succeed_if.so uid >= 1000 quiet_success +auth sufficient pam_sss.so use_first_pass +auth required pam_deny.so + +account required pam_unix.so broken_shadow +account sufficient pam_localuser.so +account sufficient pam_succeed_if.so uid < 1000 quiet +account [default=bad success=ok user_unknown=ignore] pam_sss.so +account required pam_permit.so + +password requisite pam_pwquality.so try_first_pass retry=3 type= +password sufficient pam_unix.so sha512 shadow nullok try_first_pass use_authtok +password sufficient pam_sss.so use_authtok +password required pam_deny.so + +session optional pam_keyinit.so revoke +session required pam_limits.so +-session optional pam_systemd.so +session optional pam_mkhomedir.so +session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid +session required pam_unix.so +session optional pam_sss.so diff --git a/spec/fixtures/unit/puppet/provider/pam/augeas/empty b/spec/fixtures/unit/puppet/provider/pam/augeas/empty new file mode 100644 index 00000000..e69de29b diff --git a/spec/fixtures/unit/puppet/provider/pam/augeas/full b/spec/fixtures/unit/puppet/provider/pam/augeas/full new file mode 100644 index 00000000..1caaa5dd --- /dev/null +++ b/spec/fixtures/unit/puppet/provider/pam/augeas/full @@ -0,0 +1,27 @@ +#%PAM-1.0 +# This file is auto-generated. +# User changes will be destroyed the next time authconfig is run. +auth required pam_env.so +auth sufficient pam_unix.so nullok try_first_pass +auth requisite pam_succeed_if.so uid >= 1000 quiet_success +auth sufficient pam_sss.so use_first_pass +auth required pam_deny.so + +account required pam_unix.so broken_shadow +account sufficient pam_localuser.so +account sufficient pam_succeed_if.so uid < 1000 quiet +account [default=bad success=ok user_unknown=ignore] pam_sss.so +account required pam_permit.so + +password requisite pam_pwquality.so try_first_pass retry=3 type= +password sufficient pam_unix.so sha512 shadow nullok try_first_pass use_authtok +password sufficient pam_sss.so use_authtok +password required pam_deny.so + +session optional pam_keyinit.so revoke +session required pam_limits.so +-session optional pam_systemd.so +session optional pam_mkhomedir.so +session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid +session required pam_unix.so +session optional pam_sss.so diff --git a/spec/unit/puppet/provider/pam/augeas_spec.rb b/spec/unit/puppet/provider/pam/augeas_spec.rb new file mode 100755 index 00000000..1221b36c --- /dev/null +++ b/spec/unit/puppet/provider/pam/augeas_spec.rb @@ -0,0 +1,190 @@ +#!/usr/bin/env rspec + +require 'spec_helper' + +provider_class = Puppet::Type.type(:pam).provider(:augeas) + +describe provider_class do + before :each do + FileTest.stubs(:exist?).returns false + end + + context "with empty file" do + let(:tmptarget) { aug_fixture("empty") } + let(:target) { tmptarget.path } + + it "should create simple new entry" do + apply!(Puppet::Type.type(:pam).new( + :title => "Add pam_test.so to auth for system-auth", + :service => "system-auth", + :type => "auth", + :control => "sufficient", + :module => "pam_test.so", + :arguments => "test_me_out", + :position => "before module pam_deny.so", + :target => target, + :provider => "augeas", + :ensure => "present" + )) + + aug_open(target, "Pam.lns") do |aug| + aug.get("./1/module").should == "pam_test.so" + aug.get("./1/argument[1]").should == "test_me_out" + end + end + end + + context "with full file" do + let(:tmptarget) { aug_fixture("full") } + let(:target) { tmptarget.path } + + it "should list instances" do + provider_class.stubs(:target).returns(target) + inst = provider_class.instances.map { |p| + { + :ensure => p.get(:ensure), + :service => p.get(:service), + :type => p.get(:type), + :control => p.get(:control), + :module => p.get(:module), + :arguments => p.get(:arguments), + } + } + + inst.size.should == 21 + inst[0].should == {:ensure => :present, + :service => :absent, + :type => "auth", + :control => "required", + :module => "pam_env.so", + :arguments => [],} + inst[1].should == {:ensure => :present, + :service => :absent, + :type => "auth", + :control => "sufficient", + :module => "pam_unix.so", + :arguments => ["nullok","try_first_pass"],} + inst[5].should == {:ensure => :present, + :service => :absent, + :type => "account", + :control => "required", + :module => "pam_unix.so", + :arguments => ["broken_shadow"],} + inst[8].should == {:ensure => :present, + :service => :absent, + :type => "account", + :control => "[default=bad success=ok user_unknown=ignore]", + :module => "pam_sss.so", + :arguments => [],} + inst[10].should == {:ensure => :present, + :service => :absent, + :type => "password", + :control => "requisite", + :module => "pam_pwquality.so", + :arguments => ["try_first_pass","retry=3","type="],} + end + + describe "when creating settings" do + it "should create simple new entry" do + apply!(Puppet::Type.type(:pam).new( + :title => "Add pam_test.so to auth for system-auth", + :service => "system-auth", + :type => "auth", + :control => "sufficient", + :module => "pam_test.so", + :arguments => "test_me_out", + :position => "before module pam_deny.so", + :target => target, + :provider => "augeas", + :ensure => "present" + )) + + aug_open(target, "Pam.lns") do |aug| + aug.get("./5/module").should == "pam_test.so" + aug.get("./5/argument[1]").should == "test_me_out" + end + end + end + + describe "when modifying settings" do + it "Changing the number of retries" do + apply!(Puppet::Type.type(:pam).new( + :title => "Set retry count for pwquality", + :service => "system-auth", + :type => "password", + :control => "requisite", + :module => "pam_pwquality.so", + :arguments => ["try_first_pass","retry=4","type="], + :target => target, + :provider => "augeas", + :ensure => "present" + )) + + aug_open(target, "Pam.lns") do |aug| + aug.match('./*[type="password" and module="pam_pwquality.so" and argument="retry=4"]').size.should == 1 + end + end + + it "should remove the type= argument" do + apply!(Puppet::Type.type(:pam).new( + :title => "Remove type= from pwquality check", + :service => "system-auth", + :type => "password", + :control => "requisite", + :module => "pam_pwquality.so", + :arguments => ["try_first_pass","retry=4"], + :target => target, + :provider => "augeas", + :ensure => "present" + )) + + aug_open(target, "Pam.lns") do |aug| + aug.match('./*[type="password" and module="pam_pwquality.so" and argument="type="]').size.should == 0 + end + end + end + + describe "when removing settings" do + it "should remove the entry" do + apply!(Puppet::Type.type(:pam).new( + :title => "Remove pwquality entry", + :service => "system-auth", + :type => "password", + :control => "requisite", + :module => "pam_pwquality.so", + :arguments => ["try_first_pass","retry=4"], + :target => target, + :provider => "augeas", + :ensure => "absent" + )) + + aug_open(target, "Pam.lns") do |aug| + aug.match('./*[type="password" and module="pam_pwquality.so"]').size.should == 0 + end + end + end + end + + context "with broken file" do + let(:tmptarget) { aug_fixture("broken") } + let(:target) { tmptarget.path } + + it "should fail to load" do + txn = apply(Puppet::Type.type(:pam).new( + :title => "Ensure pwquality is configured", + :service => "system-auth", + :type => "password", + :control => "requisite", + :module => "pam_pwquality.so", + :arguments => ["try_first_pass","retry=3","type="], + :target => target, + :provider => "augeas", + :ensure => "present" + )) + + txn.any_failed?.should_not == nil + @logs.first.level.should == :err + @logs.first.message.include?(target).should == true + end + end +end