001: //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/trunk/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: 10546 $
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: rbezema $
103: *
104: * @version $Revision: 10546 $, $Date: 2008-03-11 01:32:54 -0700 (Tue, 11 Mar 2008) $
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,
145: GeneralSecurityException {
146:
147: this .config = config;
148:
149: // retrieve mail configuration first
150: mailSender = getPropertySafe("mailSender");
151: mailRcpt = getPropertySafe("mailRcpt");
152: mailHost = getPropertySafe("mailHost");
153: mailLog = getPropertySafe("mailLog").equals("true")
154: || getPropertySafe("mailLog").equals("yes") ? true
155: : false;
156:
157: // get a SecurityManager (with an SQLRegistry)
158: Properties registryProperties = new Properties();
159: registryProperties.put("driver", getPropertySafe("sqlDriver"));
160: registryProperties.put("url", getPropertySafe("sqlLogon"));
161: registryProperties.put("user", getPropertySafe("sqlUser"));
162: registryProperties.put("password", getPropertySafe("sqlPass"));
163:
164: // default timeout: 20 min
165: long timeout = 1200000;
166: try {
167: timeout = Long.parseLong(getPropertySafe("timeout"));
168: } catch (NumberFormatException e) {
169: logBuffer
170: .append("Specified property value for timeout invalid. "
171: + "Defaulting to 1200 (secs).");
172: }
173:
174: if (!SecurityAccessManager.isInitialized()) {
175: SecurityAccessManager.initialize(
176: "org.deegree.security.drm.SQLRegistry",
177: registryProperties, timeout);
178: }
179:
180: manager = SecurityAccessManager.getInstance();
181: admin = manager.getUserByName(getPropertySafe("u3rAdminName"));
182: admin.authenticate(getPropertySafe("u3rAdminPassword"));
183:
184: // prepare LDAP connection
185: String jndiURL = "ldap://" + getPropertySafe("ldapHost")
186: + ":389/";
187: String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
188: String authenticationMode = "simple";
189: String contextReferral = "ignore";
190: env = new Hashtable<String, String>();
191: env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
192: env.put(Context.PROVIDER_URL, jndiURL);
193: env.put(Context.SECURITY_AUTHENTICATION, authenticationMode);
194: env
195: .put(Context.SECURITY_PRINCIPAL,
196: getPropertySafe("ldapUser"));
197: env.put(Context.SECURITY_CREDENTIALS,
198: getPropertySafe("ldapPass"));
199: env.put(Context.REFERRAL, contextReferral);
200:
201: access = manager.acquireAccess(admin);
202: trans = manager.acquireTransaction(admin);
203: ctx = new InitialLdapContext(env, null);
204:
205: }
206:
207: /**
208: * Returns a configuration property. If it is not defined, an exception is thrown.
209: * <p>
210: *
211: * @param name
212: * @return a configuration property. If it is not defined, an exception is thrown.
213: */
214: private String getPropertySafe(String name) {
215:
216: String value = config.getProperty(name);
217: if (value == null) {
218: throw new RuntimeException(
219: "Configuration does not define needed property '"
220: + name + "'.");
221: }
222: return value;
223: }
224:
225: /**
226: * Synchronizes the AD's group objects with the SecurityManager's group objects.
227: * <p>
228: *
229: * @return the mapping of the String to the Groups
230: * @throws NamingException
231: * @throws IOException
232: * @throws GeneralSecurityException
233: * @throws UnauthorizedException
234: */
235: HashMap<String, Group> synchronizeGroups() throws NamingException,
236: IOException, UnauthorizedException,
237: GeneralSecurityException {
238: // keys are names (Strings), values are Group-objects
239: HashMap<String, Group> groupMap = new HashMap<String, Group>(20);
240: // keys are distinguishedNames (Strings), values are Group-objects
241: HashMap<String, Group> groupMap2 = new HashMap<String, Group>(
242: 20);
243: // keys are names (Strings), values are NamingEnumeration-objects
244: HashMap<String, NamingEnumeration<?>> memberOfMap = new HashMap<String, NamingEnumeration<?>>(
245: 20);
246:
247: byte[] cookie = null;
248:
249: // specify the ids of the attributes to return
250: String[] attrIDs = { "distinguishedName",
251: getPropertySafe("groupName"),
252: getPropertySafe("groupTitle"),
253: getPropertySafe("groupMemberOf") };
254:
255: // set SearchControls
256: SearchControls ctls = new SearchControls();
257: ctls.setReturningAttributes(attrIDs);
258: ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
259:
260: // specify the search filter to match
261: // ask for objects that have the attribute "objectCategory" =
262: // "CN=Group*"
263: String filter = getPropertySafe("groupFilter");
264: String context = getPropertySafe("groupContext");
265:
266: // create initial PagedResultsControl
267: ctx.setRequestControls(new Control[] { new PagedResultsControl(
268: pageSize, false) });
269:
270: // phase 1: make sure that all groups from AD are present in the
271: // SecurityManager
272: // (register them to the SecurityManager if necessary)
273: do {
274: // perform the search
275: NamingEnumeration<?> answer = ctx.search(context, filter,
276: ctls);
277:
278: // process results of the last batch
279: while (answer.hasMoreElements()) {
280: SearchResult result = (SearchResult) answer
281: .nextElement();
282: Attributes atts = result.getAttributes();
283: String distinguishedName = (String) atts.get(
284: "distinguishedName").get();
285: String name = (String) atts.get(
286: getPropertySafe("groupName")).get();
287: String title = (String) atts.get(
288: getPropertySafe("groupTitle")).get();
289:
290: // check if group is already registered
291: Group group = null;
292: try {
293: group = access.getGroupByName(name);
294: } catch (UnknownException e) {
295: // no -> register group
296: logBuffer.append("Registering group: " + name
297: + "\n");
298: group = trans.registerGroup(name, title);
299: }
300: groupMap.put(name, group);
301: groupMap2.put(distinguishedName, group);
302: if (atts.get(getPropertySafe("groupMemberOf")) != null) {
303: memberOfMap.put(name, atts.get(
304: getPropertySafe("groupMemberOf")).getAll());
305: }
306: }
307:
308: // examine the paged results control response
309: Control[] controls = ctx.getResponseControls();
310: if (controls != null) {
311: for (int i = 0; i < controls.length; i++) {
312: if (controls[i] instanceof PagedResultsResponseControl) {
313: PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i];
314: // total = prrc.getResultSize();
315: cookie = prrc.getCookie();
316: }
317: }
318: }
319:
320: if (cookie != null) {
321: // re-activate paged results
322: ctx
323: .setRequestControls(new Control[] { new PagedResultsControl(
324: pageSize, cookie, Control.CRITICAL) });
325: }
326: } while (cookie != null);
327:
328: // phase 2: make sure that all groups from the SecurityManager are known
329: // to the AD
330: // (deregister them from the SecurityManager if necessary)
331: Group[] sMGroups = access.getAllGroups();
332: for (int i = 0; i < sMGroups.length; i++) {
333: if (groupMap.get(sMGroups[i].getName()) == null
334: && sMGroups[i].getID() != Group.ID_SEC_ADMIN
335: && !(sMGroups[i].getName().equals("SEC_ALL"))) {
336: logBuffer.append("Deregistering group: "
337: + sMGroups[i].getName() + "\n");
338: trans.deregisterGroup(sMGroups[i]);
339: }
340: }
341:
342: // phase 3: set the membership-relations between the groups
343: Iterator<String> it = groupMap.keySet().iterator();
344: while (it.hasNext()) {
345: String name = it.next();
346: Group group = groupMap.get(name);
347: NamingEnumeration<?> memberOf = memberOfMap.get(name);
348: ArrayList<Group> memberOfList = new ArrayList<Group>(5);
349:
350: if (memberOf != null) {
351: while (memberOf.hasMoreElements()) {
352: String memberGroupName = (String) memberOf
353: .nextElement();
354: Group memberGroup = groupMap2.get(memberGroupName);
355: if (memberGroup != null) {
356: memberOfList.add(memberGroup);
357: } else {
358: logBuffer.append("Group " + name
359: + " is member of unknown group "
360: + memberGroupName
361: + ". Membership ignored.\n");
362: }
363: }
364: }
365: Group[] newGroups = memberOfList
366: .toArray(new Group[memberOfList.size()]);
367: trans.setGroupsForGroup(group, newGroups);
368: }
369: return groupMap2;
370: }
371:
372: /**
373: * Synchronizes the AD's user objects with the SecurityManager's user objects.
374: * <p>
375: *
376: * @param groups
377: *
378: * @throws NamingException
379: * @throws IOException
380: * @throws GeneralSecurityException
381: * @throws UnauthorizedException
382: *
383: */
384: void synchronizeUsers(HashMap<String, Group> groups)
385: throws NamingException, IOException, UnauthorizedException,
386: GeneralSecurityException {
387: // keys are names (Strings), values are User-objects
388: HashMap<String, User> userMap = new HashMap<String, User>(20);
389: // keys are names (Strings), values are NamingEnumeration-objects
390: HashMap<String, NamingEnumeration<?>> memberOfMap = new HashMap<String, NamingEnumeration<?>>(
391: 20);
392:
393: byte[] cookie = null;
394:
395: // specify the ids of the attributes to return
396: String[] attrIDs = { getPropertySafe("userName"),
397: getPropertySafe("userTitle"),
398: getPropertySafe("userFirstName"),
399: getPropertySafe("userLastName"),
400: getPropertySafe("userMail"),
401: getPropertySafe("userMemberOf") };
402:
403: SearchControls ctls = new SearchControls();
404: ctls.setReturningAttributes(attrIDs);
405: ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
406:
407: // specify the search filter to match
408: String filter = getPropertySafe("userFilter");
409: String context = getPropertySafe("userContext");
410:
411: // create initial PagedResultsControl
412: ctx.setRequestControls(new Control[] { new PagedResultsControl(
413: pageSize, false) });
414:
415: // phase 1: make sure that all users from AD are present in the
416: // SecurityManager
417: // (register them to the SecurityManager if necessary)
418: do {
419: // perform the search
420: NamingEnumeration<?> answer = ctx.search(context, filter,
421: ctls);
422:
423: // process results of the last batch
424: while (answer.hasMoreElements()) {
425: SearchResult result = (SearchResult) answer
426: .nextElement();
427:
428: Attributes atts = result.getAttributes();
429: Attribute nameAtt = atts
430: .get(getPropertySafe("userName"));
431: // Attribute titleAtt = atts.get( getPropertySafe( "userTitle" ) );
432: Attribute firstNameAtt = atts
433: .get(getPropertySafe("userFirstName"));
434: Attribute lastNameAtt = atts
435: .get(getPropertySafe("userLastName"));
436: Attribute mailAtt = atts
437: .get(getPropertySafe("userMail"));
438: Attribute memberOfAtt = atts
439: .get(getPropertySafe("userMemberOf"));
440:
441: String name = (String) nameAtt.get();
442: // String title = titleAtt != null ? (String) titleAtt.get() : "";
443: String firstName = firstNameAtt != null ? (String) firstNameAtt
444: .get()
445: : "" + "";
446: String lastName = lastNameAtt != null ? (String) lastNameAtt
447: .get()
448: : "" + "";
449: String mail = mailAtt != null ? (String) mailAtt.get()
450: : "";
451:
452: // check if user is already registered
453: User user = null;
454: try {
455: user = access.getUserByName(name);
456: } catch (UnknownException e) {
457: // no -> register user
458: logBuffer
459: .append("Registering user: " + name + "\n");
460: user = trans.registerUser(name, null, lastName,
461: firstName, mail);
462: }
463: userMap.put(name, user);
464:
465: if (memberOfAtt != null) {
466: memberOfMap.put(name, memberOfAtt.getAll());
467: }
468: }
469:
470: // examine the paged results control response
471: Control[] controls = ctx.getResponseControls();
472: if (controls != null) {
473: for (int i = 0; i < controls.length; i++) {
474: if (controls[i] instanceof PagedResultsResponseControl) {
475: PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i];
476: // total = prrc.getResultSize();
477: cookie = prrc.getCookie();
478: }
479: }
480: }
481:
482: if (cookie != null) {
483: // re-activate paged results
484: ctx
485: .setRequestControls(new Control[] { new PagedResultsControl(
486: pageSize, cookie, Control.CRITICAL) });
487: }
488: } while (cookie != null);
489:
490: // phase 2: make sure that all users from the SecurityManager are known
491: // to the AD
492: // (deregister them from the SecurityManager if necessary)
493: User[] sMUsers = access.getAllUsers();
494: for (int i = 0; i < sMUsers.length; i++) {
495: if (userMap.get(sMUsers[i].getName()) == null
496: && sMUsers[i].getID() != User.ID_SEC_ADMIN) {
497: logBuffer.append("Deregistering user: "
498: + sMUsers[i].getName() + "\n");
499: trans.deregisterUser(sMUsers[i]);
500: }
501: }
502:
503: // phase 3: set the membership-relations between the groups and the
504: // users
505: Iterator<String> it = userMap.keySet().iterator();
506: while (it.hasNext()) {
507: String name = it.next();
508: User user = userMap.get(name);
509: NamingEnumeration<?> memberOf = memberOfMap.get(name);
510: ArrayList<Group> memberOfList = new ArrayList<Group>(5);
511: if (memberOf != null) {
512: while (memberOf.hasMoreElements()) {
513: String memberGroupName = (String) memberOf
514: .nextElement();
515: Group memberGroup = groups.get(memberGroupName);
516: if (memberGroup != null) {
517: memberOfList.add(memberGroup);
518: } else {
519: logBuffer.append("User " + name
520: + " is member of unknown group "
521: + memberGroupName
522: + ". Membership ignored.\n");
523: }
524: }
525: }
526: Group[] newGroups = memberOfList
527: .toArray(new Group[memberOfList.size()]);
528: trans.setGroupsForUser(user, newGroups);
529: }
530:
531: }
532:
533: /**
534: * Updates the special group "SEC_ALL" (contains all users).
535: * <p>
536: *
537: * @throws GeneralSecurityException
538: *
539: */
540: void updateSecAll() throws GeneralSecurityException {
541: Group secAll = null;
542:
543: // phase1: make sure that group "SEC_ALL" exists
544: // (register it if necessary)
545: try {
546: secAll = access.getGroupByName("SEC_ALL");
547: } catch (UnknownException e) {
548: secAll = trans.registerGroup("SEC_ALL", "SEC_ALL");
549: }
550:
551: // phase2: set all users to be members of this group
552: User[] allUsers = access.getAllUsers();
553: trans.setUsersInGroup(secAll, allUsers);
554:
555: }
556:
557: /**
558: * Checks subadmin-role validity (each user one role max).
559: * <p>
560: *
561: * @throws ManagementException
562: * @throws GeneralSecurityException
563: */
564: void checkSubadminRoleValidity() throws ManagementException,
565: GeneralSecurityException {
566: SecurityHelper.checkSubadminRoleValidity(access);
567: }
568:
569: /**
570: * Aborts the synchronization process and undoes all changes.
571: */
572: public void abortChanges() {
573: if (manager != null && trans != null) {
574: try {
575: manager.abortTransaction(trans);
576: } catch (GeneralSecurityException e) {
577: e.printStackTrace();
578: }
579: }
580: if (ctx != null) {
581: try {
582: ctx.close();
583: } catch (NamingException e) {
584: e.printStackTrace();
585: }
586: }
587: }
588:
589: /**
590: * Ends the synchronization process and commits all changes.
591: */
592: public void commitChanges() {
593: if (manager != null && trans != null) {
594: try {
595: manager.commitTransaction(trans);
596: } catch (GeneralSecurityException e) {
597: e.printStackTrace();
598: }
599: }
600: if (ctx != null) {
601: try {
602: ctx.close();
603: } catch (NamingException e) {
604: e.printStackTrace();
605: }
606: }
607: }
608:
609: /**
610: * Sends an eMail to inform the admin that something went wrong.
611: * <p>
612: * NOTE: This is static, because it must be usable even when the construction of the ADExporter
613: * failed.
614: * <p>
615: *
616: * @param e
617: */
618: public static void sendError(Exception e) {
619:
620: try {
621: String mailMessage = "Beim Synchronisieren des ActiveDirectory mit der HUIS-"
622: + "Sicherheitsdatenbank ist ein Fehler aufgetreten.\n"
623: + "Die Synchronisierung wurde NICHT durchgeführt, der letzte "
624: + "Stand wurde wiederhergestellt.\n";
625: StringWriter sw = new StringWriter();
626: PrintWriter writer = new PrintWriter(sw);
627: e.printStackTrace(writer);
628: mailMessage += "\n\nDie Java-Fehlermeldung lautet:\n"
629: + sw.getBuffer();
630:
631: mailMessage += "\n\nMit freundlichem Gruss,\nIhr ADExporter";
632: MailHelper.createAndSendMail(new EMailMessage(mailSender,
633: mailRcpt, "Fehler im ADExporter", mailMessage),
634: mailHost);
635: } catch (SendMailException ex) {
636: ex.printStackTrace();
637: }
638:
639: }
640:
641: /**
642: * Sends an eMail with a log of the transaction.
643: * <p>
644: */
645: public void sendLog() {
646:
647: try {
648: String mailMessage = "Die Synchronisierung der HUIS-Sicherheitsdatenbank mit "
649: + "dem ActiveDirectory wurde erfolgreich durchgeführt:\n\n";
650: if (logBuffer.length() == 0) {
651: mailMessage += "Keine Änderungen.";
652: } else {
653: mailMessage += logBuffer.toString();
654: }
655: mailMessage += "\n\nMit freundlichem Gruss,\nIhr ADExporter";
656: EMailMessage emm = new EMailMessage(mailSender, mailRcpt,
657: "ActiveDirectory Sychronisierung durchgeführt",
658: mailMessage);
659: MailHelper.createAndSendMail(emm, mailHost);
660: } catch (SendMailException ex) {
661: ex.printStackTrace();
662: }
663:
664: }
665:
666: /**
667: * @param args
668: * @throws Exception
669: */
670: public static void main(String[] args) throws Exception {
671:
672: if (args.length != 1) {
673: System.out.println("USAGE: ADExporter configfile");
674: System.exit(0);
675: }
676:
677: long begin = System.currentTimeMillis();
678: System.out.println("Beginning synchronisation...");
679:
680: ActiveDirectoryImporter exporter = null;
681: try {
682: Properties config = new Properties();
683: config.load(new FileInputStream(args[0]));
684: exporter = new ActiveDirectoryImporter(config);
685:
686: HashMap<String, Group> groups = exporter
687: .synchronizeGroups();
688: exporter.synchronizeUsers(groups);
689: exporter.updateSecAll();
690: exporter.checkSubadminRoleValidity();
691: exporter.commitChanges();
692: } catch (Exception e) {
693: if (exporter != null) {
694: exporter.abortChanges();
695: }
696: sendError(e);
697: System.err
698: .println("Synchronisation has been aborted. Error message: ");
699: e.printStackTrace();
700: System.exit(0);
701: }
702:
703: if (mailLog) {
704: exporter.sendLog();
705: }
706:
707: System.out.println("Synchronisation took "
708: + (System.currentTimeMillis() - begin)
709: + " milliseconds.");
710:
711: System.exit(0);
712: }
713: }
|