diff options
-rw-r--r-- | .travis.yml | 26 | ||||
-rw-r--r-- | kitchen-tests/cookbooks/base/attributes/default.rb | 8 | ||||
-rw-r--r-- | kitchen-tests/cookbooks/base/metadata.rb | 1 | ||||
-rw-r--r-- | kitchen-tests/cookbooks/base/recipes/default.rb | 12 | ||||
-rw-r--r-- | lib/chef/resource/sudo.rb | 222 | ||||
-rw-r--r-- | lib/chef/resource/support/sudoer.erb | 18 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | spec/unit/resource/sudo_spec.rb | 87 |
8 files changed, 352 insertions, 23 deletions
diff --git a/.travis.yml b/.travis.yml index 30d8ef45a1..ddfb6ff8e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -269,32 +269,32 @@ matrix: sudo: required gemfile: kitchen-tests/Gemfile before_install: - - gem update --system $(grep rubygems omnibus_overrides.rb | cut -d'"' -f2) - - gem install bundler -v $(grep :bundler omnibus_overrides.rb | cut -d'"' -f2) + - gem update --system $(grep rubygems omnibus_overrides.rb | cut -d'"' -f2) + - gem install bundler -v $(grep :bundler omnibus_overrides.rb | cut -d'"' -f2) before_script: - - sudo iptables -L DOCKER || ( echo "DOCKER iptables chain missing" ; sudo iptables -N DOCKER ) - - cd kitchen-tests + - sudo iptables -L DOCKER || ( echo "DOCKER iptables chain missing" ; sudo iptables -N DOCKER ) + - cd kitchen-tests script: - - bundle exec kitchen test base-fedora-latest + - bundle exec kitchen test base-fedora-latest after_failure: - - cat .kitchen/logs/kitchen.log + - cat .kitchen/logs/kitchen.log env: - - FEDORA=latest - - KITCHEN_YAML=.kitchen.travis.yml + - FEDORA=latest + - KITCHEN_YAML=.kitchen.travis.yml - rvm: 2.4.3 services: docker sudo: required gemfile: kitchen-tests/Gemfile before_install: - - gem update --system $(grep rubygems omnibus_overrides.rb | cut -d'"' -f2) - - gem install bundler -v $(grep :bundler omnibus_overrides.rb | cut -d'"' -f2) + - gem update --system $(grep rubygems omnibus_overrides.rb | cut -d'"' -f2) + - gem install bundler -v $(grep :bundler omnibus_overrides.rb | cut -d'"' -f2) before_script: - - sudo iptables -L DOCKER || ( echo "DOCKER iptables chain missing" ; sudo iptables -N DOCKER ) - - cd kitchen-tests + - sudo iptables -L DOCKER || ( echo "DOCKER iptables chain missing" ; sudo iptables -N DOCKER ) + - cd kitchen-tests script: - bundle exec kitchen test base-opensuse-leap after_failure: - - cat .kitchen/logs/kitchen.log + - cat .kitchen/logs/kitchen.log env: - OPENSUSELEAP=42 - KITCHEN_YAML=.kitchen.travis.yml diff --git a/kitchen-tests/cookbooks/base/attributes/default.rb b/kitchen-tests/cookbooks/base/attributes/default.rb index c952f0887e..5e8cee6736 100644 --- a/kitchen-tests/cookbooks/base/attributes/default.rb +++ b/kitchen-tests/cookbooks/base/attributes/default.rb @@ -70,14 +70,6 @@ default["resolver"]["nameservers"] = [ "8.8.8.8", "8.8.4.4" ] default["resolver"]["search"] = "chef.io" # -# sudo cookbook overrides -# - -default["authorization"]["sudo"]["passwordless"] = true -default["authorization"]["sudo"]["groups"] = %w{sysadmin} -default["authorization"]["sudo"]["users"] = %w{vagrant centos ubuntu} - -# # nscd cookbook overrides # diff --git a/kitchen-tests/cookbooks/base/metadata.rb b/kitchen-tests/cookbooks/base/metadata.rb index 87efeb710d..f526318bb7 100644 --- a/kitchen-tests/cookbooks/base/metadata.rb +++ b/kitchen-tests/cookbooks/base/metadata.rb @@ -16,7 +16,6 @@ depends "ntp" depends "openssh" depends "resolver" depends "selinux" -depends "sudo" depends "ubuntu" depends "users" depends "cron" diff --git a/kitchen-tests/cookbooks/base/recipes/default.rb b/kitchen-tests/cookbooks/base/recipes/default.rb index 350b5637e9..dac989cc81 100644 --- a/kitchen-tests/cookbooks/base/recipes/default.rb +++ b/kitchen-tests/cookbooks/base/recipes/default.rb @@ -38,7 +38,17 @@ users_manage "sysadmin" do action [:create] end -include_recipe "sudo" +sudo "sysadmins" do + group ["sysadmin", "%superadmin"] + nopasswd true +end + +sudo "some_person" do + nopasswd true + user "some_person" + commands ["/opt/chef/bin/chef-client"] + env_keep_add %w{PATH RBENV_ROOT RBENV_VERSION} +end include_recipe "chef-client::delete_validation" include_recipe "chef-client::config" diff --git a/lib/chef/resource/sudo.rb b/lib/chef/resource/sudo.rb new file mode 100644 index 0000000000..202e904a35 --- /dev/null +++ b/lib/chef/resource/sudo.rb @@ -0,0 +1,222 @@ +# +# Author:: Bryan W. Berry (<bryan.berry@gmail.com>) +# Author:: Seth Vargo (<sethvargo@gmail.com>) +# +# Copyright:: 2011-2018, Bryan w. Berry +# Copyright:: 2012-2018, Seth Vargo +# Copyright:: 2015-2018, Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "chef/resource" + +class Chef + class Resource + class Sudo < Chef::Resource + resource_name "sudo" + provides "sudo" + + description "Use the sudo resource to add or remove individual sudo entries using sudoers.d files."\ + " Sudo version 1.7.2 or newer is required to use the sudo resource as it relies on the"\ + " '#includedir' directive introduced in version 1.7.2. The resource does not enforce"\ + " installing the version. Supported releases of Ubuntu, Debian and RHEL (6+) all support"\ + " this feature." + + introduced "14.0" + + # acording to the sudo man pages sudo will ignore files in an include dir that have a `.` or `~` + # We convert either to `__` + property :filename, String, + description: "The name of the sudoers.d file", + name_property: true, + coerce: proc { |x| x.gsub(/[\.~]/, "__") } + + property :users, [String, Array], + description: "User(s) to provide sudo privileges to. This accepts either an array or a comma separated.", + default: [], + coerce: proc { |x| x.is_a?(Array) ? x : x.split(/\s*,\s*/) } + + property :groups, [String, Array], + description: "Group(s) to provide sudo privileges to. This accepts either an array or a comma separated list. Leading % on group names is optional.", + default: [], + coerce: proc { |x| coerce_groups(x) } + + property :commands, Array, + description: "An array of commands this sudoer can execute.", + default: ["ALL"] + + property :host, String, + description: "The host to set in the sudo config.", + default: "ALL" + + property :runas, String, + description: "User the command(s) can be run as", + default: "ALL" + + property :nopasswd, [TrueClass, FalseClass], + description: "Allow running sudo without specifying a password sudo", + default: false + + property :noexec, [TrueClass, FalseClass], + description: "Prevent commands from shelling out.", + default: false + + property :template, String, + description: "The name of the erb template in your cookbook if you wish to supply your own template." + + property :variables, [Hash, nil], + description: "The variables to pass to the custom template. Ignored if not using a custom template.", + default: nil + + property :defaults, Array, + description: "An array of defaults for the user/group.", + default: [] + + property :command_aliases, Array, + description: "Command aliases that can be used as allowed commands later in the config", + default: [] + + property :setenv, [TrueClass, FalseClass], + description: "Whether to permit the preserving of environment with sudo -E.", + default: false + + property :env_keep_add, Array, + description: "An array of strings to add to env_keep.", + default: [] + + property :env_keep_subtract, Array, + description: "An array of strings to remove from env_keep.", + default: [] + + property :visudo_path, String, + description: "The path to visudo for config verification.", + default: "/usr/sbin/visudo" + + property :config_prefix, String, + description: "The directory containing the sudoers config file.", + default: lazy { "config_prefix" } + + alias_method :user, :users + alias_method :group, :groups + + # make sure each group starts with a % + def coerce_groups(x) + # split strings on the commas with optional spaces on either side + groups = x.is_a?(Array) ? x : x.split(/\s*,\s*/) + + # make sure all the groups start with % + groups.map { |g| g[0] == "%" ? g : "%#{g}" } + end + + # default config prefix paths based on platform + def config_prefix + case node["platform_family"] + when "smartos" + "/opt/local/etc" + when "mac_os_x" + "/private/etc" + else + "/etc" + end + end + + action :create do + description "Create a single sudoers config in the sudoers.d directory" + + validate_platform + validate_properties + + if docker? # don't even put this into resource collection unless we're in docker + declare_resource(:package, "sudo") do + action :nothing + not_if "which sudo" + end.run_action(:install) + end + + target = "#{new_resource.config_prefix}/sudoers.d/" + declare_resource(:directory, target) unless ::File.exist?(target) + + Chef::Log.warn("#{new_resource.filename} will be rendered, but will not take effect because the #{new_resource.config_prefix}/sudoers config lacks the includedir directive that loads configs from #{new_resource.config_prefix}/sudoers.d/!") if ::File.readlines("#{new_resource.config_prefix}/sudoers").grep(/includedir/).empty? + + if new_resource.template + Chef::Log.debug("Template property provided, all other properties ignored.") + + declare_resource(:template, "#{target}#{new_resource.filename}") do + source new_resource.template + mode "0440" + variables new_resource.variables + verify "#{new_resource.visudo_path} -cf %{path}" if visudo_present? + action :create + end + else + declare_resource(:template, "#{target}#{new_resource.filename}") do + source "sudoer.erb" + source ::File.expand_path("../support/sudoer.erb", __FILE__) + local true + mode "0440" + variables sudoer: (new_resource.groups + new_resource.users).join(","), + host: new_resource.host, + runas: new_resource.runas, + nopasswd: new_resource.nopasswd, + noexec: new_resource.noexec, + commands: new_resource.commands, + command_aliases: new_resource.command_aliases, + defaults: new_resource.defaults, + setenv: new_resource.setenv, + env_keep_add: new_resource.env_keep_add, + env_keep_subtract: new_resource.env_keep_subtract + verify "#{new_resource.visudo_path} -cf %{path}" if visudo_present? + action :create + end + end + end + + action :install do + Chef::Log.warn("The sudo :install action has been renamed :create. Please update your cookbook code for the new action") + action_create + end + + # Removes a user from the sudoers group + action :delete do + description "Remove a sudoers config from the sudoers.d directory" + + file "#{new_resource.config_prefix}/sudoers.d/#{new_resource.filename}" do + action :delete + end + end + + action_class do + # Make sure we fail on FreeBSD + def validate_platform + return unless platform_family?("freebsd") + raise "The sudo resource cannot run on FreeBSD as FreeBSD does not support using a sudoers.d config directory." + end + + # Ensure that the inputs are valid (we cannot just use the resource for this) + def validate_properties + # if group, user, env_keep_add, env_keep_subtract and template are nil, throw an exception + raise "You must specify users, groups, env_keep_add, env_keep_subtract, or template properties!" if new_resource.users.empty? && new_resource.groups.empty? && new_resource.template.nil? && new_resource.env_keep_add.empty? && new_resource.env_keep_subtract.empty? + + # if specifying user or group and template at the same time fail + raise "You cannot specify users or groups properties and also specify a template. To use your own template pass in all template variables using the variables property." if (!new_resource.users.empty? || !new_resource.groups.empty?) && !new_resource.template.nil? + end + + def visudo_present? + return if ::File.exist?(new_resource.visudo_path) + Chef::Log.warn("The visudo binary cannot be found at '#{new_resource.visudo_path}'. Skipping sudoer file validation. If visudo is on this system you can specify the path using the 'visudo_path' property.") + end + end + end + end +end diff --git a/lib/chef/resource/support/sudoer.erb b/lib/chef/resource/support/sudoer.erb new file mode 100644 index 0000000000..c54507535b --- /dev/null +++ b/lib/chef/resource/support/sudoer.erb @@ -0,0 +1,18 @@ +# This file is managed by Chef. +# Do NOT modify this file directly. + +<% @command_aliases.each do |a| -%> +Cmnd_Alias <%= a[:name].upcase %> = <%= a[:command_list].join(', ') %> +<% end -%> +<% @env_keep_add.each do |env_keep| -%> +Defaults env_keep += "<%= env_keep %>" +<% end -%> +<% @env_keep_subtract.each do |env_keep| -%> +Defaults env_keep -= "<%= env_keep %>" +<% end -%> +<% @commands.each do |command| -%> +<% if @sudoer %><%= @sudoer %> <%= @host %>=(<%= @runas %>) <%= 'NOEXEC:' if @noexec %><%= 'NOPASSWD:' if @nopasswd.to_s == 'true' %><%= 'SETENV:' if @setenv.to_s == 'true' %><%= command %><% end -%> +<% end -%> +<% unless @defaults.empty? %> +Defaults:<%= @sudoer %> <%= @defaults.join(',') %> +<% end -%> diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 42150ffc82..dc235deb06 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -85,6 +85,7 @@ require "chef/resource/ruby_block" require "chef/resource/scm" require "chef/resource/script" require "chef/resource/service" +require "chef/resource/sudo" require "chef/resource/systemd_unit" require "chef/resource/windows_service" require "chef/resource/subversion" diff --git a/spec/unit/resource/sudo_spec.rb b/spec/unit/resource/sudo_spec.rb new file mode 100644 index 0000000000..660eb285da --- /dev/null +++ b/spec/unit/resource/sudo_spec.rb @@ -0,0 +1,87 @@ +# +# Copyright:: Copyright 2018, Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "spec_helper" + +describe Chef::Resource::Sudo do + + let(:node) { Chef::Node.new } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:resource) { Chef::Resource::Sudo.new("someone", run_context) } + + it "has a resource name of :sudo" do + expect(resource.resource_name).to eql(:sudo) + end + + it "has a default action of create" do + expect(resource.action).to eql([:create]) + end + + it "the filename property is the name property" do + expect(resource.filename).to eql("someone") + end + + it "coerces filename property values . & ~ to __" do + resource.filename "something.something~" + expect(resource.filename).to eql("something__something__") + end + + it "supports the legacy 'user' property" do + resource.user ["foo"] + expect(resource.users).to eql(["foo"]) + end + + it "supports the legacy 'groups' property" do + resource.group ["%foo"] + expect(resource.groups).to eql(["%foo"]) + end + + it "coerces users & groups String vals to Arrays" do + resource.users "something" + resource.groups "%something" + expect(resource.users).to eql(["something"]) + expect(resource.groups).to eql(["%something"]) + end + + it "coerces users & group String vals no matter the spacing" do + resource.users "user1, user2 , user3 ,user4" + resource.groups "group1, group2 , group3 ,group4" + expect(resource.users).to eql(%w{user1 user2 user3 user4}) + expect(resource.groups).to eql(["%group1", "%group2", "%group3", "%group4"]) + end + + it "coerces groups values to properly start with %" do + resource.groups ["foo", "%bar"] + expect(resource.groups).to eql(["%foo", "%bar"]) + end + + it "it sets the config prefix to /etc on linux" do + node.automatic[:platform_family] = "debian" + expect(resource.config_prefix).to eql("/etc") + end + + it "it sets the config prefix to /private/etc on macOS" do + node.automatic[:platform_family] = "mac_os_x" + expect(resource.config_prefix).to eql("/private/etc") + end + + it "it sets the config prefix to /opt/local/etc on smartos" do + node.automatic[:platform_family] = "smartos" + expect(resource.config_prefix).to eql("/opt/local/etc") + end +end |