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 nbt
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
615 """ Fetch a list of GUIDs for applicable GPOs """
618 def get_gpo(samdb, gpo_dn):
619 g = gpo.GROUP_POLICY_OBJECT()
625 "gPCFunctionalityVersion",
626 "gPCMachineExtensionNames",
627 "gPCUserExtensionNames",
630 "nTSecurityDescriptor",
633 if gpo_dn.startswith("LDAP://"):
634 gpo_dn = gpo_dn.lstrip("LDAP://")
636 sd_flags = (security.SECINFO_OWNER |
637 security.SECINFO_GROUP |
638 security.SECINFO_DACL)
640 res = samdb.search(gpo_dn, ldb.SCOPE_BASE, "(objectclass=*)", attrs,
641 controls=['sd_flags:1:%d' % sd_flags])
643 log.error('Failed to fetch gpo object with nTSecurityDescriptor')
646 raise ldb.LdbError(ldb.ERR_NO_SUCH_OBJECT,
647 'get_gpo: search failed')
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]))
669 def __init__(self, gPLink, gPOptions):
672 self.gpo_parse_gplink(gPLink)
673 self.gp_opts = int(gPOptions)
675 def gpo_parse_gplink(self, gPLink):
676 for p in gPLink.decode().split(']'):
679 log.debug('gpo_parse_gplink: processing link')
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))
688 if len(self.link_names) != len(self.link_opts):
689 raise RuntimeError('Link names and opts mismatch')
690 return len(self.link_names)
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)
697 raise ldb.LdbError(ldb.ERR_NO_SUCH_OBJECT,
698 "Failed to find samAccountName '{}'".format(samaccountname)
700 uac = int(res.msgs[0]['userAccountControl'][0])
701 dn = res.msgs[0]['dn']
702 log.info('Found dn {} for samaccountname {}'.format(dn, samaccountname))
705 def get_gpo_link(samdb, link_dn):
706 res = samdb.search(link_dn, ldb.SCOPE_BASE,
707 '(objectclass=*)', ['gPLink', 'gPOptions'])
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)
714 gPLink = res.msgs[0]['gPLink'][0]
716 if 'gPOptions' in res.msgs[0]:
717 gPOptions = res.msgs[0]['gPOptions'][0]
719 log.debug("get_gpo_link: no 'gPOptions' attribute found")
720 return GP_LINK(gPLink, gPOptions)
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')
730 if only_add_forced_gpos:
732 log.debug("skipping nonenforced GPO link "
733 "because GPOPTIONS_BLOCK_INHERITANCE "
737 log.debug("adding enforced GPO link although "
738 "the GPOPTIONS_BLOCK_INHERITANCE "
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])
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)
763 new_gpo.link = str(link_dn)
764 new_gpo.link_type = link_type
767 forced_gpo_list.insert(0, new_gpo)
769 gpo_list.insert(0, new_gpo)
771 log.debug("add_gplink_to_gpo_list: added GPLINK #%d %s "
772 "to GPO list" % (i, gp_link.link_names[i]))
774 def merge_with_system_token(token_1):
776 system_token = system_session().security_token
777 sids.extend(system_token.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
785 def site_dn_for_machine(samdb, dc_hostname, lp, creds, hostname):
786 # [MS-GPOL] 3.2.5.1.4 Site Search
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.
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(),
800 scope=ldb.SCOPE_BASE,
804 raise RuntimeError('site_dn_for_machine: No result')
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')
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
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
821 site_dn = samdb.get_config_basedn()
822 site_dn.add_child("CN=Sites")
823 site_dn.add_child("CN=%s" % (client_site))
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,
839 Forced GPOs are appended in the end since they override all others.
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
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)
863 token = session.security_token
865 # (O)rganizational(U)nit
866 parent_dn = dn.parent()
868 if str(parent_dn) == str(samdb.get_default_basedn().parent()):
871 # An account can be a member of more OUs
872 if parent_dn.get_component_name(0) == 'OU':
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
880 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
883 add_only_forced_gpos, token)
885 # block inheritance from now on
886 if gp_link.gp_opts & GPO_BLOCK_INHERITANCE:
887 add_only_forced_gpos = True
889 parent_dn = parent_dn.parent()
892 parent_dn = dn.parent()
894 if str(parent_dn) == str(samdb.get_default_basedn().parent()):
897 # An account can just be a member of one domain
898 if parent_dn.get_component_name(0) == 'DC':
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
906 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
909 add_only_forced_gpos, token)
911 # block inheritance from now on
912 if gp_link.gp_opts & GPO_BLOCK_INHERITANCE:
913 add_only_forced_gpos = True
915 parent_dn = parent_dn.parent()
920 site_dn = site_dn_for_machine(samdb, dc_hostname, lp, creds, username)
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
929 add_gplink_to_gpo_list(samdb, gpo_list, forced_gpo_list,
932 add_only_forced_gpos, token)
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
940 gpo_list.insert(0, gpo.GROUP_POLICY_OBJECT("Local Policy",
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
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)
953 os.makedirs(local_dir, mode=0o755)
955 if e.errno != errno.EEXIST:
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']))
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))
966 os.rename(f.name, os.path.join(local_dir, local_name))
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:]
975 return os.path.join(*dirs)
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')
988 if not gpo_obj.file_sys_path:
990 cache_gpo_dir(conn, cache_path, check_safe_path(gpo_obj.file_sys_path))
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)
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])
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)
1012 check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
1014 log.error('Failed downloading gpt cache from \'%s\' using SMB'
1020 gp_db.state(GPOSTATE.ENFORCE)
1023 for gpo_obj in gpos:
1024 if not gpo_obj.file_sys_path:
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)
1035 for ext in gp_extensions:
1037 ext = ext(lp, creds, username, store)
1038 if target == 'Computer':
1039 ext.process_group_policy(del_gpos, changed_gpos)
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)))
1050 for gpo_obj in gpos:
1051 if not gpo_obj.file_sys_path:
1054 path = check_safe_path(gpo_obj.file_sys_path).upper()
1055 version = gpo_version(lp, path)
1056 store.store(guid, '%i' % version)
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())
1066 for ext in gp_extensions:
1068 ext = ext(lp, creds, username, store)
1069 if target == 'Computer':
1070 ext.process_group_policy(del_gpos, [])
1072 drop_privileges(username, ext.process_group_policy,
1074 except Exception as e:
1075 log.error('Failed to unapply extension %s' % str(ext))
1076 log.error('Message was: ' + str(e))
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)
1090 if isinstance(vals, numbers.Number):
1091 return ' '*(level+2) + str(vals)
1093 return ' '*(level+2) + get_string(vals)
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)
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]
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))
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:
1133 ext_conf = lp.state_path('gpext.conf')
1134 parser = ConfigParser(interpolation=None)
1135 parser.read(ext_conf)
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:
1143 os.rename(f.name, ext_conf)
1146 def check_guid(guid):
1147 # Check for valid guid with curly braces
1148 if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
1151 UUID(guid, version=4)
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):
1162 if not check_guid(guid):
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")
1173 atomic_write_conf(lp, parser)
1178 def list_gp_extensions(smb_conf=None):
1179 _, parser = parse_gpext_conf(smb_conf)
1181 for guid in parser.sections():
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'))
1192 def unregister_gp_extension(guid, smb_conf=None):
1193 if not check_guid(guid):
1196 lp, parser = parse_gpext_conf(smb_conf)
1197 if guid in parser.sections():
1198 parser.remove_section(guid)
1200 atomic_write_conf(lp, parser)
1205 def set_privileges(username, uid, gid):
1207 Set current process privileges
1214 def drop_privileges(username, func, *args):
1216 Run supplied function with privileges for specified username.
1218 current_uid = os.getuid()
1220 if not current_uid == 0:
1221 raise Exception('Not enough permissions to drop privileges')
1223 user_uid = pwd.getpwnam(username).pw_uid
1224 user_gid = pwd.getpwnam(username).pw_gid
1227 set_privileges(username, user_uid, user_gid)
1229 # We need to catch exception in order to be able to restore
1230 # privileges later in this function
1235 except Exception as e:
1238 # Restore privileges
1239 set_privileges('root', current_uid, 0)
1246 def expand_pref_variables(text, gpt_path, lp, username=None):
1247 utc_dt = datetime.utcnow()
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,
1270 'GptPath': os.path.join(cache_path,
1271 check_safe_path(gpt_path).upper()),
1272 'GroupPolicyVersion': None,
1273 'LastDriveMapped': 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,
1285 'NetPlacesDir': None,
1287 'ProgramFilesDir': None,
1288 'ProgramsDir': None,
1289 'RecentDocumentsDir': None,
1292 'ReversedComputerSid': None,
1293 'ReversedUserSid': None,
1295 'StartMenuDir': None,
1300 'TimeStamp': str(datetime.timestamp(dt)),
1304 for exp_var, val in variables.items():
1305 exp_var_fmt = '%%%s%%' % exp_var
1306 if exp_var_fmt in text:
1308 raise NameError('Expansion variable %s is undefined' % exp_var)
1309 text = text.replace(exp_var_fmt, val)