-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjoinAD.py
341 lines (285 loc) · 12.3 KB
/
joinAD.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
#!/bin/env python3
#
# Simple script to join or leave a realm.
#
# This script will also rewrite the Samba and SSSD conf files and restart Samba and SSSD
#
# Script should be safe as any exception or error during re-configuration should
# result in restoring a backup copy of the previous config file
#
import os
import sh
import sys
import glob
import shutil
import logging
from configobj import ConfigObj
import time
import shlex, subprocess
import ldap3
import argparse
#import radium.utils.pwcrypt as pwcrypt
# create logger with 'spam_application'
logger = logging.getLogger('joinAD')
logger.setLevel(logging.DEBUG)
# create file handler which logs even debug messages
fh = logging.FileHandler('/var/log/joinAD.log')
fh.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
logger.addHandler(fh)
class RealmException(Exception):
pass
class ConfigSSSDException(Exception):
pass
class ConfigSambaException(Exception):
pass
def get_sid(upn, password, domain):
# Get kerberos ticket using kinit
sh.kinit(sh.echo(password),(upn))
logger.info(sh.klist())
server = ldap3.Server(host=domain, get_info=ldap3.ALL)
conn = ldap3.Connection(server, user=upn, password=password,
auto_bind=True)
search_base = ','.join(['DC=' + dp for dp in domain.split('.')])
search_filter = '(&(objectclass=domain))'
params = {
'search_base': search_base,
'search_filter': search_filter,
'search_scope': ldap3.SUBTREE,
'attributes': ['objectSid'],
'paged_size': 1,
'generator': False
}
conn.extend.standard.paged_search(**params)
return conn.entries[0].objectSid
def get_netbios(upn, password, domain):
server = ldap3.Server(host=domain, get_info=ldap3.ALL)
conn = ldap3.Connection(server, user=upn, password=password,
auto_bind=True)
domain_components = ','.join(['dc=' + dp for dp in domain.split('.')])
search_filter = '(&(netbiosname=*))'
params = {
'search_base': 'CN=Partitions,CN=Configuration,' + domain_components,
'search_filter': search_filter,
'search_scope': ldap3.SUBTREE,
'attributes': ldap3.ALL_ATTRIBUTES,
'paged_size': 1,
'generator': False
}
results = conn.extend.standard.paged_search(**params)
for info in results:
if info['attributes']['nCName'].lower() == domain_components:
return info['attributes']['nETBIOSName']
def create_samba_default_config(smbcfgfile):
# Setup the Samba confguration file
cfg = ConfigObj()
cfg.filename = smbcfgfile
cfg['global'] = {}
cfg['global']['workgroup'] = 'StrongLink'
cfg['global']['server string'] = 'StrongLink Samba Server'
cfg['global']['log file'] = '/var/log/samba/log.%m'
cfg['global']['log level'] = '3'
cfg['global']['max log size'] = '50'
cfg['global']['security'] = 'user'
cfg['global']['client signing'] = 'auto'
cfg['global']['server signing'] = 'auto'
cfg['global']['load printers'] = 'no'
cfg['global']['printing'] = 'bsd'
cfg['global']['printcap name'] = '/dev/null'
cfg['global']['disable spoolss'] = 'yes'
cfg.write()
def configure_samba(upn, password, domain, reset=False):
backup_complete = False
smbcfgfile = '/etc/samba/smb.conf'
# Make a backup of the Samba config file
if not os.path.isfile(smbcfgfile):
# If there is no existing / default smb.conf file, first create one
# so that we can both keep a backup and be able to restore if config fails
create_samba_default_config(smbcfgfile + '.bak')
else:
shutil.copy(smbcfgfile, smbcfgfile + '.bak')
backup_complete = True
try:
if not reset:
# get AD workgroup
workgroup = get_netbios(upn, password, domain)
# Setup the Samba confguration file
cfg = ConfigObj()
cfg.filename = smbcfgfile
cfg['global'] = {}
cfg['global']['workgroup'] = workgroup.lower() if workgroup else ''
cfg['global']['server string'] = 'StrongLink Samba Server'
cfg['global']['log file'] = '/var/log/samba/log.%m'
cfg['global']['log level'] = '3'
cfg['global']['max log size'] = '50'
cfg['global']['security'] = 'ads'
cfg['global']['encrypt passwords'] = 'yes'
cfg['global']['passdb backend'] = 'tdbsam'
cfg['global']['kerberos method'] = 'secrets and keytab'
cfg['global']['realm'] = domain.lower()
cfg['global']['vfs objects'] = 'acl_xattr'
cfg['global']['map acl inherit'] = 'yes'
cfg['global']['store dos attributes'] = 'yes'
cfg['global']['deadtime'] = '10'
cfg['global']['client signing'] = 'auto'
cfg['global']['server signing'] = 'auto'
cfg['global']['dns proxy'] = 'no'
cfg['global']['load printers'] = 'no'
cfg['global']['printing'] = 'bsd'
cfg['global']['printcap name'] = '/dev/null'
cfg['global']['disable spoolss'] = 'yes'
cfg['global']['map untrusted to domain'] = 'yes'
cfg.write()
# When removing a domain, rewrite the default StrongLink samba config
else:
create_samba_default_config(smbcfgfile)
# Reload SMB
sh.systemctl('restart','smb')
except Exception as exc:
# Something nasty happened when we tried to reconfigure samba
# so we need to restore our last backup file
logger.error("Error configuring Samba, restoring backup config: {}".format(str(exc)))
if backup_complete:
shutil.copy(smbcfgfile + '.bak', smbcfgfile)
# Reload SMB
sh.systemctl('restart','smb')
raise ConfigSambaException
def create_default_sssd_config(sssdcfgfile):
# Nothing to do here for default
sssdcfg = ConfigObj(sssdcfgfile)
sssdcfg.write()
def configure_sssd(upn, password, domain, reset=False):
backup_complete = False
sssdcfgfile = '/etc/sssd/sssd.conf'
if not os.path.isfile(sssdcfgfile):
# Create a default sssd.conf if one does not currently exist
logger.info("Creating default SSSD config")
create_default_sssd_config(sssdcfgfile + '.bak')
else:
# Make a backup of the sssd config file
logger.info("Backing up SSSD config")
shutil.copy(sssdcfgfile, sssdcfgfile + '.bak')
backup_complete = True
try:
# Stop SSSD and clear caches
logger.info("Stoping sssd service and clearing caches")
sh.systemctl('stop','sssd')
for f in glob.glob('/var/lib/sss/db/*'):
os.remove(f)
if not reset:
# get_sid
sid = get_sid(upn, password, domain)
logger.info("configure_sssd: Got sid: {}".format(str(sid)))
# Edit the sssd.conf with IdMap Details
sssdcfg = ConfigObj(sssdcfgfile)
logger.info("Setting SSSD config for domain: {}".format(str(domain)))
domain_key = 'domain/'+domain
# Check to see if key passed in was correct case
# If not, try to find a key that matches caseless
if domain_key not in sssdcfg:
matches = [k for k in sssdcfg if k.lower() == domain_key.lower()]
if len(matches) > 1:
logger.error("Exact key match not found, but multiple similar keys: {} --- Using {}".format(str(matches), str(matches[0])))
elif len(matches) == 1:
logger.info("Found good match for domain {} on sssd key {}".format(str(domain_key), str(matches[0])))
else:
logger.error("Found NO good matches for domain {}".format(str(domain_key)))
raise Exception("No matching domain")
domain_key = matches[0]
sssdcfg[domain_key]['ldap_id_mapping'] = useIdMap
if useIdMap:
sssdcfg[domain_key]['ldap_idmap_range_min'] = 1000000
sssdcfg[domain_key]['ldap_idmap_range_size'] = 2000000
sssdcfg[domain_key]['ldap_idmap_default_domain_sid'] = sid
else:
sssdcfg[domain_key].pop('ldap_idmap_range_min', None)
sssdcfg[domain_key].pop('ldap_idmap_range_size', None)
sssdcfg[domain_key].pop('ldap_idmap_default_domain_sid', None)
else:
# Default sssd.conf would go here
create_default_sssd_config(sssdcfgfile)
sssdcfg.write()
# Restart sssd
sh.systemctl('restart', 'sssd')
except Exception as exc:
logger.error("Error configuring sssd, restoring backup config: {}".format(str(exc)))
if backup_complete:
# Something nasty happened when we tried to reconfigure sssd.conf
# so we need to restore our last backup file
shutil.copy(sssdcfgfile + '.bak', sssdcfgfile)
# Restart SSSD now that we've restored out backup
sh.systemctl('restart', 'sssd')
raise ConfigSSSDException
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser(description='Join or leave AD')
parser.add_argument('user', help='User name')
parser.add_argument('password', help='User password')
parser.add_argument('domain', help='Domain name')
parser.add_argument('useIdMap', help='Use LDAP ID mapping?')
# Optional arguments
parser.add_argument('--leave', help='Leave domain', action='store_true')
args = parser.parse_args()
user = args.user
password = args.password
domain = args.domain
if args.useIdMap.lower() in ('true', 't'):
useIdMap = True
elif args.useIdMap.lower() in ('false', 'f'):
useIdMap = False
else:
raise argparse.ArgumentTypeError('Boolean value expected')
# True if we are leaving an AD instead of joining one
leave_AD = args.leave
upn=user+'@'+domain.upper()
# If not root then switch to root user and re-run this script
if os.geteuid() != 0:
os.execvp('sudo', ['sudo', 'python3'] + sys.argv)
logger.info('user = {}, password = {}, domain = {}'.format(user, 'XXXXXXXX', domain))
# Decrypt password
# password = pwcrypt.decrypt(password)
if not leave_AD:
logger.info("Joining realm...")
# Make sure we're not already joined to the domain
output = sh.realm('list')
# Check for a case mismatch before joining
domain_output = ''
for line in output:
if 'domain-name' not in line:
continue
domain_output = line.split()[-1]
if domain.lower() == domain_output.lower():
logger.info('Already joined to {}'.format(domain))
sys.exit(0)
# Join the AD domain
try:
sh.realm(sh.echo(password), 'join', '-v', '--user=' + user, domain)
except Exception as exc:
raise RealmException("Unable to join realm for domain: {} user: {}".format(str(domain), str(user)))
else:
logger.info("Leaving realm...")
try:
sh.realm(sh.echo(password), 'leave', '-v', '--user=' + user, domain)
except Exception as exc:
raise RealmException("Unable to leave realm for domain: {} user: {}".format(str(domain), str(user)))
logger.info(sh.realm('list'))
# Write or reset sssd.conf file and restart SSSD
logger.info("Configuring SSSD")
configure_sssd(upn, password, domain, reset=leave_AD)
# Write or reset smb.conf file and restart Samba
logger.info("Configuring Samba")
configure_samba(upn, password, domain, reset=leave_AD)
except RealmException as exc:
logger.error(str(exc))
sys.exit(1)
except ConfigSSSDException as exc:
logger.error("Error configuring SSSD: {}".format(str(exc)))
sys.exit(1)
except ConfigSambaException as exc:
logger.error("Error configuring Samba: {}".format(str(exc)))
sys.exit(1)
except Exception as exc:
logger.error("*** Unhandled exception *** : {}".format(str(exc)))
sys.exit(1)
sys.exit(0)