python: Fix NtVer check for site_dn_for_machine()
[samba.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 not (samlogon_response.ntver & nbt.NETLOGON_NT_VERSION_5EX):
809         raise RuntimeError('site_dn_for_machine: Invalid NtVer in '
810                            + 'netlogon_samlogon_response')
811
812     # We want NETLOGON_NT_VERSION_5EX out of the union!
813     samlogon_response.ntver = nbt.NETLOGON_NT_VERSION_5EX
814     samlogon_response_ex = samlogon_response.data
815
816     client_site = "Default-First-Site-Name"
817     if (samlogon_response_ex.client_site
818             and len(samlogon_response_ex.client_site) > 1):
819         client_site = samlogon_response_ex.client_site
820
821     site_dn = samdb.get_config_basedn()
822     site_dn.add_child("CN=Sites")
823     site_dn.add_child("CN=%s" % (client_site))
824
825     return site_dn
826
827
828
829 def get_gpo_list(dc_hostname, creds, lp, username):
830     """Get the full list of GROUP_POLICY_OBJECTs for a given username.
831     Push GPOs to gpo_list so that the traversal order of the list matches
832     the order of application:
833     (L)ocal (S)ite (D)omain (O)rganizational(U)nit
834     For different domains and OUs: parent-to-child.
835     Within same level of domains and OUs: Link order.
836     Since GPOs are pushed to the front of gpo_list, GPOs have to be
837     pushed in the opposite order of application (OUs first, local last,
838     child-to-parent).
839     Forced GPOs are appended in the end since they override all others.
840     """
841     gpo_list = []
842     forced_gpo_list = []
843     url = 'ldap://' + dc_hostname
844     samdb = SamDB(url=url,
845                   session_info=system_session(),
846                   credentials=creds, lp=lp)
847     # username is DOM\\SAM, but get_gpo_list expects SAM
848     uac, dn = find_samaccount(samdb, username.split('\\')[-1])
849     add_only_forced_gpos = False
850
851     # Fetch the security token
852     session_info_flags = (AUTH_SESSION_INFO_DEFAULT_GROUPS |
853                           AUTH_SESSION_INFO_AUTHENTICATED)
854     if url.startswith('ldap'):
855         session_info_flags |= AUTH_SESSION_INFO_SIMPLE_PRIVILEGES
856     session = samba.auth.user_session(samdb, lp_ctx=lp, dn=dn,
857                                       session_info_flags=session_info_flags)
858     gpo_list_machine = False
859     if uac & UF_WORKSTATION_TRUST_ACCOUNT or uac & UF_SERVER_TRUST_ACCOUNT:
860         gpo_list_machine = True
861         token = merge_with_system_token(session.security_token)
862     else:
863         token = session.security_token
864
865     # (O)rganizational(U)nit
866     parent_dn = dn.parent()
867     while True:
868         if str(parent_dn) == str(samdb.get_default_basedn().parent()):
869             break
870
871         # An account can be a member of more OUs
872         if parent_dn.get_component_name(0) == 'OU':
873             try:
874                 log.debug("get_gpo_list: query OU: [%s] for GPOs" % parent_dn)
875                 gp_link = get_gpo_link(samdb, parent_dn)
876             except ldb.LdbError as e:
877                 (enum, estr) = e.args
878                 log.debug(estr)
879             else:
880                 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
881                                        parent_dn, gp_link,
882                                        gpo.GP_LINK_OU,
883                                        add_only_forced_gpos, token)
884
885                 # block inheritance from now on
886                 if gp_link.gp_opts & GPO_BLOCK_INHERITANCE:
887                     add_only_forced_gpos = True
888
889         parent_dn = parent_dn.parent()
890
891     # (D)omain
892     parent_dn = dn.parent()
893     while True:
894         if str(parent_dn) == str(samdb.get_default_basedn().parent()):
895             break
896
897         # An account can just be a member of one domain
898         if parent_dn.get_component_name(0) == 'DC':
899             try:
900                 log.debug("get_gpo_list: query DC: [%s] for GPOs" % parent_dn)
901                 gp_link = get_gpo_link(samdb, parent_dn)
902             except ldb.LdbError as e:
903                 (enum, estr) = e.args
904                 log.debug(estr)
905             else:
906                 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
907                                        parent_dn, gp_link,
908                                        gpo.GP_LINK_DOMAIN,
909                                        add_only_forced_gpos, token)
910
911                 # block inheritance from now on
912                 if gp_link.gp_opts & GPO_BLOCK_INHERITANCE:
913                     add_only_forced_gpos = True
914
915         parent_dn = parent_dn.parent()
916
917     # (S)ite
918     if gpo_list_machine:
919         try:
920             site_dn = site_dn_for_machine(samdb, dc_hostname, lp, creds, username)
921
922             try:
923                 log.debug("get_gpo_list: query SITE: [%s] for GPOs" % site_dn)
924                 gp_link = get_gpo_link(samdb, site_dn)
925             except ldb.LdbError as e:
926                 (enum, estr) = e.args
927                 log.debug(estr)
928             else:
929                 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
930                                        site_dn, gp_link,
931                                        gpo.GP_LINK_SITE,
932                                        add_only_forced_gpos, token)
933         except ldb.LdbError:
934             # [MS-GPOL] 3.2.5.1.4 Site Search: If the method returns
935             # ERROR_NO_SITENAME, the remainder of this message MUST be skipped
936             # and the protocol sequence MUST continue at GPO Search
937             pass
938
939     # (L)ocal
940     gpo_list.insert(0, gpo.GROUP_POLICY_OBJECT("Local Policy",
941                                                "Local Policy",
942                                                gpo.GP_LINK_LOCAL))
943
944     # Append |forced_gpo_list| at the end of |gpo_list|,
945     # so that forced GPOs are applied on top of non enforced GPOs.
946     return gpo_list+forced_gpo_list
947
948
949 def cache_gpo_dir(conn, cache, sub_dir):
950     loc_sub_dir = sub_dir.upper()
951     local_dir = os.path.join(cache, loc_sub_dir)
952     try:
953         os.makedirs(local_dir, mode=0o755)
954     except OSError as e:
955         if e.errno != errno.EEXIST:
956             raise
957     for fdata in conn.list(sub_dir):
958         if fdata['attrib'] & libsmb.FILE_ATTRIBUTE_DIRECTORY:
959             cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
960         else:
961             local_name = fdata['name'].upper()
962             f = NamedTemporaryFile(delete=False, dir=local_dir)
963             fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
964             f.write(conn.loadfile(fname))
965             f.close()
966             os.rename(f.name, os.path.join(local_dir, local_name))
967
968
969 def check_safe_path(path):
970     dirs = re.split('/|\\\\', path)
971     if 'sysvol' in path.lower():
972         ldirs = re.split('/|\\\\', path.lower())
973         dirs = dirs[ldirs.index('sysvol') + 1:]
974     if '..' not in dirs:
975         return os.path.join(*dirs)
976     raise OSError(path)
977
978
979 def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
980     # Force signing for the connection
981     saved_signing_state = creds.get_smb_signing()
982     creds.set_smb_signing(SMB_SIGNING_REQUIRED)
983     conn = libsmb.Conn(dc_hostname, 'sysvol', lp=lp, creds=creds)
984     # Reset signing state
985     creds.set_smb_signing(saved_signing_state)
986     cache_path = lp.cache_path('gpo_cache')
987     for gpo_obj in gpos:
988         if not gpo_obj.file_sys_path:
989             continue
990         cache_gpo_dir(conn, cache_path, check_safe_path(gpo_obj.file_sys_path))
991
992
993 def get_deleted_gpos_list(gp_db, gpos):
994     applied_gpos = gp_db.get_applied_guids()
995     current_guids = set([p.name for p in gpos])
996     deleted_gpos = [guid for guid in applied_gpos if guid not in current_guids]
997     return gp_db.get_applied_settings(deleted_gpos)
998
999 def gpo_version(lp, path):
1000     # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
1001     # read from the gpo client cache.
1002     gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
1003     return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
1004
1005
1006 def apply_gp(lp, creds, store, gp_extensions, username, target, force=False):
1007     gp_db = store.get_gplog(username)
1008     dc_hostname = get_dc_hostname(creds, lp)
1009     gpos = get_gpo_list(dc_hostname, creds, lp, username)
1010     del_gpos = get_deleted_gpos_list(gp_db, gpos)
1011     try:
1012         check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
1013     except:
1014         log.error('Failed downloading gpt cache from \'%s\' using SMB'
1015                   % dc_hostname)
1016         return
1017
1018     if force:
1019         changed_gpos = gpos
1020         gp_db.state(GPOSTATE.ENFORCE)
1021     else:
1022         changed_gpos = []
1023         for gpo_obj in gpos:
1024             if not gpo_obj.file_sys_path:
1025                 continue
1026             guid = gpo_obj.name
1027             path = check_safe_path(gpo_obj.file_sys_path).upper()
1028             version = gpo_version(lp, path)
1029             if version != store.get_int(guid):
1030                 log.info('GPO %s has changed' % guid)
1031                 changed_gpos.append(gpo_obj)
1032         gp_db.state(GPOSTATE.APPLY)
1033
1034     store.start()
1035     for ext in gp_extensions:
1036         try:
1037             ext = ext(lp, creds, username, store)
1038             if target == 'Computer':
1039                 ext.process_group_policy(del_gpos, changed_gpos)
1040             else:
1041                 drop_privileges(username, ext.process_group_policy,
1042                                 del_gpos, changed_gpos)
1043         except Exception as e:
1044             log.error('Failed to apply extension  %s' % str(ext))
1045             _, _, tb = sys.exc_info()
1046             filename, line_number, _, _ = traceback.extract_tb(tb)[-1]
1047             log.error('%s:%d: %s: %s' % (filename, line_number,
1048                                          type(e).__name__, str(e)))
1049             continue
1050     for gpo_obj in gpos:
1051         if not gpo_obj.file_sys_path:
1052             continue
1053         guid = gpo_obj.name
1054         path = check_safe_path(gpo_obj.file_sys_path).upper()
1055         version = gpo_version(lp, path)
1056         store.store(guid, '%i' % version)
1057     store.commit()
1058
1059
1060 def unapply_gp(lp, creds, store, gp_extensions, username, target):
1061     gp_db = store.get_gplog(username)
1062     gp_db.state(GPOSTATE.UNAPPLY)
1063     # Treat all applied gpos as deleted
1064     del_gpos = gp_db.get_applied_settings(gp_db.get_applied_guids())
1065     store.start()
1066     for ext in gp_extensions:
1067         try:
1068             ext = ext(lp, creds, username, store)
1069             if target == 'Computer':
1070                 ext.process_group_policy(del_gpos, [])
1071             else:
1072                 drop_privileges(username, ext.process_group_policy,
1073                                 del_gpos, [])
1074         except Exception as e:
1075             log.error('Failed to unapply extension  %s' % str(ext))
1076             log.error('Message was: ' + str(e))
1077             continue
1078     store.commit()
1079
1080
1081 def __rsop_vals(vals, level=4):
1082     if type(vals) == dict:
1083         ret = [' '*level + '[ %s ] = %s' % (k, __rsop_vals(v, level+2))
1084                 for k, v in vals.items()]
1085         return '\n' + '\n'.join(ret)
1086     elif type(vals) == list:
1087         ret = [' '*level + '[ %s ]' % __rsop_vals(v, level+2) for v in vals]
1088         return '\n' + '\n'.join(ret)
1089     else:
1090         if isinstance(vals, numbers.Number):
1091             return ' '*(level+2) + str(vals)
1092         else:
1093             return ' '*(level+2) + get_string(vals)
1094
1095 def rsop(lp, creds, store, gp_extensions, username, target):
1096     dc_hostname = get_dc_hostname(creds, lp)
1097     gpos = get_gpo_list(dc_hostname, creds, lp, username)
1098     check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
1099
1100     print('Resultant Set of Policy')
1101     print('%s Policy\n' % target)
1102     term_width = shutil.get_terminal_size(fallback=(120, 50))[0]
1103     for gpo_obj in gpos:
1104         if gpo_obj.display_name.strip() == 'Local Policy':
1105             continue # We never apply local policy
1106         print('GPO: %s' % gpo_obj.display_name)
1107         print('='*term_width)
1108         for ext in gp_extensions:
1109             ext = ext(lp, creds, username, store)
1110             cse_name_m = re.findall(r"'([\w\.]+)'", str(type(ext)))
1111             if len(cse_name_m) > 0:
1112                 cse_name = cse_name_m[-1].split('.')[-1]
1113             else:
1114                 cse_name = ext.__module__.split('.')[-1]
1115             print('  CSE: %s' % cse_name)
1116             print('  ' + ('-'*int(term_width/2)))
1117             for section, settings in ext.rsop(gpo_obj).items():
1118                 print('    Policy Type: %s' % section)
1119                 print('    ' + ('-'*int(term_width/2)))
1120                 print(__rsop_vals(settings).lstrip('\n'))
1121                 print('    ' + ('-'*int(term_width/2)))
1122             print('  ' + ('-'*int(term_width/2)))
1123         print('%s\n' % ('='*term_width))
1124
1125
1126 def parse_gpext_conf(smb_conf):
1127     from samba.samba3 import param as s3param
1128     lp = s3param.get_context()
1129     if smb_conf is not None:
1130         lp.load(smb_conf)
1131     else:
1132         lp.load_default()
1133     ext_conf = lp.state_path('gpext.conf')
1134     parser = ConfigParser(interpolation=None)
1135     parser.read(ext_conf)
1136     return lp, parser
1137
1138
1139 def atomic_write_conf(lp, parser):
1140     ext_conf = lp.state_path('gpext.conf')
1141     with NamedTemporaryFile(mode="w+", delete=False, dir=os.path.dirname(ext_conf)) as f:
1142         parser.write(f)
1143         os.rename(f.name, ext_conf)
1144
1145
1146 def check_guid(guid):
1147     # Check for valid guid with curly braces
1148     if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
1149         return False
1150     try:
1151         UUID(guid, version=4)
1152     except ValueError:
1153         return False
1154     return True
1155
1156
1157 def register_gp_extension(guid, name, path,
1158                           smb_conf=None, machine=True, user=True):
1159     # Check that the module exists
1160     if not os.path.exists(path):
1161         return False
1162     if not check_guid(guid):
1163         return False
1164
1165     lp, parser = parse_gpext_conf(smb_conf)
1166     if guid not in parser.sections():
1167         parser.add_section(guid)
1168     parser.set(guid, 'DllName', path)
1169     parser.set(guid, 'ProcessGroupPolicy', name)
1170     parser.set(guid, 'NoMachinePolicy', "0" if machine else "1")
1171     parser.set(guid, 'NoUserPolicy', "0" if user else "1")
1172
1173     atomic_write_conf(lp, parser)
1174
1175     return True
1176
1177
1178 def list_gp_extensions(smb_conf=None):
1179     _, parser = parse_gpext_conf(smb_conf)
1180     results = {}
1181     for guid in parser.sections():
1182         results[guid] = {}
1183         results[guid]['DllName'] = parser.get(guid, 'DllName')
1184         results[guid]['ProcessGroupPolicy'] = \
1185             parser.get(guid, 'ProcessGroupPolicy')
1186         results[guid]['MachinePolicy'] = \
1187             not int(parser.get(guid, 'NoMachinePolicy'))
1188         results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
1189     return results
1190
1191
1192 def unregister_gp_extension(guid, smb_conf=None):
1193     if not check_guid(guid):
1194         return False
1195
1196     lp, parser = parse_gpext_conf(smb_conf)
1197     if guid in parser.sections():
1198         parser.remove_section(guid)
1199
1200     atomic_write_conf(lp, parser)
1201
1202     return True
1203
1204
1205 def set_privileges(username, uid, gid):
1206     """
1207     Set current process privileges
1208     """
1209
1210     os.setegid(gid)
1211     os.seteuid(uid)
1212
1213
1214 def drop_privileges(username, func, *args):
1215     """
1216     Run supplied function with privileges for specified username.
1217     """
1218     current_uid = os.getuid()
1219
1220     if not current_uid == 0:
1221         raise Exception('Not enough permissions to drop privileges')
1222
1223     user_uid = pwd.getpwnam(username).pw_uid
1224     user_gid = pwd.getpwnam(username).pw_gid
1225
1226     # Drop privileges
1227     set_privileges(username, user_uid, user_gid)
1228
1229     # We need to catch exception in order to be able to restore
1230     # privileges later in this function
1231     out = None
1232     exc = None
1233     try:
1234         out = func(*args)
1235     except Exception as e:
1236         exc = e
1237
1238     # Restore privileges
1239     set_privileges('root', current_uid, 0)
1240
1241     if exc:
1242         raise exc
1243
1244     return out
1245
1246 def expand_pref_variables(text, gpt_path, lp, username=None):
1247     utc_dt = datetime.utcnow()
1248     dt = datetime.now()
1249     cache_path = lp.cache_path(os.path.join('gpo_cache'))
1250     # These are all the possible preference variables that MS supports. The
1251     # variables set to 'None' here are currently unsupported by Samba, and will
1252     # prevent the individual policy from applying.
1253     variables = { 'AppDataDir': os.path.expanduser('~/.config'),
1254                   'BinaryComputerSid': None,
1255                   'BinaryUserSid': None,
1256                   'CommonAppdataDir': None,
1257                   'CommonDesktopDir': None,
1258                   'CommonFavoritesDir': None,
1259                   'CommonProgramsDir': None,
1260                   'CommonStartUpDir': None,
1261                   'ComputerName': lp.get('netbios name'),
1262                   'CurrentProccessId': None,
1263                   'CurrentThreadId': None,
1264                   'DateTime': utc_dt.strftime('%Y-%m-%d %H:%M:%S UTC'),
1265                   'DateTimeEx': str(utc_dt),
1266                   'DesktopDir': os.path.expanduser('~/Desktop'),
1267                   'DomainName': lp.get('realm'),
1268                   'FavoritesDir': None,
1269                   'GphPath': None,
1270                   'GptPath': os.path.join(cache_path,
1271                                           check_safe_path(gpt_path).upper()),
1272                   'GroupPolicyVersion': None,
1273                   'LastDriveMapped': None,
1274                   'LastError': None,
1275                   'LastErrorText': None,
1276                   'LdapComputerSid': None,
1277                   'LdapUserSid': None,
1278                   'LocalTime': dt.strftime('%H:%M:%S'),
1279                   'LocalTimeEx': dt.strftime('%H:%M:%S.%f'),
1280                   'LogonDomain': lp.get('realm'),
1281                   'LogonServer': None,
1282                   'LogonUser': username,
1283                   'LogonUserSid': None,
1284                   'MacAddress': None,
1285                   'NetPlacesDir': None,
1286                   'OsVersion': None,
1287                   'ProgramFilesDir': None,
1288                   'ProgramsDir': None,
1289                   'RecentDocumentsDir': None,
1290                   'ResultCode': None,
1291                   'ResultText': None,
1292                   'ReversedComputerSid': None,
1293                   'ReversedUserSid': None,
1294                   'SendToDir': None,
1295                   'StartMenuDir': None,
1296                   'StartUpDir': None,
1297                   'SystemDir': None,
1298                   'SystemDrive': '/',
1299                   'TempDir': '/tmp',
1300                   'TimeStamp': str(datetime.timestamp(dt)),
1301                   'TraceFile': None,
1302                   'WindowsDir': None
1303     }
1304     for exp_var, val in variables.items():
1305         exp_var_fmt = '%%%s%%' % exp_var
1306         if exp_var_fmt in text:
1307             if val is None:
1308                 raise NameError('Expansion variable %s is undefined' % exp_var)
1309             text = text.replace(exp_var_fmt, val)
1310     return text