001    //$HeadURL: svn+ssh://mschneider@svn.wald.intevation.org/deegree/base/trunk/src/org/deegree/tools/security/ActiveDirectoryImporter.java $
002    /*----------------------------------------------------------------------------
003     This file is part of deegree, http://deegree.org/
004     Copyright (C) 2001-2009 by:
005       Department of Geography, University of Bonn
006     and
007       lat/lon GmbH
008    
009     This library is free software; you can redistribute it and/or modify it under
010     the terms of the GNU Lesser General Public License as published by the Free
011     Software Foundation; either version 2.1 of the License, or (at your option)
012     any later version.
013     This library is distributed in the hope that it will be useful, but WITHOUT
014     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
015     FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
016     details.
017     You should have received a copy of the GNU Lesser General Public License
018     along with this library; if not, write to the Free Software Foundation, Inc.,
019     59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
020    
021     Contact information:
022    
023     lat/lon GmbH
024     Aennchenstr. 19, 53177 Bonn
025     Germany
026     http://lat-lon.de/
027    
028     Department of Geography, University of Bonn
029     Prof. Dr. Klaus Greve
030     Postfach 1147, 53001 Bonn
031     Germany
032     http://www.geographie.uni-bonn.de/deegree/
033    
034     e-mail: info@deegree.org
035    ----------------------------------------------------------------------------*/
036    package org.deegree.tools.security;
037    
038    import java.io.FileInputStream;
039    import java.io.IOException;
040    import java.io.PrintWriter;
041    import java.io.StringWriter;
042    import java.util.HashMap;
043    import java.util.Hashtable;
044    import java.util.Properties;
045    
046    import javax.naming.Context;
047    import javax.naming.NamingEnumeration;
048    import javax.naming.NamingException;
049    import javax.naming.directory.Attribute;
050    import javax.naming.directory.Attributes;
051    import javax.naming.directory.SearchControls;
052    import javax.naming.directory.SearchResult;
053    import javax.naming.ldap.Control;
054    import javax.naming.ldap.InitialLdapContext;
055    import javax.naming.ldap.LdapContext;
056    import javax.naming.ldap.PagedResultsControl;
057    import javax.naming.ldap.PagedResultsResponseControl;
058    
059    import org.deegree.framework.mail.EMailMessage;
060    import org.deegree.framework.mail.MailHelper;
061    import org.deegree.framework.mail.SendMailException;
062    import org.deegree.security.GeneralSecurityException;
063    import org.deegree.security.UnauthorizedException;
064    import org.deegree.security.drm.SecurityAccess;
065    import org.deegree.security.drm.SecurityAccessManager;
066    import org.deegree.security.drm.SecurityTransaction;
067    import org.deegree.security.drm.UnknownException;
068    import org.deegree.security.drm.model.Group;
069    import org.deegree.security.drm.model.User;
070    
071    /**
072     * This class provides the functionality to synchronize the <code>User</code> instances stored in a
073     * <code>SecurityManager</code> with a generic LDAP-Server (such as OpenLDAP).
074     * <p>
075     * Changes are committed after all steps succeeded. If an error occurs, changes in the <code>SecurityManager</code> are
076     * undone.
077     * <p>
078     *
079     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a>
080     * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a>
081     * @author last edited by: $Author: apoth $
082     *
083     * @version $Revision: 10660 $, $Date: 2008-03-24 21:39:54 +0000 (Mo, 24 Mrz 2008) $
084     */
085    public class LDAPImporter {
086    
087        private SecurityAccessManager manager;
088    
089        private SecurityAccess access;
090    
091        private SecurityTransaction trans;
092    
093        private User admin;
094    
095        private Hashtable<String, String> env;
096    
097        private LdapContext ctx;
098    
099        private Properties config;
100    
101        // mail configuration
102        private static String mailSender;
103    
104        private static String mailRcpt;
105    
106        private static String mailHost;
107    
108        private static boolean mailLog;
109    
110        // number of results to fetch in one batch
111        private int pageSize = 500;
112    
113        private StringBuffer logBuffer = new StringBuffer( 1000 );
114    
115        /**
116         * Constructs a new <code>ADExporter</code> -instance.
117         * <p>
118         *
119         * @param config
120         * @throws NamingException
121         * @throws GeneralSecurityException
122         */
123        LDAPImporter( Properties config ) throws NamingException, GeneralSecurityException {
124    
125            this.config = config;
126    
127            // retrieve mail configuration first
128            mailSender = getPropertySafe( "mailSender" );
129            mailRcpt = getPropertySafe( "mailRcpt" );
130            mailHost = getPropertySafe( "mailHost" );
131            mailLog = getPropertySafe( "mailLog" ).equals( "true" ) || getPropertySafe( "mailLog" ).equals( "yes" ) ? true
132                                                                                                                   : false;
133    
134            // get a SecurityManager (with an SQLRegistry)
135            Properties registryProperties = new Properties();
136            registryProperties.put( "driver", getPropertySafe( "sqlDriver" ) );
137            registryProperties.put( "url", getPropertySafe( "sqlLogon" ) );
138            registryProperties.put( "user", getPropertySafe( "sqlUser" ) );
139            registryProperties.put( "password", getPropertySafe( "sqlPass" ) );
140    
141            // default timeout: 20 min
142            long timeout = 1200000;
143            try {
144                timeout = Long.parseLong( getPropertySafe( "timeout" ) );
145            } catch ( NumberFormatException e ) {
146                logBuffer.append( "Specified property value for timeout invalid. " + "Defaulting to 1200 (secs)." );
147            }
148    
149            if ( !SecurityAccessManager.isInitialized() ) {
150                SecurityAccessManager.initialize( "org.deegree.security.drm.SQLRegistry", registryProperties, timeout );
151            }
152    
153            manager = SecurityAccessManager.getInstance();
154            admin = manager.getUserByName( getPropertySafe( "u3rAdminName" ) );
155            admin.authenticate( getPropertySafe( "u3rAdminPassword" ) );
156    
157            // prepare LDAP connection
158            String jndiURL = "ldap://" + getPropertySafe( "ldapHost" ) + ":389/";
159            String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
160            String authenticationMode = "simple";
161            String contextReferral = "ignore";
162            env = new Hashtable<String, String>();
163            env.put( Context.INITIAL_CONTEXT_FACTORY, initialContextFactory );
164            env.put( Context.PROVIDER_URL, jndiURL );
165            env.put( Context.SECURITY_AUTHENTICATION, authenticationMode );
166            env.put( Context.SECURITY_PRINCIPAL, getPropertySafe( "ldapUser" ) );
167            env.put( Context.SECURITY_CREDENTIALS, getPropertySafe( "ldapPass" ) );
168            env.put( Context.REFERRAL, contextReferral );
169    
170            access = manager.acquireAccess( admin );
171            trans = manager.acquireTransaction( admin );
172            ctx = new InitialLdapContext( env, null );
173    
174        }
175    
176        /**
177         * Returns a configuration property. If it is not defined, an exception is thrown.
178         * <p>
179         *
180         * @param name
181         * @return a configuration property. If it is not defined, an exception is thrown.
182         */
183        private String getPropertySafe( String name ) {
184    
185            String value = config.getProperty( name );
186            if ( value == null ) {
187                throw new RuntimeException( "Configuration does not define needed property '" + name + "'." );
188            }
189            return value;
190        }
191    
192        /**
193         * Synchronizes the LDAP's user objects with the SecurityManager's user objects.
194         * <p>
195         *
196         * @throws NamingException
197         * @throws IOException
198         * @throws GeneralSecurityException
199         * @throws UnauthorizedException
200         *
201         */
202        void synchronizeUsers()
203                                throws NamingException, IOException, UnauthorizedException, GeneralSecurityException {
204    
205            // keys are names (Strings), values are User-objects
206            HashMap<String, User> userMap = new HashMap<String, User>( 20 );
207    
208            byte[] cookie = null;
209    
210            // specify the ids of the attributes to return
211            String[] attrIDs = { getPropertySafe( "userName" ), getPropertySafe( "userTitle" ),
212                                getPropertySafe( "userFirstName" ), getPropertySafe( "userLastName" ),
213                                getPropertySafe( "userMail" ) };
214    
215            SearchControls ctls = new SearchControls();
216            ctls.setReturningAttributes( attrIDs );
217            ctls.setSearchScope( SearchControls.SUBTREE_SCOPE );
218    
219            // specify the search filter to match
220            String filter = getPropertySafe( "userFilter" );
221            String context = getPropertySafe( "userContext" );
222    
223            // create initial PagedResultsControl
224            ctx.setRequestControls( new Control[] { new PagedResultsControl( pageSize, false ) } );
225    
226            // phase 1: make sure that all users from LDAP are present in the
227            // SecurityManager
228            // (register them to the SecurityManager if necessary)
229            do {
230                // perform the search
231                NamingEnumeration<?> answer = ctx.search( context, filter, ctls );
232    
233                // process results of the last batch
234                while ( answer.hasMoreElements() ) {
235                    SearchResult result = (SearchResult) answer.nextElement();
236    
237                    Attributes atts = result.getAttributes();
238                    Attribute nameAtt = atts.get( getPropertySafe( "userName" ) );
239                    Attribute firstNameAtt = atts.get( getPropertySafe( "userFirstName" ) );
240                    Attribute lastNameAtt = atts.get( getPropertySafe( "userLastName" ) );
241                    Attribute mailAtt = atts.get( getPropertySafe( "userMail" ) );
242    
243                    String name = (String) nameAtt.get();
244                    String firstName = firstNameAtt != null ? (String) firstNameAtt.get() : "" + "";
245                    String lastName = lastNameAtt != null ? (String) lastNameAtt.get() : "" + "";
246                    String mail = mailAtt != null ? (String) mailAtt.get() : "";
247    
248                    // check if user is already registered
249                    User user = null;
250                    try {
251                        user = access.getUserByName( name );
252                    } catch ( UnknownException e ) {
253                        // no -> register user
254                        logBuffer.append( "Registering user: " + name + "\n" );
255                        user = trans.registerUser( name, null, lastName, firstName, mail );
256                    }
257                    userMap.put( name, user );
258                }
259    
260                // examine the paged results control response
261                Control[] controls = ctx.getResponseControls();
262                if ( controls != null ) {
263                    for ( int i = 0; i < controls.length; i++ ) {
264                        if ( controls[i] instanceof PagedResultsResponseControl ) {
265                            PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i];
266                            // total = prrc.getResultSize();
267                            cookie = prrc.getCookie();
268                        }
269                    }
270                }
271    
272                if ( cookie != null ) {
273                    // re-activate paged results
274                    ctx.setRequestControls( new Control[] { new PagedResultsControl( pageSize, cookie, Control.CRITICAL ) } );
275                }
276            } while ( cookie != null );
277    
278            // phase 2: make sure that all users from the SecurityManager are known
279            // to the LDAP (deregister them from the SecurityManager if necessary)
280            User[] sMUsers = access.getAllUsers();
281            for ( int i = 0; i < sMUsers.length; i++ ) {
282                if ( userMap.get( sMUsers[i].getName() ) == null && sMUsers[i].getID() != User.ID_SEC_ADMIN ) {
283                    logBuffer.append( "Deregistering user: " + sMUsers[i].getName() + "\n" );
284                    trans.deregisterUser( sMUsers[i] );
285                }
286            }
287        }
288    
289        /**
290         * Updates the special group "SEC_ALL" (contains all users).
291         * <p>
292         *
293         * @throws GeneralSecurityException
294         */
295        void updateSecAll()
296                                throws GeneralSecurityException {
297            Group secAll = null;
298    
299            // phase1: make sure that group "SEC_ALL" exists
300            // (register it if necessary)
301            try {
302                secAll = access.getGroupByName( "SEC_ALL" );
303            } catch ( UnknownException e ) {
304                secAll = trans.registerGroup( "SEC_ALL", "SEC_ALL" );
305            }
306    
307            // phase2: set all users to be members of this group
308            User[] allUsers = access.getAllUsers();
309            trans.setUsersInGroup( secAll, allUsers );
310        }
311    
312        /**
313         * Aborts the synchronization process and undoes all changes.
314         */
315        public void abortChanges() {
316            if ( manager != null && trans != null ) {
317                try {
318                    manager.abortTransaction( trans );
319                } catch ( GeneralSecurityException e ) {
320                    e.printStackTrace();
321                }
322            }
323            if ( ctx != null ) {
324                try {
325                    ctx.close();
326                } catch ( NamingException e ) {
327                    e.printStackTrace();
328                }
329            }
330        }
331    
332        /**
333         * Ends the synchronization process and commits all changes.
334         */
335        public void commitChanges() {
336            if ( manager != null && trans != null ) {
337                try {
338                    manager.commitTransaction( trans );
339                } catch ( GeneralSecurityException e ) {
340                    e.printStackTrace();
341                }
342            }
343            if ( ctx != null ) {
344                try {
345                    ctx.close();
346                } catch ( NamingException e ) {
347                    e.printStackTrace();
348                }
349            }
350        }
351    
352        /**
353         * Sends an eMail to inform the admin that something went wrong.
354         * <p>
355         * NOTE: This is static, because it must be usable even when the construction of the ADExporter failed.
356         * <p>
357         *
358         * @param e
359         */
360        public static void sendError( Exception e ) {
361    
362            try {
363                String mailMessage = "Beim Synchronisieren der LDAP-Datenbank mit der deegree-"
364                                     + "Sicherheitsdatenbank ist ein Fehler aufgetreten.\n"
365                                     + "Die Synchronisierung wurde NICHT durchgeführt, der letzte "
366                                     + "Stand wurde wiederhergestellt.\n";
367                StringWriter sw = new StringWriter();
368                PrintWriter writer = new PrintWriter( sw );
369                e.printStackTrace( writer );
370                mailMessage += "\n\nDie Java-Fehlermeldung lautet:\n" + sw.getBuffer();
371    
372                mailMessage += "\n\nMit freundlichem Gruss,\nIhr LDAPImporter";
373                MailHelper.createAndSendMail(
374                                              new EMailMessage( mailSender, mailRcpt, "Fehler im LDAPImporter", mailMessage ),
375                                              mailHost );
376            } catch ( SendMailException ex ) {
377                ex.printStackTrace();
378            }
379    
380        }
381    
382        /**
383         * Sends an eMail with a log of the transaction.
384         * <p>
385         */
386        public void sendLog() {
387    
388            try {
389                String mailMessage = "Die Synchronisierung der deegree Sicherheitsdatenbank mit "
390                                     + "der LDAP-Datenbank wurde erfolgreich durchgeführt:\n\n";
391                if ( logBuffer.length() == 0 ) {
392                    mailMessage += "Keine Änderungen.";
393                } else {
394                    mailMessage += logBuffer.toString();
395                }
396                mailMessage += "\n\nMit freundlichem Gruss,\nIhr LDAPImporter";
397                EMailMessage emm = new EMailMessage( mailSender, mailRcpt, "LDAP Sychronisierung durchgeführt", mailMessage );
398                MailHelper.createAndSendMail( emm, mailHost );
399            } catch ( SendMailException ex ) {
400                ex.printStackTrace();
401            }
402    
403        }
404    
405        /**
406         * @param args
407         * @throws Exception
408         */
409        public static void main( String[] args )
410                                throws Exception {
411    
412            if ( args.length != 1 ) {
413                System.out.println( "USAGE: LDAPImporter configfile" );
414                System.exit( 0 );
415            }
416    
417            long begin = System.currentTimeMillis();
418            System.out.println( "Beginning synchronisation..." );
419    
420            LDAPImporter exporter = null;
421            try {
422                Properties config = new Properties();
423                config.load( new FileInputStream( args[0] ) );
424                exporter = new LDAPImporter( config );
425    
426                exporter.synchronizeUsers();
427                exporter.updateSecAll();
428                exporter.commitChanges();
429            } catch ( Exception e ) {
430                if ( exporter != null ) {
431                    exporter.abortChanges();
432                }
433                sendError( e );
434                System.err.println( "Synchronisation has been aborted. Error message: " );
435                e.printStackTrace();
436                System.exit( 0 );
437            }
438    
439            if ( mailLog ) {
440                exporter.sendLog();
441            }
442    
443            System.out.println( "Synchronisation took " + ( System.currentTimeMillis() - begin ) + " milliseconds." );
444            System.exit( 0 );
445        }
446    }