From 49068ade92b3fa4874b3107923bbd5b84e1a04f3 Mon Sep 17 00:00:00 2001 From: Martin Basti Date: Thu, 22 May 2014 15:01:27 +0200 Subject: Separate master and forward DNS zones Forward zones are stored in idnsforwadzone objectclasses. design: http://www.freeipa.org/page/V4/Forward_zones Ticket: https://fedorahosted.org/freeipa/ticket/3210 Reviewed-By: Petr Vobornik --- API.txt | 94 +++++++++++++ VERSION | 4 +- install/share/60ipadns.ldif | 1 + ipalib/plugins/dns.py | 328 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 425 insertions(+), 2 deletions(-) diff --git a/API.txt b/API.txt index 30974d92e..858908878 100644 --- a/API.txt +++ b/API.txt @@ -704,6 +704,100 @@ option: Str('version?', exclude='webui') output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) output: Output('summary', (, ), None) output: PrimaryKey('value', None, None) +command: dnsforwardzone_add +args: 1,8,3 +arg: DNSNameParam('idnsname', attribute=True, cli_name='name', multivalue=False, only_absolute=True, primary_key=True, required=True) +option: Str('addattr*', cli_name='addattr', exclude='webui') +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') +option: Str('idnsforwarders', attribute=True, cli_name='forwarder', csv=True, multivalue=True, required=False) +option: StrEnum('idnsforwardpolicy', attribute=True, cli_name='forward_policy', multivalue=False, required=False, values=(u'only', u'first', u'none')) +option: Str('name_from_ip', attribute=False, cli_name='name_from_ip', multivalue=False, required=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') +option: Str('setattr*', cli_name='setattr', exclude='webui') +option: Str('version?', exclude='webui') +output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: PrimaryKey('value', None, None) +command: dnsforwardzone_add_permission +args: 1,1,3 +arg: DNSNameParam('idnsname', attribute=True, cli_name='name', multivalue=False, only_absolute=True, primary_key=True, query=True, required=True) +option: Str('version?', exclude='webui') +output: Output('result', , None) +output: Output('summary', (, ), None) +output: Output('value', , None) +command: dnsforwardzone_del +args: 1,2,3 +arg: DNSNameParam('idnsname', attribute=True, cli_name='name', multivalue=True, only_absolute=True, primary_key=True, query=True, required=True) +option: Flag('continue', autofill=True, cli_name='continue', default=False) +option: Str('version?', exclude='webui') +output: Output('result', , None) +output: Output('summary', (, ), None) +output: ListOfPrimaryKeys('value', None, None) +command: dnsforwardzone_disable +args: 1,1,3 +arg: DNSNameParam('idnsname', attribute=True, cli_name='name', multivalue=False, only_absolute=True, primary_key=True, query=True, required=True) +option: Str('version?', exclude='webui') +output: Output('result', , None) +output: Output('summary', (, ), None) +output: PrimaryKey('value', None, None) +command: dnsforwardzone_enable +args: 1,1,3 +arg: DNSNameParam('idnsname', attribute=True, cli_name='name', multivalue=False, only_absolute=True, primary_key=True, query=True, required=True) +option: Str('version?', exclude='webui') +output: Output('result', , None) +output: Output('summary', (, ), None) +output: PrimaryKey('value', None, None) +command: dnsforwardzone_find +args: 1,11,4 +arg: Str('criteria?', noextrawhitespace=False) +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') +option: Str('idnsforwarders', attribute=True, autofill=False, cli_name='forwarder', csv=True, multivalue=True, query=True, required=False) +option: StrEnum('idnsforwardpolicy', attribute=True, autofill=False, cli_name='forward_policy', multivalue=False, query=True, required=False, values=(u'only', u'first', u'none')) +option: DNSNameParam('idnsname', attribute=True, autofill=False, cli_name='name', multivalue=False, only_absolute=True, primary_key=True, query=True, required=False) +option: Bool('idnszoneactive', attribute=True, autofill=False, cli_name='zone_active', multivalue=False, query=True, required=False) +option: Str('name_from_ip', attribute=False, autofill=False, cli_name='name_from_ip', multivalue=False, query=True, required=False) +option: Flag('pkey_only?', autofill=True, default=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') +option: Int('sizelimit?', autofill=False, minvalue=0) +option: Int('timelimit?', autofill=False, minvalue=0) +option: Str('version?', exclude='webui') +output: Output('count', , None) +output: ListOfEntries('result', (, ), Gettext('A list of LDAP entries', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: Output('truncated', , None) +command: dnsforwardzone_mod +args: 1,10,3 +arg: DNSNameParam('idnsname', attribute=True, cli_name='name', multivalue=False, only_absolute=True, primary_key=True, query=True, required=True) +option: Str('addattr*', cli_name='addattr', exclude='webui') +option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') +option: Str('delattr*', cli_name='delattr', exclude='webui') +option: Str('idnsforwarders', attribute=True, autofill=False, cli_name='forwarder', csv=True, multivalue=True, required=False) +option: StrEnum('idnsforwardpolicy', attribute=True, autofill=False, cli_name='forward_policy', multivalue=False, required=False, values=(u'only', u'first', u'none')) +option: Str('name_from_ip', attribute=False, autofill=False, cli_name='name_from_ip', multivalue=False, required=False) +option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') +option: Flag('rights', autofill=True, default=False) +option: Str('setattr*', cli_name='setattr', exclude='webui') +option: Str('version?', exclude='webui') +output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: PrimaryKey('value', None, None) +command: dnsforwardzone_remove_permission +args: 1,1,3 +arg: DNSNameParam('idnsname', attribute=True, cli_name='name', multivalue=False, only_absolute=True, primary_key=True, query=True, required=True) +option: Str('version?', exclude='webui') +output: Output('result', , None) +output: Output('summary', (, ), None) +output: Output('value', , None) +command: dnsforwardzone_show +args: 1,4,3 +arg: DNSNameParam('idnsname', attribute=True, cli_name='name', multivalue=False, only_absolute=True, 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: Flag('rights', autofill=True, default=False) +option: Str('version?', exclude='webui') +output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) +output: Output('summary', (, ), None) +output: PrimaryKey('value', None, None) command: dnsrecord_add args: 2,116,3 arg: DNSNameParam('dnszoneidnsname', cli_name='dnszone', multivalue=False, only_absolute=True, primary_key=True, query=True, required=True) diff --git a/VERSION b/VERSION index 318b88a15..d2a367353 100644 --- a/VERSION +++ b/VERSION @@ -89,5 +89,5 @@ IPA_DATA_VERSION=20100614120000 # # ######################################################## IPA_API_VERSION_MAJOR=2 -IPA_API_VERSION_MINOR=89 -# Last change: npmccallum - Add support for managedBy to tokens +IPA_API_VERSION_MINOR=90 +# Last change: mbasti - Add dnsforwardzone-* commands diff --git a/install/share/60ipadns.ldif b/install/share/60ipadns.ldif index aaa1b3110..724524ea7 100644 --- a/install/share/60ipadns.ldif +++ b/install/share/60ipadns.ldif @@ -54,3 +54,4 @@ objectClasses: ( 2.16.840.1.113730.3.8.6.0 NAME 'idnsRecord' DESC 'dns Record, u 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 $ idnsAllowQuery $ idnsAllowTransfer $ idnsAllowSyncPTR $ idnsForwardPolicy $ idnsForwarders $ idnsSecInlineSigning ) ) 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' ) +objectClasses: ( 2.16.840.1.113730.3.8.6.3 NAME 'idnsForwardZone' DESC 'Forward Zone class' SUP top STRUCTURAL MUST ( idnsName $ idnsZoneActive ) MAY ( idnsForwarders $ idnsForwardPolicy ) ) diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py index b149f1f07..ead77ca9c 100644 --- a/ipalib/plugins/dns.py +++ b/ipalib/plugins/dns.py @@ -1843,6 +1843,7 @@ class dnszone(LDAPObject): doc=_('Allow inline DNSSEC signing of records in the zone'), ), ) + # Permissions will be apllied for forwardzones too managed_permissions = { 'System: Add DNS Entries': { 'non_object': True, @@ -3591,3 +3592,330 @@ class dnsconfig_show(LDAPRetrieve): self.obj.postprocess_result(result) return result + +@register() +class dnsforwardzone(LDAPObject): + """ + DNS Forward zone, container for resource records. + """ + container_dn = api.env.container_dns + object_name = _('DNS forward zone') + object_name_plural = _('DNS forward zones') + object_class = ['top', 'idnsforwardzone'] + possible_objectclasses = ['ipadnszone'] + default_attributes = [ + 'idnsname', 'idnszoneactive', 'idnsforwarders', 'idnsforwardpolicy' + ] + label = _('DNS Forward Zones') + label_singular = _('DNS Forward Zone') + default_forward_policy = u'first' + + takes_params = ( + DNSNameParam('idnsname', + only_absolute=True, + cli_name='name', + label=_('Forward zone name'), + doc=_('Forward zone name (FQDN)'), + default_from=lambda name_from_ip: _reverse_zone_name(name_from_ip), + primary_key=True, + ), + Str('name_from_ip?', _validate_ipnet, + label=_('Reverse zone IP network'), + doc=_('IP network to create reverse zone name from'), + flags=('virtual_attribute',), + ), + Bool('idnszoneactive?', + cli_name='zone_active', + label=_('Active zone'), + doc=_('Is zone active?'), + flags=['no_create', 'no_update'], + attribute=True, + ), + Str('idnsforwarders*', + _validate_bind_forwarder, + cli_name='forwarder', + label=_('Zone forwarders'), + doc=_('Per-zone forwarders. A custom port can be specified ' + 'for each forwarder using a standard format "IP_ADDRESS port PORT"'), + csv=True, + ), + StrEnum('idnsforwardpolicy?', + cli_name='forward_policy', + label=_('Forward policy'), + doc=_('Per-zone conditional forwarding policy. Set to "none" to ' + 'disable forwarding.'), + values=(u'only', u'first', u'none'), + ), + + ) + # managed_permissions: permissions was apllied in dnszone class, do NOT + # add them here, they should not be applied twice. + + def get_dn(self, *keys, **options): + zone = keys[-1] + assert isinstance(zone, DNSName) + assert zone.is_absolute() + zone = zone.ToASCII() + + # try first relative name, a new zone has to be added as absolute + # otherwise ObjectViolation is raised + zone = zone[:-1] + dn = super(dnsforwardzone, self).get_dn(zone, **options) + try: + self.backend.get_entry(dn, ['']) + except errors.NotFound: + zone = u"%s." % zone + dn = super(dnsforwardzone, self).get_dn(zone, **options) + + return dn + + def permission_name(self, zone): + assert isinstance(zone, DNSName) + return u"Manage DNS zone %s" % zone.ToASCII() + + +@register() +class dnsforwardzone_add(LDAPCreate): + __doc__ = _('Create new DNS forward zone.') + + has_output_params = LDAPCreate.has_output_params + dnszone_output_params + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): + assert isinstance(dn, DN) + if not dns_container_exists(self.api.Backend.ldap2): + raise errors.NotFound(reason=_('DNS is not configured')) + + entry_attrs['idnszoneactive'] = 'TRUE' + + if 'idnsforwardpolicy' not in entry_attrs: + entry_attrs['idnsforwardpolicy'] = self.obj.default_forward_policy + + if (not entry_attrs.get('idnsforwarders') and + entry_attrs['idnsforwardpolicy'] != u'none'): + raise errors.ValidationError(name=u'idnsforwarders', + error=_('Please specify forwarders.')) + + return dn + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + assert isinstance(dn, DN) + return dn + + +@register() +class dnsforwardzone_del(LDAPDelete): + __doc__ = _('Delete DNS forward zone.') + + msg_summary = _('Deleted DNS forward zone "%(value)s"') + + 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 + + +@register() +class dnsforwardzone_mod(LDAPUpdate): + __doc__ = _('Modify DNS forward zone.') + + has_output_params = LDAPUpdate.has_output_params + dnszone_output_params + + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): + try: + entry = ldap.get_entry(dn) + except errors.NotFound: + self.obj.handle_not_found(*keys) + + policy = self.obj.default_forward_policy + forwarders = [] + + if 'idnsforwarders' in entry_attrs: + forwarders = entry_attrs['idnsforwarders'] + elif 'idnsforwarders' in entry: + forwarders = entry['idnsforwarders'] + + if 'idnsforwardpolicy' in entry_attrs: + policy = entry_attrs['idnsforwardpolicy'] + elif 'idnsforwardpolicy' in entry: + policy = entry['idnsforwardpolicy'] + + if not forwarders and policy != u'none': + raise errors.ValidationError(name=u'idnsforwarders', + error=_('Please specify forwarders.')) + + return dn + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + return dn + + +@register() +class dnsforwardzone_find(LDAPSearch): + __doc__ = _('Search for DNS forward zones.') + + has_output_params = LDAPSearch.has_output_params + dnszone_output_params + + def args_options_2_params(self, *args, **options): + # FIXME: Check that name_from_ip is valid. This is necessary because + # custom validation rules, including _validate_ipnet, are not + # used when doing a search. Once we have a parameter type for + # IP network objects, this will no longer be necessary, as the + # parameter type will handle the validation itself (see + # ). + if 'name_from_ip' in options: + self.obj.params['name_from_ip'](unicode(options['name_from_ip'])) + return super(dnsforwardzone_find, self).args_options_2_params(*args, **options) + + def args_options_2_entry(self, *args, **options): + if 'name_from_ip' in options: + if 'idnsname' not in options: + options['idnsname'] = self.obj.params['idnsname'].get_default(**options) + del options['name_from_ip'] + search_kw = super(dnsforwardzone_find, self).args_options_2_entry(*args, + **options) + name = search_kw.get('idnsname') + if name: + search_kw['idnsname'] = [name, name.relativize(DNSName.root)] + return search_kw + + def pre_callback(self, ldap, filter, attrs_list, base_dn, scope, *args, **options): + assert isinstance(base_dn, DN) + + filter = _create_idn_filter(self, ldap, *args, **options) + + return (filter, base_dn, scope) + + def post_callback(self, ldap, entries, truncated, *args, **options): + return truncated + + +@register() +class dnsforwardzone_show(LDAPRetrieve): + __doc__ = _('Display information about a DNS forward zone.') + + has_output_params = LDAPRetrieve.has_output_params + dnszone_output_params + + def post_callback(self, ldap, dn, entry_attrs, *keys, **options): + assert isinstance(dn, DN) + return dn + + +@register() +class dnsforwardzone_disable(LDAPQuery): + __doc__ = _('Disable DNS Forward Zone.') + + has_output = output.standard_value + msg_summary = _('Disabled DNS forward zone "%(value)s"') + + def execute(self, *keys, **options): + ldap = self.obj.backend + + dn = self.obj.get_dn(*keys, **options) + entry = ldap.get_entry(dn, ['idnszoneactive']) + + entry['idnszoneactive'] = ['FALSE'] + + try: + ldap.update_entry(entry) + except errors.EmptyModlist: + pass + + return dict(result=True, value=pkey_to_value(keys[-1], options)) + + +@register() +class dnsforwardzone_enable(LDAPQuery): + __doc__ = _('Enable DNS Forward Zone.') + + has_output = output.standard_value + msg_summary = _('Enabled DNS forward zone "%(value)s"') + + def execute(self, *keys, **options): + ldap = self.obj.backend + + dn = self.obj.get_dn(*keys, **options) + entry = ldap.get_entry(dn, ['idnszoneactive']) + + entry['idnszoneactive'] = ['TRUE'] + + try: + ldap.update_entry(entry) + except errors.EmptyModlist: + pass + + return dict(result=True, value=pkey_to_value(keys[-1], options)) + + +@register() +class dnsforwardzone_add_permission(LDAPQuery): + __doc__ = _('Add a permission for per-zone access delegation.') + + has_output = _output_permissions + msg_summary = _('Added system permission "%(value)s"') + + def execute(self, *keys, **options): + ldap = self.obj.backend + dn = self.obj.get_dn(*keys, **options) + + try: + 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, + ipapermissiontype=u'SYSTEM' + )['result'] + + dnszone_ocs = entry_attrs.get('objectclass') + if dnszone_ocs: + for oc in dnszone_ocs: + if oc.lower() == 'ipadnszone': + break + else: + dnszone_ocs.append('ipadnszone') + + entry_attrs['managedby'] = [permission['dn']] + ldap.update_entry(entry_attrs) + + return dict( + result=True, + value=pkey_to_value(permission_name, options), + ) + + +@register() +class dnsforwardzone_remove_permission(LDAPQuery): + __doc__ = _('Remove a permission for per-zone access delegation.') + + has_output = _output_permissions + msg_summary = _('Removed system permission "%(value)s"') + + def execute(self, *keys, **options): + ldap = self.obj.backend + dn = self.obj.get_dn(*keys, **options) + try: + entry = ldap.get_entry(dn, ['managedby']) + except errors.NotFound: + self.obj.handle_not_found(*keys) + + entry['managedby'] = None + + try: + ldap.update_entry(entry) + 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]) + api.Command['permission_del'](permission_name, force=True) + + return dict( + result=True, + value=pkey_to_value(permission_name, options), + ) -- cgit