""" An LDAP connection product. Depends on David Leonard's ldapmodule. $Id: ZLDAP.py,v 1.11 2000/12/18 22:17:50 jeffrey Exp $ Started by Anthony Baxter, <anthony@interlink.com.au> Continued by the folks @ CodeIt <http://www.codeit.com/> Now by Jeffrey P Shell <jeffrey@Digicool.com>. """ __version__ = "$Revision: 1.11 $"[11:-2] import Acquisition, AccessControl, OFS, string from Acquisition import aq_base from Globals import HTMLFile, MessageDialog, Persistent import ldap, urllib import LDCAccessors from Entry import ZopeEntry, GenericEntry, TransactionalEntry ConnectionError='ZLDAP Connection Error' manage_addZLDAPConnectionForm = HTMLFile('add', globals()) class NoBrainer: """ Empty class for mixin to EntryFactory """ class ZLDAPConnection( Acquisition.Implicit, Persistent, OFS.SimpleItem.Item, LDCAccessors.LDAPConnectionAccessors, AccessControl.Role.RoleManager): '''LDAP Connection Object''' isPrincipiaFolderish=1 meta_type='LDAP Connection' manage_options=( {'label':'Connection Properties','action':'manage_main'}, {'label':'Open/Close','action':'manage_connection'}, {'label':'Browse', 'action':'manage_browse'}, {'label':'Security','action':'manage_access'}, ) __ac_permissions__=( ('Access contents information', ('canBrowse',),), ('View management screens',('manage_tabs','manage_main'), ('Manager',)), ('Edit connection',('manage_edit',),('Manager',)), ('Change permissions',('manage_access',)), ('Open/Close Connection',('manage_connection', 'manage_open','manage_close',), ('Manager',)), ('Browse Connection Entries', ('manage_browse',),('Manager',),), ) manage_browse=HTMLFile('browse',globals()) manage_connection=HTMLFile('connection',globals()) ### dealing with browseability on the root. def canBrowse(self): """ Returns true if the connection is open *and* the '_canBrowse' property is set to true """ return self.shouldBeOpen() and self.getBrowsable() ### constructor def __init__(self, id, title, host, port, basedn, bind_as, pw, openc, transactional=1): "init method" self._v_conn = None self._v_delete = [] self._v_openc = openc self.openc = openc self.setId(id) self.setTitle(title) self.setHost(host) self.setPort(port) self.setBindAs(bind_as) self.setPW(pw) self.setDN(basedn) self.setOpenConnection(openc) self.setTransactional(transactional) # if connection is specified to be open, open it up if openc: self._open() ### upgrade path... def __setstate__(self, state): # Makes sure we have an Entry class now self._refreshEntryClass() Persistent.__setstate__(self, state) ### Entry Factory stuff def _refreshEntryClass(self): """ This class sets up the Entry class used to return results. """ transactional = self.getTransactional() if transactional: EntryBase = TransactionalEntry else: EntryBase = GenericEntry class LdapEntry(EntryBase, ZopeEntry): pass self._v_entryclass = LdapEntry return LdapEntry def _EntryFactory(self): """ Stamps out an Entry class to be used for every entry returned, taking into account transactional versus non-transactional """ return getattr(aq_base(self), '_v_entryclass', self._refreshEntryClass()) ### Tree stuff def __bobo_traverse__(self, REQUEST, key): key=urllib.unquote(key) if getattr(self, key, None) is not None: return getattr(self, key) return self.getRoot()[key] def tpId(self): return self.id def tpURL(self): return self.id def tpValues(self): if self.canBrowse(): return self.getRoot().tpValues() else: return [] #### TransactionalObjectManager stuff ##### def tpc_begin(self,*ignored): #make sure we're open! if not self.__ping(): #we're not open raise (ConnectionError, 'LDAP Connection is not open for commiting') self._v_okobjects=[] def commit(self, o, *ignored): ' o = object to commit ' # check to see if object exists oko=[] if self.hasEntry(o.dn): oko.append(o) elif o._isNew or o._isDeleted: oko.append(o) self._v_okobjects=oko def tpc_finish(self, *ignored): " really really commit and DON'T FAIL " oko=self._v_okobjects self._isCommitting=1 d=getattr(self,'_v_delete',[]) for deldn in d: self._deleteEntry(deldn) self._v_delete=[] for o in oko: try: if o._isDeleted: pass # we shouldn't need to do anything now that # the mass delete has happened elif o._isNew: self._addEntry(o.dn, o._data.items()) o._isNew=0 del self._v_add[o.dn] else: o._modify() o._registered=0 except: pass #XXX We should log errors here del self._v_okobjects del self._isCommitting self.GetConnection().destroy_cache() def tpc_abort(self, *ignored): " really really rollback and DON'T FAIL " try: self._abort() except: pass #XXX We should also log errors here def abort(self, o, *ignored): if o.dn in getattr(self,'_v_delete',()): self._v_delete.remove(o.dn) if o._isDeleted: o.undelete() o._rollback() o._registered=0 if o._isNew: if o.dn in getattr(self,'_v_add',{}).keys(): del self._v_add[o.dn] self.GetConnection().destroy_cache() def _abort(self): oko=self._v_okobjects for o in oko: self.abort(o) self.GetConnection().destroy_cache() def tpc_vote(self, *ignored): pass ### getting entries and attributes def hasEntry(self, dn): if getattr(self, '_v_add',{}).has_key(dn): #object is marked for adding return 1 elif dn in getattr(self,'_v_delete',()): #object is marked for deletion return 0 try: e=self._connection().search_s(dn, ldap.SCOPE_BASE, 'objectclass=*') if e: return 1 except ldap.NO_SUCH_OBJECT: return 0 return 0 def getRawEntry(self, dn): " return raw entry from LDAP module " if getattr(self, '_v_add',{}).has_key(dn): return (dn, self._v_add[dn]._data) elif dn in getattr(self,'_v_delete',()): raise ldap.NO_SUCH_OBJECT, "Entry '%s' has been deleted" % dn try: e=self._connection().search_s( dn, ldap.SCOPE_BASE, 'objectclass=*' ) if e: return e[0] except: raise ldap.NO_SUCH_OBJECT, "Cannot retrieve entry '%s'" % dn def getEntry(self, dn, o=None): " return **unwrapped** Entry object, unless o is specified " Entry = self._EntryFactory() if getattr(self, '_v_add',{}).has_key(dn): e=self._v_add[dn] else: e=self.getRawEntry(dn) e=Entry(e[0],e[1],self) if o is not None: return e.__of__(o) else: return e def getRoot(self): " return root entry object " return self.getEntry(self.dn, self) def getAttributes(self, dn): " get raw attributes from entry from LDAP module " return self.getRawEntry(dn)[1] ### listing subentries def getRawSubEntries(self, dn): " get the raw entry objects of entry dn's immediate children " # XXX Do something soon to account for added but noncommited..? if dn in getattr(self,'_v_delete',()): raise ldap.NO_SUCH_OBJECT results=self._connection().search_s( dn, ldap.SCOPE_ONELEVEL, 'objectclass=*') r=[] for entry in results: #make sure that the subentry isn't marked for deletion if entry[0] not in getattr(self, '_v_delete',()): r.append(entry) return r def getSubEntries(self, dn, o=None): Entry = self._EntryFactory() r=[] se=self.getRawSubEntries(dn) for entry in se: e=Entry(entry[0],entry[1],self) if o is not None: e=e.__of__(o) r.append(e) return r ### modifying entries def _modifyEntry(self, dn, modlist): if not getattr(self,'_isCommitting',0): raise AccessError, 'Cannot modify unless in a commit' #someone's trying to be sneaky and modify an object #outside of a commit. We're not going to allow that! c=self._connection() c.modify_s(dn, modlist) ### deleting entries def _registerDelete(self, dn): " register DN for deletion " d=getattr(self,'_v_delete',[]) if dn not in d: d.append(dn) self._v_delete=d def _unregisterDelete(self, dn): " unregister DN for deletion " d=getattr(self, '_v_delete',[]) if dn in d: d.remove(dn) self._v_delete=d self._unregisterAdd(dn) def _deleteEntry(self, dn): if not getattr(self, '_isCommitting',0): raise AccessError, 'Cannot delete unless in a commit' c=self._connection() c.delete_s(dn) ### adding entries def _registerAdd(self, o): a=getattr(self, '_v_add',{}) if not a.has_key(o.dn): a[o.dn]=o self._v_add=a def _unregisterAdd(self, o=None, dn=None): a=getattr(self, '_v_add',{}) if o and o in a.values(): del a[o.dn] elif dn and a.has_key(dn): del a[dn] self._v_add=a def _addEntry(self, dn, attrs): if not getattr(self, '_isCommitting',0): raise AccessError, 'Cannot add unless in a commit' c=self._connection() c.add_s(dn, attrs) ### other stuff def title_and_id(self): "title and id, with conn state" s=ZLDAPConnection.inheritedAttribute('title_and_id')(self) if self.shouldBeOpen(): s="%s (connected)" % s else: s='%s (<font color="red"> not connected</font>)' % s return s ### connection checking stuff def _connection(self): if self.openc: if not self.isOpen(): self._open() return self._v_conn else: raise ConnectionError, 'Connection Closed' GetConnection=_connection def isOpen(self): " quickly checks to see if the connection's open " if getattr(aq_base(self), '_v_conn', None) is None: self._v_conn = None if self._v_conn is None or not self.shouldBeOpen(): return 0 elif not self.__ping(): return 0 else: return 1 def __ping(self): " more expensive check on the connection and validity of conn " try: self._connection().whoami_s() return 1 except: self._close() return 0 def _open(self): """ open a connection """ try: self._close() except: pass self._v_conn = ldap.open(self.host, self.port) #Nicolas the version of pythonldap doesn't use the enable_cache method #self._v_conn.enable_cache() try: self._v_conn.simple_bind_s(self.bind_as, self.pw) except ldap.NO_SUCH_OBJECT: return """ Error: LDAP Server returned `no such object' for %s. Possibly the bind string or password are incorrect"""%(self.bind_as) self._v_openc = 1 def manage_open(self, REQUEST=None): """ open a connection. """ self.setOpenConnection(1) ret = self._open() if not getattr(self, '_v_openc', 0): return ret if REQUEST is not None: m='Connection has been opened.' return self.manage_connection(self,REQUEST,manage_tabs_message=m) def _close(self): """ close a connection """ if self.getOpenConnection() is None: #I'm already closed, but someone is still trying to close me self._v_conn = None self._v_openc = 0 else: try: self._v_conn.unbind_s() except AttributeError: pass self._v_conn = None self._v_openc = 0 def manage_close(self, REQUEST=None): """ close a connection. """ self._close() if REQUEST is not None: m='Connection has been closed.' return self.manage_connection(self,REQUEST,manage_tabs_message=m) def manage_clearcache(self, REQUEST=None): """ clear the cache """ self._connection().destroy_cache() if REQUEST is not None: m='Cache has been cleared.' return self.manage_connection(self,REQUEST,manage_tabs_message=m) manage_main=HTMLFile("edit",globals()) def manage_edit(self, title, hostport, basedn, bind_as, pw, openc=0, canBrowse=0, transactional=1, REQUEST=None): """ handle changes to a connection """ self.title = title host, port = splitHostPort(hostport) if self.host != host: self._close() self.setHost(host) if self.port != port: self._close() self.setPort(port) if self.bind_as != bind_as: self._close() self.setBindAs(bind_as) if self.pw != pw: self._close() self.setPW(pw) if openc and not self.getOpenConnection(): self.setOpenConnection(1) ret = self._open() if not self._v_openc: return ret if not openc and self.getOpenConnection(): self.setOpenConnection(0) self._close() self.setBrowsable(canBrowse) self.setTransactional(transactional) self.setDN(basedn) if REQUEST is not None: return MessageDialog( title='Edited', message='<strong>%s</strong> has been edited.' % self.id, action ='./manage_main', ) def _isAnLDAPConnection(self): return 1 def splitHostPort(hostport): import string l = string.split(hostport,':') host = l[0] if len(l) == 1: port = 389 else: port = string.atoi(l[1]) return host, port def manage_addZLDAPConnection(self, id, title, hostport, basedn, bind_as, pw, openc, REQUEST=None): """create an LDAP connection and install it""" host, port = splitHostPort(hostport) conn = ZLDAPConnection(id, title, host, port, basedn, bind_as, pw, openc) self._setObject(id, conn) if REQUEST is not None: return self.manage_main(self, REQUEST)