samba-tool: Reimplement GPO functions in python
[metze/samba/wip.git] / source4 / scripting / python / samba / netcmd / gpo.py
1 #!/usr/bin/env python
2 #
3 # implement samba_tool gpo commands
4 #
5 # Copyright Andrew Tridgell 2010
6 # Copyright Giampaolo Lauria 2011 <lauria2@yahoo.com>
7 #
8 # based on C implementation by Guenther Deschner and Wilco Baan Hofman
9 #
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
22 #
23
24 import samba.getopt as options
25 import ldb
26
27 from samba.auth import system_session
28 from samba.netcmd import (
29     Command,
30     CommandError,
31     Option,
32     SuperCommand,
33     )
34 from samba.samdb import SamDB
35 from samba import drs_utils, nttime2string, dsdb, dcerpc
36 from samba.dcerpc import misc
37 from samba.ndr import ndr_unpack
38 import samba.security
39 import samba.auth
40 from samba.auth import AUTH_SESSION_INFO_DEFAULT_GROUPS, AUTH_SESSION_INFO_AUTHENTICATED, AUTH_SESSION_INFO_SIMPLE_PRIVILEGES
41
42 def samdb_connect(ctx):
43     '''make a ldap connection to the server'''
44     try:
45         ctx.samdb = SamDB(url=ctx.url,
46                           session_info=system_session(),
47                           credentials=ctx.creds, lp=ctx.lp)
48     except Exception, e:
49         raise CommandError("LDAP connection to %s failed " % ctx.url, e)
50
51
52 def attr_default(msg, attrname, default):
53     '''get an attribute from a ldap msg with a default'''
54     if attrname in msg:
55         return msg[attrname][0]
56     return default
57
58
59 def flags_string(flags, value):
60     '''return a set of flags as a string'''
61     if value == 0:
62         return 'NONE'
63     ret = ''
64     for (str, val) in flags:
65         if val & value:
66             ret += str + ' '
67             value &= ~val
68     if value != 0:
69         ret += '0x%08x' % value
70     return ret.rstrip()
71
72
73 def parse_gplink(gplink):
74     '''parse a gPLink into an array of dn and options'''
75     ret = []
76     a = gplink.split(']')
77     for g in a:
78         if not g:
79             continue
80         d = g.split(';')
81         if len(d) != 2 or not d[0].startswith("[LDAP://"):
82             raise RuntimeError("Badly formed gPLink '%s'" % g)
83         ret.append({ 'dn' : d[0][8:], 'options' : int(d[1])})
84     return ret
85
86
87 def encode_gplink(gplist):
88     '''Encode an array of dn and options into gPLink string'''
89     ret = ''
90     for g in gplist:
91         ret += "[LDAP://%s;%d]" % (g['dn'], g['options'])
92     return ret
93
94
95 class cmd_listall(Command):
96     """list all GPOs"""
97
98     synopsis = "%prog gpo listall"
99
100     takes_options = [
101         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
102                metavar="URL", dest="H")
103         ]
104
105     def run(self, H=None, sambaopts=None, credopts=None, versionopts=None):
106
107         self.url = H
108         self.lp = sambaopts.get_loadparm()
109
110         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
111
112         samdb_connect(self)
113
114         policies_dn = self.samdb.get_default_basedn()
115         policies_dn.add_child(ldb.Dn(self.samdb, "CN=Policies,CN=System"))
116
117         gpo_flags = [
118             ("GPO_FLAG_USER_DISABLE", dsdb.GPO_FLAG_USER_DISABLE ),
119             ( "GPO_FLAG_MACHINE_DISABLE", dsdb.GPO_FLAG_MACHINE_DISABLE ) ]
120
121         try:
122             msg = self.samdb.search(base=policies_dn, scope=ldb.SCOPE_ONELEVEL,
123                                     expression="(objectClass=groupPolicyContainer)",
124                                     attrs=['nTSecurityDescriptor',
125                                             'versionNumber',
126                                             'flags',
127                                             'name',
128                                             'displayName',
129                                             'gPCFileSysPath'])
130         except Exception, e:
131             raise CommandError("Failed to list policies in %s" % policies_dn, e)
132         for m in msg:
133             print("GPO          : %s" % m['name'][0])
134             print("display name : %s" % m['displayName'][0])
135             print("path         : %s" % m['gPCFileSysPath'][0])
136             print("dn           : %s" % m.dn)
137             print("version      : %s" % attr_default(m, 'versionNumber', '0'))
138             print("flags        : %s" % flags_string(gpo_flags, int(attr_default(m, 'flags', 0))))
139             print("")
140
141
142 class cmd_list(Command):
143     """list GPOs for an account"""
144
145     synopsis = "%prog gpo list <username>"
146
147     takes_args = [ 'username' ]
148
149     takes_options = [
150         Option("-H", "--URL", help="LDB URL for database or target server", type=str,
151                metavar="URL", dest="H")
152         ]
153
154     def run(self, username, H=None, sambaopts=None, credopts=None, versionopts=None):
155
156         self.url = H
157         self.lp = sambaopts.get_loadparm()
158
159         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
160
161         samdb_connect(self)
162
163         try:
164             msg = self.samdb.search(expression='(&(|(samAccountName=%s)(samAccountName=%s$))(objectClass=User))' %
165                                                 (username,username))
166             user_dn = msg[0].dn
167         except Exception, e:
168             raise CommandError("Failed to find account %s" % username, e)
169
170         # check if its a computer account
171         try:
172             msg = self.samdb.search(base=user_dn, scope=ldb.SCOPE_BASE, attrs=['objectClass'])[0]
173             is_computer = 'computer' in msg['objectClass']
174         except Exception, e:
175             raise CommandError("Failed to find objectClass for user %s" % username, e)
176
177         session_info_flags = ( AUTH_SESSION_INFO_DEFAULT_GROUPS |
178                                AUTH_SESSION_INFO_AUTHENTICATED )
179
180         # When connecting to a remote server, don't look up the local privilege DB
181         if self.url is not None and self.url.startswith('ldap'):
182             session_info_flags |= AUTH_SESSION_INFO_SIMPLE_PRIVILEGES
183
184         session = samba.auth.user_session(self.samdb, lp_ctx=self.lp, dn=user_dn,
185                                           session_info_flags=session_info_flags)
186
187         token = session.security_token
188
189         gpos = []
190
191         inherit = True
192         dn = ldb.Dn(self.samdb, str(user_dn)).parent()
193         while True:
194             msg = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE, attrs=['gPLink', 'gPOptions'])[0]
195             if 'gPLink' in msg:
196                 glist = parse_gplink(msg['gPLink'][0])
197                 for g in glist:
198                     if not inherit and not (g['options'] & dsdb.GPLINK_OPT_ENFORCE):
199                         continue
200                     if g['options'] & dsdb.GPLINK_OPT_DISABLE:
201                         continue
202
203                     try:
204                         gmsg = self.samdb.search(base=g['dn'], scope=ldb.SCOPE_BASE,
205                                                  attrs=['flags', 'ntSecurityDescriptor'])
206                     except Exception:
207                         print "Failed to fetch gpo object %s" % g['dn']
208                         continue
209
210                     secdesc_ndr = gmsg[0]['ntSecurityDescriptor'][0]
211                     secdesc = ndr_unpack(dcerpc.security.descriptor, secdesc_ndr)
212
213                     try:
214                         samba.security.access_check(secdesc, token,
215                                                     dcerpc.security.SEC_STD_READ_CONTROL |
216                                                     dcerpc.security.SEC_ADS_LIST |
217                                                     dcerpc.security.SEC_ADS_READ_PROP)
218                     except RuntimeError:
219                         print "Failed access check on %s" % msg.dn
220                         continue
221
222                     # check the flags on the GPO
223                     flags = int(attr_default(gmsg[0], 'flags', 0))
224                     if is_computer and (flags & dsdb.GPO_FLAG_MACHINE_DISABLE):
225                         continue
226                     if not is_computer and (flags & dsdb.GPO_FLAG_USER_DISABLE):
227                         continue
228                     gpos.append(g)
229
230             # check if this blocks inheritance
231             gpoptions = int(attr_default(msg, 'gPOptions', 0))
232             if gpoptions & dsdb.GPO_BLOCK_INHERITANCE:
233                 inherit = False
234
235             if dn == self.samdb.get_default_basedn():
236                 break
237             dn = dn.parent()
238
239         if is_computer:
240             msg_str = 'computer'
241         else:
242             msg_str = 'user'
243
244         print "GPOs for %s %s" % (msg_str, username)
245         for g in gpos:
246             print "\t%s" % g['dn']
247
248
249 class cmd_show(Command):
250     """Show information for a GPO"""
251
252     synopsis = "%prog gpo show <dn>"
253
254     takes_optiongroups = {
255         "sambaopts": options.SambaOptions,
256         "versionopts": options.VersionOptions,
257         "credopts": options.CredentialsOptions,
258     }
259
260     takes_args = [ 'dn' ]
261
262     takes_options = [
263         Option("-H", help="LDB URL for database or target server", type=str)
264         ]
265
266     def run(self, dn, H=None, sambaopts=None, credopts=None, versionopts=None):
267
268         self.url = H
269         self.lp = sambaopts.get_loadparm()
270
271         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
272
273         samdb_connect(self)
274
275         gpo_flags = [
276             ("GPO_FLAG_USER_DISABLE", dsdb.GPO_FLAG_USER_DISABLE ),
277             ( "GPO_FLAG_MACHINE_DISABLE", dsdb.GPO_FLAG_MACHINE_DISABLE ) ]
278
279         try:
280             msg = self.samdb.search(base=dn, scope=ldb.SCOPE_BASE,
281                                     expression="(objectClass=groupPolicyContainer)",
282                                     attrs=['nTSecurityDescriptor',
283                                          'versionNumber',
284                                          'flags',
285                                          'name',
286                                          'displayName',
287                                          'gPCFileSysPath'])[0]
288         except Exception, e:
289             raise CommandError("Could not find GPC with DN %s (%s)" % dn, e)
290
291         secdesc_ndr = msg['ntSecurityDescriptor'][0]
292         secdesc = ndr_unpack(dcerpc.security.descriptor, secdesc_ndr)
293
294         print("GPO          : %s" % msg['name'][0])
295         print("display name : %s" % msg['displayName'][0])
296         print("path         : %s" % msg['gPCFileSysPath'][0])
297         print("dn           : %s" % msg.dn)
298         print("version      : %s" % attr_default(msg, 'versionNumber', '0'))
299         print("flags        : %s" % flags_string(gpo_flags, int(attr_default(msg, 'flags', 0))))
300         print("ACL          : %s" % secdesc.as_sddl())
301         print("")
302
303
304 class cmd_getlink(Command):
305     """List GPO Links for a container"""
306
307     synopsis = "%prog gpo getlink <container_dn>"
308
309     takes_optiongroups = {
310         "sambaopts": options.SambaOptions,
311         "versionopts": options.VersionOptions,
312         "credopts": options.CredentialsOptions,
313     }
314
315     takes_args = [ 'container_dn' ]
316
317     takes_options = [
318         Option("-H", help="LDB URL for database or target server", type=str)
319         ]
320
321     def run(self, container_dn, H=None, sambaopts=None, credopts=None,
322                 versionopts=None):
323
324         self.url = H
325         self.lp = sambaopts.get_loadparm()
326
327         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
328
329         samdb_connect(self)
330
331         gplink_options = [
332                 ("GPLINK_OPT_DISABLE", dsdb.GPLINK_OPT_DISABLE),
333                 ("GPLINK_OPT_ENFORCE", dsdb.GPLINK_OPT_ENFORCE),
334             ]
335
336         try:
337             msg = self.samdb.search(base=container_dn, scope=ldb.SCOPE_BASE,
338                                     expression="(objectClass=*)",
339                                     attrs=['gPlink'])[0]
340         except Exception, e:
341             raise CommandError("Could not find Container DN %s (%s)" % container_dn, e)
342
343         if 'gPLink' in msg:
344             print "GPO(s) linked to DN=%s" % container_dn
345             gplist = parse_gplink(msg['gPLink'][0])
346             for g in gplist:
347                 print("GPO DN       : %s" % g['dn'])
348                 print("Options      : %s" % flags_string(gplink_options, g['options']))
349                 print("")
350         else:
351             print "No GPO(s) linked to DN=%s" % container_dn
352
353
354 class cmd_setlink(Command):
355     """Add or Update a GPO link to a container"""
356
357     synopsis = "%prog gpo setlink <container_dn> <gpo_dn>"
358
359     takes_optiongroups = {
360         "sambaopts": options.SambaOptions,
361         "versionopts": options.VersionOptions,
362         "credopts": options.CredentialsOptions,
363     }
364
365     takes_args = [ 'container_dn', 'gpo_dn' ]
366
367     takes_options = [
368         Option("-H", help="LDB URL for database or target server", type=str),
369         Option("--disable", dest="disabled", default=False, action='store_true',
370             help="Disable policy"),
371         Option("--enforce", dest="enforced", default=False, action='store_true',
372             help="Enforce policy")
373         ]
374
375     def run(self, container_dn, gpo_dn, H=None, disabled=False, enforced=False,
376                 sambaopts=None, credopts=None, versionopts=None):
377
378         self.url = H
379         self.lp = sambaopts.get_loadparm()
380
381         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
382
383         samdb_connect(self)
384
385         gplink_options = 0
386         if disabled:
387             gplink_options |= dsdb.GPLINK_OPT_DISABLE
388         if enforced:
389             gplink_options |= dsdb.GPLINK_OPT_ENFORCE
390
391         # Check if valid GPO DN
392         try:
393             msg = self.samdb.search(base=gpo_dn, scope=ldb.SCOPE_BASE,
394                                     expression="(objectClass=groupPolicyContainer)",
395                                     attrs=['dn'])[0]
396         except Exception, e:
397                 raise CommandError("DN (%s) is not a GPO" % gpo_dn)
398
399         # Check if valid Container DN
400         try:
401             msg = self.samdb.search(base=container_dn, scope=ldb.SCOPE_BASE,
402                                     expression="(objectClass=*)",
403                                     attrs=['gPlink'])[0]
404         except Exception, e:
405             raise CommandError("Could not find container DN %s (%s)" % dn, e)
406
407         # Update existing GPlinks or Add new one
408         existing_gplink = False
409         if 'gPLink' in msg:
410             gplist = parse_gplink(msg['gPLink'][0])
411             existing_gplink = True
412             found = False
413             for g in gplist:
414                 if g['dn'].lower() == gpo_dn.lower():
415                     g['options'] = gplink_options
416                     found = True
417                     break
418             if not found:
419                 gplist.insert(0, { 'dn' : gpo_dn, 'options' : gplink_options })
420         else:
421             gplist = []
422             gplist.append({ 'dn' : gpo_dn, 'options' : gplink_options })
423
424         gplink_str = encode_gplink(gplist)
425
426         m = ldb.Message()
427         m.dn = ldb.Dn(self.samdb, container_dn)
428
429         if existing_gplink:
430             m['new_value'] = ldb.MessageElement(gplink_str, ldb.FLAG_MOD_REPLACE, 'gPLink')
431         else:
432             m['new_value'] = ldb.MessageElement(gplink_str, ldb.FLAG_MOD_ADD, 'gPLink')
433
434         try:
435             self.samdb.modify(m)
436         except Exception, e:
437             raise CommandError("Error adding GPO Link (%s)" % e)
438
439         print "Added/Updated GPO link"
440         cmd_getlink().run(container_dn, H, sambaopts, credopts, versionopts)
441
442
443 class cmd_dellink(Command):
444     """Delete GPO link from a container"""
445
446     synopsis = "%prog gpo dellink <container_dn> <gpo_dn>"
447
448     takes_optiongroups = {
449         "sambaopts": options.SambaOptions,
450         "versionopts": options.VersionOptions,
451         "credopts": options.CredentialsOptions,
452     }
453
454     takes_args = [ 'container_dn', 'gpo_dn' ]
455
456     takes_options = [
457         Option("-H", help="LDB URL for database or target server", type=str),
458         ]
459
460     def run(self, container_dn, gpo_dn, H=None, sambaopts=None, credopts=None,
461                 versionopts=None):
462
463         self.url = H
464         self.lp = sambaopts.get_loadparm()
465
466         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
467
468         samdb_connect(self)
469
470         # Check if valid GPO DN
471         try:
472             msg = self.samdb.search(base=gpo_dn, scope=ldb.SCOPE_BASE,
473                                     expression="(objectClass=groupPolicyContainer)",
474                                     attrs=['dn'])[0]
475         except Exception, e:
476                 raise CommandError("DN (%s) is not a GPO" % gpo_dn)
477
478         # Check if valid Container DN and get existing GPlinks
479         try:
480             msg = self.samdb.search(base=container_dn, scope=ldb.SCOPE_BASE,
481                                     expression="(objectClass=*)",
482                                     attrs=['gPlink'])[0]
483         except Exception, e:
484             raise CommandError("Could not find container DN %s (%s)" % dn, e)
485
486         if 'gPLink' in msg:
487             gplist = parse_gplink(msg['gPLink'][0])
488             for g in gplist:
489                 if g['dn'].lower() == gpo_dn.lower():
490                     gplist.remove(g)
491                     break
492         else:
493             raise CommandError("Specified GPO is not linked to this container");
494
495
496         m = ldb.Message()
497         m.dn = ldb.Dn(self.samdb, container_dn)
498
499         if gplist:
500             gplink_str = encode_gplink(gplist)
501             m['new_value'] = ldb.MessageElement(gplink_str, ldb.FLAG_MOD_REPLACE, 'gPLink')
502         else:
503             m['new_value'] = ldb.MessageElement('', ldb.FLAG_MOD_DELETE, 'gPLink')
504
505         try:
506             self.samdb.modify(m)
507         except Exception, e:
508             raise CommandError("Error Removing GPO Link (%s)" % e)
509
510         print "Deleted GPO link."
511         cmd_getlink().run(container_dn, H, sambaopts, credopts, versionopts)
512
513
514 class cmd_getinheritance(Command):
515     """Get inheritance flag for a container"""
516
517     synopsis = "%prog gpo getinheritance <container_dn>"
518
519     takes_optiongroups = {
520         "sambaopts": options.SambaOptions,
521         "versionopts": options.VersionOptions,
522         "credopts": options.CredentialsOptions,
523     }
524
525     takes_args = [ 'container_dn' ]
526
527     takes_options = [
528         Option("-H", help="LDB URL for database or target server", type=str)
529         ]
530
531     def run(self, container_dn, H=None, sambaopts=None, credopts=None,
532                 versionopts=None):
533
534         self.url = H
535         self.lp = sambaopts.get_loadparm()
536
537         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
538
539         samdb_connect(self)
540
541         try:
542             msg = self.samdb.search(base=container_dn, scope=ldb.SCOPE_BASE,
543                                     expression="(objectClass=*)",
544                                     attrs=['gPOptions'])[0]
545         except Exception, e:
546             raise CommandError("Could not find Container DN %s (%s)" % (container_dn, e))
547
548         inheritance = 0
549         if 'gPOptions' in msg:
550             inheritance = int(msg['gPOptions'][0]);
551
552         if inheritance == dsdb.GPO_BLOCK_INHERITANCE:
553             print "Container has GPO_BLOCK_INHERITANCE"
554         else:
555             print "Container has GPO_INHERIT"
556
557
558 class cmd_setinheritance(Command):
559     """Set inheritance flag on a container"""
560
561     synopsis = "%prog gpo setinheritance <container_dn> <block|inherit>"
562
563     takes_optiongroups = {
564         "sambaopts": options.SambaOptions,
565         "versionopts": options.VersionOptions,
566         "credopts": options.CredentialsOptions,
567     }
568
569     takes_args = [ 'container_dn', 'inherit_state' ]
570
571     takes_options = [
572         Option("-H", help="LDB URL for database or target server", type=str)
573         ]
574
575     def run(self, container_dn, inherit_state, H=None, sambaopts=None, credopts=None,
576                 versionopts=None):
577
578         if inherit_state.lower() == 'block':
579             inheritance = dsdb.GPO_BLOCK_INHERITANCE
580         elif inherit_state.lower() == 'inherit':
581             inheritance = dsdb.GPO_INHERIT
582         else:
583             raise CommandError("Unknown inheritance state (%s)" % inherit_state)
584
585         self.url = H
586         self.lp = sambaopts.get_loadparm()
587
588         self.creds = credopts.get_credentials(self.lp, fallback_machine=True)
589
590         samdb_connect(self)
591
592         try:
593             msg = self.samdb.search(base=container_dn, scope=ldb.SCOPE_BASE,
594                                     expression="(objectClass=*)",
595                                     attrs=['gPOptions'])[0]
596         except Exception, e:
597             raise CommandError("Could not find Container DN %s (%s)" % (container_dn, e))
598
599         m = ldb.Message()
600         m.dn = ldb.Dn(self.samdb, container_dn)
601
602         if 'gPOptions' in msg:
603             m['new_value'] = ldb.MessageElement(str(inheritance), ldb.FLAG_MOD_REPLACE, 'gPOptions')
604         else:
605             m['new_value'] = ldb.MessageElement(str(inheritance), ldb.FLAG_MOD_ADD, 'gPOptions');
606
607         try:
608             self.samdb.modify(m)
609         except Exception, e:
610             raise CommandError("Error setting inheritance state %s (%s)" % (inherit_state, e))
611
612
613 class cmd_fetch(Command):
614     """Download a GPO"""
615
616 class cmd_create(Command):
617     """Create a GPO"""
618
619 class cmd_setacl(Command):
620     """Set ACL on a GPO"""
621
622
623 class cmd_gpo(SuperCommand):
624     """Group Policy Object (GPO) commands"""
625
626     subcommands = {}
627     subcommands["listall"] = cmd_listall()
628     subcommands["list"] = cmd_list()
629     subcommands["show"] = cmd_show()
630     subcommands["getlink"] = cmd_getlink()
631     subcommands["setlink"] = cmd_setlink()
632     subcommands["dellink"] = cmd_dellink()
633     subcommands["getinheritance"] = cmd_getinheritance()
634     subcommands["setinheritance"] = cmd_setinheritance()
635     subcommands["fetch"] = cmd_fetch()
636     subcommands["create"] = cmd_create()
637     subcommands["setacl"] = cmd_setacl()