1# Reads important GPO parameters and updates Samba
2# Copyright (C) Luke Morrison <luc785@.hotmail.com> 2013
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17
18import sys
19import os
20import errno
21import tdb
22sys.path.insert(0, "bin/python")
23from samba import NTSTATUSError
24from samba.compat import ConfigParser
25from samba.compat import StringIO
26from samba.compat import get_bytes
27from abc import ABCMeta, abstractmethod
28import xml.etree.ElementTree as etree
29import re
30from samba.net import Net
31from samba.dcerpc import nbt
32from samba.samba3 import libsmb_samba_internal as libsmb
33from samba.samba3 import param as s3param
34import samba.gpo as gpo
35from samba.param import LoadParm
36from uuid import UUID
37from tempfile import NamedTemporaryFile
38
39try:
40    from enum import Enum
41    GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
42except ImportError:
43    class GPOSTATE:
44        APPLY = 1
45        ENFORCE = 2
46        UNAPPLY = 3
47
48
49class gp_log:
50    ''' Log settings overwritten by gpo apply
51    The gp_log is an xml file that stores a history of gpo changes (and the
52    original setting value).
53
54    The log is organized like so:
55
56<gp>
57    <user name="KDC-1$">
58        <applylog>
59            <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
60        </applylog>
61        <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
62            <gp_ext name="System Access">
63                <attribute name="minPwdAge">-864000000000</attribute>
64                <attribute name="maxPwdAge">-36288000000000</attribute>
65                <attribute name="minPwdLength">7</attribute>
66                <attribute name="pwdProperties">1</attribute>
67            </gp_ext>
68            <gp_ext name="Kerberos Policy">
69                <attribute name="ticket_lifetime">1d</attribute>
70                <attribute name="renew_lifetime" />
71                <attribute name="clockskew">300</attribute>
72            </gp_ext>
73        </guid>
74    </user>
75</gp>
76
77    Each guid value contains a list of extensions, which contain a list of
78    attributes. The guid value represents a GPO. The attributes are the values
79    of those settings prior to the application of the GPO.
80    The list of guids is enclosed within a user name, which represents the user
81    the settings were applied to. This user may be the samaccountname of the
82    local computer, which implies that these are machine policies.
83    The applylog keeps track of the order in which the GPOs were applied, so
84    that they can be rolled back in reverse, returning the machine to the state
85    prior to policy application.
86    '''
87    def __init__(self, user, gpostore, db_log=None):
88        ''' Initialize the gp_log
89        param user          - the username (or machine name) that policies are
90                              being applied to
91        param gpostore      - the GPOStorage obj which references the tdb which
92                              contains gp_logs
93        param db_log        - (optional) a string to initialize the gp_log
94        '''
95        self._state = GPOSTATE.APPLY
96        self.gpostore = gpostore
97        self.username = user
98        if db_log:
99            self.gpdb = etree.fromstring(db_log)
100        else:
101            self.gpdb = etree.Element('gp')
102        self.user = user
103        user_obj = self.gpdb.find('user[@name="%s"]' % user)
104        if user_obj is None:
105            user_obj = etree.SubElement(self.gpdb, 'user')
106            user_obj.attrib['name'] = user
107
108    def state(self, value):
109        ''' Policy application state
110        param value         - APPLY, ENFORCE, or UNAPPLY
111
112        The behavior of the gp_log depends on whether we are applying policy,
113        enforcing policy, or unapplying policy. During an apply, old settings
114        are recorded in the log. During an enforce, settings are being applied
115        but the gp_log does not change. During an unapply, additions to the log
116        should be ignored (since function calls to apply settings are actually
117        reverting policy), but removals from the log are allowed.
118        '''
119        # If we're enforcing, but we've unapplied, apply instead
120        if value == GPOSTATE.ENFORCE:
121            user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
122            apply_log = user_obj.find('applylog')
123            if apply_log is None or len(apply_log) == 0:
124                self._state = GPOSTATE.APPLY
125            else:
126                self._state = value
127        else:
128            self._state = value
129
130    def set_guid(self, guid):
131        ''' Log to a different GPO guid
132        param guid          - guid value of the GPO from which we're applying
133                              policy
134        '''
135        self.guid = guid
136        user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
137        obj = user_obj.find('guid[@value="%s"]' % guid)
138        if obj is None:
139            obj = etree.SubElement(user_obj, 'guid')
140            obj.attrib['value'] = guid
141        if self._state == GPOSTATE.APPLY:
142            apply_log = user_obj.find('applylog')
143            if apply_log is None:
144                apply_log = etree.SubElement(user_obj, 'applylog')
145            prev = apply_log.find('guid[@value="%s"]' % guid)
146            if prev is None:
147                item = etree.SubElement(apply_log, 'guid')
148                item.attrib['count'] = '%d' % (len(apply_log) - 1)
149                item.attrib['value'] = guid
150
151    def store(self, gp_ext_name, attribute, old_val):
152        ''' Store an attribute in the gp_log
153        param gp_ext_name   - Name of the extension applying policy
154        param attribute     - The attribute being modified
155        param old_val       - The value of the attribute prior to policy
156                              application
157        '''
158        if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
159            return None
160        user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
161        guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
162        assert guid_obj is not None, "gpo guid was not set"
163        ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
164        if ext is None:
165            ext = etree.SubElement(guid_obj, 'gp_ext')
166            ext.attrib['name'] = gp_ext_name
167        attr = ext.find('attribute[@name="%s"]' % attribute)
168        if attr is None:
169            attr = etree.SubElement(ext, 'attribute')
170            attr.attrib['name'] = attribute
171            attr.text = old_val
172
173    def retrieve(self, gp_ext_name, attribute):
174        ''' Retrieve a stored attribute from the gp_log
175        param gp_ext_name   - Name of the extension which applied policy
176        param attribute     - The attribute being retrieved
177        return              - The value of the attribute prior to policy
178                              application
179        '''
180        user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
181        guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
182        assert guid_obj is not None, "gpo guid was not set"
183        ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
184        if ext is not None:
185            attr = ext.find('attribute[@name="%s"]' % attribute)
186            if attr is not None:
187                return attr.text
188        return None
189
190    def get_applied_guids(self):
191        ''' Return a list of applied ext guids
192        return              - List of guids for gpos that have applied settings
193                              to the system.
194        '''
195        guids = []
196        user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
197        if user_obj is not None:
198            apply_log = user_obj.find('applylog')
199            if apply_log is not None:
200                guid_objs = apply_log.findall('guid[@count]')
201                guids_by_count = [(g.get('count'), g.get('value'))
202                                  for g in guid_objs]
203                guids_by_count.sort(reverse=True)
204                guids.extend(guid for count, guid in guids_by_count)
205        return guids
206
207    def get_applied_settings(self, guids):
208        ''' Return a list of applied ext guids
209        return              - List of tuples containing the guid of a gpo, then
210                              a dictionary of policies and their values prior
211                              policy application. These are sorted so that the
212                              most recently applied settings are removed first.
213        '''
214        ret = []
215        user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
216        for guid in guids:
217            guid_settings = user_obj.find('guid[@value="%s"]' % guid)
218            exts = guid_settings.findall('gp_ext')
219            settings = {}
220            for ext in exts:
221                attr_dict = {}
222                attrs = ext.findall('attribute')
223                for attr in attrs:
224                    attr_dict[attr.attrib['name']] = attr.text
225                settings[ext.attrib['name']] = attr_dict
226            ret.append((guid, settings))
227        return ret
228
229    def delete(self, gp_ext_name, attribute):
230        ''' Remove an attribute from the gp_log
231        param gp_ext_name   - name of extension from which to remove the
232                              attribute
233        param attribute     - attribute to remove
234        '''
235        user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
236        guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
237        assert guid_obj is not None, "gpo guid was not set"
238        ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
239        if ext is not None:
240            attr = ext.find('attribute[@name="%s"]' % attribute)
241            if attr is not None:
242                ext.remove(attr)
243                if len(ext) == 0:
244                    guid_obj.remove(ext)
245
246    def commit(self):
247        ''' Write gp_log changes to disk '''
248        self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
249
250
251class GPOStorage:
252    def __init__(self, log_file):
253        if os.path.isfile(log_file):
254            self.log = tdb.open(log_file)
255        else:
256            self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT | os.O_RDWR)
257
258    def start(self):
259        self.log.transaction_start()
260
261    def get_int(self, key):
262        try:
263            return int(self.log.get(get_bytes(key)))
264        except TypeError:
265            return None
266
267    def get(self, key):
268        return self.log.get(get_bytes(key))
269
270    def get_gplog(self, user):
271        return gp_log(user, self, self.log.get(get_bytes(user)))
272
273    def store(self, key, val):
274        self.log.store(get_bytes(key), get_bytes(val))
275
276    def cancel(self):
277        self.log.transaction_cancel()
278
279    def delete(self, key):
280        self.log.delete(get_bytes(key))
281
282    def commit(self):
283        self.log.transaction_commit()
284
285    def __del__(self):
286        self.log.close()
287
288
289class gp_ext(object):
290    __metaclass__ = ABCMeta
291
292    def __init__(self, logger, lp, creds, store):
293        self.logger = logger
294        self.lp = lp
295        self.creds = creds
296        self.gp_db = store.get_gplog(creds.get_username())
297
298    @abstractmethod
299    def process_group_policy(self, deleted_gpo_list, changed_gpo_list):
300        pass
301
302    @abstractmethod
303    def read(self, policy):
304        pass
305
306    def parse(self, afile):
307        local_path = self.lp.cache_path('gpo_cache')
308        data_file = os.path.join(local_path, check_safe_path(afile).upper())
309        if os.path.exists(data_file):
310            return self.read(open(data_file, 'r').read())
311        return None
312
313    @abstractmethod
314    def __str__(self):
315        pass
316
317
318class gp_ext_setter(object):
319    __metaclass__ = ABCMeta
320
321    def __init__(self, logger, gp_db, lp, creds, attribute, val):
322        self.logger = logger
323        self.attribute = attribute
324        self.val = val
325        self.lp = lp
326        self.creds = creds
327        self.gp_db = gp_db
328
329    def explicit(self):
330        return self.val
331
332    def update_samba(self):
333        (upd_sam, value) = self.mapper().get(self.attribute)
334        upd_sam(value())
335
336    @abstractmethod
337    def mapper(self):
338        pass
339
340    def delete(self):
341        upd_sam, _ = self.mapper().get(self.attribute)
342        upd_sam(self.val)
343
344    @abstractmethod
345    def __str__(self):
346        pass
347
348
349class gp_inf_ext(gp_ext):
350    def read(self, policy):
351        inf_conf = ConfigParser()
352        inf_conf.optionxform = str
353        try:
354            inf_conf.readfp(StringIO(policy))
355        except:
356            inf_conf.readfp(StringIO(policy.decode('utf-16')))
357        return inf_conf
358
359
360''' Fetch the hostname of a writable DC '''
361
362
363def get_dc_hostname(creds, lp):
364    net = Net(creds=creds, lp=lp)
365    cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
366                                                          nbt.NBT_SERVER_DS))
367    return cldap_ret.pdc_dns_name
368
369
370''' Fetch a list of GUIDs for applicable GPOs '''
371
372
373def get_gpo_list(dc_hostname, creds, lp):
374    gpos = []
375    ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
376    if ads.connect():
377        gpos = ads.get_gpo_list(creds.get_username())
378    return gpos
379
380
381def cache_gpo_dir(conn, cache, sub_dir):
382    loc_sub_dir = sub_dir.upper()
383    local_dir = os.path.join(cache, loc_sub_dir)
384    try:
385        os.makedirs(local_dir, mode=0o755)
386    except OSError as e:
387        if e.errno != errno.EEXIST:
388            raise
389    for fdata in conn.list(sub_dir):
390        if fdata['attrib'] & libsmb.FILE_ATTRIBUTE_DIRECTORY:
391            cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
392        else:
393            local_name = fdata['name'].upper()
394            f = NamedTemporaryFile(delete=False, dir=local_dir)
395            fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
396            f.write(conn.loadfile(fname))
397            f.close()
398            os.rename(f.name, os.path.join(local_dir, local_name))
399
400
401def check_safe_path(path):
402    dirs = re.split('/|\\\\', path)
403    if 'sysvol' in path:
404        dirs = dirs[dirs.index('sysvol') + 1:]
405    if '..' not in dirs:
406        return os.path.join(*dirs)
407    raise OSError(path)
408
409
410def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
411    # the SMB bindings rely on having a s3 loadparm
412    s3_lp = s3param.get_context()
413    s3_lp.load(lp.configfile)
414    conn = libsmb.Conn(dc_hostname, 'sysvol', lp=s3_lp, creds=creds, sign=True)
415    cache_path = lp.cache_path('gpo_cache')
416    for gpo in gpos:
417        if not gpo.file_sys_path:
418            continue
419        cache_gpo_dir(conn, cache_path, check_safe_path(gpo.file_sys_path))
420
421
422def get_deleted_gpos_list(gp_db, gpos):
423    applied_gpos = gp_db.get_applied_guids()
424    current_guids = set([p.name for p in gpos])
425    deleted_gpos = [guid for guid in applied_gpos if guid not in current_guids]
426    return gp_db.get_applied_settings(deleted_gpos)
427
428def gpo_version(lp, path):
429    # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
430    # read from the gpo client cache.
431    gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
432    return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
433
434
435def apply_gp(lp, creds, logger, store, gp_extensions, force=False):
436    gp_db = store.get_gplog(creds.get_username())
437    dc_hostname = get_dc_hostname(creds, lp)
438    gpos = get_gpo_list(dc_hostname, creds, lp)
439    del_gpos = get_deleted_gpos_list(gp_db, gpos)
440    try:
441        check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
442    except:
443        logger.error('Failed downloading gpt cache from \'%s\' using SMB'
444                     % dc_hostname)
445        return
446
447    if force:
448        changed_gpos = gpos
449        gp_db.state(GPOSTATE.ENFORCE)
450    else:
451        changed_gpos = []
452        for gpo_obj in gpos:
453            if not gpo_obj.file_sys_path:
454                continue
455            guid = gpo_obj.name
456            path = check_safe_path(gpo_obj.file_sys_path).upper()
457            version = gpo_version(lp, path)
458            if version != store.get_int(guid):
459                logger.info('GPO %s has changed' % guid)
460                changed_gpos.append(gpo_obj)
461        gp_db.state(GPOSTATE.APPLY)
462
463    store.start()
464    for ext in gp_extensions:
465        try:
466            ext.process_group_policy(del_gpos, changed_gpos)
467        except Exception as e:
468            logger.error('Failed to apply extension  %s' % str(ext))
469            logger.error('Message was: ' + str(e))
470            continue
471    for gpo_obj in gpos:
472        if not gpo_obj.file_sys_path:
473            continue
474        guid = gpo_obj.name
475        path = check_safe_path(gpo_obj.file_sys_path).upper()
476        version = gpo_version(lp, path)
477        store.store(guid, '%i' % version)
478    store.commit()
479
480
481def unapply_gp(lp, creds, logger, store, gp_extensions):
482    gp_db = store.get_gplog(creds.get_username())
483    gp_db.state(GPOSTATE.UNAPPLY)
484    # Treat all applied gpos as deleted
485    del_gpos = gp_db.get_applied_settings(gp_db.get_applied_guids())
486    store.start()
487    for ext in gp_extensions:
488        try:
489            ext.process_group_policy(del_gpos, [])
490        except Exception as e:
491            logger.error('Failed to unapply extension  %s' % str(ext))
492            logger.error('Message was: ' + str(e))
493            continue
494    store.commit()
495
496
497def parse_gpext_conf(smb_conf):
498    lp = LoadParm()
499    if smb_conf is not None:
500        lp.load(smb_conf)
501    else:
502        lp.load_default()
503    ext_conf = lp.state_path('gpext.conf')
504    parser = ConfigParser()
505    parser.read(ext_conf)
506    return lp, parser
507
508
509def atomic_write_conf(lp, parser):
510    ext_conf = lp.state_path('gpext.conf')
511    with NamedTemporaryFile(mode="w+", delete=False, dir=os.path.dirname(ext_conf)) as f:
512        parser.write(f)
513        os.rename(f.name, ext_conf)
514
515
516def check_guid(guid):
517    # Check for valid guid with curly braces
518    if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
519        return False
520    try:
521        UUID(guid, version=4)
522    except ValueError:
523        return False
524    return True
525
526
527def register_gp_extension(guid, name, path,
528                          smb_conf=None, machine=True, user=True):
529    # Check that the module exists
530    if not os.path.exists(path):
531        return False
532    if not check_guid(guid):
533        return False
534
535    lp, parser = parse_gpext_conf(smb_conf)
536    if guid not in parser.sections():
537        parser.add_section(guid)
538    parser.set(guid, 'DllName', path)
539    parser.set(guid, 'ProcessGroupPolicy', name)
540    parser.set(guid, 'NoMachinePolicy', "0" if machine else "1")
541    parser.set(guid, 'NoUserPolicy', "0" if user else "1")
542
543    atomic_write_conf(lp, parser)
544
545    return True
546
547
548def list_gp_extensions(smb_conf=None):
549    _, parser = parse_gpext_conf(smb_conf)
550    results = {}
551    for guid in parser.sections():
552        results[guid] = {}
553        results[guid]['DllName'] = parser.get(guid, 'DllName')
554        results[guid]['ProcessGroupPolicy'] = \
555            parser.get(guid, 'ProcessGroupPolicy')
556        results[guid]['MachinePolicy'] = \
557            not int(parser.get(guid, 'NoMachinePolicy'))
558        results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
559    return results
560
561
562def unregister_gp_extension(guid, smb_conf=None):
563    if not check_guid(guid):
564        return False
565
566    lp, parser = parse_gpext_conf(smb_conf)
567    if guid in parser.sections():
568        parser.remove_section(guid)
569
570    atomic_write_conf(lp, parser)
571
572    return True
573