1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
|
# 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
|