URL: https://github.com/freeipa/freeipa/pull/573
Author: martbab
 Title: #573: Provide centralized management of user short name resolution
Action: opened

PR body:
"""
This PR implement an initial version of AD user short name resolution
infrastructure consumable by SSSD.[1]

Most of the stuff described in the design page[2] is in-place except of hooks
that would refresh the domain resolution orders after trust domain removal or
disablement. I would like to do them in a separate PR.

Also some edge cases like specifying only separator (':') or an empty domain
('dom1::dom2') have no special treatment, the current code will just complain
about empty DNS labels. Should I improve this behavior?

[1] https://pagure.io/freeipa/issue/6372
[2] https://www.freeipa.org/page/V4/AD_User_Short_Names
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/573/head:pr573
git checkout pr573
From 5e9291aaf7dfd92c5983f0bcd80976b1f597ac58 Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Thu, 9 Mar 2017 14:24:21 +0100
Subject: [PATCH 1/4] Short name resolution: introduce the required schema

Add ipaDomainResolutionOrder and ipaNameResolutionData to IPAv3 schema.
Extend ipaConfig object with ipaNameResolutionData objectclass during
update.

https://pagure.io/freeipa/issue/6372
---
 install/share/60basev3.ldif         | 2 ++
 install/updates/50-ipaconfig.update | 1 +
 2 files changed, 3 insertions(+)

diff --git a/install/share/60basev3.ldif b/install/share/60basev3.ldif
index 059174b..efc6c8a 100644
--- a/install/share/60basev3.ldif
+++ b/install/share/60basev3.ldif
@@ -57,6 +57,7 @@ attributeTypes: (2.16.840.1.113730.3.8.11.65 NAME 'ipaWrappingMech' DESC 'PKCS#1
 attributeTypes: (2.16.840.1.113730.3.8.11.70 NAME 'ipaPermTargetTo' DESC 'Destination location to move an entry IPA permission ACI' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'IPA v4.0' )
 attributeTypes: (2.16.840.1.113730.3.8.11.71 NAME 'ipaPermTargetFrom' DESC 'Source location from where moving an entry IPA permission ACI' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'IPA v4.0' )
 attributeTypes: ( 2.16.840.1.113730.3.8.11.75 NAME 'ipaNTAdditionalSuffixes' DESC 'Suffix for the user principal name associated with the domain' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15)
+attributeTypes: (2.16.840.1.113730.3.8.11.77 NAME 'ipaDomainResolutionOrder' DESC 'List of domains used to resolve a short name' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE X-ORIGIN 'IPA v4.5')
 attributeTypes: (2.16.840.1.113730.3.8.18.2.1 NAME 'ipaVaultType' DESC 'IPA vault type' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.2')
 attributeTypes: (2.16.840.1.113730.3.8.18.2.2 NAME 'ipaVaultSalt' DESC 'IPA vault salt' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 X-ORIGIN 'IPA v4.2' )
 # FIXME: https://bugzilla.redhat.com/show_bug.cgi?id=1267782
@@ -84,5 +85,6 @@ objectClasses: (2.16.840.1.113730.3.8.12.24 NAME 'ipaPublicKeyObject' DESC 'Wrap
 objectClasses: (2.16.840.1.113730.3.8.12.25 NAME 'ipaPrivateKeyObject' DESC 'Wrapped private keys' SUP top AUXILIARY MUST ( ipaPrivateKey $ ipaWrappingKey $ ipaWrappingMech ) X-ORIGIN 'IPA v4.1' )
 objectClasses: (2.16.840.1.113730.3.8.12.26 NAME 'ipaSecretKeyObject' DESC 'Wrapped secret keys' SUP top AUXILIARY MUST ( ipaSecretKey $ ipaWrappingKey $ ipaWrappingMech ) X-ORIGIN 'IPA v4.1' )
 objectClasses: (2.16.840.1.113730.3.8.12.34 NAME 'ipaSecretKeyRefObject' DESC 'Indirect storage for encoded key material' SUP top AUXILIARY MUST ( ipaSecretKeyRef ) X-ORIGIN 'IPA v4.1' )
+objectClasses: (2.16.840.1.113730.3.8.12.39 NAME 'ipaNameResolutionData' DESC 'Data used to resolve short names to fully-qualified form' SUP top AUXILIARY MAY ( ipaDomainResolutionOrder ) X-ORIGIN 'IPA v4.5')
 objectClasses: (2.16.840.1.113730.3.8.18.1.1 NAME 'ipaVault' DESC 'IPA vault' SUP top STRUCTURAL MUST ( cn ) MAY ( description $ ipaVaultType $ ipaVaultSalt $ ipaVaultPublicKey $ owner $ member ) X-ORIGIN 'IPA v4.2' )
 objectClasses: (2.16.840.1.113730.3.8.18.1.2 NAME 'ipaVaultContainer' DESC 'IPA vault container' SUP top STRUCTURAL MUST ( cn ) MAY ( description $ owner ) X-ORIGIN 'IPA v4.2' )
diff --git a/install/updates/50-ipaconfig.update b/install/updates/50-ipaconfig.update
index 89a1726..23d2919 100644
--- a/install/updates/50-ipaconfig.update
+++ b/install/updates/50-ipaconfig.update
@@ -4,3 +4,4 @@ add:ipaSELinuxUserMapDefault: unconfined_u:s0-s0:c0.c1023
 add:ipaUserObjectClasses: ipasshuser
 remove:ipaConfigString:AllowLMhash
 add:objectClass: ipaUserAuthTypeClass
+add:objectClass: ipaNameResolutionData

From 734025316099a45e0a353dc7778704a1a3268ad7 Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Thu, 9 Mar 2017 16:37:22 +0100
Subject: [PATCH 2/4] new infrastructure handling domainresolutionorder
 attribute

It might seem excessive but it utilizes standard framework mechanisms
for encoding/decoding LDAP attribute and parameter validation

https://pagure.io/freeipa/issue/6372
---
 ipalib/parameters.py   |  30 +++++++++++
 ipalib/rpc.py          |   4 +-
 ipapython/ipaldap.py   |   7 ++-
 ipapython/ldapattrs.py | 135 +++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 173 insertions(+), 3 deletions(-)
 create mode 100644 ipapython/ldapattrs.py

diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index 7fbe63e..7a5cfe4 100644
--- a/ipalib/parameters.py
+++ b/ipalib/parameters.py
@@ -118,6 +118,7 @@
 from ipalib.text import Gettext, FixMe
 from ipalib.util import json_serialize, validate_idna_domain
 from ipapython import kerberos
+from ipapython import ldapattr
 from ipapython.dn import DN
 from ipapython.dnsutil import DNSName
 
@@ -2009,3 +2010,32 @@ def _rule_require_service(self, _, value):
                 name=self.get_param_name(),
                 error=_("Service principal is required")
             )
+
+
+class SymbolDelimitedIdentifiers(Param):
+    """
+    List of identifiers separater by a character
+    """
+    type = ldapattrs.SymbolDelimitedIdentifiers
+    type_error = _('Must be unicode string of identifiers delimited by '
+                   'separator')
+    kwargs = Param.kwargs + (
+        ('separator', unicode, u' '),
+    )
+
+    @property
+    def allowed_types(self):
+        return (self.type, unicode)
+
+    def _convert_scalar(self, value, index=None):
+        if isinstance(value, unicode):
+            try:
+                value = ldapattrs.SymbolDelimitedIdentifiers(
+                    value, self.separator)  # pylint: disable=no-member
+            except TypeError:
+                raise ConversionError(
+                    error=_(
+                        "Malformed attribute value: %(value)s"
+                        % dict(value=value)))
+            return super(
+                SymbolDelimitedIdentifiers, self)._convert_scalar(value)
diff --git a/ipalib/rpc.py b/ipalib/rpc.py
index 3a589cb..3daf0b2 100644
--- a/ipalib/rpc.py
+++ b/ipalib/rpc.py
@@ -66,6 +66,7 @@
                              KRB5_REALM_CANT_RESOLVE, KRB5_CC_NOTFOUND, get_principal
 from ipapython.dn import DN
 from ipapython.kerberos import Principal
+from ipapython.ldapattrs import SymbolDelimitedIdentifiers
 from ipalib.capabilities import VERSION_WITHOUT_CAPABILITIES
 from ipalib import api
 
@@ -178,7 +179,7 @@ def xml_wrap(value, version):
         else:
             return unicode(value)
 
-    if isinstance(value, Principal):
+    if isinstance(value, (Principal, SymbolDelimitedIdentifiers)):
         return unicode(value)
 
     assert type(value) in (unicode, float, bool, type(None)) + six.integer_types
@@ -302,6 +303,7 @@ def __init__(self, version, _identity=_identity):
             Decimal: unicode,
             DN: str,
             Principal: unicode,
+            SymbolDelimitedIdentifiers: unicode,
             DNSName: self._enc_dnsname,
             datetime.datetime: self._enc_datetime,
             bytes: self._enc_bytes,
diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index 1b0aadd..e0f525b 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -48,6 +48,8 @@
 from ipapython.dn import DN
 from ipapython.dnsutil import DNSName
 from ipapython.kerberos import Principal
+from ipapython.ldapattrs import (
+    ColonDelimitedIdentifiers, SymbolDelimitedIdentifiers)
 
 if six.PY3:
     unicode = str
@@ -672,6 +674,7 @@ class LDAPClient(object):
         'nsds5replicalastupdateend': unicode,
         'nsds5replicalastinitstart': unicode,
         'nsds5replicalastinitend': unicode,
+        'ipadomainresolutionorder': ColonDelimitedIdentifiers
     })
     _SINGLE_VALUE_OVERRIDE = CIDict({
         'nsslapd-ssl-check-hostname': True,
@@ -851,7 +854,7 @@ def encode(self, val):
             else:
                 return 'FALSE'
         elif isinstance(val, (unicode, six.integer_types, Decimal, DN,
-                              Principal)):
+                              Principal, SymbolDelimitedIdentifiers)):
             return six.text_type(val).encode('utf-8')
         elif isinstance(val, DNSName):
             return val.to_text().encode('ascii')
@@ -888,7 +891,7 @@ def decode(self, val, attr):
                         val.decode('utf-8'), LDAP_GENERALIZED_TIME_FORMAT)
                 elif target_type is DNSName:
                     return DNSName.from_text(val.decode('utf-8'))
-                elif target_type in (DN, Principal):
+                elif target_type in (DN, Principal, ColonDelimitedIdentifiers):
                     return target_type(val.decode('utf-8'))
                 else:
                     return target_type(val)
diff --git a/ipapython/ldapattrs.py b/ipapython/ldapattrs.py
new file mode 100644
index 0000000..8fb2484
--- /dev/null
+++ b/ipapython/ldapattrs.py
@@ -0,0 +1,135 @@
+#
+# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
+#
+
+"""
+classes implementing a more object-oriented handling of selected LDAP
+attributes in the framework
+"""
+
+from __future__ import print_function, unicode_literals
+
+import six
+
+_text_type = six.text_type
+_binary_type = six.binary_type
+
+
+# FIXME: This class de-facto implements the MutableSequence API
+# However we can not directly subclass from it because it then breaks some
+# assumptions in Param initialization that rely on 'type' being the metaclass
+# of all param types
+class SymbolDelimitedIdentifiers(object):
+    """
+    An abstraction of a single-value attribute comprising of identifiers
+    separated by an arbitrary symbol. Can be used as a list-like object:
+        >>> example = SymbolDelimitedIdentifiers('val1:val2:val3', u':')
+        >>> example[0] == 'val1'
+        True
+        >>> example[-1] == 'val3'
+        True
+        >>> for e in example: print(e)
+        val1
+        val2
+        val3
+
+    calling `str()` on the object reconstructs the original stringy
+    representation:
+        >>> str(example)
+        'val1:val2:val3'
+
+    Keep in mind that you can set the value of a single identifier, but be
+    careful to use only unicode literals, otherwise you will get an exception
+        >>> example[0] = 'val0'
+        >>> str(example)
+        'val0:val2:val3'
+        >>> example[0] = b'val1'  # doctest: +IGNORE_EXCEPTION_DETAIL
+        Traceback (most recent call last):
+            ...
+        TypeError: Expected <class 'str'>, got <class 'bytes'>
+
+    The same applies when instantiating from a non-unicode string or separator
+
+    >>> example2 = SymbolDelimitedIdentifiers(
+    ...     b'val1:val2:val3', ':') # doctest: +IGNORE_EXCEPTION_DETAIL
+    Traceback (most recent call last):
+        ...
+    TypeError: Expected <class 'str'>, got <class 'bytes'>
+
+    # doctest: +IGNORE_EXCEPTION_DETAIL
+    >>> example3 = SymbolDelimitedIdentifiers(
+    ...     'val1:val2:val3', b':') # doctest: +IGNORE_EXCEPTION_DETAIL
+    Traceback (most recent call last):
+        ...
+    TypeError: Separator must be <class 'str'>, got <class 'bytes'>
+
+    :param value: the attribute value, may be Unicode string or an iterable
+    :param separator: a character that will be used to split the string value
+        into identifiers or concatenate identifier when coerced to textual
+        representation
+    """
+    def __init__(self, value, separator):
+        if isinstance(value, _text_type):
+            self.components = self._from_text(value, separator)
+        elif isinstance(value, _binary_type):
+            raise TypeError(
+                "Value cannot be constructed from bytes: {!r}".format(value))
+        else:
+            self.components = list(value)
+
+        if not isinstance(separator, six.text_type):
+            raise TypeError(
+                "Separator must be {!r}, got {!r}".format(
+                    _text_type,
+                    type(separator)))
+
+        self.separator = separator
+
+    def _from_text(self, unicode_string, separator):
+        return unicode_string.split(separator)
+
+    def __repr__(self):
+        return u'{0.__module__}.{0.__name__}({1})'.format(
+            self.__class__, self)
+
+    def __str__(self):
+        return self.separator.join(self.components)
+
+    def __getitem__(self, index):
+        return self.components[index]
+
+    def __setitem__(self, index, value):
+        if not isinstance(value, _text_type):
+            raise TypeError(
+                "Expected {!r}, got {!r}".format(_text_type, type(value)))
+
+        self.components[index] = value
+
+    def __delitem__(self, index):
+        del self.components[index]
+
+    def insert(self, index, value):
+        if not isinstance(value, _text_type):
+            raise TypeError(
+                "Expected {!r}, got {!r}".format(_text_type, type(value)))
+
+        self.components.insert(index, value)
+
+    def __contains__(self, value):
+        return value in self.components
+
+    def __len__(self):
+        return len(self.components)
+
+    def __iter__(self):
+        for component in self.components:
+            yield component
+
+
+class ColonDelimitedIdentifiers(SymbolDelimitedIdentifiers):
+    """
+    More specific class for handling colon-separated values. For use in ipaldap
+    for encoding/decoding ipadomainresolutionorder
+    """
+    def __init__(self, value):
+        super(ColonDelimitedIdentifiers, self).__init__(value, ':')

From bf214d78e7ddbd9eca6b43a29ff90aded704d6a2 Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Thu, 9 Mar 2017 18:14:52 +0100
Subject: [PATCH 3/4] ipaconfig: add the ability to manipulate domain
 resolution order

optional attribute was added to config object along with validator that
check for valid domain names and also checks whether the specified
domains exist in FreeIPA or in trusted forests and, in case of trusted
domains, are not disabled.

Part of http://www.freeipa.org/page/V4/AD_User_Short_Names

https://pagure.io/freeipa/issue/6372
---
 API.txt                     |  3 +-
 VERSION.m4                  |  4 +--
 ipalib/parameters.py        |  2 +-
 ipaserver/plugins/config.py | 69 ++++++++++++++++++++++++++++++++++++++++++++-
 4 files changed, 73 insertions(+), 5 deletions(-)

diff --git a/API.txt b/API.txt
index 90cda74..5befb3b 100644
--- a/API.txt
+++ b/API.txt
@@ -1059,7 +1059,7 @@ args: 0,1,1
 option: Str('version?')
 output: Output('result')
 command: config_mod/1
-args: 0,26,3
+args: 0,27,3
 option: Str('addattr*', cli_name='addattr')
 option: Flag('all', autofill=True, cli_name='all', default=False)
 option: Str('ca_renewal_master_server?', autofill=False)
@@ -1068,6 +1068,7 @@ option: StrEnum('ipaconfigstring*', autofill=False, cli_name='ipaconfigstring',
 option: Str('ipadefaultemaildomain?', autofill=False, cli_name='emaildomain')
 option: Str('ipadefaultloginshell?', autofill=False, cli_name='defaultshell')
 option: Str('ipadefaultprimarygroup?', autofill=False, cli_name='defaultgroup')
+option: SymbolDelimitedIdentifiers('ipadomainresolutionorder?', autofill=False, cli_name='domain_resolution_order', separator=u':')
 option: Str('ipagroupobjectclasses*', autofill=False, cli_name='groupobjectclasses')
 option: IA5Str('ipagroupsearchfields?', autofill=False, cli_name='groupsearch')
 option: IA5Str('ipahomesrootdir?', autofill=False, cli_name='homedirectory')
diff --git a/VERSION.m4 b/VERSION.m4
index f943566..4ea8976 100644
--- a/VERSION.m4
+++ b/VERSION.m4
@@ -73,8 +73,8 @@ define(IPA_DATA_VERSION, 20100614120000)
 #                                                      #
 ########################################################
 define(IPA_API_VERSION_MAJOR, 2)
-define(IPA_API_VERSION_MINOR, 220)
-# Last change: Add whoami command
+define(IPA_API_VERSION_MINOR, 221)
+# Last change: Add domain resolution order to ipaconfig
 
 
 ########################################################
diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index 7a5cfe4..2941daf 100644
--- a/ipalib/parameters.py
+++ b/ipalib/parameters.py
@@ -118,7 +118,7 @@
 from ipalib.text import Gettext, FixMe
 from ipalib.util import json_serialize, validate_idna_domain
 from ipapython import kerberos
-from ipapython import ldapattr
+from ipapython import ldapattrs
 from ipapython.dn import DN
 from ipapython.dnsutil import DNSName
 
diff --git a/ipaserver/plugins/config.py b/ipaserver/plugins/config.py
index 5d57465..4e148ec 100644
--- a/ipaserver/plugins/config.py
+++ b/ipaserver/plugins/config.py
@@ -22,6 +22,8 @@
 from ipalib import Bool, Int, Str, IA5Str, StrEnum, DNParam
 from ipalib import errors
 from ipalib.plugable import Registry
+from ipalib.parameters import SymbolDelimitedIdentifiers
+from ipalib.util import validate_domain_name
 from .baseldap import (
     LDAPObject,
     LDAPUpdate,
@@ -82,6 +84,7 @@
 
 register = Registry()
 
+
 @register()
 class config(LDAPObject):
     """
