On Mon, 2012-06-18 at 11:37 -0400, 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
> 

Ok, thank you both. I finished the patch, it should work fine for both
new installs and upgrades.

After the upgrade, all you have to do to delegate read/write privileges
to the zone is this command:

# ipa dnszone-add-managedby example.com --users=fbar

fbar then will be able to actually see the zone with dnszone-show +
modify it. Delegated permissions have several limitations though:
1) Delegated user cannot delete the zone
2) Delegated user cannot add or remove another users to the managedBy
list

Martin
>From f23fcbc55aef9789850f2a2cfd8f86a8a739cd57 Mon Sep 17 00:00:00 2001
From: Martin Kosek <mko...@redhat.com>
Date: Mon, 18 Jun 2012 13:59:36 +0200
Subject: [PATCH] Per-domain DNS record permissions

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

Create new IPA auxiliary objectclass ipaDNSZone allowing
a managedBy attribute for a DNS zone. When a user's DN is added
to the managedBy attribute, the selected user will have a read
and write access to the DNS zone and its records. There are
only 2 limitations for the selected user's permissions:
1) User cannot delete the zone
2) User cannot add or remove other users to/from managedBy list

New LDAP permissions are enforced by the updated deny ACI on
cn=dns,$SUFFIX and new add/update/delete ACIs on the same object
allowing the selected user to manipulate DNS zones.

The new objectclass ipaDNSZone is not added globally to all DNS
zones but only when needed to prevent DNS issues duting upgrades
when some replicas are updated and have the new objectclass ready
and some replicas do not know the new objectclass yet.

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                 |   59 ++++++++++++++++++++++-
 ipaserver/install/plugins/dns.py      |    2 +-
 tests/test_xmlrpc/objectclasses.py    |   11 ++++
 tests/test_xmlrpc/test_dns_plugin.py  |   87 +++++++++++++++++++++++++++------
 10 files changed, 187 insertions(+), 23 deletions(-)

diff --git a/API.txt b/API.txt
index 8127b90b91415d165590845f0ba1b6d94dab28aa..6459527fbf997e8ccca510e35ae05a5886350fb2 100644
--- a/API.txt
+++ b/API.txt
@@ -1031,6 +1031,16 @@ 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_managedby
+args: 1,4,3
+arg: Str('idnsname', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', 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: dnszone_del
 args: 1,1,3
 arg: Str('idnsname', attribute=True, cli_name='name', multivalue=True, primary_key=True, query=True, required=True)
@@ -1051,7 +1061,7 @@ output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('result', <type 'bool'>, None)
 output: Output('value', <type 'unicode'>, None)
 command: dnszone_find
-args: 1,26,4
+args: 1,28,4
 arg: Str('criteria?', noextrawhitespace=False)
 option: Str('idnsname', attribute=True, autofill=False, cli_name='name', multivalue=False, primary_key=True, query=True, required=False)
 option: Str('name_from_ip', attribute=False, autofill=False, cli_name='name_from_ip', multivalue=False, query=True, required=False)
@@ -1079,6 +1089,8 @@ option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Str('version?', exclude='webui')
 option: Flag('pkey_only?', autofill=True, default=False)
+option: Str('man_by_user*', cli_name='man_by_users', csv=True)
+option: Str('not_man_by_user*', cli_name='not_man_by_users', csv=True)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list of LDAP entries', domain='ipa', localedir=None))
 output: Output('count', <type 'int'>, None)
@@ -1113,6 +1125,16 @@ 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_managedby
+args: 1,4,3
+arg: Str('idnsname', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+option: Str('user*', alwaysask=True, cli_name='users', 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: dnszone_show
 args: 1,4,3
 arg: Str('idnsname', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
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..1f4ef73219d835d06933e86094a6e4817ff71fa5 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..3f67795831358e01e97bf66196ddf5e6a0f6d024 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 "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") and not (userattr = "parent[0,1].managedby#USERDN");)
+aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Selected users can add DNS entries";allow (add) userattr = "parent[1].managedby#USERDN";)
+aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Selected users can remove DNS entries";allow (delete) userattr = "parent[1].managedby#USERDN";)
+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 "Selected users can update DNS entries";allow (write) userattr = "parent[0,1].managedby#USERDN";)
 
 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..6e81fd6578547e612c69ae69f42c337ea6025ec9 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 "Selected users can add DNS entries";allow (add) userattr = "parent[1].managedby#USERDN";)'
