gpo: Move the file parse function to gp_ext
[nivanova/samba-autobuild/.git] / python / samba / gpclass.py
index ae06c5b361d3207ccd18e44d24bfaf7ef31d3083..7f7392d80927b0aecf224021ddd6eeb9adbc4ed4 100644 (file)
@@ -19,23 +19,26 @@ import sys
 import os
 import tdb
 sys.path.insert(0, "bin/python")
-import samba.gpo as gpo
-import optparse
-import ldb
-from samba.auth import system_session
-import samba.getopt as options
-from samba.samdb import SamDB
-from samba.netcmd import gpo as gpo_user
-import codecs
 from samba import NTSTATUSError
 from ConfigParser import ConfigParser
 from StringIO import StringIO
 from abc import ABCMeta, abstractmethod
 import xml.etree.ElementTree as etree
+import re
+
+try:
+    from enum import Enum
+    GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
+except ImportError:
+    class GPOSTATE:
+        APPLY = 1
+        ENFORCE = 2
+        UNAPPLY = 3
 
 class gp_log:
     ''' Log settings overwritten by gpo apply
-    The gp_log is an xml file that stores a history of gpo changes (and the original setting value).
+    The gp_log is an xml file that stores a history of gpo changes (and the
+    original setting value).
 
     The log is organized like so:
 
@@ -60,88 +63,129 @@ class gp_log:
     </user>
 </gp>
 
-    Each guid value contains a list of extensions, which contain a list of attributes. The guid value
-    represents a GPO. The attributes are the values of those settings prior to the application of
-    the GPO.
-    The list of guids is enclosed within a user name, which represents the user the settings were
-    applied to. This user may be the samaccountname of the local computer, which implies that these
-    are machine policies.
-    The applylog keeps track of the order in which the GPOs were applied, so that they can be rolled
-    back in reverse, returning the machine to the state prior to policy application.
+    Each guid value contains a list of extensions, which contain a list of
+    attributes. The guid value represents a GPO. The attributes are the values
+    of those settings prior to the application of the GPO.
+    The list of guids is enclosed within a user name, which represents the user
+    the settings were applied to. This user may be the samaccountname of the
+    local computer, which implies that these are machine policies.
+    The applylog keeps track of the order in which the GPOs were applied, so
+    that they can be rolled back in reverse, returning the machine to the state
+    prior to policy application.
     '''
     def __init__(self, user, gpostore, db_log=None):
         ''' Initialize the gp_log
-        param user          - the username (or machine name) that policies are being applied to
-        param gpostore      - the GPOStorage obj which references the tdb which contains gp_logs
+        param user          - the username (or machine name) that policies are
+                              being applied to
+        param gpostore      - the GPOStorage obj which references the tdb which
+                              contains gp_logs
         param db_log        - (optional) a string to initialize the gp_log
         '''
+        self._state = GPOSTATE.APPLY
         self.gpostore = gpostore
         self.username = user
         if db_log:
             self.gpdb = etree.fromstring(db_log)
         else:
             self.gpdb = etree.Element('gp')
-        self.user = self.gpdb.find('user[@name="%s"]' % user)
-        if self.user is None:
-            self.user = etree.SubElement(self.gpdb, 'user')
-            self.user.attrib['name'] = user
+        self.user = user
+        user_obj = self.gpdb.find('user[@name="%s"]' % user)
+        if user_obj is None:
+            user_obj = etree.SubElement(self.gpdb, 'user')
+            user_obj.attrib['name'] = user
+
+    def state(self, value):
+        ''' Policy application state
+        param value         - APPLY, ENFORCE, or UNAPPLY
+
+        The behavior of the gp_log depends on whether we are applying policy,
+        enforcing policy, or unapplying policy. During an apply, old settings
+        are recorded in the log. During an enforce, settings are being applied
+        but the gp_log does not change. During an unapply, additions to the log
+        should be ignored (since function calls to apply settings are actually
+        reverting policy), but removals from the log are allowed.
+        '''
+        # If we're enforcing, but we've unapplied, apply instead
+        if value == GPOSTATE.ENFORCE:
+            user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
+            apply_log = user_obj.find('applylog')
+            if apply_log is None or len(apply_log) == 0:
+                self._state = GPOSTATE.APPLY
+            else:
+                self._state = value
+        else:
+            self._state = value
 
     def set_guid(self, guid):
         ''' Log to a different GPO guid
-        param guid          - guid value of the GPO from which we're applying policy
+        param guid          - guid value of the GPO from which we're applying
+                              policy
         '''
