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