On 06/25/2012 08:50 PM, Rob Crittenden wrote:
> Simo Sorce wrote:
>> On Fri, 2012-06-22 at 14:25 +0200, Martin Kosek wrote:
>>> On 06/22/2012 02:23 PM, Simo Sorce wrote:
>>>> On Fri, 2012-06-22 at 12:20 +0200, Martin Kosek wrote:
>>>>> On 06/18/2012 05:37 PM, Rob Crittenden wrote:
>>>>>> Martin Kosek wrote:
>>>>>>> On Fri, 2012-06-15 at 10:15 -0400, Simo Sorce wrote:
>>>>>>>> On Fri, 2012-06-15 at 15:22 +0200, Martin Kosek wrote:
>>>>>>>>> Hello all,
>>>>>>>>>
>>>>>>>>> In a scope of ticket 2511 I would like to implement an ability to
>>>>>>>>> delegate a DNS update permissions to chosen user (or host) without
>>>>>>>>> having to give the user full "Update DNS Entries" privileges, i.e.
>>>>>>>>> allow
>>>>>>>>> him to modify any DNS zone or record.
>>>>>>>>>
>>>>>>>>> So far, this is what I would like to do (comments welcome):
>>>>>>>>>
>>>>>>>>> 1) Create new objectclass "idnsManagedZone" with "managedBy" attribute
>>>>>>>>> in MAY list
>>>>>>>>> 2) Create new DNS commands:
>>>>>>>>>     a] dnszone-add-managedby [--users=USERS] [--hosts=HOSTS]
>>>>>>>>>     b] dnszone-remove-managedby [--users=USERS] [--hosts=HOSTS]
>>>>>>>>>     - these commands would add/remove chosen user/host DN to managedBy
>>>>>>>>> attribute in chosen DNS zone
>>>>>>>>> 3) Add new generic ACIs to cn=dns,$SUFFIX:
>>>>>>>>> aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl
>>>>>>>>> "Users and hosts can add DNS entries";allow (add) userattr =
>>>>>>>>> "parent[1].managedby#USERDN";)
>>>>>>>>> ... add similar ACIs for UPDATE, REMOVE access
>>>>>>>>>
>>>>>>>>> With these steps done, all that an administrator would need to do to
>>>>>>>>> delegate a management of a DNS zone "example.com" is to run this
>>>>>>>>> command:
>>>>>>>>> $ ipa dnszone-add-managedby example.com --users=fbar
>>>>>>>>>
>>>>>>>>> The only downside I found so far is that the user would already need 
>>>>>>>>> to
>>>>>>>>> have "Read DNS Entries" permission assigned, otherwise he would not be
>>>>>>>>> able to actually read DNS entries (allow rules can't take precedence
>>>>>>>>> over deny rule we implemented to deny public access to DNS tree).
>>>>>>>>>
>>>>>>>>> An admin could of course create a special privilege and role with just
>>>>>>>>> "Read DNS Entries" permission and then assign it to relevant
>>>>>>>>> users/groups, but this looks awkward. Any idea to make this simpler?
>>>>>>>>> Maybe creating a group "dns readers" by default which would allow such
>>>>>>>>> access?
>>>>>>>>
>>>>>>>> Change the deny rule to deny to everyone except the user in
>>>>>>>> "parent[1].managedby#USERDN" ?
>>>>>>>>
>>>>>>>> Simo.
>>>>>>>>
>>>>>>>
>>>>>>> Good idea, I will do that. I will just use
>>>>>>> "parent[0,1].managedby#USERDN" so that user can also read the zone
>>>>>>> record. This way, a selected user will have read/write access to the
>>>>>>> chosen zone only, which is exactly what we want to achieve.
>>>>>>
>>>>>> Yes, this sounds workable to me too.
>>>>>>
>>>>>> rob
>>>>>>
>>>>>
>>>>> There were some second thoughts about the proposed design, which I would
>>>>> like to discuss so that we can eventually accept another (better)
>>>>> solution for this feature.
>>>>>
>>>>> The main concern here was that proposed solution (based on user list in
>>>>> managedBy attribute in DNS zone) is not in line with the rest of
>>>>> permission&privilege architecture in IPA.
>>>>>
>>>>> Here is another idea how to address the feature (I tested it and it
>>>>> would work):
>>>>> 1) Get rid of the deny rule on cn=dns,$SUFFIX by modifying global access
>>>>> rule (a working patch attached) to avoid current and future issues with
>>>>> extending ACIs (deny rules are evil).
>>>>>
>>>>> 2) Add new Managed Entry Definition and Template to automatically add
>>>>> "Manage DNS zone $idsname" permission. These could be used with standard
>>>>> IPA privileges, roles and thus could be assigned to users, groups,
>>>>> hosts, hostgroups...
>>>>>
>>>>> 3) New DNS zone managedBy attribute won't be manageable by user, but it
>>>>> will hold a DN of the managed Permission entry
>>>>>
>>>>> 4) Add the following ACIs to cn=dns,$SUFFIX:
>>>>> aci: (targetattr = "*")
>>>>> (version 3.0; acl "Read DNS entries"; allow (read,search,compare)
>>>>> userattr = "parent[0,1].managedby#GROUPDN";)
>>>>>
>>>>> aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)
>>>>> (version 3.0;acl "Add dns entries";allow (add)
>>>>> userattr = "parent[1].managedby#GROUPDN";)
>>>>>
>>>>> aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)
>>>>> (version 3.0;acl "Remove DNS entries";allow (delete)
>>>>> userattr = "parent[1].managedby#GROUPDN";)
>>>>>
>>>>> aci: (targetattr = "idnsname || cn || idnsallowdynupdate || dnsttl ||
>>>>> dnsclass || arecord || aaaarecord || a6record || nsrecord || cnamerecord
>>>>> || ptrrecord || srvrecord || txtrecord || mxrecord || mdrecord   ||
>>>>> hinforecord || minforecord || afsdbrecord || sigrecord || keyrecord ||
>>>>> locrecord || nxtrecord ||     naptrrecord || kxrecord || certrecord ||
>>>>> dnamerecord || dsrecord || sshfprecord || rrsigrecord || nsecrecord ||
>>>>> idnsname || idnszoneactive || idnssoamname || idnssoarname ||
>>>>> idnssoaserial || idnssoarefresh || idnssoaretry || idnssoaexpire ||
>>>>> idnssoaminimum || idnsupdatepolicy || idnsallowquery ||
>>>>> idnsallowtransfer || idnsallowsyncptr || idnsforwardpolicy ||
>>>>> idnsforwarders")
>>>>> (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Update
>>>>> DNS Entries";allow (write) userattr = "parent[0,1].managedby#GROUPDN";)
>>>>>
>>>>> I needed to add permission DN to the managedBy attribute so that I could
>>>>> create just one set of generic ACIs without having to create a set of
>>>>> ACIs for every new zone and thus let users with "Update DNS entries"
>>>>> permission have a write access to the "aci" attribute.
>>>>>
>>>>> Would this design be better than the previous one? Comments welcome.
>>>>
>>>> Removing Deny ACIs would be great.
>>>> But don't we need a second set of ACIs to allow uber admins to still
>>>> control all zones ? or is that part of current ACIs not going to
>>>> change ?
>>>>
>>>> Simo.
>>>>
>>>
>>> Thanks to the removal of the deny rule, this would be already allowed by
>>> this existing ACI:
>>>
>>> aci: (targetattr != "userPassword || krbPrincipalKey || sambaLMPassword
>>> || sambaNTPassword || passwordHistory || krbMKey || krbPrincipalName ||
>>> krbCanonicalName || krbUPEnabled || krbTicketPolicyReference ||
>>> krbPrincipalExpiration || krbPasswordExpiration || krbPwdPolicyReference
>>> || krbPrincipalType || krbPwdHistory || krbLastPwdChange ||
>>> krbPrincipalAliases || krbExtraData || krbLastSuccessfulAuth ||
>>> krbLastFailedAuth || krbLoginFailedCount || krbTicketFlags ||
>>> ipaUniqueId || memberOf || serverHostName || enrolledBy")(version 3.0;
>>> acl "Admin can manage any entry"; allow (all) groupdn =
>>> "ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";;)
>>
>> Oh right!
>> I like it even more then :-)
>>
>> Simo.
>>
> 
> Yes, this looks like it will work and eliminating a deny rule is a definite 
> plus.
> 
> rob

I have finished a patch based on the second design. IMO it is indeed better -
no deny ACI for DNS and just a standard permission for per-zone access 
delegation.

There is just one difference from the proposed design draft: per-zone
permissions are not created automatically by Managed Entry plugin, but rather
manually and only for DNS zones where per-zone access is needed. There is a new
command for that - dnszone-add-permission.

This will leave permission tree cleaner + we won't have to deal with all
Managed Entry plugin machinery.

More details can be found in a commit message.

Martin
From 7ba15098b0da6e6abec508c1ca042a36fc3976d0 Mon Sep 17 00:00:00 2001
From: Martin Kosek <mko...@redhat.com>
Date: Wed, 27 Jun 2012 13:10:10 +0200
Subject: [PATCH] Per-domain DNS record permissions

IPA implements read/write permissions for DNS record or zones.
Provided set of permissions and privileges can, however, only grant
access to the whole DNS tree, which may not be appropriate.
Administrators may miss more fine-grained permissions allowing
them to delegate access per-zone.

Create a new IPA auxiliary objectclass ipaDNSZone allowing
a managedBy attribute for a DNS zone. This attribute will hold
a group DN (in this case a permission) which allows its members
to read or write in a zone. Member permissions in given zone
will only have 2 limitations:
1) Members cannot delete the zone
2) Members cannot edit managedBy attribute