@@ -95,7 +98,7 @@ class config(LDAPObject):
         'ipamigrationenabled', 'ipacertificatesubjectbase',
         'ipapwdexpadvnotify', 'ipaselinuxusermaporder',
         'ipaselinuxusermapdefault', 'ipaconfigstring', 'ipakrbauthzdata',
-        'ipauserauthtype'
+        'ipauserauthtype', 'ipadomainresolutionorder'
     ]
     container_dn = DN(('cn', 'ipaconfig'), ('cn', 'etc'))
     permission_filter_objectclasses = ['ipaguiconfig']
@@ -250,6 +253,14 @@ class config(LDAPObject):
             label=_('IPA CA renewal master'),
             doc=_('Renewal master for IPA certificate authority'),
             flags={'virtual_attribute', 'no_create'}
+        ),
+        SymbolDelimitedIdentifiers(
+            'ipadomainresolutionorder?',
+            cli_name='domain_resolution_order',
+            label=_('Domain resolution order'),
+            doc=_('colon-separated list of domains used for short name'
+                  ' qualification'),
+            separator=u':'
         )
     )
 
@@ -266,6 +277,60 @@ def show_servroles_attributes(self, entry_attrs, **options):
             config = backend.config_retrieve(role)
             entry_attrs.update(config)
 
