--- a/python/samba/netcmd/ntacl.py +++ b/python/samba/netcmd/ntacl.py @@ -293,7 +293,10 @@ class cmd_ntacl_sysvolcheck(Command): "versionopts": options.VersionOptions, } - def run(self, credopts=None, sambaopts=None, versionopts=None): + takes_options = [ + Option("--mask-msad-differences", help="Ignore standard differences to MS AD ACLs", action="store_true"), + ] + def run(self, mask_msad_differences=False, credopts=None, sambaopts=None, versionopts=None): lp = sambaopts.get_loadparm() creds = credopts.get_credentials(lp) creds.set_kerberos_state(DONT_USE_KERBEROS) @@ -311,7 +359,7 @@ class cmd_ntacl_sysvolcheck(Command): provision.checksysvolacl(samdb, netlogon, sysvol, domain_sid, lp.get("realm").lower(), samdb.domain_dn(), - lp) + lp, mask_msad_differences) class cmd_ntacl(SuperCommand): --- a/python/samba/provision/__init__.py +++ b/python/samba/provision/__init__.py @@ -1728,8 +1729,180 @@ def check_dir_acl(path, acl, lp, domains raise ProvisioningError('%s NTACL of GPO directory %s %s does not match value %s expected from GPO object' % (acl_type(direct_db_access), os.path.join(root, name), fsacl_sddl_mapped, acl)) +def check_dir_acl_masked(path, acl_expected_for_gpo, lp, domainsid, direct_db_access): + try: + fsacl = getntacl(lp, path, direct_db_access=direct_db_access, service=SYSVOL_SERVICE) + fsacl_sddl = fsacl.as_sddl(domainsid) + except TypeError as error: + return + except NTSTATUSError as error: + if check_runtime_error(error, ntstatus.NT_STATUS_OBJECT_NAME_NOT_FOUND): + print "ERROR: File not found", path + return + else: + raise + + ## Sanitize "domainsid" to be a security.dom_sid + if isinstance(domainsid, str): + domainsid = security.dom_sid(domainsid) + + LA = security.dom_sid("%s-%d" % (domainsid, security.DOMAIN_RID_ADMINISTRATOR)) + DA = security.dom_sid("%s-%d" % (domainsid, security.DOMAIN_RID_ADMINS)) + CO = security.dom_sid(security.SID_CREATOR_OWNER) + + ## If LA in filesystem then treat it as DA for comparison (reversing what samba.ntacls.setntacl did) + if fsacl.owner_sid == LA: + fsacl.owner_sid = DA + + """ + MS doc about SE_DACL_AUTO_INHERITED of SECURITY_DESCRIPTOR_CONTROL: + For Windows 2000 ACLs that support auto-inheritance, this bit is always set. + """ + ## The default {31B2F340-016D-11D2-945F-00C04FB984F9} and {6AC1786C-016F-11D2-945F-00C04FB984F9} don't have this in Samba + ## so mask this difference here to avoid false positives + if os.path.split(path)[-1] in ("{31B2F340-016D-11D2-945F-00C04FB984F9}", "{6AC1786C-016F-11D2-945F-00C04FB984F9}"): + fsacl.type |= security.SEC_DESC_DACL_AUTO_INHERITED + fix_P_to_AI = True ## also below in the subdir and file checks + else: + fix_P_to_AI = False + + fsacl_sddl_mapped = fsacl.as_sddl(domainsid) + + ## at least in UCS, all base GPO directories have AI set, so expect that + sd = security.descriptor.from_sddl(acl_expected_for_gpo, domainsid) + sd.type |= security.SEC_DESC_DACL_AUTO_INHERITED + acl_expected_for_gpo = sd.as_sddl(domainsid) + + if fsacl_sddl_mapped != acl_expected_for_gpo: + raise ProvisioningError('%s NTACL of GPO directory %s does not match value expected from GPO object\nFSACL: %s\nDSACL: %s' % (acl_type(direct_db_access), path, fsacl_sddl_mapped, acl_expected_for_gpo)) + + ### After the base GPO directory has passed, now fix the FSACLs calculated by dsacl2fsacl to work for the subdirs and files + ## copy the security descriptor structure, we have to filter out a few ACEs + sd_masked = security.descriptor() + sd_masked.owner_sid = sd.owner_sid + sd_masked.group_sid = sd.group_sid + sd_masked.type = sd.type + sd_masked.revision = sd.revision + + ## the subdirs and files are AI and not P + sd_masked.type |= security.SEC_DESC_DACL_AUTO_INHERITED + sd_masked.type &= ~security.SEC_DESC_DACL_PROTECTED + + skip_other_da_aces = False + for i in range(0, len(sd.dacl.aces)): + if skip_other_da_aces and sd.dacl.aces[i].trustee in (DA, LA): + ## filter out additional ACEs for DA and LA, there is some duplication and ordering issue + continue + elif sd.dacl.aces[i].trustee == DA: + ## flag first occurrence of DA + skip_other_da_aces = True + if sd.dacl.aces[i].flags & security.SEC_ACE_FLAG_CONTAINER_INHERIT: + ## If GPO says CI then the subobjects must show the inherited flag + ## Note: dsacl2fsacl currently always fakes security.SEC_ACE_FLAG_CONTAINER_INHERIT + sd.dacl.aces[i].flags |= security.SEC_ACE_FLAG_INHERITED_ACE + + sd_masked.dacl_add(sd.dacl.aces[i]) + acl_expected_for_subdir = sd_masked.as_sddl(domainsid) + + ## Additionally for files MS AD and GPMC don't set the CI and OI flags + for i in range(0, len(sd_masked.dacl.aces)): + sd_masked.dacl.aces[i].flags &= ~ (security.SEC_ACE_FLAG_OBJECT_INHERIT | security.SEC_ACE_FLAG_CONTAINER_INHERIT) + try: + ## also filter out ACE for CO, not present for GPT.INI of new GPOs created via GPMC + sd_masked.dacl_del(CO) + except: + pass + acl_expected_for_file = sd_masked.as_sddl(domainsid) + + #print "ACL GPO: %s" % acl_expected_for_gpo + #print "ACL DIR: %s" % acl_expected_for_subdir + #print "ACL FILE: %s" % acl_expected_for_file + + for root, dirs, files in os.walk(path, topdown=False): + for name in files: + fsacl = getntacl(lp, os.path.join(root, name), + direct_db_access=direct_db_access, service=SYSVOL_SERVICE) + if fsacl is None: + raise ProvisioningError('%s ACL on GPO file %s %s not found!' % (acl_type(direct_db_access), os.path.join(root, name))) + + ## If LA in filesystem then treat it as DA for comparison (reversing what samba.ntacls.setntacl did) + if fsacl.owner_sid == LA: + fsacl.owner_sid = DA + + ## Mask current differences between Samba and MS AD & GPMC + fsacl_masked = security.descriptor() + fsacl_masked.owner_sid = fsacl.owner_sid + fsacl_masked.group_sid = fsacl.group_sid + fsacl_masked.type = fsacl.type + fsacl_masked.revision = fsacl.revision + + if fix_P_to_AI: + fsacl_masked.type &= ~security.SEC_DESC_DACL_PROTECTED + fsacl_masked.type |= security.SEC_DESC_DACL_AUTO_INHERITED + + skip_other_da_aces = False + for i in range(0, len(fsacl.dacl.aces)): + if skip_other_da_aces and fsacl.dacl.aces[i].trustee in (DA, LA): + ## filter out additional ACEs for DA and LA, there is some duplication and ordering issue + continue + elif fsacl.dacl.aces[i].trustee == DA: + ## flag first occurrence of DA + skip_other_da_aces = True + if str(fsacl.dacl.aces[i].trustee) == security.SID_CREATOR_OWNER: + ## filter out ACE for CO, not present for GPT.INI of new GPOs created via GPMC + continue + if fix_P_to_AI: + fsacl.dacl.aces[i].flags |= security.SEC_ACE_FLAG_INHERITED_ACE + ## The OI and CI flags don't make sense for files and are neither set by MS AD nor MS GPMC + fsacl.dacl.aces[i].flags &= ~ (security.SEC_ACE_FLAG_OBJECT_INHERIT | security.SEC_ACE_FLAG_CONTAINER_INHERIT) + fsacl_masked.dacl_add(fsacl.dacl.aces[i]) + + fsacl_sddl_mapped = fsacl_masked.as_sddl(domainsid) + + if fsacl_sddl_mapped != acl_expected_for_file: + raise ProvisioningError('%s NTACL of GPO file %s does not match value expected from GPO object\nFSACL: %s\nDSACL: %s' % (acl_type(direct_db_access), os.path.join(root, name), fsacl_sddl_mapped, acl_expected_for_file)) + + for name in dirs: + fsacl = getntacl(lp, os.path.join(root, name), + direct_db_access=direct_db_access, service=SYSVOL_SERVICE) + if fsacl is None: + raise ProvisioningError('%s ACL on GPO directory %s %s not found!' % (acl_type(direct_db_access), os.path.join(root, name))) + + ## If LA in filesystem then treat it as DA for comparison (reversing what samba.ntacls.setntacl did) + if fsacl.owner_sid == LA: + fsacl.owner_sid = DA + + ## Mask current differences between Samba and MS AD & GPMC + fsacl_masked = security.descriptor() + fsacl_masked.owner_sid = fsacl.owner_sid + fsacl_masked.group_sid = fsacl.group_sid + fsacl_masked.type = fsacl.type + fsacl_masked.revision = fsacl.revision + + if fix_P_to_AI: + fsacl_masked.type &= ~security.SEC_DESC_DACL_PROTECTED + fsacl_masked.type |= security.SEC_DESC_DACL_AUTO_INHERITED + + skip_other_da_aces = False + for i in range(0, len(fsacl.dacl.aces)): + if skip_other_da_aces and fsacl.dacl.aces[i].trustee in (DA, LA): + ## filter out additional ACEs for DA and LA, there is some duplication and ordering issue + continue + elif fsacl.dacl.aces[i].trustee == DA: + ## flag first occurrence of DA + skip_other_da_aces = True + if fix_P_to_AI: + fsacl.dacl.aces[i].flags |= security.SEC_ACE_FLAG_INHERITED_ACE + fsacl_masked.dacl_add(fsacl.dacl.aces[i]) + + fsacl_sddl_mapped = fsacl_masked.as_sddl(domainsid) + + if fsacl_sddl_mapped != acl_expected_for_subdir: + raise ProvisioningError('%s NTACL of GPO subdirectory %s does not match value expected from GPO object\nFSACL: %s\nDSACL: %s' % (acl_type(direct_db_access), os.path.join(root, name), fsacl_sddl_mapped, acl_expected_for_subdir)) + + def check_gpos_acl(sysvol, dnsdomain, domainsid, domaindn, samdb, lp, - direct_db_access): + direct_db_access, mask_msad_differences=False): """Set ACL on the sysvol//Policies folder and the policy folders beneath. @@ -1759,15 +1932,19 @@ def check_gpos_acl(sysvol, dnsdomain, do policy["nTSecurityDescriptor"][0]).as_sddl() policy_path = getpolicypath(sysvol, dnsdomain, str(policy["cn"])) try: - check_dir_acl(policy_path, dsacl2fsacl(acl, domainsid), lp, - domainsid, direct_db_access) + if mask_msad_differences: + check_dir_acl_masked(policy_path, dsacl2fsacl(acl, domainsid), lp, + domainsid, direct_db_access) + else: + check_dir_acl(policy_path, dsacl2fsacl(acl, domainsid), lp, + domainsid, direct_db_access) except Exception as e: print(e) continue def checksysvolacl(samdb, netlogon, sysvol, domainsid, dnsdomain, domaindn, - lp): + lp, mask_msad_differences=False): """Set the ACL for the sysvol share and the subfolders :param samdb: An LDB object on the SAM db @@ -1812,7 +1989,7 @@ def checksysvolacl(samdb, netlogon, sysv # Check acls on Policy folder and policies folders check_gpos_acl(sysvol, dnsdomain, domainsid, domaindn, samdb, lp, - direct_db_access) + direct_db_access, mask_msad_differences) def interface_ips_v4(lp):