summaryrefslogtreecommitdiffstats
path: root/ipalib/plugins/f_user.py
blob: d8bb49e2016a55611d88d85ae42bd26bd2828d30 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# Authors:
#   Jason Gerard DeRose <jderose@redhat.com>
#
# Copyright (C) 2008  Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; version 2 only
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

"""
Frontend plugins for user (Identity).
"""

from ipalib import frontend
from ipalib import crud
from ipalib.frontend import Param
from ipalib import api
from ipalib import errors
from ipalib import ipa_types

# Command to get the idea how plugins will interact with api.env
class envtest(frontend.Command):
    'Show current environment.'
    def run(self, *args, **kw):
        print ""
        print "Environment variables:"
        for var in api.env:
            val = api.env[var]
            if var is 'server':
                print ""
                print "  Servers:"
                for item in api.env.server:
                    print "    %s" % item
                print ""
            else:
                print "  %s: %s" % (var, val)
        return {}
api.register(envtest)

def display_user(user):
    # FIXME: for now delete dn here. In the future pass in the kw to
    # output_for_cli()
    attr = sorted(user.keys())
    # Always have sn following givenname
    try:
        l = attr.index('givenname')
        attr.remove('sn')
        attr.insert(l+1, 'sn')
    except ValueError:
        pass

    for a in attr:
        if a != 'dn':
            print "%s: %s" % (a, user[a])

default_attributes = ['uid','givenname','sn','homeDirectory','loginshell']


class user(frontend.Object):
    """
    User object.
    """
    takes_params = (
        Param('givenname',
            cli_name='first',
            doc='User\'s first name',
        ),
        Param('sn',
            cli_name='last',
            doc='User\'s last name',
        ),
        Param('uid',
            cli_name='user',
            primary_key=True,
            default_from=lambda givenname, sn: givenname[0] + sn,
            normalize=lambda value: value.lower(),
        ),
        Param('gecos?',
            doc='GECOS field',
            default_from=lambda uid: uid,
        ),
        Param('homedirectory?',
            cli_name='home',
            doc='User\'s home directory',
            default_from=lambda uid: '/home/%s' % uid,
        ),
        Param('loginshell?',
            cli_name='shell',
            default=u'/bin/sh',
            doc='User\'s Login shell',
        ),
        Param('krbprincipalname?', cli_name='principal',
            doc='User\'s Kerberos Principal name',
            default_from=lambda uid: '%s@%s' % (uid, api.env.realm),
        ),
        Param('mailaddress?',
            cli_name='mail',
            doc='User\'s e-mail address',
        ),
        Param('userpassword?',
            cli_name='password',
            doc='User\'s password',
        ),
        Param('groups?',
            doc='Add account to one or more groups (comma-separated)',
        ),
        Param('uidnumber?',
            cli_name='uid',
            type=ipa_types.Int(),
            doc='The uid to use for this user. If not included one is automatically set.',
        ),

    )
api.register(user)


class user_add(crud.Add):
    'Add a new user.'

    def execute(self, uid, **kw):
        """
        Execute the user-add operation.

        The dn should not be passed as a keyword argument as it is constructed
        by this method.

        Returns the entry as it will be created in LDAP.

        :param uid: The login name of the user being added.
        :param kw: Keyword arguments for the other LDAP attributes.
        """
        assert 'uid' not in kw
        assert 'dn' not in kw
        ldap = self.api.Backend.ldap
        kw['uid'] = uid
        kw['dn'] = ldap.make_user_dn(uid)

        # FIXME: enforce this elsewhere
#        if servercore.uid_too_long(kw['uid']):
#            raise errors.UsernameTooLong

        # Get our configuration
        config = ldap.get_ipa_config()

        # Let us add in some missing attributes
        if kw.get('homedirectory') is None:
            kw['homedirectory'] = '%s/%s' % (config.get('ipahomesrootdir'), kw.get('uid'))
            kw['homedirectory'] = kw['homedirectory'].replace('//', '/')
            kw['homedirectory'] = kw['homedirectory'].rstrip('/')
        if kw.get('loginshell') is None:
            kw['loginshell'] = config.get('ipadefaultloginshell')
        if kw.get('gecos') is None:
            kw['gecos'] = kw['uid']

        # If uidnumber is blank the the FDS dna_plugin will automatically
        # assign the next value. So we don't have to do anything with it.

        if not kw.get('gidnumber'):
            try:
                group_dn = ldap.find_entry_dn("cn", config.get('ipadefaultprimarygroup'))
                default_group = ldap.retrieve(group_dn, ['dn','gidNumber'])
                if default_group:
                    kw['gidnumber'] = default_group.get('gidnumber')
            except errors.NotFound:
                # Fake an LDAP error so we can return something useful to the kw
                raise errors.NotFound, "The default group for new kws, '%s', cannot be found." % config.get('ipadefaultprimarygroup')
            except Exception, e:
                # catch everything else
                raise e

        if kw.get('krbprincipalname') is None:
            kw['krbprincipalname'] = "%s@%s" % (kw.get('uid'), self.api.env.realm)

        # FIXME. This is a hack so we can request separate First and Last
        # name in the GUI.
        if kw.get('cn') is None:
            kw['cn'] = "%s %s" % (kw.get('givenname'),
                                           kw.get('sn'))

        # some required objectclasses
        kw['objectClass'] =  config.get('ipauserobjectclasses')

        return ldap.create(**kw)
    def output_for_cli(self, ret):
        """
        Output result of this command to command line interface.
        """
        if ret:
            print "User added"

api.register(user_add)


class user_del(crud.Del):
    'Delete an existing user.'
    def execute(self, uid, **kw):
        """Delete a user. Not to be confused with inactivate_user. This
           makes the entry go away completely.

           uid is the uid of the user to delete

           The memberOf plugin handles removing the user from any other
           groups.

           :param uid: The login name of the user being added.
           :param kw: Not used.
        """
        if uid == "admin":
            # FIXME: do we still want a "special" user?
            raise SyntaxError("admin required")
#            raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED)
#        logging.info("IPA: delete_user '%s'" % uid)

        ldap = self.api.Backend.ldap
        dn = ldap.find_entry_dn("uid", uid)
        return ldap.delete(dn)
    def output_for_cli(self, ret):
        """
        Output result of this command to command line interface.
        """
        if ret:
            print "User deleted"

api.register(user_del)


class user_mod(crud.Mod):
    'Edit an existing user.'
    def execute(self, uid, **kw):
        """
        Execute the user-mod operation.

        The dn should not be passed as a keyword argument as it is constructed
        by this method.

        Returns the entry

        :param uid: The login name of the user to retrieve.
        :param kw: Keyword arguments for the other LDAP attributes.
        """
        assert 'uid' not in kw
        assert 'dn' not in kw
        ldap = self.api.Backend.ldap
        dn = ldap.find_entry_dn("uid", uid)
        return ldap.update(dn, **kw)

    def output_for_cli(self, ret):
        """
        Output result of this command to command line interface.
        """
        if ret:
            print "User updated"

api.register(user_mod)


class user_find(crud.Find):
    'Search the users.'
    takes_options = (
        Param('all?', type=ipa_types.Bool(), doc='Retrieve all user attributes'),
    )
    def execute(self, term, **kw):
        ldap = self.api.Backend.ldap

        # Pull the list of searchable attributes out of the configuration.
        config = ldap.get_ipa_config()
        search_fields_conf_str = config.get('ipausersearchfields')
        search_fields = search_fields_conf_str.split(",")

        for s in search_fields:
            kw[s] = term

        object_type = ldap.get_object_type("uid")
        if object_type and not kw.get('objectclass'):
            kw['objectclass'] = object_type
        if kw.get('all', False):
            kw['attributes'] = ['*']
        else:
            kw['attributes'] = default_attributes
        return ldap.search(**kw)
    def output_for_cli(self, users):
        if not users:
            return
        counter = users[0]
        users = users[1:]
        if counter == 0:
            print "No entries found"
            return
        elif counter == -1:
            print "These results are truncated."
            print "Please refine your search and try again."

        for u in users:
            display_user(u)
            print ""
api.register(user_find)


class user_show(crud.Get):
    'Examine an existing user.'
    takes_options = (
        Param('all?', type=ipa_types.Bool(), doc='Retrieve all user attributes'),
    )
    def execute(self, uid, **kw):
        """
        Execute the user-show operation.

        The dn should not be passed as a keyword argument as it is constructed
        by this method.

        Returns the entry

        :param uid: The login name of the user to retrieve.
        :param kw: "all" set to True = return all attributes
        """
        ldap = self.api.Backend.ldap
        dn = ldap.find_entry_dn("uid", uid)
        # FIXME: should kw contain the list of attributes to display?
        if kw.get('all', False):
            return ldap.retrieve(dn)
        else:
            return ldap.retrieve(dn, default_attributes)
    def output_for_cli(self, user):
        if user:
            display_user(user)

api.register(user_show)

class user_lock(frontend.Command):
    'Lock a user account.'
    takes_args = (
        Param('uid', primary_key=True),
    )
    def execute(self, uid, **kw):
        ldap = self.api.Backend.ldap
        dn = ldap.find_entry_dn("uid", uid)
        return ldap.mark_entry_inactive(dn)
    def output_for_cli(self, ret):
        if ret:
            print "User locked"
api.register(user_lock)

class user_unlock(frontend.Command):
    'Unlock a user account.'
    takes_args = (
        Param('uid', primary_key=True),
    )
    def execute(self, uid, **kw):
        ldap = self.api.Backend.ldap
        dn = ldap.find_entry_dn("uid", uid)
        return ldap.mark_entry_active(dn)
    def output_for_cli(self, ret):
        if ret:
            print "User unlocked"
