validator.py is a tool to validate kerb configuration files.
authorZhanna Tsitkov <tsitkova@mit.edu>
Wed, 17 Feb 2010 16:09:43 +0000 (16:09 +0000)
committerZhanna Tsitkov <tsitkova@mit.edu>
Wed, 17 Feb 2010 16:09:43 +0000 (16:09 +0000)
First, the configuration file is parsed (confparser.py) and validated against formating errors (such as mismatching brackets)
Then the list of the allowed configuration attributes is compiled from k5-int.h and rules.yml
Finally, the kerb configuration file is validated against the list of the allowed strings.
If the error, or something that validator does not understand, is found the warning is issued in the tree-like form indicating the layer where the problem has occurred.

git-svn-id: svn://anonsvn.mit.edu/krb5/trunk@23732 dc483132-0cff-0310-8789-dd5450dbe970

src/util/confvalidator/README [new file with mode: 0644]
src/util/confvalidator/confparser.py [new file with mode: 0644]
src/util/confvalidator/rules.yml [new file with mode: 0644]
src/util/confvalidator/validator.conf [new file with mode: 0644]
src/util/confvalidator/validator.py [new file with mode: 0644]

diff --git a/src/util/confvalidator/README b/src/util/confvalidator/README
new file mode 100644 (file)
index 0000000..7bf7a10
--- /dev/null
@@ -0,0 +1,25 @@
+validator.py is a command line tool for identifying invalid attributes, values and some formating problems in Kerberos configuration files.
+The list of the valid attributes is created based on the “configuration variables” section in k5-int.h and user defined attributes from the rules file.
+
+Usage:
+
+validator.py path [-d defPath] [-r rulesPath] [-c validatorConfPath]
+
+Options:
+
+path – the path to the configuration file to validate
+
+-d defPath – path to the k5-int.h file. Starting from the 1.7 release this header holds the profile attribute names in the form #define KRB5_CONF_xxx ”ZZZ”.
+
+-r rulesPath - path the rules file in yaml format. It may be used to manage the list of the valid attributes and to define the additional validation rules.
+
+-c validatorConfPath – the same as -r and -d options, but in validator configuration file format.
+
+Example:
+
+python validator.py src/config-files/krb5.conf -r rules.yml -d src/include/k5-int.h
+or
+python validator.py src/config-files/krb5.conf -c validator.conf
+
+For more details please refer to the sample files validator.conf and rules.yml
+
diff --git a/src/util/confvalidator/confparser.py b/src/util/confvalidator/confparser.py
new file mode 100644 (file)
index 0000000..2fea142
--- /dev/null
@@ -0,0 +1,144 @@
+'''
+Created on Jan 31, 2010
+
+@author: tsitkova
+'''
+import re
+import copy
+import yaml
+
+class ConfParser(object):
+    def __init__(self, path):
+        self.configuration = self._parse(path)
+    
+    def walk(self):
+        for trio in self._walk(self.configuration):
+            yield trio
+    
+    def _parse(self, path):
+        comment_pattern = re.compile(r'(\s*[#].*)')
+        section_pattern = re.compile(r'^\s*\[(?P<section>\w+)\]\s+$')
+        empty_pattern = re.compile(r'^\s*$')
+        equalsign_pattern = re.compile(r'=')
+        
+        section = None
+        parser_stack = list()
+        result = dict()
+        value = None
+        f = open(path, 'r')
+        for (ln,line) in enumerate(f):
+            line = comment_pattern.sub('', line)
+            line = equalsign_pattern.sub(' = ',line,count=1)
+            if empty_pattern.match(line) is not None:
+                continue
+            m = section_pattern.match(line)
+            if m is not None:
+                section = m.group('section')
+                value = dict()
+                result[section] = value
+                continue
+            if section is None:
+                msg = 'Failed to determine section for line #%i' % ln
+                raise ValueError(msg)
+            try:
+                value = self._parseLine(value, line, parser_stack)
+            except:
+                print 'Error while parsing line %i: %s' % (ln+1, line)
+                raise
+        f.close()
+
+        if len(parser_stack):
+            raise 'Parsing error.'
+        
+        return result
+
+    def _parseLine(self, value, content, stack):
+        token_pattern = re.compile(r'(?P<token>\S+)(?=\s+)')
+        attr = None
+        token_stack = list()
+        
+        for m in token_pattern.finditer(content):
+            token = m.group('token')
+            if not self._validate(token):
+                raise ValueError('Invalid token %s' % token)
+            if token == '=':
+                if len(token_stack) == 0:
+                    raise ValueError('Failed to find attribute.')
+                elif len(token_stack) == 1:
+                    attr = token_stack.pop()
+                else:
+                    value[attr] = token_stack[:-1]
+                    attr = token_stack[-1]
+                    token_stack = list()
+            elif token == '{':
+                if attr is None:
+                    raise ValueError('Failed to find attribute.')
+                stack.append((attr,value))
+                value = dict()
+            elif token == '}':
+                if len(stack) == 0:
+                    raise ValueError('Failed to parse: unbalanced braces')
+                if len(token_stack):
+                    if attr is None:
+                        raise ValueError('Missing attribute')
+                    value[attr] = token_stack
+                    attr = None
+                    token_stack = list()
+                (attr,parent_value) = stack.pop()                                
+                parent_value[attr] = value
+                value = parent_value
+            else:
+                token_stack.append(token)
+        if len(token_stack):
+            if attr is None:
+                raise ValueError('Missing attribute')
+            value[attr] =  token_stack
+                
+        return value
+
+    def _validate(self, token):
+        result = True
+        for s in ['{','}']:
+            if s in token and s != token:
+                result = False
+        
+        return result
+
+    def _walk(self, parsedData, path='root'):
+        dirs = list()
+        av = list()
+        for (key, value) in parsedData.iteritems():
+            if type(value) == dict:
+                new_path = path + '.' + key
+                for trio in self._walk(value, new_path):
+                    yield trio
+                dirs.append(key)
+            else:
+                av.append((key,value))
+        yield (path, dirs, av)
+
+
+
+class ConfParserTest(ConfParser):
+    def __init__(self):
+        self.conf_path = '../tests/krb5.conf'
+        super(ConfParserTest, self).__init__(self.conf_path)
+
+    def run_tests(self):
+        self._test_walk()
+            
+    def _test_parse(self):
+        result = self._parse(self.conf_path)
+        print yaml.dump(result)
+        
+    def _test_walk(self):
+        configuration = self._parse(self.conf_path)
+        for (path,dirs,av) in self.walk():
+            print path,dirs,av
+
+
+
+        
+if __name__ == '__main__':
+    tester = ConfParserTest()
+    tester.run_tests()
diff --git a/src/util/confvalidator/rules.yml b/src/util/confvalidator/rules.yml
new file mode 100644 (file)
index 0000000..c6ccc89
--- /dev/null
@@ -0,0 +1,13 @@
+# Extend the list of the allowed enctypes and salts as needed
+Types:
+    supported_enctypes:
+        '(aes256-cts-hmac-sha1-96|aes256-cts|aes128-cts-hmac-sha1-96|aes128-cts|des3-hmac-sha1|des3-cbc-raw|des3-cbc-sha1|des3-hmac-sha1|rc4-hmac|arcfour-hmac-md5)(:(normal|v4))?$'
+    default_tgs_enctypes:
+        '(aes256-cts-hmac-sha1-96|aes256-cts|aes128-cts-hmac-sha1-96|aes128-cts|des3-hmac-sha1|des3-cbc-raw|des3-cbc-sha1|des3-hmac-sha1|rc4-hmac|arcfour-hmac-md5)'
+    default_tkt_enctypes:
+        '(aes256-cts-hmac-sha1-96|aes256-cts|aes128-cts-hmac-sha1-96|aes128-cts|des3-hmac-sha1|des3-cbc-raw|des3-cbc-sha1|des3-hmac-sha1|rc4-hmac|arcfour-hmac-md5)'
+
+# Add all valid profile attributes that are not listed in k5-int.h 
+Attributes:
+    - logging
+    - dbmodules
diff --git a/src/util/confvalidator/validator.conf b/src/util/confvalidator/validator.conf
new file mode 100644 (file)
index 0000000..71e205c
--- /dev/null
@@ -0,0 +1,2 @@
+RulesPath=./rules.yml
+HfilePath=../../include/k5-int.h
diff --git a/src/util/confvalidator/validator.py b/src/util/confvalidator/validator.py
new file mode 100644 (file)
index 0000000..d739bc0
--- /dev/null
@@ -0,0 +1,194 @@
+'''
+Created on Jan 25, 2010
+
+@author: tsitkova
+'''
+import os
+import sys
+import re
+import yaml
+from optparse import OptionParser
+from confparser import ConfParser
+
+class Rule(object):
+    def __init__(self):
+        pass
+    
+    def validate(self,node):
+        (path,dirs,avs) = node        
+
+
+class Validator(object):
+    def __init__(self, kerberosPath, confPath=None, rulesPath=None, hfilePath=None):
+        self.parser = ConfParser(kerberosPath)
+        if confPath is not None:
+            content = self._readConfigFile(confPath)
+            rulesPath = content['RulesPath']
+            hfilePath = content['HfilePath']
+        if rulesPath is not None and hfilePath is not None:    
+            self.rules = self._loadRules(rulesPath)
+            self.validKeys = SupportedKeys(hfilePath).validKeys.union(self.rules['Attributes'])
+        else:
+            raise ValueError('Invalid arguments for validator: no path to rules and definition files')
+        
+        self._attribute_pattern = re.compile(r'^\w+$')
+        self._lowercase_pattern = re.compile(r'[a-z]')
+
+    def _readConfigFile(self,path):
+        f = open(path)
+        result = dict()
+        for line in f:
+            line = line.rstrip()
+            fields = line.split('=')
+            result[fields[0]] = fields[1]
+        
+        return result
+    
+    def _loadRules(self, path):
+        f = open(path)
+        rules = yaml.load(f)
+        f.close()
+        
+        return rules
+        
+    def validate(self):
+        typeInfo = self.rules['Types']
+        
+        for node in self.parser.walk():
+            self._validateTypes(node, typeInfo)
+            self._validateAttrubutes(node, self.validKeys)
+            # self._validateRealm(node)
+
+
+    def _validateTypes(self, node, typeInfo):
+        (path, dirs, avs) = node
+        for (key, value) in avs:
+            valid_type_pattern = typeInfo.get(key)
+            if valid_type_pattern is not None:
+                for t in value:
+                    if re.match(valid_type_pattern, t) is None:
+                        print 'Wrong type %s for attribute %s.%s' % (t,path,key)
+                        
+    def _validateAttrubutes(self, node, validKeys):
+        (path, dirs, avs) = node 
+        attributes = list()
+        for attr in dirs:
+            if self._attribute_pattern.match(attr) is not None:
+                attributes.append(attr)
+        for (attr, value) in avs:
+            if self._attribute_pattern.match(attr) is not None:
+                attributes.append(attr)
+
+        for attr in attributes:
+            if attr not in validKeys:
+                print 'Unrecognized attribute %s at %s' % (attr, path)
+
+#    def _validateRealm(self, node):
+#        (path, dirs, avs) = node 
+#        if path == 'root.realms':
+#            for attr in dirs:
+#                if self._lowercase_pattern.search(attr) is not None:
+#                    print 'Lower case letter in realm attribute: %s at %s' % (attr, path)
+
+class SupportedKeys(object):
+    def __init__(self, path):
+        self.validKeys = self.getKeysFromHfile(path)
+    
+    def getKeysFromHfile(self, path):
+        pattern = re.compile(r'^[#]define KRB5_CONF_\w+\s+["](\w+)["]')
+        f = open(path)
+        result = set()
+        for l in f:
+            l = l.rstrip()
+            m = pattern.match(l)
+            if m is not None:
+                result.add(m.groups()[0])
+        f.close()
+        
+        return result
+
+    
+class ValidatorTest(Validator):
+    def __init__(self):
+        self.kerberosPath = '../tests/kdc1.conf'
+        self.rulesPath = '../tests/rules.yml'
+        self.hfilePath = '../tests/k5-int.h'
+        self.confPath = '../tests/validator.conf'
+
+        super(ValidatorTest, self).__init__(self.kerberosPath, 
+                                            rulesPath=self.rulesPath, 
+                                            hfilePath=self.hfilePath)
+
+    def run_tests(self):
+        self._test_validate()
+    
+    def _test__loadRules(self):
+        result = self._loadRules(self.rulesPath)
+        print result
+        
+    def _test_validate(self):
+        self.validate()
+        
+    def _test__readConfigFile(self):
+        result = self._readConfigFile(self.confPath)
+        print result
+
+class SupportedKeysTest(SupportedKeys):
+    def __init__(self):
+        self.path = '../tests/k5-int.h'
+        
+    def run_tests(self):
+        self._test_getKeysFromHFile()
+    
+    def _test_getKeysFromHFile(self):
+        result = set()
+        krb5keys = self.getKeysFromHfile(self.path)
+        for key in krb5keys:
+            print key
+            result.update(key)
+        print len(krb5keys)  
+        
+        return result
+    
+def _test():        
+    tester = ValidatorTest()
+    krb5keys = tester.run_tests()
+                    
+if __name__ == '__main__':
+    TEST = False
+    if TEST:
+        _test()
+        sys.exit()
+    
+    
+    usage = "\n\t%prog path [-d defPath] [-r rulesPath] [-c validatorConfPath]"
+    description = 'Description: validates kerberos configuration file'
+    parser = OptionParser(usage = usage, description = description)
+    parser.add_option("-c", dest="confPath",
+                      help='path to validator config file')
+    parser.add_option("-d", dest="hfilePath",
+                      help='path to h-file with attribute definition')
+    parser.add_option("-r", dest="rulesPath",
+                      help='path to file with validation rules')
+    (options, args) = parser.parse_args()
+
+    if len(args) != 1 and len(sys.argv) <= 3:
+        print '\n%s' % parser.get_usage()
+        sys.exit()
+
+    validator = None
+    if options.confPath is not None:
+        validator = Validator(args[0], confPath=options.confPath)
+    elif options.hfilePath is not None and options.rulesPath is not None:
+        validator = Validator(args[0], hfilePath=options.hfilePath, rulesPath=options.rulesPath)
+    else:
+        print '\nMust specify either configuration file or paths to rules and definitions files'
+        print '%s' % parser.get_usage()
+        sys.exit()
+    
+    validator.validate()
+        
+
+
+        
+