-        self.guid = self.user.find('guid[@value="%s"]' % guid)
-        if self.guid is None:
-            self.guid = etree.SubElement(self.user, 'guid')
-            self.guid.attrib['value'] = guid
-        apply_log = self.user.find('applylog')
-        if apply_log is None:
-            apply_log = etree.SubElement(self.user, 'applylog')
-        item = etree.SubElement(apply_log, 'guid')
-        item.attrib['count'] = '%d' % (len(apply_log)-1)
-        item.attrib['value'] = guid
+        self.guid = guid
+        user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
+        obj = user_obj.find('guid[@value="%s"]' % guid)
+        if obj is None:
+            obj = etree.SubElement(user_obj, 'guid')
+            obj.attrib['value'] = guid
+        if self._state == GPOSTATE.APPLY:
+            apply_log = user_obj.find('applylog')
+            if apply_log is None:
+                apply_log = etree.SubElement(user_obj, 'applylog')
+            item = etree.SubElement(apply_log, 'guid')
+            item.attrib['count'] = '%d' % (len(apply_log)-1)
+            item.attrib['value'] = guid
 
     def apply_log_pop(self):
         ''' Pop a GPO guid from the applylog
         return              - last applied GPO guid
 
-        Removes the GPO guid last added to the list, which is the most recently applied GPO.
+        Removes the GPO guid last added to the list, which is the most recently
+        applied GPO.
         '''
-        apply_log = self.user.find('applylog')
+        user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
+        apply_log = user_obj.find('applylog')
         if apply_log is not None:
             ret = apply_log.find('guid[@count="%d"]' % (len(apply_log)-1))
             if ret is not None:
                 apply_log.remove(ret)
                 return ret.attrib['value']
-            if len(apply_log) == 0 and apply_log in self.user:
-                self.user.remove(apply_log)
+            if len(apply_log) == 0 and apply_log in user_obj:
+                user_obj.remove(apply_log)
         return None
 
     def store(self, gp_ext_name, attribute, old_val):
         ''' Store an attribute in the gp_log
         param gp_ext_name   - Name of the extension applying policy
         param attribute     - The attribute being modified
-        param old_val       - The value of the attribute prior to policy application
+        param old_val       - The value of the attribute prior to policy
+                              application
         '''
-        assert self.guid is not None, "gpo guid was not set"
-        ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
+        if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
+            return None
+        user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
+        guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
+        assert guid_obj is not None, "gpo guid was not set"
+        ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
         if ext is None:
-            ext = etree.SubElement(self.guid, 'gp_ext')
+            ext = etree.SubElement(guid_obj, 'gp_ext')
             ext.attrib['name'] = gp_ext_name
         attr = ext.find('attribute[@name="%s"]' % attribute)
         if attr is None:
             attr = etree.SubElement(ext, 'attribute')
             attr.attrib['name'] = attribute
-        attr.text = old_val
+            attr.text = old_val
 
     def retrieve(self, gp_ext_name, attribute):
         ''' Retrieve a stored attribute from the gp_log
         param gp_ext_name   - Name of the extension which applied policy
         param attribute     - The attribute being retrieved
-        return              - The value of the attribute prior to policy application
+        return              - The value of the attribute prior to policy
+                              application
         '''
-        assert self.guid is not None, "gpo guid was not set"
-        ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
+        user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
+        guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
+        assert guid_obj is not None, "gpo guid was not set"
+        ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
         if ext is not None:
             attr = ext.find('attribute[@name="%s"]' % attribute)
             if attr is not None:
@@ -149,44 +193,57 @@ class gp_log:
         return None
 
     def list(self, gp_extensions):
