diff options
author | wheatevo <18470637+wheatevo@users.noreply.github.com> | 2023-03-07 14:15:50 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-07 15:15:50 -0500 |
commit | b73d00973326d882c712dd6d70f441f4660695d1 (patch) | |
tree | 93321da8f8e7afb29adb2be9aa5da10bcdef3901 | |
parent | f99e6335594d360defc97f734cd4cc5599b98c8a (diff) | |
download | chef-b73d00973326d882c712dd6d70f441f4660695d1.tar.gz |
Add selinux_user and selinux_login resources (#13511)
* Add `selinux_user` resource to manage SELinux users.
* Add `selinux_login` resource to manage OS login to SELinux user
mapping.
Related to PR https://github.com/sous-chefs/selinux/pull/92
Signed-off-by: Matthew Newell <matthew.newell@oracle.com>
-rw-r--r-- | kitchen-tests/cookbooks/end_to_end/recipes/linux.rb | 25 | ||||
-rw-r--r-- | lib/chef/resource/selinux_login.rb | 129 | ||||
-rw-r--r-- | lib/chef/resource/selinux_user.rb | 137 | ||||
-rw-r--r-- | lib/chef/resources.rb | 2 | ||||
-rw-r--r-- | spec/unit/resource/selinux_login_spec.rb | 73 | ||||
-rw-r--r-- | spec/unit/resource/selinux_user_spec.rb | 92 |
6 files changed, 458 insertions, 0 deletions
diff --git a/kitchen-tests/cookbooks/end_to_end/recipes/linux.rb b/kitchen-tests/cookbooks/end_to_end/recipes/linux.rb index d8e824fde6..b1b907a6d5 100644 --- a/kitchen-tests/cookbooks/end_to_end/recipes/linux.rb +++ b/kitchen-tests/cookbooks/end_to_end/recipes/linux.rb @@ -34,6 +34,31 @@ if platform_family?("rhel", "fedora", "amazon") selinux_state "permissive" do action :permissive end + + user "se_map_test" + + selinux_user "se_map_test_u" do + level "s0" + range "s0" + roles %w{sysadm_r staff_r} + end + + selinux_login "se_map_test" do + user "se_map_test_u" + range "s0" + end + + selinux_login "se_map_test" do + action :delete + end + + selinux_user "se_map_test_u" do + action :delete + end + + user "se_map_test" do + action :remove + end end build_essential do diff --git a/lib/chef/resource/selinux_login.rb b/lib/chef/resource/selinux_login.rb new file mode 100644 index 0000000000..f634b2cb9c --- /dev/null +++ b/lib/chef/resource/selinux_login.rb @@ -0,0 +1,129 @@ +# +# 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_relative "../resource" +require_relative "selinux/common_helpers" + +class Chef + class Resource + class SelinuxLogin < Chef::Resource + unified_mode true + + provides :selinux_login + + description "Use the **selinux_login** resource to add, update, or remove SELinux user to OS login mappings." + introduced "18.1" + examples <<~DOC + **Manage test OS user mapping with a range of s0 and associated SELinux user test_u**: + + ```ruby + selinux_login 'test' do + user 'test_u' + range 's0' + end + ``` + DOC + + property :login, String, + name_property: true, + description: "An optional property to set the OS user login value if it differs from the resource block's name." + + property :user, String, + description: "SELinux user to be mapped." + + property :range, String, + description: "MLS/MCS security range for the SELinux user." + + load_current_value do |new_resource| + logins = shell_out!("semanage login -l").stdout.split("\n") + + current_login = logins.grep(/^#{Regexp.escape(new_resource.login)}\s+/) do |l| + l.match(/^(?<login>[^\s]+)\s+(?<user>[^\s]+)\s+(?<range>[^\s]+)/) + # match returns [<Match 'data'>] or [], shift converts that to <Match 'data'> or nil + end.shift + + current_value_does_not_exist! unless current_login + + # Existing resources should maintain their current configuration unless otherwise specified + new_resource.user ||= current_login[:user] + new_resource.range ||= current_login[:range] + + user current_login[:user] + range current_login[:range] + end + + action_class do + include Chef::SELinux::CommonHelpers + + def semanage_login_args + # Generate arguments for semanage login -a or -m + args = "" + + args += " -s #{new_resource.user}" if new_resource.user + args += " -r #{new_resource.range}" if new_resource.range + + args + end + end + + action :manage, description: "Sets the SELinux login mapping to the desired settings regardless of previous state." do + run_action(:add) + run_action(:modify) + end + + # Create if doesn't exist, do not touch if user already exists + action :add, description: "Creates the SELinux login mapping if not previously created." do + raise "The user property must be populated to create a new SELinux login" if new_resource.user.to_s.empty? + + if selinux_disabled? + Chef::Log.warn("Unable to add SELinux login #{new_resource.login} as SELinux is disabled") + return + end + + unless current_resource + converge_if_changed do + shell_out!("semanage login -a#{semanage_login_args} #{new_resource.login}") + end + end + end + + # Only modify port if it exists & doesn't have the correct context already + action :modify, description: "Updates the SELinux login mapping if previously created." do + if selinux_disabled? + Chef::Log.warn("Unable to modify SELinux login #{new_resource.login} as SELinux is disabled") + return + end + + if current_resource + converge_if_changed do + shell_out!("semanage login -m#{semanage_login_args} #{new_resource.login}") + end + end + end + + # Delete if exists + action :delete, description: "Removes the SELinux login mapping if previously created." do + if selinux_disabled? + Chef::Log.warn("Unable to delete SELinux login #{new_resource.login} as SELinux is disabled") + return + end + + if current_resource + converge_by "deleting SELinux login #{new_resource.login}" do + shell_out!("semanage login -d #{new_resource.login}") + end + end + end + end + end +end diff --git a/lib/chef/resource/selinux_user.rb b/lib/chef/resource/selinux_user.rb new file mode 100644 index 0000000000..ca8d69c919 --- /dev/null +++ b/lib/chef/resource/selinux_user.rb @@ -0,0 +1,137 @@ +# +# 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_relative "../resource" +require_relative "selinux/common_helpers" + +class Chef + class Resource + class SelinuxUser < Chef::Resource + unified_mode true + + provides :selinux_user + + description "Use the **selinux_user** resource to add, update, or remove SELinux users." + introduced "18.1" + examples <<~DOC + **Manage test_u SELinux user with a level and range of s0 and roles sysadm_r and staff_r**: + + ```ruby + selinux_user 'test_u' do + level 's0' + range 's0' + roles %w(sysadm_r staff_r) + end + ``` + DOC + + property :user, String, + name_property: true, + description: "An optional property to set the SELinux user value if it differs from the resource block's name." + + property :level, String, + description: "MLS/MCS security level for the SELinux user." + + property :range, String, + description: "MLS/MCS security range for the SELinux user." + + property :roles, Array, + description: "Associated SELinux roles for the user.", + coerce: proc { |r| Array(r).sort } + + load_current_value do |new_resource| + users = shell_out!("semanage user -l").stdout.split("\n") + + current_user = users.grep(/^#{Regexp.escape(new_resource.user)}\s+/) do |u| + u.match(/^(?<user>[^\s]+)\s+(?<prefix>[^\s]+)\s+(?<level>[^\s]+)\s+(?<range>[^\s]+)\s+(?<roles>.*)$/) + # match returns [<Match 'data'>] or [], shift converts that to <Match 'data'> or nil + end.shift + + current_value_does_not_exist! unless current_user + + # Existing resources should maintain their current configuration unless otherwise specified + new_resource.level ||= current_user[:level] + new_resource.range ||= current_user[:range] + new_resource.roles ||= current_user[:roles].to_s.split.sort + + level current_user[:level] + range current_user[:range] + roles current_user[:roles].to_s.split.sort + end + + action_class do + include Chef::SELinux::CommonHelpers + + def semanage_user_args + # Generate arguments for semanage user -a or -m + args = "" + + args += " -L #{new_resource.level}" if new_resource.level + args += " -r #{new_resource.range}" if new_resource.range + args += " -R '#{new_resource.roles.join(" ")}'" unless new_resource.roles.to_a.empty? + + args + end + end + + action :manage, description: "Sets the SELinux user to the desired settings regardless of previous state." do + run_action(:add) + run_action(:modify) + end + + # Create if doesn't exist, do not touch if user already exists + action :add, description: "Creates the SELinux user if not previously created." do + raise "The roles property must be populated to create a new SELinux user" if new_resource.roles.to_a.empty? + + if selinux_disabled? + Chef::Log.warn("Unable to add SELinux user #{new_resource.user} as SELinux is disabled") + return + end + + unless current_resource + converge_if_changed do + shell_out!("semanage user -a#{semanage_user_args} #{new_resource.user}") + end + end + end + + # Only modify port if it exists & doesn't have the correct context already + action :modify, description: "Updates the SELinux user if previously created." do + if selinux_disabled? + Chef::Log.warn("Unable to modify SELinux user #{new_resource.user} as SELinux is disabled") + return + end + + if current_resource + converge_if_changed do + shell_out!("semanage user -m#{semanage_user_args} #{new_resource.user}") + end + end + end + + # Delete if exists + action :delete, description: "Removes the SELinux user if previously created." do + if selinux_disabled? + Chef::Log.warn("Unable to delete SELinux user #{new_resource.user} as SELinux is disabled") + return + end + + if current_resource + converge_by "deleting SELinux user #{new_resource.user}" do + shell_out!("semanage user -d #{new_resource.user}") + end + end + end + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 0d310f8bea..ca8e5f28c3 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -127,10 +127,12 @@ require_relative "resource/script" require_relative "resource/selinux_boolean" require_relative "resource/selinux_fcontext" require_relative "resource/selinux_install" +require_relative "resource/selinux_login" require_relative "resource/selinux_module" require_relative "resource/selinux_permissive" require_relative "resource/selinux_port" require_relative "resource/selinux_state" +require_relative "resource/selinux_user" require_relative "resource/service" require_relative "resource/sudo" require_relative "resource/sysctl" diff --git a/spec/unit/resource/selinux_login_spec.rb b/spec/unit/resource/selinux_login_spec.rb new file mode 100644 index 0000000000..42aeb52391 --- /dev/null +++ b/spec/unit/resource/selinux_login_spec.rb @@ -0,0 +1,73 @@ +# +# Copyright:: Copyright (c) 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::SelinuxLogin do + let(:node) { Chef::Node.new } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:resource) { Chef::Resource::SelinuxLogin.new("fakey_fakerton", run_context) } + let(:provider) { resource.provider_for_action(:manage) } + + it "sets login property as name_property" do + expect(resource.login).to eql("fakey_fakerton") + end + + it "sets the default action as :manage" do + expect(resource.action).to eql([:manage]) + end + + it "supports :manage, :add, :modify, :delete actions" do + expect { resource.action :manage }.not_to raise_error + expect { resource.action :add }.not_to raise_error + expect { resource.action :modify }.not_to raise_error + expect { resource.action :delete }.not_to raise_error + end + + describe "#semanage_login_args" do + let(:provider) { resource.provider_for_action(:modify) } + + context "when no parameters are provided" do + it "returns an empty string" do + expect(provider.semanage_login_args).to eq("") + end + end + + context "when all parameters are provided" do + it "returns all params" do + resource.user "user_u" + resource.range "s0" + expect(provider.semanage_login_args).to eq(" -s user_u -r s0") + end + end + + context "when no user is provided" do + it "returns range param" do + resource.range "s0" + expect(provider.semanage_login_args).to eq(" -r s0") + end + end + + context "when no range is provided" do + it "returns user param" do + resource.user "user_u" + expect(provider.semanage_login_args).to eq(" -s user_u") + end + end + end +end diff --git a/spec/unit/resource/selinux_user_spec.rb b/spec/unit/resource/selinux_user_spec.rb new file mode 100644 index 0000000000..227b79d8b9 --- /dev/null +++ b/spec/unit/resource/selinux_user_spec.rb @@ -0,0 +1,92 @@ +# +# Copyright:: Copyright (c) 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::SelinuxUser do + let(:node) { Chef::Node.new } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, {}, events) } + let(:resource) { Chef::Resource::SelinuxUser.new("fakey_fakerton", run_context) } + let(:provider) { resource.provider_for_action(:manage) } + let(:semanage_list) { double("shellout", stdout: "") } + + it "sets user property as name_property" do + expect(resource.user).to eql("fakey_fakerton") + end + + it "sets the default action as :manage" do + expect(resource.action).to eql([:manage]) + end + + it "supports :manage, :add, :modify, :delete actions" do + expect { resource.action :manage }.not_to raise_error + expect { resource.action :add }.not_to raise_error + expect { resource.action :modify }.not_to raise_error + expect { resource.action :delete }.not_to raise_error + end + + it "sorts roles property values" do + expect { resource.roles %w{c a b} }.not_to raise_error + expect(resource.roles).to eq(%w{a b c}) + end + + describe "#semanage_user_args" do + let(:provider) { resource.provider_for_action(:modify) } + + context "when no parameters are provided" do + it "returns an empty string" do + expect(provider.semanage_user_args).to eq("") + end + end + + context "when all parameters are provided" do + it "returns all params" do + resource.level "s0" + resource.range "s0" + resource.roles %w{sysadm_r staff_r} + expect(provider.semanage_user_args).to eq(" -L s0 -r s0 -R 'staff_r sysadm_r'") + end + end + + context "when no roles are provided" do + it "returns level and range params" do + resource.level "s0" + resource.range "s0" + resource.roles [] + + expect(provider.semanage_user_args).to eq(" -L s0 -r s0") + end + end + + context "when no range is provided" do + it "returns level and roles params" do + resource.level "s0" + resource.roles %w{sysadm_r staff_r} + expect(provider.semanage_user_args).to eq(" -L s0 -R 'staff_r sysadm_r'") + end + end + + context "when no level is provided" do + it "returns range and roles params" do + resource.range "s0" + resource.roles %w{sysadm_r staff_r} + expect(provider.semanage_user_args).to eq(" -r s0 -R 'staff_r sysadm_r'") + end + end + end +end |