+    def _gather_trusted_domains(self):
+        command = self.api.Command
+        try:
+            ad_forests = command.trust_find()['result']
+        except errors.NotFound:
+            return {}
+
+        trusted_domains = {}
+        for forest_name in [a['cn'][0] for a in ad_forests]:
+            forest_domains = command.trustdomain_find(forest_name)['result']
+
+            trusted_domains.update(
+                {
+                    dom['cn'][0]: dom['domain_enabled'][0]
+                    for dom in forest_domains if 'domain_enabled' in dom
+                }
+            )
+
+        return trusted_domains
+
+    def _validate_single_domain(self, attr_name, domain, trusted_domains):
+        try:
+            validate_domain_name(domain)
+        except ValueError as e:
+            raise errors.ValidationError(
+                name=attr_name,
+                error=_("Invalid domain name %(domain)s: %(e)s")
+                % dict(domain=domain, e=e))
+
+        if domain in trusted_domains:
+            if not trusted_domains[domain]:
+                raise errors.ValidationError(
+                    name=attr_name,
+                    error=_('Domain %(domain)s is disabled')
+                    % dict(domain=domain)
+                )
+        else:
+            raise errors.NotFound(
+                reason=_('No such domain: %(domain)s') % dict(domain=domain)
+            )
+
+    def validate_domain_resolution_order(self, entry_attrs):
+        attr_name = 'ipadomainresolutionorder'
+        if attr_name not in entry_attrs:
+            return
+
+        available_domains = self._gather_trusted_domains()
+
+        # add FreeIPA domain to the list of domains. This one is always enabled
+        available_domains.update({self.api.env.domain: True})
+
+        for domain in entry_attrs[attr_name]:
+            self._validate_single_domain(attr_name, domain, available_domains)
+
 
 @register()
 class config_mod(LDAPUpdate):
