#!/usr/bin/env python
import optparse
import sys

# Find right directory when running from source tree
sys.path.insert(0, "bin/python")


import samba
import ldb
import urllib
import os
from samba import getopt as options
from samba import sd_utils
from samba.samdb import SamDB
from samba.dcerpc import security, misc
from samba.ndr import ndr_pack, ndr_unpack
from samba.credentials import Credentials
from samba.auth import system_session
from samba.dcerpc.security import (SEC_ACE_TYPE_ACCESS_DENIED_OBJECT,
                                   SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT,
                                   SEC_ACE_OBJECT_TYPE_PRESENT)
parser = optparse.OptionParser("samba_CVE-2018-1057_helper")
sambaopts = options.SambaOptions(parser)
parser.add_option_group(options.VersionOptions(parser))
credopts = options.CredentialsOptions(parser)
parser.add_option_group(credopts)
parser.add_option("-H", "--URL", help="LDB URL for database",
                  type=str, metavar="URL", dest="url")
parser.add_option("--lock-pwchange",
                  help="Lock this database against CVE-2018-1057 password changes",
                  action="store_true")
parser.add_option("--unlock-pwchange",
                  help="UnLock this database against CVE-2018-1057 password changes",
                  action="store_true")
parser.add_option("--base", dest="base", default="",
          help="Pass search base that will build DN list for the first DC.")
parser.add_option("--scope", dest="scope", default="SUB",
                   help="Pass search scope that builds DN list. Options: SUB, ONE, BASE")
parser.add_option("--filter", dest="filter", default="(objectClass=user)",
                  help="LDAP filter of objects to lock against password changes")
parser.add_option("--no-schema", dest="no_schema",
                  action="store_true",
                  help="Also apply this change to default ACL in schema")
parser.add_option("--dry-run", dest="dry_run",
                  action="store_true",
                  help="Do not modify the database")


opts, args = parser.parse_args()

if len(args) != 0:
    parser.print_usage()
    sys.exit(1)

if opts.scope.upper() == "SUB":
    search_scope = ldb.SCOPE_SUBTREE
elif opts.scope.upper() == "BASE":
    self.search_scope = ldb.SCOPE_BASE
elif self.search_scope() == "ONE":
    self.search_scope = ldb.SCOPE_ONELEVEL
else:
    raise StandardError("Wrong 'scope' given. Choose from: SUB, ONE, BASE")

if not opts.lock_pwchange and not opts.unlock_pwchange:
    raise StandardError("Neither --lock-pwchange nor --unlock-pwchange specified")

lp_ctx = sambaopts.get_loadparm()
if not opts.no_schema and \
   lp_ctx.get("dsdb:schema update allowed") is None:
    lp_ctx.set("dsdb:schema update allowed", "yes")
    print("Temporarily overriding 'dsdb:schema update allowed' setting")

creds = credopts.get_credentials(lp_ctx)
sam_ldb = SamDB(opts.url, session_info=system_session(),
                credentials=creds, lp=lp_ctx)

sd_helper = samba.sd_utils.SDUtils(sam_ldb)

sam_ldb.transaction_start()

if opts.base is "":
    base_dn = None
else:
    base_dn = opts.base

res = sam_ldb.search(base=base_dn, expression=opts.filter,
                     scope=search_scope,
                     attrs=["ntSecurityDescriptor"])

# This is the right to change the password (unlocked)
pwchange_guid = misc.GUID("ab721a53-1e2f-11d0-9819-00aa0040529b");

# A different samba-only GUID to deny such changes (locked)
pwchange_lock_guid = misc.GUID("ffffffff-CECE-2018-1057-000000b13272");

# We are only worried about when 'world' has this right
sid_world = security.dom_sid(security.SID_WORLD)

def change_desc(desc):
    changed = False
    for ace in desc.dacl.aces:
        if (ace.type == SEC_ACE_TYPE_ACCESS_DENIED_OBJECT or \
            ace.type == SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT) \
            and ace.object.flags & SEC_ACE_OBJECT_TYPE_PRESENT:
            if ace.object.type == pwchange_guid and ace.trustee == sid_world:
                # Cope with a previous verison of this script
                if ace.type == SEC_ACE_TYPE_ACCESS_DENIED_OBJECT and \
                   opts.unlock_pwchange:
                    ace.type = SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT
                    changed = True

                # Lock the object by changing the GUID to a different one
                if ace.type == SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT and \
                   opts.lock_pwchange:

                    ace.object.type = pwchange_lock_guid
                    changed = True

            elif ace.object.type == pwchange_lock_guid and \
                 ace.trustee == sid_world and \
                 ace.type == SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT and \
                   opts.unlock_pwchange:
                # We unlock by changing the guid back the the proper one
                    ace.object.type = pwchange_guid
                    changed = True
    return (changed, desc)

for msg in res:
    desc = ndr_unpack(security.descriptor, msg["ntSecurityDescriptor"][0])

    (changed, new_desc) = change_desc(desc)

    if not changed:
        continue

    operation = "Would modify"
    if not opts.dry_run:
        # We have to use str(msg.dn) rather than just msg.dn as on
        # older versions of samba where this is most needed
        # modify_sd_on_dn can't handle a ldb.Dn object
        sd_helper.modify_sd_on_dn(str(msg.dn), new_desc)
        operation = "Modified"
    print("%s change-password ACL right for world on: %s" % (
          operation, msg.dn))

if not opts.no_schema and search_scope != ldb.SCOPE_BASE:
    res = sam_ldb.search(base=sam_ldb.get_schema_basedn(),
                         expression="(&(objectClass=classSchema)"
                         "(defaultSecurityDescriptor=*))",
                         attrs=["defaultSecurityDescriptor"])

    dom_sid = security.dom_sid(sam_ldb.get_domain_sid())

    for msg in res:
        desc = security.descriptor.from_sddl(msg["defaultSecurityDescriptor"][0],
                                             dom_sid)

        (changed, new_desc) = change_desc(desc)

        if not changed:
            continue

        operation = "Would modify"
        if not opts.dry_run:
            desc_sddl = new_desc.as_sddl(dom_sid)

            new_msg = ldb.Message()
            new_msg.dn = msg.dn
            new_msg["old"] = ldb.MessageElement(msg["defaultSecurityDescriptor"][0],
                                                ldb.FLAG_MOD_DELETE,
                                                "defaultSecurityDescriptor")
            new_msg["new"] = ldb.MessageElement([desc_sddl],
                                                ldb.FLAG_MOD_ADD,
                                                "defaultSecurityDescriptor")
            sam_ldb.modify(new_msg)
            operation = "Modified"
        print("%s change-password ACL right for world for new objects of: %s" % (
              operation, msg.dn))

sam_ldb.transaction_commit()
