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 | |
parent | 9fe65b2fe9323da6583625cde1b2494352207d51 (diff) | |
download | bcfg2-09e934512dc053a96bd7b16c2c95563e055720f7.tar.gz bcfg2-09e934512dc053a96bd7b16c2c95563e055720f7.tar.bz2 bcfg2-09e934512dc053a96bd7b16c2c95563e055720f7.zip |
added selinux support
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/Bcfg2/Client/Frame.py | 76 | ||||
-rw-r--r-- | src/lib/Bcfg2/Client/Tools/POSIX.py | 798 | ||||
-rw-r--r-- | src/lib/Bcfg2/Client/Tools/SELinux.py | 716 | ||||
-rw-r--r-- | src/lib/Bcfg2/Client/Tools/__init__.py | 23 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options.py | 40 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Admin/Compare.py | 3 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/RequiredAttrs.py | 134 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugin.py | 12 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugins/SEModules.py | 46 |
9 files changed, 1401 insertions, 447 deletions
diff --git a/src/lib/Bcfg2/Client/Frame.py b/src/lib/Bcfg2/Client/Frame.py index a8bcb69bf..51bc4aec7 100644 --- a/src/lib/Bcfg2/Client/Frame.py +++ b/src/lib/Bcfg2/Client/Frame.py @@ -124,33 +124,47 @@ class Frame: self.logger.info([tool.name for tool in self.tools]) # find entries not handled by any tools - problems = [entry for struct in config for \ - entry in struct if entry not in self.handled] + problems = [entry for struct in config + for entry in struct + if entry not in self.handled] if problems: self.logger.error("The following entries are not handled by any tool:") - self.logger.error(["%s:%s:%s" % (entry.tag, entry.get('type'), \ - entry.get('name')) for entry in problems]) + for entry in problems: + self.logger.error("%s:%s:%s" % (entry.tag, entry.get('type'), + entry.get('name'))) self.logger.error("") - entries = [(entry.tag, entry.get('name')) - for struct in config for entry in struct] + + self.find_dups(config) + pkgs = [(entry.get('name'), entry.get('origin')) - for struct in config for entry in struct if entry.tag == 'Package'] - multi = [] - for entry in entries[:]: - if entries.count(entry) > 1: - multi.append(entry) - entries.remove(entry) - if multi: - self.logger.debug("The following entries are included multiple times:") - self.logger.debug(["%s:%s" % entry for entry in multi]) - self.logger.debug("") + for struct in config + for entry in struct + if entry.tag == 'Package'] if pkgs: self.logger.debug("The following packages are specified in bcfg2:") self.logger.debug([pkg[0] for pkg in pkgs if pkg[1] == None]) self.logger.debug("The following packages are prereqs added by Packages:") self.logger.debug([pkg[0] for pkg in pkgs if pkg[1] == 'Packages']) + def find_dups(self, config): + entries = dict() + for struct in config: + for entry in struct: + for tool in self.tools: + if tool.handlesEntry(entry): + pkey = tool.primarykey(entry) + if pkey in entries: + entries[pkey] += 1 + else: + entries[pkey] = 1 + multi = [e for e, c in entries.items() if c > 1] + if multi: + self.logger.debug("The following entries are included multiple times:") + for entry in multi: + self.logger.debug(entry) + self.logger.debug("") + def __getattr__(self, name): if name in ['extra', 'handled', 'modified', '__important__']: ret = [] @@ -399,16 +413,32 @@ class Frame: def CondDisplayState(self, phase): """Conditionally print tracing information.""" self.logger.info('\nPhase: %s' % phase) - self.logger.info('Correct entries:\t%d' % list(self.states.values()).count(True)) - self.logger.info('Incorrect entries:\t%d' % list(self.states.values()).count(False)) + self.logger.info('Correct entries:\t%d' % + list(self.states.values()).count(True)) + self.logger.info('Incorrect entries:\t%d' % + list(self.states.values()).count(False)) if phase == 'final' and list(self.states.values()).count(False): - self.logger.info(["%s:%s" % (entry.tag, entry.get('name')) for \ - entry in self.states if not self.states[entry]]) - self.logger.info('Total managed entries:\t%d' % len(list(self.states.values()))) + for entry in self.states.keys(): + if not self.states[entry]: + etype = entry.get('type') + if etype: + self.logger.info( "%s:%s:%s" % (entry.tag, etype, + entry.get('name'))) + else: + self.logger.info(" %s:%s" % (entry.tag, + entry.get('name'))) + self.logger.info('Total managed entries:\t%d' % + len(list(self.states.values()))) self.logger.info('Unmanaged entries:\t%d' % len(self.extra)) if phase == 'final' and self.setup['extra']: - self.logger.info(["%s:%s" % (entry.tag, entry.get('name')) \ - for entry in self.extra]) + for entry in self.extra: + etype = entry.get('type') + if etype: + self.logger.info( "%s:%s:%s" % (entry.tag, etype, + entry.get('name'))) + else: + self.logger.info(" %s:%s" % (entry.tag, + entry.get('name'))) self.logger.info("") diff --git a/src/lib/Bcfg2/Client/Tools/POSIX.py b/src/lib/Bcfg2/Client/Tools/POSIX.py index 995d82356..859d4dd81 100644 --- a/src/lib/Bcfg2/Client/Tools/POSIX.py +++ b/src/lib/Bcfg2/Client/Tools/POSIX.py @@ -20,7 +20,13 @@ import Bcfg2.Client.Tools import Bcfg2.Options from Bcfg2.Client import XML -log = logging.getLogger('POSIX') +log = logging.getLogger(__name__) + +try: + import selinux + has_selinux = True +except ImportError: + has_selinux = False # map between dev_type attribute and stat constants device_map = {'block': stat.S_IFBLK, @@ -28,24 +34,7 @@ device_map = {'block': stat.S_IFBLK, 'fifo': stat.S_IFIFO} -def calcPerms(initial, perms): - """This compares ondisk permissions with specified ones.""" - pdisp = [{1:stat.S_ISVTX, 2:stat.S_ISGID, 4:stat.S_ISUID}, - {1:stat.S_IXUSR, 2:stat.S_IWUSR, 4:stat.S_IRUSR}, - {1:stat.S_IXGRP, 2:stat.S_IWGRP, 4:stat.S_IRGRP}, - {1:stat.S_IXOTH, 2:stat.S_IWOTH, 4:stat.S_IROTH}] - tempperms = initial - if len(perms) == 3: - perms = '0%s' % (perms) - pdigits = [int(perms[digit]) for digit in range(4)] - for index in range(4): - for (num, perm) in list(pdisp[index].items()): - if pdigits[index] & num: - tempperms |= perm - return tempperms - - -def normGid(entry, logger=None): +def normGid(entry): """ This takes a group name or gid and returns the corresponding gid or False. @@ -96,6 +85,115 @@ def isString(strng, encoding): return False +def secontextMatches(entry): + """ determine if the SELinux context of the file on disk matches + the desired context """ + if not has_selinux: + # no selinux libraries + return True + + path = entry.get("path") + context = entry.get("secontext") + if context is None: + # no context listed + return True + + if context == '__default__': + if selinux.getfilecon(entry.get('name'))[1] == \ + selinux.matchpathcon(entry.get('name'), 0)[1]: + return True + else: + return False + elif selinux.getfilecon(entry.get('name'))[1] == context: + return True + else: + return False + + +def setSEContext(entry, path=None, recursive=False): + """ set the SELinux context of the file on disk according to the + config""" + if not has_selinux: + return True + + if path is None: + path = entry.get("path") + context = entry.get("secontext") + if context is None: + # no context listed + return True + + rv = True + if context == '__default__': + try: + selinux.restorecon(path, recursive=recursive) + except: + err = sys.exc_info()[1] + log.error("Failed to restore SELinux context for %s: %s" % + (path, err)) + rv = False + else: + try: + rv &= selinux.lsetfilecon(path, context) == 0 + except: + err = sys.exc_info()[1] + log.error("Failed to restore SELinux context for %s: %s" % + (path, err)) + rv = False + + if recursive: + for root, dirs, files in os.walk(path): + for p in dirs + files: + try: + rv &= selinux.lsetfilecon(p, context) == 0 + except: + err = sys.exc_info()[1] + log.error("Failed to restore SELinux context for %s: %s" + % (path, err)) + rv = False + return rv + + +def setPerms(entry, path=None): + if path is None: + path = entry.get("name") + + if (entry.get('perms') == None or + entry.get('owner') == None or + entry.get('group') == None): + self.logger.error('Entry %s not completely specified. ' + 'Try running bcfg2-lint.' % entry.get('name')) + return False + + rv = True + # split this into multiple try...except blocks so that even if a + # chown fails, the chmod can succeed -- get as close to the + # desired state as we can + try: + os.chown(path, normUid(entry), normGid(entry)) + except KeyError: + logger.error('Failed to change ownership of %s' % path) + rv = False + os.chown(path, 0, 0) + except OSError: + logger.error('Failed to change ownership of %s' % path) + rv = False + + configPerms = int(entry.get('perms'), 8) + if entry.get('dev_type'): + configPerms |= device_map[entry.get('dev_type')] + try: + os.chmod(path, configPerms) + except (OSError, KeyError): + logger.error('Failed to change permissions mode of %s' % path) + rv = False + + if has_selinux: + rv &= setSEContext(entry, path=path) + + return rv + + class POSIX(Bcfg2.Client.Tools.Tool): """POSIX File support code.""" name = 'POSIX' @@ -106,7 +204,14 @@ class POSIX(Bcfg2.Client.Tools.Tool): ('Path', 'nonexistent'), ('Path', 'permissions'), ('Path', 'symlink')] - __req__ = {'Path': ['name', 'type']} + __req__ = dict(Path=dict( + device=['name', 'dev_type', 'perms', 'owner', 'group'], + directory=['name', 'perms', 'owner', 'group'], + file=['name', 'perms', 'owner', 'group'], + hardlink=['name', 'to'], + nonexistent=['name'], + permissions=['name', 'perms', 'owner', 'group'], + symlink=['name', 'to'])) # grab paranoid options from /etc/bcfg2.conf opts = {'ppath': Bcfg2.Options.PARANOID_PATH, @@ -119,13 +224,9 @@ class POSIX(Bcfg2.Client.Tools.Tool): def canInstall(self, entry): """Check if entry is complete for installation.""" if Bcfg2.Client.Tools.Tool.canInstall(self, entry): - if (entry.tag, - entry.get('type'), - entry.text, - entry.get('empty', 'false')) == ('Path', - 'file', - None, - 'false'): + if (entry.get('type') == 'file' and + entry.text is None and + entry.get('empty', 'false') == 'false'): return False return True else: @@ -145,69 +246,60 @@ class POSIX(Bcfg2.Client.Tools.Tool): entry.set('current_group', str(ondisk[stat.ST_GID])) except (OSError, KeyError): pass + + if has_selinux: + try: + entry.set('current_secontext', + selinux.getfilecon(entry.get('name'))[1]) + except (OSError, KeyError): + pass entry.set('perms', str(oct(ondisk[stat.ST_MODE])[-4:])) def Verifydevice(self, entry, _): """Verify device entry.""" - if entry.get('dev_type') == None or \ - entry.get('owner') == None or \ - entry.get('group') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % (entry.get('name'))) - return False if entry.get('dev_type') in ['block', 'char']: # check if major/minor are properly specified - if entry.get('major') == None or \ - entry.get('minor') == None: + if (entry.get('major') == None or + entry.get('minor') == None): self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % (entry.get('name'))) + 'Try running bcfg2-lint.' % + (entry.get('name'))) return False + try: - # check for file existence - filestat = os.stat(entry.get('name')) + ondisk = os.stat(path) except OSError: entry.set('current_exists', 'false') self.logger.debug("%s %s does not exist" % - (entry.tag, entry.get('name'))) + (entry.tag, path)) return False - try: - # attempt to verify device properties as specified in config - dev_type = entry.get('dev_type') - mode = calcPerms(device_map[dev_type], - entry.get('mode', '0600')) - owner = normUid(entry, logger=self.logger) - group = normGid(entry, logger=self.logger) - if dev_type in ['block', 'char']: - # check for incompletely specified entries - if entry.get('major') == None or \ - entry.get('minor') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % (entry.get('name'))) - return False - major = int(entry.get('major')) - minor = int(entry.get('minor')) - if major == os.major(filestat.st_rdev) and \ - minor == os.minor(filestat.st_rdev) and \ - mode == filestat.st_mode and \ - owner == filestat.st_uid and \ - group == filestat.st_gid: - return True - else: - return False - elif dev_type == 'fifo' and \ - mode == filestat.st_mode and \ - owner == filestat.st_uid and \ - group == filestat.st_gid: - return True - else: - self.logger.info('Device properties for %s incorrect' % \ - entry.get('name')) - return False - except OSError: - self.logger.debug("%s %s failed to verify" % - (entry.tag, entry.get('name'))) - return False + rv = self._verify_metadata(entry) + + # attempt to verify device properties as specified in config + dev_type = entry.get('dev_type') + if dev_type in ['block', 'char']: + major = int(entry.get('major')) + minor = int(entry.get('minor')) + if major != os.major(ondisk.st_rdev): + entry.set('current_mtime', mtime) + msg = ("Major number for device %s is incorrect. " + "Current major is %s but should be %s" % + (path, os.major(ondisk.st_rdev), major)) + self.logger.debug(msg) + entry.set('qtext', entry.get('qtext') + "\n" + msg) + rv = False + + if minor != os.minor(ondisk.st_rdev): + entry.set('current_mtime', mtime) + msg = ("Minor number for device %s is incorrect. " + "Current minor is %s but should be %s" % + (path, os.minor(ondisk.st_rdev), minor)) + self.logger.debug(msg) + entry.set('qtext', entry.get('qtext') + "\n" + msg) + rv = False + + return rv def Installdevice(self, entry): """Install device entries.""" @@ -218,7 +310,7 @@ class POSIX(Bcfg2.Client.Tools.Tool): os.unlink(entry.get('name')) exists = False except OSError: - self.logger.info('Failed to unlink %s' % \ + self.logger.info('Failed to unlink %s' % entry.get('name')) return False except OSError: @@ -227,14 +319,14 @@ class POSIX(Bcfg2.Client.Tools.Tool): if not exists: try: dev_type = entry.get('dev_type') - mode = calcPerms(device_map[dev_type], - entry.get('mode', '0600')) + mode = device_map[dev_type] | int(entry.get('mode', '0600'), 8) if dev_type in ['block', 'char']: # check if major/minor are properly specified - if entry.get('major') == None or \ - entry.get('minor') == None: + if (entry.get('major') == None or + entry.get('minor') == None): self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % (entry.get('name'))) + 'Try running bcfg2-lint.' % + entry.get('name')) return False major = int(entry.get('major')) minor = int(entry.get('minor')) @@ -242,17 +334,7 @@ class POSIX(Bcfg2.Client.Tools.Tool): os.mknod(entry.get('name'), mode, device) else: os.mknod(entry.get('name'), mode) - """ - Python uses the OS mknod(2) implementation which modifies the - mode based on the umask of the running process. Therefore, the - following chmod(2) call is needed to make sure the permissions - are set as specified by the user. - """ - os.chmod(entry.get('name'), mode) - os.chown(entry.get('name'), - normUid(entry, logger=self.logger), - normGid(entry, logger=self.logger)) - return True + return setPerms(entry) except KeyError: self.logger.error('Failed to install %s' % entry.get('name')) except OSError: @@ -261,47 +343,13 @@ class POSIX(Bcfg2.Client.Tools.Tool): def Verifydirectory(self, entry, modlist): """Verify Path type='directory' entry.""" - if entry.get('perms') == None or \ - entry.get('owner') == None or \ - entry.get('group') == None: - self.logger.error("POSIX: Entry %s not completely specified. " - "Try running bcfg2-lint." % (entry.get('name'))) - return False - while len(entry.get('perms', '')) < 4: - entry.set('perms', '0' + entry.get('perms', '')) - try: - ondisk = os.stat(entry.get('name')) - except OSError: - entry.set('current_exists', 'false') - self.logger.info("POSIX: %s %s does not exist" % - (entry.tag, entry.get('name'))) - return False - try: - owner = str(ondisk[stat.ST_UID]) - group = str(ondisk[stat.ST_GID]) - except (OSError, KeyError): - self.logger.info("POSIX: User/Group resolution failed " - "for path %s" % entry.get('name')) - owner = 'root' - group = '0' - finfo = os.stat(entry.get('name')) - perms = oct(finfo[stat.ST_MODE])[-4:] - if entry.get('mtime', '-1') != '-1': - mtime = str(finfo[stat.ST_MTIME]) - else: - mtime = '-1' - pTrue = ((owner == str(normUid(entry, logger=self.logger))) and - (group == str(normGid(entry, logger=self.logger))) and - (perms == entry.get('perms')) and - (mtime == entry.get('mtime', '-1'))) - pruneTrue = True ex_ents = [] - if entry.get('prune', 'false') == 'true' \ - and (entry.tag == 'Path' and entry.get('type') == 'directory'): + if (entry.get('prune', 'false') == 'true' + and (entry.tag == 'Path' and entry.get('type') == 'directory')): # check for any extra entries when prune='true' attribute is set try: - entries = ['/'.join([entry.get('name'), ent]) \ + entries = ['/'.join([entry.get('name'), ent]) for ent in os.listdir(entry.get('name'))] ex_ents = [e for e in entries if e not in modlist] if ex_ents: @@ -313,99 +361,48 @@ class POSIX(Bcfg2.Client.Tools.Tool): nqtext += "Directory %s contains extra entries: " % \ entry.get('name') nqtext += ":".join(ex_ents) - entry.set('qtest', nqtext) - [entry.append(XML.Element('Prune', path=x)) \ + entry.set('qtext', nqtext) + [entry.append(XML.Element('Prune', path=x)) for x in ex_ents] except OSError: ex_ents = [] pruneTrue = True - if not pTrue: - if owner != str(normUid(entry, logger=self.logger)): - entry.set('current_owner', owner) - self.logger.debug("%s %s ownership wrong" % \ - (entry.tag, entry.get('name'))) - nqtext = entry.get('qtext', '') + '\n' - nqtext += "%s owner wrong. is %s should be %s" % \ - (entry.get('name'), owner, entry.get('owner')) - entry.set('qtext', nqtext) - if group != str(normGid(entry, logger=self.logger)): - entry.set('current_group', group) - self.logger.debug("%s %s group wrong" % \ - (entry.tag, entry.get('name'))) - nqtext = entry.get('qtext', '') + '\n' - nqtext += "%s group is %s should be %s" % \ - (entry.get('name'), group, entry.get('group')) - entry.set('qtext', nqtext) - if perms != entry.get('perms'): - entry.set('current_perms', perms) - self.logger.debug("%s %s permissions are %s should be %s" % - (entry.tag, - entry.get('name'), - perms, - entry.get('perms'))) - nqtext = entry.get('qtext', '') + '\n' - nqtext += "%s %s perms are %s should be %s" % \ - (entry.tag, - entry.get('name'), - perms, - entry.get('perms')) - entry.set('qtext', nqtext) - if mtime != entry.get('mtime', '-1'): - entry.set('current_mtime', mtime) - self.logger.debug("%s %s mtime is %s should be %s" \ - % (entry.tag, entry.get('name'), mtime, - entry.get('mtime'))) - nqtext = entry.get('qtext', '') + '\n' - nqtext += "%s mtime is %s should be %s" % \ - (entry.get('name'), mtime, entry.get('mtime')) - entry.set('qtext', nqtext) - if entry.get('type') != 'file': - nnqtext = entry.get('qtext') - nnqtext += '\nInstall %s %s: (y/N) ' % (entry.get('type'), - entry.get('name')) - entry.set('qtext', nnqtext) - return pTrue and pruneTrue + return pruneTrue and self._verify_metadata(entry) def Installdirectory(self, entry): """Install Path type='directory' entry.""" - if entry.get('perms') == None or \ - entry.get('owner') == None or \ - entry.get('group') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % \ - (entry.get('name'))) - return False - self.logger.info("Installing directory %s" % (entry.get('name'))) + self.logger.info("Installing directory %s" % entry.get('name')) try: fmode = os.lstat(entry.get('name')) - if not stat.S_ISDIR(fmode[stat.ST_MODE]): - self.logger.debug("Found a non-directory entry at %s" % \ - (entry.get('name'))) - try: - os.unlink(entry.get('name')) - exists = False - except OSError: - self.logger.info("Failed to unlink %s" % \ - (entry.get('name'))) - return False - else: - self.logger.debug("Found a pre-existing directory at %s" % \ - (entry.get('name'))) - exists = True except OSError: # stat failed exists = False + if not stat.S_ISDIR(fmode[stat.ST_MODE]): + self.logger.debug("Found a non-directory entry at %s" % + entry.get('name')) + try: + os.unlink(entry.get('name')) + exists = False + except OSError: + self.logger.info("Failed to unlink %s" % entry.get('name')) + return False + else: + self.logger.debug("Found a pre-existing directory at %s" % + entry.get('name')) + exists = True + if not exists: parent = "/".join(entry.get('name').split('/')[:-1]) if parent: try: os.stat(parent) except: - self.logger.debug('Creating parent path for directory %s' % (entry.get('name'))) + self.logger.debug('Creating parent path for directory %s' % + entry.get('name')) for idx in range(len(parent.split('/')[:-1])): - current = '/'+'/'.join(parent.split('/')[1:2+idx]) + current = '/' + '/'.join(parent.split('/')[1:2+idx]) try: sloc = os.stat(current) except OSError: @@ -424,10 +421,10 @@ class POSIX(Bcfg2.Client.Tools.Tool): try: os.mkdir(entry.get('name')) except OSError: - self.logger.error('Failed to create directory %s' % \ - (entry.get('name'))) + self.logger.error('Failed to create directory %s' % + entry.get('name')) return False - if entry.get('prune', 'false') == 'true' and entry.get("qtest"): + if entry.get('prune', 'false') == 'true' and entry.get("qtext"): for pent in entry.findall('Prune'): pname = pent.get('path') ulfailed = False @@ -448,7 +445,7 @@ class POSIX(Bcfg2.Client.Tools.Tool): def Verifyfile(self, entry, _): """Verify Path type='file' entry.""" # permissions check + content check - permissionStatus = self.Verifydirectory(entry, _) + permissionStatus = self._verify_metadata(entry) tbin = False if entry.text == None and entry.get('empty', 'false') == 'false': self.logger.error("Cannot verify incomplete Path type='%s' %s" % @@ -466,7 +463,7 @@ class POSIX(Bcfg2.Client.Tools.Tool): tempdata = tempdata.encode(self.setup['encoding']) except UnicodeEncodeError: e = sys.exc_info()[1] - self.logger.error("Error encoding file %s:\n %s" % \ + self.logger.error("Error encoding file %s:\n %s" % (entry.get('name'), e)) different = False @@ -535,8 +532,6 @@ class POSIX(Bcfg2.Client.Tools.Tool): else: prompt.append("Diff took too long to compute, no " "printable diff") - prompt.append("Install %s %s: (y/N): " % (entry.tag, - entry.get('name'))) entry.set("qtext", "\n".join(prompt)) if entry.get('sensitive', 'false').lower() != 'true': @@ -565,12 +560,6 @@ class POSIX(Bcfg2.Client.Tools.Tool): binascii.b2a_base64("\n".join(diff))) elif not tbin and isString(content, self.setup['encoding']): entry.set('current_bfile', binascii.b2a_base64(content)) - elif permissionStatus == False and self.setup['interactive']: - prompt = [entry.get('qtext', '')] - prompt.append("Install %s %s: (y/N): " % (entry.tag, - entry.get('name'))) - entry.set("qtext", "\n".join(prompt)) - return permissionStatus and not different @@ -583,8 +572,8 @@ class POSIX(Bcfg2.Client.Tools.Tool): try: os.stat(parent) except: - self.logger.debug('Creating parent path for config file %s' % \ - (entry.get('name'))) + self.logger.debug('Creating parent path for config file %s' % + entry.get('name')) current = '/' for next in parent.split('/')[1:]: current += next + '/' @@ -592,23 +581,24 @@ class POSIX(Bcfg2.Client.Tools.Tool): sloc = os.stat(current) try: if not stat.S_ISDIR(sloc[stat.ST_MODE]): - self.logger.debug('%s is not a directory; recreating' \ - % (current)) + self.logger.debug('%s is not a directory; recreating' + % current) os.unlink(current) os.mkdir(current) except OSError: return False except OSError: try: - self.logger.debug("Creating non-existent path %s" % current) + self.logger.debug("Creating non-existent path %s" % + current) os.mkdir(current) except OSError: return False # If we get here, then the parent directory should exist - if (entry.get("paranoid", False) in ['true', 'True']) and \ - self.setup.get("paranoid", False) and not \ - (entry.get('current_exists', 'true') == 'false'): + if (entry.get("paranoid", 'false').lower() == 'true' and + self.setup.get("paranoid", False) and + entry.get('current_exists', 'true') != 'false'): bkupnam = entry.get('name').replace('/', '_') # current list of backups for this file try: @@ -627,7 +617,7 @@ class POSIX(Bcfg2.Client.Tools.Tool): try: os.remove("%s/%s" % (self.ppath, oldest)) except: - self.logger.error("Failed to remove %s/%s" % \ + self.logger.error("Failed to remove %s/%s" % (self.ppath, oldest)) return False try: @@ -639,8 +629,8 @@ class POSIX(Bcfg2.Client.Tools.Tool): (entry.get('name'), self.ppath)) except IOError: e = sys.exc_info()[1] - self.logger.error("Failed to create backup file for %s" % \ - (entry.get('name'))) + self.logger.error("Failed to create backup file for %s" % + entry.get('name')) self.logger.error(e) return False try: @@ -656,82 +646,59 @@ class POSIX(Bcfg2.Client.Tools.Tool): filedata = entry.text newfile.write(filedata) newfile.close() - try: - os.chown(newfile.name, - normUid(entry, logger=self.logger), - normGid(entry, logger=self.logger)) - except KeyError: - self.logger.error("Failed to chown %s to %s:%s" % - (newfile.name, entry.get('owner'), - entry.get('group'))) - os.chown(newfile.name, 0, 0) - except OSError: - err = sys.exc_info()[1] - self.logger.error("Could not chown %s: %s" % (newfile.name, - err)) - os.chmod(newfile.name, calcPerms(stat.S_IFREG, entry.get('perms'))) + + rv = setPerms(entry, newfile.name) os.rename(newfile.name, entry.get('name')) - if entry.get('mtime', '-1') != '-1': + if entry.get('mtime'): try: os.utime(entry.get('name'), (int(entry.get('mtime')), int(entry.get('mtime')))) except: - self.logger.error("File %s mtime fix failed" \ - % (entry.get('name'))) - return False - return True + logger.error("Failed to set mtime of %s" % path) + rv = False + return rv except (OSError, IOError): err = sys.exc_info()[1] if err.errno == errno.EACCES: - self.logger.info("Failed to open %s for writing" % (entry.get('name'))) + self.logger.info("Failed to open %s for writing" % + entry.get('name')) else: print(err) return False def Verifyhardlink(self, entry, _): """Verify HardLink entry.""" - if entry.get('to') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % \ - (entry.get('name'))) - return False + rv = True + try: - if os.path.samefile(entry.get('name'), entry.get('to')): - return True - self.logger.debug("Hardlink %s is incorrect" % \ - entry.get('name')) - entry.set('qtext', "Link %s to %s? [y/N] " % \ - (entry.get('name'), - entry.get('to'))) - return False + if not os.path.samefile(entry.get('name'), entry.get('to')): + msg = "Hardlink %s is incorrect." % entry.get('name') + self.logger.debug(msg) + entry.set('qtext', "\n".join([entry.get('qtext', ''), msg])) + rv = False except OSError: entry.set('current_exists', 'false') - entry.set('qtext', "Link %s to %s? [y/N] " % \ - (entry.get('name'), - entry.get('to'))) return False + rv &= self._verify_secontext(entry) + return rv + def Installhardlink(self, entry): """Install HardLink entry.""" - if entry.get('to') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % \ - (entry.get('name'))) - return False - self.logger.info("Installing Hardlink %s" % (entry.get('name'))) + self.logger.info("Installing Hardlink %s" % entry.get('name')) if os.path.lexists(entry.get('name')): try: fmode = os.lstat(entry.get('name'))[stat.ST_MODE] if stat.S_ISREG(fmode) or stat.S_ISLNK(fmode): self.logger.debug("Non-directory entry already exists at " - "%s. Unlinking entry." % (entry.get('name'))) + "%s. Unlinking entry." % + entry.get('name')) os.unlink(entry.get('name')) elif stat.S_ISDIR(fmode): - self.logger.debug("Directory already exists at %s" % \ - (entry.get('name'))) - self.cmd.run("mv %s/ %s.bak" % \ - (entry.get('name'), - entry.get('name'))) + self.logger.debug("Directory already exists at %s" % + entry.get('name')) + self.cmd.run("mv %s/ %s.bak" % (entry.get('name'), + entry.get('name'))) else: os.unlink(entry.get('name')) except OSError: @@ -739,7 +706,7 @@ class POSIX(Bcfg2.Client.Tools.Tool): (entry.get('name'))) try: os.link(entry.get('to'), entry.get('name')) - return True + return setPerms(entry) except OSError: return False @@ -789,135 +756,63 @@ class POSIX(Bcfg2.Client.Tools.Tool): def Verifypermissions(self, entry, _): """Verify Path type='permissions' entry""" - if entry.get('perms') == None or \ - entry.get('owner') == None or \ - entry.get('group') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % (entry.get('name'))) - return False - if entry.get('recursive') in ['True', 'true']: + rv = self._verify_metadata(entry) + + if entry.get('recursive', 'false').lower() == 'true': # verify ownership information recursively - owner = normUid(entry, logger=self.logger) - group = normGid(entry, logger=self.logger) - for root, dirs, files in os.walk(entry.get('name')): for p in dirs + files: - path = os.path.join(root, p) - pstat = os.stat(path) - if owner != pstat.st_uid: - # owner mismatch for path - entry.set('current_owner', str(pstat.st_uid)) - self.logger.debug("%s %s ownership wrong" % \ - (entry.tag, path)) - nqtext = entry.get('qtext', '') + '\n' - nqtext += ("Owner for path %s is incorrect. " - "Current owner is %s but should be %s\n" % \ - (path, pstat.st_uid, entry.get('owner'))) - nqtext += ("\nInstall %s %s: (y/N): " % - (entry.tag, entry.get('name'))) - entry.set('qtext', nqtext) - return False - if group != pstat.st_gid: - # group mismatch for path - entry.set('current_group', str(pstat.st_gid)) - self.logger.debug("%s %s group wrong" % \ - (entry.tag, path)) - nqtext = entry.get('qtext', '') + '\n' - nqtext += ("Group for path %s is incorrect. " - "Current group is %s but should be %s\n" % \ - (path, pstat.st_gid, entry.get('group'))) - nqtext += ("\nInstall %s %s: (y/N): " % - (entry.tag, entry.get('name'))) - entry.set('qtext', nqtext) - return False - return self.Verifydirectory(entry, _) - - def _diff(self, content1, content2, difffunc, filename=None): - rv = [] - start = time.time() - longtime = False - for diffline in difffunc(content1.split('\n'), - content2.split('\n')): - now = time.time() - rv.append(diffline) - if now - start > 5 and not longtime: - if filename: - self.logger.info("Diff of %s taking a long time" % - filename) - else: - self.logger.info("Diff taking a long time") - longtime = True - elif now - start > 30: - if filename: - self.logger.error("Diff of %s took too long; giving up" % - filename) - else: - self.logger.error("Diff took too long; giving up") - return False + rv &= self._verify_metadata(entry, + path=os.path.join(root, p)) return rv def Installpermissions(self, entry): """Install POSIX permissions""" - if entry.get('perms') == None or \ - entry.get('owner') == None or \ - entry.get('group') == None: - self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % (entry.get('name'))) - return False plist = [entry.get('name')] if entry.get('recursive') in ['True', 'true']: # verify ownership information recursively - owner = normUid(entry, logger=self.logger) - group = normGid(entry, logger=self.logger) - for root, dirs, files in os.walk(entry.get('name')): for p in dirs + files: - path = os.path.join(root, p) - pstat = os.stat(path) - if owner != pstat.st_uid or group != pstat.st_gid: - # owner mismatch for path + if not self._verify_metadata(entry, + path=os.path.join(root, p), + checkonly=True): plist.append(path) - try: - for p in plist: - os.chown(p, - normUid(entry, logger=self.logger), - normGid(entry, logger=self.logger)) - os.chmod(p, calcPerms(stat.S_IFDIR, entry.get('perms'))) - return True - except (OSError, KeyError): - self.logger.error('Permission fixup failed for %s' % \ - (entry.get('name'))) - return False + rv = True + for path in plist: + rv &= setPerms(entry, path) + return rv def Verifysymlink(self, entry, _): """Verify Path type='symlink' entry.""" if entry.get('to') == None: self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % \ + 'Try running bcfg2-lint.' % (entry.get('name'))) return False + + rv = True + try: sloc = os.readlink(entry.get('name')) - if sloc == entry.get('to'): - return True - self.logger.debug("Symlink %s points to %s, should be %s" % \ - (entry.get('name'), sloc, entry.get('to'))) - entry.set('current_to', sloc) - entry.set('qtext', "Link %s to %s? [y/N] " % (entry.get('name'), - entry.get('to'))) - return False + if sloc != entry.get('to'): + entry.set('current_to', sloc) + msg = ("Symlink %s points to %s, should be %s" % + (entry.get('name'), sloc, entry.get('to'))) + self.logger.debug(msg) + entry.set('qtext', "\n".join([entry.get('qtext', ''), msg])) + rv = False except OSError: entry.set('current_exists', 'false') - entry.set('qtext', "Link %s to %s? [y/N] " % (entry.get('name'), - entry.get('to'))) return False + rv &= self._verify_secontext(entry) + return rv + def Installsymlink(self, entry): """Install Path type='symlink' entry.""" if entry.get('to') == None: self.logger.error('Entry %s not completely specified. ' - 'Try running bcfg2-lint.' % \ - (entry.get('name'))) + 'Try running bcfg2-lint.' % entry.get('name')) return False self.logger.info("Installing symlink %s" % (entry.get('name'))) if os.path.lexists(entry.get('name')): @@ -925,23 +820,22 @@ class POSIX(Bcfg2.Client.Tools.Tool): fmode = os.lstat(entry.get('name'))[stat.ST_MODE] if stat.S_ISREG(fmode) or stat.S_ISLNK(fmode): self.logger.debug("Non-directory entry already exists at " - "%s. Unlinking entry." % \ - (entry.get('name'))) + "%s. Unlinking entry." % + entry.get('name')) os.unlink(entry.get('name')) elif stat.S_ISDIR(fmode): - self.logger.debug("Directory already exists at %s" %\ - (entry.get('name'))) - self.cmd.run("mv %s/ %s.bak" % \ - (entry.get('name'), - entry.get('name'))) + self.logger.debug("Directory already exists at %s" % + entry.get('name')) + self.cmd.run("mv %s/ %s.bak" % (entry.get('name'), + entry.get('name'))) else: os.unlink(entry.get('name')) except OSError: - self.logger.info("Symlink %s cleanup failed" %\ + self.logger.info("Symlink %s cleanup failed" % (entry.get('name'))) try: os.symlink(entry.get('to'), entry.get('name')) - return True + return setSEContext(entry) except OSError: return False @@ -952,5 +846,139 @@ class POSIX(Bcfg2.Client.Tools.Tool): def VerifyPath(self, entry, _): """Dispatch verify to the proper method according to type""" - ret = getattr(self, 'Verify%s' % entry.get('type')) - return ret(entry, _) + ret = getattr(self, 'Verify%s' % entry.get('type'))(entry, _) + if entry.get('qtext') and self.setup['interactive']: + entry.set('qtext', + '%s\nInstall %s %s: (y/N) ' % + (entry.get('qtext'), + entry.get('type'), entry.get('name'))) + return ret + + def _verify_metadata(self, entry, path=None, checkonly=False): + """ generic method to verify perms, owner, group, secontext, + and mtime """ + + # allow setting an alternate path for recursive permissions checking + if path is None: + path = entry.get('name') + + while len(entry.get('perms', '')) < 4: + entry.set('perms', '0' + entry.get('perms', '')) + + try: + ondisk = os.stat(path) + except OSError: + entry.set('current_exists', 'false') + self.logger.debug("POSIX: %s %s does not exist" % + (entry.tag, path)) + return False + + try: + owner = str(ondisk[stat.ST_UID]) + group = str(ondisk[stat.ST_GID]) + except (OSError, KeyError): + self.logger.error('POSIX: User/Group resolution failed for path %s' + % path) + owner = 'root' + group = '0' + + perms = oct(ondisk[stat.ST_MODE])[-4:] + if entry.get('mtime', '-1') != '-1': + mtime = str(ondisk[stat.ST_MTIME]) + else: + mtime = '-1' + + configOwner = str(normUid(entry)) + configGroup = str(normGid(entry)) + configPerms = int(entry.get('perms'), 8) + if entry.get('dev_type'): + configPerms |= device_map[entry.get('dev_type')] + if has_selinux: + if entry.get("secontext") == "__default__": + configContext = selinux.matchpathcon(path, 0)[1] + else: + configContext = entry.get("secontext") + + errors = [] + if owner != configOwner: + if checkonly: + return False + entry.set('current_owner', owner) + errors.append("POSIX: Owner for path %s is incorrect. " + "Current owner is %s but should be %s" % + (path, ondisk.st_uid, entry.get('owner'))) + + if group != configGroup: + if checkonly: + return False + entry.set('current_group', group) + errors.append("POSIX: Group for path %s is incorrect. " + "Current group is %s but should be %s" % + (path, ondisk.st_gid, entry.get('group'))) + + if oct(int(perms, 8)) != oct(configPerms): + if checkonly: + return False + entry.set('current_perms', perms) + errors.append("POSIX: Permissions for path %s are incorrect. " + "Current permissions are %s but should be %s" % + (path, perms, entry.get('perms'))) + + if entry.get('mtime') and mtime != entry.get('mtime', '-1'): + if checkonly: + return False + entry.set('current_mtime', mtime) + errors.append("POSIX: mtime for path %s is incorrect. " + "Current mtime is %s but should be %s" % + (path, mtime, entry.get('mtime'))) + + seVerifies = self._verify_secontext(entry) + + if errors: + for error in errors: + self.logger.debug(error) + entry.set('qtext', "\n".join([entry.get('qtext', '')] + errors)) + return False + else: + return seVerifies + + def _verify_secontext(self, entry): + if not secontextMatches(entry): + path = entry.get("name") + if entry.get("secontext") == "__default__": + configContext = selinux.matchpathcon(path, 0)[1] + else: + configContext = entry.get("secontext") + pcontext = selinux.getfilecon(path)[1] + entry.set('current_secontext', pcontext) + msg = ("SELinux context for path %s is incorrect. " + "Current context is %s but should be %s" % + (path, pcontext, configContext)) + self.logger.debug(msg) + entry.set('qtext', "\n".join([entry.get("qtext", ''), msg])) + return False + return True + + def _diff(self, content1, content2, difffunc, filename=None): + rv = [] + start = time.time() + longtime = False + for diffline in difffunc(content1.split('\n'), + content2.split('\n')): + now = time.time() + rv.append(diffline) + if now - start > 5 and not longtime: + if filename: + self.logger.info("Diff of %s taking a long time" % + filename) + else: + self.logger.info("Diff taking a long time") + longtime = True + elif now - start > 30: + if filename: + self.logger.error("Diff of %s took too long; giving up" % + filename) + else: + self.logger.error("Diff took too long; giving up") + return False + return rv 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] diff --git a/src/lib/Bcfg2/Client/Tools/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py index d423b6380..e4a0ec220 100644 --- a/src/lib/Bcfg2/Client/Tools/__init__.py +++ b/src/lib/Bcfg2/Client/Tools/__init__.py @@ -137,6 +137,18 @@ class Tool: """Default implementation of the information gathering routines.""" pass + def missing_attrs(self, entry): + required = self.__req__[entry.tag] + if isinstance(required, dict): + required = ["type"] + try: + required.extend(self.__req__[entry.tag][entry.get("type")]) + except KeyError: + pass + + return [attr for attr in required + if attr not in entry.attrib or not entry.attrib[attr]] + def canVerify(self, entry): """Test if entry has enough information to be verified.""" if not self.handlesEntry(entry): @@ -149,8 +161,7 @@ class Tool: entry.get('failure'))) return False - missing = [attr for attr in self.__req__[entry.tag] \ - if attr not in entry.attrib] + missing = self.missing_attrs(entry) if missing: self.logger.error("Incomplete information for entry %s:%s; cannot verify" \ % (entry.tag, entry.get('name'))) @@ -168,6 +179,11 @@ class Tool: """Return a list of extra entries.""" return [] + def primarykey(self, entry): + """ return a string that should be unique amongst all entries + in the specification """ + return "%s:%s" % (entry.tag, entry.get("name")) + def canInstall(self, entry): """Test if entry has enough information to be installed.""" if not self.handlesEntry(entry): @@ -178,8 +194,7 @@ class Tool: (entry.tag, entry.get('name'))) return False - missing = [attr for attr in self.__ireq__[entry.tag] \ - if attr not in entry.attrib or not entry.attrib[attr]] + missing = self.missing_attrs(entry) if missing: self.logger.error("Incomplete information for entry %s:%s; cannot install" \ % (entry.tag, entry.get('name'))) diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py index bbbbec343..803b2755d 100644 --- a/src/lib/Bcfg2/Options.py +++ b/src/lib/Bcfg2/Options.py @@ -330,6 +330,11 @@ MDATA_PERMS = \ default='644', odesc='octal permissions', cf=('mdata', 'perms')) +MDATA_SECONTEXT = \ + Option('Default SELinux context', + default='__default__', + odesc='SELinux context', + cf=('mdata', 'secontext')) MDATA_PARANOID = \ Option('Default Path paranoid setting', default='true', @@ -836,6 +841,41 @@ DRIVER_OPTIONS = \ yumng_verify_fail_action=CLIENT_YUMNG_VERIFY_FAIL_ACTION, yumng_verify_flags=CLIENT_YUMNG_VERIFY_FLAGS) +CLIENT_COMMON_OPTIONS = \ + dict(extra=CLIENT_EXTRA_DISPLAY, + quick=CLIENT_QUICK, + lockfile=LOCKFILE, + drivers=CLIENT_DRIVERS, + dryrun=CLIENT_DRYRUN, + paranoid=CLIENT_PARANOID, + bundle=CLIENT_BUNDLE, + skipbundle=CLIENT_SKIPBUNDLE, + bundle_quick=CLIENT_BUNDLEQUICK, + indep=CLIENT_INDEP, + skipindep=CLIENT_SKIPINDEP, + file=CLIENT_FILE, + interactive=INTERACTIVE, + cache=CLIENT_CACHE, + profile=CLIENT_PROFILE, + remove=CLIENT_REMOVE, + server=SERVER_LOCATION, + user=CLIENT_USER, + password=SERVER_PASSWORD, + retries=CLIENT_RETRIES, + kevlar=CLIENT_KEVLAR, + omit_lock_check=OMIT_LOCK_CHECK, + decision=CLIENT_DLIST, + servicemode=CLIENT_SERVICE_MODE, + key=CLIENT_KEY, + certificate=CLIENT_CERT, + ca=CLIENT_CA, + serverCN=CLIENT_SCNS, + timeout=CLIENT_TIMEOUT, + decision_list=CLIENT_DECISION_LIST) +CLIENT_COMMON_OPTIONS.update(DRIVER_OPTIONS) +CLIENT_COMMON_OPTIONS.update(CLI_COMMON_OPTIONS) + + class OptionParser(OptionSet): """ OptionParser bootstraps option parsing, diff --git a/src/lib/Bcfg2/Server/Admin/Compare.py b/src/lib/Bcfg2/Server/Admin/Compare.py index 050dd69f8..78b30120a 100644 --- a/src/lib/Bcfg2/Server/Admin/Compare.py +++ b/src/lib/Bcfg2/Server/Admin/Compare.py @@ -18,7 +18,8 @@ class Compare(Bcfg2.Server.Admin.Mode): 'important', 'paranoid', 'sensitive', 'dev_type', 'major', 'minor', 'prune', 'encoding', 'empty', 'to', 'recursive', - 'vcstype', 'sourceurl', 'revision'], + 'vcstype', 'sourceurl', 'revision', + 'secontext'], 'Package': ['name', 'type', 'version', 'simplefile', 'verify'], 'Service': ['name', 'type', 'status', 'mode', diff --git a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py index 6f76cf2db..0a369c841 100644 --- a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py +++ b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py @@ -1,50 +1,114 @@ -import os.path +import os +import re import lxml.etree import Bcfg2.Server.Lint +import Bcfg2.Client.Tools.POSIX +import Bcfg2.Client.Tools.VCS from Bcfg2.Server.Plugins.Packages import Apt, Yum +# format verifying functions +def is_filename(val): + return val.startswith("/") and len(val) > 1 + +def is_selinux_type(val): + return re.match(r'^[a-z_]+_t', val) + +def is_selinux_user(val): + return re.match(r'^[a-z_]+_u', val) + +def is_octal_mode(val): + return re.match(r'[0-7]{3,4}', val) + +def is_username(val): + return re.match(r'^([a-z]\w{0,30}|\d+)$', val) + +def is_device_mode(val): + try: + # checking upper bound seems like a good way to discover some + # obscure OS with >8-bit device numbers + return int(val) > 0 + except: + return False + class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): """ verify attributes for configuration entries (as defined in doc/server/configurationentries) """ def __init__(self, *args, **kwargs): Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs) - self.required_attrs = { - 'Path': { - 'device': ['name', 'owner', 'group', 'dev_type'], - 'directory': ['name', 'owner', 'group', 'perms'], - 'file': ['name', 'owner', 'group', 'perms', '__text__'], - 'hardlink': ['name', 'to'], - 'symlink': ['name', 'to'], - 'ignore': ['name'], - 'nonexistent': ['name'], - 'permissions': ['name', 'owner', 'group', 'perms'], - 'vcs': ['vcstype', 'revision', 'sourceurl']}, - 'Service': { - 'chkconfig': ['name'], - 'deb': ['name'], - 'rc-update': ['name'], - 'smf': ['name', 'FMRI'], - 'upstart': ['name']}, - 'Action': ['name', 'timing', 'when', 'status', 'command'], - 'Package': ['name']} + self.required_attrs = dict( + Path=dict( + device=dict(name=is_filename, owner=is_username, + group=is_username, + dev_type=lambda v: \ + v in Bcfg2.Client.Tools.POSIX.device_map), + directory=dict(name=is_filename, owner=is_username, + group=is_username, perms=is_octal_mode), + file=dict(name=is_filename, owner=is_username, + group=is_username, perms=is_octal_mode, + __text__=None), + hardlink=dict(name=is_filename, to=is_filename), + symlink=dict(name=is_filename, to=is_filename), + ignore=dict(name=is_filename), + nonexistent=dict(name=is_filename), + permissions=dict(name=is_filename, owner=is_username, + group=is_username, perms=is_octal_mode), + vcs=dict(vcstype=lambda v: (v != 'Path' and + hasattr(Bcfg2.Client.Tools.VCS, + "Install%s" % v)), + revision=None, sourceurl=None)), + Service={ + "chkconfig": dict(name=None), + "deb": dict(name=None), + "rc-update": dict(name=None), + "smf": dict(name=None, FMRI=None), + "upstart": dict(name=None)}, + Action={None: dict(name=None, + timing=lambda v: v in ['pre', 'post', 'both'], + when=lambda v: v in ['modified', 'always'], + status=lambda v: v in ['ignore', 'check'], + command=None)}, + Package={None: dict(name=None)}, + SELinux=dict( + boolean=dict(name=None, + value=lambda v: v in ['on', 'off']), + module=dict(name=None, __text__=None), + port=dict(name=lambda v: re.match(r'^\d+(-\d+)?/(tcp|udp)', v), + selinuxtype=is_selinux_type), + fcontext=dict(name=None, selinuxtype=is_selinux_type), + node=dict(name=lambda v: "/" in v, + selinuxtype=is_selinux_type, + proto=lambda v: v in ['ipv6', 'ipv4']), + login=dict(name=is_username, + selinuxuser=is_selinux_user), + user=dict(name=is_selinux_user, + roles=lambda v: all(is_selinux_user(u) + for u in " ".split(v)), + prefix=None), + interface=dict(name=None, selinuxtype=is_selinux_type), + permissive=dict(name=is_selinux_type)) + ) def Run(self): + print "checking packages\n" self.check_packages() if "Defaults" in self.core.plugins: self.logger.info("Defaults plugin enabled; skipping required " "attribute checks") else: + print "checking rules\n" self.check_rules() + print "checking bundles\n" self.check_bundles() + print 'done running RequiredAttrs' @classmethod def Errors(cls): return {"unknown-entry-type":"error", "unknown-entry-tag":"error", "required-attrs-missing":"error", + "required-attr-format":"error", "extra-attrs":"warning"} - def check_packages(self): """ check package sources for Source entries with missing attrs """ if 'Packages' in self.core.plugins: @@ -85,6 +149,7 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): """ check bundles for BoundPath entries with missing attrs """ if 'Bundler' in self.core.plugins: for bundle in self.core.plugins['Bundler'].entries.values(): + print "checking bundle %s" % bundle.name try: xdata = lxml.etree.XML(bundle.data) except (lxml.etree.XMLSyntaxError, AttributeError): @@ -103,43 +168,52 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): if tag not in self.required_attrs: self.LintError("unknown-entry-tag", "Unknown entry tag '%s': %s" % - (entry.tag, self.RenderXML(entry))) + (tag, self.RenderXML(entry))) if isinstance(self.required_attrs[tag], dict): etype = entry.get('type') if etype in self.required_attrs[tag]: - required_attrs = set(self.required_attrs[tag][etype] + - ['type']) + required_attrs = self.required_attrs[tag][etype] else: self.LintError("unknown-entry-type", "Unknown %s type %s: %s" % (tag, etype, self.RenderXML(entry))) return else: - required_attrs = set(self.required_attrs[tag]) + required_attrs = self.required_attrs[tag] attrs = set(entry.attrib.keys()) if 'dev_type' in required_attrs: dev_type = entry.get('dev_type') if dev_type in ['block', 'char']: # check if major/minor are specified - required_attrs |= set(['major', 'minor']) + required_attrs['major'] = is_device_mode + required_attrs['minor'] = is_device_mode if '__text__' in required_attrs: - required_attrs.remove('__text__') + del required_attrs['__text__'] if (not entry.text and not entry.get('empty', 'false').lower() == 'true'): self.LintError("required-attrs-missing", "Text missing for %s %s in %s: %s" % - (entry.tag, name, filename, + (tag, name, filename, self.RenderXML(entry))) - if not attrs.issuperset(required_attrs): + if not attrs.issuperset(required_attrs.keys()): self.LintError("required-attrs-missing", "The following required attribute(s) are " "missing for %s %s in %s: %s\n%s" % - (entry.tag, name, filename, + (tag, name, filename, ", ".join([attr for attr in required_attrs.difference(attrs)]), self.RenderXML(entry))) + + for attr, fmt in required_attrs.items(): + if fmt and attr in attrs and not fmt(entry.attrib[attr]): + self.LintError("required-attr-format", + "The %s attribute of %s %s in %s is " + "malformed\n%s" % + (attr, tag, name, filename, + self.RenderXML(entry))) + diff --git a/src/lib/Bcfg2/Server/Plugin.py b/src/lib/Bcfg2/Server/Plugin.py index 98e6e6f51..d035b83d4 100644 --- a/src/lib/Bcfg2/Server/Plugin.py +++ b/src/lib/Bcfg2/Server/Plugin.py @@ -32,8 +32,9 @@ encoding = encparse['encoding'] # grab default metadata info from bcfg2.conf opts = {'owner': Bcfg2.Options.MDATA_OWNER, 'group': Bcfg2.Options.MDATA_GROUP, - 'important': Bcfg2.Options.MDATA_IMPORTANT, 'perms': Bcfg2.Options.MDATA_PERMS, + 'secontext': Bcfg2.Options.MDATA_SECONTEXT, + 'important': Bcfg2.Options.MDATA_IMPORTANT, 'paranoid': Bcfg2.Options.MDATA_PARANOID, 'sensitive': Bcfg2.Options.MDATA_SENSITIVE} mdata_setup = Bcfg2.Options.OptionParser(opts) @@ -52,6 +53,7 @@ info_regex = re.compile( \ 'owner:(\s)*(?P<owner>\S+)|' + 'paranoid:(\s)*(?P<paranoid>\S+)|' + 'perms:(\s)*(?P<perms>\w+)|' + + 'secontext:(\s)*(?P<secontext>\S+)|' + 'sensitive:(\s)*(?P<sensitive>\S+)|') def bind_info(entry, metadata, infoxml=None, default=default_file_metadata): @@ -1162,13 +1164,14 @@ class GroupSpool(Plugin, Generator): filename_pattern = "" es_child_cls = object es_cls = EntrySet + entry_type = 'Path' def __init__(self, core, datastore): Plugin.__init__(self, core, datastore) Generator.__init__(self) if self.data[-1] == '/': self.data = self.data[:-1] - self.Entries['Path'] = {} + self.Entries[self.entry_type] = {} self.entries = {} self.handles = {} self.AddDirectoryMonitor('') @@ -1185,7 +1188,8 @@ class GroupSpool(Plugin, Generator): dirpath, self.es_child_cls, self.encoding) - self.Entries['Path'][ident] = self.entries[ident].bind_entry + self.Entries[self.entry_type][ident] = \ + self.entries[ident].bind_entry if not posixpath.isdir(epath): # do not pass through directory events self.entries[ident].handle_event(event) @@ -1231,7 +1235,7 @@ class GroupSpool(Plugin, Generator): if fbase in self.entries: # a directory was deleted del self.entries[fbase] - del self.Entries['Path'][fbase] + del self.Entries[self.entry_type][fbase] elif ident in self.entries: self.entries[ident].handle_event(event) elif ident not in self.entries: diff --git a/src/lib/Bcfg2/Server/Plugins/SEModules.py b/src/lib/Bcfg2/Server/Plugins/SEModules.py new file mode 100644 index 000000000..2059baf60 --- /dev/null +++ b/src/lib/Bcfg2/Server/Plugins/SEModules.py @@ -0,0 +1,46 @@ +import os +import logging +import binascii +import posixpath + +import Bcfg2.Server.Plugin +logger = logging.getLogger(__name__) + +class SEModuleData(Bcfg2.Server.Plugin.SpecificData): + def bind_entry(self, entry, _): + entry.set('encoding', 'base64') + entry.text = binascii.b2a_base64(self.data) + + +class SEModules(Bcfg2.Server.Plugin.GroupSpool): + """ Handle SELinux 'module' entries """ + name = 'SEModules' + __author__ = 'chris.a.st.pierre@gmail.com' + es_cls = Bcfg2.Server.Plugin.EntrySet + es_child_cls = SEModuleData + entry_type = 'SELinux' + experimental = True + + def _get_module_name(self, entry): + """ GroupSpool stores entries as /foo.pp, but we want people + to be able to specify module entries as name='foo' or + name='foo.pp', so we put this abstraction in between """ + if entry.get("name").endswith(".pp"): + name = entry.get("name") + else: + name = entry.get("name") + ".pp" + return "/" + name + + def HandlesEntry(self, entry, metadata): + if entry.tag in self.Entries and entry.get('type') == 'module': + return self._get_module_name(entry) in self.Entries[entry.tag] + return Bcfg2.Server.Plugin.GroupSpool.HandlesEntry(self, entry, + metadata) + + def HandleEntry(self, entry, metadata): + entry.set("name", self._get_module_name(entry)) + return self.Entries[entry.tag][name](entry, metadata) + + def add_entry(self, event): + self.filename_pattern = os.path.basename(event.filename) + Bcfg2.Server.Plugin.GroupSpool.add_entry(self, event) |