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