Source code for virttest.utils_selinux

"""
selinux test utility functions.
"""

import logging
import re
import os.path
from autotest.client import utils


[docs]class SelinuxError(Exception): """ Error selinux utility functions. """ pass
[docs]class SeCmdError(SelinuxError): """ Error in executing cmd. """ def __init__(self, cmd, detail): SelinuxError.__init__(self) self.cmd = cmd self.detail = detail def __str__(self): return str("Execute command %s failed.\n" "Detail: %s .\n" % (self.cmd, self.detail))
[docs]class SemanageError(SelinuxError): """ Error when semanage binary is not found """ def __str__(self): return ("The semanage command is not available, " "please install policycoreutils " "or equivalent for your platform.")
[docs]class RestoreconError(SelinuxError): def __str__(self): return ("Output from the restorecon command" "does not match the expected format")
STATUS_LIST = ['enforcing', 'permissive', 'disabled']
[docs]def get_status(): """ Get the status of selinux. :return: string of status in STATUS_LIST. :raise SeCmdError: if execute 'getenforce' failed. :raise SelinuxError: if 'getenforce' command exit 0, but the output is not expected. """ cmd = 'getenforce' result = utils.run(cmd, ignore_status=True) if result.exit_status: raise SeCmdError(cmd, result.stderr) for status in STATUS_LIST: if result.stdout.lower().count(status): return status else: continue raise SelinuxError("result of 'getenforce' (%s)is not expected." % result.stdout)
[docs]def set_status(status): """ Set status of selinux. :param status: status want to set selinux. :raise SelinuxError: status is not supported. :raise SelinuxError: need to reboot host. :raise SeCmdError: execute setenforce failed. :raise SelinuxError: cmd setenforce exit normally, but status of selinux is not set to expected. """ if status not in STATUS_LIST: raise SelinuxError("Status %s is not accepted." % status) current_status = get_status() if status == current_status: return else: if current_status == "disabled" or status == "disabled": raise SelinuxError("Please modify /etc/selinux/config and " "reboot host to set selinux to %s." % status) else: cmd = "setenforce %s" % status result = utils.run(cmd, ignore_status=True) if result.exit_status: raise SeCmdError(cmd, result.stderr) else: current_status = get_status() if not status == current_status: raise SelinuxError("Status of selinux is set to %s," "but not expected %s. " % (current_status, status)) else: pass logging.debug("Set status of selinux to %s success.", status)
[docs]def is_disabled(): """ Return True if the selinux is disabled. """ status = get_status() if status == "disabled": return True else: return False
[docs]def is_not_disabled(): """ Return True if the selinux is not disabled. """ return not is_disabled()
[docs]def is_enforcing(): """ Return true if the selinux is enforcing. """ return (get_status() == "enforcing")
[docs]def is_permissive(): """ Return true if the selinux is permissive. """ return (get_status() == "permissive")
[docs]def get_context_from_str(context): """ Get the context in a context. :param context: SELinux context string :raise SelinuxError: if there is no context in context. """ context_pattern = (r"[a-z,_]*_u:[a-z,_]*_r:[a-z,_]*_t" # non-greedy/non-group match on optional MLS range r"(?:\:[s,\-,0-9,:[c,\,,0-9]*]*)?") if re.search(context_pattern, context): context_list = re.findall(context_pattern, context) return context_list[0] raise SelinuxError("There is no context in %s." % context)
[docs]def get_type_from_context(context): """ Return just the type component of a full context string :param context: SELinux context string :return: Type component of SELinux context string """ # Raise exception if not a context string get_context_from_str(context) type_pattern = (r"[a-z,_]*_u:[a-z,_]*_r:([a-z,_]*_t)" r"(?:\:[s,\-,0-9,:[c,\,,0-9]*]*)?") return re.search(type_pattern, context).group(1)
[docs]def get_context_of_file(filename): """ Get the context of file. :raise SeCmdError: if execute 'getfattr' failed. """ # More direct than scraping 'ls' output. cmd = "getfattr --name security.selinux %s" % filename result = utils.run(cmd, ignore_status=True) if result.exit_status: raise SeCmdError(cmd, result.stderr) output = result.stdout return get_context_from_str(output)
[docs]def set_context_of_file(filename, context): """ Set context of file. :raise SeCmdError: if failed to execute chcon. :raise SelinuxError: if command chcon execute normally, but the context of file is not setted to context. """ context = context.strip() # setfattr used for consistency with getfattr use above cmd = ("setfattr --name security.selinux --value \"%s\" %s" % (context, filename)) result = utils.run(cmd, ignore_status=True) if result.exit_status: raise SeCmdError(cmd, result.stderr) context_result = get_context_of_file(filename) if not context == context_result: raise SelinuxError("Context of %s after chcon is %s, " "but not expected %s." % (filename, context_result, context)) logging.debug("Set context of %s success.", filename)
[docs]def get_context_of_process(pid): """ Get context of process. """ attr_filepath = "/proc/%s/attr/current" % pid attr_file = open(attr_filepath) output = attr_file.read() return get_context_from_str(output)
# Force uniform handling if semanage not found (used in unittests) def _no_semanage(cmdresult): if cmdresult.exit_status == 127: if cmdresult.stdout.lower().count('command not found'): raise SemanageError()
[docs]def get_defcon(local=False): """ Return list of dictionaries containing SELinux default file context types :param local: Only return locally modified default contexts :return: list of dictionaries of default context attributes """ if local: result = utils.run("semanage fcontext --list -C", ignore_status=True) else: result = utils.run("semanage fcontext --list", ignore_status=True) _no_semanage(result) if result.exit_status != 0: raise SeCmdError('semanage', result.stderr) result_list = result.stdout.strip().split('\n') # Need to process top-down instead of bottom-up result_list.reverse() first_line = result_list.pop() # First column name has a space in it column_names = [name.strip().lower().replace(' ', '_') for name in first_line.split(' ') if len(name) > 0] # Shorten first column name column_names[0] = column_names[0].replace("selinux_", "") fcontexts = [] for line in result_list: if len(line) < 1: # skip blank lines continue column_data = [name.strip() for name in line.split(' ') if len(name) > 0] # Enumerating data raises exception if no column_names match fcontext = dict([(column_names[idx], data) for idx, data in enumerate(column_data)]) # find/set functions only accept type, not full context string fcontext['context'] = get_type_from_context(fcontext['context']) fcontexts.append(fcontext) return fcontexts
[docs]def find_defcon_idx(defcon, pathname): """ Returns the index into defcon where pathname matches or None """ # Default context path regexes only work on canonical paths pathname = os.path.realpath(pathname) for default_context in defcon: if bool(re.search(default_context['fcontext'], pathname)): return defcon.index(default_context) return None
[docs]def find_defcon(defcon, pathname): """ Returns the context type of first match to pathname or None """ # Default context path regexes only work on canonical paths pathname = os.path.realpath(pathname) idx = find_defcon_idx(defcon, pathname) if idx is not None: return get_type_from_context(defcon[idx]['context']) else: return None
[docs]def find_pathregex(defcon, pathname): """ Returns the regular expression in defcon matching pathname """ # Default context path regexes only work on canonical paths pathname = os.path.realpath(pathname) idx = find_defcon_idx(defcon, pathname) if idx is not None: return defcon[idx]['fcontext'] else: return None
[docs]def set_defcon(context_type, pathregex, context_range=None): """ Set the default context of a file/path in local SELinux policy :param context_type: The selinux context (only type is used) :param pathregex: Pathname regex e.g. r"/foo/bar/baz(/.*)?" :param context_range: MLS/MCS Security Range e.g. s0:c87,c520 :raise SelinuxError: if semanage command not found :raise SeCmdError: if semanage exits non-zero """ cmd = "semanage fcontext --add" if context_type: cmd += ' -t %s' % context_type if context_range: cmd += ' -r %s' % context_range if pathregex: cmd += ' "%s"' % pathregex result = utils.run(cmd, ignore_status=True) _no_semanage(result) if result.exit_status != 0: raise SeCmdError(cmd, result.stderr)
[docs]def del_defcon(context_type, pathregex): """ Remove the default local SELinux policy type for a file/path :param context: The selinux context (only type is used) :pramm pathregex: Pathname regex e.g. r"/foo/bar/baz(/.*)?" :raise SelinuxError: if semanage command not found :raise SeCmdError: if semanage exits non-zero """ cmd = ("semanage fcontext --delete -t %s '%s'" % (context_type, pathregex)) result = utils.run(cmd, ignore_status=True) _no_semanage(result) if result.exit_status != 0: raise SeCmdError(cmd, result.stderr)
# Process pathname/dirdesc in uniform way for all defcon functions + unittests def _run_restorecon(pathname, dirdesc, readonly=True, force=False): cmd = 'restorecon -v' if dirdesc: cmd += 'R' if readonly: cmd += 'n' if force: cmd += 'F' cmd += ' "%s"' % pathname # Always returns 0, even if contexts wrong return utils.run(cmd).stdout.strip()
[docs]def verify_defcon(pathname, dirdesc=False, readonly=True, forcedesc=False): """ Verify contexts of pathspec (and/or below, if dirdesc) match default :param pathname: Absolute path to file, directory, or symlink :param dirdesc: True to descend into sub-directories :param readonly: True to passive check and don't change any file labels :param forcedesc: True to force a replacement of the entire context :return: True if all components match default contexts :note: By default DOES NOT follow symlinks """ # Default context path regexes only work on canonical paths changes = _run_restorecon(pathname, dirdesc, readonly=readonly, force=forcedesc) if changes.count('restorecon reset'): return False else: return True
# Provide uniform formatting for diff and apply functions def _format_changes(changes): result = [] if changes: # Empty string or None - return empty list # Could be many changes, need efficient line searching regex = re.compile('^restorecon reset (.+) context (.+)->(.+)') for change_line in changes.split('\n'): mobj = regex.search(change_line) if mobj is None: raise RestoreconError() pathname = mobj.group(1) from_con = mobj.group(2) to_con = mobj.group(3) result.append((pathname, from_con, to_con)) return result
[docs]def diff_defcon(pathname, dirdesc=False): """ Return a list of tuple(pathname, from, to) for current & default contexts :param pathname: Absolute path to file, directory, or symlink :param dirdesc: True to descend into sub-directories :return: List of tuple(pathname, from context, to context) """ return _format_changes(_run_restorecon(pathname, dirdesc))
[docs]def apply_defcon(pathname, dirdesc=False): """ Apply default contexts to pathname, possibly descending into sub-dirs also. :param pathname: Absolute path to file, directory, or symlink :param dirdesc: True to descend into sub-directories :return: List of changes applied tuple(pathname, from context, to context) """ return _format_changes(_run_restorecon(pathname, dirdesc, readonly=False))
[docs]def transmogrify_usr_local(pathregex): """ Replace usr/local/something with usr/(local/)?something """ # Whoa! don't mess with short path regex's if len(pathregex) < 3: return pathregex if pathregex.count('usr/local'): pathregex = pathregex.replace('usr/local/', r'usr/(local/)?') return pathregex
[docs]def transmogrify_sub_dirs(pathregex): """ Append '(/.*)?' regex to end of pathregex to optionally match all subdirs """ # Whoa! don't mess with short path regex's if len(pathregex) < 3: return pathregex # Doesn't work with path having trailing slash if pathregex.endswith('/'): pathregex = pathregex[0:-1] return pathregex + r'(/.*)?'