File: selinux.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (297 lines) | stat: -rw-r--r-- 9,739 bytes parent folder | download
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