diff options
author | Chris St. Pierre <chris.a.st.pierre@gmail.com> | 2012-07-03 08:56:47 -0400 |
---|---|---|
committer | Chris St. Pierre <chris.a.st.pierre@gmail.com> | 2012-07-03 08:56:47 -0400 |
commit | 09e934512dc053a96bd7b16c2c95563e055720f7 (patch) | |
tree | e1351268921fb0fc3b64df8d565044df25196930 /src/lib/Bcfg2/Client/Tools/SELinux.py | |
parent | 9fe65b2fe9323da6583625cde1b2494352207d51 (diff) | |
download | bcfg2-09e934512dc053a96bd7b16c2c95563e055720f7.tar.gz bcfg2-09e934512dc053a96bd7b16c2c95563e055720f7.tar.bz2 bcfg2-09e934512dc053a96bd7b16c2c95563e055720f7.zip |
added selinux support
Diffstat (limited to 'src/lib/Bcfg2/Client/Tools/SELinux.py')
-rw-r--r-- | src/lib/Bcfg2/Client/Tools/SELinux.py | 716 |
1 files changed, 716 insertions, 0 deletions
diff --git a/src/lib/Bcfg2/Client/Tools/SELinux.py b/src/lib/Bcfg2/Client/Tools/SELinux.py new file mode 100644 index 000000000..1c0db904b --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/SELinux.py @@ -0,0 +1,716 @@ +import os +import re +import sys +import copy +import glob +import struct +import socket +import selinux +import seobject +import Bcfg2.Client.XML +import Bcfg2.Client.Tools +import Bcfg2.Client.Tools.POSIX + +def pack128(int_val): + """ pack a 128-bit integer in big-endian format """ + max_int = 2 ** (128) - 1 + max_word_size = 2 ** 32 - 1 + + if int_val <= max_word_size: + return struct.pack('>L', int_val) + + words = [] + for i in range(4): + word = int_val & max_word_size + words.append(int(word)) + int_val >>= 32 + words.reverse() + return struct.pack('>4I', *words) + +def netmask_itoa(netmask, proto="ipv4"): + """ convert an integer netmask (e.g., /16) to dotted-quad + notation (255.255.0.0) or IPv6 prefix notation (ffff::) """ + if proto == "ipv4": + size = 32 + family = socket.AF_INET + else: # ipv6 + size = 128 + family = socket.AF_INET6 + try: + int(netmask) + except ValueError: + return netmask + + if netmask > size: + raise ValueError("Netmask too large: %s" % netmask) + + res = 0L + for n in range(netmask): + res |= 1 << (size - n - 1) + netmask = socket.inet_ntop(family, pack128(res)) + return netmask + + +class SELinux(Bcfg2.Client.Tools.Tool): + """ SELinux boolean and module support """ + name = 'SELinux' + __handles__ = [('SELinux', 'boolean'), + ('SELinux', 'port'), + ('SELinux', 'fcontext'), + ('SELinux', 'node'), + ('SELinux', 'login'), + ('SELinux', 'user'), + ('SELinux', 'interface'), + ('SELinux', 'permissive'), + ('SELinux', 'module')] + __req__ = dict(SELinux=dict(boolean=['name', 'value'], + module=['name'], + port=['name', 'selinuxtype'], + fcontext=['name', 'selinuxtype'], + node=['name', 'selinuxtype', 'proto'], + login=['name', 'selinuxuser'], + user=['name', 'roles', 'prefix'], + interface=['name', 'selinuxtype'], + permissive=['name'])) + + def __init__(self, logger, setup, config): + Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config) + self.handlers = {} + for handles in self.__handles__: + etype = handles[1] + self.handlers[etype] = \ + globals()["SELinux%sHandler" % etype.title()](self, logger, + setup, config) + + def BundleUpdated(self, _, states): + for handler in self.handlers.values(): + handler.BundleUpdated(states) + + def FindExtra(self): + extra = [] + for handler in self.handlers.values(): + extra.extend(handler.FindExtra()) + return extra + + def canInstall(self, entry): + return (Bcfg2.Client.Tools.Tool.canInstall(self, entry) and + self.handlers[entry.get('type')].canInstall(entry)) + + def primarykey(self, entry): + """ return a string that should be unique amongst all entries + in the specification """ + return self.handlers[entry.get('type')].primarykey(entry) + + def Install(self, entries, states): + # start a transaction + sr = seobject.semanageRecords("") + if hasattr(sr, "start"): + self.logger.debug("Starting SELinux transaction") + sr.start() + else: + self.logger.debug("SELinux transactions not supported; this may " + "slow things down considerably") + Bcfg2.Client.Tools.Tool.Install(self, entries, states) + if hasattr(sr, "finish"): + self.logger.debug("Committing SELinux transaction") + sr.finish() + + def InstallSELinux(self, entry): + """Dispatch install to the proper method according to type""" + return self.handlers[entry.get('type')].Install(entry) + + def VerifySELinux(self, entry, _): + """Dispatch verify to the proper method according to type""" + rv = self.handlers[entry.get('type')].Verify(entry) + if entry.get('qtext') and self.setup['interactive']: + entry.set('qtext', + '%s\nInstall SELinux %s %s: (y/N) ' % + (entry.get('qtext'), + entry.get('type'), + self.handlers[entry.get('type')].tostring(entry))) + return rv + + def Remove(self, entries): + """Dispatch verify to the proper removal method according to type""" + # sort by type + types = list() + for entry in entries: + if entry.get('type') not in types: + types.append(entry.get('type')) + + for etype in types: + self.handlers[entry.get('type')].Remove([e for e in entries + if e.get('type') == etype]) + + +class SELinuxEntryHandler(object): + etype = None + key_format = ("name",) + value_format = () + str_format = '%(name)s' + custom_re = re.compile(' (?P<name>\S+)$') + custom_format = None + + def __init__(self, tool, logger, setup, config): + self.tool = tool + self.logger = logger + self._records = None + self._all = None + if not self.custom_format: + self.custom_format = self.key_format + + @property + def records(self): + if self._records is None: + self._records = getattr(seobject, "%sRecords" % self.etype)("") + return self._records + + @property + def all_records(self): + if self._all is None: + self._all = self.records.get_all() + return self._all + + @property + def custom_records(self): + if hasattr(self.records, "customized") and self.custom_re: + return dict([(k, self.all_records[k]) for k in self.custom_keys]) + else: + # ValueError is really a pretty dumb exception to raise, + # but that's what the seobject customized() method raises + # if it's defined but not implemented. yeah, i know, wtf. + raise ValueError("custom_records") + + @property + def custom_keys(self): + keys = [] + for cmd in self.records.customized(): + match = self.custom_re.search(cmd) + if match: + if (len(self.custom_format) == 1 and + self.custom_format[0] == "name"): + keys.append(match.group("name")) + else: + keys.append(tuple([match.group(k) + for k in self.custom_format])) + return keys + + def tostring(self, entry): + return self.str_format % entry.attrib + + def keytostring(self, key): + return self.str_format % self._key2attrs(key) + + def _key(self, entry): + if len(self.key_format) == 1 and self.key_format[0] == "name": + return entry.get("name") + else: + rv = [] + for key in self.key_format: + rv.append(entry.get(key)) + return tuple(rv) + + def _key2attrs(self, key): + if isinstance(key, tuple): + rv = dict((self.key_format[i], key[i]) + for i in range(len(self.key_format)) + if self.key_format[i]) + else: + rv = dict(name=key) + if self.value_format: + vals = self.all_records[key] + rv.update(dict((self.value_format[i], vals[i]) + for i in range(len(self.value_format)) + if self.value_format[i])) + return rv + + def key2entry(self, key): + attrs = self._key2attrs(key) + attrs["type"] = self.etype + return Bcfg2.Client.XML.Element("SELinux", **attrs) + + def _args(self, entry, method): + if hasattr(self, "_%sargs" % method): + return getattr(self, "_%sargs" % method)(entry) + elif hasattr(self, "_defaultargs"): + # default args + return self._defaultargs(entry) + else: + raise NotImplementedError + + def _deleteargs(self, entry): + return (self._key(entry)) + + def canInstall(self, entry): + return bool(self._key(entry)) + + def primarykey(self, entry): + return ":".join([entry.tag, entry.get("type"), entry.get("name")]) + + def exists(self, entry): + if self._key(entry) not in self.all_records: + self.logger.debug("SELinux %s %s does not exist" % + (self.etype, self.tostring(entry))) + return False + return True + + def Verify(self, entry): + if not self.exists(entry): + entry.set('current_exists', 'false') + return False + + errors = [] + current_attrs = self._key2attrs(self._key(entry)) + desired_attrs = entry.attrib + for attr in self.value_format: + if not attr: + continue + if current_attrs[attr] != desired_attrs[attr]: + entry.set('current_%s' % attr, current_attrs[attr]) + errors.append("SELinux %s %s has wrong %s: %s, should be %s" % + (self.etype, self.tostring(entry), attr, + current_attrs[attr], desired_attrs[attr])) + + if errors: + for error in errors: + self.logger.debug(error) + entry.set('qtext', "\n".join([entry.get('qtext', '')] + errors)) + return False + else: + return True + + def Install(self, entry, method=None): + if not method: + if self.exists(entry): + method = "modify" + else: + method = "add" + self.logger.debug("%s SELinux %s %s" % + (method.title(), self.etype, self.tostring(entry))) + + try: + getattr(self.records, method)(*self._args(entry, method)) + self._all = None + return True + except ValueError: + err = sys.exc_info()[1] + self.logger.debug("Failed to %s SELinux %s %s: %s" % + (method, self.etype, self.tostring(entry), err)) + return False + + def Remove(self, entries): + for entry in entries: + try: + self.records.delete(*self._args(entry, "delete")) + self._all = None + except ValueError: + err = sys.exc_info()[1] + self.logger.info("Failed to remove SELinux %s %s: %s" % + (self.etype, self.tostring(entry), err)) + + def FindExtra(self): + specified = [self._key(e) + for e in self.tool.getSupportedEntries() + if e.get("type") == self.etype] + try: + records = self.custom_records + except ValueError: + records = self.all_records + return [self.key2entry(key) + for key in records.keys() + if key not in specified] + + def BundleUpdated(self, states): + pass + + +class SELinuxBooleanHandler(SELinuxEntryHandler): + etype = "boolean" + value_format = ("value",) + + @property + def all_records(self): + # older versions of selinux return a single 0/1 value for each + # bool, while newer versions return a list of three 0/1 values + # representing various states. we don't care about the latter + # two values, but it's easier to coerce the older format into + # the newer format as far as interoperation with the rest of + # SELinuxEntryHandler goes + rv = SELinuxEntryHandler.all_records.fget(self) + if rv.values()[0] in [0, 1]: + for key, val in rv.items(): + rv[key] = [val, val, val] + return rv + + def _key2attrs(self, key): + rv = SELinuxEntryHandler._key2attrs(self, key) + status = self.all_records[key][0] + if status: + rv['value'] = "on" + else: + rv['value'] = "off" + return rv + + def _defaultargs(self, entry): + # the only values recognized by both new and old versions of + # selinux are the strings "0" and "1". old selinux accepts + # ints or bools as well, new selinux accepts "on"/"off" + if entry.get("value").lower() == "on": + value = "1" + else: + value = "0" + return (entry.get("name"), value) + + def canInstall(self, entry): + if entry.get("value").lower() not in ["on", "off"]: + self.logger.debug("SELinux %s %s has a bad value: %s" % + (self.etype, self.tostring(entry), + entry.get("value"))) + return False + return (self.exists(entry) and + SELinuxEntryHandler.canInstall(self, entry)) + + +class SELinuxPortHandler(SELinuxEntryHandler): + etype = "port" + value_format = ('selinuxtype', None) + custom_re = re.compile(r'-p (?P<proto>tcp|udp).*? (?P<start>\d+)(?:-(?P<end>\d+))?$') + + @property + def custom_keys(self): + keys = [] + for cmd in self.records.customized(): + match = self.custom_re.search(cmd) + if match: + if match.group('end'): + keys.append((int(match.group('start')), + int(match.group('end')), + match.group('proto'))) + else: + keys.append((int(match.group('start')), + int(match.group('start')), + match.group('proto'))) + return keys + + @property + def all_records(self): + if self._all is None: + # older versions of selinux use (startport, endport) as + # they key for the ports.get_all() dict, and (type, proto, + # level) as the value; this is obviously broken, so newer + # versions use (startport, endport, proto) as the key, and + # (type, level) as the value. abstracting around this + # sucks. + ports = self.records.get_all() + if len(ports.keys()[0]) == 3: + self._all = ports + else: + # uglist list comprehension ever? + self._all = dict([((k[0], k[1], v[1]), (v[0], v[2])) + for k, v in ports.items()]) + return self._all + + def _key(self, entry): + try: + (port, proto) = entry.get("name").split("/") + except ValueError: + self.logger.error("Invalid SELinux node %s: no protocol specified" % + entry.get("name")) + return + if "-" in port: + start, end = port.split("-") + else: + start = port + end = port + return (int(start), int(end), proto) + + def _key2attrs(self, key): + if key[0] == key[1]: + port = str(key[0]) + else: + port = "%s-%s" % (key[0], key[1]) + vals = self.all_records[key] + return dict(name="%s/%s" % (port, key[2]), selinuxtype=vals[0]) + + def _defaultargs(self, entry): + (port, proto) = entry.get("name").split("/") + return (port, proto, '', entry.get("selinuxtype")) + + def _deleteargs(self, entry): + return tuple(entry.get("name").split("/")) + + +class SELinuxFcontextHandler(SELinuxEntryHandler): + etype = "fcontext" + key_format = ("name", "filetype") + value_format = (None, None, "selinuxtype", None) + filetypeargs = dict(all="", + regular="--", + directory="-d", + symlink="-l", + pipe="-p", + socket="-s", + block="-b", + char="-c", + door="-D") + filetypenames = dict(all="all files", + regular="regular file", + directory="directory", + symlink="symbolic link", + pipe="named pipe", + socket="socket", + block="block device", + char="character device", + door="door") + filetypeattrs = dict([v, k] for k, v in filetypenames.iteritems()) + custom_re = re.compile(r'-f \'(?P<filetype>[a-z ]+)\'.*? \'(?P<name>.*)\'') + + @property + def all_records(self): + if self._all is None: + # on older selinux, fcontextRecords.get_all() returns a + # list of tuples of (filespec, filetype, seuser, serole, + # setype, level); on newer selinux, get_all() returns a + # dict of (filespec, filetype) => (seuser, serole, setype, + # level). + fcontexts = self.records.get_all() + if isinstance(fcontexts, dict): + self._all = fcontexts + else: + self._all = dict([(f[0:2], f[2:]) for f in fcontexts]) + return self._all + + def _key(self, entry): + ftype = entry.get("filetype", "all") + return (entry.get("name"), + self.filetypenames.get(ftype, ftype)) + + def _key2attrs(self, key): + rv = dict(name=key[0], filetype=self.filetypeattrs[key[1]]) + vals = self.all_records[key] + # in older versions of selinux, an fcontext with no selinux + # type is the single value None; in newer versions, it's a + # tuple whose 0th (and only) value is None. + if vals and vals[0]: + rv["selinuxtype"] = vals[2] + else: + rv["selinuxtype"] = "<<none>>" + return rv + + def canInstall(self, entry): + return (entry.get("filetype", "all") in self.filetypeargs and + SELinuxEntryHandler.canInstall(self, entry)) + + def _defaultargs(self, entry): + return (entry.get("name"), entry.get("selinuxtype"), + self.filetypeargs[entry.get("filetype", "all")], + '', '') + + def primarykey(self, entry): + return ":".join([entry.tag, entry.get("type"), entry.get("name"), + entry.get("filetype", "all")]) + + +class SELinuxNodeHandler(SELinuxEntryHandler): + etype = "node" + value_format = (None, None, "selinuxtype", None) + str_format = '%(name)s (%(proto)s)' + custom_re = re.compile(r'-M (?P<netmask>\S+).*?-p (?P<proto>ipv\d).*? (?P<addr>\S+)$') + custom_format = ('addr', 'netmask', 'proto') + + def _key(self, entry): + try: + (addr, netmask) = entry.get("name").split("/") + except ValueError: + self.logger.error("Invalid SELinux node %s: no netmask specified" % + entry.get("name")) + return + netmask = netmask_itoa(netmask, proto=entry.get("proto")) + return (addr, netmask, entry.get("proto")) + + def _key2attrs(self, key): + vals = self.all_records[key] + return dict(name="%s/%s" % (key[0], key[1]), proto=key[2], + selinuxtype=vals[2]) + + def _defaultargs(self, entry): + (addr, netmask) = entry.get("name").split("/") + return (addr, netmask, entry.get("proto"), "", entry.get("selinuxtype")) + + +class SELinuxLoginHandler(SELinuxEntryHandler): + etype = "login" + value_format = ("selinuxuser", None) + + def _defaultargs(self, entry): + return (entry.get("name"), entry.get("selinuxuser"), "") + + +class SELinuxUserHandler(SELinuxEntryHandler): + etype = "user" + value_format = ("prefix", None, None, "roles") + + def __init__(self, tool, logger, setup, config): + SELinuxEntryHandler.__init__(self, tool, logger, setup, config) + self.needs_prefix = False + + @property + def records(self): + if self._records is None: + self._records = seobject.seluserRecords() + return self._records + + def Install(self, entry): + # in older versions of selinux, modify() is broken if you + # provide a prefix _at all_, so we try to avoid giving the + # prefix. however, in newer versions, prefix is _required_, + # so we a) try without a prefix; b) catch TypeError, which + # indicates that we had the wrong number of args (ValueError + # is thrown by the bug in older versions of selinux); and c) + # try with prefix. + try: + SELinuxEntryHandler.Install(self, entry) + except TypeError: + self.needs_prefix = True + SELinuxEntryHandler.Install(self, entry) + + def _defaultargs(self, entry): + # in older versions of selinux, modify() is broken if you + # provide a prefix _at all_, so we try to avoid giving the + # prefix. see the comment in Install() above for more + # details. + rv = [entry.get("name"), + entry.get("roles", "").replace(" ", ",").split(",")] + if self.needs_prefix: + rv.extend(['', '', entry.get("prefix")]) + else: + key = self._key(entry) + if key in self.all_records: + attrs = self._key2attrs(key) + if attrs['prefix'] != entry.get("prefix"): + rv.extend(['', '', entry.get("prefix")]) + return tuple(rv) + + +class SELinuxInterfaceHandler(SELinuxEntryHandler): + etype = "interface" + value_format = (None, None, "selinuxtype", None) + + def _defaultargs(self, entry): + return (entry.get("name"), '', entry.get("selinuxtype")) + + +class SELinuxPermissiveHandler(SELinuxEntryHandler): + etype = "permissive" + + @property + def records(self): + try: + return SELinuxEntryHandler.records.fget(self) + except AttributeError: + self.logger.info("Permissive domains not supported by this version " + "of SELinux") + self._records = False + return self._records + + @property + def all_records(self): + if self._all is None: + if self.records == False: + self._all = dict() + else: + # permissiveRecords.get_all() returns a list, so we just + # make it into a dict so that the rest of + # SELinuxEntryHandler works + self._all = dict([(d, d) for d in self.records.get_all()]) + return self._all + + def _defaultargs(self, entry): + return (entry.get("name"),) + + +class SELinuxModuleHandler(SELinuxEntryHandler): + etype = "module" + value_format = (None, "disabled") + + def __init__(self, tool, logger, setup, config): + SELinuxEntryHandler.__init__(self, tool, logger, setup, config) + self.posixtool = Bcfg2.Client.Tools.POSIX.POSIX(logger, setup, config) + try: + self.setype = selinux.selinux_getpolicytype()[1] + except IndexError: + self.logger.error("Unable to determine SELinux policy type") + self.setype = None + + @property + def all_records(self): + if self._all is None: + # we get a list of tuples back; coerce it into a dict + self._all = dict([(m[0], (m[1], m[2])) + for m in self.records.get_all()]) + return self._all + + def _key2attrs(self, key): + rv = SELinuxEntryHandler._key2attrs(self, key) + status = self.all_records[key][1] + if status: + rv['disabled'] = "false" + else: + rv['disabled'] = "true" + return rv + + def _filepath(self, entry): + return os.path.join("/usr/share/selinux", self.setype, + "%s.pp" % entry.get("name")) + + def _pathentry(self, entry): + pathentry = copy.deepcopy(entry) + pathentry.set("name", self._filepath(pathentry)) + pathentry.set("perms", "0644") + pathentry.set("owner", "root") + pathentry.set("group", "root") + pathentry.set("secontext", "__default__") + return pathentry + + def Verify(self, entry): + if not entry.get("disabled"): + entry.set("disabled", "false") + return (SELinuxEntryHandler.Verify(self, entry) and + self.posixtool.Verifyfile(self._pathentry(entry), None)) + + def canInstall(self, entry): + return (entry.text and self.setype and + SELinuxEntryHandler.canInstall(self, entry)) + + def Install(self, entry): + rv = self.posixtool.Installfile(self._pathentry(entry)) + try: + rv = rv and SELinuxEntryHandler.Install(self, entry) + except NameError: + # some versions of selinux have a bug in seobject that + # makes modify() calls fail. add() seems to have the same + # effect as modify, but without the bug + if self.exists(entry): + rv = rv and SELinuxEntryHandler.Install(self, entry, + method="add") + + if entry.get("disabled", "false").lower() == "true": + method = "disable" + else: + method = "enable" + return rv and SELinuxEntryHandler.Install(self, entry, method=method) + + def _addargs(self, entry): + return (self._filepath(entry),) + + def _defaultargs(self, entry): + return (entry.get("name"),) + + def FindExtra(self): + specified = [self._key(e) + for e in self.tool.getSupportedEntries() + if e.get("type") == self.etype] + return [self.key2entry(os.path.basename(f)[:-3]) + for f in glob.glob(os.path.join("/usr/share/selinux", + self.setype, "*.pp")) + if f not in specified] |