/* GASHBuilderTask.java This class is intended to dump the Ganymede datastore to GASH. Created: 21 May 1998 Module By: Jonathan Abbey, jonabbey@arlut.utexas.edu ----------------------------------------------------------------------- Ganymede Directory Management System Copyright (C) 1996-2014 The University of Texas at Austin Ganymede is a registered trademark of The University of Texas at Austin Contact information Web site: http://www.arlut.utexas.edu/gash2 Author Email: ganymede_author@arlut.utexas.edu Email mailing list: ganymede@arlut.utexas.edu US Mail: Computer Science Division Applied Research Laboratories The University of Texas at Austin PO Box 8029, Austin TX 78713-8029 Telephone: (512) 835-3200 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package arlut.csd.ganymede.gasharl; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.io.Writer; import java.rmi.RemoteException; import java.util.Comparator; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.Vector; import arlut.csd.Util.CaseInsensitiveSet; import arlut.csd.Util.FileOps; import arlut.csd.Util.NullWriter; import arlut.csd.Util.PathComplete; import arlut.csd.Util.SharedStringBuffer; import arlut.csd.Util.VectorUtils; import arlut.csd.ganymede.common.Invid; import arlut.csd.ganymede.common.SchemaConstants; import arlut.csd.ganymede.server.DBField; import arlut.csd.ganymede.server.DBLogEvent; import arlut.csd.ganymede.server.DBObject; import arlut.csd.ganymede.server.Ganymede; import arlut.csd.ganymede.server.GanymedeBuilderTask; import arlut.csd.ganymede.server.IPDBField; import arlut.csd.ganymede.server.InvidDBField; import arlut.csd.ganymede.server.PasswordDBField; import arlut.csd.ganymede.server.StringDBField; import arlut.csd.ganymede.server.ServiceNotFoundException; import arlut.csd.ganymede.server.ServiceFailedException; /*------------------------------------------------------------------------------ class GASHBuilderTask ------------------------------------------------------------------------------*/ /** * * This class is intended to dump the Ganymede datastore to GASH. * * * @author Jonathan Abbey jonabbey@arlut.utexas.edu * */ public class GASHBuilderTask extends GanymedeBuilderTask { public final static boolean debug = false; private static String path = null; private static String dnsdomain = null; private static String buildScript = null; private static final String normalCategoryLabel = "normal"; private static final String agencyCategoryLabel = "agency worker"; // --- private Date now = null; private SharedStringBuffer result = new SharedStringBuffer(); /** * We cache the normalCategory during the build cycle to make the * generation of export data for IRIS a bit more efficient. */ private Invid normalCategory = null; /** * We cache the agencyCategory during the build cycle to make the * generation of export data for IRIS a bit more efficient. */ private Invid agencyCategory = null; /** * <p>customOptions is a Set of Invids for custom type definitions that * we encountered during a cycle of writing out DHCP information.</p> * * <p>We'll use this Set to keep track of custom options that we find * during the generation of our dhcp output. At all other times, * this Map will be null.</p> */ private Set<Invid> customOptions = null; /* -- */ public GASHBuilderTask(Invid _taskObjInvid) { // set the taskDefObjInvid in GanymedeBuilderTask so // we can look up option strings taskDefObjInvid = _taskObjInvid; } /** * <p>This method runs with a dumpLock obtained for the builder * task.</p> * * <p>Code run in builderPhase1() can call getObjects() and * baseChanged().</p> * * @return true if builderPhase1 made changes necessitating the * execution of builderPhase2. */ @Override public boolean builderPhase1() { PrintWriter out; boolean success = false; /* -- */ Ganymede.debug("build: GASHBuilderTask writing files"); if (path == null) { path = System.getProperty("ganymede.builder.output"); if (path == null) { throw new RuntimeException("GASHBuilder not able to determine output directory.. need to set the ganymede.builder.output property in ganymede.properties."); } path = PathComplete.completePath(path); } if (dnsdomain == null) { dnsdomain = System.getProperty("ganymede.gash.dnsdomain"); if (dnsdomain == null) { throw new RuntimeException("GASHBuilder not able to determine DNS domain."); } // prepend a dot if we need to if (dnsdomain.indexOf('.') != 0) { dnsdomain = "." + dnsdomain; } } now = null; // passwd if (baseChanged(SchemaConstants.UserBase)) { if (debug) { Ganymede.debug("Need to build user map"); } out = null; try { out = openOutFile(path + "user_info", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.builderPhase1(): couldn't open user_info file: " + ex); } if (out != null) { try { for (DBObject user: getObjects(SchemaConstants.UserBase)) { writeUserLine(user, out); } } finally { out.close(); } } out = null; try { out = openOutFile(path + "passwd.3des", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.builderPhase1(): couldn't open passwd.3des file: " + ex); } if (out != null) { try { for (DBObject user: getObjects(SchemaConstants.UserBase)) { write3Des(user, out); } } finally { out.close(); } } writeMailDirect2(); writeMailDirect3and4(); // the new gany_iris_export.txt file for Carrie writeSambafileVersion1(); writeSambafileVersion2(); writeUserSyncFile(); writeExternalMailFiles(); success = true; } if (baseChanged(userSchema.BASE) || baseChanged(groupSchema.BASE) || // in case of rename baseChanged(userNetgroupSchema.BASE)) // ditto { writeHTTPfiles(); success = true; } // group if (baseChanged(groupSchema.BASE) || baseChanged(SchemaConstants.UserBase)) // in case a user was renamed { if (debug) { Ganymede.debug("Need to build group map"); } out = null; try { out = openOutFile(path + "group_info", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.builderPhase1(): couldn't open group_info file: " + ex); } PrintWriter out2 = null; try { out2 = openOutFile(path + "group.owner", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.builderPhase1(): couldn't open group.owner file: " + ex); } try { for (DBObject group: getObjects(groupSchema.BASE)) { if (out != null) { writeGroupLine(group, out); } if (out2 != null) { writeGroupOwnerLine(group, out2); } } } finally { if (out != null) { out.close(); } if (out2 != null) { out2.close(); } } success = true; } if (baseChanged(SchemaConstants.UserBase) || // users baseChanged(groupSchema.BASE) || // account groups baseChanged(userNetgroupSchema.BASE) || // user netgroups baseChanged(emailListSchema.BASE) || // mail lists baseChanged(emailRedirectSchema.BASE) || // external mail addresses baseChanged(MailmanListSchema.BASE)) // mailman lists { if (debug) { Ganymede.debug("Need to build aliases map"); } if (writeAliasesFile() && writeEmailLists()) { success = true; } // the postfix versions of those files. // jgs, 15 feb 2010 try { if (writeHashAliasesFile()) { success = true; } } catch (IOException ex) { throw new RuntimeException(ex); } } if (baseChanged(MailmanListSchema.BASE)) // mailman lists { if (debug) { Ganymede.debug("Need to call mailman ns8 sync script"); } if (writeMailmanListsFile()) { success = true; } } if (baseChanged(systemNetgroupSchema.BASE) || // system netgroups baseChanged(userNetgroupSchema.BASE) || // user netgroups baseChanged(SchemaConstants.UserBase) || // in case users were renamed baseChanged(systemSchema.BASE)) // in case systems were renamed { if (debug) { Ganymede.debug("Need to build netgroup map"); } if (writeNetgroupFile()) { success = true; } if (writeUserNetgroupFile()) { success = true; } } if (baseChanged(mapSchema.BASE) || // automounter maps baseChanged(volumeSchema.BASE) || // nfs volumes baseChanged(systemSchema.BASE) || // in case systems were renamed baseChanged(SchemaConstants.UserBase) || // in case users were renamed baseChanged(mapEntrySchema.BASE)) // automounter map entries { if (debug) { Ganymede.debug("Need to build automounter maps"); } if (writeAutoMounterFiles()) { success = true; } } if (baseChanged(systemSchema.BASE) || // system base baseChanged(networkSchema.BASE) || // I.P. Network base baseChanged(interfaceSchema.BASE) || // system interface base baseChanged(roomSchema.BASE)) { if (debug) { Ganymede.debug("Need to build DNS tables"); } writeSysFile(); writeSysDataFile(); success = true; } if (baseChanged(systemSchema.BASE) || baseChanged(networkSchema.BASE) || baseChanged(interfaceSchema.BASE) || baseChanged(dhcpGroupSchema.BASE) || baseChanged(dhcpOptionSchema.BASE) || baseChanged(dhcpEntrySchema.BASE) || baseChanged(dhcpNetworkSchema.BASE) || baseChanged(dhcpSubnetSchema.BASE)) { if (debug) { Ganymede.debug("Need to build DHCP configuration file"); } writeDHCPFile(); success = true; } return success; } /** * * This method runs after this task's dumpLock has been * relinquished. This method is intended to be used to finish off a * build process by running (probably external) code that does not * require direct access to the database. * * builderPhase2 is only run if builderPhase1 returns true. * */ @Override public boolean builderPhase2() { File file; /* -- */ Ganymede.debug("build: GASHBuilderTask running build"); if (buildScript == null) { buildScript = System.getProperty("ganymede.builder.scriptlocation"); if (buildScript == null) { Ganymede.debug("GASHBuilderTask couldn't find any property definition for ganymede.builder.scriptlocation."); Ganymede.debug("\nNot executing external build for GASHBuilderTask."); return false; } buildScript = PathComplete.completePath(buildScript); buildScript = buildScript + "gashbuilder"; } int resultCode = -999; // a resultCode of 0 is success file = new File(buildScript); boolean startedOk = false; if (file.exists()) { try { resultCode = FileOps.runProcess(buildScript); startedOk = true; } catch (IOException ex) { Ganymede.debug("Couldn't exec buildScript (" + buildScript + ") due to IOException: " + ex); } catch (InterruptedException ex) { Ganymede.debug("Failure during exec of buildScript (" + buildScript + "): " + ex); } } else { Ganymede.debug(buildScript + " doesn't exist, not running external GASH build script"); } if (resultCode != 0) { String path = ""; try { path = file.getCanonicalPath(); } catch (IOException ex) { path = buildScript; } String message = "Error encountered running sync script \"" + path + "\" for the GASHBuilderTask." + "\n\nI got a result code of " + resultCode + " when I tried to run it."; DBLogEvent event = new DBLogEvent("externalerror", message, null, null, null, null); Ganymede.log.logSystemEvent(event); if (startedOk) { throw new ServiceFailedException("gashbuilder returned a failure code: " + resultCode); } else { if (!file.exists()) { throw new ServiceNotFoundException("Couldn't find " + path); } else { throw new ServiceNotFoundException("Couldn't run " + path); } } } if (debug) { Ganymede.debug("GASHBuilderTask builderPhase2 completed"); } return true; } // *** // // The following private methods are used to support the GASH builder logic. // // *** /** * * This method writes out a line to the user_info GASH source file. * * The lines in this file look like the following. * * broccol:393T6k3e/9/w2:12003:12010:Jonathan Abbey,S321 CSD,3199,8343915:/home/broccol:/bin/tcsh:ss#:normal:exp:lastadm * * @param object An object from the Ganymede user object base * @param writer The destination for this user line * */ private void writeUserLine(DBObject object, PrintWriter writer) { String username; String cryptedPass; int uid; int gid; String name; String room; String div; String officePhone; String homePhone; String directory; String shell; Invid categoryInvid; String category; PasswordDBField passField; Invid groupInvid; DBObject group; boolean inactivated = false; /* -- */ result.setLength(0); username = (String) object.getFieldValueLocal(SchemaConstants.UserUserName); passField = object.getPassField(SchemaConstants.UserPassword); if (passField != null) { cryptedPass = passField.getMD5CryptText(); } else { inactivated = true; // System.err.println("GASHBuilder.writeUserLine(): null password for user " + username); cryptedPass = "**Nopass**"; } uid = ((Integer) object.getFieldValueLocal(userSchema.UID)).intValue(); // extra security precaution.. homey don't play no root accounts in NIS games. if (uid == 0) { Ganymede.debug("GASHBuilder.writeUserLine(): *** root uid in user " + username + ", skipping!! ***"); return; // no writeLine } // get the gid groupInvid = (Invid) object.getFieldValueLocal(userSchema.HOMEGROUP); // home group if (groupInvid == null) { System.err.println("GASHBuilder.writeUserLine(): null gid for user " + username); gid = -1; } else { group = getObject(groupInvid); gid = ((Integer) group.getFieldValueLocal(groupSchema.GID)).intValue(); } name = (String) object.getFieldValueLocal(userSchema.FULLNAME); room = (String) object.getFieldValueLocal(userSchema.ROOM); div = (String) object.getFieldValueLocal(userSchema.DIVISION); officePhone = (String) object.getFieldValueLocal(userSchema.OFFICEPHONE); homePhone = (String) object.getFieldValueLocal(userSchema.HOMEPHONE); directory = (String) object.getFieldValueLocal(userSchema.HOMEDIR); // force /bin/false if the user is inactivated if (inactivated) { shell = "/bin/false"; } else { shell = (String) object.getFieldValueLocal(userSchema.LOGINSHELL); } // now build our output line result.append(username); result.append(":"); result.append(cryptedPass); result.append(":"); result.append(Integer.toString(uid)); result.append(":"); result.append(Integer.toString(gid)); result.append(":"); result.append(name); result.append(","); result.append(room); result.append(" "); result.append(div); result.append(","); result.append(officePhone); if (homePhone != null && !homePhone.equals("")) { result.append(","); result.append(homePhone); } result.append(":"); result.append(directory); result.append(":"); result.append(shell); result.append("::"); categoryInvid = (Invid) object.getFieldValueLocal(userSchema.CATEGORY); if (categoryInvid != null) { category = getLabel(categoryInvid); result.append(category); } result.append(":"); Date expDate = (Date) object.getFieldValueLocal(SchemaConstants.ExpirationField); if (expDate != null) { long timecode = expDate.getTime(); // we want to emit a UNIX timecode, which is one thousandth the // value of the Java timecode. We will overflow here if the // expiration date is past 2038, but this will make Steve happy. int mytimecode = (int) (timecode/1000); result.append(mytimecode); } else { result.append("0"); } // Back in the GASH days, we appended the name of the admin who // last modified this record. nowadays we completely ignore this // field, but it's here for compatibility with external scripts // that we've carried over from the GASH world. result.append(":ganymede"); if (result.length() > 1024) { System.err.println("GASHBuilder.writeUserLine(): Warning! user " + username + " overflows the GASH line length!"); } writer.println(result.toString()); } /** * <p>This method writes out a line to the passwd.3des GASH source file.</p> * * <p>The lines in this file look like the following.</p> * * <p>broccol:393T6k3e/9/w2:12003:12010:Jonathan Abbey,S321 CSD,3199,8343915:/home/broccol:/bin/tcsh</p> * * @param object An object from the Ganymede user object base * @param writer The destination for this user line */ private void write3Des(DBObject object, PrintWriter writer) { String username; String cryptedPass; int uid; int gid; String name; String room; String div; String officePhone; String homePhone; String directory; String shell; PasswordDBField passField; Invid groupInvid; DBObject group; boolean inactivated = false; /* -- */ result.setLength(0); username = (String) object.getFieldValueLocal(SchemaConstants.UserUserName); passField = object.getPassField(SchemaConstants.UserPassword); if (passField != null) { cryptedPass = passField.getUNIXCryptText(); } else { inactivated = true; // System.err.println("GASHBuilder.write3Des(): null password for user " + username); cryptedPass = "**Nopass**"; } uid = ((Integer) object.getFieldValueLocal(userSchema.UID)).intValue(); // extra security precaution.. homey don't play no root accounts in NIS games. if (uid == 0) { Ganymede.debug("GASHBuilder.write3Des(): *** root uid in user " + username + ", skipping!! ***"); return; // no writeLine } // get the gid groupInvid = (Invid) object.getFieldValueLocal(userSchema.HOMEGROUP); // home group if (groupInvid == null) { System.err.println("GASHBuilder.write3Des(): null gid for user " + username); gid = -1; } else { group = getObject(groupInvid); gid = ((Integer) group.getFieldValueLocal(groupSchema.GID)).intValue(); } name = (String) object.getFieldValueLocal(userSchema.FULLNAME); room = (String) object.getFieldValueLocal(userSchema.ROOM); div = (String) object.getFieldValueLocal(userSchema.DIVISION); officePhone = (String) object.getFieldValueLocal(userSchema.OFFICEPHONE); homePhone = (String) object.getFieldValueLocal(userSchema.HOMEPHONE); directory = (String) object.getFieldValueLocal(userSchema.HOMEDIR); // force /bin/false if the user is inactivated if (inactivated) { shell = "/bin/false"; } else { shell = (String) object.getFieldValueLocal(userSchema.LOGINSHELL); } // now build our output line result.append(username); result.append(":"); result.append(cryptedPass); result.append(":"); result.append(Integer.toString(uid)); result.append(":"); result.append(Integer.toString(gid)); result.append(":"); result.append(name); result.append(","); result.append(room); result.append(" "); result.append(div); result.append(","); result.append(officePhone); if (homePhone != null && !homePhone.equals("")) { result.append(","); result.append(homePhone); } result.append(":"); result.append(directory); result.append(":"); result.append(shell); writer.println(result.toString()); } /** * <p>we write out a file that maps badge numbers to a user's * primary email address and user name for the personnel office's * phonebook database to use</p> * * <p>This method writes lines to the maildirect2 GASH output file.</p> * * <p>The lines in this file look like the following.</p> * * <p>8124 jonabbey@arlut.utexas.edu broccol</p> */ private void writeMailDirect2() { PrintWriter out; Map<String, DBObject> map = new HashMap<String, DBObject>(); // map badge numbers to DBObject Map<String, String> results = new HashMap<String, String>(); // map badge numbers to strings /* -- */ try { out = openOutFile(path + "maildirect2", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.builderPhase1(): couldn't open maildirect2 file: " + ex); return; } try { for (DBObject user: getObjects(SchemaConstants.UserBase)) { String username = (String) user.getFieldValueLocal(SchemaConstants.UserUserName); String signature = (String) user.getFieldValueLocal(userSchema.SIGNATURE); String badgeNum = (String) user.getFieldValueLocal(userSchema.BADGE); Invid category = (Invid) user.getFieldValueLocal(userSchema.CATEGORY); if (username != null && signature != null && badgeNum != null && category != null && category.equals(getNormalCategory()) && !user.isInactivated()) { if (map.containsKey(badgeNum)) { // we've got more than one entry with the same // badge number.. that should only // happen if one of the users is an GASH admin, or // if one is inactivated. DBObject oldUser = map.get(badgeNum); DBField field = oldUser.getField(userSchema.PERSONAE); if (field != null && field.isDefined()) { continue; // we've already got an admin record for this badge number } } result.setLength(0); result.append(badgeNum); result.append(" "); result.append(signature); result.append("@"); result.append(dnsdomain.substring(1)); // skip leading . result.append(" "); result.append(username); map.put(badgeNum, user); results.put(badgeNum, result.toString()); } } for (String line: results.values()) { out.println(line); } } finally { out.close(); } } /** * <p>We write out a couple of files that maps badge numbers to a * user's primary email address and user name for the personnel * office's phonebook database to use</p> * * <p>This method writes lines to the gany_iris_export.txt GASH * output file.</p> * * <p>The lines in this file look like the following.</p> * * <pre>XXXXXXYYYYYYYYZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ</pre> * * <p>Where XXXXXX is the badge number with trailing spaces if * necessary, YYYYYYYY is the username with trailing spaces if * necessary (up to 8 chars in one file, 12 in second), and * ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ is the email * address, with trailing spaces if necessary (up to 50 characters, * 32 plus '@arlut.utexas.edu'.</p> */ private void writeMailDirect3and4() { writeIRISMailFile(path + "gany_iris_export.txt", 8); writeIRISMailFile(path + "gany_iris_export_long.txt", 12); } private void writeIRISMailFile(String exportFilePath, int userFieldLength) { PrintWriter out; HashMap<String, DBObject> map = new HashMap<String, DBObject>(); // map badge numbers to DBObject HashMap<String, String> results = new HashMap<String, String>(); // map badge numbers to strings try { out = openOutFile(exportFilePath, "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.builderPhase1(): couldn't open gany_iris_export.txt file: " + ex); return; } try { for (DBObject user: getObjects(SchemaConstants.UserBase)) { String username = (String) user.getFieldValueLocal(SchemaConstants.UserUserName); String signature = (String) user.getFieldValueLocal(userSchema.SIGNATURE); String badgeNum = (String) user.getFieldValueLocal(userSchema.BADGE); Invid category = (Invid) user.getFieldValueLocal(userSchema.CATEGORY); if (username != null && signature != null && badgeNum != null && category != null && (category.equals(getNormalCategory()) || category.equals(getAgencyCategory())) && !user.isInactivated()) { if (map.containsKey(badgeNum)) { // we've got more than one entry with the same // badge number.. that should only // happen if one of the users is an GASH admin, or // if one is inactivated. DBObject oldUser = map.get(badgeNum); DBField field = oldUser.getField(userSchema.PERSONAE); if (field != null && field.isDefined()) { continue; // we've already got an admin record for this badge number } } result.setLength(0); result.append(badgeNum); int length = 6 - badgeNum.length(); for (int i = 0; i < length; i++) { result.append(" "); } result.append(username); length = userFieldLength - username.length(); for (int i = 0; i < length; i++) { result.append(" "); } String emailAddr = signature + "@" + dnsdomain.substring(1); result.append(emailAddr); length = 50 - emailAddr.length(); for (int i = 0; i < length; i++) { result.append(" "); } map.put(badgeNum, user); results.put(badgeNum, result.toString()); } } for (String line: results.values()) { out.println(line); } } finally { out.close(); } } /** * <p>This method writes out a simple list of all ARL employees who * are to receive email when lab-wide email is sent.</p> * * <p>The file is simple, and contains one user name per line.</p> * * <p>We take the trouble in this method to eliminate redundant * mailings that would come to the same person if they were in * multiple component lists.</p> */ private boolean writeEmailLists() { PrintWriter out, out2; Set targets = new HashSet(); // record delivery targets we've seen Set targets2 = new HashSet(); // the same, for employees only /* -- */ try { out = openOutFile(path + "all_users.txt", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.builderPhase1(): couldn't open all_users.txt file: " + ex); return false; } try { out2 = openOutFile(path + "all_employees.txt", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.builderPhase1(): couldn't open all_employees.txt file: " + ex); out.close(); return false; } try { out.println("# all_users.txt"); out.println("#"); out.println("# Complete list of user names who should receive email when mail is sent"); out.println("# to 'all users' in laboratory, filtered to avoid redundant delivery."); out.println("#"); out.println("# " + (new Date()).toString()); out.println(); out2.println("# all_employees.txt"); out2.println("#"); out2.println("# Complete list of employees in the laboratory who should receive email when mail is sent"); out2.println("# to 'all employees' in laboratory, filtered to avoid redundant delivery."); out2.println("#"); out2.println("# " + (new Date()).toString()); out2.println(); for (DBObject user: getObjects(SchemaConstants.UserBase)) { if (user.isInactivated()) { continue; } Invid category = (Invid) user.getFieldValueLocal(userSchema.CATEGORY); if (category == null || (!category.equals(getNormalCategory()) && !category.equals(getAgencyCategory()))) { continue; } String username = (String) user.getFieldValueLocal(SchemaConstants.UserUserName); Vector<String> deliveryAddresses = (Vector<String>) user.getFieldValuesLocal(userSchema.EMAILTARGET); // first all users if (!targets.contains(username) && (category.equals(getNormalCategory()) || category.equals(getAgencyCategory()))) { boolean allSeen = true; for (String addr: deliveryAddresses) { if (!targets.contains(addr)) { allSeen = false; targets.add(addr); } } if (!allSeen) { targets.add(username); out.println(username); } } // then employees only if (!targets2.contains(username) && category.equals(getNormalCategory())) { boolean allSeen = true; for (String addr: deliveryAddresses) { if (!targets2.contains(addr)) { allSeen = false; targets2.add(addr); } } if (!allSeen) { targets2.add(username); out2.println(username); } } } } finally { out.close(); out2.close(); } return true; } /** * <p>This method writes out a line to the group_info GASH source * file.</p> * * <p>The lines in this file look like the following.</p> * * <pre>adgacc:ZzZz:4015:hammp,jgeorge,dd,doodle,dhoss,corbett,monk</pre> * * @param object An object from the Ganymede user object base * @param writer The destination for this user line */ private void writeGroupLine(DBObject object, PrintWriter writer) { String groupname; String pass; int gid; Vector<String> users = new Vector<String>(); Vector<Invid> invids; String userName; String contract; String description; /* -- */ result.setLength(0); groupname = (String) object.getFieldValueLocal(groupSchema.GROUPNAME); // currently in the GASH schema, we skip group passwords gid = ((Integer) object.getFieldValueLocal(groupSchema.GID)).intValue(); invids = (Vector<Invid>) object.getFieldValuesLocal(groupSchema.USERS); for (Invid userInvid: invids) { userName = getLabel(userInvid); if (userName != null) { users.add(userName); } } // now build our output line result.append(groupname); result.append("::"); result.append(Integer.toString(gid)); result.append(":"); for (int i = 0; i < users.size(); i++) { if (i > 0) { result.append(","); } result.append(users.get(i)); } // okay, this marks the end of what we care about for the NIS // group map. We'll check the line length here, even though we // may add more GASH-type stuff afterwards. All of that gets // skipped when it comes time to make the maps. if (result.length() > 1024) { System.err.println("GASHBuilder.writeGroupLine(): Warning! group " + groupname + " overflows the GASH line length!"); } description = (String) object.getFieldValueLocal(groupSchema.DESCRIPTION); Invid contractLink = (Invid) object.getFieldValueLocal(groupSchema.CONTRACTLINK); if (contractLink != null) { contract = getLabel(contractLink); } else { contract = (String) object.getFieldValueLocal(groupSchema.CONTRACT); } result.append(":"); result.append(contract); result.append(":"); result.append(description); writer.println(result.toString()); } /** * * This method writes out a line to the group.owner source file. * * The lines in this file look like the following. * * adgacc:ITL,ATL * * @param object An object from the Ganymede user object base * @param writer The destination for this user line * */ private void writeGroupOwnerLine(DBObject object, PrintWriter writer) { String groupname; Vector<Invid> ownerList; Vector<String> ownerStringList = new Vector<String>(); /* -- */ result.setLength(0); groupname = (String) object.getFieldValueLocal(groupSchema.GROUPNAME); ownerList = (Vector<Invid>) object.getFieldValuesLocal(SchemaConstants.OwnerListField); for (Invid invid: ownerList) { ownerStringList.add(getLabel(invid)); } // now build our output line result.append(groupname); result.append(":"); for (int i = 0; i < ownerStringList.size(); i++) { if (i != 0) { result.append(","); } result.append(ownerStringList.get(i)); } writer.println(result.toString()); } /** * * This method generates a Netgroup file. * */ private boolean writeNetgroupFile() { PrintWriter netgroupFile = null; /* -- */ try { netgroupFile = openOutFile(path + "netgroup", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeNetgroup(): couldn't open netgroup file: " + ex); } try { // first the user netgroups for (DBObject netgroup: getObjects(userNetgroupSchema.BASE)) { writeUserNetgroup(netgroup, netgroupFile); } // now the system netgroups for (DBObject netgroup: getObjects(systemNetgroupSchema.BASE)) { writeSystemNetgroup(netgroup, netgroupFile); } } finally { netgroupFile.close(); } return true; } /** * * This method writes out a single user netgroup out to disk, * wrapping the netgroup if it gets too long. * * omg-u csd-u (-,broccol,) (-,gomod,) (-,etcrh,) * */ private void writeUserNetgroup(DBObject object, PrintWriter writer) { StringBuilder buffer = new StringBuilder(); String name; Vector<Invid> users; Vector<Invid> memberNetgroups; int lengthlimit; int subgroup = 1; String subname; /* -- */ name = (String) object.getFieldValueLocal(userNetgroupSchema.NETGROUPNAME); users = (Vector<Invid>) object.getFieldValuesLocal(userNetgroupSchema.USERS); memberNetgroups = (Vector<Invid>) object.getFieldValuesLocal(userNetgroupSchema.MEMBERGROUPS); // NIS limits the length of a line to 1024 characters. // If the line looks like it'll go over, we'll truncate // it, put in an entry to link the netgroup with a // sub netgroup for continuation. // Thus, we want to save enough space to be able to put the link // information at the end. We reduce it by a further 6 chars to // leave space for the per-entry syntax. // We could do this check during our buffer building loop, but by // doing it up front we guarantee that we're never going to exceed // the real limit during any single iteration of our netgroup line // construction without having to constantly be adding 6 to the // item length. lengthlimit = 900 - name.length() - 6; buffer.append(name); for (Invid ref: memberNetgroups) { String refLabel = getLabel(ref); if (buffer.length() + refLabel.length() > lengthlimit) { if (subgroup > 1) { subname = name + "-ext" + subgroup; } else { subname = name + "-ext"; } buffer.append(" "); buffer.append(subname); writer.println(buffer.toString()); buffer = new StringBuilder(); buffer.append(subname); subgroup++; } buffer.append(" "); buffer.append(refLabel); } for (Invid ref: users) { String refLabel = getLabel(ref); if (buffer.length() + refLabel.length() > lengthlimit) { if (subgroup > 1) { subname = name + "-ext" + subgroup; } else { subname = name + "-ext"; } buffer.append(" "); buffer.append(subname); writer.println(buffer.toString()); buffer = new StringBuilder(); buffer.append(subname); subgroup++; } buffer.append(" "); buffer.append("(-,"); buffer.append(refLabel); buffer.append(",)"); } writer.println(buffer.toString()); } /** * * This method writes out a single system netgroup out to disk, * wrapping the netgroup if it gets too long. * * omg-s csd-s (csdsun1.arlut.utexas.edu,-,) (ns1.arlut.utexas.edu,-,) * */ private void writeSystemNetgroup(DBObject object, PrintWriter writer) { StringBuilder buffer = new StringBuilder(); String name; Vector<Invid> systems; Vector<Invid> memberNetgroups; int lengthlimit; int subgroup = 1; String subname; /* -- */ name = (String) object.getFieldValueLocal(systemNetgroupSchema.NETGROUPNAME); systems = (Vector<Invid>) object.getFieldValuesLocal(systemNetgroupSchema.SYSTEMS); memberNetgroups = (Vector<Invid>) object.getFieldValuesLocal(systemNetgroupSchema.MEMBERGROUPS); // NIS limits the length of a line to 1024 characters. // If the line looks like it'll go over, we'll truncate // it, put in an entry to link the netgroup with a // sub netgroup for continuation. // Thus, we want to save enough space to be able to put // the link information at the end. We reduce it by // a further 6 chars to leave space for the per-entry // syntax. // We could do this check during our buffer building loop, but by // doing it up front we guarantee that we're never going to exceed // the real limit during any single iteration of our netgroup line // construction without having to constantly be adding 6 to the // item length. lengthlimit = 900 - name.length() - 6; buffer.append(name); for (Invid ref: memberNetgroups) { String refLabel = getLabel(ref); if (buffer.length() + refLabel.length() > lengthlimit) { if (subgroup > 1) { subname = name + "-ext" + subgroup; } else { subname = name + "-ext"; } buffer.append(" "); buffer.append(subname); writer.println(buffer.toString()); buffer = new StringBuilder(); buffer.append(subname); subgroup++; } buffer.append(" "); buffer.append(refLabel); } for (Invid ref: systems) { String refLabel = getLabel(ref); refLabel += dnsdomain; if (buffer.length() + refLabel.length() > lengthlimit) { if (subgroup > 1) { subname = name + "-ext" + subgroup; } else { subname = name + "-ext"; } buffer.append(" "); buffer.append(subname); writer.println(buffer.toString()); buffer = new StringBuilder(); buffer.append(subname); subgroup++; } buffer.append(" "); buffer.append("("); buffer.append(refLabel); buffer.append(",-,)"); } writer.println(buffer.toString()); } /** * This method generates a simplified user netgroup file, which maps * user netgroups to user names without including any system * netgroups or sub-groups. effectively, this maps netgroup names * to the transitive closure of members. */ private boolean writeUserNetgroupFile() { PrintWriter writer = null; Hashtable<String, String> members = new Hashtable<String, String>(); /* -- */ try { writer = openOutFile(path + "netgroup.users", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeUserNetgroup(): couldn't open netgroup file: " + ex); } try { // first the user netgroups for (DBObject netgroup: getObjects(userNetgroupSchema.BASE)) { String name = (String) netgroup.getFieldValueLocal(userNetgroupSchema.NETGROUPNAME); members.clear(); unionizeMembers(netgroup, members); if (members.size() > 0) { writer.print(name); for (String member: members.values()) { writer.print(" "); writer.print(member); } writer.println(); } } } finally { writer.close(); } return true; } /** * Recursive helper method for writeUserNetgroupFile().. takes * a netgroup object and a hashtable, inserts all user members * in the netgroup object into the hash, then calls itself * on all member groups in the netgroup. */ private void unionizeMembers(DBObject netgroup, Hashtable<String, String> hash) { Vector<Invid> memberNetgroups = (Vector<Invid>) netgroup.getFieldValuesLocal(userNetgroupSchema.MEMBERGROUPS); Vector<Invid> users = (Vector<Invid>) netgroup.getFieldValuesLocal(userNetgroupSchema.USERS); for (Invid ref: users) { String member = getLabel(ref); hash.put(member, member); } for (Invid ref: memberNetgroups) { DBObject subNetgroup = getObject(ref); unionizeMembers(subNetgroup, hash); } } /** * * This method generates an auto.vol file, along with auto.home.* * files for all automounter records in the Ganymede database. * */ private boolean writeAutoMounterFiles() { PrintWriter autoFile = null; SharedStringBuffer buf = new SharedStringBuffer(); /* -- */ // first, write out the auto.vol file try { autoFile = openOutFile(path + "auto.vol", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeAutoMounterFiles(): couldn't open auto.vol: " + ex); } try { // find the volume definitions for (DBObject obj: getObjects(volumeSchema.BASE)) { buf.setLength(0); String volName = (String) obj.getFieldValueLocal(volumeSchema.LABEL); if (volName == null) { Ganymede.debug("Couldn't emit a volume definition.. null label"); continue; } buf.append(volName); // volume label buf.append("\t\t"); // mount options.. NeXTs like this. Ugh. String mountopts = (String) obj.getFieldValueLocal(volumeSchema.MOUNTOPTIONS); if (mountopts != null && !mountopts.equals("")) { buf.append(mountopts); buf.append(" "); } String sysName = getLabel((Invid) obj.getFieldValueLocal(volumeSchema.HOST)); if (sysName == null) { Ganymede.debug("Couldn't emit proper volume definition for " + volName + ", no system found"); continue; } buf.append(sysName); buf.append(dnsdomain); buf.append(":"); // mount path buf.append((String) obj.getFieldValueLocal(volumeSchema.PATH)); autoFile.println(buf.toString()); } } finally { autoFile.close(); } // second, write out all the auto.home.* files mapping user name // to volume name. We depend on the GASH build scripts to convert // these to the form that NIS will actually use.. we could and // possibly will change this to write out the combined // auto.home/auto.vol info rather than forcing it to be done // after-the-fact via perl. for (DBObject map: getObjects(mapSchema.BASE)) { String mapname = (String) map.getFieldValueLocal(mapSchema.MAPNAME); try { autoFile = openOutFile(path + mapname, "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeAutoMounterFiles(): couldn't open " + mapname + ": " + ex); } try { List<Invid> tempVect = (List<Invid>) map.getFieldValuesLocal(mapSchema.ENTRIES); for (Invid ref: tempVect) { DBObject obj = getObject(ref); // the entry is embedded in the user's record.. get // the user' id and label Invid userRef = obj.getParentInvid(); if (userRef.getType() != SchemaConstants.UserBase) { throw new RuntimeException("Schema and/or database error"); } buf.setLength(0); buf.append(getLabel(userRef)); // the user's name buf.append("\t"); // nfs volume for this entry ref = (Invid) obj.getFieldValueLocal(mapEntrySchema.VOLUME); if (ref == null || ref.getType() != volumeSchema.BASE) { Ganymede.debug("Error, can't find a volume entry for user " + getLabel(userRef) + " on automounter map " + mapname); continue; } buf.append(getLabel(ref)); autoFile.println(buf.toString()); } } finally { autoFile.close(); } } return true; } /** * * This method generates a file with the Mailman lists info. This method must be run during * builderPhase1 so that it has access to the getObjects() method * from our superclass. To be passed to Mailman server. * */ private boolean writeMailmanListsFile() { PrintWriter mailman_sync_file = null; /* -- */ try { mailman_sync_file = openOutFile(path + "ganymede_mailman_lists", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeMailmanListsFile(): couldn't open mailman_sync_file file: " + ex); } try { // and the mailman mail lists for (DBObject mailmanList: getObjects(MailmanListSchema.BASE)) { writeMailmanList(mailmanList, mailman_sync_file); } } finally { mailman_sync_file.close(); } return true; } /** * * This Method writes out a mailman list target line to the mailman lists file.<br/><br/> * * The mail list lines in this file look like the following:<br/><br/> * * <pre> * * listname\towneremail\tpassword * * </pre> * * Where listname is the name of the mailman list, owneremail is the * email of the owner, and password is the password for the mailing list. * * @param object An object from the Ganymede user object base * @param writer The destination for this alias line * */ private void writeMailmanList(DBObject object, PrintWriter writer) { result.setLength(0); String name = (String) object.getFieldValueLocal(MailmanListSchema.NAME); String ownerEmail = (String) object.getFieldValueLocal(MailmanListSchema.OWNEREMAIL); PasswordDBField passField = object.getPassField(MailmanListSchema.PASSWORD); String password = passField.getPlainText(); Invid serverInvid = (Invid) object.getFieldValueLocal(MailmanListSchema.SERVER); DBObject server = getObject(serverInvid); String hostname = getLabel((Invid) server.getFieldValueLocal(MailmanServerSchema.HOST)); result.append(hostname); result.append("\t"); result.append(name); result.append("\t"); result.append(ownerEmail); result.append("\t"); result.append(password); writer.println(result.toString()); } /** * <p>This method generates an aliases_info file. This method must * be run during builderPhase1 so that it has access to the * getObjects() method from our superclass.</p> */ private boolean writeAliasesFile() { PrintWriter aliases_info = null; /* -- */ try { aliases_info = openOutFile(path + "aliases_info", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeAliasesFile(): couldn't open aliases_info file: " + ex); } try { // our email aliases database is spread across three separate object // bases. for (DBObject user: getObjects(SchemaConstants.UserBase)) { writeUserAlias(user, aliases_info); } // now the mail lists for (DBObject group: getObjects(emailListSchema.BASE)) { writeGroupAlias(group, aliases_info); } // add in emailable account groups for (DBObject group: getObjects(groupSchema.BASE)) { writeAccountGroupAlias(group, aliases_info); } // add in emailable user netgroups for (DBObject group: getObjects(userNetgroupSchema.BASE)) { writeUserNetgroupAlias(group, aliases_info); } // and the external mail addresses for (DBObject external: getObjects(emailRedirectSchema.BASE)) { writeExternalAlias(external, aliases_info); } // as well as the aliases for the mailman lists for (DBObject mailman: getObjects(MailmanListSchema.BASE)) { writeMailmanListAlias(mailman, aliases_info); } // and the IRIS-derived mail lists for (DBObject irisList: getObjects(IRISListSchema.BASE)) { writeIRISListAlias(irisList, aliases_info); } } finally { aliases_info.close(); } return true; } /** * <p>This method writes out a mailman alias line to the aliases_info GASH source file.</p> * * <p>The mailman alias lines in this file look like the following:</p> * * <pre> * <xxx>test:test:test@hostname.arlut.utexas.edu * </pre> * * <p>Where the first occurrence of test is the name of the mailman * list, the second is the (comma separated) list of aliases for the * list, if any (there aren't any), and * test@hostname.arlut.utexas.edu is the delivery target for the * list.</p> * * @param object An object from the Ganymede user object base * @param writer The destination for this alias line */ private void writeMailmanListAlias(DBObject object, PrintWriter writer) { String name = (String) object.getFieldValueLocal(MailmanListSchema.NAME); Invid serverInvid = (Invid) object.getFieldValueLocal(MailmanListSchema.SERVER); DBObject server = getObject(serverInvid); String hostname = getLabel((Invid) server.getFieldValueLocal(MailmanServerSchema.HOST)); result.setLength(0); result.append("<xxx>"); result.append(name); result.append(":"); result.append(name); result.append(":"); result.append(name); result.append("@"); result.append(hostname); result.append(".arlut.utexas.edu"); writer.println(result.toString()); // Loop over aliases target. List<String> aliases = (List<String>) object.getFieldValuesLocal(MailmanListSchema.ALIASES); for (String aliasName: aliases) { if (aliasName != null) { result.setLength(0); result.append("<xxx>"); result.append(aliasName); result.append(":"); result.append(aliasName); result.append(":"); result.append(aliasName); result.append("@"); result.append(hostname); result.append(".arlut.utexas.edu"); writer.println(result.toString()); } } } /** * This method writes out a user alias line to the aliases_info GASH source file.<br/><br/> * * The user alias lines in this file look like the following:<br/><br/> * * <pre> * * broccol:jonabbey, abbey, broccol, rust-admin:broccol@arlut.utexas.edu, broccol@csdsun4.arlut.utexas.edu * * </pre> * * The first item in the second field (jonabbey, above) is the 'signature' * alias, which the GASH makefile configures sendmail to rewrite as the * From: line. * * @param object An object from the Ganymede user object base * @param writer The destination for this alias line */ private void writeUserAlias(DBObject object, PrintWriter writer) { String username; String signature; List<String> aliases; List<String> addresses; /* -- */ result.setLength(0); username = (String) object.getFieldValueLocal(userSchema.USERNAME); signature = (String) object.getFieldValueLocal(userSchema.SIGNATURE); aliases = (List<String>) object.getFieldValuesLocal(userSchema.ALIASES); addresses = (List<String>) object.getFieldValuesLocal(userSchema.EMAILTARGET); result.append(username); result.append(":"); result.append(signature); // we don't include the username in the list of aliases, // but the build/gash stuff requires that it be included // in aliases_info, so if we didn't write it out as the // signature, make it the second alias. The ordering // doesn't matter past the first, so this is ok. if (!signature.equals(username)) { result.append(", "); result.append(username); } for (String alias: aliases) { if (alias.equals(signature)) { continue; } result.append(", "); result.append(alias); } result.append(":"); for (int i = 0; i < addresses.size(); i++) { if (i > 0) { result.append(", "); } result.append(addresses.get(i)); } writer.println(result.toString()); } /** * * This method writes out a mail list alias line to the aliases_info GASH source file.<br/><br/> * * The mail list lines in this file look like the following:<br/><br/> * * <pre> * * :csd-news-dist-omg:alias, alias, alias:csd_staff, granger, iselt, lemma, jonabbey@eden.com * * </pre> * * Where the leading colon identifies to the GASH makefile that it is a group * line. * * NB: I've modified this method in 2008 to add the aliases field, * above. This is a modification of the classic GASH aliases_info * file, which did not support aliases for email lists. * * @param object An object from the Ganymede user object base * @param writer The destination for this alias line */ private void writeGroupAlias(DBObject object, PrintWriter writer) { String groupname; Vector<Invid> group_targets; Vector<String> group_aliases; Vector<String> external_targets; Invid memberInvid; String target; int lengthlimit_remaining; int subgroup = 1; String subname; /* -- */ result.setLength(0); groupname = (String) object.getFieldValueLocal(emailListSchema.LISTNAME); group_aliases = (Vector<String>) object.getFieldValuesLocal(emailListSchema.ALIASES); group_targets = (Vector<Invid>) object.getFieldValuesLocal(emailListSchema.MEMBERS); external_targets = (Vector<String>) object.getFieldValuesLocal(emailListSchema.EXTERNALTARGETS); result.append(":"); result.append(groupname); result.append(":"); for (int i = 0; i < group_aliases.size(); i++) { String alias = group_aliases.get(i); if (i > 0) { result.append(", "); } result.append(alias); } result.append(":"); // NIS forces us to a 1024 character limit per key and value, we // need to truncate and extend to match, here. We'll cut it down // to 900 to give ourselves some slack so we can write out our // chain link at the end of the line lengthlimit_remaining = 900 - result.length(); for (int i = 0; i < group_targets.size(); i++) { memberInvid = group_targets.get(i); if (isVeryDeadUser(memberInvid)) { continue; } if (i > 0) { result.append(", "); } target = getLabel(memberInvid); if (2 + target.length() > lengthlimit_remaining) { if (subgroup > 1) { subname = groupname + "-gext" + subgroup; } else { subname = groupname + "-gext"; } // point to the linked sublist, terminate this entry // line result.append(subname); result.append("\n"); // and initialize the next line, containing the linked // sublist result.append(":xxx:"); result.append(subname); result.append(":"); lengthlimit_remaining = 900 - subname.length() - 6; subgroup++; } result.append(target); lengthlimit_remaining = lengthlimit_remaining - (2 + target.length()); } for (int i = 0; i < external_targets.size(); i++) { if ((i > 0) || (group_targets != null && group_targets.size() > 0)) { result.append(", "); } target = external_targets.get(i); if (2 + target.length() > lengthlimit_remaining) { if (subgroup > 1) { subname = groupname + "-gext" + subgroup; } else { subname = groupname + "-gext"; } // point to the linked sublist, terminate this entry // line result.append(subname); result.append("\n"); // and initialize the next line, containing the linked // sublist result.append(":xxx:"); result.append(subname); result.append(":"); lengthlimit_remaining = 900 - subname.length() - 6; subgroup++; } result.append(target); lengthlimit_remaining = lengthlimit_remaining - (2 + target.length()); } writer.println(result.toString()); } /** * * This method writes out a mail list alias line to the aliases_info * GASH source file, as sourced from a gasharl account * group.<br/><br/> * * The mail list lines in this file look like the following:<br/><br/> * * <pre> * * :oms:csd-news-dist-omg:csd_staff, granger, iselt, lemma, jonabbey@eden.com * * </pre> * * Where the leading colon identifies to the GASH makefile that it is a group * line and 'oms' is the GASH ownership code. Ganymede won't try to emit * a GASH ownership code that could be used to load the aliases_info file * back into GASH. * * @param object An object from the Ganymede account group object base * @param writer The destination for this alias line * */ private void writeAccountGroupAlias(DBObject object, PrintWriter writer) { String groupname; Vector<Invid> group_targets; Invid userInvid; String target; int lengthlimit_remaining; int subgroup = 1; String subname; /* -- */ if (!object.isSet(groupSchema.EMAILOK)) { return; } result.setLength(0); groupname = (String) object.getFieldValueLocal(groupSchema.GROUPNAME); group_targets = (Vector<Invid>) object.getFieldValuesLocal(groupSchema.USERS); result.append(":xxx:"); result.append(groupname); result.append(":"); // NIS forces us to a 1024 character limit per key and value, we // need to truncate and extend to match, here. We'll cut it down // to 900 to give ourselves some slack so we can write out our // chain link at the end of the line lengthlimit_remaining = 900 - result.length(); for (int i = 0; i < group_targets.size(); i++) { userInvid = group_targets.get(i); if (isVeryDeadUser(userInvid)) { continue; } if (i > 0) { result.append(", "); } target = getLabel(userInvid); if (2 + target.length() > lengthlimit_remaining) { if (subgroup > 1) { subname = groupname + "-gext" + subgroup; } else { subname = groupname + "-gext"; } // point to the linked sublist, terminate this entry // line result.append(subname); result.append("\n"); // and initialize the next line, containing the linked // sublist result.append(":xxx:"); result.append(subname); result.append(":"); lengthlimit_remaining = 900 - subname.length() - 6; subgroup++; } result.append(target); lengthlimit_remaining = lengthlimit_remaining - (2 + target.length()); } writer.println(result.toString()); } /** * <p>This method writes out a mail list alias line to the aliases_info * GASH source file, as sourced from a gasharl user netgroup object.</p> * * <p>The mail list lines in this file look like the following:</p> * * <pre> * * :oms:csd-news-dist-omg:csd_staff, granger, iselt, lemma, jonabbey@eden.com * * </pre> * * <p>Where the leading colon identifies to the GASH makefile that it is a group * line and 'oms' is the GASH ownership code. Ganymede won't try to emit * a GASH ownership code that could be used to load the aliases_info file * back into GASH.</p> * * @param object An object from the Ganymede User Netgroup object base * @param writer The destination for this alias line */ private void writeUserNetgroupAlias(DBObject object, PrintWriter writer) { String groupname; Vector<Invid> group_targets; Vector<Invid> sub_netgroups; Vector<String> targets; String target; int lengthlimit_remaining; int subgroup = 1; String subname; /* -- */ if (!object.isSet(userNetgroupSchema.EMAILOK)) { return; } result.setLength(0); groupname = (String) object.getFieldValueLocal(userNetgroupSchema.NETGROUPNAME); group_targets = (Vector<Invid>) object.getFieldValuesLocal(userNetgroupSchema.USERS); sub_netgroups = (Vector<Invid>) object.getFieldValuesLocal(userNetgroupSchema.MEMBERGROUPS); targets = new Vector<String>(); for (Invid targetInvid: group_targets) { if (isVeryDeadUser(targetInvid)) { continue; } targets.add(getLabel(targetInvid)); } for (Invid subnetInvid: sub_netgroups) { DBObject subnetgroup = getObject(subnetInvid); if (subnetgroup.isSet(userNetgroupSchema.EMAILOK)) { targets.add(subnetgroup.getLabel()); } } result.append(":xxx:"); result.append(groupname); result.append(":"); // NIS forces us to a 1024 character limit per key and value, we // need to truncate and extend to match, here. We'll cut it down // to 900 to give ourselves some slack so we can write out our // chain link at the end of the line lengthlimit_remaining = 900 - result.length(); for (int i = 0; i < targets.size(); i++) { if (i > 0) { result.append(", "); } target = targets.get(i); if (2 + target.length() > lengthlimit_remaining) { if (subgroup > 1) { subname = groupname + "-gext" + subgroup; } else { subname = groupname + "-gext"; } // point to the linked sublist, terminate this entry // line result.append(subname); result.append("\n"); // and initialize the next line, containing the linked // sublist result.append(":xxx:"); result.append(subname); result.append(":"); lengthlimit_remaining = 900 - subname.length() - 6; subgroup++; } result.append(target); lengthlimit_remaining = lengthlimit_remaining - (2 + target.length()); } writer.println(result.toString()); } /** * * This method writes out a mail list alias line to the aliases_info * GASH source file, as sourced from an emailable user * netgroup.<br/><br/> * * The mail list lines in this file look like the following:<br/><br/> * * <pre> * * <omj>abuse:abuse, postmaster:postmaster@ns1.arlut.utexas.edu * * </pre> * * Where the leading < identifies to GASH and the GASH makefile that * it is an external user line and 'omj' is the GASH ownership code. * Ganymede won't try to emit a GASH ownership code that could be * used to load the aliases_info file back into GASH. * * @param object An object from the Ganymede user object base * @param writer The destination for this alias line * */ private void writeExternalAlias(DBObject object, PrintWriter writer) { result.setLength(0); String name = (String) object.getFieldValueLocal(emailRedirectSchema.NAME); Vector<String> targets = (Vector<String>) object.getFieldValuesLocal(emailRedirectSchema.TARGETS); Vector<String> aliases = (Vector<String>) object.getFieldValuesLocal(emailRedirectSchema.ALIASES); result.append("<xxx>"); result.append(name); result.append(":"); result.append(name); // the name is one of the aliases for (String alias: aliases) { result.append(", "); result.append(alias); } result.append(":"); // targets shouldn't ever be null, but i'm tired of having // NullPointerExceptions pop up then having to recompile to // fix. for (int i = 0; i < targets.size(); i++) { if (i > 0) { result.append(", "); } result.append(targets.get(i)); } writer.println(result.toString()); } /** * <p>This method writes out an IRIS List to the aliases_info GASH * source file.</p> * * <p>The mail list lines in this file look like the following:</p> * * <pre> * :csd-news-dist-omg:alias, alias, alias:csd_staff, granger, iselt, lemma, jonabbey@eden.com * </pre> * * <p>Where the leading colon identifies to the GASH makefile that * it is a group line.</p> * * <p>NB: I've modified this method in 2008 to add the aliases * field, above. This is a modification of the classic GASH * aliases_info file, which did not support aliases for email * lists.</p> * * @param object An object from the Ganymede IRISList object base * @param writer The destination for this alias line */ private void writeIRISListAlias(DBObject object, PrintWriter writer) { String listname; Vector<Invid> list_members; Vector<String> list_aliases; Invid memberInvid; String target; int lengthlimit_remaining; int subgroup = 1; String subname; /* -- */ result.setLength(0); listname = (String) object.getFieldValueLocal(IRISListSchema.LISTNAME); list_aliases = (Vector<String>) object.getFieldValuesLocal(IRISListSchema.ALIASES); list_members = (Vector<Invid>) object.getFieldValuesLocal(IRISListSchema.MEMBERS); result.append(":"); result.append(listname); result.append(":"); for (int i = 0; i < list_aliases.size(); i++) { if (i > 0) { result.append(", "); } result.append(list_aliases.get(i)); } result.append(":"); // NIS forces us to a 1024 character limit per key and value, we // need to truncate and extend to match, here. We'll cut it down // to 900 to give ourselves some slack so we can write out our // chain link at the end of the line lengthlimit_remaining = 900 - result.length(); for (int i = 0; i < list_members.size(); i++) { memberInvid = list_members.get(i); if (isVeryDeadUser(memberInvid)) { continue; } if (i > 0) { result.append(", "); } target = getLabel(memberInvid); if (2 + target.length() > lengthlimit_remaining) { if (subgroup > 1) { subname = listname + "-gext" + subgroup; } else { subname = listname + "-gext"; } // point to the linked sublist, terminate this entry // line result.append(subname); result.append("\n"); // and initialize the next line, containing the linked // sublist result.append(":xxx:"); result.append(subname); result.append(":"); lengthlimit_remaining = 900 - subname.length() - 6; subgroup++; } result.append(target); lengthlimit_remaining = lengthlimit_remaining - (2 + target.length()); } writer.println(result.toString()); } /** * This method generates a postfix-compatible aliases (name * undetermined so far) file. This method must be run during * builderPhase1 so that it has access to the getObjects() * method from our superclass. * * AHEM!!! where you see "write Hash*" below... what that * really means is: * write the flat file that postfix (through postalias or * postmap) will turn into a hash file. * the file has to get flung over via ssh and then * something has to run postmap/postalias on the file. * * jgs */ private boolean writeHashAliasesFile() throws IOException { boolean success = false; PrintWriter pfgenerics = openOutFile(path + "pfgenerics", "gasharl"); try { PrintWriter pfmalias = openOutFile(path + "pfmalias", "gasharl"); try { PrintWriter pftransport = openOutFile(path + "pftransport", "gasharl"); try { PrintWriter pfknownu = openOutFile(path + "pfknown_users", "gasharl"); try { PrintWriter pfcanonical = openOutFile(path + "pfrecipient_canonical", "gasharl"); try { writeHashTransport(pftransport); writeHashKnownuser(pfknownu, pfcanonical); for (DBObject user: getObjects(SchemaConstants.UserBase)) { writeHashGenerics(user, pfgenerics); writeHashUserAlias(user, pfmalias); } // mail lists for (DBObject group: getObjects(emailListSchema.BASE)) { writeHashGroupAlias(group, pfmalias); } // emailable account groups for (DBObject group: getObjects(groupSchema.BASE)) { writeHashAccountGroupAlias(group, pfmalias); } // emailable user netgroups for (DBObject group: getObjects(userNetgroupSchema.BASE)) { writeHashUserNetgroupAlias(group, pfmalias); } // external mail addresses for (DBObject external: getObjects(emailRedirectSchema.BASE)) { writeHashExternalAlias(external, pfmalias); } success = true; } finally { pfcanonical.close(); } } finally { pfknownu.close(); } } finally { pftransport.close(); } } finally { pfmalias.close(); } } finally { pfgenerics.close(); } return success; } /** * This method writes out a user alias line to the pfmalias file.<br/><br/> * * The user alias lines in this file look like the following:<br/><br/> * * <pre> * * aliasthing: real1, real2, real3 * * </pre> * * Where aliasthing is the name of an alias, and * real<n> are actual email addresses to deliver to (but, as you know, * those things can be aliases themselves). * * @param object An object from the Ganymede user object base * @param writer The destination for this alias line * * AHEM!!! where you see "write HashUserAlias" below... * what that really means is: * write the flat file that postfix (through postalias or * postmap) will turn into a hash file. * the file has to get flung over via ssh and then * something has to run postmap/postalias on the file. * * jgs */ private void writeHashUserAlias(DBObject object, PrintWriter writer) { String username = (String) object.getFieldValueLocal(userSchema.USERNAME); String signature = (String) object.getFieldValueLocal(userSchema.SIGNATURE); Vector<String> aliases = (Vector<String>) object.getFieldValuesLocal(userSchema.ALIASES); Vector<String> addresses = (Vector<String>) object.getFieldValuesLocal(userSchema.EMAILTARGET); if (empty(addresses)) { return; } // write out the delivery targets of the signature alias as the // first entry writer.print(signature); writer.print(": "); for (int i = 0; i < addresses.size(); i++) { if (i > 0) { writer.print(", "); } writer.print(fixup(addresses.get(i))); } writer.println(); // in case the username wasn't the signature alias aliases.add(username); // for each alias, write out a delivery targets to the signature // alias for (String alias: aliases) { if (alias.equals(signature)) { continue; } writer.print(alias.toLowerCase()); writer.print(": "); writer.println(signature.toLowerCase()); } } /** * This method writes out a user alias line to the pfmalias file.<br/><br/> * * AHEM!!! where you see "write Hash Generics" below... * what that really means is: * write the flat file that postfix (through postalias or * postmap) will turn into a hash file. * the file has to get flung over via ssh and then * something has to run postmap/postalias on the file. * * jgs */ private void writeHashGenerics(DBObject object, PrintWriter writer) { String username = (String) object.getFieldValueLocal(userSchema.USERNAME); String signature = (String) object.getFieldValueLocal(userSchema.SIGNATURE); Vector<String> aliases = (Vector<String>) object.getFieldValuesLocal(userSchema.ALIASES); Vector<String> addresses = (Vector<String>) object.getFieldValuesLocal(userSchema.EMAILTARGET); result.setLength(0); // we should never have @ chars in signature aliases, but if we // do, trim if (signature.indexOf('@') != -1) { try { throw new RuntimeException("Warning, @ in signature alias!"); } catch (RuntimeException ex) { Ganymede.logError(ex); } signature = signature.substring(0, signature.indexOf('@')); } result.append(signature); result.append(": "); result.append(signature); result.append("@arlut.utexas.edu."); writer.println(result.toString().toLowerCase()); for (String alias: aliases) { if (alias.equals(signature)) { result.setLength(0); result.append(username); result.append(": "); result.append(signature); result.append("@arlut.utexas.edu."); writer.println(result.toString().toLowerCase()); } else { result.setLength(0); result.append(alias); result.append(": "); result.append(signature); result.append("@arlut.utexas.edu."); writer.println(result.toString().toLowerCase()); } } } /** * This method writes out a mail list alias line to the pfmalias file.<br/><br/> * * The mail list lines in this file look like the following:<br/><br/> * * <pre> * * aliasthing: real1, real2, real3 * * </pre> * * Where aliasthing is the name of an alias, and * real<n> are actual email addresses to deliver to (but, as you know, * those things can be aliases themselves). * * @param object An object from the Ganymede user object base * @param writer The destination for this alias line * * AHEM!!! where you see "write HashGroupAlias" below... * what that really means is: * write the flat file that postfix (through postalias or * postmap) will turn into a hash file. * the file has to get flung over via ssh and then * something has to run postmap/postalias on the file. * * jgs */ private void writeHashGroupAlias(DBObject object, PrintWriter writer) { String groupname = (String) object.getFieldValueLocal(emailListSchema.LISTNAME); Vector<String> group_aliases = (Vector<String>) object.getFieldValuesLocal(emailListSchema.ALIASES); Vector<Invid> group_targets = (Vector<Invid>) object.getFieldValuesLocal(emailListSchema.MEMBERS); Vector<String> external_targets = (Vector<String>) object.getFieldValuesLocal(emailListSchema.EXTERNALTARGETS); boolean didSomething = false; // if the idea is to write each group out as the full list, then, // okay, i guess we can do that. actually, that is a chore, // isn't it? so let's spit out each alias and the groupname, // then just do the groupname once. for (String alias: group_aliases) { result.setLength(0); result.append(alias); result.append(": "); result.append(groupname); writer.println(result.toString().toLowerCase()); } // whoops. need to know that we have something to spit out. if (!empty(group_targets) || !empty(external_targets)) { result.setLength(0); result.append(groupname); result.append(": "); for (int i = 0; i < group_targets.size(); i++) { Invid memberInvid = group_targets.get(i); if (isVeryDeadUser(memberInvid)) { continue; } if (i > 0) { result.append(", "); } result.append(getLabel(memberInvid)); didSomething = true; } for (int i = 0; i < external_targets.size(); i++) { if (i > 0 || !empty(group_targets)) { result.append(", "); } result.append(external_targets.get(i)); didSomething = true; } if (didSomething) { writer.println(result.toString().toLowerCase()); } } } /** * This method writes out a mail list alias line to the pfmalias * file, as sourced from a gasharl account * group.<br/><br/> * * <pre> * * aliasthing: real1, real2, real3 * * </pre> * * Where aliasthing is the name of an alias, and * real<n> are actual email addresses to deliver to (but, as you know, * those things can be aliases themselves). * * @param object An object from the Ganymede user object base * @param writer The destination for this alias line * * AHEM!!! where you see "write HashAccountGroupAlias" below... * what that really means is: * write the flat file that postfix (through postalias or * postmap) will turn into a hash file. * the file has to get flung over via ssh and then * something has to run postmap/postalias on the file. * * jgs */ private void writeHashAccountGroupAlias(DBObject object, PrintWriter writer) { if (!object.isSet(groupSchema.EMAILOK)) { return; } String groupname = (String) object.getFieldValueLocal(groupSchema.GROUPNAME); Vector<Invid> group_targets = (Vector<Invid>) object.getFieldValuesLocal(groupSchema.USERS); if (!empty(group_targets)) { result.setLength(0); result.append(groupname); result.append(": "); for (int i = 0; i < group_targets.size(); i++) { Invid userInvid = group_targets.get(i); if (isVeryDeadUser(userInvid)) { continue; } if (i > 0) { result.append(", "); } result.append(getLabel(userInvid)); } writer.println(result.toString().toLowerCase()); } } /** * * This method writes out a mail list alias line to the pfmalias * file, as sourced from a gasharl user netgroup object.<br/><br/> * * The mail list lines in this file look like the following:<br/><br/> * <pre> * * aliasthing: real1, real2, real3 * * </pre> * * Where aliasthing is the name of an alias, and * real<n> are actual email addresses to deliver to (but, as you know, * those things can be aliases themselves). * * @param object An object from the Ganymede user object base * @param writer The destination for this alias line * * AHEM!!! where you see "write HashUserNetgroupAlias" below... * what that really means is: * write the flat file that postfix (through postalias or * postmap) will turn into a hash file. * the file has to get flung over via ssh and then * something has to run postmap/postalias on the file. * * jgs */ private void writeHashUserNetgroupAlias(DBObject object, PrintWriter writer) { if (!object.isSet(userNetgroupSchema.EMAILOK)) { return; } String groupname = (String) object.getFieldValueLocal(userNetgroupSchema.NETGROUPNAME); Vector<Invid> group_targets = (Vector<Invid>) object.getFieldValuesLocal(userNetgroupSchema.USERS); Vector<Invid> sub_netgroups = (Vector<Invid>) object.getFieldValuesLocal(userNetgroupSchema.MEMBERGROUPS); Vector<String> targets = new Vector<String>(); for (Invid targetInvid: group_targets) { if (isVeryDeadUser(targetInvid)) { continue; } targets.add(getLabel(targetInvid)); } for (Invid subNetGroup: sub_netgroups) { DBObject subnetgroup = getObject(subNetGroup); if (subnetgroup.isSet(userNetgroupSchema.EMAILOK)) { targets.add(subnetgroup.getLabel()); } } if (!empty(targets)) { result.setLength(0); result.append(groupname); result.append(": "); for (int i = 0; i < targets.size(); i++) { if (i > 0) { result.append(", "); } result.append(targets.get(i)); } writer.println(result.toString().toLowerCase()); } } /** * This method writes out a mail list alias line to the pfmalias * file, as sourced from an emailable user * netgroup.<br/><br/> * * The mail list lines in this file look like the following:<br/><br/> * <pre> * * aliasthing: real1, real2, real3 * * </pre> * * Where aliasthing is the name of an alias, and * real<n> are actual email addresses to deliver to (but, as you know, * those things can be aliases themselves). * * @param object An object from the Ganymede user object base * @param writer The destination for this alias line * * AHEM!!! where you see "write HashExternalAlias" below... * what that really means is: * write the flat file that postfix (through postalias or * postmap) will turn into a hash file. * the file has to get flung over via ssh and then * something has to run postmap/postalias on the file. * * jgs */ private void writeHashExternalAlias(DBObject object, PrintWriter writer) { String name = (String) object.getFieldValueLocal(emailRedirectSchema.NAME); Vector<String> targets = (Vector<String>) object.getFieldValuesLocal(emailRedirectSchema.TARGETS); Vector<String> aliases = (Vector<String>) object.getFieldValuesLocal(emailRedirectSchema.ALIASES); for (String alias: aliases) { if (!alias.equals(name)) { result.setLength(0); result.append(alias); result.append(": "); result.append(name); writer.println(result.toString().toLowerCase()); } } // if targets is null, we mustn't put out a stub line. if (!empty(targets)) { result.setLength(0); result.append(name); result.append(": "); for (int i = 0; i < targets.size(); i++) { if (i > 0) { result.append(", "); } result.append(fixup(targets.get(i))); } writer.println(result.toString().toLowerCase()); } } /** * This method writes out a transport file for use by postfix.<br/><br/> * * The lines look like this:<br/><br/> * * <pre> * * thingy.arlut.utexas.edu smtp:[thingy.arlut.utexas.edu] * * </pre> * * Where thingy is a mail server that does local delivery. * * * AHEM!!! where you see "write HashSomething" below... * what that really means is: * write the flat file that postfix (through postalias or * postmap) will turn into a hash file. * the file has to get flung over via ssh and then * something has to run postmap/postalias on the file. * * jgs */ private void writeHashTransport(PrintWriter writer) { Set<String> set = new CaseInsensitiveSet(); // some things in here will NOT be found by the loop following // this one. so you do have to do this. for (DBObject loluser: getObjects(SchemaConstants.UserBase)) { Vector<String> addresses = (Vector<String>) loluser.getFieldValuesLocal(userSchema.EMAILTARGET); for (String address: addresses) { String host = getEmailHost(fixup(address)); if (host.endsWith("arlut.utexas.edu")) { set.add(host); } } } for (DBObject external: getObjects(emailRedirectSchema.BASE)) { Vector<String> targets = (Vector<String>) external.getFieldValuesLocal(emailRedirectSchema.TARGETS); for (String target: targets) { String host = getEmailHost(fixup(target)); if (host.endsWith("arlut.utexas.edu")) { set.add(host); } } } // XXX // // No emailListSchema checking here? // // XXX for (String host: set) { writer.print(host); writer.print("\tsmtp:["); writer.print(host); writer.println("]"); } } /** * This method writes out a list of the users allowed to use mail; * the list is for use by postfix.<br/><br/> * * The lines look like this:<br/><br/> * * <pre> * * user OK * * </pre> * * jgs */ private void writeHashKnownuser(PrintWriter writer, PrintWriter writer2) { Set<String> set = new CaseInsensitiveSet(); for (DBObject user: getObjects(SchemaConstants.UserBase)) { String username = (String) user.getFieldValueLocal(userSchema.USERNAME); set.add(username); Vector<String> aliases = (Vector<String>) user.getFieldValuesLocal(userSchema.ALIASES); for (String alias: aliases) { set.add(alias); } Vector<String> targets = (Vector<String>) user.getFieldValuesLocal(userSchema.EMAILTARGET); for (String target: targets) { String account = getEmailAccount(target); String host = getEmailHost(target); if (host.endsWith("arlut.utexas.edu")) { // account could be the user's name or any of his // aliases, above, but set.add() will check that // for us efficiently // // we could check for whether account is equals to // "no_longer_employed" here.. set.add(account); } } } for (DBObject group: getObjects(emailListSchema.BASE)) { String groupname = (String) group.getFieldValueLocal(emailListSchema.LISTNAME); Vector<String> group_aliases = (Vector<String>) group.getFieldValuesLocal(emailListSchema.ALIASES); Vector<Invid> group_targets = (Vector<Invid>) group.getFieldValuesLocal(emailListSchema.MEMBERS); Vector external_targets = group.getFieldValuesLocal(emailListSchema.EXTERNALTARGETS); if (!empty(group_aliases) || !empty(group_targets) || !empty(external_targets)) { set.add(groupname); } for (String alias: group_aliases) { set.add(alias); } for (Invid memberInvid: group_targets) { if (!isVeryDeadUser(memberInvid)) { set.add(getLabel(memberInvid)); } } } for (DBObject group: getObjects(groupSchema.BASE)) { if (group.isSet(groupSchema.EMAILOK)) { String groupname = (String) group.getFieldValueLocal(groupSchema.GROUPNAME); set.add(groupname); } } for (DBObject group: getObjects(userNetgroupSchema.BASE)) { if (group.isSet(userNetgroupSchema.EMAILOK)) { String groupname = (String) group.getFieldValueLocal(userNetgroupSchema.NETGROUPNAME); set.add(groupname); } } for (DBObject external: getObjects(emailRedirectSchema.BASE)) { String name = (String) external.getFieldValueLocal(emailRedirectSchema.NAME); set.add(name); Vector<String> aliases = (Vector<String>) external.getFieldValuesLocal(emailRedirectSchema.ALIASES); for (String alias: aliases) { if (!alias.equals(name)) { set.add(alias); } } Vector<String> targets = (Vector<String>) external.getFieldValuesLocal(emailRedirectSchema.TARGETS); for (String target: targets) { String host = getEmailHost(target); String user = getEmailAccount(target); if (host.endsWith("arlut.utexas.edu")) { set.add(user); } } } for (String account: set) { // needs to end in @arlut.utexas.edu on postfix side. // jgs 17 nov 2010 writer.print(account); writer.println("@arlut.utexas.edu OK"); } // // once more, w/ feeling: // /^user@.*\.arlut\.utexas\.edu$/ user@arlut.utexas.edu // for (String account: set) { writer2.print("/^"); writer2.print(account); writer2.print("@.*\\.arlut\\.utexas\\.edu$/"); writer2.print("\t"); writer2.print(account); writer2.println("@arlut.utexas.edu"); } return; } /** * Cleans up / fixes up address for our use in generating Postfix * email input files. * * jon/jgs */ private String fixup(Object in) { // if the target has @arlut.utexas.edu // change it to @arlmail.arlut.utexas.edu. sigh. return in.toString().replace("@arlut.utexas.edu", "@arlmail.arlut.utexas.edu"); } /** * Returns a lower case String containing everything after the @ * sign in address, or the empty string if no @ was found in the * address parameter. */ private String getEmailHost(String address) { if (address.indexOf('@') != -1) { return address.substring(address.indexOf('@') + 1).toLowerCase(); } return ""; } /** * Returns the (lower cased) account/alias name in front of the @ * sign in address, or throws an InvalidArgumentException if the * address is null. */ private String getEmailAccount(String address) { if (address == null || "".equals(address)) { throw new IllegalArgumentException(); } if (address.indexOf('@') == -1) { return address.toLowerCase(); } return address.substring(0, address.indexOf('@')).toLowerCase(); } /** * Convenience method, returns true if in is null or empty. */ private boolean empty(Vector in) { return in == null || in.size() == 0; } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ /** * This method checks to see if an invid is a user, and if that user * was inactivated (or at least last edited by) anyone other than * the password aging task. * * If we find such a user, we will not include him in email lists of * any kind, lest we generate bounce messages to people sending to * those lists. If anyone other than the password aging task * inactivated a user, we're going to assume that the user should * not receive any more mail that was sent to a Ganymede mail list * (of any variety), rather than directly to him. */ private boolean isVeryDeadUser(Invid invid) { if (invid.getType() != 3) { return false; } DBObject userObject = getObject(invid); if (!userObject.isInactivated()) { return false; } Vector<String> emailTargets = (Vector<String>) userObject.getFieldValuesLocal(userSchema.EMAILTARGET); if (empty(emailTargets)) { // huh, no targets? that's pretty dead! return true; } if (emailTargets.size() > 1) { // multiple addresses? someone's doing something fancy on // purpose, let it pass return false; } String target = emailTargets.get(0); if (target.indexOf('@') == -1) { // we're pointing to another user, presumably. let it pass return false; } if (target.toLowerCase().endsWith("redirect")) { return true; // no sending to bounce addresses, thanks } String modifierName = (String) userObject.getFieldValueLocal(SchemaConstants.ModifierField); if (modifierName.equals("[" + PasswordAgingTask.name + "]")) { return false; } if (target.startsWith((userObject.getLabel() + "@")) && target.endsWith("arlut.utexas.edu")) { return true; // we're mailing to the user himself, // and they weren't password // expired, skip mailing to this user } return false; } /** * <p>Samba version 1:</p> * * <p>broccol:12003:612EE67D1EFC2FB60B42BCD4578197DF:27A4F1E1E377CAD237C95B6146457F86:Jonathan Abbey,S321 CSD,3199,6803522:/home/broccol:/bin/tcsh</p> * */ private boolean writeSambafileVersion1() { PrintWriter sambaFile = null; try { sambaFile = openOutFile(path + "smb.passwd", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeSambaFileVersion1(): couldn't open smb.passwd file: " + ex); return false; } try { for (DBObject user: getObjects(SchemaConstants.UserBase)) { if (user.isInactivated()) { // we just leave inactivated users out of a Version 1 // Samba password file continue; } String username = (String) user.getFieldValueLocal(userSchema.USERNAME); PasswordDBField passField = user.getPassField(userSchema.PASSWORD); if (passField == null) { continue; } String hash1 = passField.getLANMANCryptText(); if (hash1 == null || hash1.equals("")) { continue; } String hash2 = passField.getNTUNICODECryptText(); if (hash2 == null || hash2.equals("")) { continue; } Integer uid = (Integer) user.getFieldValueLocal(userSchema.UID); if (uid == null) { continue; } String fullname = cleanString((String) user.getFieldValueLocal(userSchema.FULLNAME)); String room = cleanString((String) user.getFieldValueLocal(userSchema.ROOM)); String div = cleanString((String) user.getFieldValueLocal(userSchema.DIVISION)); String workphone = cleanString((String) user.getFieldValueLocal(userSchema.OFFICEPHONE)); String homephone = cleanString((String) user.getFieldValueLocal(userSchema.HOMEPHONE)); String homedir = cleanString((String) user.getFieldValueLocal(userSchema.HOMEDIR)); String shell = cleanString((String) user.getFieldValueLocal(userSchema.LOGINSHELL)); String composite = cleanString(fullname + "," + room + " " + div + "," + workphone + "," + homephone); sambaFile.println(username + ":" + uid.intValue() + ":" + hash1 + ":" + hash2 + ":" + composite + ":" + homedir + ":" + shell); } } finally { sambaFile.close(); } return true; } /** * <p>Samba version 2:</p> * * <p>broccol:12003:612EE67D1EFC2FB60B42BCD4578197DF:27A4F1E1E377CAD237C95B6146457F86:[U ]:LCT-375412BE:</p> */ private boolean writeSambafileVersion2() { String hash1 = null; String hash2 = null; PrintWriter sambaFile = null; try { sambaFile = openOutFile(path + "smb.passwd2", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeSambaFileVersion2(): couldn't open smb.passwd2 file: " + ex); return false; } try { for (DBObject user: getObjects(SchemaConstants.UserBase)) { boolean inactivated = user.isInactivated(); String username = (String) user.getFieldValueLocal(userSchema.USERNAME); if (username == null || username.equals("")) { continue; } PasswordDBField passField = user.getPassField(userSchema.PASSWORD); if (passField == null) { inactivated = true; } if (!inactivated) { hash1 = passField.getLANMANCryptText(); if (hash1 == null || hash1.equals("")) { inactivated = true; } else { hash2 = passField.getNTUNICODECryptText(); if (hash2 == null || hash2.equals("")) { inactivated = true; } } } if (inactivated) { hash1 = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; hash2 = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; } Integer uid = (Integer) user.getFieldValueLocal(userSchema.UID); if (uid == null) { continue; } // Samba 2.0 uses a flag string with 11 spaces and/or flag chars // between a pair of brackets. String flagString; if (inactivated) { flagString = "[UD ]"; } else { flagString = "[U ]"; } // sanity checking if (hash1 == null || hash1.length() != 32) { throw new RuntimeException("bad LANMAN hash string: " + hash1); } if (hash2 == null || hash2.length() != 32) { throw new RuntimeException("bad LANMAN hash string: " + hash1); } if (flagString.length() != 13) { throw new RuntimeException("bad flag string"); } String dateString = "LCT-" + dateToSMBHex(System.currentTimeMillis()); sambaFile.println(username + ":" + uid.intValue() + ":" + hash1 + ":" + hash2 + ":" + flagString + ":" + dateString); } } finally { sambaFile.close(); } return true; } /** * <p>Samba knows how to handle an 8 byte hex encoded date from the * version 2 smb.passwd file. This method takes a standard long * Java timecode and generates an 8 byte hex string which holds the * number of seconds since epoch.</p> * * <p><b><blink>Note that this will overflow in the year 2038.</blink></b></p> */ private String dateToSMBHex(long timecode) { timecode = timecode / 1000; if (timecode < 0) { throw new IllegalArgumentException("Time code is out of range from before the epoch"); } if (timecode > java.lang.Integer.MAX_VALUE) { throw new IllegalArgumentException("Time code has overflowed"); } StringBuilder timeString = new StringBuilder(); timeString.append(java.lang.Integer.toHexString((int) timecode)); // make sure we pad it out to 8 characters if it is less if (timeString.length() < 8) { for (int i = timeString.length(); i < 8; i++) { timeString.insert(0, "0"); } } return timeString.toString().toUpperCase(); } /** * <p>This method writes out a userSync.txt file which includes the * username, password, and invid. This is used to allow generic * username/password synchronization for external SQL applications.</p> * * <p>We include the invid to provide a guaranteed unique identifier, * which will remain invariant even in the face of user rename.</p> * * <p>The userSync.txt file contains lines of the following format:</p> * * <PRE> * username|cryptText|invid|emailAddress|fullName * </PRE> * * <p>i.e.,</p> * * <PRE> * broccol|MMn1MiLY1ZbZ.|3:627|jonabbey@arlut.utexas.edu|Jonathan Abbey * </PRE> * * <p>Note that if the user is inactivated or the user's password is undefined, * the cryptText field in the userSync.txt file will be empty. This * should be construed as having the user be unusable, *not* having the user * be usable with no password.</p> * */ private boolean writeUserSyncFile() { PrintWriter out = null; /* -- */ try { out = openOutFile(path + "userSync.txt", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.builderPhase1(): couldn't open userSync.txt file: " + ex); return false; } try { for (DBObject user: getObjects(SchemaConstants.UserBase)) { String username = user.getLabel(); Invid invid = user.getInvid(); String signature = (String) user.getFieldValueLocal(userSchema.SIGNATURE); String fullname = (String) user.getFieldValueLocal(userSchema.FULLNAME); String cryptText = null; if (!user.isInactivated()) { PasswordDBField passField = user.getPassField(SchemaConstants.UserPassword); if (passField != null) { cryptText = passField.getMD5CryptText(); } } // ok, we've got a user with valid cryptText password // info. Write it. out.print(username); out.print("|"); if (cryptText != null) { out.print(cryptText); } out.print("|"); out.print(invid); out.print("|"); out.print(signature); out.print("@arlut.utexas.edu"); out.print("|"); out.println(fullname); } } finally { out.close(); } return true; } /** * <p>This method writes out password and group files compatible * with the Apache web server. The password file is formatted * according to the standard .htpasswd file format, as follows:</p> * * <PRE> * user1:3vWsXVZDX5E7E * user2:DX5E7E3vWsXVZ * </PRE> * * <p>The group file is likewise formatted for use with Apache, as follows:</p> * * <PRE> * group1: user1 user2 user3 * group2: user9 user2 user1 * </PRE> * * <p>All users and all groups and user netgroups will be written to the * files.</p> * */ private boolean writeHTTPfiles() { PrintWriter webPassword = null; PrintWriter webGroups = null; try { webPassword = openOutFile(path + "httpd.pass", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeHTTPfiles(): couldn't open httpd.pass file: " + ex); return false; } try { for (DBObject user: getObjects(SchemaConstants.UserBase)) { if (user.isInactivated()) { continue; } PasswordDBField passField = user.getPassField(SchemaConstants.UserPassword); if (passField == null) { continue; } String password = passField.getApacheMD5CryptText(); if (password == null) { continue; } // ok, we've got a user with valid Apache MD5Crypt password // info. Write it. webPassword.print(user.getLabel()); webPassword.print(":"); webPassword.println(password); } } finally { webPassword.close(); } try { webGroups = openOutFile(path + "httpd.groups", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeHTTPfiles(): couldn't open httpd.groups file: " + ex); return false; } try { // first we write out UNIX account groups for (DBObject group: getObjects(groupSchema.BASE)) { if (group.isInactivated()) { continue; } InvidDBField usersField = group.getInvidField(groupSchema.USERS); if (usersField == null) { continue; } String usersList = usersField.getValueString(); if (usersList == null || usersList.equals("")) { continue; } webGroups.print(group.getLabel()); webGroups.print(": "); // InvidDBField.getValueString() returns a comma separated // list.. we want a space separated list for Apache webGroups.println(usersList.replace(',',' ')); } // second we write out user netgroups for (DBObject group: getObjects(userNetgroupSchema.BASE)) { if (group.isInactivated()) { continue; } String usersList = VectorUtils.vectorString(netgroupMembers(group)); if (usersList == null || usersList.equals("")) { continue; } webGroups.print(group.getLabel()); webGroups.print(": "); // VectorUtils.vectorString() returns a comma separated // list.. we want a space separated list for Apache webGroups.println(usersList.replace(',',' ')); } } finally { webGroups.close(); } return true; } /** * <p>This method writes out credentials to our external SMTP server. * The credentials file is formatted as follows:</p> * * <PRE> * mailusername mailpassword * mailusername mailpassword * </PRE> * * <p>It also writes out credentials for our internal server. * The credentials file is formatted as follows:</p> * * <PRE> * username mailusername mailpassword * username mailusername mailpassword * </PRE> */ private boolean writeExternalMailFiles() { PrintWriter mailCredentials = null; PrintWriter extIMAPCredentials = null; try { mailCredentials = openOutFile(path + "extMailCredentials", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeExternalMailFiles(): couldn't open extMailCredentials file: " + ex); return false; } try { try { extIMAPCredentials = openOutFile(path + "extIMAPCredentials", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeExternalMailFiles(): couldn't open extIMAPCredentials file: " + ex); return false; } try { for (DBObject user: getObjects(SchemaConstants.UserBase)) { if (user.isInactivated() || !user.isSet(userSchema.ALLOWEXTERNAL) || !user.isDefined(userSchema.MAILUSER) || !user.isDefined(userSchema.MAILPASSWORD2)) { continue; } StringDBField usernameField = user.getStringField(userSchema.USERNAME); String username = (String) usernameField.getValueLocal(); StringDBField mailUsernameField = user.getStringField(userSchema.MAILUSER); String mailUsername = (String) mailUsernameField.getValueLocal(); PasswordDBField mailpassField = user.getPassField(userSchema.MAILPASSWORD2); String mailpass = mailpassField.getPlainText(); mailCredentials.print(mailUsername); mailCredentials.print(" "); mailCredentials.println(mailpass); extIMAPCredentials.print(username); extIMAPCredentials.print(" "); extIMAPCredentials.print(mailUsername); extIMAPCredentials.print(" "); extIMAPCredentials.println(mailpass); if (user.isDefined(userSchema.OLDMAILUSER) && user.isDefined(userSchema.OLDMAILPASSWORD2)) { mailUsernameField = user.getStringField(userSchema.OLDMAILUSER); mailUsername = (String) mailUsernameField.getValueLocal(); mailpassField = user.getPassField(userSchema.OLDMAILPASSWORD2); mailpass = mailpassField.getPlainText(); mailCredentials.print(mailUsername); mailCredentials.print(" "); mailCredentials.println(mailpass); extIMAPCredentials.print(username); extIMAPCredentials.print(" "); extIMAPCredentials.print(mailUsername); extIMAPCredentials.print(" "); extIMAPCredentials.println(mailpass); } } } finally { extIMAPCredentials.close(); } } finally { mailCredentials.close(); } return true; } /** * <p>This method generates a transitive closure of the members of a * user netgroup, including all users in all member netgroups, * recursively.</p> */ private Vector netgroupMembers(DBObject object) { return netgroupMembers(object, null, null); } /** * <p>This method generates a transitive closure of the members of a * user netgroup, including all users in all member netgroups, * recursively.</p> */ private Vector netgroupMembers(DBObject object, Vector oldMembers, Hashtable graphCheck) { if (oldMembers == null) { oldMembers = new Vector(); } if (graphCheck == null) { graphCheck = new Hashtable(); } // make sure we don't get into an infinite loop if someone made // the user netgroup graph circular if (graphCheck.containsKey(object.getInvid())) { return oldMembers; } else { graphCheck.put(object.getInvid(), object.getInvid()); } // add users in this Netgroup to oldMembers InvidDBField users = object.getInvidField(userNetgroupSchema.USERS); if (users != null) { oldMembers = VectorUtils.union(oldMembers, VectorUtils.stringVector(users.getValueString(), ", ")); } // recursively add in users in any netgroups in this netgroup InvidDBField subGroups = object.getInvidField(userNetgroupSchema.MEMBERGROUPS); if (subGroups != null) { for (int i = 0; i < subGroups.size(); i++) { DBObject subGroup = getObject(subGroups.value(i)); if (!subGroup.isInactivated()) { oldMembers = netgroupMembers(subGroup, oldMembers, graphCheck); } } } return oldMembers; } /** * We can't have any : characters in the Samba password file other than * as field separators, so we strip any we find out. */ private String cleanString(String in) { if (in == null) { return ""; } StringBuilder buffer = new StringBuilder(); char[] ary = in.toCharArray(); /* -- */ // do it for (int i = 0; i < ary.length; i++) { if (ary[i] == ':') { continue; } else { buffer.append(ary[i]); } } return buffer.toString(); } /** * We can't have any : characters in passwords in the rshNT.txt * file we generate, since we use : chars as field separators in * this file. Make sure that we backslash any such chars. */ private String escapeString(String in) { if (in == null) { return ""; } StringBuilder buffer = new StringBuilder(); char[] ary = in.toCharArray(); /* -- */ // do it for (int i = 0; i < ary.length; i++) { if (ary[i] == ':') { buffer.append("\\:"); } else if (ary[i] == '\\') { buffer.append("\\\\"); } else { buffer.append(ary[i]); } } return buffer.toString(); } // *** // // The following private methods are used to support the DNS builder logic. // // *** /** * <p>This method generates a file that maps i.p. addresses to mac addresses, system names, * room of the system, and usernames (if any). This method must be run during * builderPhase1 so that it has access to the getObjects() method * from our superclass.</p> */ private boolean writeSysDataFile() { PrintWriter sys_dataFile = null; /* -- */ try { sys_dataFile = openOutFile(path + "sysdata_info", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeSysFile(): couldn't open sysdata_info file: " + ex); return false; } try { // the hosts_info file is kind of squirrely. We emit all of // the system lines first, followed by all of the interface // lines. for (DBObject system: getObjects(systemSchema.BASE)) { writeSysDataLine(system, sys_dataFile); } } finally { sys_dataFile.close(); } return true; } /** * <p>Writes out one or more lines that maps I.P. addresses to MAC addresses, * system names, room of the system, and usernames (if any).</p> * * <p>Format:</p> * * <code>129.116.224.12|01:02:03:04:05:06|sysname|room|username</code> */ private void writeSysDataLine(DBObject object, PrintWriter writer) { String sysname; Vector<Invid> interfaceInvids; Invid roomInvid; String room; String interfaceName; Invid primaryUserInvid; String primaryUser = null; String MACstring = null; String IPstring = null; String ownerString = null; /* -- */ sysname = (String) object.getFieldValueLocal(systemSchema.SYSTEMNAME); sysname += dnsdomain; interfaceInvids = (Vector<Invid>) object.getFieldValuesLocal(systemSchema.INTERFACES); for (Invid interfaceInvid: interfaceInvids) { String local_sysname = null; /* -- */ result.setLength(0); DBObject interfaceObj = getObject(interfaceInvid); interfaceName = getInterfaceHostname(interfaceObj); if (interfaceName != null) { local_sysname = interfaceName; } else { local_sysname = sysname; } roomInvid = (Invid) object.getFieldValueLocal(systemSchema.ROOM); if (roomInvid != null) { room = getLabel(roomInvid); } else { room = "<unknown>"; } primaryUserInvid = (Invid) object.getFieldValueLocal(systemSchema.PRIMARYUSER); if (primaryUserInvid != null) { primaryUser = getLabel(primaryUserInvid); } try { IPstring = interfaceObj.getIPField(interfaceSchema.ADDRESS).getValueString(); MACstring = interfaceObj.getStringField(interfaceSchema.ETHERNETINFO).getValueString(); MACstring = MACstring.replace('-',':'); } catch (NullPointerException ex) { } if (IPstring == null || MACstring == null) { continue; } try { ownerString = object.getInvidField(SchemaConstants.OwnerListField).getValueString(); } catch (Exception ex) { ownerString = null; } result.append(IPstring); result.append("|"); result.append(MACstring); result.append("|"); result.append(local_sysname); result.append("|"); if (ownerString == null || ownerString.equals("")) { result.append("supergash"); } else { result.append(ownerString); } result.append("|"); result.append(room); result.append("|"); if (primaryUser != null) { result.append(primaryUser); } writer.println(result.toString()); } } /** * * This method generates a hosts_info file. This method must be run during * builderPhase1 so that it has access to the getObjects() method * from our superclass. * */ private boolean writeSysFile() { PrintWriter hosts_info = null; /* -- */ try { hosts_info = openOutFile(path + "hosts_info", "gasharl"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeSysFile(): couldn't open hosts_info file: " + ex); return false; } try { // the hosts_info file is kind of squirrely. We emit all of // the system lines first, followed by all of the interface // lines. for (DBObject system: getObjects(systemSchema.BASE)) { writeSystem(system, hosts_info); } // now the interfaces for (DBObject interfaceObj: getObjects(interfaceSchema.BASE)) { writeInterface(interfaceObj, hosts_info); } } finally { hosts_info.close(); } return true; } /** * * This method writes out a type 1 line to the hosts_info DNS source file.<br/><br/> * * The lines in this file look like the following:<br/><br/> * * <pre> * * ns1.arlut.utexas.edu, ns1b ns1d ns1f ns1e ns1z ns1g ns1h ns1i ns1j ns1k ns1l ns1a ns1c ns1m , \ * news imap-server arlvs1 mail-firewall mail mailhost pop-server ftp sunos sunos2 wais fs1 gopher \ * cso www2 ldap-server www : gl, halls, gil, broccol : S219 : Servers : Sun : SparcCenter 2000 : 2.5.1 : * * </pre> * * @param object An object from the Ganymede system object base * @param writer The destination for this system line * */ private void writeSystem(DBObject object, PrintWriter writer) { String sysname; Vector<Invid> interfaceInvids; Vector<String> interfaceNames = new Vector<String>(); Vector<String> sysAliases; Invid roomInvid; String room; Invid typeInvid; String type; String manufacturer; String model; String os; String interfaceName; Invid primaryUserInvid; String primaryUser = null; /* -- */ result.setLength(0); sysname = (String) object.getFieldValueLocal(systemSchema.SYSTEMNAME); sysname += dnsdomain; interfaceInvids = (Vector<Invid>) object.getFieldValuesLocal(systemSchema.INTERFACES); for (Invid interfaceInvid: interfaceInvids) { interfaceName = getInterfaceHostname(getObject(interfaceInvid)); if (interfaceName != null) { interfaceNames.add(interfaceName); } } sysAliases = (Vector<String>) object.getFieldValuesLocal(systemSchema.SYSTEMALIASES); roomInvid = (Invid) object.getFieldValueLocal(systemSchema.ROOM); if (roomInvid != null) { room = getLabel(roomInvid); } else { room = "<unknown>"; } typeInvid = (Invid) object.getFieldValueLocal(systemSchema.SYSTEMTYPE); if (typeInvid != null) { type = getLabel(typeInvid); } else { type = "<unknown>"; } manufacturer = (String) object.getFieldValueLocal(systemSchema.MANUFACTURER); model = (String) object.getFieldValueLocal(systemSchema.MODEL); os = (String) object.getFieldValueLocal(systemSchema.OS); primaryUserInvid = (Invid) object.getFieldValueLocal(systemSchema.PRIMARYUSER); if (primaryUserInvid != null) { primaryUser = getLabel(primaryUserInvid); } // now build our output line result.append(sysname); result.append(", "); for (String name: interfaceNames) { result.append(name); result.append(" "); } result.append(", "); for (String name: sysAliases) { result.append(name); result.append(" "); } result.append(": : "); // no admins result.append(room); result.append(" : "); result.append(type); result.append(" : "); result.append(manufacturer); result.append(" : "); result.append(model); result.append(" : "); result.append(os); result.append(" : "); if (primaryUser != null) { result.append(primaryUser); } if (result.length() > 1024) { System.err.println("GASHBuilder.writeSystem(): Warning! hosts_info line " + sysname + " overflows the GASH line length!"); } writer.println(result.toString()); } /** * * This method writes out a type 2 line to the hosts_info DNS source file.<br/><br/> * * The lines in this file look like the following:<br/><br/> * * <pre> * * >ns1a, ns1.arlut.utexas.edu, hostalias : 129.116.240.2 : 8-0-20-1b-d7-23 * * </pre> * * for a multi-host system, or * * <pre> * * >, sgdmac201.arlut.utexas.edu, : 129.116.208.201 : 0-0-89-1-c4-c * * </pre> * * for a single-interface system. * * @param object An object from the Ganymede system object base * @param writer The destination for this system line * */ private void writeInterface(DBObject object, PrintWriter writer) { String hostname = null; String sysname; String IPString; String MAC; Vector<String> hostAliases = null; IPDBField ipField; /* -- */ result.setLength(0); // we need to assemble the information that gash uses for our output if (object.isDefined(interfaceSchema.ETHERNETINFO)) { MAC = (String) object.getFieldValueLocal(interfaceSchema.ETHERNETINFO); // We want to use dashes to separate the hex bytes in our ethernet addr MAC = MAC.replace(':','-'); } else { MAC = "00-00-00-00-00-00"; } // an interface is contained in the associated system, so we check our // containing object for its name.. we assume that this interface *does* // have a containing field (it's embedded, so it must, eh?), so we don't // check for null container field here. sysname = getLabel((Invid) object.getFieldValueLocal(SchemaConstants.ContainerField)); sysname += dnsdomain; // an interface can theoretically have multiple IP records and DNS records, but // GASH only supported one. // get the IP address for this interface ipField = object.getIPField(interfaceSchema.ADDRESS); if (ipField == null) { System.err.println("GASHBuilder.writeInterface(): WARNING! Interface for " + sysname + " has no IP address! Skipping!"); return; } if (!ipField.isIPV4()) { System.err.println("GASHBuilder.writeInterface(): WARNING! Interface for " + sysname + " has an IPV6 record! This isn't compatible with the GASH makefiles! Skipping!"); return; } IPString = ipField.getValueString(); // and the DNS info hostname = (String) object.getFieldValueLocal(interfaceSchema.NAME); hostAliases = (Vector<String>) object.getFieldValuesLocal(interfaceSchema.ALIASES); // now build our output line result.append(">"); if (hostname != null) { result.append(hostname); } result.append(", "); result.append(sysname); result.append(","); for (String name: hostAliases) { result.append(" "); result.append(name); } result.append(" : "); result.append(IPString); result.append(" : "); result.append(MAC); if (result.length() > 1024) { System.err.println("GASHBuilder.writeInterface(): Warning! hosts_info type 2 line " + ((hostname == null) ? sysname : hostname) + " overflows the GASH line length!"); } writer.println(result.toString()); } /** * * This method extracts an embedded hostname from a top-level interface * object. * */ private String getInterfaceHostname(DBObject object) { return (String) object.getFieldValueLocal(interfaceSchema.NAME); } // *** // // The following private methods are used to support the DHCP emitter logic. // // *** /** * <p>This method writes out the ISC DHCP server * configuration file from the data in the Ganymede data store.</p> * * <p>The pieces of this file include: * Custom Option Declarations.<br/> * _GLOBAL_ dhcp network options<br/> * List of DHCP Network settings<br/> * List of System DHCP settings</p> */ private boolean writeDHCPFile() { NullWriter nullWriter = new NullWriter(); PrintWriter dhcpFileWriter = null; List<DBObject> networks = null; // Do a first pass to collect the customOptions declarations we'll // need up top, using a side effect of writeDHCPNetwork() and // writeDHCPSystem(). this.customOptions = new HashSet<Invid>(); networks = (List<DBObject>) java.util.Collections.list(enumerateObjects(dhcpNetworkSchema.BASE)); java.util.Collections.sort(networks, new NetworkSortByName()); for (DBObject networkObject: networks) { writeDHCPNetwork(networkObject, nullWriter); } for (DBObject systemObject: getObjects(systemSchema.BASE)) { writeDHCPSystem(systemObject, nullWriter); } // okay, we've got our custom options, we can go ahead and write // out the file in a single, second pass try { try { dhcpFileWriter = openOutFile(path + "new_dhcpd.conf", "gasharl"); } catch (IOException ex) { Ganymede.debug("GASHBuilderTask.writeDHCPFile(): couldn't open new_dhcpd_temp file: " + ex); return false; } dhcpFileWriter.println("# Generated by Ganymede GASHBuilderTask"); dhcpFileWriter.println("# " + new Date().toString()); dhcpFileWriter.println("#"); dhcpFileWriter.println("# NOTE: This file, in its entirety, is now being generated by Ganymede."); dhcpFileWriter.println("#"); dhcpFileWriter.println("# To reconfigure global or shared network dhcp options, edit the"); dhcpFileWriter.println("# DHCP Network objects in Ganymede under the Configuration section."); dhcpFileWriter.println("#"); dhcpFileWriter.println("# -- James and Jon"); dhcpFileWriter.println("#"); dhcpFileWriter.println("######################################################################"); dhcpFileWriter.println(""); dhcpFileWriter.println("authoritative;"); dhcpFileWriter.println("ddns-update-style none;"); dhcpFileWriter.println(""); writeDHCPCustomOptions(dhcpFileWriter); dhcpFileWriter.println("\n#==============================================================================="); dhcpFileWriter.println("# Shared Networks Data"); dhcpFileWriter.println("#==============================================================================="); // we're going to sort the DHCPNetwork objects by name so that // we are sure to write out the _GLOBAL_ record first. networks = (List<DBObject>) java.util.Collections.list(enumerateObjects(dhcpNetworkSchema.BASE)); java.util.Collections.sort(networks, new NetworkSortByName()); for (DBObject networkObject: networks) { writeDHCPNetwork(networkObject, dhcpFileWriter); } dhcpFileWriter.println("\n#==============================================================================="); dhcpFileWriter.println("# Per System Data"); dhcpFileWriter.println("#==============================================================================="); for (DBObject systemObject: getObjects(systemSchema.BASE)) { writeDHCPSystem(systemObject, dhcpFileWriter); } return true; } finally { this.customOptions = null; if (dhcpFileWriter != null) { dhcpFileWriter.close(); } } } /** * This method writes out all DHCP custom option declarations * discovered during generation of the shared network and individual * host record definitions in the writeDHCPFile() method, which in * turn calls us so that we can write these options to the top of * the generated dhcp_dataFile. */ private boolean writeDHCPCustomOptions(PrintWriter writer) { if (this.customOptions.size() == 0) { return true; } writer.println("# Custom Option Declarations"); writer.println("#==============================================================================="); // loop once to find custom option spaces HashSet<String> foundOptions = new HashSet<String>(); for (Invid optionInvid: customOptions) { DBObject obj = getObject(optionInvid); if (obj != null) { String name = (String) obj.getFieldValueLocal(dhcpOptionSchema.OPTIONNAME); if (name.indexOf('.') != -1) { String optionSpace = name.substring(0, name.indexOf('.')); if (!foundOptions.contains(optionSpace)) { writer.println("option space " + optionSpace + ";"); foundOptions.add(optionSpace); } } } } // loop again to declare our custom options for (Invid optionInvid: customOptions) { DBObject obj = getObject(optionInvid); if (obj != null) { String name = (String) obj.getFieldValueLocal(dhcpOptionSchema.OPTIONNAME); String type = (String) obj.getFieldValueLocal(dhcpOptionSchema.OPTIONTYPE); Integer code = (Integer) obj.getFieldValueLocal(dhcpOptionSchema.CUSTOMCODE); // we need to use the expanded syntax for the // option types for the ISC DHCP server if (type.equals("uint8")) { type = "unsigned integer 8"; } else if (type.equals("int8")) { type = "signed integer 8"; } else if (type.equals("uint16")) { type = "unsigned integer 16"; } else if (type.equals("int16")) { type = "signed integer 16"; } else if (type.equals("uint32")) { type = "unsigned integer 16"; } else if (type.equals("int32")) { type = "signed integer 16"; } writer.println("option " + name + " code " + code + " = " + type + ";"); } } writer.println("#==============================================================================="); return true; } /** * <p>This method writes out DHCP info for a shared network to the * new dhcpd file.</p> * * <p>As a side effect, this method collects information in the * GASHBuilderTask.customOptions Set for use by the other DHCP * writer methods.</p> * * @param object An object from the Ganymede system object base * @param writer The destination for this system line */ private void writeDHCPNetwork(DBObject object, Writer writer) { String name = (String) object.getFieldValueLocal(dhcpNetworkSchema.NAME); HashMap<String, dhcp_entry> options = new HashMap<String, dhcp_entry>(); // If global, just write out options only now. if (name.equals("_GLOBAL_") && object.isDefined(dhcpNetworkSchema.OPTIONS)) { findDHCPOptions(options, object.getFieldValuesLocal(dhcpNetworkSchema.OPTIONS)); writeDHCPOptionList(options, object, writer, ""); return; } try { writer.write("\n#===============================================================================\n"); writer.write("shared-network " + name + "\n"); writer.write("{\n"); if (object.isDefined(dhcpNetworkSchema.OPTIONS)) { findDHCPOptions(options, object.getFieldValuesLocal(dhcpNetworkSchema.OPTIONS)); writeDHCPOptionList(options, object, writer, "\t"); } for (Invid subnet: (Vector<Invid>) object.getFieldValuesLocal(dhcpNetworkSchema.SUBNETS)) { DBObject subnetObject = getObject(subnet); String network_number = ""; String network_mask = ""; IPDBField ipField = subnetObject.getIPField(dhcpSubnetSchema.NETWORK_NUMBER); if (ipField != null) { network_number = (String) ipField.getEncodingString(); } ipField = subnetObject.getIPField(dhcpSubnetSchema.NETWORK_MASK); if (ipField != null) { network_mask = (String) ipField.getEncodingString(); } writer.write("\tsubnet\t" + network_number + "\tnetmask\t\t" + network_mask + "\n"); writer.write("\t{ \n"); if (subnetObject.isDefined(dhcpSubnetSchema.OPTIONS)) { options.clear(); findDHCPOptions(options, subnetObject.getFieldValuesLocal(dhcpSubnetSchema.OPTIONS)); writeDHCPOptionList(options, subnetObject, writer, "\t\t"); } if (subnetObject.isSet(dhcpSubnetSchema.ALLOW_REGISTERED_GUESTS)) { writer.write("\t\tpool\n"); writer.write("\t\t{\n"); String guest_range = (String) subnetObject.getFieldValueLocal(dhcpSubnetSchema.GUEST_RANGE); writer.write("\t\t\trange\t" + guest_range + ";\n"); if (subnetObject.isDefined(dhcpSubnetSchema.GUEST_OPTIONS)) { options.clear(); findDHCPOptions(options, subnetObject.getFieldValuesLocal(dhcpSubnetSchema.GUEST_OPTIONS)); writeDHCPOptionList(options, subnetObject, writer, "\t\t\t"); } writer.write("\t\t\tallow known clients;\n"); writer.write("\t\t}\n"); } writer.write("\t} # END SUBNET " + network_number + "\n"); } writer.write("} # END SHARED-NETWORK " + name + "\n"); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeDHCPNetwork(): couldn't write to file: " + ex); } } /** * <p>This method writes out DHCP info for a single system to the * new dhcpd file.</p> * * <p>As a side effect, this method collects information in the * GASHBuilderTask.customOptions Set for use by the other DHCP * writer methods.</p> * * @param object An object from the Ganymede system object base * @param writer The destination for this system line */ private void writeDHCPSystem(DBObject object, Writer writer) { String sysname = null; Vector<Invid> interfaceInvids = null; DBObject interfaceObj = null; IPDBField ipField = null; String ipAddress = null; StringDBField macField = null; String macAddress = null; HashMap<String, dhcp_entry> options = new HashMap<String, dhcp_entry>(); StringBuilder buffer = new StringBuilder(); /* -- */ interfaceInvids = (Vector<Invid>) object.getFieldValuesLocal(systemSchema.INTERFACES); if (interfaceInvids.size() != 1) { return; // we don't write out DHCP for systems // with more than one interface } interfaceObj = getObject(interfaceInvids.get(0)); ipField = interfaceObj.getIPField(interfaceSchema.ADDRESS); ipAddress = ipField.getEncodingString(); if (interfaceObj.isDefined(interfaceSchema.ETHERNETINFO)) { macField = interfaceObj.getStringField(interfaceSchema.ETHERNETINFO); macAddress = macField.getEncodingString(); if (macAddress.equals("00:00:00:00:00:00")) { return; // don't write out DHCP for systems // with unspecified MAC addresses } } else { return; // ditto } Invid networkInvid = (Invid) interfaceObj.getFieldValueLocal(interfaceSchema.IPNET); DBObject networkObj = getObject(networkInvid); if (!networkObj.isSet(networkSchema.DHCP)) { return; // no DHCP for this network } sysname = (String) object.getFieldValueLocal(systemSchema.SYSTEMNAME); // now let's see if we have any custom dhcp options for this // system. we'll look up options from dhcp group membership // first, then from locally defined options. In this way the // locally defined options will overwrite any group-derived // options. if (object.isDefined(systemSchema.DHCPGROUPS)) { Vector<Invid> dhcpGroupInvids = (Vector<Invid>) object.getFieldValuesLocal(systemSchema.DHCPGROUPS); for (Invid dhcpInvid: dhcpGroupInvids) { DBObject dhcpGroup = getObject(dhcpInvid); findDHCPOptions(options, dhcpGroup.getFieldValuesLocal(dhcpGroupSchema.OPTIONS)); } } if (object.isDefined(systemSchema.DHCPOPTIONS)) { findDHCPOptions(options, object.getFieldValuesLocal(systemSchema.DHCPOPTIONS)); } // now build our output stanza // // we need to loop twice on this, the first time writing out // the fixed address, the second time the roaming definition for (int i = 0; i < 2; i++) { buffer.setLength(0); buffer.append("host "); if (i == 0) { buffer.append(sysname); } else { buffer.append(sysname + "_roaming"); } buffer.append("\n{\n"); buffer.append("\thardware ethernet\t\t"); buffer.append(macAddress); buffer.append(";\n"); if (i == 0) { // we'll skip this the second time for our roaming entry buffer.append("\tfixed-address\t\t\t"); buffer.append(ipAddress); buffer.append(";\n"); } buffer.append("\toption host-name\t\t"); buffer.append(quote(sysname)); buffer.append(";\n"); try { writer.write(buffer.toString()); writeDHCPOptionList(options, object, writer, "\t"); if (i == 0) { writer.write("} # END host\n\n"); } else { writer.write("} # END roaming host entry\n\n"); } } catch (IOException ex) { System.err.println("GASHBuilderTask.writeDHCPSystem(): couldn't write to file: " + ex); } if (options.size() == 0) { // no custom dhcp, so we don't need to create a roaming // entry, just break out break; } } } /** * <p>This method writes out the DHCP option definitions to be * contained within a global, shared network, subnet, or host record * in the generated DHCP file.</p> */ private void writeDHCPOptionList(HashMap<String, dhcp_entry> options, DBObject object, Writer writer, String tabs) { result.setLength(0); // first make sure we've declared any site-option-space that // we'll need to use HashSet<String> spaces = new HashSet<String>(); for (dhcp_entry entry: options.values()) { if (entry.builtin) { continue; } if (entry.name.indexOf('.') != -1) { String spaceName = entry.name.substring(0, entry.name.indexOf('.')); if (spaces.size() == 0) { result.append(tabs+"site-option-space\t\t\"" + spaceName + "\";\n"); spaces.add(spaceName); } else { if (!spaces.contains(spaceName)) { Ganymede.debug("GASHBuilderTask: writeDHCPSystem() ran into problems with " + object.getLabel() + " due to conflicting DHCP option spaces."); } } } } // second make sure that we've forced any mandatory options HashSet<dhcp_entry> forcedOptions = new HashSet<dhcp_entry>(); for (dhcp_entry entry: options.values()) { if (entry.forced) { forcedOptions.add(entry); } } if (forcedOptions.size() > 0) { StringBuilder hexOptionCodes = new StringBuilder(); StringBuilder concatPrefix = new StringBuilder(); for (dhcp_entry entry: forcedOptions) { if (entry.forced && entry.code != 0) { if (hexOptionCodes.length() == 0) { hexOptionCodes.append(","); } else { hexOptionCodes.append("),"); } if (concatPrefix.length () == 0) { concatPrefix.append("concat(option dhcp-parameter-request-list"); } else { concatPrefix.insert(0, "concat("); } hexOptionCodes.append(java.lang.Integer.toHexString(entry.code)); } } if (hexOptionCodes.length() != 0) { hexOptionCodes.append(");\n"); result.append(tabs+"if exists dhcp-parameter-request-list {\n"); result.append(tabs+"\t# Ganymede forced dhcp options\n"); result.append(tabs+"\toption dhcp-parameter-request-list = "); result.append(concatPrefix); result.append(hexOptionCodes); result.append(tabs+"}\n"); } } // third, let's write out the actual options for this host for (dhcp_entry entry: options.values()) { int length = 0; result.append(tabs); if (!entry.builtin) { result.append("option "); length = 7; } else { length = 0; } result.append(entry.name); if (length + entry.name.length() < 16) { result.append("\t\t\t"); } else if (length + entry.name.length() < 24) { result.append("\t\t"); } else { result.append("\t"); } if (entry.type.equals("string") || entry.type.equals("text")) { result.append(quote(entry.value)); result.append(";\n"); } else { result.append(entry.value); result.append(";\n"); } } try { writer.write(result.toString()); } catch (IOException ex) { System.err.println("GASHBuilderTask.writeDHCPOptionList(): couldn't write to file: " + ex); } } private String quote(String in) { return "\"" + in + "\""; } private void findDHCPOptions(HashMap<String, dhcp_entry> resultMap, Vector entryInvids) { if (entryInvids == null) { return; } for (Invid entryInvid: (Vector<Invid>) entryInvids) { DBObject entryObject = getObject(entryInvid); Invid optionInvid = (Invid) entryObject.getFieldValueLocal(dhcpEntrySchema.TYPE); String value = (String) entryObject.getFieldValueLocal(dhcpEntrySchema.VALUE); DBObject optionObject = getObject(optionInvid); String typeName = (String) optionObject.getFieldValueLocal(dhcpOptionSchema.OPTIONNAME); String typeString = (String) optionObject.getFieldValueLocal(dhcpOptionSchema.OPTIONTYPE); Integer typeCode = (Integer) optionObject.getFieldValueLocal(dhcpOptionSchema.CUSTOMCODE); int typecode = 0; if (typeCode != null) { typecode = typeCode.intValue(); } resultMap.put(typeName, new dhcp_entry(typeName, typeString, value, optionObject.isSet(dhcpOptionSchema.BUILTIN), typecode, optionObject.isSet(dhcpOptionSchema.FORCESEND))); if (!optionObject.isSet(dhcpOptionSchema.BUILTIN) && optionObject.isSet(dhcpOptionSchema.CUSTOMOPTION)) { this.customOptions.add(optionInvid); } } } /** * This method returns the Invid for the 'normal' user category in * the gasharl schema from our local cache if we've looked it up * before, or else scans the user category objects looking for it. * * Note that this only works so long as scanCategories() has the * proper constant for the name of the normal user category. */ private Invid getNormalCategory() { if (this.normalCategory != null) { return this.normalCategory; } scanCategories(); return this.normalCategory; } /** * This method returns the Invid for the 'agency worker' user * category in the gasharl schema from our local cache if we've * looked it up before, or else scans the user category objects * looking for it. * * Note that this only works so long as scanCategories() has the * proper constant for the name of the agency worker category. */ private Invid getAgencyCategory() { if (this.agencyCategory != null) { return this.agencyCategory; } scanCategories(); return this.agencyCategory; } /** * This method scans the user category object base in order to * identify the normal worker and agency worker user category * invids. */ private void scanCategories() { this.normalCategory = findLabeledObject(normalCategoryLabel, userCategorySchema.BASE); this.agencyCategory = findLabeledObject(agencyCategoryLabel, userCategorySchema.BASE); if (this.normalCategory == null) { Ganymede.debug("ERROR: GASHBuilderTask.scanCategories() couldn't find the " + normalCategoryLabel + " user category!"); } if (this.agencyCategory == null) { Ganymede.debug("ERROR: GASHBuilderTask.scanCategories() couldn't find the " + agencyCategoryLabel + " user category!"); } } } /*------------------------------------------------------------------------------ class dhcp_entry ------------------------------------------------------------------------------*/ /** * Non-public data carrying "struct" class used to make things easier * for us as we assemble our DHCP output in the GASHBuilderTask. */ class dhcp_entry { public String name; public String type; public String value; public boolean builtin; public int code; public boolean forced; /* -- */ public dhcp_entry(String name, String type, String value, boolean builtin, int code, boolean forced) { this.name = name; this.type = type; this.value = value; this.builtin = builtin; this.code = code; this.forced = forced; } } /*------------------------------------------------------------------------------ class NetworkSortByName ------------------------------------------------------------------------------*/ /** * Comparator to aid in sorting DHCPNetwork objects in the Ganymede server. */ class NetworkSortByName implements Comparator<DBObject> { public int compare(DBObject o1, DBObject o2) { String s1 = (String) o1.getFieldValueLocal(dhcpNetworkSchema.NAME); String s2 = (String) o2.getFieldValueLocal(dhcpNetworkSchema.NAME); return s1.compareTo(s2); } }