001 //$HeadURL: svn+ssh://jwilden@svn.wald.intevation.org/deegree/base/branches/2.5_testing/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.ArrayList;
043 import java.util.HashMap;
044 import java.util.Hashtable;
045 import java.util.Iterator;
046 import java.util.Properties;
047
048 import javax.naming.Context;
049 import javax.naming.NamingEnumeration;
050 import javax.naming.NamingException;
051 import javax.naming.directory.Attribute;
052 import javax.naming.directory.Attributes;
053 import javax.naming.directory.SearchControls;
054 import javax.naming.directory.SearchResult;
055 import javax.naming.ldap.Control;
056 import javax.naming.ldap.InitialLdapContext;
057 import javax.naming.ldap.LdapContext;
058 import javax.naming.ldap.PagedResultsControl;
059 import javax.naming.ldap.PagedResultsResponseControl;
060
061 import org.deegree.framework.mail.EMailMessage;
062 import org.deegree.framework.mail.MailHelper;
063 import org.deegree.framework.mail.SendMailException;
064 import org.deegree.security.GeneralSecurityException;
065 import org.deegree.security.UnauthorizedException;
066 import org.deegree.security.drm.ManagementException;
067 import org.deegree.security.drm.SecurityAccess;
068 import org.deegree.security.drm.SecurityAccessManager;
069 import org.deegree.security.drm.SecurityHelper;
070 import org.deegree.security.drm.SecurityTransaction;
071 import org.deegree.security.drm.UnknownException;
072 import org.deegree.security.drm.model.Group;
073 import org.deegree.security.drm.model.User;
074
075 /**
076 * This class provides the functionality to synchronize the <code>User</code> and
077 * <code>Group</code> instances stored in a <code>SecurityManager</code> with an
078 * ActiveDirectory-Server.
079 * <p>
080 * Synchronization involves four steps:
081 * <ul>
082 * <li>synchronization of groups
083 * <li>synchronization of users
084 * <li>updating of the special group "SEC_ALL" (contains all users)
085 * <li>testing of subadmin-role validity (only one role per user max)
086 * </ul>
087 * Changes are committed after all steps succeeded. If an error occurs, changes in the
088 * <code>SecurityManager</code> are undone.
089 * <p>
090 *
091 *
092 * @version $Revision: 18195 $
093 * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a>
094 * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a>
095 * @author last edited by: $Author: mschneider $
096 *
097 * @version $Revision: 18195 $, $Date: 2009-06-18 17:55:39 +0200 (Do, 18 Jun 2009) $
098 */
099 public class ActiveDirectoryImporter {
100
101 private SecurityAccessManager manager;
102
103 private SecurityAccess access;
104
105 private SecurityTransaction trans;
106
107 private User admin;
108
109 private Hashtable<String, String> env;
110
111 private LdapContext ctx;
112
113 private Properties config;
114
115 // mail configuration
116 private static String mailSender;
117
118 private static String mailRcpt;
119
120 private static String mailHost;
121
122 private static boolean mailLog;
123
124 // number of results to fetch in one batch
125 private int pageSize = 500;
126
127 private StringBuffer logBuffer = new StringBuffer( 1000 );
128
129 /**
130 * Constructs a new <code>ADExporter</code> -instance.
131 * <p>
132 *
133 * @param config
134 * @throws NamingException
135 * @throws GeneralSecurityException
136 */
137 ActiveDirectoryImporter( Properties config ) throws NamingException, GeneralSecurityException {
138
139 this.config = config;
140
141 // retrieve mail configuration first
142 mailSender = getPropertySafe( "mailSender" );
143 mailRcpt = getPropertySafe( "mailRcpt" );
144 mailHost = getPropertySafe( "mailHost" );
145 mailLog = getPropertySafe( "mailLog" ).equals( "true" ) || getPropertySafe( "mailLog" ).equals( "yes" ) ? true
146 : false;
147
148 // get a SecurityManager (with an SQLRegistry)
149 Properties registryProperties = new Properties();
150 registryProperties.put( "driver", getPropertySafe( "sqlDriver" ) );
151 registryProperties.put( "url", getPropertySafe( "sqlLogon" ) );
152 registryProperties.put( "user", getPropertySafe( "sqlUser" ) );
153 registryProperties.put( "password", getPropertySafe( "sqlPass" ) );
154
155 // default timeout: 20 min
156 long timeout = 1200000;
157 try {
158 timeout = Long.parseLong( getPropertySafe( "timeout" ) );
159 } catch ( NumberFormatException e ) {
160 logBuffer.append( "Specified property value for timeout invalid. " + "Defaulting to 1200 (secs)." );
161 }
162
163 if ( !SecurityAccessManager.isInitialized() ) {
164 SecurityAccessManager.initialize( "org.deegree.security.drm.SQLRegistry", registryProperties, timeout );
165 }
166
167 manager = SecurityAccessManager.getInstance();
168 admin = manager.getUserByName( getPropertySafe( "u3rAdminName" ) );
169 admin.authenticate( getPropertySafe( "u3rAdminPassword" ) );
170
171 // prepare LDAP connection
172 String jndiURL = "ldap://" + getPropertySafe( "ldapHost" ) + ":389/";
173 String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
174 String authenticationMode = "simple";
175 String contextReferral = "ignore";
176 env = new Hashtable<String, String>();
177 env.put( Context.INITIAL_CONTEXT_FACTORY, initialContextFactory );
178 env.put( Context.PROVIDER_URL, jndiURL );
179 env.put( Context.SECURITY_AUTHENTICATION, authenticationMode );
180 env.put( Context.SECURITY_PRINCIPAL, getPropertySafe( "ldapUser" ) );
181 env.put( Context.SECURITY_CREDENTIALS, getPropertySafe( "ldapPass" ) );
182 env.put( Context.REFERRAL, contextReferral );
183
184 access = manager.acquireAccess( admin );
185 trans = manager.acquireTransaction( admin );
186 ctx = new InitialLdapContext( env, null );
187
188 }
189
190 /**
191 * Returns a configuration property. If it is not defined, an exception is thrown.
192 * <p>
193 *
194 * @param name
195 * @return a configuration property. If it is not defined, an exception is thrown.
196 */
197 private String getPropertySafe( String name ) {
198
199 String value = config.getProperty( name );
200 if ( value == null ) {
201 throw new RuntimeException( "Configuration does not define needed property '" + name + "'." );
202 }
203 return value;
204 }
205
206 /**
207 * Synchronizes the AD's group objects with the SecurityManager's group objects.
208 * <p>
209 *
210 * @return the mapping of the String to the Groups
211 * @throws NamingException
212 * @throws IOException
213 * @throws GeneralSecurityException
214 * @throws UnauthorizedException
215 */
216 HashMap<String, Group> synchronizeGroups()
217 throws NamingException, IOException, UnauthorizedException, GeneralSecurityException {
218 // keys are names (Strings), values are Group-objects
219 HashMap<String, Group> groupMap = new HashMap<String, Group>( 20 );
220 // keys are distinguishedNames (Strings), values are Group-objects
221 HashMap<String, Group> groupMap2 = new HashMap<String, Group>( 20 );
222 // keys are names (Strings), values are NamingEnumeration-objects
223 HashMap<String, NamingEnumeration<?>> memberOfMap = new HashMap<String, NamingEnumeration<?>>( 20 );
224
225 byte[] cookie = null;
226
227 // specify the ids of the attributes to return
228 String[] attrIDs = { "distinguishedName", getPropertySafe( "groupName" ), getPropertySafe( "groupTitle" ),
229 getPropertySafe( "groupMemberOf" ) };
230
231 // set SearchControls
232 SearchControls ctls = new SearchControls();
233 ctls.setReturningAttributes( attrIDs );
234 ctls.setSearchScope( SearchControls.SUBTREE_SCOPE );
235
236 // specify the search filter to match
237 // ask for objects that have the attribute "objectCategory" =
238 // "CN=Group*"
239 String filter = getPropertySafe( "groupFilter" );
240 String context = getPropertySafe( "groupContext" );
241
242 // create initial PagedResultsControl
243 ctx.setRequestControls( new Control[] { new PagedResultsControl( pageSize, false ) } );
244
245 // phase 1: make sure that all groups from AD are present in the
246 // SecurityManager
247 // (register them to the SecurityManager if necessary)
248 do {
249 // perform the search
250 NamingEnumeration<?> answer = ctx.search( context, filter, ctls );
251
252 // process results of the last batch
253 while ( answer.hasMoreElements() ) {
254 SearchResult result = (SearchResult) answer.nextElement();
255 Attributes atts = result.getAttributes();
256 String distinguishedName = (String) atts.get( "distinguishedName" ).get();
257 String name = (String) atts.get( getPropertySafe( "groupName" ) ).get();
258 String title = (String) atts.get( getPropertySafe( "groupTitle" ) ).get();
259
260 // check if group is already registered
261 Group group = null;
262 try {
263 group = access.getGroupByName( name );
264 } catch ( UnknownException e ) {
265 // no -> register group
266 logBuffer.append( "Registering group: " + name + "\n" );
267 group = trans.registerGroup( name, title );
268 }
269 groupMap.put( name, group );
270 groupMap2.put( distinguishedName, group );
271 if ( atts.get( getPropertySafe( "groupMemberOf" ) ) != null ) {
272 memberOfMap.put( name, atts.get( getPropertySafe( "groupMemberOf" ) ).getAll() );
273 }
274 }
275
276 // examine the paged results control response
277 Control[] controls = ctx.getResponseControls();
278 if ( controls != null ) {
279 for ( int i = 0; i < controls.length; i++ ) {
280 if ( controls[i] instanceof PagedResultsResponseControl ) {
281 PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i];
282 // total = prrc.getResultSize();
283 cookie = prrc.getCookie();
284 }
285 }
286 }
287
288 if ( cookie != null ) {
289 // re-activate paged results
290 ctx.setRequestControls( new Control[] { new PagedResultsControl( pageSize, cookie, Control.CRITICAL ) } );
291 }
292 } while ( cookie != null );
293
294 // phase 2: make sure that all groups from the SecurityManager are known
295 // to the AD
296 // (deregister them from the SecurityManager if necessary)
297 Group[] sMGroups = access.getAllGroups();
298 for ( int i = 0; i < sMGroups.length; i++ ) {
299 if ( groupMap.get( sMGroups[i].getName() ) == null && sMGroups[i].getID() != Group.ID_SEC_ADMIN
300 && !( sMGroups[i].getName().equals( "SEC_ALL" ) ) ) {
301 logBuffer.append( "Deregistering group: " + sMGroups[i].getName() + "\n" );
302 trans.deregisterGroup( sMGroups[i] );
303 }
304 }
305
306 // phase 3: set the membership-relations between the groups
307 Iterator<String> it = groupMap.keySet().iterator();
308 while ( it.hasNext() ) {
309 String name = it.next();
310 Group group = groupMap.get( name );
311 NamingEnumeration<?> memberOf = memberOfMap.get( name );
312 ArrayList<Group> memberOfList = new ArrayList<Group>( 5 );
313
314 if ( memberOf != null ) {
315 while ( memberOf.hasMoreElements() ) {
316 String memberGroupName = (String) memberOf.nextElement();
317 Group memberGroup = groupMap2.get( memberGroupName );
318 if ( memberGroup != null ) {
319 memberOfList.add( memberGroup );
320 } else {
321 logBuffer.append( "Group " + name + " is member of unknown group " + memberGroupName
322 + ". Membership ignored.\n" );
323 }
324 }
325 }
326 Group[] newGroups = memberOfList.toArray( new Group[memberOfList.size()] );
327 trans.setGroupsForGroup( group, newGroups );
328 }
329 return groupMap2;
330 }
331
332 /**
333 * Synchronizes the AD's user objects with the SecurityManager's user objects.
334 * <p>
335 *
336 * @param groups
337 *
338 * @throws NamingException
339 * @throws IOException
340 * @throws GeneralSecurityException
341 * @throws UnauthorizedException
342 *
343 */
344 void synchronizeUsers( HashMap<String, Group> groups )
345 throws NamingException, IOException, UnauthorizedException, GeneralSecurityException {
346 // keys are names (Strings), values are User-objects
347 HashMap<String, User> userMap = new HashMap<String, User>( 20 );
348 // keys are names (Strings), values are NamingEnumeration-objects
349 HashMap<String, NamingEnumeration<?>> memberOfMap = new HashMap<String, NamingEnumeration<?>>( 20 );
350
351 byte[] cookie = null;
352
353 // specify the ids of the attributes to return
354 String[] attrIDs = { getPropertySafe( "userName" ), getPropertySafe( "userTitle" ),
355 getPropertySafe( "userFirstName" ), getPropertySafe( "userLastName" ),
356 getPropertySafe( "userMail" ), getPropertySafe( "userMemberOf" ) };
357
358 SearchControls ctls = new SearchControls();
359 ctls.setReturningAttributes( attrIDs );
360 ctls.setSearchScope( SearchControls.SUBTREE_SCOPE );
361
362 // specify the search filter to match
363 String filter = getPropertySafe( "userFilter" );
364 String context = getPropertySafe( "userContext" );
365
366 // create initial PagedResultsControl
367 ctx.setRequestControls( new Control[] { new PagedResultsControl( pageSize, false ) } );
368
369 // phase 1: make sure that all users from AD are present in the
370 // SecurityManager
371 // (register them to the SecurityManager if necessary)
372 do {
373 // perform the search
374 NamingEnumeration<?> answer = ctx.search( context, filter, ctls );
375
376 // process results of the last batch
377 while ( answer.hasMoreElements() ) {
378 SearchResult result = (SearchResult) answer.nextElement();
379
380 Attributes atts = result.getAttributes();
381 Attribute nameAtt = atts.get( getPropertySafe( "userName" ) );
382 // Attribute titleAtt = atts.get( getPropertySafe( "userTitle" ) );
383 Attribute firstNameAtt = atts.get( getPropertySafe( "userFirstName" ) );
384 Attribute lastNameAtt = atts.get( getPropertySafe( "userLastName" ) );
385 Attribute mailAtt = atts.get( getPropertySafe( "userMail" ) );
386 Attribute memberOfAtt = atts.get( getPropertySafe( "userMemberOf" ) );
387
388 String name = (String) nameAtt.get();
389 // String title = titleAtt != null ? (String) titleAtt.get() : "";
390 String firstName = firstNameAtt != null ? (String) firstNameAtt.get() : "" + "";
391 String lastName = lastNameAtt != null ? (String) lastNameAtt.get() : "" + "";
392 String mail = mailAtt != null ? (String) mailAtt.get() : "";
393
394 // check if user is already registered
395 User user = null;
396 try {
397 user = access.getUserByName( name );
398 } catch ( UnknownException e ) {
399 // no -> register user
400 logBuffer.append( "Registering user: " + name + "\n" );
401 user = trans.registerUser( name, null, lastName, firstName, mail );
402 }
403 userMap.put( name, user );
404
405 if ( memberOfAtt != null ) {
406 memberOfMap.put( name, memberOfAtt.getAll() );
407 }
408 }
409
410 // examine the paged results control response
411 Control[] controls = ctx.getResponseControls();
412 if ( controls != null ) {
413 for ( int i = 0; i < controls.length; i++ ) {
414 if ( controls[i] instanceof PagedResultsResponseControl ) {
415 PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i];
416 // total = prrc.getResultSize();
417 cookie = prrc.getCookie();
418 }
419 }
420 }
421
422 if ( cookie != null ) {
423 // re-activate paged results
424 ctx.setRequestControls( new Control[] { new PagedResultsControl( pageSize, cookie, Control.CRITICAL ) } );
425 }
426 } while ( cookie != null );
427
428 // phase 2: make sure that all users from the SecurityManager are known
429 // to the AD
430 // (deregister them from the SecurityManager if necessary)
431 User[] sMUsers = access.getAllUsers();
432 for ( int i = 0; i < sMUsers.length; i++ ) {
433 if ( userMap.get( sMUsers[i].getName() ) == null && sMUsers[i].getID() != User.ID_SEC_ADMIN ) {
434 logBuffer.append( "Deregistering user: " + sMUsers[i].getName() + "\n" );
435 trans.deregisterUser( sMUsers[i] );
436 }
437 }
438
439 // phase 3: set the membership-relations between the groups and the
440 // users
441 Iterator<String> it = userMap.keySet().iterator();
442 while ( it.hasNext() ) {
443 String name = it.next();
444 User user = userMap.get( name );
445 NamingEnumeration<?> memberOf = memberOfMap.get( name );
446 ArrayList<Group> memberOfList = new ArrayList<Group>( 5 );
447 if ( memberOf != null ) {
448 while ( memberOf.hasMoreElements() ) {
449 String memberGroupName = (String) memberOf.nextElement();
450 Group memberGroup = groups.get( memberGroupName );
451 if ( memberGroup != null ) {
452 memberOfList.add( memberGroup );
453 } else {
454 logBuffer.append( "User " + name + " is member of unknown group " + memberGroupName
455 + ". Membership ignored.\n" );
456 }
457 }
458 }
459 Group[] newGroups = memberOfList.toArray( new Group[memberOfList.size()] );
460 trans.setGroupsForUser( user, newGroups );
461 }
462
463 }
464
465 /**
466 * Updates the special group "SEC_ALL" (contains all users).
467 * <p>
468 *
469 * @throws GeneralSecurityException
470 *
471 */
472 void updateSecAll()
473 throws GeneralSecurityException {
474 Group secAll = null;
475
476 // phase1: make sure that group "SEC_ALL" exists
477 // (register it if necessary)
478 try {
479 secAll = access.getGroupByName( "SEC_ALL" );
480 } catch ( UnknownException e ) {
481 secAll = trans.registerGroup( "SEC_ALL", "SEC_ALL" );
482 }
483
484 // phase2: set all users to be members of this group
485 User[] allUsers = access.getAllUsers();
486 trans.setUsersInGroup( secAll, allUsers );
487
488 }
489
490 /**
491 * Checks subadmin-role validity (each user one role max).
492 * <p>
493 *
494 * @throws ManagementException
495 * @throws GeneralSecurityException
496 */
497 void checkSubadminRoleValidity()
498 throws ManagementException, GeneralSecurityException {
499 SecurityHelper.checkSubadminRoleValidity( access );
500 }
501
502 /**
503 * Aborts the synchronization process and undoes all changes.
504 */
505 public void abortChanges() {
506 if ( manager != null && trans != null ) {
507 try {
508 manager.abortTransaction( trans );
509 } catch ( GeneralSecurityException e ) {
510 e.printStackTrace();
511 }
512 }
513 if ( ctx != null ) {
514 try {
515 ctx.close();
516 } catch ( NamingException e ) {
517 e.printStackTrace();
518 }
519 }
520 }
521
522 /**
523 * Ends the synchronization process and commits all changes.
524 */
525 public void commitChanges() {
526 if ( manager != null && trans != null ) {
527 try {
528 manager.commitTransaction( trans );
529 } catch ( GeneralSecurityException e ) {
530 e.printStackTrace();
531 }
532 }
533 if ( ctx != null ) {
534 try {
535 ctx.close();
536 } catch ( NamingException e ) {
537 e.printStackTrace();
538 }
539 }
540 }
541
542 /**
543 * Sends an eMail to inform the admin that something went wrong.
544 * <p>
545 * NOTE: This is static, because it must be usable even when the construction of the ADExporter
546 * failed.
547 * <p>
548 *
549 * @param e
550 */
551 public static void sendError( Exception e ) {
552
553 try {
554 String mailMessage = "Beim Synchronisieren des ActiveDirectory mit der HUIS-"
555 + "Sicherheitsdatenbank ist ein Fehler aufgetreten.\n"
556 + "Die Synchronisierung wurde NICHT durchgeführt, der letzte "
557 + "Stand wurde wiederhergestellt.\n";
558 StringWriter sw = new StringWriter();
559 PrintWriter writer = new PrintWriter( sw );
560 e.printStackTrace( writer );
561 mailMessage += "\n\nDie Java-Fehlermeldung lautet:\n" + sw.getBuffer();
562
563 mailMessage += "\n\nMit freundlichem Gruss,\nIhr ADExporter";
564 MailHelper.createAndSendMail(
565 new EMailMessage( mailSender, mailRcpt, "Fehler im ADExporter", mailMessage ),
566 mailHost );
567 } catch ( SendMailException ex ) {
568 ex.printStackTrace();
569 }
570
571 }
572
573 /**
574 * Sends an eMail with a log of the transaction.
575 * <p>
576 */
577 public void sendLog() {
578
579 try {
580 String mailMessage = "Die Synchronisierung der HUIS-Sicherheitsdatenbank mit "
581 + "dem ActiveDirectory wurde erfolgreich durchgeführt:\n\n";
582 if ( logBuffer.length() == 0 ) {
583 mailMessage += "Keine Änderungen.";
584 } else {
585 mailMessage += logBuffer.toString();
586 }
587 mailMessage += "\n\nMit freundlichem Gruss,\nIhr ADExporter";
588 EMailMessage emm = new EMailMessage( mailSender, mailRcpt, "ActiveDirectory Sychronisierung durchgeführt",
589 mailMessage );
590 MailHelper.createAndSendMail( emm, mailHost );
591 } catch ( SendMailException ex ) {
592 ex.printStackTrace();
593 }
594
595 }
596
597 /**
598 * @param args
599 * @throws Exception
600 */
601 public static void main( String[] args )
602 throws Exception {
603
604 if ( args.length != 1 ) {
605 System.out.println( "USAGE: ADExporter configfile" );
606 System.exit( 0 );
607 }
608
609 long begin = System.currentTimeMillis();
610 System.out.println( "Beginning synchronisation..." );
611
612 ActiveDirectoryImporter exporter = null;
613 try {
614 Properties config = new Properties();
615 config.load( new FileInputStream( args[0] ) );
616 exporter = new ActiveDirectoryImporter( config );
617
618 HashMap<String, Group> groups = exporter.synchronizeGroups();
619 exporter.synchronizeUsers( groups );
620 exporter.updateSecAll();
621 exporter.checkSubadminRoleValidity();
622 exporter.commitChanges();
623 } catch ( Exception e ) {
624 if ( exporter != null ) {
625 exporter.abortChanges();
626 }
627 sendError( e );
628 System.err.println( "Synchronisation has been aborted. Error message: " );
629 e.printStackTrace();
630 System.exit( 0 );
631 }
632
633 if ( mailLog ) {
634 exporter.sendLog();
635 }
636
637 System.out.println( "Synchronisation took " + ( System.currentTimeMillis() - begin ) + " milliseconds." );
638
639 System.exit( 0 );
640 }
641 }