Current DNS deny ACI used to enforce read access is removed so that
DNS privileges are based on allow ACIs only, which is much more
flexible approach as deny ACIs have always precedence and limit
other extensions. Per-zone access is allowed in 3 generic ACIs
placed in cn=dns,$SUFFIX so that no special ACIs has to be added
to DNS zones itselves.

2 new commands have been added which allows an administrator to
create the system permission allowing the per-zone access and
fill a zone's managedBy attribute:
 * dnszone-add-permission: Add per-zone permission
 * dnszone-remove-permission: Remove per-zone permission

https://fedorahosted.org/freeipa/ticket/2511
---
 API.txt                               |   24 ++++++-
 VERSION                               |    2 +-
 install/share/60ipadns.ldif           |    1 +
 install/share/dns.ldif                |    5 +-
 install/updates/10-bind-schema.update |    7 ++
 install/updates/40-dns.update         |   12 +++-
 ipalib/plugins/dns.py                 |   82 +++++++++++++++++++++++
 ipalib/plugins/permission.py          |   42 +++++++++++-
 ipaserver/install/plugins/dns.py      |    2 +-
 tests/test_xmlrpc/objectclasses.py    |   11 ++++
 tests/test_xmlrpc/test_dns_plugin.py  |  115 ++++++++++++++++++++++++++++-----
 11 files changed, 279 insertions(+), 24 deletions(-)

