Current File : //opt/puppetlabs/puppet/lib/ruby/vendor_ruby/puppet/util/selinux.rb |
# Provides utility functions to help interface Puppet to SELinux.
#
# This requires the very new SELinux Ruby bindings. These bindings closely
# mirror the SELinux C library interface.
#
# Support for the command line tools is not provided because the performance
# was abysmal. At this time (2008-11-02) the only distribution providing
# these Ruby SELinux bindings which I am aware of is Fedora (in libselinux-ruby).
Puppet.features.selinux? # check, but continue even if it's not
require 'pathname'
module Puppet::Util::SELinux
S_IFREG = 0100000
S_IFDIR = 0040000
S_IFLNK = 0120000
def self.selinux_support?
return false unless defined?(Selinux)
if Selinux.is_selinux_enabled == 1
return true
end
false
end
def selinux_support?
Puppet::Util::SELinux.selinux_support?
end
# Retrieve and return the full context of the file. If we don't have
# SELinux support or if the SELinux call fails then return nil.
def get_selinux_current_context(file)
return nil unless selinux_support?
retval = Selinux.lgetfilecon(file)
if retval == -1
return nil
end
retval[1]
end
# Retrieve and return the default context of the file. If we don't have
# SELinux support or if the SELinux call fails to file a default then return nil.
def get_selinux_default_context(file, resource_ensure=nil)
return nil unless selinux_support?
# If the filesystem has no support for SELinux labels, return a default of nil
# instead of what matchpathcon would return
return nil unless selinux_label_support?(file)
# If the file exists we should pass the mode to matchpathcon for the most specific
# matching. If not, we can pass a mode of 0.
begin
filestat = file_lstat(file)
mode = filestat.mode
rescue Errno::EACCES
mode = 0
rescue Errno::ENOENT
if resource_ensure
mode = get_create_mode(resource_ensure)
else
mode = 0
end
end
retval = Selinux.matchpathcon(file, mode)
if retval == -1
return nil
end
retval[1]
end
# Take the full SELinux context returned from the tools and parse it
# out to the three (or four) component parts. Supports :seluser, :selrole,
# :seltype, and on systems with range support, :selrange.
def parse_selinux_context(component, context)
if context.nil? or context == "unlabeled"
return nil
end
components = /^([^\s:]+):([^\s:]+):([^\s:]+)(?::([\sa-zA-Z0-9:,._-]+))?$/.match(context)
unless components
raise Puppet::Error, _("Invalid context to parse: %{context}") % { context: context }
end
case component
when :seluser
components[1]
when :selrole
components[2]
when :seltype
components[3]
when :selrange
components[4]
else
raise Puppet::Error, _("Invalid SELinux parameter type")
end
end
# This updates the actual SELinux label on the file. You can update
# only a single component or update the entire context.
# The caveat is that since setting a partial context makes no sense the
# file has to already exist. Puppet (via the File resource) will always
# just try to set components, even if all values are specified by the manifest.
# I believe that the OS should always provide at least a fall-through context
# though on any well-running system.
def set_selinux_context(file, value, component = false)
return nil unless selinux_support? && selinux_label_support?(file)
if component
# Must first get existing context to replace a single component
context = Selinux.lgetfilecon(file)[1]
if context == -1
# We can't set partial context components when no context exists
# unless/until we can find a way to make Puppet call this method
# once for all selinux file label attributes.
Puppet.warning _("Can't set SELinux context on file unless the file already has some kind of context")
return nil
end
context = context.split(':')
case component
when :seluser
context[0] = value
when :selrole
context[1] = value
when :seltype
context[2] = value
when :selrange
context[3] = value
else
raise ArgumentError, _("set_selinux_context component must be one of :seluser, :selrole, :seltype, or :selrange")
end
context = context.join(':')
else
context = value
end
retval = Selinux.lsetfilecon(file, context)
if retval == 0
return true
else
Puppet.warning _("Failed to set SELinux context %{context} on %{file}") % { context: context, file: file }
return false
end
end
# Since this call relies on get_selinux_default_context it also needs a
# full non-relative path to the file. Fortunately, that seems to be all
# Puppet uses. This will set the file's SELinux context to the policy's
# default context (if any) if it differs from the context currently on
# the file.
def set_selinux_default_context(file, resource_ensure=nil)
new_context = get_selinux_default_context(file, resource_ensure)
return nil unless new_context
cur_context = get_selinux_current_context(file)
if new_context != cur_context
set_selinux_context(file, new_context)
return new_context
end
nil
end
##
# selinux_category_to_label is an internal method that converts all
# selinux categories to their internal representation, avoiding
# potential issues when mcstransd is not functional.
#
# It is not marked private because it is needed by File's
# selcontext.rb, but it is not intended for use outside of Puppet's
# code.
#
# @param category [String] An selinux category, such as "s0" or "SystemLow"
#
# @return [String] the numeric category name, such as "s0"
def selinux_category_to_label(category)
# We don't cache this, but there's already a ton of duplicate work
# in the selinux handling code.
path = Selinux.selinux_translations_path
begin
File.open(path).each do |line|
line.strip!
next if line.empty?
next if line[0] == "#" # skip comments
line.gsub!(/[[:space:]]+/m, '')
mapping = line.split("=", 2)
if category == mapping[1]
return mapping[0]
end
end
rescue SystemCallError => ex
log_exception(ex)
raise Puppet::Error, _("Could not open SELinux category translation file %{path}.") % { context: context }
end
category
end
########################################################################
# Internal helper methods from here on in, kids. Don't fiddle.
private
# Check filesystem a path resides on for SELinux support against
# whitelist of known-good filesystems.
# Returns true if the filesystem can support SELinux labels and
# false if not.
def selinux_label_support?(file)
fstype = find_fs(file)
return false if fstype.nil?
filesystems = ['ext2', 'ext3', 'ext4', 'gfs', 'gfs2', 'xfs', 'jfs', 'btrfs', 'tmpfs', 'zfs']
filesystems.include?(fstype)
end
# Get mode file type bits set based on ensure on
# the file resource. This helps SELinux determine
# what context a new resource being created should have.
def get_create_mode(resource_ensure)
mode = 0
case resource_ensure
when :present, :file
mode |= S_IFREG
when :directory
mode |= S_IFDIR
when :link
mode |= S_IFLNK
end
mode
end
# Internal helper function to read and parse /proc/mounts
def read_mounts
mounts = ""
begin
if File.method_defined? "read_nonblock"
# If possible we use read_nonblock in a loop rather than read to work-
# a linux kernel bug. See ticket #1963 for details.
mountfh = File.open("/proc/mounts")
loop do
mounts += mountfh.read_nonblock(1024)
end
else
# Otherwise we shell out and let cat do it for us
mountfh = IO.popen("/bin/cat /proc/mounts")
mounts = mountfh.read
end
rescue EOFError
# that's expected
rescue
return nil
ensure
mountfh.close if mountfh
end
mntpoint = {}
# Read all entries in /proc/mounts. The second column is the
# mountpoint and the third column is the filesystem type.
# We skip rootfs because it is always mounted at /
mounts.each_line do |line|
params = line.split(' ')
next if params[2] == 'rootfs'
mntpoint[params[1]] = params[2]
end
mntpoint
end
# Internal helper function to return which type of filesystem a given file
# path resides on
def find_fs(path)
mounts = read_mounts
return nil unless mounts
# cleanpath eliminates useless parts of the path (like '.', or '..', or
# multiple slashes), without touching the filesystem, and without
# following symbolic links. This gives the right (logical) tree to follow
# while we try and figure out what file-system the target lives on.
path = Pathname(path).cleanpath
unless path.absolute?
raise Puppet::DevError, _("got a relative path in SELinux find_fs: %{path}") % { path: path }
end
# Now, walk up the tree until we find a match for that path in the hash.
path.ascend do |segment|
return mounts[segment.to_s] if mounts.has_key?(segment.to_s)
end
# Should never be reached...
return mounts['/']
end
##
# file_lstat is an internal, private method to allow precise stubbing and
# mocking without affecting the rest of the system.
#
# @return [File::Stat] File.lstat result
def file_lstat(path)
Puppet::FileSystem.lstat(path)
end
private :file_lstat
end