@@ -396,6 +461,8 @@ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
             backend = self.api.Backend.serverroles
             backend.config_update(ca_renewal_master_server=new_master)
 
+        self.obj.validate_domain_resolution_order(entry_attrs)
+
         return dn
 
     def exc_callback(self, keys, options, exc, call_func,

From 25667167c76b2be2dd0be9bcc2db339fa24d2ccc Mon Sep 17 00:00:00 2001
From: Martin Babinsky <mbabi...@redhat.com>
Date: Thu, 9 Mar 2017 19:02:49 +0100
Subject: [PATCH 4/4] idview: add domain_resolution_order attribute

`idview-add` and `idview-mod` can now set and validate the attribute.
The required objectclass is added on-demand after modification

https://pagure.io/freeipa/issue/6372
---
 API.txt                      |  6 ++++--
 VERSION.m4                   |  4 ++--
 ipaserver/plugins/idviews.py | 31 ++++++++++++++++++++++++++++++-
 3 files changed, 36 insertions(+), 5 deletions(-)

diff --git a/API.txt b/API.txt
index 5befb3b..d6b3dde 100644
--- a/API.txt
+++ b/API.txt
@@ -3036,11 +3036,12 @@ output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
 command: idview_add/1
-args: 1,6,3
+args: 1,7,3
 arg: Str('cn', cli_name='name')
 option: Str('addattr*', cli_name='addattr')
 option: Flag('all', autofill=True, cli_name='all', default=False)
 option: Str('description?', cli_name='desc')
+option: SymbolDelimitedIdentifiers('ipadomainresolutionorder?', cli_name='domain_resolution_order', separator=u':')
 option: Flag('raw', autofill=True, cli_name='raw', default=False)
 option: Str('setattr*', cli_name='setattr')
 option: Str('version?')
@@ -3081,12 +3082,13 @@ output: ListOfEntries('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: Output('truncated', type=[<type 'bool'>])
 command: idview_mod/1
-args: 1,9,3
+args: 1,10,3
 arg: Str('cn', cli_name='name')
 option: Str('addattr*', cli_name='addattr')
 option: Flag('all', autofill=True, cli_name='all', default=False)
 option: Str('delattr*', cli_name='delattr')
 option: Str('description?', autofill=False, cli_name='desc')
+option: SymbolDelimitedIdentifiers('ipadomainresolutionorder?', autofill=False, cli_name='domain_resolution_order', separator=u':')
 option: Flag('raw', autofill=True, cli_name='raw', default=False)
 option: Str('rename?', cli_name='rename')
 option: Flag('rights', autofill=True, default=False)
diff --git a/VERSION.m4 b/VERSION.m4
index 4ea8976..685271a 100644
--- a/VERSION.m4
+++ b/VERSION.m4
@@ -73,8 +73,8 @@ define(IPA_DATA_VERSION, 20100614120000)
 #                                                      #
 ########################################################
 define(IPA_API_VERSION_MAJOR, 2)
-define(IPA_API_VERSION_MINOR, 221)
-# Last change: Add domain resolution order to ipaconfig
+define(IPA_API_VERSION_MINOR, 222)
+# Last change: Add domain resolution order to ID views
 
 
 ########################################################
diff --git a/ipaserver/plugins/idviews.py b/ipaserver/plugins/idviews.py
index b38a4ad..314224e 100644
--- a/ipaserver/plugins/idviews.py
+++ b/ipaserver/plugins/idviews.py
@@ -35,6 +35,7 @@
     PATTERN_GROUPUSER_NAME,
 )
 from ipalib.plugable import Registry
+from ipalib.parameters import SymbolDelimitedIdentifiers
 from ipalib.util import (normalize_sshpubkey, validate_sshpubkey,
     convert_sshpubkey_post)
 
@@ -94,7 +95,7 @@ class idview(LDAPObject):
     container_dn = api.env.container_views
     object_name = _('ID View')
     object_name_plural = _('ID Views')
-    object_class = ['ipaIDView', 'top']
+    object_class = ['ipaIDView', 'ipaNameResolutionData', 'top']
     default_attributes = ['cn', 'description']
     rdn_is_primary_key = True
 
@@ -123,6 +124,15 @@ class idview(LDAPObject):
             label=_('Hosts the view applies to'),
             flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
         ),