+addifexist: aci:'(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Selected users can remove DNS entries";allow (delete) userattr = "parent[1].managedby#USERDN";)'
+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 "Selected users can update DNS entries";allow (write) userattr = "parent[0,1].managedby#USERDN";)'
 
 # 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 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 "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") and not (userattr = "parent[0,1].managedby#USERDN");)'
diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py
index 0f1014caed58105334a3eb5590329e8fa749de5b..6f09d32107225d3f6f0692d105da11c9fe91e28c 100644
--- a/ipalib/plugins/dns.py
+++ b/ipalib/plugins/dns.py
@@ -92,6 +92,9 @@ EXAMPLES:
  Add second nameserver for example.com:
    ipa dnsrecord-add example.com @ --ns-rec=nameserver2.example.com
 
+ Let user "tuser1" view and modify example.com:
+   ipa dnszone-add-managedby example.com --users=tuser1
+
  Add a mail server for example.com:
    ipa dnsrecord-add example.com @ --mx-rec="10 mail1"
 
@@ -1520,6 +1523,12 @@ def default_zone_update_policy(zone):
     else:
         return get_dns_forward_zone_update_policy(api.env.realm)
 
+dnszone_output_params = (
+    Str('managedby_user',
+        label='Managed by',
+    ),
+)
+
 class dnszone(LDAPObject):
     """
     DNS Zone, container for resource records.
@@ -1528,12 +1537,19 @@ 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',
         'idnssoaminimum', 'idnsallowquery', 'idnsallowtransfer',
-        'idnsforwarders', 'idnsforwardpolicy'
+        'idnsforwarders', 'idnsforwardpolicy', 'managedby'
     ] + _record_attributes
+    attribute_members = {
+        'managedby': ['user'],
+    }
+    relationships = {
+        'managedby': ('Managed by', 'man_by_', 'not_man_by_'),
+    }
     label = _('DNS Zones')
     label_singular = _('DNS Zone')
 
@@ -1702,6 +1718,8 @@ api.register(dnszone)
 class dnszone_add(LDAPCreate):
     __doc__ = _('Create new DNS zone (SOA record).')
 
+    has_output_params = LDAPCreate.has_output_params + dnszone_output_params
+
     takes_options = LDAPCreate.takes_options + (
         Flag('force',
              label=_('Force'),
@@ -1757,11 +1775,15 @@ api.register(dnszone_del)
 
 class dnszone_mod(LDAPUpdate):
     __doc__ = _('Modify DNS zone (SOA record).')
+    has_output_params = LDAPUpdate.has_output_params + dnszone_output_params
 
 api.register(dnszone_mod)
 
 
 class dnszone_find(LDAPSearch):
+    member_attributes = ['managedby']
+    has_output_params = LDAPSearch.has_output_params + dnszone_output_params
+
     __doc__ = _('Search for DNS zones (SOA records).')
 
     def args_options_2_params(self, *args, **options):
@@ -1806,6 +1828,7 @@ api.register(dnszone_find)
 
 class dnszone_show(LDAPRetrieve):
     __doc__ = _('Display information about a DNS zone (SOA record).')
+    has_output_params = LDAPRetrieve.has_output_params + dnszone_output_params
 
 api.register(dnszone_show)
 
@@ -1852,6 +1875,40 @@ class dnszone_enable(LDAPQuery):
 api.register(dnszone_enable)
 
 
+class dnszone_add_managedby(LDAPAddMember):
+    __doc__ = _('Add users that can manage this zone.')
+
+    member_attributes = ['managedby']
+    has_output_params = LDAPAddMember.has_output_params + dnszone_output_params
+
+    def pre_callback(self, ldap, dn, member_dns, failed, *keys, **options):
+        try:
+            (dn_, entry_attrs) = ldap.get_entry(dn, ['objectclass'])
+        except errors.NotFound:
+            self.obj.handle_not_found(*keys)
+        dnszone_ocs = entry_attrs.get('objectclass')
+        if dnszone_ocs is None:
+            raise errors.DatabaseError(desc=_(u'Cannot update DNS zone objectclass list'),
+                                       info=_('objectclass attribute not returned'))
+        elif u'ipadnszone' not in dnszone_ocs:
+            # Add missing objectclass which contains managedBy attribute
+            dnszone_ocs.append(u'ipadnszone')
+            ldap.update_entry(dn, {'objectclass' : dnszone_ocs})
+
+        return dn
+
+api.register(dnszone_add_managedby)
+
+
+class dnszone_remove_managedby(LDAPRemoveMember):
+    __doc__ = _('Remove users that can manage this zone.')
+
+    member_attributes = ['managedby']
+    has_output_params = LDAPRemoveMember.has_output_params + dnszone_output_params
+
+api.register(dnszone_remove_managedby)
+
+
 class dnsrecord(LDAPObject):
     """
     DNS record.
