3 # update our servicePrincipalName names from spn_update_list
5 # Copyright (C) Andrew Tridgell 2010
6 # Copyright (C) Matthieu Patou <mat@matws.net> 2012
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # ensure we get messages out immediately, so they get in the samba logs,
25 # and don't get swallowed by a timeout
26 os
.environ
['PYTHONUNBUFFERED'] = '1'
28 # forcing GMT avoids a problem in some timezones with kerberos. Both MIT
29 # heimdal can get mutual authentication errors due to the 24 second difference
30 # between UTC and GMT when using some zone files (eg. the PDT zone from
32 os
.environ
["TZ"] = "GMT"
34 # Find right directory when running from source tree
35 sys
.path
.insert(0, "bin/python")
40 from samba
import getopt
as options
41 from samba
.auth
import system_session
42 from samba
.samdb
import SamDB
43 from samba
.credentials
import Credentials
, DONT_USE_KERBEROS
45 parser
= optparse
.OptionParser("samba_spnupdate")
46 sambaopts
= options
.SambaOptions(parser
)
47 parser
.add_option_group(sambaopts
)
48 parser
.add_option_group(options
.VersionOptions(parser
))
49 parser
.add_option("--verbose", action
="store_true")
51 credopts
= options
.CredentialsOptions(parser
)
52 parser
.add_option_group(credopts
)
56 opts
, args
= parser
.parse_args()
62 lp
= sambaopts
.get_loadparm()
63 creds
= credopts
.get_credentials(lp
)
65 domain
= lp
.get("realm")
66 host
= lp
.get("netbios name")
69 # get the list of substitution vars
70 def get_subst_vars(samdb
):
74 vars['DNSDOMAIN'] = samdb
.domain_dns_name()
75 vars['DNSFOREST'] = samdb
.forest_dns_name()
76 vars['HOSTNAME'] = samdb
.host_dns_name()
77 vars['NETBIOSNAME'] = lp
.get('netbios name').upper()
78 vars['WORKGROUP'] = lp
.get('workgroup')
79 vars['NTDSGUID'] = samdb
.get_ntds_GUID()
80 res
= samdb
.search(base
=samdb
.get_default_basedn(), scope
=ldb
.SCOPE_BASE
, attrs
=["objectGUID"])
81 guid
= samdb
.schema_format_value("objectGUID", res
[0]['objectGUID'][0])
82 vars['DOMAINGUID'] = guid
86 private_dir
= lp
.get("private dir")
87 secrets_path
= os
.path
.join(private_dir
, "secrets.ldb")
89 secrets_db
= Ldb(url
=secrets_path
, session_info
=system_session(),
90 credentials
=creds
, lp
=lp
)
91 res
= secrets_db
.search(base
=None,
92 expression
="(&(objectclass=ldapSecret)(cn=SAMDB Credentials))",
93 attrs
=["samAccountName", "secret"])
96 credentials
= Credentials()
97 credentials
.set_kerberos_state(DONT_USE_KERBEROS
)
99 if "samAccountName" in res
[0]:
100 credentials
.set_username(res
[0]["samAccountName"][0])
102 if "secret" in res
[0]:
103 credentials
.set_password(res
[0]["secret"][0])
108 samdb
= SamDB(url
=lp
.samdb_url(), session_info
=system_session(), credentials
=credentials
, lp
=lp
)
109 except ldb
.LdbError
, (num
, msg
):
110 print("Unable to open sam database %s : %s" % (lp
.samdb_url(), msg
))
114 # get the substitution dictionary
115 sub_vars
= get_subst_vars(samdb
)
117 # get the list of SPN entries we should have
118 spn_update_list
= lp
.private_path('spn_update_list')
120 file = open(spn_update_list
, "r")
124 has_forest_dns
= False
125 has_domain_dns
= False
126 # check if we "are DNS server"
127 res
= samdb
.search(base
=samdb
.get_config_basedn(),
128 expression
='(objectguid=%s)' % sub_vars
['NTDSGUID'],
129 attrs
=["msDS-hasMasterNCs"])
131 basedn
= str(samdb
.get_default_basedn())
133 if "msDS-hasMasterNCs" in res
[0]:
134 for e
in res
[0]["msDS-hasMasterNCs"]:
135 if str(e
) == "DC=DomainDnsZones,%s" % basedn
:
136 has_domain_dns
= True
137 if str(e
) == "DC=ForestDnsZones,%s" % basedn
:
138 has_forest_dns
= True
144 if line
== '' or line
[0] == "#":
146 if re
.match(r
".*/DomainDnsZones\..*", line
) and not has_domain_dns
:
148 if re
.match(r
".*/ForestDnsZones\..*", line
) and not has_forest_dns
:
150 line
= samba
.substitute_var(line
, sub_vars
)
151 spn_list
.append(line
)
153 # get the current list of SPNs in our sam
154 res
= samdb
.search(base
=samdb
.get_default_basedn(),
155 expression
='(&(objectClass=computer)(samaccountname=%s$))' % sub_vars
['NETBIOSNAME'],
156 attrs
=["servicePrincipalName"])
157 if not res
or len(res
) != 1:
158 print("Failed to find computer object for %s$" % sub_vars
['NETBIOSNAME'])
161 machine_dn
= res
[0]["dn"]
164 if "servicePrincipalName" in res
[0]:
165 for s
in res
[0]["servicePrincipalName"]:
169 print("Existing SPNs: %s" % old_spns
)
173 # work out what needs to be added
177 if s2
.upper() == s
.upper():
184 print("New SPNs: %s" % add_list
)
188 print("Nothing to add")
191 def local_update(add_list
):
195 msg
.dn
= res
[0]['dn']
196 msg
[""] = ldb
.MessageElement(add_list
,
197 ldb
.FLAG_MOD_ADD
, "servicePrincipalName")
198 res
= samdb
.modify(msg
)
200 def call_rodc_update(d
):
201 '''RODCs need to use the writeSPN DRS call'''
203 from samba
import drs_utils
204 from samba
.dcerpc
import drsuapi
, nbt
205 from samba
.net
import Net
208 print("Using RODC SPN update")
210 creds
= credopts
.get_credentials(lp
)
211 creds
.set_machine_account(lp
)
213 net
= Net(creds
=creds
, lp
=lp
)
215 cldap_ret
= net
.finddc(domain
=domain
, flags
=nbt
.NBT_SERVER_DS | nbt
.NBT_SERVER_WRITABLE
)
216 except Exception, reason
:
217 print("Unable to find writeable DC for domain '%s' to send DRS writeSPN to : %s" % (domain
, reason
))
219 server
= cldap_ret
.pdc_dns_name
221 binding_options
= "seal"
222 if int(lp
.get("log level")) >= 5:
223 binding_options
+= ",print"
224 drs
= drsuapi
.drsuapi('ncacn_ip_tcp:%s[%s]' % (server
, binding_options
), lp
, creds
)
225 (drs_handle
, supported_extensions
) = drs_utils
.drs_DsBind(drs
)
226 except Exception, reason
:
227 print("Unable to connect to DC '%s' for domain '%s' : %s" % (server
, domain
, reason
))
229 req1
= drsuapi
.DsWriteAccountSpnRequest1()
230 req1
.operation
= drsuapi
.DRSUAPI_DS_SPN_OPERATION_ADD
231 req1
.object_dn
= str(machine_dn
)
235 if n
.find('E3514235-4B06-11D1-AB04-00C04FC2DCD2') != -1:
236 # this one isn't allowed for RODCs, but we don't know why yet
238 ns
= drsuapi
.DsNameString()
241 req1
.count
= req1
.count
+ 1
244 req1
.spn_names
= spn_names
245 (level
, res
) = drs
.DsWriteAccountSpn(drs_handle
, 1, req1
)
246 if (res
.status
!= (0, 'WERR_OK')):
247 print "WriteAccountSpn has failed with error %s" % str(res
.status
)
250 call_rodc_update(add_list
)
252 local_update(add_list
)