TODO review after conflicts s4:provision: fix sysvol security_descriptors (let them...
[metze/samba/wip.git] / python / samba / netcmd / gpo.py
1 # implement samba_tool gpo commands
2 #
3 # Copyright Andrew Tridgell 2010
4 # Copyright Amitay Isaacs 2011-2012 <amitay@gmail.com>
5 #
6 # based on C implementation by Guenther Deschner and Wilco Baan Hofman
7 #
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.
12 #
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.
17 #
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/>.
20 #
21 from __future__ import print_function
22 import os
23 import samba.getopt as options
24 import ldb
25 import re
26 import xml.etree.ElementTree as ET
27 import shutil
28 import tempfile
29
30 from samba.auth import system_session
31 from samba.netcmd import (
32     Command,
33     CommandError,
34     Option,
35     SuperCommand,
36 )
37 from samba.samdb import SamDB
38 from samba import dsdb
39 from samba.dcerpc import security
40 from samba.ndr import ndr_unpack
41 import samba.security
42 import samba.auth
43 from samba.auth import AUTH_SESSION_INFO_DEFAULT_GROUPS, AUTH_SESSION_INFO_AUTHENTICATED, AUTH_SESSION_INFO_SIMPLE_PRIVILEGES
44 from samba.netcmd.common import netcmd_finddc
45 from samba import policy
46 from samba.samba3 import param as s3param
47 from samba.samba3 import libsmb_samba_internal as libsmb
48 from samba import NTSTATUSError
49 import uuid
50 from samba.ntacls import dsacl2fsacl
51 from samba.dcerpc import nbt
52 from samba.net import Net
53 from samba.gp_parse import GPParser, GPNoParserException, GPGeneralizeException
54 from samba.gp_parse.gp_pol import GPPolParser
55 from samba.gp_parse.gp_ini import (
56     GPIniParser,
57     GPTIniParser,
58     GPFDeploy1IniParser,
59     GPScriptsIniParser
60 )
61 from samba.gp_parse.gp_csv import GPAuditCsvParser
62 from samba.gp_parse.gp_inf import GptTmplInfParser
63 from samba.gp_parse.gp_aas import GPAasParser
64
65 from samba.provision import DEFAULT_POLICY_GUID, DEFAULT_DC_POLICY_GUID, SYSVOL_SUBFOLDER_SD
66
67
68 def attr_default(msg, attrname, default):
69     '''get an attribute from a ldap msg with a default'''
70     if attrname in msg:
71         return msg[attrname][0]
72     return default
73
74
75 def gpo_flags_string(value):
76     '''return gpo flags string'''
77     flags = policy.get_gpo_flags(value)
78     if not flags:
79         ret = 'NONE'
80     else:
81         ret = ' '.join(flags)
82     return ret
83
84
85 def gplink_options_string(value):
86     '''return gplink options string'''
87     options = policy.get_gplink_options(value)
88     if not options:
89         ret = 'NONE'
90     else:
91         ret = ' '.join(options)
92     return ret
93
94
95 def parse_gplink(gplink):
96     '''parse a gPLink into an array of dn and options'''
97     ret = []
98     a = gplink.split(']')
99     for g in a:
100         if not g:
101             continue
102         d = g.split(';')
103         if len(d) != 2 or not d[0].startswith("[LDAP://"):
104             raise RuntimeError("Badly formed gPLink '%s'" % g)
105         ret.append({'dn': d[0][8:], 'options': int(d[1])})
106     return ret
107
108
109 def encode_gplink(gplist):
110     '''Encode an array of dn and options into gPLink string'''
111     ret = ''
112     for g in gplist:
113         ret += "[LDAP://%s;%d]" % (g['dn'], g['options'])
114     return ret
115
116
117 def dc_url(lp, creds, url=None, dc=None):
118     '''If URL is not specified, return URL for writable DC.
119     If dc is provided, use that to construct ldap URL'''
120
121     if url is None:
122         if dc is None:
123             try:
124                 dc = netcmd_finddc(lp, creds)
125             except Exception as e:
126                 raise RuntimeError("Could not find a DC for domain", e)
127         url = 'ldap://' + dc
128     return url
129
130
131 def get_gpo_dn(samdb, gpo):
132     '''Construct the DN for gpo'''
133
134     dn = samdb.get_default_basedn()
135     dn.add_child(ldb.Dn(samdb, "CN=Policies,CN=System"))
136     dn.add_child(ldb.Dn(samdb, "CN=%s" % gpo))
137     return dn
138
139
140 def get_gpo_info(samdb, gpo=None, displayname=None, dn=None,
141                  sd_flags=(security.SECINFO_OWNER |
142                            security.SECINFO_GROUP |
143                            security.SECINFO_DACL |
144                            security.SECINFO_SACL)):
145     '''Get GPO information using gpo, displayname or dn'''
146
147     policies_dn = samdb.get_default_basedn()
148     policies_dn.add_child(ldb.Dn(samdb, "CN=Policies,CN=System"))
149
150     base_dn = policies_dn
151     search_expr = "(objectClass=groupPolicyContainer)"
152     search_scope = ldb.SCOPE_ONELEVEL
153
154     if gpo is not None:
155         search_expr = "(&(objectClass=groupPolicyContainer)(name=%s))" % ldb.binary_encode(gpo)
156
157     if displayname is not None:
158         search_expr = "(&(objectClass=groupPolicyContainer)(displayname=%s))" % ldb.binary_encode(displayname)
159
160     if dn is not None:
161         base_dn = dn
162         search_scope = ldb.SCOPE_BASE
163
164     try:
165         msg = samdb.search(base=base_dn, scope=search_scope,
166                            expression=search_expr,
167                            attrs=['nTSecurityDescriptor',
168                                   'versionNumber',
169                                   'flags',
170                                   'name',
171                                   'displayName',
172                                   'gPCFileSysPath'],
173                            controls=['sd_flags:1:%d' % sd_flags])
174     except Exception as e:
175         if gpo is not None:
176             mesg = "Cannot get information for GPO %s" % gpo
177         else:
178             mesg = "Cannot get information for GPOs"
179         raise CommandError(mesg, e)
180
181     return msg
182
183
184 def get_gpo_containers(samdb, gpo):
185     '''lists dn of containers for a GPO'''
186
187     search_expr = "(&(objectClass=*)(gPLink=*%s*))" % gpo
188     try:
189         msg = samdb.search(expression=search_expr, attrs=['gPLink'])
190     except Exception as e:
191         raise CommandError("Could not find container(s) with GPO %s" % gpo, e)
192
193     return msg
194
195
196 def del_gpo_link(samdb, container_dn, gpo):
197     '''delete GPO link for the container'''
198     # Check if valid Container DN and get existing GPlinks
199     try:
200         msg = samdb.search(base=container_dn, scope=ldb.SCOPE_BASE,
201                            expression="(objectClass=*)",
202                            attrs=['gPLink'])[0]
203     except Exception as e:
204         raise CommandError("Container '%s' does not exist" % container_dn, e)
205
206     found = False
207     gpo_dn = str(get_gpo_dn(samdb, gpo))
208     if 'gPLink' in msg:
209         gplist = parse_gplink(str(msg['gPLink'][0]))
210         for g in gplist:
211             if g['dn'].lower() == gpo_dn.lower():
212                 gplist.remove(g)
213                 found = True
214                 break
215     else:
216         raise CommandError("No GPO(s) linked to this container")
217
218     if not found:
219         raise CommandError("GPO '%s' not linked to this container" % gpo)
220
221     m = ldb.Message()
222     m.dn = container_dn
223     if gplist:
224         gplink_str = encode_gplink(gplist)
225         m['r0'] = ldb.MessageElement(gplink_str, ldb.FLAG_MOD_REPLACE, 'gPLink')
226     else:
227         m['d0'] = ldb.MessageElement(msg['gPLink'][0], ldb.FLAG_MOD_DELETE, 'gPLink')
228     try:
229         samdb.modify(m)
230     except Exception as e:
231         raise CommandError("Error removing GPO from container", e)
232
233
234 def parse_unc(unc):
235     '''Parse UNC string into a hostname, a service, and a filepath'''
236     if unc.startswith('\\\\') and unc.startswith('//'):
237         raise ValueError("UNC doesn't start with \\\\ or //")
238     tmp = unc[2:].split('/', 2)
239     if len(tmp) == 3:
240         return tmp
241     tmp = unc[2:].split('\\', 2)
242     if len(tmp) == 3:
243         return tmp
244     raise ValueError("Invalid UNC string: %s" % unc)
245
246
247 def find_parser(name, flags=re.IGNORECASE):
248     if re.match('fdeploy1\.ini$', name, flags=flags):
249         return GPFDeploy1IniParser()
250     if re.match('audit\.csv$', name, flags=flags):
251         return GPAuditCsvParser()
252     if re.match('GptTmpl\.inf$', name, flags=flags):
253         return GptTmplInfParser()
254     if re.match('GPT\.INI$', name, flags=flags):
255         return GPTIniParser()
256     if re.match('scripts.ini$', name, flags=flags):
257         return GPScriptsIniParser()
258     if re.match('psscripts.ini$', name, flags=flags):
259         return GPScriptsIniParser()
260     if re.match('.*\.ini$', name, flags=flags):
261         return GPIniParser()
262     if re.match('.*\.pol$', name, flags=flags):
263         return GPPolParser()
264     if re.match('.*\.aas$', name, flags=flags):
265         return GPAasParser()
266
267     return GPParser()
268
269
270 def backup_directory_remote_to_local(conn, remotedir, localdir):
271     SUFFIX = '.SAMBABACKUP'
272     if not os.path.isdir(localdir):
273         os.mkdir(localdir)
274     r_dirs = [ remotedir ]
275     l_dirs = [ localdir ]
276     while r_dirs:
277         r_dir = r_dirs.pop()
278         l_dir = l_dirs.pop()
279
280         dirlist = conn.list(r_dir, attribs=attr_flags)
281         dirlist.sort(key=lambda x : x['name'])
282         for e in dirlist:
283             r_name = r_dir + '\\' + e['name']
284             l_name = os.path.join(l_dir, e['name'])
285
286             if e['attrib'] & libsmb.FILE_ATTRIBUTE_DIRECTORY:
287                 r_dirs.append(r_name)
288                 l_dirs.append(l_name)
289                 os.mkdir(l_name)
290             else:
291                 data = conn.loadfile(r_name)
292                 with open(l_name + SUFFIX, 'wb') as f:
293                     f.write(data)
294
295                 parser = find_parser(e['name'])
296                 parser.parse(data)
297                 parser.write_xml(l_name + '.xml')
298
299
300 attr_flags = libsmb.FILE_ATTRIBUTE_SYSTEM | \
301              libsmb.FILE_ATTRIBUTE_DIRECTORY | \
302              libsmb.FILE_ATTRIBUTE_ARCHIVE | \
303              libsmb.FILE_ATTRIBUTE_HIDDEN
304
305
306 def copy_directory_remote_to_local(conn, remotedir, localdir):
307     if not os.path.isdir(localdir):
308         os.mkdir(localdir)
309     r_dirs = [remotedir]
310     l_dirs = [localdir]
311     while r_dirs:
312         r_dir = r_dirs.pop()
313         l_dir = l_dirs.pop()
314
315         dirlist = conn.list(r_dir, attribs=attr_flags)
316         dirlist.sort(key=lambda x : x['name'])
317         for e in dirlist:
318             r_name = r_dir + '\\' + e['name']
319             l_name = os.path.join(l_dir, e['name'])
320
321             if e['attrib'] & libsmb.FILE_ATTRIBUTE_DIRECTORY:
322                 r_dirs.append(r_name)
323                 l_dirs.append(l_name)
324                 os.mkdir(l_name)
325             else:
326                 data = conn.loadfile(r_name)
327                 open(l_name, 'wb').write(data)
328
329
330 def copy_directory_local_to_remote(conn, localdir, remotedir,
331                                    ignore_existing=False):
332     if not conn.chkpath(remotedir):
333         conn.mkdir(remotedir)
334     l_dirs = [localdir]
335     r_dirs = [remotedir]
336     while l_dirs:
337         l_dir = l_dirs.pop()
338         r_dir = r_dirs.pop()
339
340         dirlist = os.listdir(l_dir)
341         dirlist.sort()
342         for e in dirlist:
343             l_name = os.path.join(l_dir, e)
344             r_name = r_dir + '\\' + e
345
346             if os.path.isdir(l_name):
347                 l_dirs.append(l_name)
348                 r_dirs.append(r_name)
349                 try:
350                     conn.mkdir(r_name)
351                 except NTSTATUSError:
352                     if not ignore_existing:
353                         raise
354             else:
355                 data = open(l_name, 'rb').read()
356                 conn.savefile(r_name, data)
357
358
359 def create_directory_hier(conn, remotedir):
360     elems = remotedir.replace('/', '\\').split('\\')
361     path = ""
362     for e in elems:
363         path = path + '\\' + e
364         if not conn.chkpath(path):
365             conn.mkdir(path)
366
367 def smb_connection(dc_hostname, service, lp, creds, sign=False):
368     # SMB connect to DC
369     try:
370         # the SMB bindings rely on having a s3 loadparm
371         s3_lp = s3param.get_context()
372         s3_lp.load(lp.configfile)
373         conn = libsmb.Conn(dc_hostname, service, lp=s3_lp, creds=creds, sign=sign)
374     except Exception:
375         raise CommandError("Error connecting to '%s' using SMB" % dc_hostname)
376     return conn
377
378
379 class GPOCommand(Command):
380     def construct_tmpdir(self, tmpdir, gpo):
381         """Ensure that the temporary directory structure used in fetch,
382         backup, create, and restore is consistent.
383
384         If --tmpdir is used the named directory must be present, which may
385         contain a 'policy' subdirectory, but 'policy' must not itself have
386         a subdirectory with the gpo name. The policy and gpo directories
387         will be created.
388
389         If --tmpdir is not used, a temporary directory is securely created.
390         """
391         if tmpdir is None:
392             tmpdir = tempfile.mkdtemp()
393             print("Using temporary directory %s (use --tmpdir to change)" % tmpdir,
394                   file=self.outf)
395
396         if not os.path.isdir(tmpdir):
397             raise CommandError("Temporary directory '%s' does not exist" % tmpdir)
398
399         localdir = os.path.join(tmpdir, "policy")
400         if not os.path.isdir(localdir):
401             os.mkdir(localdir)
402
403         gpodir = os.path.join(localdir, gpo)
404         if os.path.isdir(gpodir):
405             raise CommandError(
406                 "GPO directory '%s' already exists, refusing to overwrite" % gpodir)
407
408         try:
409             os.mkdir(gpodir)
410         except (IOError, OSError) as e:
411             raise CommandError("Error creating teporary GPO directory", e)
412
413         return tmpdir, gpodir
414
415     def samdb_connect(self):
416         '''make a ldap connection to the server'''
417         try:
418             self.samdb = SamDB(url=self.url,
419                                session_info=system_session(),
420                                credentials=self.creds, lp=self.lp)
421         except Exception as e:
422             raise CommandError("LDAP connection to %s failed " % self.url, e)
423
424
425 class cmd_listall(GPOCommand):
426     """List all GPOs."""
427
428     synopsis = "%prog [options]"
429
430     takes_optiongroups = {
431         "sambaopts": options.SambaOptions,
432         "versionopts": options.VersionOptions,
433         "credopts": options.CredentialsOptions,
434     }
435
436     takes_options = [
437         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
438                metavar="URL", dest="H")
439     ]
440
441     def run(self, H=None, sambaopts=None, credopts=None, versionopts=None):
442
443         self.lp = sambaopts.get_loadparm()
444         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
445
446         self.url = dc_url(self.lp, self.creds, H)
447
448         self.samdb_connect()
449
450         msg = get_gpo_info(self.samdb, None)
451
452         for m in msg:
453             self.outf.write("GPO          : %s\n" % m['name'][0])
454             self.outf.write("display name : %s\n" % m['displayName'][0])
455             self.outf.write("path         : %s\n" % m['gPCFileSysPath'][0])
456             self.outf.write("dn           : %s\n" % m.dn)
457             self.outf.write("version      : %s\n" % attr_default(m, 'versionNumber', '0'))
458             self.outf.write("flags        : %s\n" % gpo_flags_string(int(attr_default(m, 'flags', 0))))
459             self.outf.write("\n")
460
461
462 class cmd_list(GPOCommand):
463     """List GPOs for an account."""
464
465     synopsis = "%prog <username> [options]"
466
467     takes_args = ['username']
468     takes_optiongroups = {
469         "sambaopts": options.SambaOptions,
470         "versionopts": options.VersionOptions,
471         "credopts": options.CredentialsOptions,
472     }
473
474     takes_options = [
475         Option("-H", "--URL", help="LDB URL for database or target server",
476                type=str, metavar="URL", dest="H")
477     ]
478
479     def run(self, username, H=None, sambaopts=None, credopts=None, versionopts=None):
480
481         self.lp = sambaopts.get_loadparm()
482         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
483
484         self.url = dc_url(self.lp, self.creds, H)
485
486         self.samdb_connect()
487
488         try:
489             msg = self.samdb.search(expression='(&(|(samAccountName=%s)(samAccountName=%s$))(objectClass=User))' %
490                                     (ldb.binary_encode(username), ldb.binary_encode(username)))
491             user_dn = msg[0].dn
492         except Exception:
493             raise CommandError("Failed to find account %s" % username)
494
495         # check if its a computer account
496         try:
497             msg = self.samdb.search(base=user_dn, scope=ldb.SCOPE_BASE, attrs=['objectClass'])[0]
498             is_computer = 'computer' in msg['objectClass']
499         except Exception:
500             raise CommandError("Failed to find objectClass for user %s" % username)
501
502         session_info_flags = (AUTH_SESSION_INFO_DEFAULT_GROUPS |
503                               AUTH_SESSION_INFO_AUTHENTICATED)
504
505         # When connecting to a remote server, don't look up the local privilege DB
506         if self.url is not None and self.url.startswith('ldap'):
507             session_info_flags |= AUTH_SESSION_INFO_SIMPLE_PRIVILEGES
508
509         session = samba.auth.user_session(self.samdb, lp_ctx=self.lp, dn=user_dn,
510                                           session_info_flags=session_info_flags)
511
512         token = session.security_token
513
514         gpos = []
515
516         inherit = True
517         dn = ldb.Dn(self.samdb, str(user_dn)).parent()
518         while True:
519             msg = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE, attrs=['gPLink', 'gPOptions'])[0]
520             if 'gPLink' in msg:
521                 glist = parse_gplink(str(msg['gPLink'][0]))
522                 for g in glist:
523                     if not inherit and not (g['options'] & dsdb.GPLINK_OPT_ENFORCE):
524                         continue
525                     if g['options'] & dsdb.GPLINK_OPT_DISABLE:
526                         continue
527
528                     try:
529                         sd_flags = (security.SECINFO_OWNER |
530                                     security.SECINFO_GROUP |
531                                     security.SECINFO_DACL)
532                         gmsg = self.samdb.search(base=g['dn'], scope=ldb.SCOPE_BASE,
533                                                  attrs=['name', 'displayName', 'flags',
534                                                         'nTSecurityDescriptor'],
535                                                  controls=['sd_flags:1:%d' % sd_flags])
536                         secdesc_ndr = gmsg[0]['nTSecurityDescriptor'][0]
537                         secdesc = ndr_unpack(security.descriptor, secdesc_ndr)
538                     except Exception:
539                         self.outf.write("Failed to fetch gpo object with nTSecurityDescriptor %s\n" %
540                                         g['dn'])
541                         continue
542
543                     try:
544                         samba.security.access_check(secdesc, token,
545                                                     security.SEC_STD_READ_CONTROL |
546                                                     security.SEC_ADS_LIST |
547                                                     security.SEC_ADS_READ_PROP)
548                     except RuntimeError:
549                         self.outf.write("Failed access check on %s\n" % msg.dn)
550                         continue
551
552                     # check the flags on the GPO
553                     flags = int(attr_default(gmsg[0], 'flags', 0))
554                     if is_computer and (flags & dsdb.GPO_FLAG_MACHINE_DISABLE):
555                         continue
556                     if not is_computer and (flags & dsdb.GPO_FLAG_USER_DISABLE):
557                         continue
558                     gpos.append((gmsg[0]['displayName'][0], gmsg[0]['name'][0]))
559
560             # check if this blocks inheritance
561             gpoptions = int(attr_default(msg, 'gPOptions', 0))
562             if gpoptions & dsdb.GPO_BLOCK_INHERITANCE:
563                 inherit = False
564
565             if dn == self.samdb.get_default_basedn():
566                 break
567             dn = dn.parent()
568
569         if is_computer:
570             msg_str = 'computer'
571         else:
572             msg_str = 'user'
573
574         self.outf.write("GPOs for %s %s\n" % (msg_str, username))
575         for g in gpos:
576             self.outf.write("    %s %s\n" % (g[0], g[1]))
577
578
579 class cmd_show(GPOCommand):
580     """Show information for a GPO."""
581
582     synopsis = "%prog <gpo> [options]"
583
584     takes_optiongroups = {
585         "sambaopts": options.SambaOptions,
586         "versionopts": options.VersionOptions,
587         "credopts": options.CredentialsOptions,
588     }
589
590     takes_args = ['gpo']
591
592     takes_options = [
593         Option("-H", help="LDB URL for database or target server", type=str)
594     ]
595
596     def run(self, gpo, H=None, sambaopts=None, credopts=None, versionopts=None):
597
598         self.lp = sambaopts.get_loadparm()
599         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
600
601         self.url = dc_url(self.lp, self.creds, H)
602
603         self.samdb_connect()
604
605         try:
606             msg = get_gpo_info(self.samdb, gpo)[0]
607         except Exception:
608             raise CommandError("GPO '%s' does not exist" % gpo)
609
610         try:
611             secdesc_ndr = msg['nTSecurityDescriptor'][0]
612             secdesc = ndr_unpack(security.descriptor, secdesc_ndr)
613             secdesc_sddl = secdesc.as_sddl()
614         except Exception:
615             secdesc_sddl = "<hidden>"
616
617         self.outf.write("GPO          : %s\n" % msg['name'][0])
618         self.outf.write("display name : %s\n" % msg['displayName'][0])
619         self.outf.write("path         : %s\n" % msg['gPCFileSysPath'][0])
620         self.outf.write("dn           : %s\n" % msg.dn)
621         self.outf.write("version      : %s\n" % attr_default(msg, 'versionNumber', '0'))
622         self.outf.write("flags        : %s\n" % gpo_flags_string(int(attr_default(msg, 'flags', 0))))
623         self.outf.write("ACL          : %s\n" % secdesc_sddl)
624         self.outf.write("\n")
625
626
627 class cmd_getlink(GPOCommand):
628     """List GPO Links for a container."""
629
630     synopsis = "%prog <container_dn> [options]"
631
632     takes_optiongroups = {
633         "sambaopts": options.SambaOptions,
634         "versionopts": options.VersionOptions,
635         "credopts": options.CredentialsOptions,
636     }
637
638     takes_args = ['container_dn']
639
640     takes_options = [
641         Option("-H", help="LDB URL for database or target server", type=str)
642     ]
643
644     def run(self, container_dn, H=None, sambaopts=None, credopts=None,
645             versionopts=None):
646
647         self.lp = sambaopts.get_loadparm()
648         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
649
650         self.url = dc_url(self.lp, self.creds, H)
651
652         self.samdb_connect()
653
654         try:
655             msg = self.samdb.search(base=container_dn, scope=ldb.SCOPE_BASE,
656                                     expression="(objectClass=*)",
657                                     attrs=['gPLink'])[0]
658         except Exception:
659             raise CommandError("Container '%s' does not exist" % container_dn)
660
661         if msg['gPLink']:
662             self.outf.write("GPO(s) linked to DN %s\n" % container_dn)
663             gplist = parse_gplink(str(msg['gPLink'][0]))
664             for g in gplist:
665                 msg = get_gpo_info(self.samdb, dn=g['dn'])
666                 self.outf.write("    GPO     : %s\n" % msg[0]['name'][0])
667                 self.outf.write("    Name    : %s\n" % msg[0]['displayName'][0])
668                 self.outf.write("    Options : %s\n" % gplink_options_string(g['options']))
669                 self.outf.write("\n")
670         else:
671             self.outf.write("No GPO(s) linked to DN=%s\n" % container_dn)
672
673
674 class cmd_setlink(GPOCommand):
675     """Add or update a GPO link to a container."""
676
677     synopsis = "%prog <container_dn> <gpo> [options]"
678
679     takes_optiongroups = {
680         "sambaopts": options.SambaOptions,
681         "versionopts": options.VersionOptions,
682         "credopts": options.CredentialsOptions,
683     }
684
685     takes_args = ['container_dn', 'gpo']
686
687     takes_options = [
688         Option("-H", help="LDB URL for database or target server", type=str),
689         Option("--disable", dest="disabled", default=False, action='store_true',
690                help="Disable policy"),
691         Option("--enforce", dest="enforced", default=False, action='store_true',
692                help="Enforce policy")
693     ]
694
695     def run(self, container_dn, gpo, H=None, disabled=False, enforced=False,
696             sambaopts=None, credopts=None, versionopts=None):
697
698         self.lp = sambaopts.get_loadparm()
699         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
700
701         self.url = dc_url(self.lp, self.creds, H)
702
703         self.samdb_connect()
704
705         gplink_options = 0
706         if disabled:
707             gplink_options |= dsdb.GPLINK_OPT_DISABLE
708         if enforced:
709             gplink_options |= dsdb.GPLINK_OPT_ENFORCE
710
711         # Check if valid GPO DN
712         try:
713             msg = get_gpo_info(self.samdb, gpo=gpo)[0]
714         except Exception:
715             raise CommandError("GPO '%s' does not exist" % gpo)
716         gpo_dn = str(get_gpo_dn(self.samdb, gpo))
717
718         # Check if valid Container DN
719         try:
720             msg = self.samdb.search(base=container_dn, scope=ldb.SCOPE_BASE,
721                                     expression="(objectClass=*)",
722                                     attrs=['gPLink'])[0]
723         except Exception:
724             raise CommandError("Container '%s' does not exist" % container_dn)
725
726         # Update existing GPlinks or Add new one
727         existing_gplink = False
728         if 'gPLink' in msg:
729             gplist = parse_gplink(str(msg['gPLink'][0]))
730             existing_gplink = True
731             found = False
732             for g in gplist:
733                 if g['dn'].lower() == gpo_dn.lower():
734                     g['options'] = gplink_options
735                     found = True
736                     break
737             if found:
738                 raise CommandError("GPO '%s' already linked to this container" % gpo)
739             else:
740                 gplist.insert(0, {'dn': gpo_dn, 'options': gplink_options})
741         else:
742             gplist = []
743             gplist.append({'dn': gpo_dn, 'options': gplink_options})
744
745         gplink_str = encode_gplink(gplist)
746
747         m = ldb.Message()
748         m.dn = ldb.Dn(self.samdb, container_dn)
749
750         if existing_gplink:
751             m['new_value'] = ldb.MessageElement(gplink_str, ldb.FLAG_MOD_REPLACE, 'gPLink')
752         else:
753             m['new_value'] = ldb.MessageElement(gplink_str, ldb.FLAG_MOD_ADD, 'gPLink')
754
755         try:
756             self.samdb.modify(m)
757         except Exception as e:
758             raise CommandError("Error adding GPO Link", e)
759
760         self.outf.write("Added/Updated GPO link\n")
761         cmd_getlink().run(container_dn, H, sambaopts, credopts, versionopts)
762
763
764 class cmd_dellink(GPOCommand):
765     """Delete GPO link from a container."""
766
767     synopsis = "%prog <container_dn> <gpo> [options]"
768
769     takes_optiongroups = {
770         "sambaopts": options.SambaOptions,
771         "versionopts": options.VersionOptions,
772         "credopts": options.CredentialsOptions,
773     }
774
775     takes_args = ['container', 'gpo']
776
777     takes_options = [
778         Option("-H", help="LDB URL for database or target server", type=str),
779     ]
780
781     def run(self, container, gpo, H=None, sambaopts=None, credopts=None,
782             versionopts=None):
783
784         self.lp = sambaopts.get_loadparm()
785         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
786
787         self.url = dc_url(self.lp, self.creds, H)
788
789         self.samdb_connect()
790
791         # Check if valid GPO
792         try:
793             get_gpo_info(self.samdb, gpo=gpo)[0]
794         except Exception:
795             raise CommandError("GPO '%s' does not exist" % gpo)
796
797         container_dn = ldb.Dn(self.samdb, container)
798         del_gpo_link(self.samdb, container_dn, gpo)
799         self.outf.write("Deleted GPO link.\n")
800         cmd_getlink().run(container_dn, H, sambaopts, credopts, versionopts)
801
802
803 class cmd_listcontainers(GPOCommand):
804     """List all linked containers for a GPO."""
805
806     synopsis = "%prog <gpo> [options]"
807
808     takes_optiongroups = {
809         "sambaopts": options.SambaOptions,
810         "versionopts": options.VersionOptions,
811         "credopts": options.CredentialsOptions,
812     }
813
814     takes_args = ['gpo']
815
816     takes_options = [
817         Option("-H", help="LDB URL for database or target server", type=str)
818     ]
819
820     def run(self, gpo, H=None, sambaopts=None, credopts=None,
821             versionopts=None):
822
823         self.lp = sambaopts.get_loadparm()
824         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
825
826         self.url = dc_url(self.lp, self.creds, H)
827
828         self.samdb_connect()
829
830         msg = get_gpo_containers(self.samdb, gpo)
831         if len(msg):
832             self.outf.write("Container(s) using GPO %s\n" % gpo)
833             for m in msg:
834                 self.outf.write("    DN: %s\n" % m['dn'])
835         else:
836             self.outf.write("No Containers using GPO %s\n" % gpo)
837
838
839 class cmd_getinheritance(GPOCommand):
840     """Get inheritance flag for a container."""
841
842     synopsis = "%prog <container_dn> [options]"
843
844     takes_optiongroups = {
845         "sambaopts": options.SambaOptions,
846         "versionopts": options.VersionOptions,
847         "credopts": options.CredentialsOptions,
848     }
849
850     takes_args = ['container_dn']
851
852     takes_options = [
853         Option("-H", help="LDB URL for database or target server", type=str)
854     ]
855
856     def run(self, container_dn, H=None, sambaopts=None, credopts=None,
857             versionopts=None):
858
859         self.lp = sambaopts.get_loadparm()
860         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
861
862         self.url = dc_url(self.lp, self.creds, H)
863
864         self.samdb_connect()
865
866         try:
867             msg = self.samdb.search(base=container_dn, scope=ldb.SCOPE_BASE,
868                                     expression="(objectClass=*)",
869                                     attrs=['gPOptions'])[0]
870         except Exception:
871             raise CommandError("Container '%s' does not exist" % container_dn)
872
873         inheritance = 0
874         if 'gPOptions' in msg:
875             inheritance = int(msg['gPOptions'][0])
876
877         if inheritance == dsdb.GPO_BLOCK_INHERITANCE:
878             self.outf.write("Container has GPO_BLOCK_INHERITANCE\n")
879         else:
880             self.outf.write("Container has GPO_INHERIT\n")
881
882
883 class cmd_setinheritance(GPOCommand):
884     """Set inheritance flag on a container."""
885
886     synopsis = "%prog <container_dn> <block|inherit> [options]"
887
888     takes_optiongroups = {
889         "sambaopts": options.SambaOptions,
890         "versionopts": options.VersionOptions,
891         "credopts": options.CredentialsOptions,
892     }
893
894     takes_args = ['container_dn', 'inherit_state']
895
896     takes_options = [
897         Option("-H", help="LDB URL for database or target server", type=str)
898     ]
899
900     def run(self, container_dn, inherit_state, H=None, sambaopts=None, credopts=None,
901             versionopts=None):
902
903         if inherit_state.lower() == 'block':
904             inheritance = dsdb.GPO_BLOCK_INHERITANCE
905         elif inherit_state.lower() == 'inherit':
906             inheritance = dsdb.GPO_INHERIT
907         else:
908             raise CommandError("Unknown inheritance state (%s)" % inherit_state)
909
910         self.lp = sambaopts.get_loadparm()
911         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
912
913         self.url = dc_url(self.lp, self.creds, H)
914
915         self.samdb_connect()
916         try:
917             msg = self.samdb.search(base=container_dn, scope=ldb.SCOPE_BASE,
918                                     expression="(objectClass=*)",
919                                     attrs=['gPOptions'])[0]
920         except Exception:
921             raise CommandError("Container '%s' does not exist" % container_dn)
922
923         m = ldb.Message()
924         m.dn = ldb.Dn(self.samdb, container_dn)
925
926         if 'gPOptions' in msg:
927             m['new_value'] = ldb.MessageElement(str(inheritance), ldb.FLAG_MOD_REPLACE, 'gPOptions')
928         else:
929             m['new_value'] = ldb.MessageElement(str(inheritance), ldb.FLAG_MOD_ADD, 'gPOptions')
930
931         try:
932             self.samdb.modify(m)
933         except Exception as e:
934             raise CommandError("Error setting inheritance state %s" % inherit_state, e)
935
936
937 class cmd_fetch(GPOCommand):
938     """Download a GPO."""
939
940     synopsis = "%prog <gpo> [options]"
941
942     takes_optiongroups = {
943         "sambaopts": options.SambaOptions,
944         "versionopts": options.VersionOptions,
945         "credopts": options.CredentialsOptions,
946     }
947
948     takes_args = ['gpo']
949
950     takes_options = [
951         Option("-H", help="LDB URL for database or target server", type=str),
952         Option("--tmpdir", help="Temporary directory for copying policy files", type=str)
953     ]
954
955     def run(self, gpo, H=None, tmpdir=None, sambaopts=None, credopts=None, versionopts=None):
956
957         self.lp = sambaopts.get_loadparm()
958         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
959
960         # We need to know writable DC to setup SMB connection
961         if H and H.startswith('ldap://'):
962             dc_hostname = H[7:]
963             self.url = H
964         else:
965             dc_hostname = netcmd_finddc(self.lp, self.creds)
966             self.url = dc_url(self.lp, self.creds, dc=dc_hostname)
967
968         self.samdb_connect()
969         try:
970             msg = get_gpo_info(self.samdb, gpo)[0]
971         except Exception:
972             raise CommandError("GPO '%s' does not exist" % gpo)
973
974         # verify UNC path
975         unc = str(msg['gPCFileSysPath'][0])
976         try:
977             [dom_name, service, sharepath] = parse_unc(unc)
978         except ValueError:
979             raise CommandError("Invalid GPO path (%s)" % unc)
980
981         # SMB connect to DC
982         conn = smb_connection(dc_hostname, service, lp=self.lp,
983                               creds=self.creds, sign=True)
984
985         # Copy GPT
986         tmpdir, gpodir = self.construct_tmpdir(tmpdir, gpo)
987         try:
988             copy_directory_remote_to_local(conn, sharepath, gpodir)
989         except Exception as e:
990             # FIXME: Catch more specific exception
991             raise CommandError("Error copying GPO from DC", e)
992         self.outf.write('GPO copied to %s\n' % gpodir)
993
994
995 class cmd_backup(GPOCommand):
996     """Backup a GPO."""
997
998     synopsis = "%prog <gpo> [options]"
999
1000     takes_optiongroups = {
1001         "sambaopts": options.SambaOptions,
1002         "versionopts": options.VersionOptions,
1003         "credopts": options.CredentialsOptions,
1004     }
1005
1006     takes_args = ['gpo']
1007
1008     takes_options = [
1009         Option("-H", help="LDB URL for database or target server", type=str),
1010         Option("--tmpdir", help="Temporary directory for copying policy files", type=str),
1011         Option("--generalize", help="Generalize XML entities to restore",
1012                default=False, action='store_true'),
1013         Option("--entities", help="File to export defining XML entities for the restore",
1014                dest='ent_file', type=str)
1015     ]
1016
1017     def run(self, gpo, H=None, tmpdir=None, generalize=False, sambaopts=None,
1018             credopts=None, versionopts=None, ent_file=None):
1019
1020         self.lp = sambaopts.get_loadparm()
1021         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
1022
1023         # We need to know writable DC to setup SMB connection
1024         if H and H.startswith('ldap://'):
1025             dc_hostname = H[7:]
1026             self.url = H
1027         else:
1028             dc_hostname = netcmd_finddc(self.lp, self.creds)
1029             self.url = dc_url(self.lp, self.creds, dc=dc_hostname)
1030
1031         self.samdb_connect()
1032         try:
1033             msg = get_gpo_info(self.samdb, gpo)[0]
1034         except Exception:
1035             raise CommandError("GPO '%s' does not exist" % gpo)
1036
1037         # verify UNC path
1038         unc = str(msg['gPCFileSysPath'][0])
1039         try:
1040             [dom_name, service, sharepath] = parse_unc(unc)
1041         except ValueError:
1042             raise CommandError("Invalid GPO path (%s)" % unc)
1043
1044         # SMB connect to DC
1045         conn = smb_connection(dc_hostname, service, lp=self.lp,
1046                               creds=self.creds)
1047
1048         # Copy GPT
1049         tmpdir, gpodir = self.construct_tmpdir(tmpdir, gpo)
1050
1051         try:
1052             backup_directory_remote_to_local(conn, sharepath, gpodir)
1053         except Exception as e:
1054             # FIXME: Catch more specific exception
1055             raise CommandError("Error copying GPO from DC", e)
1056
1057         self.outf.write('GPO copied to %s\n' % gpodir)
1058
1059         if generalize:
1060             self.outf.write('\nAttempting to generalize XML entities:\n')
1061             entities = cmd_backup.generalize_xml_entities(self.outf, gpodir,
1062                                                           gpodir)
1063             import operator
1064             ents = ''
1065             for ent in sorted(entities.items(), key=operator.itemgetter(1)):
1066                 ents += '<!ENTITY {} "{}">\n'.format(ent[1].strip('&;'), ent[0])
1067
1068             if ent_file:
1069                 with open(ent_file, 'w') as f:
1070                     f.write(ents)
1071                 self.outf.write('Entities successfully written to %s\n' %
1072                                 ent_file)
1073             else:
1074                 self.outf.write('\nEntities:\n')
1075                 self.outf.write(ents)
1076
1077     @staticmethod
1078     def generalize_xml_entities(outf, sourcedir, targetdir):
1079         entities = {}
1080
1081         if not os.path.exists(targetdir):
1082             os.mkdir(targetdir)
1083
1084         l_dirs = [ sourcedir ]
1085         r_dirs = [ targetdir ]
1086         while l_dirs:
1087             l_dir = l_dirs.pop()
1088             r_dir = r_dirs.pop()
1089
1090             dirlist = os.listdir(l_dir)
1091             dirlist.sort()
1092             for e in dirlist:
1093                 l_name = os.path.join(l_dir, e)
1094                 r_name = os.path.join(r_dir, e)
1095
1096                 if os.path.isdir(l_name):
1097                     l_dirs.append(l_name)
1098                     r_dirs.append(r_name)
1099                     if not os.path.exists(r_name):
1100                         os.mkdir(r_name)
1101                 else:
1102                     if l_name.endswith('.xml'):
1103                         # Restore the xml file if possible
1104
1105                         # Get the filename to find the parser
1106                         to_parse = os.path.basename(l_name)[:-4]
1107
1108                         parser = find_parser(to_parse)
1109                         try:
1110                             with open(l_name, 'r') as ltemp:
1111                                 data = ltemp.read()
1112
1113                             concrete_xml = ET.fromstring(data)
1114                             found_entities = parser.generalize_xml(concrete_xml, r_name, entities)
1115                         except GPGeneralizeException:
1116                             outf.write('SKIPPING: Generalizing failed for %s\n' % to_parse)
1117
1118                     else:
1119                         # No need to generalize non-xml files.
1120                         #
1121                         # TODO This could be improved with xml files stored in
1122                         # the renamed backup file (with custom extension) by
1123                         # inlining them into the exported backups.
1124                         if not os.path.samefile(l_name, r_name):
1125                             shutil.copy2(l_name, r_name)
1126
1127         return entities
1128
1129
1130 class cmd_create(GPOCommand):
1131     """Create an empty GPO."""
1132
1133     synopsis = "%prog <displayname> [options]"
1134
1135     takes_optiongroups = {
1136         "sambaopts": options.SambaOptions,
1137         "versionopts": options.VersionOptions,
1138         "credopts": options.CredentialsOptions,
1139     }
1140
1141     takes_args = ['displayname']
1142
1143     takes_options = [
1144         Option("-H", help="LDB URL for database or target server", type=str),
1145         Option("--tmpdir", help="Temporary directory for copying policy files", type=str)
1146     ]
1147
1148     def run(self, displayname, H=None, tmpdir=None, sambaopts=None, credopts=None,
1149             versionopts=None):
1150
1151         self.lp = sambaopts.get_loadparm()
1152         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
1153
1154         net = Net(creds=self.creds, lp=self.lp)
1155
1156         # We need to know writable DC to setup SMB connection
1157         if H and H.startswith('ldap://'):
1158             dc_hostname = H[7:]
1159             self.url = H
1160             flags = (nbt.NBT_SERVER_LDAP |
1161                      nbt.NBT_SERVER_DS |
1162                      nbt.NBT_SERVER_WRITABLE)
1163             cldap_ret = net.finddc(address=dc_hostname, flags=flags)
1164         else:
1165             flags = (nbt.NBT_SERVER_LDAP |
1166                      nbt.NBT_SERVER_DS |
1167                      nbt.NBT_SERVER_WRITABLE)
1168             cldap_ret = net.finddc(domain=self.lp.get('realm'), flags=flags)
1169             dc_hostname = cldap_ret.pdc_dns_name
1170             self.url = dc_url(self.lp, self.creds, dc=dc_hostname)
1171
1172         self.samdb_connect()
1173
1174         msg = get_gpo_info(self.samdb, displayname=displayname)
1175         if msg.count > 0:
1176             raise CommandError("A GPO already existing with name '%s'" % displayname)
1177
1178         # Create new GUID
1179         guid  = str(uuid.uuid4())
1180         gpo = "{%s}" % guid.upper()
1181
1182         self.gpo_name = gpo
1183
1184         realm = cldap_ret.dns_domain
1185         unc_path = "\\\\%s\\sysvol\\%s\\Policies\\%s" % (realm, realm, gpo)
1186
1187         # Create GPT
1188         self.tmpdir, gpodir = self.construct_tmpdir(tmpdir, gpo)
1189         self.gpodir = gpodir
1190
1191         try:
1192             os.mkdir(os.path.join(gpodir, "Machine"))
1193             os.mkdir(os.path.join(gpodir, "User"))
1194             gpt_contents = "[General]\r\nVersion=0\r\n"
1195             open(os.path.join(gpodir, "GPT.INI"), "w").write(gpt_contents)
1196         except Exception as e:
1197             raise CommandError("Error Creating GPO files", e)
1198
1199         # Connect to DC over SMB
1200         [dom_name, service, sharepath] = parse_unc(unc_path)
1201         self.sharepath = sharepath
1202         conn = smb_connection(dc_hostname, service, lp=self.lp,
1203                               creds=self.creds)
1204
1205         self.conn = conn
1206
1207         self.samdb.transaction_start()
1208         try:
1209             # Add cn=<guid>
1210             gpo_dn = get_gpo_dn(self.samdb, gpo)
1211
1212             m = ldb.Message()
1213             m.dn = gpo_dn
1214             m['a01'] = ldb.MessageElement("groupPolicyContainer", ldb.FLAG_MOD_ADD, "objectClass")
1215             self.samdb.add(m)
1216
1217             # Add cn=User,cn=<guid>
1218             m = ldb.Message()
1219             m.dn = ldb.Dn(self.samdb, "CN=User,%s" % str(gpo_dn))
1220             m['a01'] = ldb.MessageElement("container", ldb.FLAG_MOD_ADD, "objectClass")
1221             self.samdb.add(m)
1222
1223             # Add cn=Machine,cn=<guid>
1224             m = ldb.Message()
1225             m.dn = ldb.Dn(self.samdb, "CN=Machine,%s" % str(gpo_dn))
1226             m['a01'] = ldb.MessageElement("container", ldb.FLAG_MOD_ADD, "objectClass")
1227             self.samdb.add(m)
1228
1229             # Get new security descriptor
1230             ds_sd_flags = (security.SECINFO_OWNER |
1231                            security.SECINFO_GROUP |
1232                            security.SECINFO_DACL)
1233             msg = get_gpo_info(self.samdb, gpo=gpo, sd_flags=ds_sd_flags)[0]
1234             ds_sd_ndr = msg['nTSecurityDescriptor'][0]
1235             ds_sd = ndr_unpack(security.descriptor, ds_sd_ndr).as_sddl()
1236
1237             # Create a file system security descriptor
1238             domain_sid = security.dom_sid(self.samdb.get_domain_sid())
1239             fs_sd = dsacl2fsacl(ds_sd, domain_sid, as_sddl=False)
1240             fs_sd.type = security.SEC_DESC_SELF_RELATIVE
1241             fs_sd.type |= security.SEC_DESC_DACL_PROTECTED
1242             fs_sd.type |= security.SEC_DESC_DACL_AUTO_INHERITED
1243             fs_sd.type |= security.SEC_DESC_DACL_AUTO_INHERIT_REQ
1244             fs_sd.type |= security.SEC_DESC_SACL_AUTO_INHERITED
1245
1246             # Copy GPO directory
1247             create_directory_hier(conn, sharepath)
1248
1249             # Set ACL
1250             sio = (security.SECINFO_OWNER |
1251                    security.SECINFO_GROUP |
1252                    security.SECINFO_DACL |
1253                    security.SECINFO_PROTECTED_DACL)
1254             conn.set_acl(sharepath, fs_sd, sio)
1255
1256             # Copy GPO files over SMB
1257             copy_directory_local_to_remote(conn, gpodir, sharepath)
1258
1259             m = ldb.Message()
1260             m.dn = gpo_dn
1261             m['a02'] = ldb.MessageElement(displayname, ldb.FLAG_MOD_REPLACE, "displayName")
1262             m['a03'] = ldb.MessageElement(unc_path, ldb.FLAG_MOD_REPLACE, "gPCFileSysPath")
1263             m['a05'] = ldb.MessageElement("0", ldb.FLAG_MOD_REPLACE, "versionNumber")
1264             m['a07'] = ldb.MessageElement("2", ldb.FLAG_MOD_REPLACE, "gpcFunctionalityVersion")
1265             m['a04'] = ldb.MessageElement("0", ldb.FLAG_MOD_REPLACE, "flags")
1266             controls = ["permissive_modify:0"]
1267             self.samdb.modify(m, controls=controls)
1268         except Exception:
1269             self.samdb.transaction_cancel()
1270             raise
1271         else:
1272             self.samdb.transaction_commit()
1273
1274         self.outf.write("GPO '%s' created as %s\n" % (displayname, gpo))
1275
1276
1277 class cmd_restore(cmd_create):
1278     """Restore a GPO to a new container."""
1279
1280     synopsis = "%prog <displayname> <backup location> [options]"
1281
1282     takes_optiongroups = {
1283         "sambaopts": options.SambaOptions,
1284         "versionopts": options.VersionOptions,
1285         "credopts": options.CredentialsOptions,
1286     }
1287
1288     takes_args = ['displayname', 'backup']
1289
1290     takes_options = [
1291         Option("-H", help="LDB URL for database or target server", type=str),
1292         Option("--tmpdir", help="Temporary directory for copying policy files", type=str),
1293         Option("--entities", help="File defining XML entities to insert into DOCTYPE header", type=str)
1294     ]
1295
1296     def restore_from_backup_to_local_dir(self, sourcedir, targetdir, dtd_header=''):
1297         SUFFIX = '.SAMBABACKUP'
1298
1299         if not os.path.exists(targetdir):
1300             os.mkdir(targetdir)
1301
1302         l_dirs = [ sourcedir ]
1303         r_dirs = [ targetdir ]
1304         while l_dirs:
1305             l_dir = l_dirs.pop()
1306             r_dir = r_dirs.pop()
1307
1308             dirlist = os.listdir(l_dir)
1309             dirlist.sort()
1310             for e in dirlist:
1311                 l_name = os.path.join(l_dir, e)
1312                 r_name = os.path.join(r_dir, e)
1313
1314                 if os.path.isdir(l_name):
1315                     l_dirs.append(l_name)
1316                     r_dirs.append(r_name)
1317                     if not os.path.exists(r_name):
1318                         os.mkdir(r_name)
1319                 else:
1320                     if l_name.endswith('.xml'):
1321                         # Restore the xml file if possible
1322
1323                         # Get the filename to find the parser
1324                         to_parse = os.path.basename(l_name)[:-4]
1325
1326                         parser = find_parser(to_parse)
1327                         try:
1328                             with open(l_name, 'r') as ltemp:
1329                                 data = ltemp.read()
1330                                 xml_head = '<?xml version="1.0" encoding="utf-8"?>'
1331
1332                                 if data.startswith(xml_head):
1333                                     # It appears that sometimes the DTD rejects
1334                                     # the xml header being after it.
1335                                     data = data[len(xml_head):]
1336
1337                                     # Load the XML file with the DTD (entity) header
1338                                     parser.load_xml(ET.fromstring(xml_head + dtd_header + data))
1339                                 else:
1340                                     parser.load_xml(ET.fromstring(dtd_header + data))
1341
1342                                 # Write out the substituted files in the output
1343                                 # location, ready to copy over.
1344                                 parser.write_binary(r_name[:-4])
1345
1346                         except GPNoParserException:
1347                             # In the failure case, we fallback
1348                             original_file = l_name[:-4] + SUFFIX
1349                             shutil.copy2(original_file, r_name[:-4])
1350
1351                             self.outf.write('WARNING: No such parser for %s\n' % to_parse)
1352                             self.outf.write('WARNING: Falling back to simple copy-restore.\n')
1353                         except:
1354                             import traceback
1355                             traceback.print_exc()
1356
1357                             # In the failure case, we fallback
1358                             original_file = l_name[:-4] + SUFFIX
1359                             shutil.copy2(original_file, r_name[:-4])
1360
1361                             self.outf.write('WARNING: Error during parsing for %s\n' % l_name)
1362                             self.outf.write('WARNING: Falling back to simple copy-restore.\n')
1363
1364     def run(self, displayname, backup, H=None, tmpdir=None, entities=None, sambaopts=None, credopts=None,
1365             versionopts=None):
1366
1367         dtd_header = ''
1368
1369         if not os.path.exists(backup):
1370             raise CommandError("Backup directory does not exist %s" % backup)
1371
1372         if entities is not None:
1373             # DOCTYPE name is meant to match root element, but ElementTree does
1374             # not seem to care, so this seems to be enough.
1375
1376             dtd_header = '<!DOCTYPE foobar [\n'
1377
1378             if not os.path.exists(entities):
1379                 raise CommandError("Entities file does not exist %s" %
1380                                    entities)
1381             with open(entities, 'r') as entities_file:
1382                 entities_content = entities_file.read()
1383
1384                 # Do a basic regex test of the entities file format
1385                 if re.match('(\s*<!ENTITY\s*[a-zA-Z0-9_]+\s*.*?>)+\s*\Z',
1386                             entities_content, flags=re.MULTILINE) is None:
1387                     raise CommandError("Entities file does not appear to "
1388                                        "conform to format\n"
1389                                        'e.g. <!ENTITY entity "value">')
1390                 dtd_header += entities_content.strip()
1391
1392             dtd_header += '\n]>\n'
1393
1394         super(cmd_restore, self).run(displayname, H, tmpdir, sambaopts,
1395                                      credopts, versionopts)
1396
1397         try:
1398             # Iterate over backup files and restore with DTD
1399             self.restore_from_backup_to_local_dir(backup, self.gpodir,
1400                                                   dtd_header)
1401
1402             # Copy GPO files over SMB
1403             copy_directory_local_to_remote(self.conn, self.gpodir,
1404                                            self.sharepath,
1405                                            ignore_existing=True)
1406
1407         except Exception as e:
1408             import traceback
1409             traceback.print_exc()
1410             self.outf.write(str(e) + '\n')
1411
1412             self.outf.write("Failed to restore GPO -- deleting...\n")
1413             cmd = cmd_del()
1414             cmd.run(self.gpo_name, H, sambaopts, credopts, versionopts)
1415
1416             raise CommandError("Failed to restore: %s" % e)
1417
1418
1419 class cmd_del(GPOCommand):
1420     """Delete a GPO."""
1421
1422     synopsis = "%prog <gpo> [options]"
1423
1424     takes_optiongroups = {
1425         "sambaopts": options.SambaOptions,
1426         "versionopts": options.VersionOptions,
1427         "credopts": options.CredentialsOptions,
1428     }
1429
1430     takes_args = ['gpo']
1431
1432     takes_options = [
1433         Option("-H", help="LDB URL for database or target server", type=str),
1434     ]
1435
1436     def run(self, gpo, H=None, sambaopts=None, credopts=None,
1437             versionopts=None):
1438
1439         self.lp = sambaopts.get_loadparm()
1440         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
1441
1442         # We need to know writable DC to setup SMB connection
1443         if H and H.startswith('ldap://'):
1444             dc_hostname = H[7:]
1445             self.url = H
1446         else:
1447             dc_hostname = netcmd_finddc(self.lp, self.creds)
1448             self.url = dc_url(self.lp, self.creds, dc=dc_hostname)
1449
1450         self.samdb_connect()
1451
1452         # Check if valid GPO
1453         try:
1454             msg = get_gpo_info(self.samdb, gpo=gpo)[0]
1455             unc_path = str(msg['gPCFileSysPath'][0])
1456         except Exception:
1457             raise CommandError("GPO '%s' does not exist" % gpo)
1458
1459         # Connect to DC over SMB
1460         [dom_name, service, sharepath] = parse_unc(unc_path)
1461         conn = smb_connection(dc_hostname, service, lp=self.lp,
1462                               creds=self.creds)
1463
1464         self.samdb.transaction_start()
1465         try:
1466             # Check for existing links
1467             msg = get_gpo_containers(self.samdb, gpo)
1468
1469             if len(msg):
1470                 self.outf.write("GPO %s is linked to containers\n" % gpo)
1471                 for m in msg:
1472                     del_gpo_link(self.samdb, m['dn'], gpo)
1473                     self.outf.write("    Removed link from %s.\n" % m['dn'])
1474
1475             # Remove LDAP entries
1476             gpo_dn = get_gpo_dn(self.samdb, gpo)
1477             self.samdb.delete(ldb.Dn(self.samdb, "CN=User,%s" % str(gpo_dn)))
1478             self.samdb.delete(ldb.Dn(self.samdb, "CN=Machine,%s" % str(gpo_dn)))
1479             self.samdb.delete(gpo_dn)
1480
1481             # Remove GPO files
1482             conn.deltree(sharepath)
1483
1484         except Exception:
1485             self.samdb.transaction_cancel()
1486             raise
1487         else:
1488             self.samdb.transaction_commit()
1489
1490         self.outf.write("GPO %s deleted.\n" % gpo)
1491
1492
1493 class cmd_aclcheck(GPOCommand):
1494     """Check all GPOs have matching LDAP and DS ACLs."""
1495
1496     synopsis = "%prog [options]"
1497
1498     takes_optiongroups = {
1499         "sambaopts": options.SambaOptions,
1500         "versionopts": options.VersionOptions,
1501         "credopts": options.CredentialsOptions,
1502     }
1503
1504     takes_options = [
1505         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
1506                metavar="URL", dest="H")
1507     ]
1508
1509     def run(self, H=None, sambaopts=None, credopts=None, versionopts=None):
1510
1511         self.lp = sambaopts.get_loadparm()
1512         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
1513
1514         self.url = dc_url(self.lp, self.creds, H)
1515
1516         # We need to know writable DC to setup SMB connection
1517         if H and H.startswith('ldap://'):
1518             dc_hostname = H[7:]
1519             self.url = H
1520         else:
1521             dc_hostname = netcmd_finddc(self.lp, self.creds)
1522             self.url = dc_url(self.lp, self.creds, dc=dc_hostname)
1523
1524         self.samdb_connect()
1525
1526         msg = get_gpo_info(self.samdb, None)
1527
1528         for m in msg:
1529             # verify UNC path
1530             try:
1531                 unc = str(m['gPCFileSysPath'][0])
1532             except Exception:
1533                 continue
1534
1535             try:
1536                 [dom_name, service, sharepath] = parse_unc(unc)
1537             except ValueError:
1538                 raise CommandError("Invalid GPO path (%s)" % unc)
1539
1540             # SMB connect to DC
1541             conn = smb_connection(dc_hostname, service, lp=self.lp,
1542                                   creds=self.creds)
1543
1544             try:
1545                 fs_sd = conn.get_acl(sharepath, security.SECINFO_OWNER | security.SECINFO_GROUP | security.SECINFO_DACL, security.SEC_FLAG_MAXIMUM_ALLOWED)
1546             except Exception:
1547                 raise CommandError("Failed to get security_descriptor of '%s' using SMB" % sharepath)
1548
1549             if 'nTSecurityDescriptor' not in m:
1550                 raise CommandError("Could not read nTSecurityDescriptor. "
1551                                    "This requires an Administrator account")
1552
1553             ds_sd_ndr = m['nTSecurityDescriptor'][0]
1554             ds_sd = ndr_unpack(security.descriptor, ds_sd_ndr).as_sddl()
1555
1556             domain_sid = security.dom_sid(self.samdb.get_domain_sid())
1557             name = m['name'][0]
1558             if DEFAULT_POLICY_GUID in name or DEFAULT_DC_POLICY_GUID in name:
1559                 expected_fs_sd = security.descriptor.from_sddl(SYSVOL_SUBFOLDER_SD, domain_sid)
1560                 expected_fs_sd.sacl = None
1561                 expected_fs_sddl = expected_fs_sd.as_sddl(domain_sid)
1562             else:
1563                 ds_sd_ndr = m['nTSecurityDescriptor'][0]
1564                 ds_sd = ndr_unpack(security.descriptor, ds_sd_ndr).as_sddl()
1565
1566                 # Create a file system security descriptor
1567                 expected_fs_sddl = dsacl2fsacl(ds_sd, domain_sid)
1568
1569             if (fs_sd.as_sddl(domain_sid) != expected_fs_sddl):
1570                 raise CommandError("Invalid GPO ACL %s on path (%s), should be %s" % (fs_sd.as_sddl(domain_sid), sharepath, expected_fs_sddl))
1571
1572
1573 class cmd_gpo(SuperCommand):
1574     """Group Policy Object (GPO) management."""
1575
1576     subcommands = {}
1577     subcommands["listall"] = cmd_listall()
1578     subcommands["list"] = cmd_list()
1579     subcommands["show"] = cmd_show()
1580     subcommands["getlink"] = cmd_getlink()
1581     subcommands["setlink"] = cmd_setlink()
1582     subcommands["dellink"] = cmd_dellink()
1583     subcommands["listcontainers"] = cmd_listcontainers()
1584     subcommands["getinheritance"] = cmd_getinheritance()
1585     subcommands["setinheritance"] = cmd_setinheritance()
1586     subcommands["fetch"] = cmd_fetch()
1587     subcommands["create"] = cmd_create()
1588     subcommands["del"] = cmd_del()
1589     subcommands["aclcheck"] = cmd_aclcheck()
1590     subcommands["backup"] = cmd_backup()
1591     subcommands["restore"] = cmd_restore()