python:gp: Implement client site lookup in site_dn_for_machine()
[janger/samba-autobuild-v4-20-test/.git] / python / samba / gp / gpclass.py
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
18 import sys
19 import os, shutil
20 import errno
21 import tdb
22 import pwd
23 sys.path.insert(0, "bin/python")
24 from samba import WERRORError
25 from configparser import ConfigParser
26 from io import StringIO
27 import traceback
28 from samba.common import get_bytes
29 from abc import ABCMeta, abstractmethod
30 import xml.etree.ElementTree as etree
31 import re
32 from samba.net import Net
33 from samba.dcerpc import nbt
34 from samba.samba3 import libsmb_samba_internal as libsmb
35 import samba.gpo as gpo
36 from uuid import UUID
37 from tempfile import NamedTemporaryFile
38 from samba.dcerpc import preg
39 from samba.ndr import ndr_unpack
40 from samba.credentials import SMB_SIGNING_REQUIRED
41 from samba.gp.util.logging import log
42 from hashlib import blake2b
43 import numbers
44 from samba.common import get_string
45 from samba.samdb import SamDB
46 from samba.auth import system_session
47 import ldb
48 from samba.dsdb import UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, GPLINK_OPT_ENFORCE, GPLINK_OPT_DISABLE, GPO_BLOCK_INHERITANCE
49 from samba.auth import AUTH_SESSION_INFO_DEFAULT_GROUPS, AUTH_SESSION_INFO_AUTHENTICATED, AUTH_SESSION_INFO_SIMPLE_PRIVILEGES
50 from samba.dcerpc import security
51 import samba.security
52 from samba.dcerpc import nbt
53 from datetime import datetime
54
55
56 try:
57     from enum import Enum
58     GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
59 except ImportError:
60     class GPOSTATE:
61         APPLY = 1
62         ENFORCE = 2
63         UNAPPLY = 3
64
65
66 class gp_log:
67     """ Log settings overwritten by gpo apply
68     The gp_log is an xml file that stores a history of gpo changes (and the
69     original setting value).
70
71     The log is organized like so:
72
73 <gp>
74     <user name="KDC-1$">
75         <applylog>
76             <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
77         </applylog>
78         <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
79             <gp_ext name="System Access">
80                 <attribute name="minPwdAge">-864000000000</attribute>
81                 <attribute name="maxPwdAge">-36288000000000</attribute>
82                 <attribute name="minPwdLength">7</attribute>
83                 <attribute name="pwdProperties">1</attribute>
84             </gp_ext>
85             <gp_ext name="Kerberos Policy">
86                 <attribute name="ticket_lifetime">1d</attribute>
87                 <attribute name="renew_lifetime" />
88                 <attribute name="clockskew">300</attribute>
89             </gp_ext>
90         </guid>
91     </user>
92 </gp>
93
94     Each guid value contains a list of extensions, which contain a list of
95     attributes. The guid value represents a GPO. The attributes are the values
96     of those settings prior to the application of the GPO.
97     The list of guids is enclosed within a user name, which represents the user
98     the settings were applied to. This user may be the samaccountname of the
99     local computer, which implies that these are machine policies.
100     The applylog keeps track of the order in which the GPOs were applied, so
101     that they can be rolled back in reverse, returning the machine to the state
102     prior to policy application.
103     """
104     def __init__(self, user, gpostore, db_log=None):
105         """ Initialize the gp_log
106         param user          - the username (or machine name) that policies are
107                               being applied to
108         param gpostore      - the GPOStorage obj which references the tdb which
109                               contains gp_logs
110         param db_log        - (optional) a string to initialize the gp_log
111         """
112         self._state = GPOSTATE.APPLY
113         self.gpostore = gpostore
114         self.username = user
115         if db_log:
116             self.gpdb = etree.fromstring(db_log)
117         else:
118             self.gpdb = etree.Element('gp')
119         self.user = user
120         user_obj = self.gpdb.find('user[@name="%s"]' % user)
121         if user_obj is None:
122             user_obj = etree.SubElement(self.gpdb, 'user')
123             user_obj.attrib['name'] = user
124
125     def state(self, value):
126         """ Policy application state
127         param value         - APPLY, ENFORCE, or UNAPPLY
128
129         The behavior of the gp_log depends on whether we are applying policy,
130         enforcing policy, or unapplying policy. During an apply, old settings
131         are recorded in the log. During an enforce, settings are being applied
132         but the gp_log does not change. During an unapply, additions to the log
133         should be ignored (since function calls to apply settings are actually
134         reverting policy), but removals from the log are allowed.
135         """
136         # If we're enforcing, but we've unapplied, apply instead
137         if value == GPOSTATE.ENFORCE:
138             user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
139             apply_log = user_obj.find('applylog')
140             if apply_log is None or len(apply_log) == 0:
141                 self._state = GPOSTATE.APPLY
142             else:
143                 self._state = value
144         else:
145             self._state = value
146
147     def get_state(self):
148         """Check the GPOSTATE
149         """
150         return self._state
151
152     def set_guid(self, guid):
153         """ Log to a different GPO guid
154         param guid          - guid value of the GPO from which we're applying
155                               policy
156         """
157         self.guid = guid
158         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
159         obj = user_obj.find('guid[@value="%s"]' % guid)
160         if obj is None:
161             obj = etree.SubElement(user_obj, 'guid')
162             obj.attrib['value'] = guid
163         if self._state == GPOSTATE.APPLY:
164             apply_log = user_obj.find('applylog')
165             if apply_log is None:
166                 apply_log = etree.SubElement(user_obj, 'applylog')
167             prev = apply_log.find('guid[@value="%s"]' % guid)
168             if prev is None:
169                 item = etree.SubElement(apply_log, 'guid')
170                 item.attrib['count'] = '%d' % (len(apply_log) - 1)
171                 item.attrib['value'] = guid
172
173     def store(self, gp_ext_name, attribute, old_val):
174         """ Store an attribute in the gp_log
175         param gp_ext_name   - Name of the extension applying policy
176         param attribute     - The attribute being modified
177         param old_val       - The value of the attribute prior to policy
178                               application
179         """
180         if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
181             return None
182         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
183         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
184         assert guid_obj is not None, "gpo guid was not set"
185         ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
186         if ext is None:
187             ext = etree.SubElement(guid_obj, 'gp_ext')
188             ext.attrib['name'] = gp_ext_name
189         attr = ext.find('attribute[@name="%s"]' % attribute)
190         if attr is None:
191             attr = etree.SubElement(ext, 'attribute')
192             attr.attrib['name'] = attribute
193             attr.text = old_val
194
195     def retrieve(self, gp_ext_name, attribute):
196         """ Retrieve a stored attribute from the gp_log
197         param gp_ext_name   - Name of the extension which applied policy
198         param attribute     - The attribute being retrieved
199         return              - The value of the attribute prior to policy
200                               application
201         """
202         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
203         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
204         assert guid_obj is not None, "gpo guid was not set"
205         ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
206         if ext is not None:
207             attr = ext.find('attribute[@name="%s"]' % attribute)
208             if attr is not None:
209                 return attr.text
210         return None
211
212     def retrieve_all(self, gp_ext_name):
213         """ Retrieve all stored attributes for this user, GPO guid, and CSE
214         param gp_ext_name   - Name of the extension which applied policy
215         return              - The values of the attributes prior to policy
216                               application
217         """
218         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
219         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
220         assert guid_obj is not None, "gpo guid was not set"
221         ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
222         if ext is not None:
223             attrs = ext.findall('attribute')
224             return {attr.attrib['name']: attr.text for attr in attrs}
225         return {}
226
227     def get_applied_guids(self):
228         """ Return a list of applied ext guids
229         return              - List of guids for gpos that have applied settings
230                               to the system.
231         """
232         guids = []
233         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
234         if user_obj is not None:
235             apply_log = user_obj.find('applylog')
236             if apply_log is not None:
237                 guid_objs = apply_log.findall('guid[@count]')
238                 guids_by_count = [(g.get('count'), g.get('value'))
239                                   for g in guid_objs]
240                 guids_by_count.sort(reverse=True)
241                 guids.extend(guid for count, guid in guids_by_count)
242         return guids
243
244     def get_applied_settings(self, guids):
245         """ Return a list of applied ext guids
246         return              - List of tuples containing the guid of a gpo, then
247                               a dictionary of policies and their values prior
248                               policy application. These are sorted so that the
249                               most recently applied settings are removed first.
250         """
251         ret = []
252         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
253         for guid in guids:
254             guid_settings = user_obj.find('guid[@value="%s"]' % guid)
255             exts = guid_settings.findall('gp_ext')
256             settings = {}
257             for ext in exts:
258                 attr_dict = {}
259                 attrs = ext.findall('attribute')
260                 for attr in attrs:
261                     attr_dict[attr.attrib['name']] = attr.text
262                 settings[ext.attrib['name']] = attr_dict
263             ret.append((guid, settings))
264         return ret
265
266     def delete(self, gp_ext_name, attribute):
267         """ Remove an attribute from the gp_log
268         param gp_ext_name   - name of extension from which to remove the
269                               attribute
270         param attribute     - attribute to remove
271         """
272         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
273         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
274         assert guid_obj is not None, "gpo guid was not set"
275         ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
276         if ext is not None:
277             attr = ext.find('attribute[@name="%s"]' % attribute)
278             if attr is not None:
279                 ext.remove(attr)
280                 if len(ext) == 0:
281                     guid_obj.remove(ext)
282
283     def commit(self):
284         """ Write gp_log changes to disk """
285         self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
286
287
288 class GPOStorage:
289     def __init__(self, log_file):
290         if os.path.isfile(log_file):
291             self.log = tdb.open(log_file)
292         else:
293             self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT | os.O_RDWR)
294
295     def start(self):
296         self.log.transaction_start()
297
298     def get_int(self, key):
299         try:
300             return int(self.log.get(get_bytes(key)))
301         except TypeError:
302             return None
303
304     def get(self, key):
305         return self.log.get(get_bytes(key))
306
307     def get_gplog(self, user):
308         return gp_log(user, self, self.log.get(get_bytes(user)))
309
310     def store(self, key, val):
311         self.log.store(get_bytes(key), get_bytes(val))
312
313     def cancel(self):
314         self.log.transaction_cancel()
315
316     def delete(self, key):
317         self.log.delete(get_bytes(key))
318
319     def commit(self):
320         self.log.transaction_commit()
321
322     def __del__(self):
323         self.log.close()
324
325
326 class gp_ext(object):
327     __metaclass__ = ABCMeta
328
329     def __init__(self, lp, creds, username, store):
330         self.lp = lp
331         self.creds = creds
332         self.username = username
333         self.gp_db = store.get_gplog(username)
334
335     @abstractmethod
336     def process_group_policy(self, deleted_gpo_list, changed_gpo_list):
337         pass
338
339     @abstractmethod
340     def read(self, policy):
341         pass
342
343     def parse(self, afile):
344         local_path = self.lp.cache_path('gpo_cache')
345         data_file = os.path.join(local_path, check_safe_path(afile).upper())
346         if os.path.exists(data_file):
347             return self.read(data_file)
348         return None
349
350     @abstractmethod
351     def __str__(self):
352         pass
353
354     @abstractmethod
355     def rsop(self, gpo):
356         return {}
357
358
359 class gp_inf_ext(gp_ext):
360     def read(self, data_file):
361         with open(data_file, 'rb') as f:
362             policy = f.read()
363         inf_conf = ConfigParser(interpolation=None)
364         inf_conf.optionxform = str
365         try:
366             inf_conf.read_file(StringIO(policy.decode()))
367         except UnicodeDecodeError:
368             inf_conf.read_file(StringIO(policy.decode('utf-16')))
369         return inf_conf
370
371
372 class gp_pol_ext(gp_ext):
373     def read(self, data_file):
374         with open(data_file, 'rb') as f:
375             raw = f.read()
376         return ndr_unpack(preg.file, raw)
377
378
379 class gp_xml_ext(gp_ext):
380     def read(self, data_file):
381         with open(data_file, 'rb') as f:
382             raw = f.read()
383         try:
384             return etree.fromstring(raw.decode())
385         except UnicodeDecodeError:
386             return etree.fromstring(raw.decode('utf-16'))
387
388
389 class gp_applier(object):
390     """Group Policy Applier/Unapplier/Modifier
391     The applier defines functions for monitoring policy application,
392     removal, and modification. It must be a multi-derived class paired
393     with a subclass of gp_ext.
394     """
395     __metaclass__ = ABCMeta
396
397     def cache_add_attribute(self, guid, attribute, value):
398         """Add an attribute and value to the Group Policy cache
399         guid        - The GPO guid which applies this policy
400         attribute   - The attribute name of the policy being applied
401         value       - The value of the policy being applied
402
403         Normally called by the subclass apply() function after applying policy.
404         """
405         self.gp_db.set_guid(guid)
406         self.gp_db.store(str(self), attribute, value)
407         self.gp_db.commit()
408
409     def cache_remove_attribute(self, guid, attribute):
410         """Remove an attribute from the Group Policy cache
411         guid        - The GPO guid which applies this policy
412         attribute   - The attribute name of the policy being unapplied
413
414         Normally called by the subclass unapply() function when removing old
415         policy.
416         """
417         self.gp_db.set_guid(guid)
418         self.gp_db.delete(str(self), attribute)
419         self.gp_db.commit()
420
421     def cache_get_attribute_value(self, guid, attribute):
422         """Retrieve the value stored in the cache for the given attribute
423         guid        - The GPO guid which applies this policy
424         attribute   - The attribute name of the policy
425         """
426         self.gp_db.set_guid(guid)
427         return self.gp_db.retrieve(str(self), attribute)
428
429     def cache_get_all_attribute_values(self, guid):
430         """Retrieve all attribute/values currently stored for this gpo+policy
431         guid        - The GPO guid which applies this policy
432         """
433         self.gp_db.set_guid(guid)
434         return self.gp_db.retrieve_all(str(self))
435
436     def cache_get_apply_state(self):
437         """Return the current apply state
438         return      - APPLY|ENFORCE|UNAPPLY
439         """
440         return self.gp_db.get_state()
441
442     def generate_attribute(self, name, *args):
443         """Generate an attribute name from arbitrary data
444         name            - A name to ensure uniqueness
445         args            - Any arbitrary set of args, str or bytes
446         return          - A blake2b digest of the data, the attribute
447
448         The importance here is the digest of the data makes the attribute
449         reproducible and uniquely identifies it. Hashing the name with
450         the data ensures we don't falsely identify a match which is the same
451         text in a different file. Using this attribute generator is optional.
452         """
453         data = b''.join([get_bytes(arg) for arg in [*args]])
454         return blake2b(get_bytes(name)+data).hexdigest()
455
456     def generate_value_hash(self, *args):
457         """Generate a unique value which identifies value changes
458         args            - Any arbitrary set of args, str or bytes
459         return          - A blake2b digest of the data, the value represented
460         """
461         data = b''.join([get_bytes(arg) for arg in [*args]])
462         return blake2b(data).hexdigest()
463
464     @abstractmethod
465     def unapply(self, guid, attribute, value):
466         """Group Policy Unapply
467         guid            - The GPO guid which applies this policy
468         attribute       - The attribute name of the policy being unapplied
469         value           - The value of the policy being unapplied
470         """
471         pass
472
473     @abstractmethod
474     def apply(self, guid, attribute, applier_func, *args):
475         """Group Policy Apply
476         guid            - The GPO guid which applies this policy
477         attribute       - The attribute name of the policy being applied
478         applier_func    - An applier function which takes variable args
479         args            - The variable arguments to pass to applier_func
480
481         The applier_func function MUST return the value of the policy being
482         applied. It's important that implementations of `apply` check for and
483         first unapply any changed policy. See for example calls to
484         `cache_get_all_attribute_values()` which searches for all policies
485         applied by this GPO for this Client Side Extension (CSE).
486         """
487         pass
488
489     def clean(self, guid, keep=None, remove=None, **kwargs):
490         """Cleanup old removed attributes
491         keep    - A list of attributes to keep
492         remove  - A single attribute to remove, or a list of attributes to
493                   remove
494         kwargs  - Additional keyword args required by the subclass unapply
495                   function
496
497         This is only necessary for CSEs which provide multiple attributes.
498         """
499         # Clean syntax is, either provide a single remove attribute,
500         # or a list of either removal attributes or keep attributes.
501         if keep is None:
502             keep = []
503         if remove is None:
504             remove = []
505
506         if type(remove) != list:
507             value = self.cache_get_attribute_value(guid, remove)
508             if value is not None:
509                 self.unapply(guid, remove, value, **kwargs)
510         else:
511             old_vals = self.cache_get_all_attribute_values(guid)
512             for attribute, value in old_vals.items():
513                 if (len(remove) > 0 and attribute in remove) or \
514                    (len(keep) > 0 and attribute not in keep):
515                     self.unapply(guid, attribute, value, **kwargs)
516
517
518 class gp_misc_applier(gp_applier):
519     """Group Policy Miscellaneous Applier/Unapplier/Modifier
520     """
521
522     def generate_value(self, **kwargs):
523         data = etree.Element('data')
524         for k, v in kwargs.items():
525             arg = etree.SubElement(data, k)
526             arg.text = get_string(v)
527         return get_string(etree.tostring(data, 'utf-8'))
528
529     def parse_value(self, value):
530         vals = {}
531         try:
532             data = etree.fromstring(value)
533         except etree.ParseError:
534             # If parsing fails, then it's an old cache value
535             return {'old_val': value}
536         except TypeError:
537             return {}
538         itr = data.iter()
539         next(itr) # Skip the top element
540         for item in itr:
541             vals[item.tag] = item.text
542         return vals
543
544
545 class gp_file_applier(gp_applier):
546     """Group Policy File Applier/Unapplier/Modifier
547     Subclass of abstract class gp_applier for monitoring policy applied
548     via a file.
549     """
550
551     def __generate_value(self, value_hash, files, sep):
552         data = [value_hash]
553         data.extend(files)
554         return sep.join(data)
555
556     def __parse_value(self, value, sep):
557         """Parse a value
558         return          - A unique HASH, followed by the file list
559         """
560         if value is None:
561             return None, []
562         data = value.split(sep)
563         if '/' in data[0]:
564             # The first element is not a hash, but a filename. This is a
565             # legacy value.
566             return None, data
567         else:
568             return data[0], data[1:] if len(data) > 1 else []
569
570     def unapply(self, guid, attribute, files, sep=':'):
571         # If the value isn't a list of files, parse value from the log
572         if type(files) != list:
573             _, files = self.__parse_value(files, sep)
574         for file in files:
575             if os.path.exists(file):
576                 os.unlink(file)
577         self.cache_remove_attribute(guid, attribute)
578
579     def apply(self, guid, attribute, value_hash, applier_func, *args, sep=':'):
580         """
581         applier_func MUST return a list of files created by the applier.
582
583         This applier is for policies which only apply to a single file (with
584         a couple small exceptions). This applier will remove any policy applied
585         by this GPO which doesn't match the new policy.
586         """
587         # If the policy has changed, unapply, then apply new policy
588         old_val = self.cache_get_attribute_value(guid, attribute)
589         # Ignore removal if this policy is applied and hasn't changed
590         old_val_hash, old_val_files = self.__parse_value(old_val, sep)
591         if (old_val_hash != value_hash or
592                 self.cache_get_apply_state() == GPOSTATE.ENFORCE) or \
593                 not all([os.path.exists(f) for f in old_val_files]):
594             self.unapply(guid, attribute, old_val_files)
595         else:
596             # If policy is already applied, skip application
597             return
598
599         # Apply the policy and log the changes
600         files = applier_func(*args)
601         new_value = self.__generate_value(value_hash, files, sep)
602         self.cache_add_attribute(guid, attribute, new_value)
603
604
605 """ Fetch the hostname of a writable DC """
606
607
608 def get_dc_hostname(creds, lp):
609     net = Net(creds=creds, lp=lp)
610     cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
611                                                           nbt.NBT_SERVER_DS))
612     return cldap_ret.pdc_dns_name
613
614
615 """ Fetch a list of GUIDs for applicable GPOs """
616
617
618 def get_gpo(samdb, gpo_dn):
619     g = gpo.GROUP_POLICY_OBJECT()
620     attrs = [
621         "cn",
622         "displayName",
623         "flags",
624         "gPCFileSysPath",
625         "gPCFunctionalityVersion",
626         "gPCMachineExtensionNames",
627         "gPCUserExtensionNames",
628         "gPCWQLFilter",
629         "name",
630         "nTSecurityDescriptor",
631         "versionNumber"
632     ]
633     if gpo_dn.startswith("LDAP://"):
634         gpo_dn = gpo_dn.lstrip("LDAP://")
635
636     sd_flags = (security.SECINFO_OWNER |
637                 security.SECINFO_GROUP |
638                 security.SECINFO_DACL)
639     try:
640         res = samdb.search(gpo_dn, ldb.SCOPE_BASE, "(objectclass=*)", attrs,
641                            controls=['sd_flags:1:%d' % sd_flags])
642     except Exception:
643         log.error('Failed to fetch gpo object with nTSecurityDescriptor')
644         raise
645     if res.count != 1:
646         raise ldb.LdbError(ldb.ERR_NO_SUCH_OBJECT,
647                            'get_gpo: search failed')
648
649     g.ds_path = gpo_dn
650     if 'versionNumber' in res.msgs[0].keys():
651         g.version = int(res.msgs[0]['versionNumber'][0])
652     if 'flags' in res.msgs[0].keys():
653         g.options = int(res.msgs[0]['flags'][0])
654     if 'gPCFileSysPath' in res.msgs[0].keys():
655         g.file_sys_path = res.msgs[0]['gPCFileSysPath'][0].decode()
656     if 'displayName' in res.msgs[0].keys():
657         g.display_name = res.msgs[0]['displayName'][0].decode()
658     if 'name' in res.msgs[0].keys():
659         g.name = res.msgs[0]['name'][0].decode()
660     if 'gPCMachineExtensionNames' in res.msgs[0].keys():
661         g.machine_extensions = str(res.msgs[0]['gPCMachineExtensionNames'][0])
662     if 'gPCUserExtensionNames' in res.msgs[0].keys():
663         g.user_extensions = str(res.msgs[0]['gPCUserExtensionNames'][0])
664     if 'nTSecurityDescriptor' in res.msgs[0].keys():
665         g.set_sec_desc(bytes(res.msgs[0]['nTSecurityDescriptor'][0]))
666     return g
667
668 class GP_LINK:
669     def __init__(self, gPLink, gPOptions):
670         self.link_names = []
671         self.link_opts = []
672         self.gpo_parse_gplink(gPLink)
673         self.gp_opts = int(gPOptions)
674
675     def gpo_parse_gplink(self, gPLink):
676         for p in gPLink.decode().split(']'):
677             if not p:
678                 continue
679             log.debug('gpo_parse_gplink: processing link')
680             p = p.lstrip('[')
681             link_name, link_opt = p.split(';')
682             log.debug('gpo_parse_gplink: link: {}'.format(link_name))
683             log.debug('gpo_parse_gplink: opt: {}'.format(link_opt))
684             self.link_names.append(link_name)
685             self.link_opts.append(int(link_opt))
686
687     def num_links(self):
688         if len(self.link_names) != len(self.link_opts):
689             raise RuntimeError('Link names and opts mismatch')
690         return len(self.link_names)
691
692 def find_samaccount(samdb, samaccountname):
693     attrs = ['dn', 'userAccountControl']
694     res = samdb.search(samdb.get_default_basedn(), ldb.SCOPE_SUBTREE,
695                        '(sAMAccountName={})'.format(samaccountname), attrs)
696     if res.count != 1:
697         raise ldb.LdbError(ldb.ERR_NO_SUCH_OBJECT,
698             "Failed to find samAccountName '{}'".format(samaccountname)
699         )
700     uac = int(res.msgs[0]['userAccountControl'][0])
701     dn = res.msgs[0]['dn']
702     log.info('Found dn {} for samaccountname {}'.format(dn, samaccountname))
703     return uac, dn
704
705 def get_gpo_link(samdb, link_dn):
706     res = samdb.search(link_dn, ldb.SCOPE_BASE,
707                        '(objectclass=*)', ['gPLink', 'gPOptions'])
708     if res.count != 1:
709         raise ldb.LdbError(ldb.ERR_NO_SUCH_OBJECT, 'get_gpo_link: no result')
710     if 'gPLink' not in res.msgs[0]:
711         raise ldb.LdbError(ldb.ERR_NO_SUCH_ATTRIBUTE,
712             "get_gpo_link: no 'gPLink' attribute found for '{}'".format(link_dn)
713         )
714     gPLink = res.msgs[0]['gPLink'][0]
715     gPOptions = 0
716     if 'gPOptions' in res.msgs[0]:
717         gPOptions = res.msgs[0]['gPOptions'][0]
718     else:
719         log.debug("get_gpo_link: no 'gPOptions' attribute found")
720     return GP_LINK(gPLink, gPOptions)
721
722 def add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list, link_dn, gp_link,
723                            link_type, only_add_forced_gpos, token):
724     for i in range(gp_link.num_links()-1, -1, -1):
725         is_forced = (gp_link.link_opts[i] & GPLINK_OPT_ENFORCE) != 0
726         if gp_link.link_opts[i] & GPLINK_OPT_DISABLE:
727             log.debug('skipping disabled GPO')
728             continue
729
730         if only_add_forced_gpos:
731             if not is_forced:
732                 log.debug("skipping nonenforced GPO link "
733                           "because GPOPTIONS_BLOCK_INHERITANCE "
734                           "has been set")
735                 continue
736             else:
737                 log.debug("adding enforced GPO link although "
738                           "the GPOPTIONS_BLOCK_INHERITANCE "
739                           "has been set")
740
741         try:
742             new_gpo = get_gpo(samdb, gp_link.link_names[i])
743         except ldb.LdbError as e:
744             (enum, estr) = e.args
745             log.debug("failed to get gpo: %s" % gp_link.link_names[i])
746             if enum == ldb.ERR_NO_SUCH_OBJECT:
747                 log.debug("skipping empty gpo: %s" % gp_link.link_names[i])
748                 continue
749             return
750         else:
751             try:
752                 sec_desc = ndr_unpack(security.descriptor,
753                                       new_gpo.get_sec_desc_buf())
754                 samba.security.access_check(sec_desc, token,
755                                             security.SEC_STD_READ_CONTROL |
756                                             security.SEC_ADS_LIST |
757                                             security.SEC_ADS_READ_PROP)
758             except Exception as e:
759                 log.debug("skipping GPO \"%s\" as object "
760                           "has no access to it" % new_gpo.display_name)
761                 continue
762
763             new_gpo.link = str(link_dn)
764             new_gpo.link_type = link_type
765
766             if is_forced:
767                 forced_gpo_list.insert(0, new_gpo)
768             else:
769                 gpo_list.insert(0, new_gpo)
770
771             log.debug("add_gplink_to_gpo_list: added GPLINK #%d %s "
772                       "to GPO list" % (i, gp_link.link_names[i]))
773
774 def merge_with_system_token(token_1):
775     sids = token_1.sids
776     system_token = system_session().security_token
777     sids.extend(system_token.sids)
778     token_1.sids = sids
779     token_1.rights_mask |= system_token.rights_mask
780     token_1.privilege_mask |= system_token.privilege_mask
781     # There are no claims in the system token, so it is safe not to merge the claims
782     return token_1
783
784
785 def site_dn_for_machine(samdb, dc_hostname, lp, creds, hostname):
786     # [MS-GPOL] 3.2.5.1.4 Site Search
787
788     # The netr_DsRGetSiteName() needs to run over local rpc, however we do not
789     # have the call implemented in our rpc_server.
790     # What netr_DsRGetSiteName() actually does is an ldap query to get
791     # the sitename, we can do the same.
792
793     # NtVer=(NETLOGON_NT_VERSION_IP|NETLOGON_NT_VERSION_WITH_CLOSEST_SITE|
794     #        NETLOGON_NT_VERSION_5EX) [0x20000014]
795     expr = "(&(DnsDomain=%s.)(User=%s)(NtVer=\\14\\00\\00\\20))" % (
796         samdb.domain_dns_name(),
797         hostname)
798     res = samdb.search(
799         base='',
800         scope=ldb.SCOPE_BASE,
801         expression=expr,
802         attrs=["Netlogon"])
803     if res.count != 1:
804         raise RuntimeError('site_dn_for_machine: No result')
805
806     samlogon_response = ndr_unpack(nbt.netlogon_samlogon_response,
807                                    bytes(res.msgs[0]['Netlogon'][0]))
808     if samlogon_response.ntver not in [nbt.NETLOGON_NT_VERSION_5EX,
809                                        (nbt.NETLOGON_NT_VERSION_1
810                                         | nbt.NETLOGON_NT_VERSION_5EX)]:
811         raise RuntimeError('site_dn_for_machine: Invalid NtVer in '
812                            + 'netlogon_samlogon_response')
813
814     # We want NETLOGON_NT_VERSION_5EX out of the union!
815     samlogon_response.ntver = nbt.NETLOGON_NT_VERSION_5EX
816     samlogon_response_ex = samlogon_response.data
817
818     client_site = "Default-First-Site-Name"
819     if (samlogon_response_ex.client_site
820             and len(samlogon_response_ex.client_site) > 1):
821         client_site = samlogon_response_ex.client_site
822
823     site_dn = samdb.get_config_basedn()
824     site_dn.add_child("CN=Sites")
825     site_dn.add_child("CN=%s" % (client_site))
826
827     return site_dn
828
829
830
831 def get_gpo_list(dc_hostname, creds, lp, username):
832     """Get the full list of GROUP_POLICY_OBJECTs for a given username.
833     Push GPOs to gpo_list so that the traversal order of the list matches
834     the order of application:
835     (L)ocal (S)ite (D)omain (O)rganizational(U)nit
836     For different domains and OUs: parent-to-child.
837     Within same level of domains and OUs: Link order.
838     Since GPOs are pushed to the front of gpo_list, GPOs have to be
839     pushed in the opposite order of application (OUs first, local last,
840     child-to-parent).
841     Forced GPOs are appended in the end since they override all others.
842     """
843     gpo_list = []
844     forced_gpo_list = []
845     url = 'ldap://' + dc_hostname
846     samdb = SamDB(url=url,
847                   session_info=system_session(),
848                   credentials=creds, lp=lp)
849     # username is DOM\\SAM, but get_gpo_list expects SAM
850     uac, dn = find_samaccount(samdb, username.split('\\')[-1])
851     add_only_forced_gpos = False
852
853     # Fetch the security token
854     session_info_flags = (AUTH_SESSION_INFO_DEFAULT_GROUPS |
855                           AUTH_SESSION_INFO_AUTHENTICATED)
856     if url.startswith('ldap'):
857         session_info_flags |= AUTH_SESSION_INFO_SIMPLE_PRIVILEGES
858     session = samba.auth.user_session(samdb, lp_ctx=lp, dn=dn,
859                                       session_info_flags=session_info_flags)
860     gpo_list_machine = False
861     if uac & UF_WORKSTATION_TRUST_ACCOUNT or uac & UF_SERVER_TRUST_ACCOUNT:
862         gpo_list_machine = True
863         token = merge_with_system_token(session.security_token)
864     else:
865         token = session.security_token
866
867     # (O)rganizational(U)nit
868     parent_dn = dn.parent()
869     while True:
870         if str(parent_dn) == str(samdb.get_default_basedn().parent()):
871             break
872
873         # An account can be a member of more OUs
874         if parent_dn.get_component_name(0) == 'OU':
875             try:
876                 log.debug("get_gpo_list: query OU: [%s] for GPOs" % parent_dn)
877                 gp_link = get_gpo_link(samdb, parent_dn)
878             except ldb.LdbError as e:
879                 (enum, estr) = e.args
880                 log.debug(estr)
881             else:
882                 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
883                                        parent_dn, gp_link,
884                                        gpo.GP_LINK_OU,
885                                        add_only_forced_gpos, token)
886
887                 # block inheritance from now on
888                 if gp_link.gp_opts & GPO_BLOCK_INHERITANCE:
889                     add_only_forced_gpos = True
890
891         parent_dn = parent_dn.parent()
892
893     # (D)omain
894     parent_dn = dn.parent()
895     while True:
896         if str(parent_dn) == str(samdb.get_default_basedn().parent()):
897             break
898
899         # An account can just be a member of one domain
900         if parent_dn.get_component_name(0) == 'DC':
901             try:
902                 log.debug("get_gpo_list: query DC: [%s] for GPOs" % parent_dn)
903                 gp_link = get_gpo_link(samdb, parent_dn)
904             except ldb.LdbError as e:
905                 (enum, estr) = e.args
906                 log.debug(estr)
907             else:
908                 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
909                                        parent_dn, gp_link,
910                                        gpo.GP_LINK_DOMAIN,
911                                        add_only_forced_gpos, token)
912
913                 # block inheritance from now on
914                 if gp_link.gp_opts & GPO_BLOCK_INHERITANCE:
915                     add_only_forced_gpos = True
916
917         parent_dn = parent_dn.parent()
918
919     # (S)ite
920     if gpo_list_machine:
921         try:
922             site_dn = site_dn_for_machine(samdb, dc_hostname, lp, creds, username)
923
924             try:
925                 log.debug("get_gpo_list: query SITE: [%s] for GPOs" % site_dn)
926                 gp_link = get_gpo_link(samdb, site_dn)
927             except ldb.LdbError as e:
928                 (enum, estr) = e.args
929                 log.debug(estr)
930             else:
931                 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
932                                        site_dn, gp_link,
933                                        gpo.GP_LINK_SITE,
934                                        add_only_forced_gpos, token)
935         except ldb.LdbError:
936             # [MS-GPOL] 3.2.5.1.4 Site Search: If the method returns
937             # ERROR_NO_SITENAME, the remainder of this message MUST be skipped
938             # and the protocol sequence MUST continue at GPO Search
939             pass
940
941     # (L)ocal
942     gpo_list.insert(0, gpo.GROUP_POLICY_OBJECT("Local Policy",
943                                                "Local Policy",
944                                                gpo.GP_LINK_LOCAL))
945
946     # Append |forced_gpo_list| at the end of |gpo_list|,
947     # so that forced GPOs are applied on top of non enforced GPOs.
948     return gpo_list+forced_gpo_list
949
950
951 def cache_gpo_dir(conn, cache, sub_dir):
952     loc_sub_dir = sub_dir.upper()
953     local_dir = os.path.join(cache, loc_sub_dir)
954     try:
955         os.makedirs(local_dir, mode=0o755)
956     except OSError as e:
957         if e.errno != errno.EEXIST:
958             raise
959     for fdata in conn.list(sub_dir):
960         if fdata['attrib'] & libsmb.FILE_ATTRIBUTE_DIRECTORY:
961             cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
962         else:
963             local_name = fdata['name'].upper()
964             f = NamedTemporaryFile(delete=False, dir=local_dir)
965             fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
966             f.write(conn.loadfile(fname))
967             f.close()
968             os.rename(f.name, os.path.join(local_dir, local_name))
969
970
971 def check_safe_path(path):
972     dirs = re.split('/|\\\\', path)
973     if 'sysvol' in path.lower():
974         ldirs = re.split('/|\\\\', path.lower())
975         dirs = dirs[ldirs.index('sysvol') + 1:]
976     if '..' not in dirs:
977         return os.path.join(*dirs)
978     raise OSError(path)
979
980
981 def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
982     # Force signing for the connection
983     saved_signing_state = creds.get_smb_signing()
984     creds.set_smb_signing(SMB_SIGNING_REQUIRED)
985     conn = libsmb.Conn(dc_hostname, 'sysvol', lp=lp, creds=creds)
986     # Reset signing state
987     creds.set_smb_signing(saved_signing_state)
988     cache_path = lp.cache_path('gpo_cache')
989     for gpo_obj in gpos:
990         if not gpo_obj.file_sys_path:
991             continue
992         cache_gpo_dir(conn, cache_path, check_safe_path(gpo_obj.file_sys_path))
993
994
995 def get_deleted_gpos_list(gp_db, gpos):
996     applied_gpos = gp_db.get_applied_guids()
997     current_guids = set([p.name for p in gpos])
998     deleted_gpos = [guid for guid in applied_gpos if guid not in current_guids]
999     return gp_db.get_applied_settings(deleted_gpos)
1000
1001 def gpo_version(lp, path):
1002     # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
1003     # read from the gpo client cache.
1004     gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
1005     return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
1006
1007
1008 def apply_gp(lp, creds, store, gp_extensions, username, target, force=False):
1009     gp_db = store.get_gplog(username)
1010     dc_hostname = get_dc_hostname(creds, lp)
1011     gpos = get_gpo_list(dc_hostname, creds, lp, username)
1012     del_gpos = get_deleted_gpos_list(gp_db, gpos)
1013     try:
1014         check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
1015     except:
1016         log.error('Failed downloading gpt cache from \'%s\' using SMB'
1017                   % dc_hostname)
1018         return
1019
1020     if force:
1021         changed_gpos = gpos
1022         gp_db.state(GPOSTATE.ENFORCE)
1023     else:
1024         changed_gpos = []
1025         for gpo_obj in gpos:
1026             if not gpo_obj.file_sys_path:
1027                 continue
1028             guid = gpo_obj.name
1029             path = check_safe_path(gpo_obj.file_sys_path).upper()
1030             version = gpo_version(lp, path)
1031             if version != store.get_int(guid):
1032                 log.info('GPO %s has changed' % guid)
1033                 changed_gpos.append(gpo_obj)
1034         gp_db.state(GPOSTATE.APPLY)
1035
1036     store.start()
1037     for ext in gp_extensions:
1038         try:
1039             ext = ext(lp, creds, username, store)
1040             if target == 'Computer':
1041                 ext.process_group_policy(del_gpos, changed_gpos)
1042             else:
1043                 drop_privileges(username, ext.process_group_policy,
1044                                 del_gpos, changed_gpos)
1045         except Exception as e:
1046             log.error('Failed to apply extension  %s' % str(ext))
1047             _, _, tb = sys.exc_info()
1048             filename, line_number, _, _ = traceback.extract_tb(tb)[-1]
1049             log.error('%s:%d: %s: %s' % (filename, line_number,
1050                                          type(e).__name__, str(e)))
1051             continue
1052     for gpo_obj in gpos:
1053         if not gpo_obj.file_sys_path:
1054             continue
1055         guid = gpo_obj.name
1056         path = check_safe_path(gpo_obj.file_sys_path).upper()
1057         version = gpo_version(lp, path)
1058         store.store(guid, '%i' % version)
1059     store.commit()
1060
1061
1062 def unapply_gp(lp, creds, store, gp_extensions, username, target):
1063     gp_db = store.get_gplog(username)
1064     gp_db.state(GPOSTATE.UNAPPLY)
1065     # Treat all applied gpos as deleted
1066     del_gpos = gp_db.get_applied_settings(gp_db.get_applied_guids())
1067     store.start()
1068     for ext in gp_extensions:
1069         try:
1070             ext = ext(lp, creds, username, store)
1071             if target == 'Computer':
1072                 ext.process_group_policy(del_gpos, [])
1073             else:
1074                 drop_privileges(username, ext.process_group_policy,
1075                                 del_gpos, [])
1076         except Exception as e:
1077             log.error('Failed to unapply extension  %s' % str(ext))
1078             log.error('Message was: ' + str(e))
1079             continue
1080     store.commit()
1081
1082
1083 def __rsop_vals(vals, level=4):
1084     if type(vals) == dict:
1085         ret = [' '*level + '[ %s ] = %s' % (k, __rsop_vals(v, level+2))
1086                 for k, v in vals.items()]
1087         return '\n' + '\n'.join(ret)
1088     elif type(vals) == list:
1089         ret = [' '*level + '[ %s ]' % __rsop_vals(v, level+2) for v in vals]
1090         return '\n' + '\n'.join(ret)
1091     else:
1092         if isinstance(vals, numbers.Number):
1093             return ' '*(level+2) + str(vals)
1094         else:
1095             return ' '*(level+2) + get_string(vals)
1096
1097 def rsop(lp, creds, store, gp_extensions, username, target):
1098     dc_hostname = get_dc_hostname(creds, lp)
1099     gpos = get_gpo_list(dc_hostname, creds, lp, username)
1100     check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
1101
1102     print('Resultant Set of Policy')
1103     print('%s Policy\n' % target)
1104     term_width = shutil.get_terminal_size(fallback=(120, 50))[0]
1105     for gpo_obj in gpos:
1106         if gpo_obj.display_name.strip() == 'Local Policy':
1107             continue # We never apply local policy
1108         print('GPO: %s' % gpo_obj.display_name)
1109         print('='*term_width)
1110         for ext in gp_extensions:
1111             ext = ext(lp, creds, username, store)
1112             cse_name_m = re.findall(r"'([\w\.]+)'", str(type(ext)))
1113             if len(cse_name_m) > 0:
1114                 cse_name = cse_name_m[-1].split('.')[-1]
1115             else:
1116                 cse_name = ext.__module__.split('.')[-1]
1117             print('  CSE: %s' % cse_name)
1118             print('  ' + ('-'*int(term_width/2)))
1119             for section, settings in ext.rsop(gpo_obj).items():
1120                 print('    Policy Type: %s' % section)
1121                 print('    ' + ('-'*int(term_width/2)))
1122                 print(__rsop_vals(settings).lstrip('\n'))
1123                 print('    ' + ('-'*int(term_width/2)))
1124             print('  ' + ('-'*int(term_width/2)))
1125         print('%s\n' % ('='*term_width))
1126
1127
1128 def parse_gpext_conf(smb_conf):
1129     from samba.samba3 import param as s3param
1130     lp = s3param.get_context()
1131     if smb_conf is not None:
1132         lp.load(smb_conf)
1133     else:
1134         lp.load_default()
1135     ext_conf = lp.state_path('gpext.conf')
1136     parser = ConfigParser(interpolation=None)
1137     parser.read(ext_conf)
1138     return lp, parser
1139
1140
1141 def atomic_write_conf(lp, parser):
1142     ext_conf = lp.state_path('gpext.conf')
1143     with NamedTemporaryFile(mode="w+", delete=False, dir=os.path.dirname(ext_conf)) as f:
1144         parser.write(f)
1145         os.rename(f.name, ext_conf)
1146
1147
1148 def check_guid(guid):
1149     # Check for valid guid with curly braces
1150     if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
1151         return False
1152     try:
1153         UUID(guid, version=4)
1154     except ValueError:
1155         return False
1156     return True
1157
1158
1159 def register_gp_extension(guid, name, path,
1160                           smb_conf=None, machine=True, user=True):
1161     # Check that the module exists
1162     if not os.path.exists(path):
1163         return False
1164     if not check_guid(guid):
1165         return False
1166
1167     lp, parser = parse_gpext_conf(smb_conf)
1168     if guid not in parser.sections():
1169         parser.add_section(guid)
1170     parser.set(guid, 'DllName', path)
1171     parser.set(guid, 'ProcessGroupPolicy', name)
1172     parser.set(guid, 'NoMachinePolicy', "0" if machine else "1")
1173     parser.set(guid, 'NoUserPolicy', "0" if user else "1")
1174
1175     atomic_write_conf(lp, parser)
1176
1177     return True
1178
1179
1180 def list_gp_extensions(smb_conf=None):
1181     _, parser = parse_gpext_conf(smb_conf)
1182     results = {}
1183     for guid in parser.sections():
1184         results[guid] = {}
1185         results[guid]['DllName'] = parser.get(guid, 'DllName')
1186         results[guid]['ProcessGroupPolicy'] = \
1187             parser.get(guid, 'ProcessGroupPolicy')
1188         results[guid]['MachinePolicy'] = \
1189             not int(parser.get(guid, 'NoMachinePolicy'))
1190         results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
1191     return results
1192
1193
1194 def unregister_gp_extension(guid, smb_conf=None):
1195     if not check_guid(guid):
1196         return False
1197
1198     lp, parser = parse_gpext_conf(smb_conf)
1199     if guid in parser.sections():
1200         parser.remove_section(guid)
1201
1202     atomic_write_conf(lp, parser)
1203
1204     return True
1205
1206
1207 def set_privileges(username, uid, gid):
1208     """
1209     Set current process privileges
1210     """
1211
1212     os.setegid(gid)
1213     os.seteuid(uid)
1214
1215
1216 def drop_privileges(username, func, *args):
1217     """
1218     Run supplied function with privileges for specified username.
1219     """
1220     current_uid = os.getuid()
1221
1222     if not current_uid == 0:
1223         raise Exception('Not enough permissions to drop privileges')
1224
1225     user_uid = pwd.getpwnam(username).pw_uid
1226     user_gid = pwd.getpwnam(username).pw_gid
1227
1228     # Drop privileges
1229     set_privileges(username, user_uid, user_gid)
1230
1231     # We need to catch exception in order to be able to restore
1232     # privileges later in this function
1233     out = None
1234     exc = None
1235     try:
1236         out = func(*args)
1237     except Exception as e:
1238         exc = e
1239
1240     # Restore privileges
1241     set_privileges('root', current_uid, 0)
1242
1243     if exc:
1244         raise exc
1245
1246     return out
1247
1248 def expand_pref_variables(text, gpt_path, lp, username=None):
1249     utc_dt = datetime.utcnow()
1250     dt = datetime.now()
1251     cache_path = lp.cache_path(os.path.join('gpo_cache'))
1252     # These are all the possible preference variables that MS supports. The
1253     # variables set to 'None' here are currently unsupported by Samba, and will
1254     # prevent the individual policy from applying.
1255     variables = { 'AppDataDir': os.path.expanduser('~/.config'),
1256                   'BinaryComputerSid': None,
1257                   'BinaryUserSid': None,
1258                   'CommonAppdataDir': None,
1259                   'CommonDesktopDir': None,
1260                   'CommonFavoritesDir': None,
1261                   'CommonProgramsDir': None,
1262                   'CommonStartUpDir': None,
1263                   'ComputerName': lp.get('netbios name'),
1264                   'CurrentProccessId': None,
1265                   'CurrentThreadId': None,
1266                   'DateTime': utc_dt.strftime('%Y-%m-%d %H:%M:%S UTC'),
1267                   'DateTimeEx': str(utc_dt),
1268                   'DesktopDir': os.path.expanduser('~/Desktop'),
1269                   'DomainName': lp.get('realm'),
1270                   'FavoritesDir': None,
1271                   'GphPath': None,
1272                   'GptPath': os.path.join(cache_path,
1273                                           check_safe_path(gpt_path).upper()),
1274                   'GroupPolicyVersion': None,
1275                   'LastDriveMapped': None,
1276                   'LastError': None,
1277                   'LastErrorText': None,
1278                   'LdapComputerSid': None,
1279                   'LdapUserSid': None,
1280                   'LocalTime': dt.strftime('%H:%M:%S'),
1281                   'LocalTimeEx': dt.strftime('%H:%M:%S.%f'),
1282                   'LogonDomain': lp.get('realm'),
1283                   'LogonServer': None,
1284                   'LogonUser': username,
1285                   'LogonUserSid': None,
1286                   'MacAddress': None,
1287                   'NetPlacesDir': None,
1288                   'OsVersion': None,
1289                   'ProgramFilesDir': None,
1290                   'ProgramsDir': None,
1291                   'RecentDocumentsDir': None,
1292                   'ResultCode': None,
1293                   'ResultText': None,
1294                   'ReversedComputerSid': None,
1295                   'ReversedUserSid': None,
1296                   'SendToDir': None,
1297                   'StartMenuDir': None,
1298                   'StartUpDir': None,
1299                   'SystemDir': None,
1300                   'SystemDrive': '/',
1301                   'TempDir': '/tmp',
1302                   'TimeStamp': str(datetime.timestamp(dt)),
1303                   'TraceFile': None,
1304                   'WindowsDir': None
1305     }
1306     for exp_var, val in variables.items():
1307         exp_var_fmt = '%%%s%%' % exp_var
1308         if exp_var_fmt in text:
1309             if val is None:
1310                 raise NameError('Expansion variable %s is undefined' % exp_var)
1311             text = text.replace(exp_var_fmt, val)
1312     return text