diff --git a/ipaserver/install/plugins/dns.py b/ipaserver/install/plugins/dns.py
index 29b71dd9d6fb05d8d6ed7a535d30ac20d18937ac..f225d092ceba87e3ea41def6d9798f02634d4338 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 "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") and not (userattr = "parent[0,1].managedby#USERDN");)\''  % 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..3448ee85871413eca35dbaf1a14ff6198aefa40a 100644
--- a/tests/test_xmlrpc/test_dns_plugin.py
+++ b/tests/test_xmlrpc/test_dns_plugin.py
@@ -151,7 +151,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 +212,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 +305,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 +503,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 +548,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 +626,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 +674,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 +731,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 +756,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 +780,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 +797,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 +817,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 +849,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 +943,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 +964,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 +1072,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 +1095,61 @@ class test_dns(Declarative):
 
 
         dict(
+            desc='Add user managing zone %r' % dnszone1,
+            command=('dnszone_add_managedby', [dnszone1], {'user': u'admin'}),
+            expected={
+                'completed': 1,
+                'failed': {'managedby': {'user': tuple()}},
+                'result': {
+                    'dn': unicode(dnszone1_dn),
+                    'idnsname': [dnszone1],
+                    'idnszoneactive': [u'TRUE'],
+                    'nsrecord': [dnszone1_mname],
+                    'mxrecord': [u'0 ns1.dnszone.test.'],
+                    'locrecord': [u"49 11 42.400 N 16 36 29.600 E 227.64"],
+                    'idnssoamname': [dnszone1_mname],
+                    'idnssoarname': [dnszone1_rname],
+                    'idnssoaserial': [fuzzy_digits],
+                    'idnssoarefresh': [u'5478'],
+                    'idnssoaretry': [fuzzy_digits],
+                    'idnssoaexpire': [fuzzy_digits],
+                    'idnssoaminimum': [fuzzy_digits],
+                    'idnsallowquery': [u'!10.0.0.0/8;any;'],
+                    'idnsallowtransfer': [u'80.142.15.80;'],
+                    'managedby_user': [u'admin'],
+                },
+            },
+        ),
+
+
+        dict(
+            desc='Remove user managing zone %r' % dnszone1,
+            command=('dnszone_remove_managedby', [dnszone1], {'user': u'admin'}),
+            expected={
+                'completed': 1,
+                'failed': {'managedby': {'user': tuple()}},
+                'result': {
+                    'dn': unicode(dnszone1_dn),
+                    'idnsname': [dnszone1],
+                    'idnszoneactive': [u'TRUE'],
+                    'nsrecord': [dnszone1_mname],
+                    'mxrecord': [u'0 ns1.dnszone.test.'],
+                    'locrecord': [u"49 11 42.400 N 16 36 29.600 E 227.64"],
+                    'idnssoamname': [dnszone1_mname],
+                    'idnssoarname': [dnszone1_rname],
+                    'idnssoaserial': [fuzzy_digits],
+                    'idnssoarefresh': [u'5478'],
+                    'idnssoaretry': [fuzzy_digits],
+                    'idnssoaexpire': [fuzzy_digits],
+                    'idnssoaminimum': [fuzzy_digits],
+                    'idnsallowquery': [u'!10.0.0.0/8;any;'],
+                    'idnsallowtransfer': [u'80.142.15.80;'],
+                },
+            },
+        ),
+
+
+        dict(
             desc='Delete zone %r' % dnszone1,
             command=('dnszone_del', [dnszone1], {}),
             expected={
-- 
1.7.7.6

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

Reply via email to