-        ''' Return a list of attributes, their previous values, and functions to set them
-        param gp_extensions - list of extension objects, for retrieving attr to func mappings
-        return              - list of (attr, value, apply_func) tuples for unapplying policy
+        ''' Return a list of attributes, their previous values, and functions
+            to set them
+        param gp_extensions - list of extension objects, for retrieving attr to
+                              func mappings
+        return              - list of (attr, value, apply_func) tuples for
+                              unapplying policy
         '''
-        assert self.guid is not None, "gpo guid was not set"
+        user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
+        guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
+        assert guid_obj is not None, "gpo guid was not set"
         ret = []
         data_maps = {}
         for gp_ext in gp_extensions:
             data_maps.update(gp_ext.apply_map())
-        exts = self.guid.findall('gp_ext')
+        exts = guid_obj.findall('gp_ext')
         if exts is not None:
             for ext in exts:
-                ext_map = {val[0]: val[1] for (key, val) in data_maps[ext.attrib['name']].items()}
                 attrs = ext.findall('attribute')
                 for attr in attrs:
-                    ret.append((attr.attrib['name'], attr.text, ext_map[attr.attrib['name']]))
+                    func = None
+                    if attr.attrib['name'] in data_maps[ext.attrib['name']]:
+                        func = data_maps[ext.attrib['name']]\
+                               [attr.attrib['name']][-1]
+                    else:
+                        for dmap in data_maps[ext.attrib['name']].keys():
+                            if data_maps[ext.attrib['name']][dmap][0] == \
+                               attr.attrib['name']:
+                                func = data_maps[ext.attrib['name']][dmap][-1]
+                                break
+                    ret.append((attr.attrib['name'], attr.text, func))
         return ret
 
     def delete(self, gp_ext_name, attribute):
         ''' Remove an attribute from the gp_log
-        param gp_ext_name   - name of extension from which to remove the attribute
+        param gp_ext_name   - name of extension from which to remove the
+                              attribute
         param attribute     - attribute to remove
         '''
-        assert self.guid is not None, "gpo guid was not set"
-        ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
+        user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
+        guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
+        assert guid_obj is not None, "gpo guid was not set"
+        ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
         if ext is not None:
             attr = ext.find('attribute[@name="%s"]' % attribute)
             if attr is not None:
                 ext.remove(attr)
                 if len(ext) == 0:
-                    self.guid.remove(ext)
+                    guid_obj.remove(ext)
 
     def commit(self):
         ''' Write gp_log changes to disk '''
-        if len(self.guid) == 0 and self.guid in self.user:
-            self.user.remove(self.guid)
-        if len(self.user) == 0 and self.user in self.gpdb:
-            self.gpdb.remove(self.user)
         self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
 
 class GPOStorage:
@@ -229,6 +286,9 @@ class GPOStorage:
 class gp_ext(object):
     __metaclass__ = ABCMeta
 
+    def __init__(self, logger):
+        self.logger = logger
+
     @abstractmethod
     def list(self, rootpath):
         pass
@@ -238,14 +298,41 @@ class gp_ext(object):
         pass
 
     @abstractmethod
-    def parse(self, afile, ldb, conn, gp_db, lp):
+    def read(self, policy):
         pass
 
+    def parse(self, afile, ldb, conn, gp_db, lp):
+        self.ldb = ldb
+        self.gp_db = gp_db
+        self.lp = lp
+
+        # Fixing the bug where only some Linux Boxes capitalize MACHINE
+        try:
+            blist = afile.split('/')
+            idx = afile.lower().split('/').index('machine')
+            for case in [
+                            blist[idx].upper(),
+                            blist[idx].capitalize(),
+                            blist[idx].lower()
+                        ]:
+                bfile = '/'.join(blist[:idx]) + '/' + case + '/' + \
+                    '/'.join(blist[idx+1:])
+                try:
+                    return self.read(conn.loadfile(bfile.replace('/', '\\')))
+                except NTSTATUSError:
+                    continue
+        except ValueError:
+            try:
+                return self.read(conn.loadfile(afile.replace('/', '\\')))
+            except Exception as e:
+                self.logger.error(str(e))
+                return None
+
     @abstractmethod
     def __str__(self):
         pass
 
-class inf_to():
+class gp_ext_setter():
     __metaclass__ = ABCMeta
 
     def __init__(self, logger, ldb, gp_db, lp, attribute, val):
@@ -271,37 +358,71 @@ class inf_to():
     def __str__(self):
         pass
 
-class inf_to_ldb(inf_to):
-    '''This class takes the .inf file parameter (essentially a GPO file mapped to a GUID),
-    hashmaps it to the Samba parameter, which then uses an ldb object to update the
-    parameter to Samba4. Not registry oriented whatsoever.
+class inf_to_kdc_tdb(gp_ext_setter):
+    def mins_to_hours(self):
+        return '%d' % (int(self.val)/60)
+
+    def days_to_hours(self):
+        return '%d' % (int(self.val)*24)
+
+    def set_kdc_tdb(self, val):
+        old_val = self.gp_db.gpostore.get(self.attribute)
+        self.logger.info('%s was changed from %s to %s' % (self.attribute,
+                                                           old_val, val))
+        if val is not None:
+            self.gp_db.gpostore.store(self.attribute, val)
+            self.gp_db.store(str(self), self.attribute, old_val)
+        else:
+            self.gp_db.gpostore.delete(self.attribute)
+            self.gp_db.delete(str(self), self.attribute)
+
+    def mapper(self):
+        return { 'kdc:user_ticket_lifetime': (self.set_kdc_tdb, self.explicit),
+                 'kdc:service_ticket_lifetime': (self.set_kdc_tdb,
+                                                 self.mins_to_hours),
+                 'kdc:renewal_lifetime': (self.set_kdc_tdb,
+                                          self.days_to_hours),
+               }
+
+    def __str__(self):
+        return 'Kerberos Policy'
+
+class inf_to_ldb(gp_ext_setter):
+    '''This class takes the .inf file parameter (essentially a GPO file mapped
+    to a GUID), hashmaps it to the Samba parameter, which then uses an ldb
+    object to update the parameter to Samba4. Not registry oriented whatsoever.
     '''
 
     def ch_minPwdAge(self, val):
         old_val = self.ldb.get_minPwdAge()