+        SymbolDelimitedIdentifiers(
+            'ipadomainresolutionorder?',
+            cli_name='domain_resolution_order',
+            label=_('Domain resolution order'),
+            doc=_('colon-separated list of domains used for short name'
+                  ' qualification'),
+            separator=u':',
+            flags={'no_search'}
+        )
     )
 
     permission_filter_objectclasses = ['nsContainer']
@@ -136,12 +146,29 @@ class idview(LDAPObject):
         },
     }
 
+    def handle_domain_resolution_order(self, entry_attrs):
+        attr_name = 'ipadomainresolutionorder'
+        obj_class_name = 'ipaNameResolutionData'
+
+        if attr_name not in entry_attrs:
+            return
+
+        self.api.Object.config.validate_domain_resolution_order(entry_attrs)
+
+        objectclasses = {o.lower() for o in entry_attrs.get('objectclass', [])}
+
+        if obj_class_name.lower() not in objectclasses:
+            entry_attrs['objectclass'].append(obj_class_name)
+
 
 @register()
 class idview_add(LDAPCreate):
     __doc__ = _('Add a new ID View.')
     msg_summary = _('Added ID View "%(value)s"')
 
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        self.obj.handle_domain_resolution_order(entry_attrs)
+
 
 @register()
 class idview_del(LDAPDelete):
@@ -166,6 +193,8 @@ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
             if key.lower() == DEFAULT_TRUST_VIEW_NAME:
                 raise protected_default_trust_view_error
 
+        self.obj.handle_domain_resolution_order(entry_attrs)
+
         return dn
 
 
-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to