diff --git a/API.txt b/API.txt
index a0c22143dc04b47400003a5fc84a40d1c71a5e82..3753e8afbb9ea549b885dd3d79238bece4082789 100644
--- a/API.txt
+++ b/API.txt
@@ -1031,6 +1031,12 @@ option: Str('version?', exclude='webui')
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('value', <type 'unicode'>, None)
+command: dnszone_add_permission
+args: 1,0,3
+arg: Str('idnsname', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('result', <type 'bool'>, None)
+output: Output('value', <type 'unicode'>, None)
 command: dnszone_del
 args: 1,1,3
 arg: Str('idnsname', attribute=True, cli_name='name', multivalue=True, primary_key=True, query=True, required=True)
@@ -1113,6 +1119,12 @@ option: Str('version?', exclude='webui')
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('value', <type 'unicode'>, None)
+command: dnszone_remove_permission
+args: 1,0,3
+arg: Str('idnsname', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('result', <type 'bool'>, None)
+output: Output('value', <type 'unicode'>, None)
 command: dnszone_show
 args: 1,4,3
 arg: Str('idnsname', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
@@ -2075,10 +2087,20 @@ option: Str('privilege*', alwaysask=True, cli_name='privileges', csv=True)
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('failed', <type 'dict'>, None)
 output: Output('completed', <type 'int'>, None)
+command: permission_add_noaci
+args: 1,3,3
+arg: Str('cn', cli_name='name', multivalue=False, pattern=None, primary_key=True, required=True)
+option: StrEnum('permissiontype?', values=(u'SYSTEM',))
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('value', <type 'unicode'>, None)
 command: permission_del
-args: 1,1,3
+args: 1,2,3
 arg: Str('cn', attribute=True, cli_name='name', multivalue=True, pattern='^[-_ a-zA-Z0-9]+$', primary_key=True, query=True, required=True)
 option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Flag('force', autofill=True, default=False)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('result', <type 'dict'>, None)
 output: Output('value', <type 'unicode'>, None)
diff --git a/VERSION b/VERSION
index 77340e02e91c91b45e5431810aac2a5c9d6237b6..bc76959b3a1709c6bbad76a0e4405c2c6e329bdd 100644
--- a/VERSION
+++ b/VERSION
@@ -79,4 +79,4 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=38
+IPA_API_VERSION_MINOR=39
diff --git a/install/share/60ipadns.ldif b/install/share/60ipadns.ldif
index 6f88d05e212c26addf55e2c2a9576840eeb9f87b..9697227fb7166b3711568ddea3e5c345277befa3 100644
--- a/install/share/60ipadns.ldif
+++ b/install/share/60ipadns.ldif
@@ -52,3 +52,4 @@ attributeTypes: ( 2.16.840.1.113730.3.8.5.17 NAME 'idnsPersistentSearch' DESC 'a
 objectClasses: ( 2.16.840.1.113730.3.8.6.0 NAME 'idnsRecord' DESC 'dns Record, usually a host' SUP top STRUCTURAL MUST idnsName MAY ( cn $ idnsAllowDynUpdate $ DNSTTL $ DNSClass $ ARecord $ AAAARecord $ A6Record $ NSRecord $ CNAMERecord $ PTRRecord $ SRVRecord $ TXTRecord $ MXRecord $ MDRecord $ HINFORecord $ MINFORecord $ AFSDBRecord $ SIGRecord $ KEYRecord $ LOCRecord $ NXTRecord $ NAPTRRecord $ KXRecord $ CERTRecord $ DNAMERecord $ DSRecord $ SSHFPRecord $ RRSIGRecord $ NSECRecord )
 objectClasses: ( 2.16.840.1.113730.3.8.6.1 NAME 'idnsZone' DESC 'Zone class' SUP idnsRecord STRUCTURAL MUST ( idnsName $ idnsZoneActive $ idnsSOAmName $ idnsSOArName $ idnsSOAserial $ idnsSOArefresh $ idnsSOAretry $ idnsSOAexpire $ idnsSOAminimum ) MAY ( idnsUpdatePolicy $ idnsAllowQuery $ idnsAllowTransfer $ idnsAllowSyncPTR $ idnsForwardPolicy $ idnsForwarders ) )
 objectClasses: ( 2.16.840.1.113730.3.8.6.2 NAME 'idnsConfigObject' DESC 'DNS global config options' STRUCTURAL MAY ( idnsForwardPolicy $ idnsForwarders $ idnsAllowSyncPTR $ idnsZoneRefresh $ idnsPersistentSearch ) )
+objectClasses: ( 2.16.840.1.113730.3.8.12.18 NAME 'ipaDNSZone' SUP top AUXILIARY MUST idnsName MAY managedBy X-ORIGIN 'IPA v3' )
diff --git a/install/share/dns.ldif b/install/share/dns.ldif
index 81ba21009ea5583437022344a6e72f7b26419cd9..d27f105b75ab1ac635ad16b31fe7f1332715f5f5 100644
--- a/install/share/dns.ldif
+++ b/install/share/dns.ldif
@@ -4,7 +4,10 @@ objectClass: idnsConfigObject
 objectClass: nsContainer
 objectClass: top
 cn: dns
-aci: (targetattr = "*")(version 3.0; acl "No access to DNS tree without a permission"; deny (read,search,compare) (groupdn != "ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";) and (groupdn != "ldap:///cn=Read DNS Entries,cn=permissions,cn=pbac,$SUFFIX");)
+aci: (targetattr = "*")(version 3.0; acl "Allow read access"; allow (read,search,compare) groupdn = "ldap:///cn=Read DNS Entries,cn=permissions,cn=pbac,$SUFFIX" or userattr = "parent[0,1].managedby#GROUPDN";)
+aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Add DNS entries in a zone";allow (add) userattr = "parent[1].managedby#GROUPDN";)
+aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Remove DNS entries from a zone";allow (delete) userattr = "parent[1].managedby#GROUPDN";)
+aci: (targetattr = "idnsname || cn || idnsallowdynupdate || dnsttl || dnsclass || arecord || aaaarecord || a6record || nsrecord || cnamerecord || ptrrecord || srvrecord || txtrecord || mxrecord || mdrecord || hinforecord || minforecord || afsdbrecord || sigrecord || keyrecord || locrecord || nxtrecord || naptrrecord || kxrecord || certrecord || dnamerecord || dsrecord || sshfprecord || rrsigrecord || nsecrecord || idnsname || idnszoneactive || idnssoamname || idnssoarname || idnssoaserial || idnssoarefresh || idnssoaretry || idnssoaexpire || idnssoaminimum || idnsupdatepolicy || idnsallowquery || idnsallowtransfer || idnsallowsyncptr || idnsforwardpolicy || idnsforwarders")(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Update DNS entries in a zone";allow (write) userattr = "parent[0,1].managedby#GROUPDN";)
 
 dn: $SUFFIX
 changetype: modify
diff --git a/install/updates/10-bind-schema.update b/install/updates/10-bind-schema.update
index c3398c1f2d1ed1b57530b32b04beb17eb7e0249b..0edbad2046929210e8b88d8232d24b5298912eb5 100644
--- a/install/updates/10-bind-schema.update
+++ b/install/updates/10-bind-schema.update
@@ -68,4 +68,11 @@ add:objectClasses:
       MAY ( idnsForwardPolicy $$ idnsForwarders $$ idnsAllowSyncPTR $$
         idnsZoneRefresh $$ idnsPersistentSearch
       ) )
+add:objectClasses:
+    ( 2.16.840.1.113730.3.8.12.18
+      NAME 'ipaDNSZone'
+      SUP top AUXILIARY
+      MUST idnsName
+      MAY managedBy
+      X-ORIGIN 'IPA v3' )
 replace:objectClasses:( 2.16.840.1.113730.3.8.6.1 NAME 'idnsZone' DESC 'Zone class' SUP idnsRecord STRUCTURAL MUST ( idnsZoneActive $$ idnsSOAmName $$ idnsSOArName $$ idnsSOAserial $$ idnsSOArefresh $$ idnsSOAretry $$ idnsSOAexpire $$ idnsSOAminimum ) MAY idnsUpdatePolicy )::( 2.16.840.1.113730.3.8.6.1 NAME 'idnsZone' DESC 'Zone class' SUP idnsRecord    STRUCTURAL MUST ( idnsName $$ idnsZoneActive $$ idnsSOAmName $$ idnsSOArName $$ idnsSOAserial $$ idnsSOArefresh $$ idnsSOAretry $$ idnsSOAexpire $$ idnsSOAminimum ) MAY ( idnsUpdatePolicy $$ idnsAllowQuery $$ idnsAllowTransfer $$ idnsAllowSyncPTR $$ idnsForwardPolicy $$ idnsForwarders ) )
diff --git a/install/updates/40-dns.update b/install/updates/40-dns.update
index 3dacb248f06626431e4eef9a65394008a5c71acb..3478a03ca27fc38c3c08d556fcd7af851aea8b9b 100644
--- a/install/updates/40-dns.update
+++ b/install/updates/40-dns.update
@@ -26,10 +26,18 @@ add: basedn: 'cn=privileges,cn=pbac,$SUFFIX'
 add: filter: (objectclass=*)
 add: ttl: 10
 
-# add idnsConfigObject if it is not there already
+# update DNS container
 dn: cn=dns, $SUFFIX
 addifexist: objectClass: idnsConfigObject
+addifexist: aci:'(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Add DNS entries in a zone";allow (add) userattr = "parent[1].managedby#GROUPDN";)'
+addifexist: aci:'(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Remove DNS entries from a zone";allow (delete) userattr = "parent[1].managedby#GROUPDN";)'
+addifexist: aci:'(targetattr = "idnsname || cn || idnsallowdynupdate || dnsttl || dnsclass || arecord || aaaarecord || a6record || nsrecord || cnamerecord || ptrrecord || srvrecord || txtrecord || mxrecord || mdrecord || hinforecord || minforecord || afsdbrecord || sigrecord || keyrecord || locrecord || nxtrecord || naptrrecord || kxrecord || certrecord || dnamerecord || dsrecord || sshfprecord || rrsigrecord || nsecrecord || idnsname || idnszoneactive || idnssoamname || idnssoarname || idnssoaserial || idnssoarefresh || idnssoaretry || idnssoaexpire || idnssoaminimum || idnsupdatepolicy || idnsallowquery || idnsallowtransfer || idnsallowsyncptr || idnsforwardpolicy || idnsforwarders")(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Update DNS entries in a zone";allow (write) userattr = "parent[0,1].managedby#GROUPDN";)'
 
 # update DNS acis with new idnsRecord attributes
 dn: $SUFFIX
-replace:aci:'(targetattr = "idnsname || cn || idnsallowdynupdate || dnsttl || dnsclass || arecord || aaaarecord || a6record || nsrecord || cnamerecord || ptrrecord || srvrecord || txtrecord || mxrecord || mdrecord || hinforecord || minforecord || afsdbrecord || sigrecord || keyrecord || locrecord || nxtrecord || naptrrecord || kxrecord || certrecord || dnamerecord || dsrecord || sshfprecord || rrsigrecord || nsecrecord || idnsname || idnszoneactive || idnssoamname || idnssoarname || idnssoaserial || idnssoarefresh || idnssoaretry || idnssoaexpire || idnssoaminimum || idnsupdatepolicy")(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "permission:update dns entries";allow (write) groupdn = "ldap:///cn=update dns entries,cn=permissions,cn=pbac,$SUFFIX";)::(targetattr = "idnsname || cn || idnsallowdynupdate || dnsttl || dnsclass || arecord || aaaarecord || a6record || nsrecord || cnamerecord || ptrrecord || srvrecord || txtrecord || mxrecord || mdrecord || hinforecord || minforecord || afsdbrecord || sigrecord || keyrecord || locrecord || nxtrecord || naptrrecord || kxrecord || certrecord || dnamerecord || dsrecord || sshfprecord || rrsigrecord || nsecrecord || idnsname || idnszoneactive || idnssoamname || idnssoarname || idnssoaserial || idnssoarefresh || idnssoaretry || idnssoaexpire || idnssoaminimum || idnsupdatepolicy || idnsallowquery || idnsallowtransfer || idnsallowsyncptr || idnsforwardpolicy || idnsforwarders")(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "permission:update dns entries";allow (write) groupdn = "ldap:///cn=update dns entries,cn=permissions,cn=pbac,$SUFFIX";)'
+replace:aci:'(targetattr = "idnsname || cn || idnsallowdynupdate || dnsttl || dnsclass || arecord || aaaarecord || a6record || nsrecord || cnamerecord || ptrrecord || srvrecord || txtrecord || mxrecord || mdrecord || hinforecord || minforecord || afsdbrecord || sigrecord || keyrecord || locrecord || nxtrecord || naptrrecord || kxrecord || certrecord || dnamerecord || dsrecord || sshfprecord || rrsigrecord || nsecrecord || idnsname || idnszoneactive || idnssoamname || idnssoarname || idnssoaserial || idnssoarefresh || idnssoaretry || idnssoaexpire || idnssoaminimum || idnsupdatepolicy")(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "permission:update dns entries";allow (write) groupdn = "ldap:///cn=update dns entries,cn=permissions,cn=pbac,$SUFFIX";)::(targetattr = "idnsname || cn || idnsallowdynupdate || dnsttl || dnsclass || arecord || aaaarecord || a6record || nsrecord || cnamerecord || ptrrecord || srvrecord || txtrecord || mxrecord || mdrecord || hinforecord || minforecord || afsdbrecord || sigrecord || keyrecord || locrecord || nxtrecord || naptrrecord || kxrecord || certrecord || dnamerecord || dsrecord || sshfprecord || rrsigrecord || nsecrecord || idnsname || idnszoneactive || idnssoamname || idnssoarname || idnssoaserial || idnssoarefresh || idnssoaretry || idnssoaexpire || idnssoaminimum || idnsupdatepolicy || idnsallowquery || idnsallowtransfer || idnsallowsyncptr || idnsforwardpolicy || idnsforwarders || managedby")(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "permission:update dns entries";allow (write) groupdn = "ldap:///cn=update dns entries,cn=permissions,cn=pbac,$SUFFIX";)'
+replace:aci:'(targetattr = "idnsname || cn || idnsallowdynupdate || dnsttl || dnsclass || arecord || aaaarecord || a6record || nsrecord || cnamerecord || ptrrecord || srvrecord || txtrecord || mxrecord || mdrecord || hinforecord || minforecord || afsdbrecord || sigrecord || keyrecord || locrecord || nxtrecord || naptrrecord || kxrecord || certrecord || dnamerecord || dsrecord || sshfprecord || rrsigrecord || nsecrecord || idnsname || idnszoneactive || idnssoamname || idnssoarname || idnssoaserial || idnssoarefresh || idnssoaretry || idnssoaexpire || idnssoaminimum || idnsupdatepolicy || idnsallowquery || idnsallowtransfer || idnsallowsyncptr || idnsforwardpolicy || idnsforwarders")(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "permission:update dns entries";allow (write) groupdn = "ldap:///cn=update dns entries,cn=permissions,cn=pbac,$SUFFIX";)::(targetattr = "idnsname || cn || idnsallowdynupdate || dnsttl || dnsclass || arecord || aaaarecord || a6record || nsrecord || cnamerecord || ptrrecord || srvrecord || txtrecord || mxrecord || mdrecord || hinforecord || minforecord || afsdbrecord || sigrecord || keyrecord || locrecord || nxtrecord || naptrrecord || kxrecord || certrecord || dnamerecord || dsrecord || sshfprecord || rrsigrecord || nsecrecord || idnsname || idnszoneactive || idnssoamname || idnssoarname || idnssoaserial || idnssoarefresh || idnssoaretry || idnssoaexpire || idnssoaminimum || idnsupdatepolicy || idnsallowquery || idnsallowtransfer || idnsallowsyncptr || idnsforwardpolicy || idnsforwarders || managedby")(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "permission:update dns entries";allow (write) groupdn = "ldap:///cn=update dns entries,cn=permissions,cn=pbac,$SUFFIX";)'
+
+# replace DNS tree deny rule with managedBy enhanced allow rule
+dn: cn=dns, $SUFFIX
+replace:aci:'(targetattr = "*")(version 3.0; acl "No access to DNS tree without a permission"; deny (read,search,compare) (groupdn != "ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";) and (groupdn != "ldap:///cn=Read DNS Entries,cn=permissions,cn=pbac,$SUFFIX");)::(targetattr = "*")(version 3.0; acl "Allow read access"; allow (read,search,compare) groupdn = "ldap:///cn=Read DNS Entries,cn=permissions,cn=pbac,$SUFFIX" or userattr = "parent[0,1].managedby#GROUPDN";)'
diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py
index 0f1014caed58105334a3eb5590329e8fa749de5b..4dc5caa7d62eeeae0373c9d43a538f7b48f4c77b 100644
--- a/ipalib/plugins/dns.py
+++ b/ipalib/plugins/dns.py
@@ -75,6 +75,9 @@ EXAMPLES:
    ipa dnszone-add example.com --name-server=nameserver.example.com \\
                                --admin-email=ad...@example.com
 
+ Add system permission that can be used for per-zone privilege delegation:
+   ipa dnszone-add-permission example.com
+
  Modify the zone to allow dynamic updates for hosts own records in realm EXAMPLE.COM:
    ipa dnszone-mod example.com --dynamic-update=TRUE
 
@@ -1528,6 +1531,7 @@ class dnszone(LDAPObject):
     object_name = _('DNS zone')
     object_name_plural = _('DNS zones')
     object_class = ['top', 'idnsrecord', 'idnszone']
+    possible_objectclasses = ['ipadnszone']
     default_attributes = [
         'idnsname', 'idnszoneactive', 'idnssoamname', 'idnssoarname',
         'idnssoaserial', 'idnssoarefresh', 'idnssoaretry', 'idnssoaexpire',
@@ -1696,6 +1700,9 @@ class dnszone(LDAPObject):
 
         return dn
 
+    def permission_name(self, zone):
+        return u"Manage DNS zone %s" % zone
+
 api.register(dnszone)
 
 
@@ -1752,6 +1759,14 @@ api.register(dnszone_add)
 class dnszone_del(LDAPDelete):
     __doc__ = _('Delete DNS zone (SOA record).')
 
+    def post_callback(self, ldap, dn, *keys, **options):
+        try:
+            api.Command['permission_del'](self.obj.permission_name(keys[-1]),
+                    force=True)
+        except errors.NotFound:
+            pass
+        return True
+
 api.register(dnszone_del)
 
 
@@ -1851,6 +1866,73 @@ class dnszone_enable(LDAPQuery):
 
 api.register(dnszone_enable)
 
+class dnszone_add_permission(LDAPQuery):
+    __doc__ = _('Add a permission for per-zone access delegation.')
+
+    has_output = output.standard_value
+    msg_summary = _('Added system permission "%(value)s"')
+
+    def execute(self, *keys, **options):
+        ldap = self.obj.backend
+        dn = self.obj.get_dn(*keys, **options)
+
+        try:
+            (dn_, entry_attrs) = ldap.get_entry(dn, ['objectclass'])
+        except errors.NotFound:
+            self.obj.handle_not_found(*keys)
+
+        permission_name = self.obj.permission_name(keys[-1])
+        permission = api.Command['permission_add_noaci'](permission_name,
+                         permissiontype=u'SYSTEM'
+                     )['result']
+
+        update = {}
+        dnszone_ocs = entry_attrs.get('objectclass')
+        if dnszone_ocs:
+            dnszone_ocs.append('ipadnszone')
+            update['objectclass'] = list(set(dnszone_ocs))
+
+        update['managedby'] = [permission['dn']]
+        ldap.update_entry(dn, update)
+
+        return dict(
+            result=True,
+            value=permission_name,
+        )
+
+api.register(dnszone_add_permission)
+
+class dnszone_remove_permission(LDAPQuery):
+    __doc__ = _('Remove a permission for per-zone access delegation.')
+
+    has_output = output.standard_value
+    msg_summary = _('Removed system permission "%(value)s"')
+
+    def execute(self, *keys, **options):
+        ldap = self.obj.backend
+        dn = self.obj.get_dn(*keys, **options)
+
+        try:
+            ldap.update_entry(dn, {'managedby': None})
+        except errors.NotFound:
+            self.obj.handle_not_found(*keys)
+        except errors.EmptyModlist:
+            # managedBy attribute is clean, lets make sure there is also no
+            # dangling DNS zone permission
+            pass
+
+        permission_name = self.obj.permission_name(keys[-1])
+        try:
+            api.Command['permission_del'](permission_name, force=True)
+        except errors.NotFound:
+            pass
+
+        return dict(
+            result=True,
+            value=permission_name,
+        )
+
+api.register(dnszone_remove_permission)
 
 class dnsrecord(LDAPObject):
     """
diff --git a/ipalib/plugins/permission.py b/ipalib/plugins/permission.py
index 05d19ad8d56ec31fad7d463df37b45b20a72e847..16362e51aef954bb5d4b61ca9ff8b339363bee82 100644
--- a/ipalib/plugins/permission.py
+++ b/ipalib/plugins/permission.py
@@ -246,14 +246,54 @@ class permission_add(LDAPCreate):
 
 api.register(permission_add)
 
+class permission_add_noaci(LDAPCreate):
+    __doc__ = _('Add a system permission without an ACI')
+
+    msg_summary = _('Added permission "%(value)s"')
+    has_output_params = LDAPCreate.has_output_params + output_params
+    NO_CLI = True
+
+    takes_options = (
+        StrEnum('permissiontype?',
+            label=_('Permission type'),
+            values=(u'SYSTEM',),
+        ),
+    )
+
+    def get_args(self):
+        # do not validate system permission names
+        yield self.obj.primary_key.clone(pattern=None, pattern_errmsg=None)
+
+    def get_options(self):
+        for option in super(permission_add_noaci, self).get_options():
+            # filter ACI options out
+            if option.name in ('all', 'raw', 'permissiontype'):
+                yield option
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        permission_type = options.get('permissiontype')
+        if permission_type:
+            entry_attrs['ipapermissiontype'] = [ permission_type ]
+        return dn
+
+api.register(permission_add_noaci)
+
 
 class permission_del(LDAPDelete):
     __doc__ = _('Delete a permission.')
 
     msg_summary = _('Deleted permission "%(value)s"')
 
+    takes_options = LDAPDelete.takes_options + (
+        Flag('force',
+             label=_('Force'),
+             flags=['no_option', 'no_output'],
+             doc=_('force delete of SYSTEM permissions'),
+        ),
+    )
+
     def pre_callback(self, ldap, dn, *keys, **options):
-        if not self.obj.check_system(ldap, dn, *keys):
+        if not options.get('force') and not self.obj.check_system(ldap, dn, *keys):
             raise errors.ACIError(info='A SYSTEM permission may not be removed')
         # remove permission even when the underlying ACI is missing
         try:
diff --git a/ipaserver/install/plugins/dns.py b/ipaserver/install/plugins/dns.py
index 29b71dd9d6fb05d8d6ed7a535d30ac20d18937ac..e11c331a4dd953405275aff9a8a334ced854a2c8 100644
--- a/ipaserver/install/plugins/dns.py
+++ b/ipaserver/install/plugins/dns.py
@@ -119,7 +119,7 @@ class update_dns_permissions(PostUpdate):
     _write_dns_aci_entry = ['add:aci:\'(targetattr = "idnsforwardpolicy || idnsforwarders || idnsallowsyncptr || idnszonerefresh || idnspersistentsearch")(target = "ldap:///cn=dns,%(realm)s")(version 3.0;acl "permission:Write DNS Configuration";allow (write) groupdn = "ldap:///cn=Write DNS Configuration,cn=permissions,cn=pbac,%(realm)s";)\'' % dict(realm=api.env.basedn)]
 
     _read_dns_aci_dn = DN(api.env.container_dns, api.env.basedn)
-    _read_dns_aci_entry = ['add:aci:\'(targetattr = "*")(version 3.0; acl "No access to DNS tree without a permission"; deny (read,search,compare) (groupdn != "ldap:///cn=admins,cn=groups,cn=accounts,%(realm)s") and (groupdn != "ldap:///cn=Read DNS Entries,cn=permissions,cn=pbac,%(realm)s");)\''  % dict(realm=api.env.basedn) ]
+    _read_dns_aci_entry = ['add:aci:\'(targetattr = "*")(version 3.0; acl "Allow read access"; allow (read,search,compare) groupdn = "ldap:///cn=Read DNS Entries,cn=permissions,cn=pbac,%(realm)s" or userattr = "parent[0,1].managedby#GROUPDN";)\''  % dict(realm=api.env.basedn) ]
 
     def execute(self, **options):
         ldap = self.obj.backend
diff --git a/tests/test_xmlrpc/objectclasses.py b/tests/test_xmlrpc/objectclasses.py
index a036b34dee195619ce4f2d5e557dae2d5069c700..4bb2b3510196f8e390b439e1b3a5e12980a42c69 100644
--- a/tests/test_xmlrpc/objectclasses.py
+++ b/tests/test_xmlrpc/objectclasses.py
@@ -141,3 +141,14 @@ hbacrule = [
     u'ipaassociation',
     u'ipahbacrule',
 ]
+
+dnszone = [
+    u'top',
+    u'idnsrecord',
+    u'idnszone',
+]
+
+dnsrecord = [
+    u'top',
+    u'idnsrecord',
+]
diff --git a/tests/test_xmlrpc/test_dns_plugin.py b/tests/test_xmlrpc/test_dns_plugin.py
index ab1d4f0be58b599e1119caa0118d35b130912d81..d121b2f0fb625c5bbe25a58874500e77065bd21e 100644
--- a/tests/test_xmlrpc/test_dns_plugin.py
+++ b/tests/test_xmlrpc/test_dns_plugin.py
@@ -31,6 +31,9 @@ dnszone1_dn = DN(('idnsname',dnszone1),('cn','dns'),api.env.basedn)
 dnszone1_mname = u'ns1.%s.' % dnszone1
 dnszone1_mname_dn = DN(('idnsname','ns1'), dnszone1_dn)
 dnszone1_rname = u'root.%s.' % dnszone1
+dnszone1_permission = u'Manage DNS zone %s' % dnszone1
+dnszone1_permission_dn = DN(('cn',dnszone1_permission),
+                            api.env.container_permission,api.env.basedn)
 dnszone2 = u'dnszone2.test'
 dnszone2_dn = DN(('idnsname',dnszone2),('cn','dns'),api.env.basedn)
 dnszone2_mname = u'ns1.%s.' % dnszone2
@@ -76,7 +79,8 @@ class test_dns(Declarative):
                                'idnsforwardpolicy' : None,
                                'idnsallowsyncptr' : None,
                                'idnszonerefresh' : None,
-                               })
+                               }),
+        ('permission_del', [dnszone1_permission], {'force': True}),
     ]
 
     tests = [
@@ -151,7 +155,7 @@ class test_dns(Declarative):
                                          % dict(realm=api.env.realm)],
                     'idnsallowtransfer': [u'none;'],
                     'idnsallowquery': [u'any;'],
-                    'objectclass': [u'top', u'idnsrecord', u'idnszone'],
+                    'objectclass': objectclasses.dnszone,
                 },
             },
         ),
@@ -212,7 +216,7 @@ class test_dns(Declarative):
                                          % dict(realm=api.env.realm)],
                     'idnsallowtransfer': [u'none;'],
                     'idnsallowquery': [u'any;'],
-                    'objectclass': [u'top', u'idnsrecord', u'idnszone'],
+                    'objectclass': objectclasses.dnszone,
                 },
             },
         ),
@@ -305,7 +309,7 @@ class test_dns(Declarative):
                                          % dict(realm=api.env.realm, zone=revdnszone1)],
                     'idnsallowtransfer': [u'none;'],
                     'idnsallowquery': [u'any;'],
-                    'objectclass': [u'top', u'idnsrecord', u'idnszone'],
+                    'objectclass': objectclasses.dnszone,
                 },
             },
         ),
@@ -503,7 +507,7 @@ class test_dns(Declarative):
                 'result': {
                     'dn': unicode(dnsres1_dn),
                     'idnsname': [dnsres1],
-                    'objectclass': [u'top', u'idnsrecord'],
+                    'objectclass': objectclasses.dnsrecord,
                     'arecord': [u'127.0.0.1'],
                 },
             },
@@ -548,7 +552,7 @@ class test_dns(Declarative):
                     'dn': unicode(dnsres1_dn),
                     'idnsname': [dnsres1],
                     'arecord': [u'127.0.0.1', u'10.10.0.1'],
-                    'objectclass': [u'top', u'idnsrecord'],
+                    'objectclass': objectclasses.dnsrecord,
                 },
             },
         ),
@@ -626,7 +630,7 @@ class test_dns(Declarative):
                 'value': u'@',
                 'summary': None,
                 'result': {
-                    'objectclass': [u'top', u'idnsrecord', u'idnszone'],
+                    'objectclass': objectclasses.dnszone,
                     'dn': unicode(dnszone1_dn),
                     'idnsname': [u'@'],
                     'mxrecord': [u"0 %s" % dnszone1_mname],
@@ -674,7 +678,7 @@ class test_dns(Declarative):
                 'value': u'_foo._tcp',
                 'summary': None,
                 'result': {
-                    'objectclass': [u'top', u'idnsrecord'],
+                    'objectclass': objectclasses.dnsrecord,
                     'dn': unicode(DN(('idnsname', u'_foo._tcp'), dnszone1_dn)),
                     'idnsname': [u'_foo._tcp'],
                     'srvrecord': [u"0 100 1234 %s" % dnszone1_mname],
@@ -731,7 +735,7 @@ class test_dns(Declarative):
                 'value': u'@',
                 'summary': None,
                 'result': {
-                    'objectclass': [u'top', u'idnsrecord', u'idnszone'],
+                    'objectclass': objectclasses.dnszone,
                     'dn': unicode(dnszone1_dn),
                     'idnsname': [u'@'],
                     'mxrecord': [u"0 %s" % dnszone1_mname],
@@ -756,7 +760,7 @@ class test_dns(Declarative):
                 'value': dnsres1,
                 'summary': None,
                 'result': {
-                    'objectclass': [u'top', u'idnsrecord'],
+                    'objectclass': objectclasses.dnsrecord,
                     'dn': unicode(dnsres1_dn),
                     'idnsname': [dnsres1],
                     'arecord': [u'10.10.0.1'],
@@ -780,7 +784,7 @@ class test_dns(Declarative):
                 'value': dnsres1,
                 'summary': None,
                 'result': {
-                    'objectclass': [u'top', u'idnsrecord'],
+                    'objectclass': objectclasses.dnsrecord,
                     'dn': unicode(dnsres1_dn),
                     'idnsname': [dnsres1],
                     'arecord': [u'10.10.0.1'],
@@ -797,7 +801,7 @@ class test_dns(Declarative):
                 'value': dnsres1,
                 'summary': None,
                 'result': {
-                    'objectclass': [u'top', u'idnsrecord'],
+                    'objectclass': objectclasses.dnsrecord,
                     'dn': unicode(dnsres1_dn),
                     'idnsname': [dnsres1],
                     'arecord': [u'10.10.0.1'],
@@ -817,7 +821,7 @@ class test_dns(Declarative):
                 'value': dnsres1,
                 'summary': None,
                 'result': {
-                    'objectclass': [u'top', u'idnsrecord'],
+                    'objectclass': objectclasses.dnsrecord,
                     'dn': unicode(dnsres1_dn),
                     'idnsname': [dnsres1],
                     'arecord': [u'10.10.0.1'],
@@ -849,7 +853,7 @@ class test_dns(Declarative):
                 'value': dnsres1,
                 'summary': None,
                 'result': {
-                    'objectclass': [u'top', u'idnsrecord'],
+                    'objectclass': objectclasses.dnsrecord,
                     'dn': unicode(dnsres1_dn),
                     'idnsname': [dnsres1],
                     'arecord': [u'10.10.0.1'],
@@ -943,7 +947,7 @@ class test_dns(Declarative):
                                          % dict(realm=api.env.realm, zone=revdnszone1)],
                     'idnsallowtransfer': [u'none;'],
                     'idnsallowquery': [u'any;'],
-                    'objectclass': [u'top', u'idnsrecord', u'idnszone'],
+                    'objectclass': objectclasses.dnszone,
                 },
             },
         ),
@@ -964,7 +968,7 @@ class test_dns(Declarative):
                 'value': dnsrev1,
                 'summary': None,
                 'result': {
-                    'objectclass': [u'top', u'idnsrecord'],
+                    'objectclass': objectclasses.dnsrecord,
                     'dn': unicode(dnsrev1_dn),
                     'idnsname': [dnsrev1],
                     'ptrrecord': [u'foo-1.example.com.'],
@@ -1072,7 +1076,7 @@ class test_dns(Declarative):
                 'result': {
                     'dn': unicode(dnsres1_dn),
                     'idnsname': [dnsres1],
-                    'objectclass': [u'top', u'idnsrecord'],
+                    'objectclass': objectclasses.dnsrecord,
                     'arecord': [u'80.142.15.81'],
                 },
             },
@@ -1095,6 +1099,83 @@ class test_dns(Declarative):
 
 
         dict(
+            desc='Try to add per-zone permission for unknown zone',
+            command=('dnszone_add_permission', [u'does.not.exist'], {}),
+            expected=errors.NotFound(reason=u'does.not.exist: DNS zone not found')
+        ),
+
+
+        dict(
+            desc='Add per-zone permission for zone %r' % dnszone1,
+            command=(
+                'dnszone_add_permission', [dnszone1], {}
+            ),
+            expected=dict(
+                result=True,
+                value=dnszone1_permission,
+                summary=u'Added system permission "%s"' % dnszone1_permission,
+            ),
+        ),
+
+
+        dict(
+            desc='Try to add duplicate per-zone permission for zone %r' % dnszone1,
+            command=(
+                'dnszone_add_permission', [dnszone1], {}
+            ),
+            expected=errors.DuplicateEntry(message=u'permission with name '
+                '"%s" already exists' % dnszone1_permission)
+        ),
+
+
+        dict(
+            desc='Make sure the permission was created %r' % dnszone1,
+            command=(
+                'permission_show', [dnszone1_permission], {}
+            ),
+            expected=dict(
+                value=dnszone1_permission,
+                summary=None,
+                result={
+                    'dn': lambda x: DN(x) == dnszone1_permission_dn,
+                    'cn': [dnszone1_permission],
+                    'ipapermissiontype': [u'SYSTEM'],
+                },
+            ),
+        ),
+
+
+        dict(
+            desc='Try to remove per-zone permission for unknown zone',
+            command=('dnszone_remove_permission', [u'does.not.exist'], {}),
+            expected=errors.NotFound(reason=u'does.not.exist: DNS zone not found')
+        ),
+
+
+        dict(
+            desc='Remove per-zone permission for zone %r' % dnszone1,
+            command=(
+                'dnszone_remove_permission', [dnszone1], {}
+            ),
+            expected=dict(
+                result=True,
+                value=dnszone1_permission,
+                summary=u'Removed system permission "%s"' % dnszone1_permission,
+            ),
+        ),
+
+
+        dict(
+            desc='Make sure the permission for zone %r was deleted' % dnszone1,
+            command=(
+                'permission_show', [dnszone1_permission], {}
+            ),
+            expected=errors.NotFound(reason=u'%s: permission not found'
+                                     % dnszone1_permission)
+        ),
+
+
+        dict(
             desc='Delete zone %r' % dnszone1,
             command=('dnszone_del', [dnszone1], {}),
             expected={
-- 
1.7.10.2

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to