diff options
author | Jason Gerard DeRose <jderose@redhat.com> | 2008-10-07 17:00:36 -0600 |
---|---|---|
committer | Jason Gerard DeRose <jderose@redhat.com> | 2008-10-07 17:00:36 -0600 |
commit | c747c12fb2249e987d327f764bf9f1ddb5e60ee6 (patch) | |
tree | 96c689843f2e6abdd5c3506783268fb0dfc36816 | |
parent | 439d9c305b7a2353ecb9373cf14c4e926522aaa3 (diff) | |
parent | db9d8dd3e0924bb9c7f9c89a56e6b6057dabc710 (diff) | |
download | freeipa-c747c12fb2249e987d327f764bf9f1ddb5e60ee6.tar.gz freeipa-c747c12fb2249e987d327f764bf9f1ddb5e60ee6.tar.xz freeipa-c747c12fb2249e987d327f764bf9f1ddb5e60ee6.zip |
Merge branch 'master' of git://git.engineering.redhat.com/users/rcritten/freeipa2
-rw-r--r-- | ipa_server/ipaldap.py | 18 | ||||
-rw-r--r-- | ipa_server/ipautil.py | 11 | ||||
-rw-r--r-- | ipa_server/servercore.py | 209 | ||||
-rw-r--r-- | ipalib/plugins/b_xmlrpc.py | 9 | ||||
-rw-r--r-- | ipalib/plugins/f_group.py | 116 | ||||
-rw-r--r-- | ipalib/plugins/f_user.py | 153 | ||||
-rwxr-xr-x | simple-server.py | 2 | ||||
-rwxr-xr-x | test_server (renamed from ipa_server/test_server) | 15 |
8 files changed, 501 insertions, 32 deletions
diff --git a/ipa_server/ipaldap.py b/ipa_server/ipaldap.py index c1d134a00..4ab0d759c 100644 --- a/ipa_server/ipaldap.py +++ b/ipa_server/ipaldap.py @@ -33,7 +33,8 @@ import struct import ldap.sasl from ldap.controls import LDAPControl,DecodeControlTuples,EncodeControlTuples from ldap.ldapobject import SimpleLDAPObject -import ipautil +from ipa_server import ipautil + # Global variable to define SASL auth sasl_auth = ldap.sasl.sasl({},'GSSAPI') @@ -108,7 +109,20 @@ class Entry: """Convert the attrs and values to a list of 2-tuples. The first element of the tuple is the attribute name. The second element is either a single value or a list of values.""" - return self.data.items() + r = [] + for i in self.data.iteritems(): + n = ipautil.utf8_encode_values(i[1]) + r.append((i[0], n)) + return r + + def toDict(self): + """Convert the attrs and values to a dict. The dict is keyed on the + attribute name. The value is either single value or a list of values.""" + result = ipautil.CIDict(self.data) + for i in result.keys(): + result[i] = ipautil.utf8_encode_values(result[i]) + result['dn'] = self.dn + return result def __str__(self): """Convert the Entry to its LDIF representation""" diff --git a/ipa_server/ipautil.py b/ipa_server/ipautil.py index 6b0e2c892..6422fe5a6 100644 --- a/ipa_server/ipautil.py +++ b/ipa_server/ipautil.py @@ -188,3 +188,14 @@ def get_gsserror(e): secondary = e[0][1] return (primary[0], secondary[0]) + +def utf8_encode_value(value): + if isinstance(value,unicode): + return value.encode('utf-8') + return value + +def utf8_encode_values(values): + if isinstance(values,list) or isinstance(values,tuple): + return map(utf8_encode_value, values) + else: + return utf8_encode_value(values) diff --git a/ipa_server/servercore.py b/ipa_server/servercore.py index eeecd4b03..f0d3dd9aa 100644 --- a/ipa_server/servercore.py +++ b/ipa_server/servercore.py @@ -17,10 +17,9 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -import sys -sys.path.insert(0, ".") -sys.path.insert(0, "..") import ldap +import string +import re from ipa_server.context import context import ipautil @@ -31,6 +30,10 @@ krbctx = krbV.default_context() realm = krbctx.default_realm basedn = ipautil.realm_to_suffix(realm) +DefaultUserContainer = "cn=users,cn=accounts" +DefaultGroupContainer = "cn=groups,cn=accounts" +DefaultServiceContainer = "cn=services,cn=accounts" + def convert_entry(ent): entry = dict(ent.data) entry['dn'] = ent.dn @@ -55,6 +58,30 @@ def convert_scalar_values(orig_dict): return new_dict +def generate_match_filters(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) # TODO: rethink the get_entry vs get_list API calls. # they currently restrict the data coming back without @@ -95,14 +122,160 @@ def get_list (base, searchfilter, sattrs=None): return map(convert_entry, entries) -def update_entry (oldentry, newentry): +# General searches + +def get_entry_by_dn (dn, sattrs=None): + """Get a specific entry. Return as a dict of values. + Multi-valued fields are represented as lists. + """ + searchfilter = "(objectClass=*)" +# logging.info("IPA: get_entry_by_dn '%s'" % dn) + return get_base_entry(dn, searchfilter, sattrs) + +# User support + +def is_user_unique(uid): + """Return True if the uid is unique in the tree, False otherwise.""" + # FIXME +# uid = self.__safe_filter(uid) + searchfilter = "(&(uid=%s)(objectclass=posixAccount))" % uid + + try: + entry = get_sub_entry("cn=accounts," + basedn, searchfilter, ['dn','uid']) + return False +# except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + except Exception: + return True + +def get_user_by_uid (uid, sattrs): + """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 SyntaxError("uid is not a string") +# raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER) + if sattrs is not None and not isinstance(sattrs,list): + raise SyntaxError("sattrs is not a 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 get_sub_entry("cn=accounts," + basedn, searchfilter, sattrs) + +def uid_too_long(uid): + """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 = get_ipa_config() + 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)) + pass + + return False + +def find_users (criteria, sattrs, sizelimit=-1, timelimit=-1): + """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 = get_ipa_config() + 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) = 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 + + try: + exact_results = context.conn.getConn().getListAsync("cn=accounts," + basedn, ldap.SCOPE_SUBTREE, exact_match_filter, sattrs, 0, None, None, timelimit, sizelimit) +# except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + except Exception: + exact_results = [0] + + try: + partial_results = context.conn.getConn().getListAsync("cn=accounts," + basedn, ldap.SCOPE_SUBTREE, partial_match_filter, sattrs, 0, None, None, timelimit, sizelimit) +# except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + except Exception: + partial_results = [0] + + 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(convert_entry(u)) + + return users + +def update_entry (entry): """Update an LDAP entry - oldentry is a dict - newentry is a dict + entry is a dict + + This refreshes the record from LDAP in order to obtain the list of + attributes that has changed. """ - oldentry = convert_scalar_values(oldentry) - newentry = convert_scalar_values(newentry) + attrs = entry.keys() + o = get_base_entry(entry['dn'], "objectclass=*", attrs) + oldentry = convert_scalar_values(o) + newentry = convert_scalar_values(entry) # 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 @@ -113,13 +286,16 @@ def update_entry (oldentry, newentry): # FIXME: return a missing DN error message raise e - res = context.conn.getConn().updateEntry(moddn, oldentry, newentry) - return res + return context.conn.getConn().updateEntry(moddn, oldentry, newentry) + +def add_entry(entry): + """Add a new entry""" + return context.conn.getConn().addEntry(entry) def uniq_list(x): """Return a unique list, preserving order and ignoring case""" myset = {} - return [set.setdefault(e.lower(),e) for e in x if e.lower() not in myset] + return [myset.setdefault(e.lower(),e) for e in x if e.lower() not in myset] def get_schema(): """Retrieves the current LDAP schema from the LDAP server.""" @@ -146,3 +322,14 @@ def get_objectclasses(): result.append(oc[3].replace("'","")) return result + +def get_ipa_config(): + """Retrieve the IPA configuration""" + searchfilter = "cn=ipaconfig" + try: + config = get_sub_entry("cn=etc," + basedn, searchfilter) + except ldap.NO_SUCH_OBJECT, e: + # FIXME + raise e + + return config diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index 61935f013..d7cbd8560 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -24,6 +24,7 @@ Lightwieght XML-RPC client using Python standard library xmlrpclib. """ import xmlrpclib +import socket from ipalib.backend import Backend from ipalib.util import xmlrpc_marshal from ipalib import api @@ -35,7 +36,7 @@ class xmlrpc(Backend): def get_client(self): # FIXME: The server uri should come from self.api.env.server_uri - return xmlrpclib.ServerProxy('http://localhost:8080', allow_none=True) + return xmlrpclib.ServerProxy('http://localhost:8888', allow_none=True) def forward_call(self, name, *args, **kw): """ @@ -44,6 +45,10 @@ class xmlrpc(Backend): client = self.get_client() command = getattr(client, name) params = xmlrpc_marshal(*args, **kw) - return command(*params) + try: + return command(*params) + except socket.error, e: + print e[1] + return False api.register(xmlrpc) diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py new file mode 100644 index 000000000..c5a37e725 --- /dev/null +++ b/ipalib/plugins/f_group.py @@ -0,0 +1,116 @@ +# 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 group (Identity). +""" + +from ipalib import frontend +from ipalib import crud +from ipalib.frontend import Param +from ipalib import api +from ipa_server import servercore +from ipa_server import ipaldap +import ldap + + +class group(frontend.Object): + """ + Group object. + """ + takes_params = ( + 'description', + Param('cn', + primary_key=True, + normalize=lambda value: value.lower(), + ) + ) +api.register(group) + + +class group_add(crud.Add): + 'Add a new group.' + def execute(self, *args, **kw): + """args[0] = uid of the group to add + kw{container} is the location in the DIT to add the group, not + required + kw otherwise contains all the attributes + """ + # FIXME: ug, really? + if not kw.get('container'): + group_container = servercore.DefaultGroupContainer + else: + group_container = kw['container'] + del kw['container'] + + group = kw + + group['cn'] = args[0] + + # Get our configuration + config = servercore.get_ipa_config() + + dn="cn=%s,%s,%s" % (ldap.dn.escape_dn_chars(group['cn']), + group_container,servercore.basedn) + + entry = 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]) + + result = servercore.add_entry(entry) + return result + + +api.register(group_add) + + +class group_del(crud.Del): + 'Delete an existing group.' +api.register(group_del) + + +class group_mod(crud.Mod): + 'Edit an existing group.' +api.register(group_mod) + + +class group_find(crud.Find): + 'Search the groups.' + def execute(self, *args, **kw): + cn=args[0] + result = servercore.get_sub_entry(servercore.basedn, "cn=%s" % cn, ["*"]) + return result + def forward(self, *args, **kw): + result = super(crud.Find, self).forward(*args, **kw) + for a in result: + print a, ": ", result[a] +api.register(group_find) + + +class group_show(crud.Get): + 'Examine an existing group.' +api.register(group_show) diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py index 150d48ead..b006c24b2 100644 --- a/ipalib/plugins/f_user.py +++ b/ipalib/plugins/f_user.py @@ -25,7 +25,9 @@ from ipalib import frontend from ipalib import crud from ipalib.frontend import Param from ipalib import api - +from ipa_server import servercore +from ipa_server import ipaldap +import ldap # Command to get the idea how plugins will interact with api.env class envtest(frontend.Command): @@ -71,7 +73,105 @@ api.register(user) class user_add(crud.Add): 'Add a new user.' def execute(self, *args, **kw): - return 1 + """args[0] = uid of the user to add + kw{container} is the location in the DIT to add the user, not + required + kw otherwise contains all the attributes + """ + # FIXME: ug, really? + if not kw.get('container'): + user_container = servercore.DefaultUserContainer + else: + user_container = kw['container'] + del kw['container'] + + user = kw + + if not isinstance(user, dict): + # FIXME, need proper error + raise SyntaxError + + user['uid'] = args[0] + + if not servercore.is_user_unique(user['uid']): + # FIXME, specific error + raise SyntaxError("user already exists") + if servercore.uid_too_long(user['uid']): + # FIXME, specific error + raise SyntaxError("uid is 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,servercore.basedn) + + entry = ipaldap.Entry(dn) + + # Get our configuration + config = servercore.get_ipa_config() + + # 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'), servercore.DefaultGroupContainer, servercore.basedn) + try: + default_group = servercore.get_entry_by_dn(group_dn, ['dn','gidNumber']) + 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')) + except Exception, e: + # FIXME + raise e + + 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')) + + # some required objectclasses + entry.setValues('objectClass', (config.get('ipauserobjectclasses'))) + # entry.setValues('objectClass', ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'inetUser', 'posixAccount', 'krbPrincipalAux']) + + # fill in our new entry with everything sent by the user + for u in user: + entry.setValues(u, user[u]) + + result = servercore.add_entry(entry) + return result + def forward(self, *args, **kw): + result = super(crud.Add, self).forward(*args, **kw) + if result != False: + print "User %s added" % args[0] + api.register(user_add) @@ -82,18 +182,59 @@ api.register(user_del) class user_mod(crud.Mod): 'Edit an existing user.' + def execute(self, *args, **kw): + uid=args[0] + result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % uid, ["*"]) + + user = kw + dn = result.get('dn') + del result['dn'] + entry = ipaldap.Entry((dn, servercore.convert_scalar_values(result))) + + for u in user: + entry.setValues(u, user[u]) + + result = servercore.update_entry(entry.toDict()) + + return result + def forward(self, *args, **kw): + result = super(crud.Mod, self).forward(*args, **kw) + if result != False: + print "User %s modified" % args[0] api.register(user_mod) class user_find(crud.Find): 'Search the users.' -# def execute(self, *args, **kw): -# uid=args[0] -# result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % uid, ["*"]) -# return result + def execute(self, *args, **kw): + uid=args[0] + result = servercore.find_users(uid, ["*"]) + return result + def forward(self, *args, **kw): + users = super(crud.Find, self).forward(*args, **kw) + counter = users[0] + users = users[1:] + if counter == 0: + print "No entries found for", args[0] + return + elif counter == -1: + print "These results are truncated." + print "Please refine your search and try again." + + for u in users: + for a in u.keys(): + print "%s: %s" % (a, u[a]) api.register(user_find) class user_show(crud.Get): 'Examine an existing user.' + def execute(self, *args, **kw): + uid=args[0] + result = servercore.get_user_by_uid(uid, ["*"]) + return result + def forward(self, *args, **kw): + result = super(crud.Get, self).forward(*args, **kw) + for a in result: + print a, ": ", result[a] api.register(user_show) diff --git a/simple-server.py b/simple-server.py index 2f0d92266..6d29d74ab 100755 --- a/simple-server.py +++ b/simple-server.py @@ -43,7 +43,7 @@ class Dispatch(object): return self.__cmd(*args, **kw) -server = SimpleXMLRPCServer(('localhost', 8080), allow_none=True) +server = SimpleXMLRPCServer(('localhost', 8880), allow_none=True) server.register_introspection_functions() for cmd in api.Command(): server.register_function(Dispatch(cmd), cmd.name) diff --git a/ipa_server/test_server b/test_server index a67263699..0ee250ad7 100755 --- a/ipa_server/test_server +++ b/test_server @@ -1,8 +1,6 @@ #!/usr/bin/env python import sys -sys.path.insert(0, "..") -sys.path.insert(0, ".") import SimpleXMLRPCServer import logging import xmlrpclib @@ -10,9 +8,10 @@ import re import threading import commands from ipalib import api -import conn +from ipa_server import conn from ipa_server.servercore import context import ipalib.load_plugins +from ipalib.util import xmlrpc_unmarshal import traceback PORT=8888 @@ -59,12 +58,8 @@ class LoggingSimpleXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHa func = funcs[method] except KeyError: raise Exception('method "%s" is not supported' % method) - if len(params) > 1 and isinstance(params[-1], dict): - kw = params[-1] - params = params[:-1] - return func(*params, **kw) - else: - return func(*params) + (args, kw) = xmlrpc_unmarshal(*params) + return func(*args, **kw) finally: # Clean up any per-request data and connections # for k in context.__dict__.keys(): @@ -109,7 +104,6 @@ class LoggingSimpleXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHa # Log client request logger.info('Client request: \n%s\n' % data) -# response = self.server._marshaled_dispatch( response = self._marshaled_dispatch( data, getattr(self, '_dispatch', None)) @@ -147,6 +141,7 @@ XMLRPCServer = StoppableXMLRPCServer(("",PORT), LoggingSimpleXMLRPCRequestHandle XMLRPCServer.register_introspection_functions() # Get and register all the methods +api.env.server_context = True api.finalize() for cmd in api.Method: logger.info("registering %s" % cmd) |