-        self.logger.info('KDC Minimum Password age was changed from %s to %s' % (old_val, val))
+        self.logger.info('KDC Minimum Password age was changed from %s to %s' \
+                         % (old_val, val))
         self.gp_db.store(str(self), self.attribute, old_val)
         self.ldb.set_minPwdAge(val)
 
     def ch_maxPwdAge(self, val):
         old_val = self.ldb.get_maxPwdAge()
-        self.logger.info('KDC Maximum Password age was changed from %s to %s' % (old_val, val))
+        self.logger.info('KDC Maximum Password age was changed from %s to %s' \
+                         % (old_val, val))
         self.gp_db.store(str(self), self.attribute, old_val)
         self.ldb.set_maxPwdAge(val)
 
     def ch_minPwdLength(self, val):
         old_val = self.ldb.get_minPwdLength()
-        self.logger.info('KDC Minimum Password length was changed from %s to %s' % (old_val, val))
+        self.logger.info(
+            'KDC Minimum Password length was changed from %s to %s' \
+             % (old_val, val))
         self.gp_db.store(str(self), self.attribute, old_val)
         self.ldb.set_minPwdLength(val)
 
     def ch_pwdProperties(self, val):
         old_val = self.ldb.get_pwdProperties()
-        self.logger.info('KDC Password Properties were changed from %s to %s' % (old_val, val))
+        self.logger.info('KDC Password Properties were changed from %s to %s' \
+                         % (old_val, val))
         self.gp_db.store(str(self), self.attribute, old_val)
         self.ldb.set_pwdProperties(val)
 
-    def nttime2unix(self):
+    def days2rel_nttime(self):
         seconds = 60
         minutes = 60
         hours = 24
@@ -312,9 +433,10 @@ class inf_to_ldb(inf_to):
 
     def mapper(self):
         '''ldap value : samba setter'''
