summaryrefslogtreecommitdiffstats
path: root/ipa-server/xmlrpc-server/funcs.py
diff options
context:
space:
mode:
Diffstat (limited to 'ipa-server/xmlrpc-server/funcs.py')
-rw-r--r--ipa-server/xmlrpc-server/funcs.py2291
1 files changed, 2291 insertions, 0 deletions
diff --git a/ipa-server/xmlrpc-server/funcs.py b/ipa-server/xmlrpc-server/funcs.py
new file mode 100644
index 00000000..cf9e7de5
--- /dev/null
+++ b/ipa-server/xmlrpc-server/funcs.py
@@ -0,0 +1,2291 @@
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 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
+#
+
+import sys
+
+import krbV
+import ldap
+import ldap.dn
+import ipaserver.dsinstance
+import ipaserver.ipaldap
+import copy
+from ipaserver import attrs
+from ipa import version
+from ipa import ipaerror
+from ipa import ipautil
+from urllib import quote,unquote
+from ipa import radius_util
+from ipa import dnsclient
+
+import string
+from types import *
+import re
+import logging
+import subprocess
+
+try:
+ from threading import Lock
+except ImportError:
+ from dummy_threading import Lock
+
+# Need a global to store this between requests
+_LDAPPool = None
+
+ACIContainer = "cn=accounts"
+DefaultUserContainer = "cn=users,cn=accounts"
+DefaultGroupContainer = "cn=groups,cn=accounts"
+DefaultServiceContainer = "cn=services,cn=accounts"
+
+#
+# Apache runs in multi-process mode so each process will have its own
+# connection. This could theoretically drive the total number of connections
+# very high but since this represents just the administrative interface
+# this is not anticipated.
+#
+# The pool consists of two things, a dictionary keyed on the principal name
+# that contains the connection and a list that is used to keep track of the
+# order. If the list fills up just pop the top entry off and you've got
+# the least recently used.
+
+# maxsize = 0 means no limit
+class IPAConnPool:
+ def __init__(self, maxsize = 0):
+ self._dict = {}
+ self._lru = []
+ self._lock = Lock()
+ self._maxsize = maxsize
+ self._ctx = krbV.default_context()
+
+ def getConn(self, host, port, krbccache=None, debug=None):
+ conn = None
+
+ ccache = krbV.CCache(name=krbccache, context=self._ctx)
+ cprinc = ccache.principal()
+
+ conn = ipaserver.ipaldap.IPAdmin(host,port,None,None,None,debug)
+
+ # This will bind the connection
+ try:
+ conn.set_krbccache(krbccache, cprinc.name)
+ except ldap.UNWILLING_TO_PERFORM:
+ raise ipaerror.gen_exception(ipaerror.CONNECTION_UNWILLING)
+ except Exception, e:
+ raise ipaerror.gen_exception(ipaerror.CONNECTION_NO_CONN, nested_exception=e)
+
+ return conn
+
+ def releaseConn(self, conn):
+ if conn is None:
+ return
+
+ conn.unbind_s()
+
+class IPAServer:
+
+ def __init__(self):
+ global _LDAPPool
+ # FIXME, this needs to be auto-discovered
+ self.host = 'localhost'
+ self.port = 389
+ self.sslport = 636
+ self.bindcert = "/usr/share/ipa/cert.pem"
+ self.bindkey = "/usr/share/ipa/key.pem"
+ self.bindca = "/usr/share/ipa/cacert.asc"
+ self.krbctx = krbV.default_context()
+ self.realm = self.krbctx.default_realm
+
+ if _LDAPPool is None:
+ _LDAPPool = IPAConnPool(128)
+ self.basedn = ipautil.realm_to_suffix(self.realm)
+ self.accountsdn = "cn=accounts," + self.basedn
+ self.scope = ldap.SCOPE_SUBTREE
+ self.princ = None
+ self.krbccache = None
+
+ def set_principal(self, princ):
+ self.princ = princ
+
+ def set_krbccache(self, krbccache):
+ self.krbccache = krbccache
+
+ def get_dn_from_principal(self, princ, debug):
+ """Given a kerberos principal get the LDAP uid"""
+ global _LDAPPool
+
+ princ = self.__safe_filter(princ)
+ searchfilter = "(krbPrincipalName=" + princ + ")"
+ # The only anonymous search we should have
+ conn = _LDAPPool.getConn(self.host,self.sslport,self.bindca,self.bindcert,self.bindkey,None,None,debug)
+ try:
+ ent = conn.getEntry(self.accountsdn, self.scope, searchfilter, ['dn'])
+ finally:
+ _LDAPPool.releaseConn(conn)
+
+ return "dn:" + ent.dn
+
+ def __setup_connection(self, opts):
+ """Set up common things done in the connection.
+ If there is a Kerberos credentials cache then return None as the
+ proxy dn and the ccache otherwise return the proxy dn and None as
+ the ccache.
+
+ We only want one or the other used at one time and we prefer
+ the Kerberos credentials cache. So if there is a ccache, return
+ that and None for proxy dn to make calling getConn() easier.
+ """
+
+ debug = "Off"
+
+ if opts is not None:
+ debug = opts.get('ipadebug')
+ if opts.get('krbccache'):
+ self.set_krbccache(opts['krbccache'])
+ self.set_principal(None)
+ else:
+ self.set_krbccache(None)
+ self.set_principal(opts['remoteuser'])
+ else:
+ # The caller should have already set the principal or the
+ # krbccache. If not they'll get an authentication error later.
+ pass
+
+ if self.princ is not None:
+ return self.get_dn_from_principal(self.princ, debug), None, debug
+ else:
+ return None, self.krbccache, debug
+
+ def getConnection(self, opts):
+ """Wrapper around IPAConnPool.getConn() so we don't have to pass
+ around self.* every time a connection is needed.
+
+ For SASL connections (where we have a krbccache) we can't set
+ the SSL variables for certificates. It confuses the ldap
+ module.
+ """
+ global _LDAPPool
+
+ (proxy_dn, krbccache, debug) = self.__setup_connection(opts)
+
+ if krbccache is not None:
+ bindca = None
+ bindcert = None
+ bindkey = None
+ port = self.port
+ else:
+ raise ipaerror.gen_exception(ipaerror.CONNECTION_NO_CCACHE)
+
+ try:
+ conn = _LDAPPool.getConn(self.host,port,krbccache,debug)
+ except ldap.INVALID_CREDENTIALS, e:
+ raise ipaerror.gen_exception(ipaerror.CONNECTION_GSSAPI_CREDENTIALS, nested_exception=e)
+
+ if conn is None:
+ raise ipaerror.gen_exception(ipaerror.CONNECTION_NO_CONN)
+
+ return conn
+
+ def releaseConnection(self, conn):
+ global _LDAPPool
+
+ _LDAPPool.releaseConn(conn)
+
+ def convert_entry(self, ent):
+ entry = dict(ent.data)
+ entry['dn'] = ent.dn
+ # For now convert single entry lists to a string for the ui.
+ # TODO: we need to deal with multi-values better
+ for key,value in entry.iteritems():
+ if isinstance(value,list) or isinstance(value,tuple):
+ if len(value) == 0:
+ entry[key] = ''
+ elif len(value) == 1:
+ entry[key] = value[0]
+ return entry
+
+ # TODO: rethink the get_entry vs get_list API calls.
+ # they currently restrict the data coming back without
+ # restricting scope. For now adding a __get_base/sub_entry()
+ # calls, but the API isn't great.
+ def __get_entry (self, base, scope, searchfilter, sattrs=None, opts=None):
+ """Get a specific entry (with a parametized scope).
+ Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ ent=""
+
+ conn = self.getConnection(opts)
+ try:
+ ent = conn.getEntry(base, scope, searchfilter, sattrs)
+
+ finally:
+ self.releaseConnection(conn)
+
+ return self.convert_entry(ent)
+
+ def __get_base_entry (self, base, searchfilter, sattrs=None, opts=None):
+ """Get a specific entry (with a scope of BASE).
+ Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ return self.__get_entry(base, ldap.SCOPE_BASE, searchfilter, sattrs, opts)
+
+ def __get_sub_entry (self, base, searchfilter, sattrs=None, opts=None):
+ """Get a specific entry (with a scope of SUB).
+ Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ return self.__get_entry(base, ldap.SCOPE_SUBTREE, searchfilter, sattrs, opts)
+
+ def __get_list (self, base, searchfilter, sattrs=None, opts=None):
+ """Gets a list of entries. Each is converted to a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ entries = []
+
+ conn = self.getConnection(opts)
+ try:
+ entries = conn.getList(base, self.scope, searchfilter, sattrs)
+ finally:
+ self.releaseConnection(conn)
+
+ return map(self.convert_entry, entries)
+
+ def __update_entry (self, oldentry, newentry, opts=None):
+ """Update an LDAP entry
+
+ oldentry is a dict
+ newentry is a dict
+ """
+ oldentry = self.convert_scalar_values(oldentry)
+ newentry = self.convert_scalar_values(newentry)
+
+ # Should be able to get this from either the old or new entry
+ # but just in case someone has decided to try changing it, use the
+ # original
+ try:
+ moddn = oldentry['dn']
+ except KeyError:
+ raise ipaerror.gen_exception(ipaerror.LDAP_MISSING_DN)
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.updateEntry(moddn, oldentry, newentry)
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def __safe_filter(self, criteria):
+ """Make sure any arguments used when creating a filter are safe."""
+
+ # TODO: this escaper assumes the python-ldap library will error out
+ # on invalid codepoints. we need to check malformed utf-8 input
+ # where the second byte in a multi-byte character
+ # is (illegally) ')' and make sure python-ldap
+ # bombs out.
+ criteria = re.sub(r'[\(\)\\\*]', ldap_search_escape, criteria)
+
+ return criteria
+
+ def __generate_match_filters(self, search_fields, criteria_words):
+ """Generates a search filter based on a list of words and a list
+ of fields to search against.
+
+ Returns a tuple of two filters: (exact_match, partial_match)"""
+
+ # construct search pattern for a single word
+ # (|(f1=word)(f2=word)...)
+ search_pattern = "(|"
+ for field in search_fields:
+ search_pattern += "(" + field + "=%(match)s)"
+ search_pattern += ")"
+ gen_search_pattern = lambda word: search_pattern % {'match':word}
+
+ # construct the giant match for all words
+ exact_match_filter = "(&"
+ partial_match_filter = "(|"
+ for word in criteria_words:
+ exact_match_filter += gen_search_pattern(word)
+ partial_match_filter += gen_search_pattern("*%s*" % word)
+ exact_match_filter += ")"
+ partial_match_filter += ")"
+
+ return (exact_match_filter, partial_match_filter)
+
+ def __get_schema(self, opts=None):
+ """Retrieves the current LDAP schema from the LDAP server."""
+
+ schema_entry = self.__get_base_entry("", "objectclass=*", ['dn','subschemasubentry'], opts)
+ schema_cn = schema_entry.get('subschemasubentry')
+ schema = self.__get_base_entry(schema_cn, "objectclass=*", ['*'], opts)
+
+ return schema
+
+ def __get_objectclasses(self, opts=None):
+ """Returns a list of available objectclasses that the LDAP
+ server supports. This parses out the syntax, attributes, etc
+ and JUST returns a lower-case list of the names."""
+
+ schema = self.__get_schema(opts)
+
+ objectclasses = schema.get('objectclasses')
+
+ # Convert this list into something more readable
+ result = []
+ for i in range(len(objectclasses)):
+ oc = objectclasses[i].lower().split(" ")
+ result.append(oc[3].replace("'",""))
+
+ return result
+
+ def __has_nsaccountlock(self, dn, opts):
+ """Check to see if an entry has the nsaccountlock attribute.
+ This attribute is provided by the Class of Service plugin so
+ doing a search isn't enough. It is provided by the two
+ entries cn=inactivated and cn=activated. So if the entry has
+ the attribute and isn't in either cn=activated or cn=inactivated
+ then the attribute must be in the entry itself.
+
+ Returns True or False
+ """
+ # First get the entry. If it doesn't have nsaccountlock at all we
+ # can exit early.
+ entry = self.get_entry_by_dn(dn, ['dn', 'nsaccountlock', 'memberof'], opts)
+ if not entry.get('nsaccountlock'):
+ return False
+
+ # Now look to see if they are in activated or inactivated
+ # entry is a member
+ memberof = entry.get('memberof')
+ if isinstance(memberof, basestring):
+ memberof = [memberof]
+ for m in memberof:
+ inactivated = m.find("cn=inactivated")
+ activated = m.find("cn=activated")
+ # if they are in either group that means that the nsaccountlock
+ # value comes from there, otherwise it must be in this entry.
+ if inactivated >= 0 or activated >= 0:
+ return False
+
+ return True
+
+# Higher-level API
+ def version(self, opts=None):
+ """The version of IPA"""
+ logging.debug("IPA: version %d" % version.NUM_VERSION)
+ return version.NUM_VERSION
+
+ def get_aci_entry(self, sattrs, opts=None):
+ """Returns the entry containing access control ACIs."""
+
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: get_aci_entry")
+
+ dn="%s,%s" % (ACIContainer, self.basedn)
+ return self.get_entry_by_dn(dn, sattrs, opts)
+
+# General searches
+
+ def get_entry_by_dn (self, dn, sattrs, opts=None):
+ """Get a specific entry. Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ if not isinstance(dn,basestring) or len(dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ searchfilter = "(objectClass=*)"
+ logging.info("IPA: get_entry_by_dn '%s'" % dn)
+ return self.__get_base_entry(dn, searchfilter, sattrs, opts)
+
+ def get_entry_by_cn (self, cn, sattrs, opts=None):
+ """Get a specific entry by cn. Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+
+ if not isinstance(cn,basestring) or len(cn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: get_entry_by_cn '%s'" % cn)
+ cn = self.__safe_filter(cn)
+ searchfilter = "(cn=" + cn + ")"
+ return self.__get_sub_entry(self.accountsdn, searchfilter, sattrs, opts)
+
+ def update_entry (self, oldentry, newentry, opts=None):
+ """Update an entry in LDAP
+
+ oldentry and newentry are XML-RPC structs.
+
+ If oldentry is not empty then it is used when determine what
+ has changed.
+
+ If oldentry is empty then the value of newentry is compared
+ to the current value of oldentry.
+ """
+ if not newentry:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ if not oldentry:
+ oldentry = self.get_entry_by_dn(newentry.get('dn'), None, opts)
+ if oldentry is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ logging.info("IPA: update_entry '%s'" % newentry.get('dn'))
+ return self.__update_entry(oldentry, newentry, opts)
+
+# User support
+
+ def __is_user_unique(self, uid, opts):
+ """Return True if the uid is unique in the tree, False otherwise."""
+ uid = self.__safe_filter(uid)
+ searchfilter = "(&(uid=%s)(objectclass=posixAccount))" % uid
+
+ try:
+ entry = self.__get_sub_entry(self.accountsdn, searchfilter, ['dn','uid'], opts)
+ return False
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return True
+
+ def __uid_too_long(self, uid, opts):
+ """Verify that the new uid is within the limits we set. This is a
+ very narrow test.
+
+ Returns True if it is longer than allowed
+ False otherwise
+ """
+ if not isinstance(uid,basestring) or len(uid) == 0:
+ # It is bad, but not too long
+ return False
+ logging.debug("IPA: __uid_too_long(%s)" % uid)
+ try:
+ config = self.get_ipa_config(opts)
+ maxlen = int(config.get('ipamaxusernamelength', 0))
+ if maxlen > 0 and len(uid) > maxlen:
+ return True
+ except Exception, e:
+ logging.debug("There was a problem " + str(e))
+
+ return False
+
+ def get_user_by_uid (self, uid, sattrs, opts=None):
+ """Get a specific user's entry. Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+
+ if not isinstance(uid,basestring) or len(uid) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: get_user_by_uid '%s'" % uid)
+ uid = self.__safe_filter(uid)
+ searchfilter = "(uid=" + uid + ")"
+ return self.__get_sub_entry(self.accountsdn, searchfilter, sattrs, opts)
+
+ def get_user_by_principal(self, principal, sattrs, opts=None):
+ """Get a user entry searching by Kerberos Principal Name.
+ Return as a dict of values. Multi-valued fields are
+ represented as lists.
+ """
+
+ if not isinstance(principal,basestring) or len(principal) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ searchfilter = "(krbPrincipalName="+self.__safe_filter(principal)+")"
+ logging.info("IPA: get_user_by_principal '%s'" % principal)
+ return self.__get_sub_entry(self.accountsdn, searchfilter, sattrs, opts)
+
+ def get_user_by_email (self, email, sattrs, opts=None):
+ """Get a specific user's entry. Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+
+ if not isinstance(email,basestring) or len(email) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: get_user_by_email '%s'" % email)
+ email = self.__safe_filter(email)
+ searchfilter = "(mail=" + email + ")"
+ return self.__get_sub_entry(self.basedn, searchfilter, sattrs, opts)
+
+ def get_users_by_manager (self, manager_dn, sattrs, opts=None):
+ """Gets the users that report to a particular manager.
+ """
+
+ if not isinstance(manager_dn,basestring) or len(manager_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: get_user_by_manager '%s'" % manager_dn)
+ manager_dn = self.__safe_filter(manager_dn)
+ searchfilter = "(&(objectClass=person)(manager=%s))" % manager_dn
+
+ try:
+ return self.__get_list(self.accountsdn, searchfilter, sattrs, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return []
+
+ def add_user (self, user, user_container, opts=None):
+ """Add a user in LDAP. Takes as input a dict where the key is the
+ attribute name and the value is either a string or in the case
+ of a multi-valued field a list of values. user_container sets
+ where in the tree the user is placed.
+ """
+ logging.info("IPA: add_user")
+ if not user_container:
+ user_container = DefaultUserContainer
+
+ if not isinstance(user,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(user_container,basestring) or len(user_container) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ if not self.__is_user_unique(user['uid'], opts):
+ raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE)
+ if self.__uid_too_long(user['uid'], opts):
+ raise ipaerror.gen_exception(ipaerror.INPUT_UID_TOO_LONG)
+
+ # dn is set here, not by the user
+ try:
+ del user['dn']
+ except KeyError:
+ pass
+
+ # No need to set empty fields, and they can cause issues when they
+ # get to LDAP, like:
+ # TypeError: ('expected a string in the list', None)
+ for k in user.keys():
+ if not user[k] or len(user[k]) == 0 or (isinstance(user[k],list) and len(user[k]) == 1 and '' in user[k]):
+ del user[k]
+
+ dn="uid=%s,%s,%s" % (ldap.dn.escape_dn_chars(user['uid']),
+ user_container,self.basedn)
+ entry = ipaserver.ipaldap.Entry(dn)
+
+ # FIXME: This should be dynamic and can include just about anything
+
+ # Get our configuration
+ config = self.get_ipa_config(opts)
+
+ # Let us add in some missing attributes
+ if user.get('homedirectory') is None:
+ user['homedirectory'] = '%s/%s' % (config.get('ipahomesrootdir'), user.get('uid'))
+ user['homedirectory'] = user['homedirectory'].replace('//', '/')
+ user['homedirectory'] = user['homedirectory'].rstrip('/')
+ if user.get('loginshell') is None:
+ user['loginshell'] = config.get('ipadefaultloginshell')
+ if user.get('gecos') is None:
+ user['gecos'] = user['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.
+
+ group_dn="cn=%s,%s,%s" % (config.get('ipadefaultprimarygroup'), DefaultGroupContainer, self.basedn)
+ try:
+ default_group = self.get_entry_by_dn(group_dn, ['dn','gidNumber'], opts)
+ if default_group:
+ user['gidnumber'] = default_group.get('gidnumber')
+ except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR), e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, message=None, nested_exception=e.detail)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # Fake an LDAP error so we can return something useful to the user
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND, "The default group for new users, '%s', cannot be found." % config.get('ipadefaultprimarygroup'))
+
+ if user.get('krbprincipalname') is None:
+ user['krbprincipalname'] = "%s@%s" % (user.get('uid'), self.realm)
+
+ # FIXME. This is a hack so we can request separate First and Last
+ # name in the GUI.
+ if user.get('cn') is None:
+ user['cn'] = "%s %s" % (user.get('givenname'),
+ user.get('sn'))
+
+ if user.get('gn'):
+ del user['gn']
+
+ # some required objectclasses
+ entry.setValues('objectClass', (config.get('ipauserobjectclasses')))
+
+ # fill in our new entry with everything sent by the user
+ for u in user:
+ entry.setValues(u, user[u])
+
+ conn = self.getConnection(opts)
+ try:
+ try:
+ res = conn.addEntry(entry)
+ except TypeError, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, "There is a problem with one of the data types.")
+ except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR), e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, message=None, nested_exception=e.detail)
+ except Exception, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, nested_exception=e)
+ try:
+ self.add_user_to_group(user.get('uid'), group_dn, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_DATABASE_ERROR), e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, message=None, nested_exception=e.detail)
+ except Exception, e:
+ raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, "The user was created but adding to group %s failed" % group_dn)
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def get_custom_fields (self, opts=None):
+ """Get the list of custom user fields.
+
+ A schema is a list of dict's of the form:
+ label: The label dispayed to the user
+ field: the attribute name
+ required: true/false
+
+ It is displayed to the user in the order of the list.
+ """
+
+ config = self.get_ipa_config(opts)
+
+ fields = config.get('ipacustomfields')
+
+ if fields is None or fields == '':
+ return []
+
+ fl = fields.split('$')
+ schema = []
+ for x in range(len(fl)):
+ vals = fl[x].split(',')
+ if len(vals) != 3:
+ # Raise?
+ logging.debug("IPA: Invalid field, skipping: %s", vals)
+ d = dict(label=unquote(vals[0]), field=unquote(vals[1]), required=unquote(vals[2]))
+ schema.append(d)
+
+ return schema
+# radius support
+
+ # clients
+ def get_radius_client_by_ip_addr(self, ip_addr, container=None, sattrs=None, opts=None):
+ filter = radius_util.radius_client_filter(ip_addr)
+ basedn = radius_util.radius_clients_basedn(container, self.basedn)
+ return self.__get_sub_entry(basedn, filter, sattrs, opts)
+
+ def __radius_client_exists(self, ip_addr, container, opts):
+ filter = radius_util.radius_client_filter(ip_addr)
+ basedn = radius_util.radius_clients_basedn(container, self.basedn)
+
+ try:
+ entry = self.__get_sub_entry(basedn, filter, ['dn','uid'], opts)
+ return True
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return False
+
+ def add_radius_client (self, client, container=None, opts=None):
+ if container is None:
+ container = radius_util.clients_container
+
+ ip_addr = client['radiusClientIPAddress']
+
+ if self.__radius_client_exists(ip_addr, container, opts):
+ raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE)
+
+ dn = radius_util.radius_client_dn(ip_addr, container, self.basedn)
+ entry = ipaserver.ipaldap.Entry(dn)
+
+ # some required objectclasses
+ entry.setValues('objectClass', 'top', 'radiusClientProfile')
+
+ # fill in our new entry with everything sent by the client
+ for attr in client:
+ entry.setValues(attr, client[attr])
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.addEntry(entry)
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def update_radius_client(self, oldentry, newentry, opts=None):
+ return self.update_entry(oldentry, newentry, opts)
+
+ def delete_radius_client(self, ip_addr, container=None, opts=None):
+ client = self.get_radius_client_by_ip_addr(ip_addr, container, ['dn', 'cn'], opts)
+ if client is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.deleteEntry(client['dn'])
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def find_radius_clients(self, ip_attrs, container=None, sattrs=None, sizelimit=-1, timelimit=-1, opts=None):
+ def gen_filter(objectclass, attr, values):
+ '''Given ('myclass', 'myattr', [v1, v2]) returns
+ (&(objectclass=myclass)(|(myattr=v1)(myattr=v2)))
+ '''
+ # Don't use __safe_filter, prevents wildcarding
+ #attrs = ''.join(['(%s=%s)' % (attr, self.__safe_filter(val)) for val in values])
+ attrs = ''.join(['(%s=%s)' % (attr, val) for val in values])
+ filter = "(&(objectclass=%s)(|%s))" % (objectclass, attrs)
+ return filter
+
+ basedn = radius_util.radius_clients_basedn(container, self.basedn)
+ filter = gen_filter('radiusClientProfile', 'radiusClientIPAddress', ip_attrs)
+ conn = self.getConnection(opts)
+ try:
+ try:
+ results = conn.getListAsync(basedn, self.scope, filter, sattrs, 0, None, None, timelimit, sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ results = [0]
+ finally:
+ self.releaseConnection(conn)
+
+ counter = results[0]
+ results = results[1:]
+ radius_clients = [counter]
+ for radius_client in results:
+ radius_clients.append(self.convert_entry(radius_client))
+
+ return radius_clients
+
+ # profiles
+ def get_radius_profile_by_uid(self, uid, user_profile=True, sattrs=None, opts=None):
+ if user_profile:
+ container = DefaultUserContainer
+ else:
+ container = radius_util.profiles_container
+
+ uid = self.__safe_filter(uid)
+ filter = radius_util.radius_profile_filter(uid)
+ basedn = radius_util.radius_profiles_basedn(container, self.basedn)
+ return self.__get_sub_entry(basedn, filter, sattrs, opts)
+
+ def __radius_profile_exists(self, uid, user_profile, opts):
+ if user_profile:
+ container = DefaultUserContainer
+ else:
+ container = radius_util.profiles_container
+
+ uid = self.__safe_filter(uid)
+ filter = radius_util.radius_profile_filter(uid)
+ basedn = radius_util.radius_profiles_basedn(container, self.basedn)
+
+ try:
+ entry = self.__get_sub_entry(basedn, filter, ['dn','uid'], opts)
+ return True
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return False
+
+ def add_radius_profile (self, profile, user_profile=True, opts=None):
+ uid = profile['uid']
+
+ if self.__radius_profile_exists(uid, user_profile, opts):
+ raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE)
+
+ if user_profile:
+ container = DefaultUserContainer
+ else:
+ container = radius_util.profiles_container
+
+ dn = radius_util.radius_profile_dn(uid, container, self.basedn)
+ entry = ipaserver.ipaldap.Entry(dn)
+
+ # some required objectclasses
+ entry.setValues('objectClass', 'top', 'radiusprofile')
+
+ # fill in our new entry with everything sent by the profile
+ for attr in profile:
+ entry.setValues(attr, profile[attr])
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.addEntry(entry)
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def update_radius_profile(self, oldentry, newentry, opts=None):
+ return self.update_entry(oldentry, newentry, opts)
+
+ def delete_radius_profile(self, uid, user_profile, opts=None):
+ profile = self.get_radius_profile_by_uid(uid, user_profile, ['dn', 'cn'], opts)
+ if profile is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.deleteEntry(profile['dn'])
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def find_radius_profiles(self, uids, user_profile=True, sattrs=None, sizelimit=-1, timelimit=-1, opts=None):
+ def gen_filter(objectclass, attr, values):
+ '''Given ('myclass', 'myattr', [v1, v2]) returns
+ (&(objectclass=myclass)(|(myattr=v1)(myattr=v2)))
+ '''
+ # Don't use __safe_filter, prevents wildcarding
+ #attrs = ''.join(['(%s=%s)' % (attr, self.__safe_filter(val)) for val in values])
+ attrs = ''.join(['(%s=%s)' % (attr, val) for val in values])
+ filter = "(&(objectclass=%s)(|%s))" % (objectclass, attrs)
+ return filter
+
+ if user_profile:
+ container = DefaultUserContainer
+ else:
+ container = radius_util.profiles_container
+
+ filter = gen_filter('radiusprofile', 'uid', uids)
+ basedn="%s,%s" % (container, self.basedn)
+ conn = self.getConnection(opts)
+ try:
+ try:
+ results = conn.getListAsync(basedn, self.scope, filter, sattrs, 0, None, None, timelimit, sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ results = [0]
+ finally:
+ self.releaseConnection(conn)
+
+ counter = results[0]
+ results = results[1:]
+ radius_profiles = [counter]
+ for radius_profile in results:
+ radius_profiles.append(self.convert_entry(radius_profile))
+
+ return radius_profiles
+
+ def set_custom_fields (self, schema, opts=None):
+ """Set the list of custom user fields.
+
+ A schema is a list of dict's of the form:
+ label: The label dispayed to the user
+ field: the attribute name
+ required: true/false
+
+ It is displayed to the user in the order of the list.
+ """
+ if not isinstance(schema,basestring) or len(schema) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ config = self.get_ipa_config(opts)
+
+ # The schema is stored as:
+ # label,field,required$label,field,required$...
+ # quote() from urilib is used to ensure that it is easy to unparse
+
+ stored_schema = ""
+ for i in range(len(schema)):
+ entry = schema[i]
+ entry = quote(entry.get('label')) + "," + quote(entry.get('field')) + "," + quote(entry.get('required'))
+
+ if stored_schema != "":
+ stored_schema = stored_schema + "$" + entry
+ else:
+ stored_schema = entry
+
+ new_config = copy.deepcopy(config)
+ new_config['ipacustomfields'] = stored_schema
+
+ return self.update_entry(config, new_config, opts)
+
+ def get_all_users (self, opts=None):
+ """Return a list containing a User object for each
+ existing user.
+ """
+ logging.info("IPA: get_all_users")
+ searchfilter = "(objectclass=posixAccount)"
+
+ conn = self.getConnection(opts)
+ try:
+ all_users = conn.getList(self.accountsdn, self.scope, searchfilter, None)
+ finally:
+ self.releaseConnection(conn)
+
+ users = []
+ for u in all_users:
+ users.append(self.convert_entry(u))
+
+ return users
+
+ def find_users (self, criteria, sattrs, sizelimit=-1, timelimit=-1,
+ opts=None):
+ """Returns a list: counter followed by the results.
+ If the results are truncated, counter will be set to -1."""
+
+ if not isinstance(criteria,basestring) or len(criteria) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs, list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(sizelimit,int):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(timelimit,int):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: find_users '%s'" % criteria)
+ config = self.get_ipa_config(opts)
+ if timelimit < 0:
+ timelimit = float(config.get('ipasearchtimelimit'))
+ if sizelimit < 0:
+ sizelimit = int(config.get('ipasearchrecordslimit'))
+
+ # Assume the list of fields to search will come from a central
+ # configuration repository. A good format for that would be
+ # a comma-separated list of fields
+ search_fields_conf_str = config.get('ipausersearchfields')
+ search_fields = string.split(search_fields_conf_str, ",")
+
+ criteria = self.__safe_filter(criteria)
+ criteria_words = re.split(r'\s+', criteria)
+ criteria_words = filter(lambda value:value!="", criteria_words)
+ if len(criteria_words) == 0:
+ return [0]
+
+ (exact_match_filter, partial_match_filter) = self.__generate_match_filters(
+ search_fields, criteria_words)
+
+ #
+ # further constrain search to just the objectClass
+ # TODO - need to parameterize this into generate_match_filters,
+ # and work it into the field-specification search feature
+ #
+ exact_match_filter = "(&(objectClass=person)%s)" % exact_match_filter
+ partial_match_filter = "(&(objectClass=person)%s)" % partial_match_filter
+
+ conn = self.getConnection(opts)
+ try:
+ try:
+ exact_results = conn.getListAsync(self.accountsdn, self.scope,
+ exact_match_filter, sattrs, 0, None, None, timelimit,
+ sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ exact_results = [0]
+
+ try:
+ partial_results = conn.getListAsync(self.accountsdn, self.scope,
+ partial_match_filter, sattrs, 0, None, None, timelimit,
+ sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ partial_results = [0]
+ finally:
+ self.releaseConnection(conn)
+
+ exact_counter = exact_results[0]
+ partial_counter = partial_results[0]
+
+ exact_results = exact_results[1:]
+ partial_results = partial_results[1:]
+
+ # Remove exact matches from the partial_match list
+ exact_dns = set(map(lambda e: e.dn, exact_results))
+ partial_results = filter(lambda e: e.dn not in exact_dns,
+ partial_results)
+
+ if (exact_counter == -1) or (partial_counter == -1):
+ counter = -1
+ else:
+ counter = len(exact_results) + len(partial_results)
+
+ users = [counter]
+ for u in exact_results + partial_results:
+ users.append(self.convert_entry(u))
+
+ return users
+
+ def convert_scalar_values(self, orig_dict):
+ """LDAP update dicts expect all values to be a list (except for dn).
+ This method converts single entries to a list."""
+ if not orig_dict or not isinstance(orig_dict, dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ new_dict={}
+ for (k,v) in orig_dict.iteritems():
+ if not isinstance(v, list) and k != 'dn':
+ v = [v]
+ new_dict[k] = v
+
+ return new_dict
+
+ def update_user (self, oldentry, newentry, opts=None):
+ """Wrapper around update_entry with user-specific handling.
+
+ oldentry and newentry are XML-RPC structs.
+
+ If oldentry is not empty then it is used when determine what
+ has changed.
+
+ If oldentry is empty then the value of newentry is compared
+ to the current value of oldentry.
+
+ If you want to change the RDN of a user you must use
+ this function. update_entry will fail.
+ """
+ logging.info("IPA: update_user")
+ if not isinstance(newentry,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if oldentry and not isinstance(oldentry,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not oldentry:
+ oldentry = self.get_entry_by_dn(newentry.get('dn'), None, opts)
+ if oldentry is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ newrdn = 0
+
+ if oldentry.get('uid') != newentry.get('uid'):
+ if self.__uid_too_long(newentry.get('uid'), opts):
+ raise ipaerror.gen_exception(ipaerror.INPUT_UID_TOO_LONG)
+ # RDN change
+ conn = self.getConnection(opts)
+ try:
+ res = conn.updateRDN(oldentry.get('dn'), "uid=" + newentry.get('uid'))
+ newdn = oldentry.get('dn')
+ newdn = newdn.replace("uid=%s" % oldentry.get('uid'), "uid=%s" % newentry.get('uid'))
+
+ # Now fix up the dns and uids so they aren't seen as having
+ # changed.
+ oldentry['dn'] = newdn
+ newentry['dn'] = newdn
+ oldentry['uid'] = newentry['uid']
+ newrdn = 1
+ finally:
+ self.releaseConnection(conn)
+
+ # Get our configuration
+ config = self.get_ipa_config(opts)
+
+ # Make sure we have the latest object classes
+ # newentry['objectclass'] = uniq_list(newentry.get('objectclass') + config.get('ipauserobjectclasses'))
+
+ try:
+ rv = self.update_entry(oldentry, newentry, opts)
+ return rv
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ # This means that there was just an rdn change, nothing else.
+ if newrdn == 1:
+ return "Success"
+ else:
+ raise
+
+ def mark_entry_active (self, dn, opts=None):
+ """Mark an entry as active in LDAP."""
+
+ # This can be tricky. The entry itself can be marked inactive
+ # by being in the inactivated group. It can also be inactivated by
+ # being the member of an inactive group.
+ #
+ # First we try to remove the entry from the inactivated group. Then
+ # if it is still inactive we have to add it to the activated group
+ # which will override the group membership.
+
+ if not dn:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ res = ""
+ # First, check the entry status
+ entry = self.get_entry_by_dn(dn, ['dn', 'nsAccountlock'], opts)
+
+ if entry.get('nsaccountlock', 'false').lower() == "false":
+ logging.debug("IPA: already active")
+ raise ipaerror.gen_exception(ipaerror.STATUS_ALREADY_ACTIVE)
+
+ if self.__has_nsaccountlock(dn, opts):
+ logging.debug("IPA: appears to have the nsaccountlock attribute")
+ raise ipaerror.gen_exception(ipaerror.STATUS_HAS_NSACCOUNTLOCK)
+
+ group = self.get_entry_by_cn("inactivated", None, opts)
+ try:
+ self.remove_member_from_group(entry.get('dn'), group.get('dn'), opts)
+ except ipaerror.exception_for(ipaerror.STATUS_NOT_GROUP_MEMBER):
+ # Perhaps the user is there as a result of group membership
+ pass
+
+ # Now they aren't a member of inactivated directly, what is the status
+ # now?
+ entry = self.get_entry_by_dn(dn, ['dn', 'nsAccountlock'], opts)
+
+ if entry.get('nsaccountlock', 'false').lower() == "false":
+ # great, we're done
+ logging.debug("IPA: removing from inactivated did it.")
+ return res
+
+ # So still inactive, add them to activated
+ group = self.get_entry_by_cn("activated", None, opts)
+ res = self.add_member_to_group(dn, group.get('dn'), opts)
+ logging.debug("IPA: added to activated.")
+
+ return res
+
+ def mark_entry_inactive (self, dn, opts=None):
+ """Mark an entry as inactive in LDAP."""
+
+ if not dn:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ entry = self.get_entry_by_dn(dn, ['dn', 'nsAccountlock', 'memberOf'], opts)
+
+ if entry.get('nsaccountlock', 'false').lower() == "true":
+ logging.debug("IPA: already marked as inactive")
+ raise ipaerror.gen_exception(ipaerror.STATUS_ALREADY_INACTIVE)
+
+ if self.__has_nsaccountlock(dn, opts):
+ logging.debug("IPA: appears to have the nsaccountlock attribute")
+ raise ipaerror.gen_exception(ipaerror.STATUS_HAS_NSACCOUNTLOCK)
+
+ # First see if they are in the activated group as this will override
+ # the our inactivation.
+ group = self.get_entry_by_cn("activated", None, opts)
+ try:
+ self.remove_member_from_group(dn, group.get('dn'), opts)
+ except ipaerror.exception_for(ipaerror.STATUS_NOT_GROUP_MEMBER):
+ # this is fine, they may not be explicitly in this group
+ pass
+
+ # Now add them to inactivated
+ group = self.get_entry_by_cn("inactivated", None, opts)
+ res = self.add_member_to_group(dn, group.get('dn'), opts)
+
+ return res
+
+ def mark_user_active(self, uid, opts=None):
+ """Mark a user as active"""
+
+ if not isinstance(uid,basestring) or len(uid) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ user = self.get_user_by_uid(uid, ['dn', 'uid'], opts)
+ logging.info("IPA: mark_user_active '%s'" % user.get('dn'))
+ return self.mark_entry_active(user.get('dn'))
+
+ def mark_user_inactive(self, uid, opts=None):
+ """Mark a user as inactive"""
+
+ if not isinstance(uid,basestring) or len(uid) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if uid == "admin":
+ raise ipaerror.gen_exception(ipaerror.INPUT_CANT_INACTIVATE)
+ user = self.get_user_by_uid(uid, ['dn', 'uid'], opts)
+ logging.info("IPA: mark_user_inactive '%s'" % user.get('dn'))
+ return self.mark_entry_inactive(user.get('dn'))
+
+ def delete_user (self, uid, opts=None):
+ """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.
+ """
+ if not isinstance(uid,basestring) or len(uid) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if uid == "admin":
+ raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED)
+ logging.info("IPA: delete_user '%s'" % uid)
+ user = self.get_user_by_uid(uid, ['dn', 'uid', 'objectclass'], opts)
+ if user is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.deleteEntry(user['dn'])
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def modifyPassword (self, principal, oldpass, newpass, opts=None):
+ """Set/Reset a user's password
+
+ uid tells us who's password to change
+ oldpass is the old password (if available)
+ newpass is the new password
+ """
+ if not isinstance(principal,basestring) or len(principal) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if oldpass and not isinstance(oldpass,basestring):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(newpass,basestring) or len(newpass) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: modifyPassword '%s'" % principal)
+
+ user = self.get_user_by_principal(principal, ['krbprincipalname'], opts)
+ if user is None or user['krbprincipalname'] != principal:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.modifyPassword(user['dn'], oldpass, newpass)
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+# Group support
+
+ def __is_group_unique(self, cn, opts):
+ """Return True if the cn is unique in the tree, False otherwise."""
+ cn = self.__safe_filter(cn)
+ searchfilter = "(&(cn=%s)(objectclass=posixGroup))" % cn
+
+ try:
+ entry = self.__get_sub_entry(self.accountsdn, searchfilter, ['dn','cn'], opts)
+ return False
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return True
+
+ def get_groups_by_member (self, member_dn, sattrs, opts=None):
+ """Get all of the groups an object is explicitly a member of.
+
+ This does not include groups an entry may be a member of as a
+ result of recursion (being a group that is a member of another
+ group). In other words, this searches on 'member' and not
+ 'memberof'.
+
+ Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ if not isinstance(member_dn,basestring) or len(member_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: get_groups_by_member '%s'" % member_dn)
+
+ member_dn = self.__safe_filter(member_dn)
+ searchfilter = "(&(objectClass=posixGroup)(member=%s))" % member_dn
+
+ try:
+ return self.__get_list(self.accountsdn, searchfilter, sattrs, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return []
+
+ def add_group (self, group, group_container, opts=None):
+ """Add a group in LDAP. Takes as input a dict where the key is the
+ attribute name and the value is either a string or in the case
+ of a multi-valued field a list of values. group_container sets
+ where in the tree the group is placed."""
+ if not group_container:
+ group_container = DefaultGroupContainer
+
+ if not isinstance(group,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_container,basestring) or len(group_container) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ if not self.__is_group_unique(group['cn'], opts):
+ raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE)
+
+ # Get our configuration
+ config = self.get_ipa_config(opts)
+
+ dn="cn=%s,%s,%s" % (ldap.dn.escape_dn_chars(group['cn']),
+ group_container,self.basedn)
+ logging.info("IPA: add_group '%s'" % dn)
+ entry = ipaserver.ipaldap.Entry(dn)
+
+ # some required objectclasses
+ entry.setValues('objectClass', (config.get('ipagroupobjectclasses')))
+
+ # No need to explicitly set gidNumber. The dna_plugin will do this
+ # for us if the value isn't provided by the user.
+
+ # fill in our new entry with everything sent by the user
+ for g in group:
+ entry.setValues(g, group[g])
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.addEntry(entry)
+ finally:
+ self.releaseConnection(conn)
+
+ def find_groups (self, criteria, sattrs, sizelimit=-1, timelimit=-1,
+ opts=None):
+ """Return a list containing a User object for each
+ existing group that matches the criteria.
+ """
+ if not isinstance(criteria,basestring) or len(criteria) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs, list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(sizelimit,int):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(timelimit,int):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: find groups '%s'" % criteria)
+
+ config = self.get_ipa_config(opts)
+ if timelimit < 0:
+ timelimit = float(config.get('ipasearchtimelimit'))
+ if sizelimit < 0:
+ sizelimit = int(config.get('ipasearchrecordslimit'))
+
+ # Assume the list of fields to search will come from a central
+ # configuration repository. A good format for that would be
+ # a comma-separated list of fields
+ search_fields_conf_str = config.get('ipagroupsearchfields')
+ search_fields = string.split(search_fields_conf_str, ",")
+
+ criteria = self.__safe_filter(criteria)
+ criteria_words = re.split(r'\s+', criteria)
+ criteria_words = filter(lambda value:value!="", criteria_words)
+ if len(criteria_words) == 0:
+ return [0]
+
+ (exact_match_filter, partial_match_filter) = self.__generate_match_filters(
+ search_fields, criteria_words)
+
+ #
+ # further constrain search to just the objectClass
+ # TODO - need to parameterize this into generate_match_filters,
+ # and work it into the field-specification search feature
+ #
+ exact_match_filter = "(&(objectClass=posixGroup)%s)" % exact_match_filter
+ partial_match_filter = "(&(objectClass=posixGroup)%s)" % partial_match_filter
+
+ #
+ # TODO - copy/paste from find_users. needs to be refactored
+ #
+ conn = self.getConnection(opts)
+ try:
+ try:
+ exact_results = conn.getListAsync(self.accountsdn, self.scope,
+ exact_match_filter, sattrs, 0, None, None, timelimit,
+ sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ exact_results = [0]
+
+ try:
+ partial_results = conn.getListAsync(self.accountsdn, self.scope,
+ partial_match_filter, sattrs, 0, None, None, timelimit,
+ sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ partial_results = [0]
+ finally:
+ self.releaseConnection(conn)
+
+ exact_counter = exact_results[0]
+ partial_counter = partial_results[0]
+
+ exact_results = exact_results[1:]
+ partial_results = partial_results[1:]
+
+ # Remove exact matches from the partial_match list
+ exact_dns = set(map(lambda e: e.dn, exact_results))
+ partial_results = filter(lambda e: e.dn not in exact_dns,
+ partial_results)
+
+ if (exact_counter == -1) or (partial_counter == -1):
+ counter = -1
+ else:
+ counter = len(exact_results) + len(partial_results)
+
+ groups = [counter]
+ for u in exact_results + partial_results:
+ groups.append(self.convert_entry(u))
+
+ return groups
+
+ def add_member_to_group(self, member_dn, group_dn, opts=None):
+ """Add a member to an existing group.
+ """
+ if not isinstance(member_dn,basestring) or len(member_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: add_member_to_group '%s' to '%s'" % (member_dn, group_dn))
+ if member_dn.lower() == group_dn.lower():
+ raise ipaerror.gen_exception(ipaerror.INPUT_SAME_GROUP)
+
+ old_group = self.get_entry_by_dn(group_dn, None, opts)
+ if old_group is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+ new_group = copy.deepcopy(old_group)
+
+ # check to make sure member_dn exists
+ member_entry = self.__get_base_entry(member_dn, "(objectClass=*)", ['dn','uid'], opts)
+ if not member_entry:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ if new_group.get('member') is not None:
+ if isinstance(new_group.get('member'),basestring):
+ new_group['member'] = [new_group['member']]
+ new_group['member'].append(member_dn)
+ else:
+ new_group['member'] = member_dn
+
+ try:
+ ret = self.__update_entry(old_group, new_group, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ raise
+ return ret
+
+ def add_members_to_group(self, member_dns, group_dn, opts=None):
+ """Given a list of dn's, add them to the group cn denoted by group
+ Returns a list of the member_dns that were not added to the group.
+ """
+ if not (isinstance(member_dns,list) or isinstance(member_dns,basestring)):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ if not member_dns or not group_dn:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: add_members_to_group '%s'" % group_dn)
+
+ failed = []
+
+ if (isinstance(member_dns,basestring)):
+ member_dns = [member_dns]
+
+ for member_dn in member_dns:
+ try:
+ self.add_member_to_group(member_dn, group_dn, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ # User is already in the group
+ failed.append(member_dn)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # User or the group does not exist
+ failed.append(member_dn)
+
+ return failed
+
+ def remove_member_from_group(self, member_dn, group_dn, opts=None):
+ """Remove a member_dn from an existing group.
+ """
+ if not isinstance(member_dn,basestring) or len(member_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ old_group = self.get_entry_by_dn(group_dn, None, opts)
+ if old_group is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+ if old_group.get('cn') == "admins":
+ member = self.get_entry_by_dn(member_dn, ['dn','uid'], opts)
+ if member.get('uid') == "admin":
+ raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED_IN_ADMINS)
+ logging.info("IPA: remove_member_from_group '%s' from '%s'" % (member_dn, group_dn))
+ new_group = copy.deepcopy(old_group)
+
+ if new_group.get('member') is not None:
+ if isinstance(new_group.get('member'),basestring):
+ new_group['member'] = [new_group['member']]
+ for i in range(len(new_group['member'])):
+ new_group['member'][i] = ipaserver.ipaldap.IPAdmin.normalizeDN(new_group['member'][i])
+ try:
+ new_group['member'].remove(member_dn)
+ except ValueError:
+ # member is not in the group
+ # FIXME: raise more specific error?
+ raise ipaerror.gen_exception(ipaerror.STATUS_NOT_GROUP_MEMBER)
+ else:
+ # Nothing to do if the group has no members
+ raise ipaerror.gen_exception(ipaerror.STATUS_NOT_GROUP_MEMBER)
+
+ try:
+ ret = self.__update_entry(old_group, new_group, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ raise
+ return ret
+
+ def remove_members_from_group(self, member_dns, group_dn, opts=None):
+ """Given a list of member dn's remove them from the group.
+ Returns a list of the members not removed from the group.
+ """
+ if not (isinstance(member_dns,list) or isinstance(member_dns,basestring)):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: remove_members_from_group '%s'" % group_dn)
+ failed = []
+
+ if (isinstance(member_dns,basestring)):
+ member_dns = [member_dns]
+
+ for member_dn in member_dns:
+ try:
+ self.remove_member_from_group(member_dn, group_dn, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ # member is not in the group
+ failed.append(member_dn)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # member_dn or the group does not exist
+ failed.append(member_dn)
+ except ipaerror.exception_for(ipaerror.STATUS_NOT_GROUP_MEMBER):
+ # not a member of the group
+ failed.append(member_dn)
+ except ipaerror.exception_for(ipaerror.INPUT_ADMIN_REQUIRED_IN_ADMINS):
+ # Can't remove admin from admins group
+ failed.append(member_dn)
+
+ return failed
+
+ def add_user_to_group(self, user_uid, group_dn, opts=None):
+ """Add a user to an existing group.
+ """
+ if not isinstance(user_uid,basestring) or len(user_uid) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: add_user_to_group '%s' to '%s'" % (user_uid, group_dn))
+
+ user = self.get_user_by_uid(user_uid, ['dn', 'uid', 'objectclass'], opts)
+ if user is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ return self.add_member_to_group(user['dn'], group_dn, opts)
+
+ def add_users_to_group(self, user_uids, group_dn, opts=None):
+ """Given a list of user uid's add them to the group cn denoted by group
+ Returns a list of the users were not added to the group.
+ """
+ if not (isinstance(user_uids,list) or isinstance(user_uids,basestring)):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: add_users_to_group '%s'" % group_dn)
+ failed = []
+
+ if (isinstance(user_uids,basestring)):
+ user_uids = [user_uids]
+
+ for user_uid in user_uids:
+ try:
+ self.add_user_to_group(user_uid, group_dn, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ # User is already in the group
+ failed.append(user_uid)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # User or the group does not exist
+ failed.append(user_uid)
+
+ return failed
+
+ def remove_user_from_group(self, user_uid, group_dn, opts=None):
+ """Remove a user from an existing group.
+ """
+ if not isinstance(user_uid,basestring) or len(user_uid) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: remove_user_from_group '%s' from '%s'" % (user_uid, group_dn))
+ user = self.get_user_by_uid(user_uid, ['dn', 'uid', 'objectclass'], opts)
+ if user is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ return self.remove_member_from_group(user['dn'], group_dn, opts)
+
+ def remove_users_from_group(self, user_uids, group_dn, opts=None):
+ """Given a list of user uid's remove them from the group
+ Returns a list of the user uids not removed from the group.
+ """
+ if not (isinstance(user_uids,list) or isinstance(user_uids,basestring)):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: remove_users_from_group '%s'" % group_dn)
+ failed = []
+
+ if (isinstance(user_uids,basestring)):
+ user_uids = [user_uids]
+
+ for user_uid in user_uids:
+ try:
+ self.remove_user_from_group(user_uid, group_dn, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ # User is not in the group
+ failed.append(user_uid)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # User or the group does not exist
+ failed.append(user_uid)
+
+ return failed
+
+ def add_groups_to_user(self, group_dns, user_dn, opts=None):
+ """Given a list of group dn's add them to the user.
+
+ Returns a list of the group dns that were not added.
+ """
+ if not (isinstance(group_dns,list) or isinstance(group_dns,basestring)):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(user_dn,basestring) or len(user_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: add_groups_to_user '%s'" % user_dn)
+ failed = []
+
+ if (isinstance(group_dns, basestring)):
+ group_dns = [group_dns]
+
+ for group_dn in group_dns:
+ try:
+ self.add_member_to_group(user_dn, group_dn, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ # User is already in the group
+ failed.append(group_dn)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # User or the group does not exist
+ failed.append(group_dn)
+
+ return failed
+
+ def remove_groups_from_user(self, group_dns, user_dn, opts=None):
+ """Given a list of group dn's remove them from the user.
+
+ Returns a list of the group dns that were not removed.
+ """
+ if not (isinstance(group_dns,list) or isinstance(group_dns,basestring)):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(user_dn,basestring) or len(user_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ logging.info("IPA: remove_groups_from_user '%s'" % user_dn)
+ failed = []
+
+ if (isinstance(group_dns,basestring)):
+ group_dns = [group_dns]
+
+ for group_dn in group_dns:
+ try:
+ self.remove_member_from_group(user_dn, group_dn, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ # User is not in the group
+ failed.append(group_dn)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ # User or the group does not exist
+ failed.append(group_dn)
+ except ipaerror.exception_for(ipaerror.STATUS_NOT_GROUP_MEMBER):
+ # User is not in the group
+ failed.append(group_dn)
+ except ipaerror.exception_for(ipaerror.INPUT_ADMIN_REQUIRED_IN_ADMINS):
+ # Can't remove admin from admins group
+ failed.append(member_dn)
+
+ return failed
+
+ def update_group (self, oldentry, newentry, opts=None):
+ """Wrapper around update_entry with group-specific handling.
+
+ oldentry and newentry are XML-RPC structs.
+
+ If oldentry is not empty then it is used when determine what
+ has changed.
+
+ If oldentry is empty then the value of newentry is compared
+ to the current value of oldentry.
+
+ If you want to change the RDN of a group you must use
+ this function. update_entry will fail.
+ """
+ if not isinstance(newentry,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if oldentry and not isinstance(oldentry,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not oldentry:
+ oldentry = self.get_entry_by_dn(newentry.get('dn'), None, opts)
+ if oldentry is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ logging.info("IPA: update_group '%s'" % oldentry.get('cn'))
+ newrdn = 0
+
+ oldcn=oldentry.get('cn')
+ newcn=newentry.get('cn')
+ if isinstance(oldcn,basestring):
+ oldcn = [oldcn]
+ if isinstance(newcn,basestring):
+ newcn = [newcn]
+
+ if "admins" in oldcn:
+ raise ipaerror.gen_exception(ipaerror.INPUT_ADMINS_IMMUTABLE)
+
+ oldcn.sort()
+ newcn.sort()
+ if oldcn != newcn:
+ # RDN change
+ conn = self.getConnection(opts)
+ try:
+ res = conn.updateRDN(oldentry.get('dn'), "cn=" + newcn[0])
+ newdn = oldentry.get('dn')
+ newcn = newentry.get('cn')
+ if isinstance(newcn,basestring):
+ newcn = [newcn]
+
+ # Ick. Need to find the exact cn used in the old DN so we'll
+ # walk the list of cns and skip the obviously bad ones:
+ for c in oldentry.get('dn').split("cn="):
+ if c and c != "groups" and not c.startswith("accounts"):
+ newdn = newdn.replace("cn=%s" % c, "cn=%s," % newcn[0])
+ break
+
+ # Now fix up the dns and cns so they aren't seen as having
+ # changed.
+ oldentry['dn'] = newdn
+ newentry['dn'] = newdn
+ oldentry['cn'] = newentry.get('cn')
+ newrdn = 1
+ finally:
+ self.releaseConnection(conn)
+
+ # Get our configuration
+ config = self.get_ipa_config(opts)
+
+ # Make sure we have the latest object classes
+ # newentry['objectclass'] = uniq_list(newentry.get('objectclass') + config.get('ipagroupobjectclasses'))
+
+ try:
+ rv = self.update_entry(oldentry, newentry, opts)
+ return rv
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ if newrdn == 1:
+ # This means that there was just the rdn change, no other
+ # attributes
+ return "Success"
+ else:
+ raise
+
+ def delete_group (self, group_dn, opts=None):
+ """Delete a group
+ group_dn is the DN of the group to delete
+
+ The memberOf plugin handles removing the group from any other
+ groups.
+ """
+ if not isinstance(group_dn,basestring) or len(group_dn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ group = self.get_entry_by_dn(group_dn, ['dn', 'cn'], opts)
+ if group is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+ logging.info("IPA: delete_group '%s'" % group_dn)
+
+ # We have 2 special groups, don't allow them to be removed
+ if "admins" in group.get('cn') or "editors" in group.get('cn'):
+ raise ipaerror.gen_exception(ipaerror.CONFIG_REQUIRED_GROUPS)
+
+ # Don't allow the default user group to be removed
+ config=self.get_ipa_config(opts)
+ default_group = self.get_entry_by_cn(config.get('ipadefaultprimarygroup'), None, opts)
+ if group_dn == default_group.get('dn'):
+ raise ipaerror.gen_exception(ipaerror.CONFIG_DEFAULT_GROUP)
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.deleteEntry(group_dn)
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def add_group_to_group(self, group, tgroup, opts=None):
+ """Add a group to an existing group.
+ group is a DN of the group to add
+ tgroup is the DN of the target group to be added to
+ """
+ if not isinstance(group,basestring) or len(group) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(tgroup,basestring) or len(tgroup) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if group.lower() == tgroup.lower():
+ raise ipaerror.gen_exception(ipaerror.INPUT_SAME_GROUP)
+ old_group = self.get_entry_by_dn(tgroup, None, opts)
+ if old_group is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+ logging.info("IPA: add_group_to_group '%s' to '%s'" % (group, tgroup))
+ new_group = copy.deepcopy(old_group)
+
+ group_dn = self.get_entry_by_dn(group, ['dn', 'cn', 'objectclass'], opts)
+ if group_dn is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ if new_group.get('member') is not None:
+ if isinstance(new_group.get('member'),basestring):
+ new_group['member'] = [new_group['member']]
+ new_group['member'].append(group_dn['dn'])
+ else:
+ new_group['member'] = group_dn['dn']
+
+ try:
+ ret = self.__update_entry(old_group, new_group, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_EMPTY_MODLIST):
+ raise
+ return ret
+
+ def attrs_to_labels(self, attr_list, opts=None):
+ """Take a list of LDAP attributes and convert them to more friendly
+ labels."""
+ if not (isinstance(attr_list,list)):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: attrs_to_labels")
+
+ label_list = {}
+
+ for a in attr_list:
+ label_list[a] = attrs.attr_label_list.get(a,a)
+
+ return label_list
+
+ def get_all_attrs(self, opts=None):
+ """We have a list of hardcoded attributes -> readable labels. Return
+ that complete list if someone wants it.
+ """
+ logging.info("IPA: get_all_attrs")
+
+ return attrs.attr_label_list
+
+ def group_members(self, groupdn, attr_list, membertype, opts=None):
+ """Do a memberOf search of groupdn and return the attributes in
+ attr_list (an empty list returns all attributes).
+
+ membertype = 0 all members returned
+ membertype = 1 only direct members are returned
+ membertype = 2 only inherited members are returned
+
+ Members may be included in a group as a result of being a member
+ of a group that is a member of the group being queried.
+ """
+
+ if not isinstance(groupdn,basestring) or len(groupdn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if attr_list is not None and not isinstance(attr_list,list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if membertype is not None and not isinstance(membertype,int):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if membertype is None:
+ membertype = 0
+ if membertype < 0 or membertype > 3:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: group_members '%s' %d" % (groupdn, membertype))
+ config = self.get_ipa_config(opts)
+ timelimit = float(config.get('ipasearchtimelimit'))
+
+ sizelimit = int(config.get('ipasearchrecordslimit'))
+
+ groupdn = self.__safe_filter(groupdn)
+ searchfilter = "(memberOf=%s)" % groupdn
+
+ if attr_list is None:
+ attr_list = []
+ attr_list.append("member")
+
+ conn = self.getConnection(opts)
+ try:
+ try:
+ results = conn.getListAsync(self.accountsdn, self.scope,
+ searchfilter, attr_list, 0, None, None, timelimit,
+ sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ results = [0]
+ finally:
+ self.releaseConnection(conn)
+
+ counter = results[0]
+ results = results[1:]
+
+ if membertype == 0:
+ entries = [counter]
+ for e in results:
+ entries.append(self.convert_entry(e))
+
+ return entries
+
+ group = self.get_entry_by_dn(groupdn, ['dn', 'member'], opts)
+ real_members = group.get('member')
+ if isinstance(real_members, basestring):
+ real_members = [real_members]
+ if real_members is None:
+ real_members = []
+
+ # Normalize all the dns
+ for i in range(len(real_members)):
+ real_members[i] = ipaserver.ipaldap.IPAdmin.normalizeDN(real_members[i])
+
+ entries = [0]
+ for e in results:
+ if ipaserver.ipaldap.IPAdmin.normalizeDN(e.dn) not in real_members:
+ if membertype == 2:
+ entries.append(self.convert_entry(e))
+ else:
+ if membertype == 1:
+ entries.append(self.convert_entry(e))
+
+ if len(entries) > 1:
+ entries[0] = len(entries) - 1
+
+ return entries
+
+ def mark_group_active(self, cn, opts=None):
+ """Mark a group as active"""
+
+ if not isinstance(cn,basestring) or len(cn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ logging.info("IPA: mark_group_active '%s'" % cn)
+ group = self.get_entry_by_cn(cn, ['dn', 'cn'], opts)
+ return self.mark_entry_active(group.get('dn'))
+
+ def mark_group_inactive(self, cn, opts=None):
+ """Mark a group as inactive"""
+
+ if not isinstance(cn,basestring) or len(cn) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if cn == "admins" or cn == "editors":
+ raise ipaerror.gen_exception(ipaerror.INPUT_CANT_INACTIVATE)
+ logging.info("IPA: mark_group_inactive '%s'" % cn)
+ group = self.get_entry_by_cn(cn, ['dn', 'uid'], opts)
+ return self.mark_entry_inactive(group.get('dn'))
+
+ def __is_service_unique(self, name, opts):
+ """Return True if the uid is unique in the tree, False otherwise."""
+ name = self.__safe_filter(name)
+ searchfilter = "(&(krbprincipalname=%s)(objectclass=krbPrincipal))" % name
+
+ try:
+ entry = self.__get_sub_entry(self.accountsdn, searchfilter, ['dn','krbprincipalname'], opts)
+ return False
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ return True
+
+ def add_service_principal(self, name, force, opts=None):
+ """Given a name of the form: service/FQDN create a service
+ principal for it in the default realm.
+
+ Ensure that the principal points at a DNS A record so it will
+ work with Kerberos unless force is set to 1"""
+ if not name:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ try:
+ f = int(force)
+ except ValueError:
+ f = 1
+ logging.info("IPA: add_service_principal '%s' (%d)" % (name, f))
+
+ # Break down the principal into its component parts, which may or
+ # may not include the realm.
+ sp = name.split('/')
+ if len(sp) != 2:
+ raise ipaerror.gen_exception(ipaerror.INPUT_MALFORMED_SERVICE_PRINCIPAL)
+ service = sp[0]
+
+ sr = sp[1].split('@')
+ if len(sr) == 1:
+ hostname = sr[0].lower()
+ realm = self.realm
+ elif len(sr) == 2:
+ hostname = sr[0].lower()
+ realm = sr[1]
+ else:
+ raise ipaerror.gen_exception(ipaerror.INPUT_MALFORMED_SERVICE_PRINCIPAL)
+
+ if not f:
+ fqdn = hostname + "."
+ rs = dnsclient.query(fqdn, dnsclient.DNS_C_IN, dnsclient.DNS_T_A)
+ if len(rs) == 0:
+ logging.debug("IPA: DNS A record lookup failed for '%s'" % hostname)
+ raise ipaerror.gen_exception(ipaerror.INPUT_NOT_DNS_A_RECORD)
+ else:
+ logging.debug("IPA: found %d records for '%s'" % (len(rs), hostname))
+
+ service_container = DefaultServiceContainer
+
+ # At some point we'll support multiple realms
+ if (realm != self.realm):
+ raise ipaerror.gen_exception(ipaerror.INPUT_REALM_MISMATCH)
+
+ # Put the principal back together again
+ princ_name = service + "/" + hostname + "@" + realm
+
+ conn = self.getConnection(opts)
+ if not self.__is_service_unique(princ_name, opts):
+ raise ipaerror.gen_exception(ipaerror.LDAP_DUPLICATE)
+
+ dn = "krbprincipalname=%s,%s,%s" % (ldap.dn.escape_dn_chars(princ_name),
+ service_container,self.basedn)
+ entry = ipaserver.ipaldap.Entry(dn)
+
+ entry.setValues('objectclass', 'krbPrincipal', 'krbPrincipalAux', 'krbTicketPolicyAux')
+ entry.setValues('krbprincipalname', princ_name)
+
+ try:
+ res = conn.addEntry(entry)
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def delete_service_principal (self, principal, opts=None):
+ """Delete a service principal.
+
+ principal is the full DN of the entry to delete.
+
+ This should be called with much care.
+ """
+ if not isinstance(principal,basestring) or len(principal) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ entry = self.get_entry_by_dn(principal, ['dn', 'objectclass'], opts)
+ if entry is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+ dn_list = ldap.explode_dn(entry['dn'].lower())
+ if "cn=kerberos" in dn_list:
+ raise ipaerror.gen_exception(ipaerror.INPUT_SERVICE_PRINCIPAL_REQUIRED)
+ logging.info("IPA: delete_service_principal '%s'" % principal)
+
+ conn = self.getConnection(opts)
+ try:
+ res = conn.deleteEntry(entry['dn'])
+ finally:
+ self.releaseConnection(conn)
+ return res
+
+ def find_service_principal(self, criteria, sattrs, sizelimit=-1,
+ timelimit=-1, opts=None):
+ """Returns a list: counter followed by the results.
+ If the results are truncated, counter will be set to -1."""
+ if not isinstance(criteria,basestring) or len(criteria) == 0:
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs, list):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(sizelimit,int):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not isinstance(timelimit,int):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+
+ config = self.get_ipa_config(opts)
+ if timelimit < 0:
+ timelimit = float(config.get('ipasearchtimelimit'))
+ if sizelimit < 0:
+ sizelimit = int(config.get('ipasearchrecordslimit'))
+
+ search_fields = ["krbprincipalname"]
+ logging.info("IPA: find_service_principal '%s'" % criteria)
+
+ criteria = self.__safe_filter(criteria)
+ criteria_words = re.split(r'\s+', criteria)
+ criteria_words = filter(lambda value:value!="", criteria_words)
+ if len(criteria_words) == 0:
+ return [0]
+
+ (exact_match_filter, partial_match_filter) = self.__generate_match_filters(
+ search_fields, criteria_words)
+
+ #
+ # further constrain search to just the objectClass
+ # TODO - need to parameterize this into generate_match_filters,
+ # and work it into the field-specification search feature
+ #
+ exact_match_filter = "(&(objectclass=krbPrincipalAux)(!(objectClass=person))(!(|(krbprincipalname=kadmin/*)(krbprincipalname=K/M@*)(krbprincipalname=krbtgt/*)))%s)" % exact_match_filter
+ partial_match_filter = "(&(objectclass=krbPrincipalAux)(!(objectClass=person))(!(|(krbprincipalname=kadmin/*)(krbprincipalname=K/M@*)(krbprincipalname=krbtgt/*)))%s)" % partial_match_filter
+
+
+ conn = self.getConnection(opts)
+ try:
+ try:
+ exact_results = conn.getListAsync(self.basedn, self.scope,
+ exact_match_filter, sattrs, 0, None, None, timelimit,
+ sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ exact_results = [0]
+
+ try:
+ partial_results = conn.getListAsync(self.basedn, self.scope,
+ partial_match_filter, sattrs, 0, None, None, timelimit,
+ sizelimit)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ partial_results = [0]
+ finally:
+ self.releaseConnection(conn)
+
+ exact_counter = exact_results[0]
+ partial_counter = partial_results[0]
+
+ exact_results = exact_results[1:]
+ partial_results = partial_results[1:]
+
+ # Remove exact matches from the partial_match list
+ exact_dns = set(map(lambda e: e.dn, exact_results))
+ partial_results = filter(lambda e: e.dn not in exact_dns,
+ partial_results)
+
+ if (exact_counter == -1) or (partial_counter == -1):
+ counter = -1
+ else:
+ counter = len(exact_results) + len(partial_results)
+
+ entries = [counter]
+ for e in exact_results + partial_results:
+ entries.append(self.convert_entry(e))
+
+ return entries
+
+
+# Configuration support
+ def get_ipa_config(self, opts=None):
+ """Retrieve the IPA configuration"""
+ searchfilter = "cn=ipaconfig"
+ try:
+ config = self.__get_sub_entry("cn=etc," + self.basedn, searchfilter, None, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ raise ipaerror.gen_exception(ipaerror.LDAP_NO_CONFIG)
+
+ return config
+
+ def update_ipa_config(self, oldconfig, newconfig, opts=None):
+ """Update the IPA configuration.
+
+ oldconfig and newconfig are XML-RPC structs.
+
+ If oldconfig is not empty then it is used when determine what
+ has changed.
+
+ If oldconfig is empty then the value of newconfig is compared
+ to the current value of oldconfig.
+
+ """
+ if not isinstance(newconfig,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if oldconfig and not isinstance(oldconfig,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not oldconfig:
+ oldconfig = self.get_entry_by_dn(newconfig.get('dn'), None, opts)
+ if oldconfig is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+ # The LDAP routines want strings, not ints, so convert a few
+ # things. Otherwise it sees a string -> int conversion as a change.
+ try:
+ newconfig['ipapwdexpadvnotify'] = str(newconfig.get('ipapwdexpadvnotify'))
+ newconfig['ipasearchtimelimit'] = str(newconfig.get('ipasearchtimelimit'))
+ newconfig['ipasearchrecordslimit'] = str(newconfig.get('ipasearchrecordslimit'))
+ newconfig['ipamaxusernamelength'] = str(newconfig.get('ipamaxusernamelength'))
+ except KeyError:
+ # These should all be there but if not, let things proceed
+ pass
+
+ # Ensure that the default group for users exists
+ try:
+ group = self.get_entry_by_cn(newconfig.get('ipadefaultprimarygroup'), None, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ raise
+ except:
+ raise
+
+ # Run through the list of User and Group object classes to make
+ # sure they are all valid. This doesn't handle dependencies but it
+ # will at least catch typos.
+ classes = self.__get_objectclasses(opts)
+ oc = newconfig['ipauserobjectclasses']
+ for i in range(len(oc)):
+ if not oc[i].lower() in classes:
+ raise ipaerror.gen_exception(ipaerror.CONFIG_INVALID_OC)
+ oc = newconfig['ipagroupobjectclasses']
+ for i in range(len(oc)):
+ if not oc[i].lower() in classes:
+ raise ipaerror.gen_exception(ipaerror.CONFIG_INVALID_OC)
+
+ return self.update_entry(oldconfig, newconfig, opts)
+
+ def get_password_policy(self, opts=None):
+ """Retrieve the IPA password policy"""
+ try:
+ policy = self.get_entry_by_cn("accounts", None, opts)
+ except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
+ raise ipaerror.gen_exception(ipaerror.LDAP_NO_CONFIG)
+
+ # convert some values for display purposes
+ policy['krbmaxpwdlife'] = str(int(policy.get('krbmaxpwdlife')) / 86400)
+ policy['krbminpwdlife'] = str(int(policy.get('krbminpwdlife')) / 3600)
+
+ return policy
+
+ def update_password_policy(self, oldpolicy, newpolicy, opts=None):
+ """Update the IPA configuration
+
+ oldpolicy and newpolicy are XML-RPC structs.
+
+ If oldpolicy is not empty then it is used when determine what
+ has changed.
+
+ If oldpolicy is empty then the value of newpolicy is compared
+ to the current value of oldpolicy.
+
+ """
+ if not isinstance(newpolicy,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if oldpolicy and not isinstance(oldpolicy,dict):
+ raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if not oldpolicy:
+ oldpolicy = self.get_entry_by_dn(newpolicy.get('dn'), None, opts)
+ if oldpolicy is None:
+ raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
+
+
+ # The LDAP routines want strings, not ints, so convert a few
+ # things. Otherwise it sees a string -> int conversion as a change.
+ try:
+ for k in oldpolicy.iterkeys():
+ if k.startswith("krb", 0, 3):
+ oldpolicy[k] = str(oldpolicy[k])
+ for k in newpolicy.iterkeys():
+ if k.startswith("krb", 0, 3):
+ newpolicy[k] = str(newpolicy[k])
+
+ # Convert hours and days to seconds
+ oldpolicy['krbmaxpwdlife'] = str(int(oldpolicy.get('krbmaxpwdlife')) * 86400)
+ oldpolicy['krbminpwdlife'] = str(int(oldpolicy.get('krbminpwdlife')) * 3600)
+ newpolicy['krbmaxpwdlife'] = str(int(newpolicy.get('krbmaxpwdlife')) * 86400)
+ newpolicy['krbminpwdlife'] = str(int(newpolicy.get('krbminpwdlife')) * 3600)
+ except KeyError:
+ # These should all be there but if not, let things proceed
+ pass
+ except:
+ # Anything else raise an error
+ raise
+
+ return self.update_entry(oldpolicy, newpolicy, opts)
+
+def ldap_search_escape(match):
+ """Escapes out nasty characters from the ldap search.
+ See RFC 2254."""
+ value = match.group()
+ if (len(value) != 1):
+ return ""
+
+ if value == "(":
+ return "\\28"
+ elif value == ")":
+ return "\\29"
+ elif value == "\\":
+ return "\\5c"
+ elif value == "*":
+ # drop '*' from input. search performs its own wildcarding
+ return ""
+ elif value =='\x00':
+ return r'\00'
+ else:
+ return value
+
+def uniq_list(x):
+ """Return a unique list, preserving order and ignoring case"""
+ set = {}
+ return [set.setdefault(e.lower(),e) for e in x if e.lower() not in set]