1 # Reads important GPO parameters and updates Samba
2 # Copyright (C) Luke Morrison <luc785@.hotmail.com> 2013
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.
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.
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/>.
23 sys.path.insert(0, "bin/python")
24 from samba import WERRORError
25 from configparser import ConfigParser
26 from io import StringIO
28 from samba.common import get_bytes
29 from abc import ABCMeta, abstractmethod
30 import xml.etree.ElementTree as etree
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
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
44 from samba.common import get_string
45 from samba.samdb import SamDB
46 from samba.auth import system_session
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
52 from samba.dcerpc import netlogon
53 from datetime import datetime
58 GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
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).
71 The log is organized like so:
76 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
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>
85 <gp_ext name="Kerberos Policy">
86 <attribute name="ticket_lifetime">1d</attribute>
87 <attribute name="renew_lifetime" />
88 <attribute name="clockskew">300</attribute>
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.
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
108 param gpostore - the GPOStorage obj which references the tdb which
110 param db_log - (optional) a string to initialize the gp_log
112 self._state = GPOSTATE.APPLY
113 self.gpostore = gpostore
116 self.gpdb = etree.fromstring(db_log)
118 self.gpdb = etree.Element('gp')
120 user_obj = self.gpdb.find('user[@name="%s"]' % user)
122 user_obj = etree.SubElement(self.gpdb, 'user')
123 user_obj.attrib['name'] = user
125 def state(self, value):
126 """ Policy application state
127 param value - APPLY, ENFORCE, or UNAPPLY
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.
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
148 """Check the GPOSTATE
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
158 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
159 obj = user_obj.find('guid[@value="%s"]' % guid)
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)
169 item = etree.SubElement(apply_log, 'guid')
170 item.attrib['count'] = '%d' % (len(apply_log) - 1)
171 item.attrib['value'] = guid
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
180 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
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)
187 ext = etree.SubElement(guid_obj, 'gp_ext')
188 ext.attrib['name'] = gp_ext_name
189 attr = ext.find('attribute[@name="%s"]' % attribute)
191 attr = etree.SubElement(ext, 'attribute')
192 attr.attrib['name'] = attribute
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
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)
207 attr = ext.find('attribute[@name="%s"]' % attribute)
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
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)
223 attrs = ext.findall('attribute')
224 return {attr.attrib['name']: attr.text for attr in attrs}
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
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'))
240 guids_by_count.sort(reverse=True)
241 guids.extend(guid for count, guid in guids_by_count)
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.
252 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
254 guid_settings = user_obj.find('guid[@value="%s"]' % guid)
255 exts = guid_settings.findall('gp_ext')
259 attrs = ext.findall('attribute')
261 attr_dict[attr.attrib['name']] = attr.text
262 settings[ext.attrib['name']] = attr_dict
263 ret.append((guid, settings))
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
270 param attribute - attribute to remove
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)
277 attr = ext.find('attribute[@name="%s"]' % attribute)
284 """ Write gp_log changes to disk """
285 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
289 def __init__(self, log_file):
290 if os.path.isfile(log_file):
291 self.log = tdb.open(log_file)
293 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT | os.O_RDWR)
296 self.log.transaction_start()
298 def get_int(self, key):
300 return int(self.log.get(get_bytes(key)))
305 return self.log.get(get_bytes(key))
307 def get_gplog(self, user):
308 return gp_log(user, self, self.log.get(get_bytes(user)))
310 def store(self, key, val):
311 self.log.store(get_bytes(key), get_bytes(val))
314 self.log.transaction_cancel()
316 def delete(self, key):
317 self.log.delete(get_bytes(key))
320 self.log.transaction_commit()
326 class gp_ext(object):
327 __metaclass__ = ABCMeta
329 def __init__(self, lp, creds, username, store):
332 self.username = username
333 self.gp_db = store.get_gplog(username)
336 def process_group_policy(self, deleted_gpo_list, changed_gpo_list):
340 def read(self, policy):
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)
359 class gp_inf_ext(gp_ext):
360 def read(self, data_file):
361 with open(data_file, 'rb') as f:
363 inf_conf = ConfigParser(interpolation=None)
364 inf_conf.optionxform = str
366 inf_conf.read_file(StringIO(policy.decode()))
367 except UnicodeDecodeError:
368 inf_conf.read_file(StringIO(policy.decode('utf-16')))
372 class gp_pol_ext(gp_ext):
373 def read(self, data_file):
374 with open(data_file, 'rb') as f:
376 return ndr_unpack(preg.file, raw)
379 class gp_xml_ext(gp_ext):
380 def read(self, data_file):
381 with open(data_file, 'rb') as f:
384 return etree.fromstring(raw.decode())
385 except UnicodeDecodeError:
386 return etree.fromstring(raw.decode('utf-16'))
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.
395 __metaclass__ = ABCMeta
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
403 Normally called by the subclass apply() function after applying policy.
405 self.gp_db.set_guid(guid)
406 self.gp_db.store(str(self), attribute, value)
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
414 Normally called by the subclass unapply() function when removing old
417 self.gp_db.set_guid(guid)
418 self.gp_db.delete(str(self), attribute)
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
426 self.gp_db.set_guid(guid)
427 return self.gp_db.retrieve(str(self), attribute)
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
433 self.gp_db.set_guid(guid)
434 return self.gp_db.retrieve_all(str(self))
436 def cache_get_apply_state(self):
437 """Return the current apply state
438 return - APPLY|ENFORCE|UNAPPLY
440 return self.gp_db.get_state()
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
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.
453 data = b''.join([get_bytes(arg) for arg in [*args]])
454 return blake2b(get_bytes(name)+data).hexdigest()
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
461 data = b''.join([get_bytes(arg) for arg in [*args]])
462 return blake2b(data).hexdigest()
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
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
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).
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
494 kwargs - Additional keyword args required by the subclass unapply
497 This is only necessary for CSEs which provide multiple attributes.
499 # Clean syntax is, either provide a single remove attribute,
500 # or a list of either removal attributes or keep attributes.
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)
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)
518 class gp_misc_applier(gp_applier):
519 """Group Policy Miscellaneous Applier/Unapplier/Modifier
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'))
529 def parse_value(self, value):
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}
539 next(itr) # Skip the top element
541 vals[item.tag] = item.text
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
551 def __generate_value(self, value_hash, files, sep):
554 return sep.join(data)
556 def __parse_value(self, value, sep):
558 return - A unique HASH, followed by the file list
562 data = value.split(sep)
564 # The first element is not a hash, but a filename. This is a
568 return data[0], data[1:] if len(data) > 1 else []
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)
575 if os.path.exists(file):
577 self.cache_remove_attribute(guid, attribute)
579 def apply(self, guid, attribute, value_hash, applier_func, *args, sep=':'):
581 applier_func MUST return a list of files created by the applier.
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.
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)
596 # If policy is already applied, skip application
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)
605 """ Fetch the hostname of a writable DC """
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 |
612 return cldap_ret.pdc_dns_name
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 |
618 return cldap_ret.pdc_name
621 """ Fetch a list of GUIDs for applicable GPOs """
624 def get_gpo(samdb, gpo_dn):
625 g = gpo.GROUP_POLICY_OBJECT()
631 "gPCFunctionalityVersion",
632 "gPCMachineExtensionNames",
633 "gPCUserExtensionNames",
636 "nTSecurityDescriptor",
639 if gpo_dn.startswith("LDAP://"):
640 gpo_dn = gpo_dn.lstrip("LDAP://")
642 sd_flags = (security.SECINFO_OWNER |
643 security.SECINFO_GROUP |
644 security.SECINFO_DACL)
646 res = samdb.search(gpo_dn, ldb.SCOPE_BASE, "(objectclass=*)", attrs,
647 controls=['sd_flags:1:%d' % sd_flags])
649 log.error('Failed to fetch gpo object with nTSecurityDescriptor')
652 raise ldb.LdbError(ldb.ERR_NO_SUCH_OBJECT,
653 'get_gpo: search failed')
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]))
675 def __init__(self, gPLink, gPOptions):
678 self.gpo_parse_gplink(gPLink)
679 self.gp_opts = int(gPOptions)
681 def gpo_parse_gplink(self, gPLink):
682 for p in gPLink.decode().split(']'):
685 log.debug('gpo_parse_gplink: processing link')
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))
694 if len(self.link_names) != len(self.link_opts):
695 raise RuntimeError('Link names and opts mismatch')
696 return len(self.link_names)
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)
703 raise ldb.LdbError(ldb.ERR_NO_SUCH_OBJECT,
704 "Failed to find samAccountName '{}'".format(samaccountname)
706 uac = int(res.msgs[0]['userAccountControl'][0])
707 dn = res.msgs[0]['dn']
708 log.info('Found dn {} for samaccountname {}'.format(dn, samaccountname))
711 def get_gpo_link(samdb, link_dn):
712 res = samdb.search(link_dn, ldb.SCOPE_BASE,
713 '(objectclass=*)', ['gPLink', 'gPOptions'])
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)
720 gPLink = res.msgs[0]['gPLink'][0]
722 if 'gPOptions' in res.msgs[0]:
723 gPOptions = res.msgs[0]['gPOptions'][0]
725 log.debug("get_gpo_link: no 'gPOptions' attribute found")
726 return GP_LINK(gPLink, gPOptions)
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')
736 if only_add_forced_gpos:
738 log.debug("skipping nonenforced GPO link "
739 "because GPOPTIONS_BLOCK_INHERITANCE "
743 log.debug("adding enforced GPO link although "
744 "the GPOPTIONS_BLOCK_INHERITANCE "
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])
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)
769 new_gpo.link = str(link_dn)
770 new_gpo.link_type = link_type
773 forced_gpo_list.insert(0, new_gpo)
775 gpo_list.insert(0, new_gpo)
777 log.debug("add_gplink_to_gpo_list: added GPLINK #%d %s "
778 "to GPO list" % (i, gp_link.link_names[i]))
780 def merge_with_system_token(token_1):
782 system_token = system_session().security_token
783 sids.extend(system_token.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
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()
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)
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'])
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()
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,
819 Forced GPOs are appended in the end since they override all others.
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
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)
843 token = session.security_token
845 # (O)rganizational(U)nit
846 parent_dn = dn.parent()
848 if str(parent_dn) == str(samdb.get_default_basedn().parent()):
851 # An account can be a member of more OUs
852 if parent_dn.get_component_name(0) == 'OU':
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
860 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
863 add_only_forced_gpos, token)
865 # block inheritance from now on
866 if gp_link.gp_opts & GPO_BLOCK_INHERITANCE:
867 add_only_forced_gpos = True
869 parent_dn = parent_dn.parent()
872 parent_dn = dn.parent()
874 if str(parent_dn) == str(samdb.get_default_basedn().parent()):
877 # An account can just be a member of one domain
878 if parent_dn.get_component_name(0) == 'DC':
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
886 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
889 add_only_forced_gpos, token)
891 # block inheritance from now on
892 if gp_link.gp_opts & GPO_BLOCK_INHERITANCE:
893 add_only_forced_gpos = True
895 parent_dn = parent_dn.parent()
900 site_dn = site_dn_for_machine(samdb, dc_hostname, lp, creds, username)
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
909 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
912 add_only_forced_gpos, token)
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
920 gpo_list.insert(0, gpo.GROUP_POLICY_OBJECT("Local Policy",
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
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)
933 os.makedirs(local_dir, mode=0o755)
935 if e.errno != errno.EEXIST:
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']))
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))
946 os.rename(f.name, os.path.join(local_dir, local_name))
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:]
955 return os.path.join(*dirs)
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')
968 if not gpo_obj.file_sys_path:
970 cache_gpo_dir(conn, cache_path, check_safe_path(gpo_obj.file_sys_path))
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)
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])
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)
992 check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
994 log.error('Failed downloading gpt cache from \'%s\' using SMB'
1000 gp_db.state(GPOSTATE.ENFORCE)
1003 for gpo_obj in gpos:
1004 if not gpo_obj.file_sys_path:
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)
1015 for ext in gp_extensions:
1017 ext = ext(lp, creds, username, store)
1018 if target == 'Computer':
1019 ext.process_group_policy(del_gpos, changed_gpos)
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)))
1030 for gpo_obj in gpos:
1031 if not gpo_obj.file_sys_path:
1034 path = check_safe_path(gpo_obj.file_sys_path).upper()
1035 version = gpo_version(lp, path)
1036 store.store(guid, '%i' % version)
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())
1046 for ext in gp_extensions:
1048 ext = ext(lp, creds, username, store)
1049 if target == 'Computer':
1050 ext.process_group_policy(del_gpos, [])
1052 drop_privileges(username, ext.process_group_policy,
1054 except Exception as e:
1055 log.error('Failed to unapply extension %s' % str(ext))
1056 log.error('Message was: ' + str(e))
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)
1070 if isinstance(vals, numbers.Number):
1071 return ' '*(level+2) + str(vals)
1073 return ' '*(level+2) + get_string(vals)
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)
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]
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))
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:
1113 ext_conf = lp.state_path('gpext.conf')
1114 parser = ConfigParser(interpolation=None)
1115 parser.read(ext_conf)
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:
1123 os.rename(f.name, ext_conf)
1126 def check_guid(guid):
1127 # Check for valid guid with curly braces
1128 if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
1131 UUID(guid, version=4)
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):
1142 if not check_guid(guid):
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")
1153 atomic_write_conf(lp, parser)
1158 def list_gp_extensions(smb_conf=None):
1159 _, parser = parse_gpext_conf(smb_conf)
1161 for guid in parser.sections():
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'))
1172 def unregister_gp_extension(guid, smb_conf=None):
1173 if not check_guid(guid):
1176 lp, parser = parse_gpext_conf(smb_conf)
1177 if guid in parser.sections():
1178 parser.remove_section(guid)
1180 atomic_write_conf(lp, parser)
1185 def set_privileges(username, uid, gid):
1187 Set current process privileges
1194 def drop_privileges(username, func, *args):
1196 Run supplied function with privileges for specified username.
1198 current_uid = os.getuid()
1200 if not current_uid == 0:
1201 raise Exception('Not enough permissions to drop privileges')
1203 user_uid = pwd.getpwnam(username).pw_uid
1204 user_gid = pwd.getpwnam(username).pw_gid
1207 set_privileges(username, user_uid, user_gid)
1209 # We need to catch exception in order to be able to restore
1210 # privileges later in this function
1215 except Exception as e:
1218 # Restore privileges
1219 set_privileges('root', current_uid, 0)
1226 def expand_pref_variables(text, gpt_path, lp, username=None):
1227 utc_dt = datetime.utcnow()
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,
1250 'GptPath': os.path.join(cache_path,
1251 check_safe_path(gpt_path).upper()),
1252 'GroupPolicyVersion': None,
1253 'LastDriveMapped': 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,
1265 'NetPlacesDir': None,
1267 'ProgramFilesDir': None,
1268 'ProgramsDir': None,
1269 'RecentDocumentsDir': None,
1272 'ReversedComputerSid': None,
1273 'ReversedUserSid': None,
1275 'StartMenuDir': None,
1280 'TimeStamp': str(datetime.timestamp(dt)),
1284 for exp_var, val in variables.items():
1285 exp_var_fmt = '%%%s%%' % exp_var
1286 if exp_var_fmt in text:
1288 raise NameError('Expansion variable %s is undefined' % exp_var)
1289 text = text.replace(exp_var_fmt, val)