api.register(user_unlock)
1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199
# Authors:
#   Pavel Zuna <pzuna@redhat.com>
#
# Copyright (C) 2009  Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
Base classes for LDAP plugins.
"""

import re
import json
import time
from copy import deepcopy
import base64

from ipalib import api, crud, errors
from ipalib import Method, Object, Command
from ipalib import Flag, Int, Str
from ipalib.base import NameSpace
from ipalib.cli import to_cli, from_cli
from ipalib import output
from ipalib.text import _
from ipalib.util import json_serialize, validate_hostname
from ipalib.dn import *

global_output_params = (
    Flag('has_password',
        label=_('Password'),
    ),
    Str('member',
        label=_('Failed members'),
    ),
    Str('member_user?',
        label=_('Member users'),
    ),
    Str('member_group?',
        label=_('Member groups'),
    ),
    Str('memberof_group?',
        label=_('Member of groups'),
    ),
    Str('member_host?',
        label=_('Member hosts'),
    ),
    Str('member_hostgroup?',
        label=_('Member host-groups'),
    ),
    Str('memberof_hostgroup?',
        label=_('Member of host-groups'),
    ),
    Str('memberof_permission?',
        label=_('Permissions'),
    ),
    Str('memberof_privilege?',
        label='Privileges',
    ),
    Str('memberof_role?',
        label=_('Roles'),
    ),
    Str('memberof_sudocmdgroup?',
        label=_('Sudo Command Groups'),
    ),
    Str('member_privilege?',
        label='Granted to Privilege',
    ),
    Str('member_role?',
        label=_('Granting privilege to roles'),
    ),
    Str('member_netgroup?',
        label=_('Member netgroups'),
    ),
    Str('memberof_netgroup?',
        label=_('Member of netgroups'),
    ),
    Str('member_service?',
        label=_('Member services'),
    ),
    Str('member_servicegroup?',
        label=_('Member service groups'),
    ),
    Str('memberof_servicegroup?',
        label='Member of service groups',
    ),
    Str('member_hbacsvc?',
        label=_('Member HBAC service'),
    ),
    Str('member_hbacsvcgroup?',
        label=_('Member HBAC service groups'),
    ),
    Str('memberof_hbacsvcgroup?',
        label='Member of HBAC service groups',
    ),
    Str('member_sudocmd?',
        label='Member Sudo commands',
    ),
    Str('memberof_sudorule?',
        label='Member of Sudo rule',
    ),
    Str('memberof_hbacrule?',
        label='Member of HBAC rule',
    ),
    Str('memberindirect_user?',
        label=_('Indirect Member users'),
    ),
    Str('memberindirect_group?',
        label=_('Indirect Member groups'),
    ),
    Str('memberindirect_host?',
        label=_('Indirect Member hosts'),
    ),
    Str('memberindirect_hostgroup?',
        label=_('Indirect Member host-groups'),
    ),
    Str('memberindirect_role?',
        label=_('Indirect Member of roles'),
    ),
    Str('memberindirect_permission?',
        label=_('Indirect Member permissions'),
    ),
    Str('memberindirect_hbacsvc?',
        label=_('Indirect Member HBAC service'),
    ),
    Str('memberindirect_hbacsvcgrp?',
        label=_('Indirect Member HBAC service group'),
    ),
    Str('memberindirect_netgroup?',
        label=_('Indirect Member netgroups'),
    ),
    Str('memberofindirect_group?',
        label='Indirect Member of group',
    ),
    Str('memberofindirect_netgroup?',
        label='Indirect Member of netgroup',
    ),
    Str('memberofindirect_hostgroup?',
        label='Indirect Member of host-group',
    ),
    Str('memberofindirect_role?',
        label='Indirect Member of role',
    ),
    Str('memberofindirect_sudorule?',
        label='Indirect Member of Sudo rule',
    ),
    Str('memberofindirect_hbacrule?',
        label='Indirect Member of HBAC rule',
    ),
    Str('externalhost?',
        label=_('External host'),
    ),
    Str('sourcehost',
        label=_('Failed source hosts/hostgroups'),
    ),
    Str('memberhost',
        label=_('Failed hosts/hostgroups'),
    ),
    Str('memberuser',
        label=_('Failed users/groups'),
    ),
    Str('memberservice',
        label=_('Failed service/service groups'),
    ),
    Str('managedby',
        label=_('Failed managedby'),
    ),
    Str('failed',
        label=_('Failed to remove'),
        flags=['suppress_empty'],
    ),
    Str('ipasudorunas',
        label=_('Failed RunAs'),
    ),
    Str('ipasudorunasgroup',
        label=_('Failed RunAsGroup'),
    ),
)


def validate_add_attribute(ugettext, attr):
    validate_attribute(ugettext, 'addattr', attr)

def validate_set_attribute(ugettext, attr):
    validate_attribute(ugettext, 'setattr', attr)

def validate_del_attribute(ugettext, attr):
    validate_attribute(ugettext, 'delattr', attr)

def validate_attribute(ugettext, name, attr):
    m = re.match("\s*(.*?)\s*=\s*(.*?)\s*$", attr)
    if not m or len(m.groups()) != 2:
        raise errors.ValidationError(name=name, error='Invalid format. Should be name=value')

def get_effective_rights(ldap, dn, attrs=None):
    if attrs is None:
        attrs = ['*', 'nsaccountlock', 'cospriority']
    rights = ldap.get_effective_rights(dn, attrs)
    rdict = {}
    if 'attributelevelrights' in rights[1]:
        rights = rights[1]['attributelevelrights']
        rights = rights[0].split(', ')
        for r in rights:
            (k,v) = r.split(':')
            rdict[k.strip().lower()] = v

    return rdict

def entry_from_entry(entry, newentry):
    """
    Python is more or less pass-by-value except for immutable objects. So if
    you pass in a dict to a function you are free to change members of that
    dict but you can't create a new dict in the function and expect to replace
    what was passed in.

    In some post-op plugins that is exactly what we want to do, so here is a
    clumsy way around the problem.
    """

    # Wipe out the current data
    for e in entry.keys():
        del entry[e]

    # Re-populate it with new wentry
    for e in newentry:
        entry[e] = newentry[e]

def wait_for_memberof(keys, entry_start, completed, show_command, adding=True):
    """
    When adding or removing reverse members we are faking an update to
    object A by updating the member attribute in object B. The memberof
    plugin makes this work by adding or removing the memberof attribute
    to/from object A, it just takes a little bit of time.

    This will loop for 6+ seconds, retrieving object A so we can see
    if all the memberof attributes have been updated.
    """
    if completed == 0:
        # nothing to do
        return api.Command[show_command](keys[-1])['result']

    if 'memberof' in entry_start:
        starting_memberof = len(entry_start['memberof'])
    else:
        starting_memberof = 0

    # Loop a few times to give the memberof plugin a chance to add the
    # entries. Don't sleep for more than 6 seconds.
    memberof = 0
    x = 0
    while x < 20:
        # sleep first because the first search, even on a quiet system,
        # almost always fails to have memberof set.
        time.sleep(.3)
        x = x + 1

        # FIXME: put a try/except around here? I think it is probably better
        # to just let the exception filter up to the caller.
        entry_attrs = api.Command[show_command](keys[-1])['result']
        if 'memberof' in entry_attrs:
            memberof = len(entry_attrs['memberof'])

        if adding:
            if starting_memberof + completed >= memberof:
                break
        else:
            if starting_memberof + completed <= memberof:
                break

    return entry_attrs

def wait_for_value(ldap, dn, attr, value):
    """
    389-ds postoperation plugins are executed after the data has been
    returned to a client. This means that plugins that add data in a
    postop are not included in data returned to the user.

    The downside of waiting is that this increases the time of the
    command.

    The updated entry is returned.
    """
    # Loop a few times to give the postop-plugin a chance to complete
    # Don't sleep for more than 6 seconds.
    x = 0
    while x < 20:
        # sleep first because the first search, even on a quiet system,
        # almost always fails.
        time.sleep(.3)
        x = x + 1

        # FIXME: put a try/except around here? I think it is probably better
        # to just let the exception filter up to the caller.
        (dn, entry_attrs) = ldap.get_entry( dn, ['*'])
        if attr in entry_attrs:
            if isinstance(entry_attrs[attr], (list, tuple)):
                values = map(lambda y:y.lower(), entry_attrs[attr])
                if value.lower() in values:
                    break
            else:
                if value.lower() == entry_attrs[attr].lower():
                    break

    return entry_attrs

def add_external_pre_callback(membertype, ldap, dn, keys, options):
    """
    Pre callback to validate external members.

    This should be called by a command pre callback directly.

    membertype is the type of member
    """
    # validate hostname with allowed underscore characters, non-fqdn
    # hostnames are allowed
    def validate_host(hostname):
        validate_hostname(hostname, check_fqdn=False, allow_underscore=True)

    if membertype in options:
        if membertype == 'host':
            validator = validate_host
        else:
            validator = api.Object[membertype].primary_key
        for value in options[membertype]:
            try:
                validator(value)
            except errors.ValidationError as e:
                raise errors.ValidationError(name=membertype, error=e.error)
            except ValueError as e:
                raise errors.ValidationError(name=membertype, error=e)
    return dn

def add_external_post_callback(memberattr, membertype, externalattr, ldap, completed, failed, dn, entry_attrs, *keys, **options):
    """
    Post callback to add failed members as external members.

    This should be called by a commands post callback directly.

    memberattr is one of memberuser,
    membertype is the type of member: user,
    externalattr is one of externaluser,
    """
    completed_external = 0
    # Sift through the failures. We assume that these are all
    # entries that aren't stored in IPA, aka external entries.
    if memberattr in failed and membertype in failed[memberattr]:
        (dn, entry_attrs_) = ldap.get_entry(dn, [externalattr])
        members = entry_attrs.get(memberattr, [])
        external_entries = entry_attrs_.get(externalattr, [])
        failed_entries = []
        for entry in failed[memberattr][membertype]:
            membername = entry[0].lower()
            member_dn = api.Object[membertype].get_dn(membername)
            if membername not in external_entries and \
              member_dn not in members:
                # Not an IPA entry, assume external
                external_entries.append(membername)
                completed_external += 1
            elif membername in external_entries and \
              member_dn not in members:
                # Already an external member, reset the error message
                msg = unicode(errors.AlreadyGroupMember().message)
                newerror = (entry[0], msg)
                ind = failed[memberattr][membertype].index(entry)
                failed[memberattr][membertype][ind] = newerror
                failed_entries.append(membername)
            else:
                # Really a failure
                failed_entries.append(membername)

        if completed_external:
            try:
                ldap.update_entry(dn, {externalattr: external_entries})
            except errors.EmptyModlist:
                pass
            failed[memberattr][membertype] = failed_entries
            entry_attrs[externalattr] = external_entries

    return (completed + completed_external, dn)

def remove_external_post_callback(memberattr, membertype, externalattr, ldap, completed, failed, dn, entry_attrs, *keys, **options):
    # Run through the failures and gracefully remove any member defined
    # as an external member.
    if memberattr in failed and membertype in failed[memberattr]:
        (dn, entry_attrs_) = ldap.get_entry(dn, [externalattr])
        external_entries = entry_attrs_.get(externalattr, [])
        failed_entries = []
        completed_external = 0
        for entry in failed[memberattr][membertype]:
            membername = entry[0].lower()
            if membername in external_entries:
                external_entries.remove(membername)
                completed_external += 1
            else:
                failed_entries.append(membername)

        if completed_external:
            try:
                ldap.update_entry(dn, {externalattr: external_entries})
            except errors.EmptyModlist:
                pass
            failed[memberattr][membertype] = failed_entries
            entry_attrs[externalattr] = external_entries

    return (completed + completed_external, dn)

def host_is_master(ldap, fqdn):
    """
    Check to see if this host is a master.

    Raises an exception if a master, otherwise returns nothing.
    """
    master_dn = str(DN('cn=%s' % fqdn, 'cn=masters,cn=ipa,cn=etc', api.env.basedn))
    try:
        (dn, entry_attrs) = ldap.get_entry(master_dn, ['objectclass'])
        raise errors.ValidationError(name='hostname', error=_('An IPA master host cannot be deleted or disabled'))
    except errors.NotFound:
        # Good, not a master
        return


class LDAPObject(Object):
    """
    Object representing a LDAP entry.
    """
    backend_name = 'ldap2'

    parent_object = ''
    container_dn = ''
    normalize_dn = True
    object_name = _('entry')
    object_name_plural = _('entries')
    object_class = []
    object_class_config = None
    # If an objectclass is possible but not default in an entry. Needed for
    # collecting attributes for ACI UI.
    possible_objectclasses = []
    limit_object_classes = [] # Only attributes in these are allowed
    disallow_object_classes = [] # Disallow attributes in these
    search_attributes = []
    search_attributes_config = None
    default_attributes = []
    search_display_attributes = [] # attributes displayed in LDAPSearch
    hidden_attributes = ['objectclass', 'aci']
    # set rdn_attribute only if RDN attribute differs from primary key!
    rdn_attribute = ''
    uuid_attribute = ''
    attribute_members = {}
    rdn_is_primary_key = False # Do we need RDN change to do a rename?
    password_attributes = []
    # Can bind as this entry (has userPassword or krbPrincipalKey)
    bindable = False
    relationships = {
        # attribute: (label, inclusive param prefix, exclusive param prefix)
        'member': ('Member', '', 'no_'),
        'memberof': ('Member Of', 'in_', 'not_in_'),
        'memberindirect': (
            'Indirect Member', None, 'no_indirect_'
        ),
        'memberofindirect': (
            'Indirect Member Of', None, 'not_in_indirect_'
        ),
    }
    label = _('Entry')
    label_singular = _('Entry')

    container_not_found_msg = _('container entry (%(container)s) not found')
    parent_not_found_msg = _('%(parent)s: %(oname)s not found')
    object_not_found_msg = _('%(pkey)s: %(oname)s not found')
    already_exists_msg = _('%(oname)s with name "%(pkey)s" already exists')

    def get_dn(self, *keys, **kwargs):
        if self.parent_object:
            parent_dn = self.api.Object[self.parent_object].get_dn(*keys[:-1])
        else:
            parent_dn = self.container_dn
        if self.rdn_attribute:
            try:
                (dn, entry_attrs) = self.backend.find_entry_by_attr(
                    self.primary_key.name, keys[-1], self.object_class, [''],
                    self.container_dn
                )
            except errors.NotFound:
                pass
            else:
                return dn
        if self.primary_key and keys[-1] is not None:
            return self.backend.make_dn_from_attr(
                self.primary_key.name, keys[-1], parent_dn
            )
        return parent_dn

    def get_primary_key_from_dn(self, dn):
        try:
            if self.rdn_attribute:
                (dn, entry_attrs) = self.backend.get_entry(
                    dn, [self.primary_key.name]
                )
                try:
                    return entry_attrs[self.primary_key.name][0]
                except (KeyError, IndexError):
                    return ''
        except errors.NotFound:
            pass
        # DN object assures we're returning a decoded (unescaped) value
        dn = DN(dn)
        try:
            return dn[self.primary_key.name]
        except KeyError:
            # The primary key is not in the DN.
            # This shouldn't happen, but we don't want a "show" command to
            # crash.
            # Just return the entire DN, it's all we have if the entry
            # doesn't exist
            return unicode(dn)

    def get_ancestor_primary_keys(self):
        if self.parent_object:
            parent_obj = self.api.Object[self.parent_object]
            for key in parent_obj.get_ancestor_primary_keys():
                yield key
            if parent_obj.primary_key:
                pkey = parent_obj.primary_key
                yield pkey.__class__(
                    parent_obj.name + pkey.name, required=True, query=True,
                    cli_name=parent_obj.name, label=pkey.label
                )

    def has_objectclass(self, classes, objectclass):
        oc = map(lambda x:x.lower(),classes)
        return objectclass.lower() in oc

    def convert_attribute_members(self, entry_attrs, *keys, **options):
        if options.get('raw', False):
            return
        for attr in self.attribute_members:
            for member in entry_attrs.setdefault(attr, []):
                for ldap_obj_name in self.attribute_members[attr]:
                    ldap_obj = self.api.Object[ldap_obj_name]
                    if member.find(ldap_obj.container_dn) > 0:
                        new_attr = '%s_%s' % (attr, ldap_obj.name)
                        entry_attrs.setdefault(new_attr, []).append(
                            ldap_obj.get_primary_key_from_dn(member)
                        )
            del entry_attrs[attr]

    def get_password_attributes(self, ldap, dn, entry_attrs):
        """
        Search on the entry to determine if it has a password or
        keytab set.

        A tuple is used to determine which attribute is set
        in entry_attrs. The value is set to True/False whether a
        given password type is set.
        """
        for (pwattr, attr) in self.password_attributes:
            search_filter = '(%s=*)' % pwattr
            try:
                (entries, truncated) = ldap.find_entries(
                    search_filter, [pwattr], dn, ldap.SCOPE_BASE
                )
                entry_attrs[attr] = True
            except errors.NotFound:
                entry_attrs[attr] = False

    def handle_not_found(self, *keys):
        pkey = ''
        if self.primary_key:
            pkey = keys[-1]
        raise errors.NotFound(
            reason=self.object_not_found_msg % {
                'pkey': pkey, 'oname': self.object_name,
            }
        )

    def handle_duplicate_entry(self, *keys):
        pkey = ''
        if self.primary_key:
            pkey = keys[-1]
        raise errors.DuplicateEntry(
            message=self.already_exists_msg % {
                'pkey': pkey, 'oname': self.object_name,
            }
        )

    # list of attributes we want exported to JSON
    json_friendly_attributes = (
        'parent_object', 'container_dn', 'object_name', 'object_name_plural',
        'object_class', 'object_class_config', 'default_attributes', 'label', 'label_singular',
        'hidden_attributes', 'uuid_attribute', 'attribute_members', 'name',
        'takes_params', 'rdn_attribute', 'bindable', 'relationships',
    )

    def __json__(self):
        ldap = self.backend
        ldap.get_schema()
        json_dict = dict(
            (a, getattr(self, a)) for a in self.json_friendly_attributes
        )
        if self.primary_key:
            json_dict['primary_key'] = self.primary_key.name
        objectclasses = self.object_class
        if self.object_class_config:
            config = ldap.get_ipa_config()[1]
            objectclasses = config.get(
                self.object_class_config, objectclasses
            )
        objectclasses += self.possible_objectclasses
        # Get list of available attributes for this object for use
        # in the ACI UI.
        attrs = self.api.Backend.ldap2.schema.attribute_types(objectclasses)
        attrlist = []
        # Go through the MUST first
        for (oid, attr) in attrs[0].iteritems():
            attrlist.append(attr.names[0].lower())
        # And now the MAY
        for (oid, attr) in attrs[1].iteritems():
            attrlist.append(attr.names[0].lower())
        json_dict['aciattrs'] = attrlist
        attrlist.sort()
        json_dict['methods'] = [m for m in self.methods]
        return json_dict


# addattr can cause parameters to have more than one value even if not defined
# as multivalue, make sure this isn't the case
def _check_single_value_attrs(params, entry_attrs):
    for (a, v) in entry_attrs.iteritems():
        if isinstance(v, (list, tuple)) and len(v) > 1:
            if a in params and not params[a].multivalue:
                raise errors.OnlyOneValueAllowed(attr=a)

# setattr or --option='' can cause parameters to be empty that are otherwise
# required, make sure we enforce that.
def _check_empty_attrs(params, entry_attrs):
    for (a, v) in entry_attrs.iteritems():
        if v is None or (isinstance(v, basestring) and len(v) == 0):
            if a in params and params[a].required:
                raise errors.RequirementError(name=a)


def _check_limit_object_class(attributes, attrs, allow_only):
    """
    If the set of objectclasses is limited enforce that only those
    are updated in entry_attrs (plus dn)

    allow_only tells us what mode to check in:

    If True then we enforce that the attributes must be in the list of
    allowed.

    If False then those attributes are not allowed.
    """
    if len(attributes[0]) == 0 and len(attributes[1]) == 0:
        return
    limitattrs = deepcopy(attrs)
    # Go through the MUST first
    for (oid, attr) in attributes[0].iteritems():
        if attr.names[0].lower() in limitattrs:
            if not allow_only:
                raise errors.ObjectclassViolation(info='attribute "%(attribute)s" not allowed' % dict(attribute=attr.names[0].lower()))
            limitattrs.remove(attr.names[0].lower())
    # And now the MAY
    for (oid, attr) in attributes[1].iteritems():
        if attr.names[0].lower() in limitattrs:
            if not allow_only:
                raise errors.ObjectclassViolation(info='attribute "%(attribute)s" not allowed' % dict(attribute=attr.names[0].lower()))
            limitattrs.remove(attr.names[0].lower())
    if len(limitattrs) > 0 and allow_only:
        raise errors.ObjectclassViolation(info='attribute "%(attribute)s" not allowed' % dict(attribute=limitattrs[0]))

class CallbackInterface(Method):
    """
    Callback registration interface
    """
    def __init__(self):
        #pylint: disable=E1003
        if not hasattr(self.__class__, 'PRE_CALLBACKS'):
            self.__class__.PRE_CALLBACKS = []
        if not hasattr(self.__class__, 'POST_CALLBACKS'):
            self.__class__.POST_CALLBACKS = []
        if not hasattr(self.__class__, 'EXC_CALLBACKS'):
            self.__class__.EXC_CALLBACKS = []
        if not hasattr(self.__class__, 'INTERACTIVE_PROMPT_CALLBACKS'):
            self.__class__.INTERACTIVE_PROMPT_CALLBACKS = []
        if hasattr(self, 'pre_callback'):
            self.register_pre_callback(self.pre_callback, True)
        if hasattr(self, 'post_callback'):
            self.register_post_callback(self.post_callback, True)
        if hasattr(self, 'exc_callback'):
            self.register_exc_callback(self.exc_callback, True)
        if hasattr(self, 'interactive_prompt_callback'):
            self.register_interactive_prompt_callback(
                    self.interactive_prompt_callback, True) #pylint: disable=E1101
        super(Method, self).__init__()

    @classmethod
    def register_pre_callback(klass, callback, first=False):
        assert callable(callback)
        if not hasattr(klass, 'PRE_CALLBACKS'):
            klass.PRE_CALLBACKS = []
        if first:
            klass.PRE_CALLBACKS.insert(0, callback)
        else:
            klass.PRE_CALLBACKS.append(callback)

    @classmethod
    def register_post_callback(klass, callback, first=False):
        assert callable(callback)
        if not hasattr(klass, 'POST_CALLBACKS'):
            klass.POST_CALLBACKS = []
        if first:
            klass.POST_CALLBACKS.insert(0, callback)
        else:
            klass.POST_CALLBACKS.append(callback)

    @classmethod
    def register_exc_callback(klass, callback, first=False):
        assert callable(callback)
        if not hasattr(klass, 'EXC_CALLBACKS'):
            klass.EXC_CALLBACKS = []
        if first:
            klass.EXC_CALLBACKS.insert(0, callback)
        else:
            klass.EXC_CALLBACKS.append(callback)

    @classmethod
    def register_interactive_prompt_callback(klass, callback, first=False):
        assert callable(callback)
        if not hasattr(klass, 'INTERACTIVE_PROMPT_CALLBACKS'):
            klass.INTERACTIVE_PROMPT_CALLBACKS = []
        if first:
            klass.INTERACTIVE_PROMPT_CALLBACKS.insert(0, callback)
        else:
            klass.INTERACTIVE_PROMPT_CALLBACKS.append(callback)

    def _call_exc_callbacks(self, args, options, exc, call_func, *call_args, **call_kwargs):
        rv = None
        for i in xrange(len(getattr(self, 'EXC_CALLBACKS', []))):
            callback = self.EXC_CALLBACKS[i]
            try:
                if hasattr(callback, 'im_self'):
                    rv = callback(
                        args, options, exc, call_func, *call_args, **call_kwargs
                    )
                else:
                    rv = callback(
                        self, args, options, exc, call_func, *call_args,
                        **call_kwargs
                    )
            except errors.ExecutionError, e:
                if (i + 1) < len(self.EXC_CALLBACKS):
                    exc = e
                    continue
                raise e
        return rv


class BaseLDAPCommand(CallbackInterface, Command):
    """
    Base class for Base LDAP Commands.
    """
    setattr_option = Str('setattr*', validate_set_attribute,
                         cli_name='setattr',
                         doc=_("""Set an attribute to a name/value pair. Format is attr=value.
