From 861cda3cb5256a177845029ddf1900f51271b56c Mon Sep 17 00:00:00 2001 From: "rcritten@redhat.com" Date: Fri, 24 Aug 2007 15:42:56 -0400 Subject: Initial support for Groups Create separate object for Users and Groups (using same base class) Check for uniqueness before adding new users and groups Remove user_container from everything but add operations Abstract out a number of functions that are common across users and groups Make sure all strings passed in to be in a filter are checked Add new error message: No modifications specified --- ipa-python/entity.py | 153 +++++++++++++++++++++++++++++++++++++++++ ipa-python/group.py | 7 ++ ipa-python/ipaclient.py | 102 +++++++++++++++++++++++++-- ipa-python/ipaerror.py | 5 ++ ipa-python/rpcclient.py | 178 +++++++++++++++++++++++++++++++++++++++++++----- ipa-python/user.py | 156 ++---------------------------------------- 6 files changed, 428 insertions(+), 173 deletions(-) create mode 100644 ipa-python/entity.py create mode 100644 ipa-python/group.py (limited to 'ipa-python') diff --git a/ipa-python/entity.py b/ipa-python/entity.py new file mode 100644 index 000000000..9153c4159 --- /dev/null +++ b/ipa-python/entity.py @@ -0,0 +1,153 @@ +import ldap +import ldif +import re +import cStringIO + +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) + +class Entity: + """This class represents an IPA user. An LDAP entry consists of a DN + and a list of attributes. Each attribute consists of a name and a list of + values. For the time being I will maintain this. + + In python-ldap, entries are returned as a list of 2-tuples. + Instance variables: + dn - string - the string DN of the entry + data - cidict - case insensitive dict of the attributes and values + orig_data - cidict - case insentiive dict of the original attributes and values""" + + def __init__(self,entrydata=None): + """data is the raw data returned from the python-ldap result method, + which is a search result entry or a reference or None. + If creating a new empty entry, data is the string DN.""" + if entrydata: + if isinstance(entrydata,tuple): + self.dn = entrydata[0] + self.data = ldap.cidict.cidict(entrydata[1]) + elif isinstance(entrydata,str) or isinstance(entrydata,unicode): + self.dn = entrydata + self.data = ldap.cidict.cidict() + elif isinstance(entrydata,dict): + self.dn = entrydata['dn'] + del entrydata['dn'] + self.data = ldap.cidict.cidict(entrydata) + else: + self.dn = '' + self.data = ldap.cidict.cidict() + + self.orig_data = dict(self.data) + + def __nonzero__(self): + """This allows us to do tests like if entry: returns false if there is no data, + true otherwise""" + return self.data != None and len(self.data) > 0 + + def hasAttr(self,name): + """Return True if this entry has an attribute named name, False otherwise""" + return self.data and self.data.has_key(name) + + def __setattr__(self,name,value): + """One should use setValue() or setValues() to set values except for + dn and data which are special.""" + if name != 'dn' and name != 'data' and name != 'orig_data': + raise KeyError, 'use setValue() or setValues()' + else: + self.__dict__[name] = value + + def __getattr__(self,name): + """If name is the name of an LDAP attribute, return the first value for that + attribute - equivalent to getValue - this allows the use of + entry.cn + instead of + entry.getValue('cn') + This also allows us to return None if an attribute is not found rather than + throwing an exception""" + return self.getValue(name) + + def getValues(self,name): + """Get the list (array) of values for the attribute named name""" + return self.data.get(name) + + def getValue(self,name): + """Get the first value for the attribute named name""" + value = self.data.get(name,[None]) + if isinstance(value,list) or isinstance(value,tuple): + return value[0] + else: + return value + + def setValue(self,name,*value): + """Value passed in may be a single value, several values, or a single sequence. + For example: + ent.setValue('name', 'value') + ent.setValue('name', 'value1', 'value2', ..., 'valueN') + ent.setValue('name', ['value1', 'value2', ..., 'valueN']) + ent.setValue('name', ('value1', 'value2', ..., 'valueN')) + Since *value is a tuple, we may have to extract a list or tuple from that + tuple as in the last two examples above""" + if (len(value) < 1): + return + if (len(value) == 1): + self.data[name] = utf8_encode_values(value[0]) + else: + self.data[name] = utf8_encode_values(value) + + setValues = setValue + + def toTupleList(self): + """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() + + 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 = {} + for k in self.data.keys(): + result[k] = self.data[k] + result['dn'] = self.dn + return result + + def attrList(self): + """Return a list of all attributes in the entry""" + return self.data.keys() + + def origDataDict(self): + """Returns a dict of the original values of the user. Used for updates.""" + result = {} + for k in self.orig_data.keys(): + result[k] = self.orig_data[k] + result['dn'] = self.dn + return result + +# def __str__(self): +# """Convert the Entry to its LDIF representation""" +# return self.__repr__() +# +# # the ldif class base64 encodes some attrs which I would rather see in raw form - to +# # encode specific attrs as base64, add them to the list below +# ldif.safe_string_re = re.compile('^$') +# base64_attrs = ['nsstate', 'krbprincipalkey', 'krbExtraData'] +# +# def __repr__(self): +# """Convert the Entry to its LDIF representation""" +# sio = cStringIO.StringIO() +# # what's all this then? the unparse method will currently only accept +# # a list or a dict, not a class derived from them. self.data is a +# # cidict, so unparse barfs on it. I've filed a bug against python-ldap, +# # but in the meantime, we have to convert to a plain old dict for printing +# # I also don't want to see wrapping, so set the line width really high (1000) +# newdata = {} +# newdata.update(self.data) +# ldif.LDIFWriter(sio,User.base64_attrs,1000).unparse(self.dn,newdata) +# return sio.getvalue() diff --git a/ipa-python/group.py b/ipa-python/group.py new file mode 100644 index 000000000..d4a27624a --- /dev/null +++ b/ipa-python/group.py @@ -0,0 +1,7 @@ +from ipa.entity import Entity + +class Group(Entity): + + def __init2__(self): + pass + diff --git a/ipa-python/ipaclient.py b/ipa-python/ipaclient.py index dc8e1dc9b..28573acad 100644 --- a/ipa-python/ipaclient.py +++ b/ipa-python/ipaclient.py @@ -26,6 +26,7 @@ sys.path.append("/usr/share/ipa") from ipaserver import funcs import ipa.rpcclient as rpcclient import user +import group import ipa import config @@ -54,20 +55,23 @@ class IPAClient: if self.local: self.transport.set_principal(princ) +# User support def get_user_by_uid(self,uid,sattrs=None): """Get a specific user by uid. If sattrs is set then only those - attributes will be returned.""" + attributes will be returned, otherwise all available attributes + are returned.""" result = self.transport.get_user_by_uid(uid,sattrs) return user.User(result) def get_user_by_dn(self,dn,sattrs=None): - """Get a specific user by uid. If sattrs is set then only those - attributes will be returned.""" + """Get a specific user by dn. If sattrs is set then only those + attributes will be returned, otherwise all available attributes + are returned.""" result = self.transport.get_user_by_dn(dn,sattrs) return user.User(result) def add_user(self,user,user_container=None): - """Add a user. user is a ipa.user object""" + """Add a user. user is a ipa.user.User object""" realm = config.config.get_realm() @@ -97,10 +101,10 @@ class IPAClient: result = self.transport.get_add_schema() return result - def find_users(self, criteria, sattrs=None, user_container=None): + def find_users(self, criteria, sattrs=None): """Find users whose uid matches the criteria. Wildcards are acceptable. Returns a list of User objects.""" - result = self.transport.find_users(criteria, sattrs, user_container) + result = self.transport.find_users(criteria, sattrs) users = [] for (attrs) in result: @@ -124,3 +128,89 @@ class IPAClient: result = self.transport.mark_user_deleted(uid) return result + +# Groups support + + def get_group_by_cn(self,cn,sattrs=None): + """Get a specific group by cn. If sattrs is set then only those + attributes will be returned, otherwise all available attributes + are returned.""" + result = self.transport.get_group_by_cn(cn,sattrs) + return group.Group(result) + + def get_group_by_dn(self,dn,sattrs=None): + """Get a specific group by cn. If sattrs is set then only those + attributes will be returned, otherwise all available attributes + are returned.""" + result = self.transport.get_group_by_dn(dn,sattrs) + return group.Group(result) + + def add_group(self,group,group_container=None): + """Add a group. group is a ipa.group.Group object""" + + realm = config.config.get_realm() + + group_dict = group.toDict() + + # dn is set on the server-side + del group_dict['dn'] + + # convert to a regular dict before sending + result = self.transport.add_group(group_dict, group_container) + return result + + def find_groups(self, criteria, sattrs=None): + """Find groups whose cn matches the criteria. Wildcards are + acceptable. Returns a list of Group objects.""" + result = self.transport.find_groups(criteria, sattrs) + + groups = [] + for (attrs) in result: + if attrs is not None: + groups.append(group.Group(attrs)) + + return groups + + def add_user_to_group(self, user, group): + """Add a user to an existing group. + user is a uid of the user to add + group is the cn of the group to be added to + """ + + return self.transport.add_user_to_group(user, group) + + def add_users_to_group(self, users, group): + """Add several users to an existing group. + user is a list of uids of the users to add + group is the cn of the group to be added to + + Returns a list of the users that were not added. + """ + + return self.transport.add_users_to_group(users, group) + + def remove_user_from_group(self, user, group): + """Remove a user from an existing group. + user is a uid of the user to remove + group is the cn of the group to be removed from + """ + + return self.transport.remove_user_from_group(user, group) + + def remove_users_from_group(self, users, group): + """Remove several users from an existing group. + user is a list of uids of the users to remove + group is the cn of the group to be removed from + + Returns a list of the users that were not removed. + """ + + return self.transport.remove_users_from_group(users, group) + + def update_group(self,group): + """Update a group entry.""" + + realm = config.config.get_realm() + + result = self.transport.update_group(group.origDataDict(), group.toDict()) + return result diff --git a/ipa-python/ipaerror.py b/ipa-python/ipaerror.py index c637aeef0..efcd855f3 100644 --- a/ipa-python/ipaerror.py +++ b/ipa-python/ipaerror.py @@ -115,6 +115,11 @@ LDAP_MISSING_DN = gen_error_code( 0x0005, "Entry missing dn") +LDAP_EMPTY_MODLIST = gen_error_code( + LDAP_CATEGORY, + 0x0006, + "No modifications to be performed") + # # Input errors (sample - replace me) # diff --git a/ipa-python/rpcclient.py b/ipa-python/rpcclient.py index cd94eb99c..2301edde8 100644 --- a/ipa-python/rpcclient.py +++ b/ipa-python/rpcclient.py @@ -51,25 +51,28 @@ class RPCClient: def convert_entry(self,ent): # Convert into a dict. We need to handle multi-valued attributes as well # so we'll convert those into lists. - user={} + obj={} for (k) in ent: k = k.lower() - if user.get(k) is not None: - if isinstance(user[k],list): - user[k].append(ent[k].strip()) + if obj.get(k) is not None: + if isinstance(obj[k],list): + obj[k].append(ent[k].strip()) else: - first = user[k] - user[k] = () - user[k].append(first) - user[k].append(ent[k].strip()) + first = obj[k] + obj[k] = () + obj[k].append(first) + obj[k].append(ent[k].strip()) else: - user[k] = ent[k] + obj[k] = ent[k] - return user + return obj +# User support + def get_user_by_uid(self,uid,sattrs=None): """Get a specific user. If sattrs is not None then only those - attributes will be returned. The result is a dict.""" + attributes will be returned, otherwise all available + attributes are returned. The result is a dict.""" server = self.setup_server() if sattrs is None: sattrs = "__NONE__" @@ -84,7 +87,8 @@ class RPCClient: def get_user_by_dn(self,dn,sattrs=None): """Get a specific user. If sattrs is not None then only those - attributes will be returned. The result is a dict.""" + attributes will be returned, otherwise all available + attributes are returned. The result is a dict.""" server = self.setup_server() if sattrs is None: sattrs = "__NONE__" @@ -145,7 +149,7 @@ class RPCClient: return result - def find_users (self, criteria, sattrs=None, user_container=None): + def find_users (self, criteria, sattrs=None): """Return a list containing a User object for each user that matches the criteria.""" @@ -154,9 +158,7 @@ class RPCClient: # None values are not allowed in XML-RPC if sattrs is None: sattrs = "__NONE__" - if user_container is None: - user_container = "__NONE__" - result = server.find_users(criteria, sattrs, user_container) + result = server.find_users(criteria, sattrs) except xmlrpclib.Fault, fault: raise ipaerror.gen_exception(fault.faultCode, fault.faultString) except socket.error, (value, msg): @@ -189,3 +191,147 @@ class RPCClient: raise xmlrpclib.Fault(value, msg) return result + +# Group support + + def get_group_by_cn(self,cn,sattrs=None): + """Get a specific group. If sattrs is not None then only those + attributes will be returned, otherwise all available + attributes are returned. The result is a dict.""" + server = self.setup_server() + if sattrs is None: + sattrs = "__NONE__" + try: + result = server.get_group_by_cn(cn, sattrs) + except xmlrpclib.Fault, fault: + raise ipaerror.gen_exception(fault.faultCode, fault.faultString) + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + + return result + + def get_group_by_dn(self,dn,sattrs=None): + """Get a specific group. If sattrs is not None then only those + attributes will be returned, otherwise all available + attributes are returned. The result is a dict.""" + server = self.setup_server() + if sattrs is None: + sattrs = "__NONE__" + try: + result = server.get_group_by_dn(dn, sattrs) + except xmlrpclib.Fault, fault: + raise ipaerror.gen_exception(fault.faultCode, fault.faultString) + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + + return result + + def add_group(self,group,group_container=None): + """Add a new group. 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""" + server = self.setup_server() + + if group_container is None: + group_container = "__NONE__" + + try: + result = server.add_group(group, group_container) + except xmlrpclib.Fault, fault: + raise ipaerror.gen_exception(fault.faultCode, fault.faultString) + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + + def find_groups (self, criteria, sattrs=None): + """Return a list containing a Group object for each group that matches + the criteria.""" + + server = self.setup_server() + try: + # None values are not allowed in XML-RPC + if sattrs is None: + sattrs = "__NONE__" + result = server.find_groups(criteria, sattrs) + except xmlrpclib.Fault, fault: + raise ipaerror.gen_exception(fault.faultCode, fault.faultString) + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + + return result + + def add_user_to_group(self, user, group): + """Add a user to an existing group. + user is a uid of the user to add + group is the cn of the group to be added to + """ + server = self.setup_server() + try: + result = server.add_user_to_group(user, group) + except xmlrpclib.Fault, fault: + raise ipaerror.gen_exception(fault.faultCode, fault.faultString) + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + + return result + + def add_users_to_group(self, users, group): + """Add several users to an existing group. + user is a list of the uids of the users to add + group is the cn of the group to be added to + + Returns a list of the users that were not added. + """ + server = self.setup_server() + try: + result = server.add_users_to_group(users, group) + except xmlrpclib.Fault, fault: + raise ipaerror.gen_exception(fault.faultCode, fault.faultString) + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + + return result + + def remove_user_from_group(self, user, group): + """Remove a user from an existing group. + user is a uid of the user to remove + group is the cn of the group to be removed from + """ + server = self.setup_server() + try: + result = server.remove_user_from_group(user, group) + except xmlrpclib.Fault, fault: + raise ipaerror.gen_exception(fault.faultCode, fault.faultString) + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + + return result + + def remove_users_from_group(self, users, group): + """Remove several users from an existing group. + user is a list of the uids of the users to remove + group is the cn of the group to be removed from + + Returns a list of the users that were not removed. + """ + server = self.setup_server() + try: + result = server.remove_users_from_group(users, group) + except xmlrpclib.Fault, fault: + raise ipaerror.gen_exception(fault.faultCode, fault.faultString) + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + + return result + + def update_group(self,oldgroup,newgroup): + """Update an existing group. oldgroup and newgroup are dicts of attributes""" + server = self.setup_server() + + try: + result = server.update_group(oldgroup, newgroup) + except xmlrpclib.Fault, fault: + raise ipaerror.gen_exception(fault.faultCode, fault.faultString) + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + + return result diff --git a/ipa-python/user.py b/ipa-python/user.py index 9d547e80d..cd908e3af 100644 --- a/ipa-python/user.py +++ b/ipa-python/user.py @@ -1,153 +1,7 @@ -import ldap -import ldif -import re -import cStringIO +from ipa.entity import Entity -def utf8_encode_value(value): - if isinstance(value,unicode): - return value.encode('utf-8') - return value +class User(Entity): -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) - -class User: - """This class represents an IPA user. An LDAP entry consists of a DN - and a list of attributes. Each attribute consists of a name and a list of - values. For the time being I will maintain this. - - In python-ldap, entries are returned as a list of 2-tuples. - Instance variables: - dn - string - the string DN of the entry - data - cidict - case insensitive dict of the attributes and values - orig_data - cidict - case insentiive dict of the original attributes and values""" - - def __init__(self,entrydata=None): - """data is the raw data returned from the python-ldap result method, - which is a search result entry or a reference or None. - If creating a new empty entry, data is the string DN.""" - if entrydata: - if isinstance(entrydata,tuple): - self.dn = entrydata[0] - self.data = ldap.cidict.cidict(entrydata[1]) - elif isinstance(entrydata,str) or isinstance(entrydata,unicode): - self.dn = entrydata - self.data = ldap.cidict.cidict() - elif isinstance(entrydata,dict): - self.dn = entrydata['dn'] - del entrydata['dn'] - self.data = ldap.cidict.cidict(entrydata) - else: - self.dn = '' - self.data = ldap.cidict.cidict() - - self.orig_data = dict(self.data) - - def __nonzero__(self): - """This allows us to do tests like if entry: returns false if there is no data, - true otherwise""" - return self.data != None and len(self.data) > 0 - - def hasAttr(self,name): - """Return True if this entry has an attribute named name, False otherwise""" - return self.data and self.data.has_key(name) - - def __setattr__(self,name,value): - """One should use setValue() or setValues() to set values except for - dn and data which are special.""" - if name != 'dn' and name != 'data' and name != 'orig_data': - raise KeyError, 'use setValue() or setValues()' - else: - self.__dict__[name] = value - - def __getattr__(self,name): - """If name is the name of an LDAP attribute, return the first value for that - attribute - equivalent to getValue - this allows the use of - entry.cn - instead of - entry.getValue('cn') - This also allows us to return None if an attribute is not found rather than - throwing an exception""" - return self.getValue(name) - - def getValues(self,name): - """Get the list (array) of values for the attribute named name""" - return self.data.get(name) - - def getValue(self,name): - """Get the first value for the attribute named name""" - value = self.data.get(name,[None]) - if isinstance(value,list) or isinstance(value,tuple): - return value[0] - else: - return value - - def setValue(self,name,*value): - """Value passed in may be a single value, several values, or a single sequence. - For example: - ent.setValue('name', 'value') - ent.setValue('name', 'value1', 'value2', ..., 'valueN') - ent.setValue('name', ['value1', 'value2', ..., 'valueN']) - ent.setValue('name', ('value1', 'value2', ..., 'valueN')) - Since *value is a tuple, we may have to extract a list or tuple from that - tuple as in the last two examples above""" - if (len(value) < 1): - return - if (len(value) == 1): - self.data[name] = utf8_encode_values(value[0]) - else: - self.data[name] = utf8_encode_values(value) - - setValues = setValue - - def toTupleList(self): - """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() - - 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 = {} - for k in self.data.keys(): - result[k] = self.data[k] - result['dn'] = self.dn - return result - - def attrList(self): - """Return a list of all attributes in the entry""" - return self.data.keys() - - def origDataDict(self): - """Returns a dict of the original values of the user. Used for updates.""" - result = {} - for k in self.orig_data.keys(): - result[k] = self.orig_data[k] - result['dn'] = self.dn - return result - -# def __str__(self): -# """Convert the Entry to its LDIF representation""" -# return self.__repr__() -# -# # the ldif class base64 encodes some attrs which I would rather see in raw form - to -# # encode specific attrs as base64, add them to the list below -# ldif.safe_string_re = re.compile('^$') -# base64_attrs = ['nsstate', 'krbprincipalkey', 'krbExtraData'] -# -# def __repr__(self): -# """Convert the Entry to its LDIF representation""" -# sio = cStringIO.StringIO() -# # what's all this then? the unparse method will currently only accept -# # a list or a dict, not a class derived from them. self.data is a -# # cidict, so unparse barfs on it. I've filed a bug against python-ldap, -# # but in the meantime, we have to convert to a plain old dict for printing -# # I also don't want to see wrapping, so set the line width really high (1000) -# newdata = {} -# newdata.update(self.data) -# ldif.LDIFWriter(sio,User.base64_attrs,1000).unparse(self.dn,newdata) -# return sio.getvalue() + def __init2__(self): + pass + -- cgit