-        return { "minPwdAge" : (self.ch_minPwdAge, self.nttime2unix),
-                 "maxPwdAge" : (self.ch_maxPwdAge, self.nttime2unix),
-                 # Could be none, but I like the method assignment in update_samba
+        return { "minPwdAge" : (self.ch_minPwdAge, self.days2rel_nttime),
+                 "maxPwdAge" : (self.ch_maxPwdAge, self.days2rel_nttime),
+                 # Could be none, but I like the method assignment in
+                 # update_samba
                  "minPwdLength" : (self.ch_minPwdLength, self.explicit),
                  "pwdProperties" : (self.ch_pwdProperties, self.explicit),
 
@@ -332,14 +454,12 @@ class gp_sec_ext(gp_ext):
 
     count = 0
 
-    def __init__(self, logger):
-        self.logger = logger
-
     def __str__(self):
         return "Security GPO extension"
 
     def list(self, rootpath):
-        return os.path.join(rootpath, "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
+        return os.path.join(rootpath,
+                            "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
 
     def listmachpol(self, rootpath):
         return os.path.join(rootpath, "Machine/Registry.pol")
@@ -348,18 +468,34 @@ class gp_sec_ext(gp_ext):
         return os.path.join(rootpath, "User/Registry.pol")
 
     def apply_map(self):
-        return {"System Access": {"MinimumPasswordAge": ("minPwdAge", inf_to_ldb),
-                                  "MaximumPasswordAge": ("maxPwdAge", inf_to_ldb),
-                                  "MinimumPasswordLength": ("minPwdLength", inf_to_ldb),
-                                  "PasswordComplexity": ("pwdProperties", inf_to_ldb),
-                                 }
+        return {"System Access": {"MinimumPasswordAge": ("minPwdAge",
+                                                         inf_to_ldb),
+                                  "MaximumPasswordAge": ("maxPwdAge",
+                                                         inf_to_ldb),
+                                  "MinimumPasswordLength": ("minPwdLength",
+                                                            inf_to_ldb),
+                                  "PasswordComplexity": ("pwdProperties",
+                                                         inf_to_ldb),
+                                 },
+                "Kerberos Policy": {"MaxTicketAge": (
+                                        "kdc:user_ticket_lifetime",
+                                        inf_to_kdc_tdb
+                                    ),
+                                    "MaxServiceAge": (
+                                        "kdc:service_ticket_lifetime",
+                                        inf_to_kdc_tdb
+                                    ),
+                                    "MaxRenewAge": (
+                                        "kdc:renewal_lifetime",
+                                        inf_to_kdc_tdb
+                                    ),
+                                   }
                }
 
-    def read_inf(self, path, conn):
+    def read(self, policy):
         ret = False
         inftable = self.apply_map()
 
-        policy = conn.loadfile(path.replace('/', '\\'))
         current_section = None
 
         # So here we would declare a boolean,
@@ -384,29 +520,8 @@ class gp_sec_ext(gp_ext):
                     (att, setter) = current_section.get(key)
                     value = value.encode('ascii', 'ignore')
                     ret = True
-                    setter(self.logger, self.ldb, self.gp_db, self.lp, att, value).update_samba()
+                    setter(self.logger, self.ldb, self.gp_db, self.lp, att,
+                           value).update_samba()
                     self.gp_db.commit()
         return ret
 
-    def parse(self, afile, ldb, conn, gp_db, lp):
-        self.ldb = ldb
-        self.gp_db = gp_db
-        self.lp = lp
-
-        # Fixing the bug where only some Linux Boxes capitalize MACHINE
-        if afile.endswith('inf'):
-            try:
-                blist = afile.split('/')
-                idx = afile.lower().split('/').index('machine')
-                for case in [blist[idx].upper(), blist[idx].capitalize(), blist[idx].lower()]:
-                    bfile = '/'.join(blist[:idx]) + '/' + case + '/' + '/'.join(blist[idx+1:])
-                    try:
-                        return self.read_inf(bfile, conn)
-                    except NTSTATUSError:
-                        continue
-            except ValueError:
-                try:
-                    return self.read_inf(afile, conn)
-                except:
-                    return None
-