For multi-valued attributes, the command replaces the values already present."""),
                         exclude='webui',
                        )
    addattr_option = Str('addattr*', validate_add_attribute,
                         cli_name='addattr',
                         doc=_("""Add an attribute/value pair. Format is attr=value. The attribute
must be part of the schema."""),
                         exclude='webui',
                        )
    delattr_option = Str('delattr*', validate_del_attribute,
                         cli_name='delattr',
                         doc=_("""Delete an attribute/value pair. The option will be evaluated
last, after all sets and adds."""),
                         exclude='webui',
                        )

    def _convert_2_dict(self, attrs, append=True):
        """
        Convert a string in the form of name/value pairs into a dictionary.
        The incoming attribute may be a string or a list.

        :param attrs: A list of name/value pairs

        :param append: controls whether this returns a list of values or a single
        value.
        """
        newdict = {}
        if not type(attrs) in (list, tuple):
            attrs = [attrs]
        for a in attrs:
            m = re.match("\s*(.*?)\s*=\s*(.*?)\s*$", a)
            attr = str(m.group(1)).lower()
            value = m.group(2)
            if len(value) == 0:
                # None means "delete this attribute"
                value = None
            if append and attr in newdict:
                if type(value) in (tuple,):
                    newdict[attr] += list(value)
                else:
                    newdict[attr].append(value)
            else:
                if type(value) in (tuple,):
                    newdict[attr] = list(value)
                else:
                    newdict[attr] = [value]
        return newdict

    def process_attr_options(self, entry_attrs, dn, keys, options):
        """
        Process all --setattr, --addattr, and --delattr options and add the
        resulting value to the list of attributes. --setattr is processed first,
        then --addattr and finally --delattr.

        When --setattr is not used then the original LDAP object is looked up
        (of course, not when dn is None) and the changes are applied to old
        object values.

        Attribute values deleted by --delattr may be deleted from attribute
        values set or added by --setattr, --addattr. For example, the following
        attributes will result in a NOOP:

        --addattr=attribute=foo --delattr=attribute=foo

        AttrValueNotFound exception may be raised when an attribute value was
        not found either by --setattr and --addattr nor in existing LDAP object.

        :param entry_attrs: A list of attributes that will be updated
        :param dn: dn of updated LDAP object or None if a new object is created
        :param keys: List of command arguments
        :param options: List of options
        """
        if all(k not in options for k in ("setattr", "addattr", "delattr")):
            return

        ldap = self.obj.backend

        adddict = self._convert_2_dict(options.get('addattr', []))
        setdict = self._convert_2_dict(options.get('setattr', []))
        deldict = self._convert_2_dict(options.get('delattr', []))

        setattrs = set(setdict.keys())
        addattrs = set(adddict.keys())
        delattrs = set(deldict.keys())

        if dn is None:
            direct_add = addattrs
            direct_del = delattrs
            needldapattrs = []
        else:
            direct_add = setattrs & addattrs
            direct_del = setattrs & delattrs
            needldapattrs = list((addattrs | delattrs) - setattrs)

        for attr, val in setdict.iteritems():
            entry_attrs[attr] = val

        for attr in direct_add:
            entry_attrs.setdefault(attr, []).extend(adddict[attr])

        for attr in direct_del:
            for delval in deldict[attr]:
                try:
                    entry_attrs[attr].remove(delval)
                except ValueError:
                    raise errors.AttrValueNotFound(attr=attr,
                                                   value=delval)

        if needldapattrs:
            try:
                (dn, old_entry) = ldap.get_entry(
                    dn, needldapattrs, normalize=self.obj.normalize_dn
                )
            except errors.ExecutionError, e:
                try:
                    (dn, old_entry) = self._call_exc_callbacks(
                        keys, options, e, ldap.get_entry, dn, [],
                        normalize=self.obj.normalize_dn
                    )
                except errors.NotFound:
                    self.obj.handle_not_found(*keys)
            for attr in needldapattrs:
                entry_attrs[attr] = old_entry.get(attr, [])

                if attr in addattrs:
                    entry_attrs[attr].extend(adddict.get(attr, []))

                for delval in deldict.get(attr, []):
                    try:
                        entry_attrs[attr].remove(delval)
                    except ValueError:
                        if isinstance(delval, str):
                            # This is a Binary value, base64 encode it
                            delval = unicode(base64.b64encode(delval))
                        raise errors.AttrValueNotFound(attr=attr, value=delval)

        # normalize all values
        changedattrs = setattrs | addattrs | delattrs
        for attr in changedattrs:
            if attr in self.obj.params:
                # convert single-value params to scalars
                value = entry_attrs[attr]
                try:
                    param = self.params[attr]
                except KeyError:
                    # The CRUD classes filter their disallowed parameters out.
                    # Yet {set,add,del}attr are powerful enough to change these
                    # (e.g. Config's ipacertificatesubjectbase)
                    # So, use the parent's attribute
                    param = self.obj.params[attr]
                if not param.multivalue:
                    if len(value) == 1:
                        value = value[0]
                    elif not value:
                        value = None
                    else:
                        raise errors.OnlyOneValueAllowed(attr=attr)
                # validate, convert and encode params
                try:
                   value = param(value)
                except errors.ValidationError, err:
                    raise errors.ValidationError(name=attr, error=err.error)
                except errors.ConversionError, err:
                    raise errors.ConversionError(name=attr, error=err.error)
                # FIXME: We use `force` when encoding because we know this is
                # an attribute, even if it does not have the `attribute` flag
                # set. This happens with no_update attributes, which are
                # not cloned to Update commands. This cloning is where the flag
                # gets set.
                value = param.encode(value, force=True)
                entry_attrs[attr] = value
            else:
                # unknown attribute: remove duplicite and invalid values
                entry_attrs[attr] = list(set([val for val in entry_attrs[attr] if val]))
                if not entry_attrs[attr]:
                    entry_attrs[attr] = None
                elif isinstance(entry_attrs[attr], (tuple, list)) and len(entry_attrs[attr]) == 1:
                    entry_attrs[attr] = entry_attrs[attr][0]


class LDAPCreate(BaseLDAPCommand, crud.Create):
    """
    Create a new entry in LDAP.
    """
    takes_options = (BaseLDAPCommand.setattr_option, BaseLDAPCommand.addattr_option)

    def get_args(self):
        #pylint: disable=E1003
        for key in self.obj.get_ancestor_primary_keys():
            yield key
        if self.obj.primary_key:
            yield self.obj.primary_key.clone(attribute=True)
        for arg in super(crud.Create, self).get_args():
            yield arg

    has_output_params = global_output_params

    def execute(self, *keys, **options):
        ldap = self.obj.backend

        entry_attrs = self.args_options_2_entry(*keys, **options)

        self.process_attr_options(entry_attrs, None, keys, options)

        entry_attrs['objectclass'] = deepcopy(self.obj.object_class)

        if self.obj.object_class_config:
            config = ldap.get_ipa_config()[1]
            entry_attrs['objectclass'] = config.get(
                self.obj.object_class_config, entry_attrs['objectclass']
            )

        if self.obj.uuid_attribute:
            entry_attrs[self.obj.uuid_attribute] = 'autogenerate'

        dn = self.obj.get_dn(*keys, **options)
        if self.obj.rdn_attribute:
            if not dn.startswith('%s=' % self.obj.primary_key.name):
                self.obj.handle_duplicate_entry(*keys)
            dn = ldap.make_dn(
                entry_attrs, self.obj.rdn_attribute, self.obj.container_dn
            )

        if options.get('all', False):
            attrs_list = ['*'] + self.obj.default_attributes
        else:
            attrs_list = list(
                set(self.obj.default_attributes + entry_attrs.keys())
            )

        for callback in self.PRE_CALLBACKS:
            if hasattr(callback, 'im_self'):
                dn = callback(
                    ldap, dn, entry_attrs, attrs_list, *keys, **options
                )
            else:
                dn = callback(
                    self, ldap, dn, entry_attrs, attrs_list, *keys, **options
                )

        _check_single_value_attrs(self.params, entry_attrs)
        ldap.get_schema()
        _check_limit_object_class(self.api.Backend.ldap2.schema.attribute_types(self.obj.limit_object_classes), entry_attrs.keys(), allow_only=True)
        _check_limit_object_class(self.api.Backend.ldap2.schema.attribute_types(self.obj.disallow_object_classes), entry_attrs.keys(), allow_only=False)

        try:
            ldap.add_entry(dn, entry_attrs, normalize=self.obj.normalize_dn)
        except errors.ExecutionError, e:
            try:
                self._call_exc_callbacks(
                    keys, options, e, ldap.add_entry, dn, entry_attrs,
                    normalize=self.obj.normalize_dn
                )
            except errors.NotFound:
                parent = self.obj.parent_object
                if parent:
                    raise errors.NotFound(
                        reason=self.obj.parent_not_found_msg % {
                            'parent': keys[-2],
                            'oname': self.api.Object[parent].object_name,
                        }
                    )
                raise errors.NotFound(
                    reason=self.obj.container_not_found_msg % {
                        'container': self.obj.container_dn,
                    }
                )
            except errors.DuplicateEntry:
                self.obj.handle_duplicate_entry(*keys)

        try:
            if self.obj.rdn_attribute:
                # make sure objectclass is either set or None
                if self.obj.object_class:
                    object_class = self.obj.object_class
                else:
                    object_class = None
                (dn, entry_attrs) = ldap.find_entry_by_attr(
                    self.obj.primary_key.name, keys[-1], object_class, attrs_list,
                    self.obj.container_dn
                )
            else:
                (dn, entry_attrs) = ldap.get_entry(
                    dn, attrs_list, normalize=self.obj.normalize_dn
                )
        except errors.ExecutionError, e:
            try:
                (dn, entry_attrs) = self._call_exc_callbacks(
                    keys, options, e, ldap.get_entry, dn, attrs_list,
                    normalize=self.obj.normalize_dn
                )
            except errors.NotFound:
                self.obj.handle_not_found(*keys)

        for callback in self.POST_CALLBACKS:
            if hasattr(callback, 'im_self'):
                dn = callback(ldap, dn, entry_attrs, *keys, **options)
            else:
                dn = callback(self, ldap, dn, entry_attrs, *keys, **options)

        entry_attrs['dn'] = dn

        self.obj.convert_attribute_members(entry_attrs, *keys, **options)
        if self.obj.primary_key and keys[-1] is not None:
            return dict(result=entry_attrs, value=keys[-1])
        return dict(result=entry_attrs, value=u'')

    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
        return dn

    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
        return dn

    def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
        raise exc

    def interactive_prompt_callback(self, kw):
        return

    # list of attributes we want exported to JSON
    json_friendly_attributes = (
        'takes_args', 'takes_options',
    )

    def __json__(self):
        json_dict = dict(
            (a, getattr(self, a)) for a in self.json_friendly_attributes
        )
        return json_dict

class LDAPQuery(BaseLDAPCommand, crud.PKQuery):
    """
    Base class for commands that need to retrieve an existing entry.
    """
    def get_args(self):
        #pylint: disable=E1003
        for key in self.obj.get_ancestor_primary_keys():
            yield key
        if self.obj.primary_key:
            yield self.obj.primary_key.clone(attribute=True, query=True)
        for arg in super(crud.PKQuery, self).get_args():
            yield arg

    # list of attributes we want exported to JSON
    json_friendly_attributes = (
        'takes_args', 'takes_options',
    )

    def __json__(self):
        json_dict = dict(
            (a, getattr(self, a)) for a in self.json_friendly_attributes
        )
        return json_dict

class LDAPMultiQuery(LDAPQuery):
    """
    Base class for commands that need to retrieve one or more existing entries.
    """
    takes_options = (
        Flag('continue',
            cli_name='continue',
            doc=_('Continuous mode: Don\'t stop on errors.'),
        ),
    )

    def get_args(self):
        #pylint: disable=E1003
        for key in self.obj.get_ancestor_primary_keys():
            yield key
        if self.obj.primary_key:
            yield self.obj.primary_key.clone(
                attribute=True, query=True, multivalue=True
            )
        for arg in super(crud.PKQuery, self).get_args():
            yield arg


class LDAPRetrieve(LDAPQuery):
    """
    Retrieve an LDAP entry.
    """
    has_output = output.standard_entry
    has_output_params = global_output_params

    takes_options = (
        Flag('rights',
            label=_('Rights'),
            doc=_('Display the access rights of this entry (requires --all). See ipa man page for details.'),
        ),
    )

    def execute(self, *keys, **options):
        ldap = self.obj.backend

        dn = self.obj.get_dn(*keys, **options)

        if options.get('all', False):
            attrs_list = ['*'] + self.obj.default_attributes
        else:
            attrs_list = list(self.obj.default_attributes)

        for callback in self.PRE_CALLBACKS:
            if hasattr(callback, 'im_self'):
                dn = callback(ldap, dn, attrs_list, *keys, **options)
            else:
                dn = callback(self, ldap, dn, attrs_list, *keys, **options)

        try:
            (dn, entry_attrs) = ldap.get_entry(
                dn, attrs_list, normalize=self.obj.normalize_dn
            )
        except errors.ExecutionError, e:
            try:
                (dn, entry_attrs) = self._call_exc_callbacks(
                    keys, options, e, ldap.get_entry, dn, attrs_list,
                    normalize=self.obj.normalize_dn
                )
            except errors.NotFound:
                self.obj.handle_not_found(*keys)

        if options.get('rights', False) and options.get('all', False):
            entry_attrs['attributelevelrights'] = get_effective_rights(ldap, dn)

        for callback in self.POST_CALLBACKS:
            if hasattr(callback, 'im_self'):
                dn = callback(ldap, dn, entry_attrs, *keys, **options)
            else:
                dn = callback(self, ldap, dn, entry_attrs, *keys, **options)

        self.obj.convert_attribute_members(entry_attrs, *keys, **options)
        entry_attrs['dn'] = dn
        if self.obj.primary_key and keys[-1] is not None:
            return dict(result=entry_attrs, value=keys[-1])
        return dict(result=entry_attrs, value=u'')

    def pre_callback(self, ldap, dn, attrs_list, *keys, **options):
        return dn

    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
        return dn

    def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
        raise exc

    def interactive_prompt_callback(self, kw):
        return


class LDAPUpdate(LDAPQuery, crud.Update):
    """
    Update an LDAP entry.
    """

    takes_options = (
        BaseLDAPCommand.setattr_option,
        BaseLDAPCommand.addattr_option,
        BaseLDAPCommand.delattr_option,
        Flag('rights',
            label=_('Rights'),
            doc=_('Display the access rights of this entry (requires --all). See ipa man page for details.'),
        ),
    )

    has_output_params = global_output_params

    def _get_rename_option(self):
        rdnparam = getattr(self.obj.params, self.obj.primary_key.name)
        return rdnparam.clone_rename('rename',
            cli_name='rename', required=False, label=_('Rename'),
            doc=_('Rename the %(ldap_obj_name)s object') % dict(
                ldap_obj_name=self.obj.object_name
            )
        )

    def get_options(self):
        for option in super(LDAPUpdate, self).get_options():
            yield option
        if self.obj.rdn_is_primary_key:
            yield self._get_rename_option()

    def execute(self, *keys, **options):
        ldap = self.obj.backend

        if len(options) == 2: # 'all' and 'raw' are always sent
            raise errors.EmptyModlist()

        dn = self.obj.get_dn(*keys, **options)

        entry_attrs = self.args_options_2_entry(**options)

        self.process_attr_options(entry_attrs, dn, keys, options)

        if options.get('all', False):
            attrs_list = ['*'] + self.obj.default_attributes
        else:
            attrs_list = list(
                set(self.obj.default_attributes + entry_attrs.keys())
            )

        for callback in self.PRE_CALLBACKS:
            if hasattr(callback, 'im_self'):
                dn = callback(
                    ldap, dn, entry_attrs, attrs_list, *keys, **options
                )
            else:
                dn = callback(
                    self, ldap, dn, entry_attrs, attrs_list, *keys, **options
                )

        _check_single_value_attrs(self.params, entry_attrs)
        _check_empty_attrs(self.obj.params, entry_attrs)
        ldap.get_schema()
        _check_limit_object_class(self.api.Backend.ldap2.schema.attribute_types(self.obj.limit_object_classes), entry_attrs.keys(), allow_only=True)
        _check_limit_object_class(self.api.Backend.ldap2.schema.attribute_types(self.obj.disallow_object_classes), entry_attrs.keys(), allow_only=False)

        rdnupdate = False
        try:
            if self.obj.rdn_is_primary_key and 'rename' in options:
                if not options['rename']:
                    raise errors.ValidationError(name='rename', error=u'can\'t be empty')
                entry_attrs[self.obj.primary_key.name] = options['rename']

            if self.obj.rdn_is_primary_key and self.obj.primary_key.name in entry_attrs:
                # RDN change
                ldap.update_entry_rdn(dn,
                    unicode('%s=%s' % (self.obj.primary_key.name,
                    entry_attrs[self.obj.primary_key.name])))
                rdnkeys = keys[:-1] + (entry_attrs[self.obj.primary_key.name], )
                dn = self.obj.get_dn(*rdnkeys)
                del entry_attrs[self.obj.primary_key.name]
                options['rdnupdate'] = True
                rdnupdate = True

            ldap.update_entry(dn, entry_attrs, normalize=self.obj.normalize_dn)
        except errors.ExecutionError, e:
            # Exception callbacks will need to test for options['rdnupdate']
            # to decide what to do. An EmptyModlist in this context doesn't
            # mean an error occurred, just that there were no other updates to
            # perform.
            try:
                self._call_exc_callbacks(
                    keys, options, e, ldap.update_entry, dn, entry_attrs,
                    normalize=self.obj.normalize_dn
                )
            except errors.EmptyModlist, e:
                if not rdnupdate:
                    raise e
            except errors.NotFound:
                self.obj.handle_not_found(*keys)

        try:
            (dn, entry_attrs) = ldap.get_entry(
                dn, attrs_list, normalize=self.obj.normalize_dn
            )
        except errors.ExecutionError, e:
            try:
                (dn, entry_attrs) = self._call_exc_callbacks(
                    keys, options, e, ldap.get_entry, dn, attrs_list,
                    normalize=self.obj.normalize_dn
                )
            except errors.NotFound:
                raise errors.MidairCollision(
                    format=_('the entry was deleted while being modified')
                )

        if options.get('rights', False) and options.get('all', False):
            entry_attrs['attributelevelrights'] = get_effective_rights(ldap, dn)

        for callback in self.POST_CALLBACKS:
            if hasattr(callback, 'im_self'):
                dn = callback(ldap, dn, entry_attrs, *keys, **options)
            else:
                dn = callback(self, ldap, dn, entry_attrs, *keys, **options)

        self.obj.convert_attribute_members(entry_attrs, *keys, **options)
        if self.obj.primary_key and keys[-1] is not None:
            return dict(result=entry_attrs, value=keys[-1])
        return dict(result=entry_attrs, value=u'')

    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
        return dn

    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
        return dn

    def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
        raise exc

    def interactive_prompt_callback(self, kw):
        return


class LDAPDelete(LDAPMultiQuery):
    """
    Delete an LDAP entry and all of its direct subentries.
    """
    has_output = output.standard_delete

    has_output_params = global_output_params

    def execute(self, *keys, **options):
        ldap = self.obj.backend

        def delete_entry(pkey):
            nkeys = keys[:-1] + (pkey, )
            dn = self.obj.get_dn(*nkeys, **options)

            for callback in self.PRE_CALLBACKS:
                if hasattr(callback, 'im_self'):
                    dn = callback(ldap, dn, *nkeys, **options)
                else:
                    dn = callback(self, ldap, dn, *nkeys, **options)

            def delete_subtree(base_dn):
                truncated = True
                while truncated:
                    try:
                        (subentries, truncated) = ldap.find_entries(
                            None, [''], base_dn, ldap.SCOPE_ONELEVEL
                        )
                    except errors.NotFound:
                        break
                    else:
                        for (dn_, entry_attrs) in subentries:
                            delete_subtree(dn_)
                try:
                    ldap.delete_entry(base_dn, normalize=self.obj.normalize_dn)
                except errors.ExecutionError, e:
                    try:
                        self._call_exc_callbacks(
                            nkeys, options, e, ldap.delete_entry, base_dn,
                            normalize=self.obj.normalize_dn
                        )
                    except errors.NotFound:
                        self.obj.handle_not_found(*nkeys)

            delete_subtree(dn)

            for callback in self.POST_CALLBACKS:
                if hasattr(callback, 'im_self'):
                    result = callback(ldap, dn, *nkeys, **options)
                else:
                    result = callback(self, ldap, dn, *nkeys, **options)

            return result

        if not self.obj.primary_key or not isinstance(keys[-1], (list, tuple)):
            pkeyiter = (keys[-1], )
        else:
            pkeyiter = keys[-1]

        deleted = []
        failed = []
        result = True
        for pkey in pkeyiter:
            try:
                if not delete_entry(pkey):
                    result = False
            except errors.ExecutionError:
                if not options.get('continue', False):
                    raise
                failed.append(pkey)
            else:
                deleted.append(pkey)

        if self.obj.primary_key and pkeyiter[0] is not None:
            return dict(result=dict(failed=u','.join(failed)), value=u','.join(deleted))
        return dict(result=dict(failed=u''), value=u'')

    def pre_callback(self, ldap, dn, *keys, **options):
        return dn

    def post_callback(self, ldap, dn, *keys, **options):
        return True

    def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
        raise exc

    def interactive_prompt_callback(self, kw):
        return


class LDAPModMember(LDAPQuery):
    """
    Base class for member manipulation.
    """
    member_attributes = ['member']
    member_param_doc = _('comma-separated list of %s')
    member_count_out = ('%i member processed.', '%i members processed.')

    def get_options(self):
        for option in super(LDAPModMember, self).get_options():
            yield option
        for attr in self.member_attributes:
            for ldap_obj_name in self.obj.attribute_members[attr]:
                ldap_obj = self.api.Object[ldap_obj_name]
                name = to_cli(ldap_obj_name)
                doc = self.member_param_doc % ldap_obj.object_name_plural
                yield Str('%s*' % name, cli_name='%ss' % name, doc=doc,
                          label=_('member %s') % ldap_obj.object_name,
                          csv=True, alwaysask=True)

    def get_member_dns(self, **options):
        dns = {}
        failed = {}
        for attr in self.member_attributes:
            dns[attr] = {}
            failed[attr] = {}
            for ldap_obj_name in self.obj.attribute_members[attr]:
                dns[attr][ldap_obj_name] = []
                failed[attr][ldap_obj_name] = []
                names = options.get(to_cli(ldap_obj_name), [])
                if not names:
                    continue
                for name in names:
                    if not name:
                        continue
                    ldap_obj = self.api.Object[ldap_obj_name]
                    try:
                        dns[attr][ldap_obj_name].append(ldap_obj.get_dn(name))
                    except errors.PublicError, e:
                        failed[attr][ldap_obj_name].append((name, unicode(e)))
        return (dns, failed)


class LDAPAddMember(LDAPModMember):
    """
    Add other LDAP entries to members.
    """
    member_param_doc = _('comma-separated list of %s to add')
    member_count_out = ('%i member added.', '%i members added.')
    allow_same = False

    has_output = (
        output.Entry('result'),
        output.Output('failed',
            type=dict,
            doc=_('Members that could not be added'),
        ),
        output.Output('completed',
            type=int,
            doc=_('Number of members added'),
        ),
    )

    has_output_params = global_output_params

    def execute(self, *keys, **options):
        ldap = self.obj.backend

        (member_dns, failed) = self.get_member_dns(**options)

        dn = self.obj.get_dn(*keys, **options)

        for callback in self.PRE_CALLBACKS:
            if hasattr(callback, 'im_self'):
                dn = callback(ldap, dn, member_dns, failed, *keys, **options)
            else:
                dn = callback(
                    self, ldap, dn, member_dns, failed, *keys, **options
                )

        completed = 0
        for (attr, objs) in member_dns.iteritems():
            for ldap_obj_name in objs:
                for m_dn in member_dns[attr][ldap_obj_name]:
                    if not m_dn:
                        continue
                    try:
                        ldap.add_entry_to_group(m_dn, dn, attr, allow_same=self.allow_same)
                    except errors.PublicError, e:
                        ldap_obj = self.api.Object[ldap_obj_name]
                        failed[attr][ldap_obj_name].append((
                            ldap_obj.get_primary_key_from_dn(m_dn),
                            unicode(e),)
                        )
                    else:
                        completed += 1

        if options.get('all', False):
            attrs_list = ['*'] + self.obj.default_attributes
        else:
            attrs_list = list(
                set(self.obj.default_attributes + member_dns.keys())
            )

        try:
            (dn, entry_attrs) = ldap.get_entry(
                dn, attrs_list, normalize=self.obj.normalize_dn
            )
        except errors.ExecutionError, e:
            try:
                (dn, entry_attrs) = self._call_exc_callbacks(
                    keys, options, e, ldap.get_entry, dn, attrs_list,
                    normalize=self.obj.normalize_dn
                )
            except errors.NotFound:
                self.obj.handle_not_found(*keys)

        for callback in self.POST_CALLBACKS:
            if hasattr(callback, 'im_self'):
                (completed, dn) = callback(
                    ldap, completed, failed, dn, entry_attrs, *keys, **options
                )
            else:
                (completed, dn) = callback(
                    self, ldap, completed, failed, dn, entry_attrs, *keys,
                    **options
                )

        entry_attrs['dn'] = dn
        self.obj.convert_attribute_members(entry_attrs, *keys, **options)
        return dict(
            completed=completed,
            failed=failed,
            result=entry_attrs,
        )

    def pre_callback(self, ldap, dn, found, not_found, *keys, **options):
        return dn

    def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options):
        return (completed, dn)

    def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
        raise exc

    def interactive_prompt_callback(self, kw):
        return


class LDAPRemoveMember(LDAPModMember):
    """
    Remove LDAP entries from members.
    """
    member_param_doc = _('comma-separated list of %s to remove')
    member_count_out = ('%i member removed.', '%i members removed.')

    has_output = (
        output.Entry('result'),
        output.Output('failed',
            type=dict,
            doc=_('Members that could not be removed'),
        ),
        output.Output('completed',
            type=int,
            doc=_('Number of members removed'),
        ),
    )

    has_output_params = global_output_params

    def execute(self, *keys, **options):
        ldap = self.obj.backend

        (member_dns, failed) = self.get_member_dns(**options)

        dn = self.obj.get_dn(*keys, **options)

        for callback in self.PRE_CALLBACKS:
            if hasattr(callback, 'im_self'):
                dn = callback(ldap, dn, member_dns, failed, *keys, **options)
            else:
                dn = callback(
                    self, ldap, dn, member_dns, failed, *keys, **options
                )

        completed = 0
        for (attr, objs) in member_dns.iteritems():
            for ldap_obj_name, m_dns in objs.iteritems():
                for m_dn in m_dns:
                    if not m_dn:
                        continue
                    try:
                        ldap.remove_entry_from_group(m_dn, dn, attr)
                    except errors.PublicError, e:
                        ldap_obj = self.api.Object[ldap_obj_name]
                        failed[attr][ldap_obj_name].append((
                            ldap_obj.get_primary_key_from_dn(m_dn),
                            unicode(e),)
                        )
                    else:
                        completed += 1

        if options.get('all', False):
            attrs_list = ['*'] + self.obj.default_attributes
        else:
            attrs_list = list(
                set(self.obj.default_attributes + member_dns.keys())
            )

        # Give memberOf a chance to update entries
        time.sleep(.3)

        try:
            (dn, entry_attrs) = ldap.get_entry(
                dn, attrs_list, normalize=self.obj.normalize_dn
            )
        except errors.ExecutionError, e:
            try:
                (dn, entry_attrs) = self._call_exc_callbacks(
                    keys, options, e, ldap.get_entry, dn, attrs_list,
                    normalize=self.obj.normalize_dn
                )
            except errors.NotFound:
                self.obj.handle_not_found(*keys)

        for callback in self.POST_CALLBACKS:
            if hasattr(callback, 'im_self'):
                (completed, dn) = callback(
                    ldap, completed, failed, dn, entry_attrs, *keys, **options
                )
            else:
                (completed, dn) = callback(
                    self, ldap, completed, failed, dn, entry_attrs, *keys,
                    **options
                )

        entry_attrs['dn'] = dn

        self.obj.convert_attribute_members(entry_attrs, *keys, **options)
        return dict(
            completed=completed,
            failed=failed,
            result=entry_attrs,
        )

    def pre_callback(self, ldap, dn, found, not_found, *keys, **options):
        return dn

    def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options):
        return (completed, dn)

    def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
        raise exc

    def interactive_prompt_callback(self, kw):
        return


def gen_pkey_only_option(cli_name):
    return Flag('pkey_only?',
                label=_('Primary key only'),
                doc=_('Results should contain primary key attribute only ("%s")') \
                    % to_cli(cli_name),)

class LDAPSearch(BaseLDAPCommand, crud.Search):
    """
    Retrieve all LDAP entries matching the given criteria.
    """
    member_attributes = []
    member_param_incl_doc = _('Search for %(searched_object)s with these %(relationship)s %(ldap_object)s.')
    member_param_excl_doc = _('Search for %(searched_object)s without these %(relationship)s %(ldap_object)s.')

    # pointer to function for entries sorting
    # if no function is assigned the entries are sorted by their primary key value
    entries_sortfn = None

    takes_options = (
        Int('timelimit?',
            label=_('Time Limit'),
            doc=_('Time limit of search in seconds'),
            flags=['no_display'],
            minvalue=0,
            autofill=False,
        ),
        Int('sizelimit?',
            label=_('Size Limit'),
            doc=_('Maximum number of entries returned'),
            flags=['no_display'],
            minvalue=0,
            autofill=False,
        ),
    )

    def get_args(self):
        #pylint: disable=E1003
        for key in self.obj.get_ancestor_primary_keys():
            yield key
        yield Str('criteria?',
                  noextrawhitespace=False,
                  doc=_('A string searched in all relevant object attributes'))
        for arg in super(crud.Search, self).get_args():
            yield arg

    def get_member_options(self, attr):
        for ldap_obj_name in self.obj.attribute_members[attr]:
            ldap_obj = self.api.Object[ldap_obj_name]
            relationship = self.obj.relationships.get(
                attr, ['member', '', 'no_']
            )
            doc = self.member_param_incl_doc % dict(
                searched_object=self.obj.object_name_plural,
                relationship=relationship[0].lower(),
                ldap_object=ldap_obj.object_name_plural
            )
            name = '%s%s' % (relationship[1], to_cli(ldap_obj_name))
            yield Str(
                '%s*' % name, cli_name='%ss' % name, doc=doc,
                label=ldap_obj.object_name, csv=True
            )
            doc = self.member_param_excl_doc % dict(
                searched_object=self.obj.object_name_plural,
                relationship=relationship[0].lower(),
                ldap_object=ldap_obj.object_name_plural
            )
            name = '%s%s' % (relationship[2], to_cli(ldap_obj_name))
            yield Str(
                '%s*' % name, cli_name='%ss' % name, doc=doc,
                label=ldap_obj.object_name, csv=True
            )

    def get_options(self):
        for option in super(LDAPSearch, self).get_options():
            yield option
        if self.obj.primary_key and \
                'no_output' not in self.obj.primary_key.flags:
            yield gen_pkey_only_option(self.obj.primary_key.cli_name)
        for attr in self.member_attributes:
            for option in self.get_member_options(attr):
                yield option

    def get_member_filter(self, ldap, **options):
        filter = ''
        for attr in self.member_attributes:
            for ldap_obj_name in self.obj.attribute_members[attr]:
                ldap_obj = self.api.Object[ldap_obj_name]
                relationship = self.obj.relationships.get(
                    attr, ['member', '', 'no_']
                )
                # Handle positive (MATCH_ALL) and negative (MATCH_NONE)
                # searches similarly
                param_prefixes = relationship[1:]  # e.g. ('in_', 'not_in_')
                rules = ldap.MATCH_ALL, ldap.MATCH_NONE
                for param_prefix, rule in zip(param_prefixes, rules):
                    param_name = '%s%s' % (param_prefix, to_cli(ldap_obj_name))
                    if options.get(param_name):
                        dns = []
                        for pkey in options[param_name]:
                            dns.append(ldap_obj.get_dn(pkey))
                        flt = ldap.make_filter_from_attr(attr, dns, rule)
                        filter = ldap.combine_filters(
                            (filter, flt), ldap.MATCH_ALL
                        )
        return filter

    has_output_params = global_output_params

    def execute(self, *args, **options):
        ldap = self.obj.backend

        term = args[-1]
        if self.obj.parent_object:
            base_dn = self.api.Object[self.obj.parent_object].get_dn(*args[:-1])
        else:
            base_dn = self.obj.container_dn

        search_kw = self.args_options_2_entry(**options)

        if self.obj.search_display_attributes:
            defattrs = self.obj.search_display_attributes
        else:
            defattrs = self.obj.default_attributes

        if options.get('pkey_only', False):
            attrs_list = [self.obj.primary_key.name]
        elif options.get('all', False):
            attrs_list = ['*'] + defattrs
        else:
            attrs_list = list(
                set(defattrs + search_kw.keys())
            )

        if self.obj.search_attributes:
            search_attrs = self.obj.search_attributes
        else:
            search_attrs = self.obj.default_attributes
        if self.obj.search_attributes_config:
            config = ldap.get_ipa_config()[1]
            config_attrs = config.get(
                self.obj.search_attributes_config, [])
            if len(config_attrs) == 1 and (
                isinstance(config_attrs[0], basestring)):
                search_attrs = config_attrs[0].split(',')

        search_kw['objectclass'] = self.obj.object_class
        attr_filter = ldap.make_filter(search_kw, rules=ldap.MATCH_ALL)

        search_kw = {}
        for a in search_attrs:
            search_kw[a] = term
        term_filter = ldap.make_filter(search_kw, exact=False)

        member_filter = self.get_member_filter(ldap, **options)

        filter = ldap.combine_filters(
            (term_filter, attr_filter, member_filter), rules=ldap.MATCH_ALL
        )

        scope = ldap.SCOPE_ONELEVEL
        for callback in self.PRE_CALLBACKS:
            if hasattr(callback, 'im_self'):
                    (filter, base_dn, scope) = callback(
                        ldap, filter, attrs_list, base_dn, scope, *args, **options
                    )
            else:
                (filter, base_dn, scope) = callback(
                    self, ldap, filter, attrs_list, base_dn, scope, *args, **options
                )

        try:
            (entries, truncated) = ldap.find_entries(
                filter, attrs_list, base_dn, scope,
                time_limit=options.get('timelimit', None),
                size_limit=options.get('sizelimit', None)
            )
        except errors.ExecutionError, e:
            try:
                (entries, truncated) = self._call_exc_callbacks(
                    args, options, e, ldap.find_entries, filter, attrs_list,
                    base_dn, scope=ldap.SCOPE_ONELEVEL,
                    normalize=self.obj.normalize_dn
                )
            except errors.NotFound:
                (entries, truncated) = ([], False)

        for callback in self.POST_CALLBACKS:
            if hasattr(callback, 'im_self'):
                callback(ldap, entries, truncated, *args, **options)
            else:
                callback(self, ldap, entries, truncated, *args, **options)

        if not self.entries_sortfn:
            if self.obj.primary_key:
                sortfn=lambda x,y: cmp(x[1][self.obj.primary_key.name][0].lower(), y[1][self.obj.primary_key.name][0].lower())
                entries.sort(sortfn)
        else:
            entries.sort(self.entries_sortfn)

        if not options.get('raw', False):
            for e in entries:
                self.obj.convert_attribute_members(e[1], *args, **options)

        for e in entries:
            e[1]['dn'] = e[0]
        entries = [e for (dn, e) in entries]

        return dict(
            result=entries,
            count=len(entries),
            truncated=truncated,
        )

    def pre_callback(self, ldap, filters, attrs_list, base_dn, scope, *args, **options):
        return (filters, base_dn, scope)

    def post_callback(self, ldap, entries, truncated, *args, **options):
        pass

    def exc_callback(self, args, options, exc, call_func, *call_args, **call_kwargs):
        raise exc

    def interactive_prompt_callback(self, kw):
        return

    # list of attributes we want exported to JSON
    json_friendly_attributes = (
        'takes_options',
    )

    def __json__(self):
        json_dict = dict(
            (a, getattr(self, a)) for a in self.json_friendly_attributes
        )
        return json_dict

class LDAPModReverseMember(LDAPQuery):
    """
    Base class for reverse member manipulation.
    """
    reverse_attributes = ['member']
    reverse_param_doc = _('comma-separated list of %s')
    reverse_count_out = ('%i member processed.', '%i members processed.')

    has_output_params = global_output_params

    def get_options(self):
        for option in super(LDAPModReverseMember, self).get_options():
            yield option
        for attr in self.reverse_attributes:
            for ldap_obj_name in self.obj.reverse_members[attr]:
                ldap_obj = self.api.Object[ldap_obj_name]
                name = to_cli(ldap_obj_name)
                doc = self.reverse_param_doc % ldap_obj.object_name_plural
                yield Str('%s*' % name, cli_name='%ss' % name, doc=doc,
                          label=ldap_obj.object_name, csv=True,
                          alwaysask=True)


class LDAPAddReverseMember(LDAPModReverseMember):
    """
    Add other LDAP entries to members in reverse.

    The call looks like "add A to B" but in fact executes
    add B to A to handle reverse membership.
    """
    member_param_doc = _('comma-separated list of %s to add')
    member_count_out = ('%i member added.', '%i members added.')

    show_command = None
    member_command = None
    reverse_attr = None
    member_attr = None

    has_output = (
        output.Entry('result'),
        output.Output('failed',
            type=dict,
            doc=_('Members that could not be added'),
        ),
        output.Output('completed',
            type=int,
            doc=_('Number of members added'),
        ),
    )

    has_output_params = global_output_params

    def execute(self, *keys, **options):
        ldap = self.obj.backend

        # Ensure our target exists
        result = self.api.Command[self.show_command](keys[-1])['result']
        dn = result['dn']

        for callback in self.PRE_CALLBACKS:
            if hasattr(callback, 'im_self'):
                dn = callback(ldap, dn, *keys, **options)
            else:
                dn = callback(
                    self, ldap, dn, *keys, **options
                )

        if options.get('all', False):
            attrs_list = ['*'] + self.obj.default_attributes
        else:
            attrs_list = self.obj.default_attributes

        # Pull the record as it is now so we can know how many members
        # there are.
        entry_start = self.api.Command[self.show_command](keys[-1])['result']
        completed = 0
        failed = {'member': {self.reverse_attr: []}}
        for attr in options.get(self.reverse_attr, []):
            try:
                options = {'%s' % self.member_attr: keys[-1]}
                try:
                    result = self.api.Command[self.member_command](attr, **options)
                    if result['completed'] == 1:
                        completed = completed + 1
                    else:
                        failed['member'][self.reverse_attr].append((attr, result['failed']['member'][self.member_attr][0][1]))
                except errors.ExecutionError, e:
                    try:
                        (dn, entry_attrs) = self._call_exc_callbacks(
                            keys, options, e, self.member_command, dn, attrs_list,
                            normalize=self.obj.normalize_dn
                        )
                    except errors.NotFound, e:
                        msg = str(e)
                        (attr, msg) = msg.split(':', 1)
                        failed['member'][self.reverse_attr].append((attr, unicode(msg.strip())))

            except errors.PublicError, e:
                failed['member'][self.reverse_attr].append((attr, unicode(msg)))

        # Wait for the memberof plugin to update the entry
        try:
            entry_attrs = wait_for_memberof(keys, entry_start, completed, self.show_command, adding=True)
        except Exception, e:
            raise errors.ReverseMemberError(verb=_('added'), exc=str(e))

        for callback in self.POST_CALLBACKS:
            if hasattr(callback, 'im_self'):
                (completed, dn) = callback(
                    ldap, completed, failed, dn, entry_attrs, *keys, **options
                )
            else:
                (completed, dn) = callback(
                    self, ldap, completed, failed, dn, entry_attrs, *keys,
                    **options
                )

        entry_attrs['dn'] = dn
        return dict(
            completed=completed,
            failed=failed,
            result=entry_attrs,
        )

    def pre_callback(self, ldap, dn, *keys, **options):
        return dn

    def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options):
        return (completed, dn)

    def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
        raise exc

    def interactive_prompt_callback(self, kw):
        return

class LDAPRemoveReverseMember(LDAPModReverseMember):
    """
    Remove other LDAP entries from members in reverse.

    The call looks like "remove A from B" but in fact executes
    remove B from A to handle reverse membership.
    """
    member_param_doc = _('comma-separated list of %s to remove')
    member_count_out = ('%i member removed.', '%i members removed.')

    show_command = None
    member_command = None
    reverse_attr = None
    member_attr = None

    has_output = (
        output.Entry('result'),
        output.Output('failed',
            type=dict,
            doc=_('Members that could not be removed'),
        ),
        output.Output('completed',
            type=int,
            doc=_('Number of members removed'),
        ),
    )

    has_output_params = global_output_params

    def execute(self, *keys, **options):
        ldap = self.obj.backend

        # Ensure our target exists
        result = self.api.Command[self.show_command](keys[-1])['result']
        dn = result['dn']

        for callback in self.PRE_CALLBACKS:
            if hasattr(callback, 'im_self'):
                dn = callback(ldap, dn, *keys, **options)
            else:
                dn = callback(
                    self, ldap, dn, *keys, **options
                )

        if options.get('all', False):
            attrs_list = ['*'] + self.obj.default_attributes
        else:
            attrs_list = self.obj.default_attributes

        # Pull the record as it is now so we can know how many members
        # there are.
        entry_start = self.api.Command[self.show_command](keys[-1])['result']
        completed = 0
        failed = {'member': {self.reverse_attr: []}}
        for attr in options.get(self.reverse_attr, []):
            try:
                options = {'%s' % self.member_attr: keys[-1]}
                try:
                    result = self.api.Command[self.member_command](attr, **options)
                    if result['completed'] == 1:
                        completed = completed + 1
                    else:
                        failed['member'][self.reverse_attr].append((attr, result['failed']['member'][self.member_attr][0][1]))
                except errors.ExecutionError, e:
                    try:
                        (dn, entry_attrs) = self._call_exc_callbacks(
                            keys, options, e, self.member_command, dn, attrs_list,
                            normalize=self.obj.normalize_dn
                        )
                    except errors.NotFound, e:
                        msg = str(e)
                        (attr, msg) = msg.split(':', 1)
                        failed['member'][self.reverse_attr].append((attr, unicode(msg.strip())))

            except errors.PublicError, e:
                failed['member'][self.reverse_attr].append((attr, unicode(msg)))

        # Wait for the memberof plugin to update the entry
        try:
            entry_attrs = wait_for_memberof(keys, entry_start, completed, self.show_command, adding=False)
        except Exception, e:
            raise errors.ReverseMemberError(verb=_('removed'), exc=str(e))

        for callback in self.POST_CALLBACKS:
            if hasattr(callback, 'im_self'):
                (completed, dn) = callback(
                    ldap, completed, failed, dn, entry_attrs, *keys, **options
                )
            else:
                (completed, dn) = callback(
                    self, ldap, completed, failed, dn, entry_attrs, *keys,
                    **options
                )

        entry_attrs['dn'] = dn
        return dict(
            completed=completed,
            failed=failed,
            result=entry_attrs,
        )

    def pre_callback(self, ldap, dn, *keys, **options):
        return dn

    def post_callback(self, ldap, completed, failed, dn, entry_attrs, *keys, **options):
        return (completed, dn)

    def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
        raise exc

    def interactive_prompt_callback(self, kw):
        return