26c2386847e0067fc93d1f08ef2d9874e98a882d
[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 netlogon
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 def get_dc_netbios_hostname(creds, lp):
615     net = Net(creds=creds, lp=lp)
616     cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
617                                                           nbt.NBT_SERVER_DS))
618     return cldap_ret.pdc_name
619
620
621 """ Fetch a list of GUIDs for applicable GPOs """
622
623
624 def get_gpo(samdb, gpo_dn):
625     g = gpo.GROUP_POLICY_OBJECT()
626     attrs = [
627         "cn",
628         "displayName",
629         "flags",
630         "gPCFileSysPath",
631         "gPCFunctionalityVersion",
632         "gPCMachineExtensionNames",
633         "gPCUserExtensionNames",
634         "gPCWQLFilter",
635         "name",
636         "nTSecurityDescriptor",
637         "versionNumber"
638     ]
639     if gpo_dn.startswith("LDAP://"):
640         gpo_dn = gpo_dn.lstrip("LDAP://")
641
642     sd_flags = (security.SECINFO_OWNER |
643                 security.SECINFO_GROUP |
644                 security.SECINFO_DACL)
645     try:
646         res = samdb.search(gpo_dn, ldb.SCOPE_BASE, "(objectclass=*)", attrs,
647                            controls=['sd_flags:1:%d' % sd_flags])
648     except Exception:
649         log.error('Failed to fetch gpo object with nTSecurityDescriptor')
650         raise
651     if res.count != 1:
652         raise ldb.LdbError(ldb.ERR_NO_SUCH_OBJECT,
653                            'get_gpo: search failed')
654
655     g.ds_path = gpo_dn
656     if 'versionNumber' in res.msgs[0].keys():
657         g.version = int(res.msgs[0]['versionNumber'][0])
658     if 'flags' in res.msgs[0].keys():
659         g.options = int(res.msgs[0]['flags'][0])
660     if 'gPCFileSysPath' in res.msgs[0].keys():
661         g.file_sys_path = res.msgs[0]['gPCFileSysPath'][0].decode()
662     if 'displayName' in res.msgs[0].keys():
663         g.display_name = res.msgs[0]['displayName'][0].decode()
664     if 'name' in res.msgs[0].keys():
665         g.name = res.msgs[0]['name'][0].decode()
666     if 'gPCMachineExtensionNames' in res.msgs[0].keys():
667         g.machine_extensions = str(res.msgs[0]['gPCMachineExtensionNames'][0])
668     if 'gPCUserExtensionNames' in res.msgs[0].keys():
669         g.user_extensions = str(res.msgs[0]['gPCUserExtensionNames'][0])
670     if 'nTSecurityDescriptor' in res.msgs[0].keys():
671         g.set_sec_desc(bytes(res.msgs[0]['nTSecurityDescriptor'][0]))
672     return g
673
674 class GP_LINK:
675     def __init__(self, gPLink, gPOptions):
676         self.link_names = []
677         self.link_opts = []
678         self.gpo_parse_gplink(gPLink)
679         self.gp_opts = int(gPOptions)
680
681     def gpo_parse_gplink(self, gPLink):
682         for p in gPLink.decode().split(']'):
683             if not p:
684                 continue
685             log.debug('gpo_parse_gplink: processing link')
686             p = p.lstrip('[')
687             link_name, link_opt = p.split(';')
688             log.debug('gpo_parse_gplink: link: {}'.format(link_name))
689             log.debug('gpo_parse_gplink: opt: {}'.format(link_opt))
690             self.link_names.append(link_name)
691             self.link_opts.append(int(link_opt))
692
693     def num_links(self):
694         if len(self.link_names) != len(self.link_opts):
695             raise RuntimeError('Link names and opts mismatch')
696         return len(self.link_names)
697
698 def find_samaccount(samdb, samaccountname):
699     attrs = ['dn', 'userAccountControl']
700     res = samdb.search(samdb.get_default_basedn(), ldb.SCOPE_SUBTREE,
701                        '(sAMAccountName={})'.format(samaccountname), attrs)
702     if res.count != 1:
703         raise ldb.LdbError(ldb.ERR_NO_SUCH_OBJECT,
704             "Failed to find samAccountName '{}'".format(samaccountname)
705         )
706     uac = int(res.msgs[0]['userAccountControl'][0])
707     dn = res.msgs[0]['dn']
708     log.info('Found dn {} for samaccountname {}'.format(dn, samaccountname))
709     return uac, dn
710
711 def get_gpo_link(samdb, link_dn):
712     res = samdb.search(link_dn, ldb.SCOPE_BASE,
713                        '(objectclass=*)', ['gPLink', 'gPOptions'])
714     if res.count != 1:
715         raise ldb.LdbError(ldb.ERR_NO_SUCH_OBJECT, 'get_gpo_link: no result')
716     if 'gPLink' not in res.msgs[0]:
717         raise ldb.LdbError(ldb.ERR_NO_SUCH_ATTRIBUTE,
718             "get_gpo_link: no 'gPLink' attribute found for '{}'".format(link_dn)
719         )
720     gPLink = res.msgs[0]['gPLink'][0]
721     gPOptions = 0
722     if 'gPOptions' in res.msgs[0]:
723         gPOptions = res.msgs[0]['gPOptions'][0]
724     else:
725         log.debug("get_gpo_link: no 'gPOptions' attribute found")
726     return GP_LINK(gPLink, gPOptions)
727
728 def add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list, link_dn, gp_link,
729                            link_type, only_add_forced_gpos, token):
730     for i in range(gp_link.num_links()-1, -1, -1):
731         is_forced = (gp_link.link_opts[i] & GPLINK_OPT_ENFORCE) != 0
732         if gp_link.link_opts[i] & GPLINK_OPT_DISABLE:
733             log.debug('skipping disabled GPO')
734             continue
735
736         if only_add_forced_gpos:
737             if not is_forced:
738                 log.debug("skipping nonenforced GPO link "
739                           "because GPOPTIONS_BLOCK_INHERITANCE "
740                           "has been set")
741                 continue
742             else:
743                 log.debug("adding enforced GPO link although "
744                           "the GPOPTIONS_BLOCK_INHERITANCE "
745                           "has been set")
746
747         try:
748             new_gpo = get_gpo(samdb, gp_link.link_names[i])
749         except ldb.LdbError as e:
750             (enum, estr) = e.args
751             log.debug("failed to get gpo: %s" % gp_link.link_names[i])
752             if enum == ldb.ERR_NO_SUCH_OBJECT:
753                 log.debug("skipping empty gpo: %s" % gp_link.link_names[i])
754                 continue
755             return
756         else:
757             try:
758                 sec_desc = ndr_unpack(security.descriptor,
759                                       new_gpo.get_sec_desc_buf())
760                 samba.security.access_check(sec_desc, token,
761                                             security.SEC_STD_READ_CONTROL |
762                                             security.SEC_ADS_LIST |
763                                             security.SEC_ADS_READ_PROP)
764             except Exception as e:
765                 log.debug("skipping GPO \"%s\" as object "
766                           "has no access to it" % new_gpo.display_name)
767                 continue
768
769             new_gpo.link = str(link_dn)
770             new_gpo.link_type = link_type
771
772             if is_forced:
773                 forced_gpo_list.insert(0, new_gpo)
774             else:
775                 gpo_list.insert(0, new_gpo)
776
777             log.debug("add_gplink_to_gpo_list: added GPLINK #%d %s "
778                       "to GPO list" % (i, gp_link.link_names[i]))
779
780 def merge_with_system_token(token_1):
781     sids = token_1.sids
782     system_token = system_session().security_token
783     sids.extend(system_token.sids)
784     token_1.sids = sids
785     token_1.rights_mask |= system_token.rights_mask
786     token_1.privilege_mask |= system_token.privilege_mask
787     # There are no claims in the system token, so it is safe not to merge the claims
788     return token_1
789
790 def site_dn_for_machine(samdb, dc_hostname, lp, creds, hostname):
791     # [MS-GPOL] 3.2.5.1.4 Site Search
792     config_context = samdb.get_config_basedn()
793     try:
794         c = netlogon.netlogon("ncacn_np:%s[seal]" % dc_hostname, lp, creds)
795         site_name = c.netr_DsRGetSiteName(hostname)
796         return 'CN={},CN=Sites,{}'.format(site_name, config_context)
797     except WERRORError:
798         # Fallback to the old method found in ads_site_dn_for_machine
799         nb_hostname = get_dc_netbios_hostname(creds, lp)
800         res = samdb.search(config_context, ldb.SCOPE_SUBTREE,
801                            "(cn=%s)" % nb_hostname, ['dn'])
802         if res.count != 1:
803             raise ldb.LdbError(ldb.ERR_NO_SUCH_OBJECT,
804                                'site_dn_for_machine: no result')
805         dn = res.msgs[0]['dn']
806         site_dn = dn.parent().parent()
807         return site_dn
808
809 def get_gpo_list(dc_hostname, creds, lp, username):
810     """Get the full list of GROUP_POLICY_OBJECTs for a given username.
811     Push GPOs to gpo_list so that the traversal order of the list matches
812     the order of application:
813     (L)ocal (S)ite (D)omain (O)rganizational(U)nit
814     For different domains and OUs: parent-to-child.
815     Within same level of domains and OUs: Link order.
816     Since GPOs are pushed to the front of gpo_list, GPOs have to be
817     pushed in the opposite order of application (OUs first, local last,
818     child-to-parent).
819     Forced GPOs are appended in the end since they override all others.
820     """
821     gpo_list = []
822     forced_gpo_list = []
823     url = 'ldap://' + dc_hostname
824     samdb = SamDB(url=url,
825                   session_info=system_session(),
826                   credentials=creds, lp=lp)
827     # username is DOM\\SAM, but get_gpo_list expects SAM
828     uac, dn = find_samaccount(samdb, username.split('\\')[-1])
829     add_only_forced_gpos = False
830
831     # Fetch the security token
832     session_info_flags = (AUTH_SESSION_INFO_DEFAULT_GROUPS |
833                           AUTH_SESSION_INFO_AUTHENTICATED)
834     if url.startswith('ldap'):
835         session_info_flags |= AUTH_SESSION_INFO_SIMPLE_PRIVILEGES
836     session = samba.auth.user_session(samdb, lp_ctx=lp, dn=dn,
837                                       session_info_flags=session_info_flags)
838     gpo_list_machine = False
839     if uac & UF_WORKSTATION_TRUST_ACCOUNT or uac & UF_SERVER_TRUST_ACCOUNT:
840         gpo_list_machine = True
841         token = merge_with_system_token(session.security_token)
842     else:
843         token = session.security_token
844
845     # (O)rganizational(U)nit
846     parent_dn = dn.parent()
847     while True:
848         if str(parent_dn) == str(samdb.get_default_basedn().parent()):
849             break
850
851         # An account can be a member of more OUs
852         if parent_dn.get_component_name(0) == 'OU':
853             try:
854                 log.debug("get_gpo_list: query OU: [%s] for GPOs" % parent_dn)
855                 gp_link = get_gpo_link(samdb, parent_dn)
856             except ldb.LdbError as e:
857                 (enum, estr) = e.args
858                 log.debug(estr)
859             else:
860                 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
861                                        parent_dn, gp_link,
862                                        gpo.GP_LINK_OU,
863                                        add_only_forced_gpos, token)
864
865                 # block inheritance from now on
866                 if gp_link.gp_opts & GPO_BLOCK_INHERITANCE:
867                     add_only_forced_gpos = True
868
869         parent_dn = parent_dn.parent()
870
871     # (D)omain
872     parent_dn = dn.parent()
873     while True:
874         if str(parent_dn) == str(samdb.get_default_basedn().parent()):
875             break
876
877         # An account can just be a member of one domain
878         if parent_dn.get_component_name(0) == 'DC':
879             try:
880                 log.debug("get_gpo_list: query DC: [%s] for GPOs" % parent_dn)
881                 gp_link = get_gpo_link(samdb, parent_dn)
882             except ldb.LdbError as e:
883                 (enum, estr) = e.args
884                 log.debug(estr)
885             else:
886                 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
887                                        parent_dn, gp_link,
888                                        gpo.GP_LINK_DOMAIN,
889                                        add_only_forced_gpos, token)
890
891                 # block inheritance from now on
892                 if gp_link.gp_opts & GPO_BLOCK_INHERITANCE:
893                     add_only_forced_gpos = True
894
895         parent_dn = parent_dn.parent()
896
897     # (S)ite
898     if gpo_list_machine:
899         try:
900             site_dn = site_dn_for_machine(samdb, dc_hostname, lp, creds, username)
901
902             try:
903                 log.debug("get_gpo_list: query SITE: [%s] for GPOs" % site_dn)
904                 gp_link = get_gpo_link(samdb, site_dn)
905             except ldb.LdbError as e:
906                 (enum, estr) = e.args
907                 log.debug(estr)
908             else:
909                 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
910                                        site_dn, gp_link,
911                                        gpo.GP_LINK_SITE,
912                                        add_only_forced_gpos, token)
913         except ldb.LdbError:
914             # [MS-GPOL] 3.2.5.1.4 Site Search: If the method returns
915             # ERROR_NO_SITENAME, the remainder of this message MUST be skipped
916             # and the protocol sequence MUST continue at GPO Search
917             pass
918
919     # (L)ocal
920     gpo_list.insert(0, gpo.GROUP_POLICY_OBJECT("Local Policy",
921                                                "Local Policy",
922                                                gpo.GP_LINK_LOCAL))
923
924     # Append |forced_gpo_list| at the end of |gpo_list|,
925     # so that forced GPOs are applied on top of non enforced GPOs.
926     return gpo_list+forced_gpo_list
927
928
929 def cache_gpo_dir(conn, cache, sub_dir):
930     loc_sub_dir = sub_dir.upper()
931     local_dir = os.path.join(cache, loc_sub_dir)
932     try:
933         os.makedirs(local_dir, mode=0o755)
934     except OSError as e:
935         if e.errno != errno.EEXIST:
936             raise
937     for fdata in conn.list(sub_dir):
938         if fdata['attrib'] & libsmb.FILE_ATTRIBUTE_DIRECTORY:
939             cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
940         else:
941             local_name = fdata['name'].upper()
942             f = NamedTemporaryFile(delete=False, dir=local_dir)
943             fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
944             f.write(conn.loadfile(fname))
945             f.close()
946             os.rename(f.name, os.path.join(local_dir, local_name))
947
948
949 def check_safe_path(path):
950     dirs = re.split('/|\\\\', path)
951     if 'sysvol' in path.lower():
952         ldirs = re.split('/|\\\\', path.lower())
953         dirs = dirs[ldirs.index('sysvol') + 1:]
954     if '..' not in dirs:
955         return os.path.join(*dirs)
956     raise OSError(path)
957
958
959 def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
960     # Force signing for the connection
961     saved_signing_state = creds.get_smb_signing()
962     creds.set_smb_signing(SMB_SIGNING_REQUIRED)
963     conn = libsmb.Conn(dc_hostname, 'sysvol', lp=lp, creds=creds)
964     # Reset signing state
965     creds.set_smb_signing(saved_signing_state)
966     cache_path = lp.cache_path('gpo_cache')
967     for gpo_obj in gpos:
968         if not gpo_obj.file_sys_path:
969             continue
970         cache_gpo_dir(conn, cache_path, check_safe_path(gpo_obj.file_sys_path))
971
972
973 def get_deleted_gpos_list(gp_db, gpos):
974     applied_gpos = gp_db.get_applied_guids()
975     current_guids = set([p.name for p in gpos])
976     deleted_gpos = [guid for guid in applied_gpos if guid not in current_guids]
977     return gp_db.get_applied_settings(deleted_gpos)
978
979 def gpo_version(lp, path):
980     # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
981     # read from the gpo client cache.
982     gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
983     return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
984
985
986 def apply_gp(lp, creds, store, gp_extensions, username, target, force=False):
987     gp_db = store.get_gplog(username)
988     dc_hostname = get_dc_hostname(creds, lp)
989     gpos = get_gpo_list(dc_hostname, creds, lp, username)
990     del_gpos = get_deleted_gpos_list(gp_db, gpos)
991     try:
992         check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
993     except:
994         log.error('Failed downloading gpt cache from \'%s\' using SMB'
995                   % dc_hostname)
996         return
997
998     if force:
999         changed_gpos = gpos
1000         gp_db.state(GPOSTATE.ENFORCE)
1001     else:
1002         changed_gpos = []
1003         for gpo_obj in gpos:
1004             if not gpo_obj.file_sys_path:
1005                 continue
1006             guid = gpo_obj.name
1007             path = check_safe_path(gpo_obj.file_sys_path).upper()
1008             version = gpo_version(lp, path)
1009             if version != store.get_int(guid):
1010                 log.info('GPO %s has changed' % guid)
1011                 changed_gpos.append(gpo_obj)
1012         gp_db.state(GPOSTATE.APPLY)
1013
1014     store.start()
1015     for ext in gp_extensions:
1016         try:
1017             ext = ext(lp, creds, username, store)
1018             if target == 'Computer':
1019                 ext.process_group_policy(del_gpos, changed_gpos)
1020             else:
1021                 drop_privileges(username, ext.process_group_policy,
1022                                 del_gpos, changed_gpos)
1023         except Exception as e:
1024             log.error('Failed to apply extension  %s' % str(ext))
1025             _, _, tb = sys.exc_info()
1026             filename, line_number, _, _ = traceback.extract_tb(tb)[-1]
1027             log.error('%s:%d: %s: %s' % (filename, line_number,
1028                                          type(e).__name__, str(e)))
1029             continue
1030     for gpo_obj in gpos:
1031         if not gpo_obj.file_sys_path:
1032             continue
1033         guid = gpo_obj.name
1034         path = check_safe_path(gpo_obj.file_sys_path).upper()
1035         version = gpo_version(lp, path)
1036         store.store(guid, '%i' % version)
1037     store.commit()
1038
1039
1040 def unapply_gp(lp, creds, store, gp_extensions, username, target):
1041     gp_db = store.get_gplog(username)
1042     gp_db.state(GPOSTATE.UNAPPLY)
1043     # Treat all applied gpos as deleted
1044     del_gpos = gp_db.get_applied_settings(gp_db.get_applied_guids())
1045     store.start()
1046     for ext in gp_extensions:
1047         try:
1048             ext = ext(lp, creds, username, store)
1049             if target == 'Computer':
1050                 ext.process_group_policy(del_gpos, [])
1051             else:
1052                 drop_privileges(username, ext.process_group_policy,
1053                                 del_gpos, [])
1054         except Exception as e:
1055             log.error('Failed to unapply extension  %s' % str(ext))
1056             log.error('Message was: ' + str(e))
1057             continue
1058     store.commit()
1059
1060
1061 def __rsop_vals(vals, level=4):
1062     if type(vals) == dict:
1063         ret = [' '*level + '[ %s ] = %s' % (k, __rsop_vals(v, level+2))
1064                 for k, v in vals.items()]
1065         return '\n' + '\n'.join(ret)
1066     elif type(vals) == list:
1067         ret = [' '*level + '[ %s ]' % __rsop_vals(v, level+2) for v in vals]
1068         return '\n' + '\n'.join(ret)
1069     else:
1070         if isinstance(vals, numbers.Number):
1071             return ' '*(level+2) + str(vals)
1072         else:
1073             return ' '*(level+2) + get_string(vals)
1074
1075 def rsop(lp, creds, store, gp_extensions, username, target):
1076     dc_hostname = get_dc_hostname(creds, lp)
1077     gpos = get_gpo_list(dc_hostname, creds, lp, username)
1078     check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
1079
1080     print('Resultant Set of Policy')
1081     print('%s Policy\n' % target)
1082     term_width = shutil.get_terminal_size(fallback=(120, 50))[0]
1083     for gpo_obj in gpos:
1084         if gpo_obj.display_name.strip() == 'Local Policy':
1085             continue # We never apply local policy
1086         print('GPO: %s' % gpo_obj.display_name)
1087         print('='*term_width)
1088         for ext in gp_extensions:
1089             ext = ext(lp, creds, username, store)
1090             cse_name_m = re.findall(r"'([\w\.]+)'", str(type(ext)))
1091             if len(cse_name_m) > 0:
1092                 cse_name = cse_name_m[-1].split('.')[-1]
1093             else:
1094                 cse_name = ext.__module__.split('.')[-1]
1095             print('  CSE: %s' % cse_name)
1096             print('  ' + ('-'*int(term_width/2)))
1097             for section, settings in ext.rsop(gpo_obj).items():
1098                 print('    Policy Type: %s' % section)
1099                 print('    ' + ('-'*int(term_width/2)))
1100                 print(__rsop_vals(settings).lstrip('\n'))
1101                 print('    ' + ('-'*int(term_width/2)))
1102             print('  ' + ('-'*int(term_width/2)))
1103         print('%s\n' % ('='*term_width))
1104
1105
1106 def parse_gpext_conf(smb_conf):
1107     from samba.samba3 import param as s3param
1108     lp = s3param.get_context()
1109     if smb_conf is not None:
1110         lp.load(smb_conf)
1111     else:
1112         lp.load_default()
1113     ext_conf = lp.state_path('gpext.conf')
1114     parser = ConfigParser(interpolation=None)
1115     parser.read(ext_conf)
1116     return lp, parser
1117
1118
1119 def atomic_write_conf(lp, parser):
1120     ext_conf = lp.state_path('gpext.conf')
1121     with NamedTemporaryFile(mode="w+", delete=False, dir=os.path.dirname(ext_conf)) as f:
1122         parser.write(f)
1123         os.rename(f.name, ext_conf)
1124
1125
1126 def check_guid(guid):
1127     # Check for valid guid with curly braces
1128     if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
1129         return False
1130     try:
1131         UUID(guid, version=4)
1132     except ValueError:
1133         return False
1134     return True
1135
1136
1137 def register_gp_extension(guid, name, path,
1138                           smb_conf=None, machine=True, user=True):
1139     # Check that the module exists
1140     if not os.path.exists(path):
1141         return False
1142     if not check_guid(guid):
1143         return False
1144
1145     lp, parser = parse_gpext_conf(smb_conf)
1146     if guid not in parser.sections():
1147         parser.add_section(guid)
1148     parser.set(guid, 'DllName', path)
1149     parser.set(guid, 'ProcessGroupPolicy', name)
1150     parser.set(guid, 'NoMachinePolicy', "0" if machine else "1")
1151     parser.set(guid, 'NoUserPolicy', "0" if user else "1")
1152
1153     atomic_write_conf(lp, parser)
1154
1155     return True
1156
1157
1158 def list_gp_extensions(smb_conf=None):
1159     _, parser = parse_gpext_conf(smb_conf)
1160     results = {}
1161     for guid in parser.sections():
1162         results[guid] = {}
1163         results[guid]['DllName'] = parser.get(guid, 'DllName')
1164         results[guid]['ProcessGroupPolicy'] = \
1165             parser.get(guid, 'ProcessGroupPolicy')
1166         results[guid]['MachinePolicy'] = \
1167             not int(parser.get(guid, 'NoMachinePolicy'))
1168         results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
1169     return results
1170
1171
1172 def unregister_gp_extension(guid, smb_conf=None):
1173     if not check_guid(guid):
1174         return False
1175
1176     lp, parser = parse_gpext_conf(smb_conf)
1177     if guid in parser.sections():
1178         parser.remove_section(guid)
1179
1180     atomic_write_conf(lp, parser)
1181
1182     return True
1183
1184
1185 def set_privileges(username, uid, gid):
1186     """
1187     Set current process privileges
1188     """
1189
1190     os.setegid(gid)
1191     os.seteuid(uid)
1192
1193
1194 def drop_privileges(username, func, *args):
1195     """
1196     Run supplied function with privileges for specified username.
1197     """
1198     current_uid = os.getuid()
1199
1200     if not current_uid == 0:
1201         raise Exception('Not enough permissions to drop privileges')
1202
1203     user_uid = pwd.getpwnam(username).pw_uid
1204     user_gid = pwd.getpwnam(username).pw_gid
1205
1206     # Drop privileges
1207     set_privileges(username, user_uid, user_gid)
1208
1209     # We need to catch exception in order to be able to restore
1210     # privileges later in this function
1211     out = None
1212     exc = None
1213     try:
1214         out = func(*args)
1215     except Exception as e:
1216         exc = e
1217
1218     # Restore privileges
1219     set_privileges('root', current_uid, 0)
1220
1221     if exc:
1222         raise exc
1223
1224     return out
1225
1226 def expand_pref_variables(text, gpt_path, lp, username=None):
1227     utc_dt = datetime.utcnow()
1228     dt = datetime.now()
1229     cache_path = lp.cache_path(os.path.join('gpo_cache'))
1230     # These are all the possible preference variables that MS supports. The
1231     # variables set to 'None' here are currently unsupported by Samba, and will
1232     # prevent the individual policy from applying.
1233     variables = { 'AppDataDir': os.path.expanduser('~/.config'),
1234                   'BinaryComputerSid': None,
1235                   'BinaryUserSid': None,
1236                   'CommonAppdataDir': None,
1237                   'CommonDesktopDir': None,
1238                   'CommonFavoritesDir': None,
1239                   'CommonProgramsDir': None,
1240                   'CommonStartUpDir': None,
1241                   'ComputerName': lp.get('netbios name'),
1242                   'CurrentProccessId': None,
1243                   'CurrentThreadId': None,
1244                   'DateTime': utc_dt.strftime('%Y-%m-%d %H:%M:%S UTC'),
1245                   'DateTimeEx': str(utc_dt),
1246                   'DesktopDir': os.path.expanduser('~/Desktop'),
1247                   'DomainName': lp.get('realm'),
1248                   'FavoritesDir': None,
1249                   'GphPath': None,
1250                   'GptPath': os.path.join(cache_path,
1251                                           check_safe_path(gpt_path).upper()),
1252                   'GroupPolicyVersion': None,
1253                   'LastDriveMapped': None,
1254                   'LastError': None,
1255                   'LastErrorText': None,
1256                   'LdapComputerSid': None,
1257                   'LdapUserSid': None,
1258                   'LocalTime': dt.strftime('%H:%M:%S'),
1259                   'LocalTimeEx': dt.strftime('%H:%M:%S.%f'),
1260                   'LogonDomain': lp.get('realm'),
1261                   'LogonServer': None,
1262                   'LogonUser': username,
1263                   'LogonUserSid': None,
1264                   'MacAddress': None,
1265                   'NetPlacesDir': None,
1266                   'OsVersion': None,
1267                   'ProgramFilesDir': None,
1268                   'ProgramsDir': None,
1269                   'RecentDocumentsDir': None,
1270                   'ResultCode': None,
1271                   'ResultText': None,
1272                   'ReversedComputerSid': None,
1273                   'ReversedUserSid': None,
1274                   'SendToDir': None,
1275                   'StartMenuDir': None,
1276                   'StartUpDir': None,
1277                   'SystemDir': None,
1278                   'SystemDrive': '/',
1279                   'TempDir': '/tmp',
1280                   'TimeStamp': str(datetime.timestamp(dt)),
1281                   'TraceFile': None,
1282                   'WindowsDir': None
1283     }
1284     for exp_var, val in variables.items():
1285         exp_var_fmt = '%%%s%%' % exp_var
1286         if exp_var_fmt in text:
1287             if val is None:
1288                 raise NameError('Expansion variable %s is undefined' % exp_var)
1289             text = text.replace(exp_var_fmt, val)
1290     return text