1
0
mirror of https://github.com/samba-team/samba.git synced 2025-08-02 00:22:11 +03:00

gpo: Add GPO unapply

Keep a log of applied settings, and add an option to samba_gpoupdate to allow unapply. An unapply will revert settings to a state prior to any policy application.

Signed-off-by: David Mulder <dmulder@suse.com>
Reviewed-by: Garming Sam <garming@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
This commit is contained in:
David Mulder
2017-06-08 11:47:57 -06:00
committed by Garming Sam
parent e750e4a35f
commit 8d4c7229e9
2 changed files with 278 additions and 59 deletions

View File

@ -31,30 +31,200 @@ from samba import NTSTATUSError
from ConfigParser import ConfigParser
from StringIO import StringIO
from abc import ABCMeta, abstractmethod
import xml.etree.ElementTree as etree
class Backlog:
def __init__(self, sysvol_log):
if os.path.isfile(sysvol_log):
self.backlog = tdb.open(sysvol_log)
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 log is organized like so:
<gp>
<user name="KDC-1$">
<applylog>
<guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
</applylog>
<guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
<gp_ext name="System Access">
<attribute name="minPwdAge">-864000000000</attribute>
<attribute name="maxPwdAge">-36288000000000</attribute>
<attribute name="minPwdLength">7</attribute>
<attribute name="pwdProperties">1</attribute>
</gp_ext>
<gp_ext name="Kerberos Policy">
<attribute name="ticket_lifetime">1d</attribute>
<attribute name="renew_lifetime" />
<attribute name="clockskew">300</attribute>
</gp_ext>
</guid>
</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.
'''
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 db_log - (optional) a string to initialize the gp_log
'''
self.gpostore = gpostore
self.username = user
if db_log:
self.gpdb = etree.fromstring(db_log)
else:
self.backlog = tdb.Tdb(sysvol_log, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
self.backlog.transaction_start()
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
def version(self, guid):
try:
old_version = int(self.backlog.get(guid))
except TypeError:
old_version = -1
return old_version
def set_guid(self, guid):
''' Log to a different GPO guid
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
def store(self, guid, version):
self.backlog.store(guid, '%i' % version)
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.
'''
apply_log = self.user.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)
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
'''
assert self.guid is not None, "gpo guid was not set"
ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
if ext is None:
ext = etree.SubElement(self.guid, '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
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
'''
assert self.guid is not None, "gpo guid was not set"
ext = self.guid.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:
return attr.text
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
'''
assert self.guid 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')
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']]))
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 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)
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)
def commit(self):
self.backlog.transaction_commit()
''' 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:
def __init__(self, log_file):
if os.path.isfile(log_file):
self.log = tdb.open(log_file)
else:
self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
def start(self):
self.log.transaction_start()
def get_int(self, key):
try:
return int(self.log.get(key))
except TypeError:
return None
def get(self, key):
return self.log.get(key)
def get_gplog(self, user):
return gp_log(user, self, self.log.get(user))
def store(self, key, val):
self.log.store(key, val)
def cancel(self):
self.log.transaction_cancel()
def delete(self, key):
self.log.delete(key)
def commit(self):
self.log.transaction_commit()
def __del__(self):
self.backlog.close()
self.log.close()
class gp_ext(object):
__metaclass__ = ABCMeta
@ -64,23 +234,27 @@ class gp_ext(object):
pass
@abstractmethod
def parse(self, afile, ldb, conn, lp):
def apply_map(self):
pass
@abstractmethod
def parse(self, afile, ldb, conn, gp_db, lp):
pass
@abstractmethod
def __str__(self):
pass
class inf_to():
__metaclass__ = ABCMeta
def __init__(self, logger, ldb, lp, attribute, val):
def __init__(self, logger, ldb, gp_db, lp, attribute, val):
self.logger = logger
self.ldb = ldb
self.attribute = attribute
self.val = val
self.lp = lp
self.gp_db = gp_db
def explicit(self):
return self.val
@ -93,6 +267,10 @@ class inf_to():
def mapper(self):
pass
@abstractmethod
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
@ -100,19 +278,27 @@ class inf_to_ldb(inf_to):
'''
def ch_minPwdAge(self, val):
self.logger.info('KDC Minimum Password age was changed from %s to %s' % (self.ldb.get_minPwdAge(), val))
old_val = self.ldb.get_minPwdAge()
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):
self.logger.info('KDC Maximum Password age was changed from %s to %s' % (self.ldb.get_maxPwdAge(), val))
old_val = self.ldb.get_maxPwdAge()
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):
self.logger.info('KDC Minimum Password length was changed from %s to %s' % (self.ldb.get_minPwdLength(), val))
old_val = self.ldb.get_minPwdLength()
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):
self.logger.info('KDC Password Properties were changed from %s to %s' % (self.ldb.get_pwdProperties(), val))
old_val = self.ldb.get_pwdProperties()
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):
@ -134,6 +320,9 @@ class inf_to_ldb(inf_to):
}
def __str__(self):
return 'System Access'
class gp_sec_ext(gp_ext):
'''This class does the following two things:
@ -158,7 +347,7 @@ class gp_sec_ext(gp_ext):
def listuserpol(self, rootpath):
return os.path.join(rootpath, "User/Registry.pol")
def populate_inf(self):
def apply_map(self):
return {"System Access": {"MinimumPasswordAge": ("minPwdAge", inf_to_ldb),
"MaximumPasswordAge": ("maxPwdAge", inf_to_ldb),
"MinimumPasswordLength": ("minPwdLength", inf_to_ldb),
@ -168,7 +357,7 @@ class gp_sec_ext(gp_ext):
def read_inf(self, path, conn):
ret = False
inftable = self.populate_inf()
inftable = self.apply_map()
policy = conn.loadfile(path.replace('/', '\\'))
current_section = None
@ -195,11 +384,13 @@ 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.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, lp):
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