| 1 | #!/usr/bin/env python
|
|---|
| 2 | #
|
|---|
| 3 | # update our servicePrincipalName names from spn_update_list
|
|---|
| 4 | #
|
|---|
| 5 | # Copyright (C) Andrew Tridgell 2010
|
|---|
| 6 | #
|
|---|
| 7 | # This program is free software; you can redistribute it and/or modify
|
|---|
| 8 | # it under the terms of the GNU General Public License as published by
|
|---|
| 9 | # the Free Software Foundation; either version 3 of the License, or
|
|---|
| 10 | # (at your option) any later version.
|
|---|
| 11 | #
|
|---|
| 12 | # This program is distributed in the hope that it will be useful,
|
|---|
| 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|---|
| 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|---|
| 15 | # GNU General Public License for more details.
|
|---|
| 16 | #
|
|---|
| 17 | # You should have received a copy of the GNU General Public License
|
|---|
| 18 | # along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|---|
| 19 |
|
|---|
| 20 |
|
|---|
| 21 | import os, sys
|
|---|
| 22 |
|
|---|
| 23 | # ensure we get messages out immediately, so they get in the samba logs,
|
|---|
| 24 | # and don't get swallowed by a timeout
|
|---|
| 25 | os.environ['PYTHONUNBUFFERED'] = '1'
|
|---|
| 26 |
|
|---|
| 27 | # forcing GMT avoids a problem in some timezones with kerberos. Both MIT
|
|---|
| 28 | # heimdal can get mutual authentication errors due to the 24 second difference
|
|---|
| 29 | # between UTC and GMT when using some zone files (eg. the PDT zone from
|
|---|
| 30 | # the US)
|
|---|
| 31 | os.environ["TZ"] = "GMT"
|
|---|
| 32 |
|
|---|
| 33 | # Find right directory when running from source tree
|
|---|
| 34 | sys.path.insert(0, "bin/python")
|
|---|
| 35 |
|
|---|
| 36 | import samba, ldb
|
|---|
| 37 | import optparse
|
|---|
| 38 | from samba import Ldb
|
|---|
| 39 | from samba import getopt as options
|
|---|
| 40 | from samba.auth import system_session
|
|---|
| 41 | from samba.samdb import SamDB
|
|---|
| 42 | from samba.credentials import Credentials, DONT_USE_KERBEROS
|
|---|
| 43 |
|
|---|
| 44 | parser = optparse.OptionParser("samba_spnupdate")
|
|---|
| 45 | sambaopts = options.SambaOptions(parser)
|
|---|
| 46 | parser.add_option_group(sambaopts)
|
|---|
| 47 | parser.add_option_group(options.VersionOptions(parser))
|
|---|
| 48 | parser.add_option("--verbose", action="store_true")
|
|---|
| 49 |
|
|---|
| 50 | credopts = options.CredentialsOptions(parser)
|
|---|
| 51 | parser.add_option_group(credopts)
|
|---|
| 52 |
|
|---|
| 53 | ccachename = None
|
|---|
| 54 |
|
|---|
| 55 | opts, args = parser.parse_args()
|
|---|
| 56 |
|
|---|
| 57 | if len(args) != 0:
|
|---|
| 58 | parser.print_usage()
|
|---|
| 59 | sys.exit(1)
|
|---|
| 60 |
|
|---|
| 61 | lp = sambaopts.get_loadparm()
|
|---|
| 62 | creds = credopts.get_credentials(lp)
|
|---|
| 63 |
|
|---|
| 64 | domain = lp.get("realm")
|
|---|
| 65 | host = lp.get("netbios name")
|
|---|
| 66 |
|
|---|
| 67 |
|
|---|
| 68 | # get the list of substitution vars
|
|---|
| 69 | def get_subst_vars(samdb):
|
|---|
| 70 | global lp
|
|---|
| 71 | vars = {}
|
|---|
| 72 |
|
|---|
| 73 | vars['DNSDOMAIN'] = lp.get('realm').lower()
|
|---|
| 74 | vars['HOSTNAME'] = lp.get('netbios name').lower() + "." + vars['DNSDOMAIN']
|
|---|
| 75 | vars['NETBIOSNAME'] = lp.get('netbios name').upper()
|
|---|
| 76 | vars['WORKGROUP'] = lp.get('workgroup')
|
|---|
| 77 | vars['NTDSGUID'] = samdb.get_ntds_GUID()
|
|---|
| 78 | res = samdb.search(base=None, scope=ldb.SCOPE_BASE, attrs=["objectGUID"])
|
|---|
| 79 | guid = samdb.schema_format_value("objectGUID", res[0]['objectGUID'][0])
|
|---|
| 80 | vars['DOMAINGUID'] = guid
|
|---|
| 81 | return vars
|
|---|
| 82 |
|
|---|
| 83 | try:
|
|---|
| 84 | private_dir = lp.get("private dir")
|
|---|
| 85 | secrets_path = os.path.join(private_dir, lp.get("secrets database"))
|
|---|
| 86 |
|
|---|
| 87 | secrets_db = Ldb(url=secrets_path, session_info=system_session(),
|
|---|
| 88 | credentials=creds, lp=lp)
|
|---|
| 89 | res = secrets_db.search(base=None,
|
|---|
| 90 | expression="(&(objectclass=ldapSecret)(cn=SAMDB Credentials))",
|
|---|
| 91 | attrs=["samAccountName", "secret"])
|
|---|
| 92 |
|
|---|
| 93 | if len(res) == 1:
|
|---|
| 94 | credentials = Credentials()
|
|---|
| 95 | credentials.set_kerberos_state(DONT_USE_KERBEROS)
|
|---|
| 96 |
|
|---|
| 97 | if "samAccountName" in res[0]:
|
|---|
| 98 | credentials.set_username(res[0]["samAccountName"][0])
|
|---|
| 99 |
|
|---|
| 100 | if "secret" in res[0]:
|
|---|
| 101 | credentials.set_password(res[0]["secret"][0])
|
|---|
| 102 |
|
|---|
| 103 | else:
|
|---|
| 104 | credentials = None
|
|---|
| 105 |
|
|---|
| 106 | samdb = SamDB(url=lp.get("sam database"), session_info=system_session(), credentials=credentials, lp=lp)
|
|---|
| 107 | except ldb.LdbError, (num, msg):
|
|---|
| 108 | print("Unable to open sam database %s : %s" % (lp.get("sam database"), msg))
|
|---|
| 109 | sys.exit(1)
|
|---|
| 110 |
|
|---|
| 111 |
|
|---|
| 112 | # get the substitution dictionary
|
|---|
| 113 | sub_vars = get_subst_vars(samdb)
|
|---|
| 114 |
|
|---|
| 115 | # get the list of SPN entries we should have
|
|---|
| 116 | spn_update_list = lp.private_path('spn_update_list')
|
|---|
| 117 |
|
|---|
| 118 | file = open(spn_update_list, "r")
|
|---|
| 119 |
|
|---|
| 120 | spn_list = []
|
|---|
| 121 |
|
|---|
| 122 | # build the spn list
|
|---|
| 123 | for line in file:
|
|---|
| 124 | line = line.strip()
|
|---|
| 125 | if line == '' or line[0] == "#":
|
|---|
| 126 | continue
|
|---|
| 127 | line = samba.substitute_var(line, sub_vars)
|
|---|
| 128 | spn_list.append(line)
|
|---|
| 129 |
|
|---|
| 130 | # get the current list of SPNs in our sam
|
|---|
| 131 | res = samdb.search(base="",
|
|---|
| 132 | expression='(&(objectClass=computer)(samaccountname=%s$))' % sub_vars['NETBIOSNAME'],
|
|---|
| 133 | attrs=["servicePrincipalName"])
|
|---|
| 134 | if not res or len(res) != 1:
|
|---|
| 135 | print("Failed to find computer object for %s$" % sub_vars['NETBIOSNAME'])
|
|---|
| 136 | sys.exit(1)
|
|---|
| 137 |
|
|---|
| 138 | machine_dn = res[0]["dn"]
|
|---|
| 139 |
|
|---|
| 140 | old_spns = []
|
|---|
| 141 | if "servicePrincipalName" in res[0]:
|
|---|
| 142 | for s in res[0]["servicePrincipalName"]:
|
|---|
| 143 | old_spns.append(s)
|
|---|
| 144 |
|
|---|
| 145 | if opts.verbose:
|
|---|
| 146 | print("Existing SPNs: %s" % old_spns)
|
|---|
| 147 |
|
|---|
| 148 | add_list = []
|
|---|
| 149 |
|
|---|
| 150 | # work out what needs to be added
|
|---|
| 151 | for s in spn_list:
|
|---|
| 152 | in_list = False
|
|---|
| 153 | for s2 in old_spns:
|
|---|
| 154 | if s2.upper() == s.upper():
|
|---|
| 155 | in_list = True
|
|---|
| 156 | break
|
|---|
| 157 | if not in_list:
|
|---|
| 158 | add_list.append(s)
|
|---|
| 159 |
|
|---|
| 160 | if opts.verbose:
|
|---|
| 161 | print("New SPNs: %s" % add_list)
|
|---|
| 162 |
|
|---|
| 163 | if add_list == []:
|
|---|
| 164 | if opts.verbose:
|
|---|
| 165 | print("Nothing to add")
|
|---|
| 166 | sys.exit(0)
|
|---|
| 167 |
|
|---|
| 168 | def local_update(add_list):
|
|---|
| 169 | '''store locally'''
|
|---|
| 170 | global res
|
|---|
| 171 | msg = ldb.Message()
|
|---|
| 172 | msg.dn = res[0]['dn']
|
|---|
| 173 | msg[""] = ldb.MessageElement(add_list,
|
|---|
| 174 | ldb.FLAG_MOD_ADD, "servicePrincipalName")
|
|---|
| 175 | res = samdb.modify(msg)
|
|---|
| 176 |
|
|---|
| 177 | def call_rodc_update(d):
|
|---|
| 178 | '''RODCs need to use the writeSPN DRS call'''
|
|---|
| 179 | global lp, sub_vars
|
|---|
| 180 | from samba import drs_utils
|
|---|
| 181 | from samba.dcerpc import drsuapi, nbt
|
|---|
| 182 | from samba.net import Net
|
|---|
| 183 |
|
|---|
| 184 | if opts.verbose:
|
|---|
| 185 | print("Using RODC SPN update")
|
|---|
| 186 |
|
|---|
| 187 | creds = credopts.get_credentials(lp)
|
|---|
| 188 | creds.set_machine_account(lp)
|
|---|
| 189 |
|
|---|
| 190 | net = Net(creds=creds, lp=lp)
|
|---|
| 191 | try:
|
|---|
| 192 | cldap_ret = net.finddc(domain, nbt.NBT_SERVER_DS | nbt.NBT_SERVER_WRITABLE)
|
|---|
| 193 | except Exception, reason:
|
|---|
| 194 | print("Unable to find writeable DC for domain '%s' to send DRS writeSPN to : %s" % (domain, reason))
|
|---|
| 195 | sys.exit(1)
|
|---|
| 196 | server = cldap_ret.pdc_dns_name
|
|---|
| 197 | try:
|
|---|
| 198 | binding_options = "seal"
|
|---|
| 199 | if lp.get("log level") >= 5:
|
|---|
| 200 | binding_options += ",print"
|
|---|
| 201 | drs = drsuapi.drsuapi('ncacn_ip_tcp:%s[%s]' % (server, binding_options), lp, creds)
|
|---|
| 202 | (drs_handle, supported_extensions) = drs_utils.drs_DsBind(drs)
|
|---|
| 203 | except Exception, reason:
|
|---|
| 204 | print("Unable to connect to DC '%s' for domain '%s' : %s" % (server, domain, reason))
|
|---|
| 205 | sys.exit(1)
|
|---|
| 206 | req1 = drsuapi.DsWriteAccountSpnRequest1()
|
|---|
| 207 | req1.operation = drsuapi.DRSUAPI_DS_SPN_OPERATION_ADD
|
|---|
| 208 | req1.object_dn = str(machine_dn)
|
|---|
| 209 | req1.count = 0
|
|---|
| 210 | spn_names = []
|
|---|
| 211 | for n in add_list:
|
|---|
| 212 | if n.find('E3514235-4B06-11D1-AB04-00C04FC2DCD2') != -1:
|
|---|
| 213 | # this one isn't allowed for RODCs, but we don't know why yet
|
|---|
| 214 | continue
|
|---|
| 215 | ns = drsuapi.DsNameString()
|
|---|
| 216 | ns.str = n
|
|---|
| 217 | spn_names.append(ns)
|
|---|
| 218 | req1.count = req1.count + 1
|
|---|
| 219 | if spn_names == []:
|
|---|
| 220 | return
|
|---|
| 221 | req1.spn_names = spn_names
|
|---|
| 222 | (level, res) = drs.DsWriteAccountSpn(drs_handle, 1, req1)
|
|---|
| 223 |
|
|---|
| 224 | if samdb.am_rodc():
|
|---|
| 225 | call_rodc_update(add_list)
|
|---|
| 226 | else:
|
|---|
| 227 | local_update(add_list)
|
|---|