/*
userCustom.java
This file is a management class for user objects in Ganymede.
Created: 30 July 1997
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
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.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.Vector;
import org.doomdark.uuid.EthernetAddress;
import org.doomdark.uuid.UUIDGenerator;
import arlut.csd.JDialog.JDialogBuff;
import arlut.csd.Util.FileOps;
import arlut.csd.Util.PathComplete;
import arlut.csd.Util.RandomUtils;
import arlut.csd.Util.StringUtils;
import arlut.csd.Util.VectorUtils;
import arlut.csd.ganymede.common.GanyPermissionsException;
import arlut.csd.ganymede.common.Invid;
import arlut.csd.ganymede.common.NotLoggedInException;
import arlut.csd.ganymede.common.GanyParseException;
import arlut.csd.ganymede.common.ObjectStatus;
import arlut.csd.ganymede.common.PermEntry;
import arlut.csd.ganymede.common.Query;
import arlut.csd.ganymede.common.QueryDataNode;
import arlut.csd.ganymede.common.QueryResult;
import arlut.csd.ganymede.common.Result;
import arlut.csd.ganymede.common.ReturnVal;
import arlut.csd.ganymede.common.SchemaConstants;
import arlut.csd.ganymede.server.DBEditObject;
import arlut.csd.ganymede.server.DBEditSet;
import arlut.csd.ganymede.server.DBField;
import arlut.csd.ganymede.server.DBLog;
import arlut.csd.ganymede.server.DBNameSpace;
import arlut.csd.ganymede.server.DBObject;
import arlut.csd.ganymede.server.DBObjectBase;
import arlut.csd.ganymede.server.DBObjectBaseField;
import arlut.csd.ganymede.server.DBSession;
import arlut.csd.ganymede.server.DateDBField;
import arlut.csd.ganymede.server.Ganymede;
import arlut.csd.ganymede.server.GanymedeSession;
import arlut.csd.ganymede.server.InvidDBField;
import arlut.csd.ganymede.server.NumericDBField;
import arlut.csd.ganymede.server.PasswordDBField;
import arlut.csd.ganymede.server.StringDBField;
import arlut.csd.ganymede.server.adminPersonaCustom;
/*------------------------------------------------------------------------------
class
userCustom
------------------------------------------------------------------------------*/
/**
* <p>This class is the custom plug-in to handle the user object type in the
* Ganymede server. It does special validations of operations on the user,
* handles inactivation and reactivation logic, and generates Wizards as
* needed.</p>
*
* <p>See the userSchema.java file for a list of field definitions that this
* module expects to work with.</p>
*
* @see arlut.csd.ganymede.gasharl.userSchema
* @see arlut.csd.ganymede.server.DBEditObject
*/
public class userCustom extends DBEditObject implements SchemaConstants, userSchema {
static final boolean debug = false;
static QueryResult shellChoices = new QueryResult();
static Date shellChoiceStamp = null;
static String mailsuffix = null;
static String homedir = null;
static String renameFilename = null;
static File renameHandler = null;
static String createFilename = null;
static File createHandler = null;
static String deleteFilename = null;
static File deleteHandler = null;
static final int lowUID = 2001;
static int file_identifier = 0;
public static synchronized int getNextAuthIdent()
{
if (file_identifier == Integer.MAX_VALUE)
{
file_identifier = 0;
}
else
{
file_identifier++;
}
return file_identifier;
}
public static File getNextFileName()
{
return new File("/tmp/ganymede_ext_validate_" + getNextAuthIdent());
}
// ---
QueryResult groupChoices = null;
String newUsername = null;
private boolean amChangingExpireDate = false;
/**
* Private flag used to allow us to return an error dialog the first
* time we detect an HR/IRIS problem. Once we've presented an HR
* error to the admin, we'll set this flag and disregard the problem
* if the admin hits commit again in the client without changing
* anything.
*/
private boolean IRISWarningGiven = false;
/**
* <p>Dummy constructor, is responsible for creating a DBEditObject
* strictly for the purpose of having a handle to call our
* pseudostatic customization methods on.</p>
*
* <p>This is the version of the constructor that the {@link
* arlut.csd.ganymede.server.DBObjectBase DBObjectBase}'s {@link
* arlut.csd.ganymede.server.DBObjectBase#createHook() createHook()}
* method uses to create the {@link
* arlut.csd.ganymede.server.DBObjectBase#objectHook objectHook}
* object.</p>
*/
public userCustom(DBObjectBase objectBase)
{
super(objectBase);
}
/**
* <p>Creation constructor, is responsible for creating a new
* editable object with all fields listed in the {@link
* arlut.csd.ganymede.server.DBObjectBaseField DBObjectBaseField}
* instantiated but undefined.</p>
*
* <p>This constructor is not really intended to be overridden in
* subclasses. Creation time field value initialization is to be
* handled by initializeNewObject().</p>
*
* @see arlut.csd.ganymede.server.DBField
*/
public userCustom(DBObjectBase objectBase, Invid invid, DBEditSet editset)
{
super(objectBase, invid, editset);
}
/**
* Check-out constructor, used by {@link
* arlut.csd.ganymede.server.DBObject#createShadow(arlut.csd.ganymede.server.DBEditSet)
* DBObject.createShadow()} to pull out an object for editing.
*/
public userCustom(DBObject original, DBEditSet editset)
{
super(original, editset);
}
/**
* <p>Initializes a newly created DBEditObject.</p>
*
* <p>When this method is called, the DBEditObject has been created,
* its ownership set, and all fields defined in the controlling
* {@link arlut.csd.ganymede.server.DBObjectBase DBObjectBase} have
* been instantiated without defined values. If this DBEditObject
* is an embedded type, it will have been linked into its parent
* object before this method is called.</p>
*
* <p>This method is responsible for filling in any default values
* that can be calculated from the {@link
* arlut.csd.ganymede.server.DBSession DBSession} associated with
* the editset defined in this DBEditObject.</p>
*
* <p>If initialization fails for some reason, initializeNewObject()
* will return a ReturnVal with an error result.. If the owning
* GanymedeSession is not in bulk-loading mode (i.e.,
* GanymedeSession.enableOversight is true), {@link
* arlut.csd.ganymede.server.DBSession#createDBObject(short,
* arlut.csd.ganymede.common.Invid, java.util.Vector)
* DBSession.createDBObject()} will checkpoint the transaction
* before calling this method. If this method returns a failure
* code, the calling method will rollback the transaction. This
* method has no responsibility for undoing partial initialization,
* the checkpoint/rollback logic will take care of that.</p>
*
* <p>If enableOversight is false, DBSession.createDBObject() will
* not checkpoint the transaction status prior to calling
* initializeNewObject(), so it is the responsibility of this method
* to handle any checkpointing needed.</p>
*
* <p>This method should be overridden in subclasses.</p>
*
* @return A ReturnVal indicating success or failure. May
* be simply 'null' to indicate success if no feedback need
* be provided.
*/
@Override public ReturnVal initializeNewObject()
{
try
{
ReturnVal retVal;
Random rand = new Random();
Integer uidVal = null;
/* -- */
// we don't want to do any of this initialization during
// bulk-loading.
if (!getGSession().enableOversight)
{
return null;
}
// need to find a global unique id (guid) for this user
StringDBField guidField = getStringField(GUID);
if (guidField == null)
{
return Ganymede.createErrorDialog(this.getGSession(),
"User Initialization Failure",
"Couldn't find the guid field.. schema problem?");
}
String guid = generateGUID(); // create a globally unique uid
retVal = guidField.setValueLocal(guid);
if (!ReturnVal.didSucceed(retVal))
{
return retVal;
}
// need to find a uid for this user
NumericDBField numField = getNumericField(UID);
if (numField == null)
{
return Ganymede.createErrorDialog(this.getGSession(),
"User Initialization Failure",
"Couldn't find the uid field.. schema problem?");
}
DBNameSpace namespace = numField.getNameSpace();
if (namespace == null)
{
return Ganymede.createErrorDialog(this.getGSession(),
"User Initialization Failure",
"Couldn't find the uid namespace.. schema problem?");
}
// now, find a uid.. unfortunately, we have to use immutable Integers here.. not
// the most efficient at all.
int count = 0;
uidVal = new Integer(rand.nextInt(31767) + lowUID);
while (!namespace.reserve(getEditSet(), uidVal) && count < 30000)
{
uidVal = new Integer(rand.nextInt(31767) + lowUID);
count++;
}
if (count > 30000)
{
// we've been looping too long, maybe there's no
// uid's free? let's do an exhaustive search
uidVal = new Integer(lowUID);
while (!namespace.reserve(getEditSet(), uidVal))
{
uidVal = new Integer(uidVal.intValue() + 1);
if (uidVal.intValue() > 32767)
{
throw new RuntimeException("Couldn't find an allocatable uid through random search");
}
}
}
// we use setValueLocal so we can set a value that the user can't edit.
retVal = numField.setValueLocal(uidVal);
if (!ReturnVal.didSucceed(retVal))
{
return retVal;
}
// set the new user account to type 'normal'.
InvidDBField catf = getInvidField(userSchema.CATEGORY);
Invid normalCat = getGSession().findLabeledObject("normal", userCategorySchema.BASE);
retVal = catf.setValueLocal(normalCat, true);
if (!ReturnVal.didSucceed(retVal))
{
return retVal;
}
// create a volume entry for the user.
InvidDBField invf = getInvidField(userSchema.VOLUMES);
try
{
retVal = invf.createNewEmbedded(true);
}
catch (GanyPermissionsException ex)
{
return Ganymede.createErrorDialog(this.getGSession(),
"permissions",
"permissions error creating embedded object" + ex);
}
if ((retVal == null) || (!retVal.didSucceed()))
{
return retVal;
}
Invid invid = retVal.getInvid();
if (invid != null)
{
// find the auto.home.default map, if we can.
Vector<Result> results = getGSession().internalQuery(new Query((short) 277, // automounter map
new QueryDataNode(QueryDataNode.EQUALS,
"auto.home.default"),
false));
// if we found auto.home.default, set the new volume entry map
// field to point to auto.home.default.
if (results != null && results.size() == 1)
{
Result objid = results.get(0);
DBEditObject eObj = getDBSession().editDBObject(invid);
invf = eObj.getInvidField(mapEntrySchema.MAP);
retVal = invf.setValueLocal(objid.getInvid());
if (retVal != null && !retVal.didSucceed())
{
return retVal;
}
// we want the permissions system to reject edit privs
// on this now.. by setting permCache to null, we allow
// the mapEntryCustom permOverride method to get a chance
// to refuse edit privileges.
eObj.clearFieldPerm(mapEntrySchema.MAP);
}
}
return null;
}
catch (NotLoggedInException ex)
{
return Ganymede.loginError(ex);
}
}
/**
* <p>Private method to create a globally unique UID value suitable
* for certain LDAP applications</p>
*/
private String generateGUID()
{
UUIDGenerator gen = UUIDGenerator.getInstance();
org.doomdark.uuid.UUID guid = gen.generateTimeBasedUUID(new EthernetAddress("00:11:43:D5:F7:F8")); // csdsun9
return guid.toString();
}
/**
* <p>This method provides a pre-commit hook that runs after the
* user has hit commit but before the system has established write
* locks for the commit.</p>
*
* <p>The intended purpose of this hook is to allow objects that
* dynamically maintain hidden label fields to update those fields
* from the contents of the object's other fields at commit
* time.</p>
*
* <p>This method runs in a checkpointed context. If this method
* fails in any operation, you should return a ReturnVal with a
* failure dialog encoded, and the transaction's commit will be
* blocked and a dialog explaining the problem will be presented to
* the user.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*
* @return A ReturnVal indicating success or failure. May
* be simply 'null' to indicate success if no feedback need
* be provided.
*/
@Override public ReturnVal preCommitHook()
{
if (this.getStatus() == ObjectStatus.DELETING ||
this.getStatus() == ObjectStatus.DROPPING)
{
return null;
}
// james test debug TODO TEST
//return Ganymede.createErrorDialog("Test popup title....debug", "TEST DEBUG DIALOG ");
ReturnVal retVal = renameEntries(this.getLabel());
if (!ReturnVal.didSucceed(retVal))
{
return retVal;
}
Invid category = (Invid) this.getFieldValueLocal(userSchema.CATEGORY);
DBObject categoryObj = lookupInvid(category, false);
// james look at invid function
if (categoryObj == null)
{
// The fieldRequired() method is run after preCommitHook(), so
// this could be null here.
//
// If null, we'll just return early and let DBEditSet call
// checkRequiredFields() on this later to report the error.
return null;
}
String categoryName = categoryObj.getLabel();
// now check to make sure that the username/badge combination
// doesn't conflict with something in IRIS' history.
//
// we do this in preCommitHook() so that we can check the final
// status of the username and badge information since we need both
// to make an intelligent check.
boolean needBadgeNameCheck = false;
if (this.getStatus() == ObjectStatus.CREATING)
{
needBadgeNameCheck = true;
}
else
{
Set<DBObjectBaseField> fieldsChanged = new HashSet<DBObjectBaseField>();
String ignoreResult = diff(fieldsChanged);
if (fieldsChanged.contains(getFieldDef(userSchema.USERNAME)) ||
fieldsChanged.contains(getFieldDef(userSchema.BADGE)) ||
fieldsChanged.contains(getFieldDef(userSchema.CATEGORY)))
{
needBadgeNameCheck = true;
}
}
if (!categoryName.equals("normal"))
{
needBadgeNameCheck = false;
}
if (needBadgeNameCheck)
{
String username = (String) getFieldValueLocal(userSchema.USERNAME);
String badge = (String) getFieldValueLocal(userSchema.BADGE);
if (badge == null || badge.trim().equals(""))
{
return Ganymede.createErrorDialog(this.getGSession(),
"Badge Number Required",
"The " + username + " user object is categorized as a " + categoryName + " account, and requires a badge number.");
}
// if we have a SQL exception thrown, we'll catch and log it,
// but we won't block user creation / edit.
try
{
if (!IRISLink.isRegisteredBadgeNumber(badge))
{
if (!IRISWarningGiven)
{
IRISWarningGiven = true;
return ReturnVal.merge(retVal, Ganymede.createErrorDialog(this.getGSession(),
"Warning: Badge number not in HR database",
"The " + username + " user object is currently registered as having badge number " +
badge + ", which is not registered in the HR database.\n\n" +
"You should either correct the badge number or contact HR to find out why the badge " +
"is not properly recorded in HR's database."));
}
}
if (!IRISLink.okayToUseName(username, badge))
{
String badgeConflict = IRISLink.findHistoricalBadge(username);
String fullNameConflict = IRISLink.findHistoricalEmployeeName(username);
if (!IRISWarningGiven)
{
IRISWarningGiven = true;
return ReturnVal.merge(retVal, Ganymede.createErrorDialog(this.getGSession(),
"Warning: Historical username conflict",
"The '" + username +
"' user object (with badge id '" + badge + "') conflicts with an earlier '" + username +
"' account that is still referenced in the HR database.\n\n" +
"The previous account was owned by employee '" + fullNameConflict +
"', with badge id '" + badgeConflict +
"'.\n\n" +
"You should change the username or badge id in the '" + username + "' object in order " +
"to resolve this conflict.\n\n" +
"You could also change this user object's user category to something other than 'normal'.\n\n" +
"If you feel this message is in error, hit commit again to proceed."));
}
}
}
catch (Exception ex)
{
Ganymede.logError(ex);
}
}
// Send out mail describing the external credentials for this
// user, if such are defined
// TODO MAKE THIS A FUNCTION.
if (isSet(userSchema.ALLOWEXTERNAL) && isDefined(userSchema.MAILUSER) && isDefined(userSchema.MAILPASSWORD2))
{
DBObject originalObject = getOriginal();
String titleString = null;
String messageString = null;
String expireString = null;
String mailUsername = (String) getFieldValueLocal(userSchema.MAILUSER);
PasswordDBField mailPasswordField = getPassField(userSchema.MAILPASSWORD2);
String mailPassword = mailPasswordField.getPlainText();
if (mailPassword == null)
{
mailPassword = ""; // shouldn't need this if MAILPASSWORD2 is configured properly
}
Date mailExpireDate = (Date) getFieldValueLocal(userSchema.MAILEXPDATE);
if (originalObject == null || !originalObject.isDefined(userSchema.MAILUSER) || !originalObject.isDefined(userSchema.MAILPASSWORD2))
{
titleString = "External Email Credentials Set For User " + this.getLabel();
messageString = "User account " + this.getLabel() +
" has been granted access to laboratory email from outside the internal ARL:UT network.\n\n" +
"In order to read and send mail from outside the laboratory, you will need to configure your external email client " +
"to send outgoing email through smail.arlut.utexas.edu using TLS-encrypted SMTP on port 25 or port 587, and to " +
"read incoming mail from mailboxes.arlut.utexas.edu via IMAP over SSL.\n\n" +
"You will need to specify the following randomly assigned user name and password for both services:\n\n" +
"Username: " + mailUsername + "\n" +
"Password: " + mailPassword;
if (isDefined(userSchema.MAILEXPDATE))
{
messageString = messageString + "\n\nThese credentials will expire on " + mailExpireDate.toString() +
". You will be assigned new credentials for your external mail access four weeks before these credentials expire.";
}
}
else
{
PasswordDBField oldMailPasswordField = originalObject.getPassField(userSchema.MAILPASSWORD2);
String oldPassword = oldMailPasswordField.getPlainText();
if (!mailUsername.equals(originalObject.getFieldValueLocal(userSchema.MAILUSER)) ||
!mailPassword.equals(oldPassword))
{
PasswordDBField myOldPasswordField = this.getPassField(userSchema.OLDMAILPASSWORD2);
String myOldPassword = myOldPasswordField != null ? myOldPasswordField.getPlainText(): "";
if (this.getGSession().getSessionName().equals("ExternalMailTask") && myOldPassword.equals(oldPassword))
{
// we're processing a credentials renewal by ExternalMailTask
titleString = "New Email Credentials Created For User " + this.getLabel();
messageString = "The current external mail credentials for user account " + this.getLabel() +
" are due to expire in four weeks.\n\n" +
"New external email credentials have been prepared for you:\n\n" +
"Username: " + mailUsername + "\n" +
"Password: " + mailPassword + "\n\n" +
"These credentials are now active on your account, and should be entered into any email client " +
"that you use outside of the laboratory's internal network.\n\n" +
"Your current external email credentials will continue to function for one month, after " +
"which time they will be removed from your account.";
}
else
{
titleString = "External Email Credentials Changed For User " + this.getLabel();
messageString = "The external mail credentials for user account " + this.getLabel() +
" have been changed.\n\n" +
"In order to continue to read and send mail from outside the laboratory, you will need to configure your external email client " +
"to send outgoing email through smail.arlut.utexas.edu using TLS-encrypted SMTP on port 25 or port 587, and to " +
"read incoming mail from mailboxes.arlut.utexas.edu via IMAP over SSL.\n\n" +
"You will need to specify the following randomly assigned user name and password for both services:\n\n" +
"Username: " + mailUsername + "\n" +
"Password: " + mailPassword;
if (isDefined(userSchema.MAILEXPDATE))
{
messageString = messageString + "\n\nThese credentials will expire on " + mailExpireDate.toString() +
". You will be assigned new credentials for your external mail access four weeks before these credentials expire.";
}
}
}
}
if (titleString != null)
{
Vector<Invid> objVect = new Vector<Invid>();
objVect.add(this.getInvid());
// We want to send to the user but not to the owners.
Ganymede.log.sendMail(null, titleString, messageString, DBLog.MailMode.USERS, objVect);
}
}
return retVal;
}
/**
* This private helper method goes through all embedded automounter
* map entries and updates their hidden labels, using the newName as
* the prefix.
*/
private ReturnVal renameEntries(String newName)
{
ReturnVal retVal = null;
InvidDBField volumeMapEntries = getInvidField(userSchema.VOLUMES);
Vector<Invid> values = (Vector<Invid>) volumeMapEntries.getValuesLocal();
for (Invid entryInvid: values)
{
DBEditObject eObj = getDBSession().editDBObject(entryInvid);
Invid mapInvid = (Invid) eObj.getFieldValueLocal(mapEntrySchema.MAP);
if (mapInvid == null)
{
return Ganymede.createErrorDialog(this.getGSession(),
null,
"Can't commit transaction with an empty automounter map definition in user " + this.getLabel());
}
DBObject mapObj = getDBSession().viewDBObject(mapInvid);
String mapName = mapObj.getLabel();
retVal = eObj.setFieldValueLocal(mapEntrySchema.XMLLABEL, newName + "/" + mapName);
if (retVal != null && !retVal.didSucceed())
{
return retVal;
}
}
return null;
}
/**
* <p>This method provides a hook that can be used to indicate whether
* a field that is defined in this object's field dictionary
* should be newly instantiated in this particular object.</p>
*
* <p>This method does not affect those fields which are actually present
* in a previously existing object's record in the
* {@link arlut.csd.ganymede.server.DBStore DBStore}. What this method allows
* you to do is have a subclass decide whether it wants to instantiate
* a potential field (one that is declared in the field dictionary for
* this object, but which doesn't happen to be presently defined in
* this object) in this particular object.</p>
*
* <p>A concrete example will help here. The Permissions Object type
* (base number SchemaConstants.PermBase) holds a permission
* matrix, a descriptive title, and a list of admin personae that hold
* those permissions for objects they own.</p>
*
* <p>There are a few specific instances of SchemaConstants.PermBase
* that don't properly need the list of admin personae, as their
* object invids are hard-coded into the Ganymede security system, and
* their permission matrices are automatically consulted in certain
* situations. In order to support this, we're going to want to have
* a DBEditObject subclass for managing permission objects. In that
* subclass, we'll define instantiateNewField() so that it will return
* false if the fieldID corresponds to the admin personae list if the
* object's ID is that of one of these special objects. As a result,
* when the objects are viewed by an administrator, the admin personae
* list will not be seen.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*/
@Override public boolean instantiateNewField(short fieldID)
{
if (fieldID == userSchema.PASSWORDCHANGETIME)
{
return true;
}
return super.instantiateNewField(fieldID);
}
/**
* <p>Customization method to verify whether the user should be able to
* see a specific field in a given object. Instances of
* {@link arlut.csd.ganymede.server.DBField DBField} will
* wind up calling up to here to let us override the normal visibility
* process.</p>
*
* <p>Note that it is permissible for session to be null, in which case
* this method will always return the default visiblity for the field
* in question.</p>
*
* <p>If field is not from an object of the same base as this DBEditObject,
* an exception will be thrown.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*
* <p><b>*PSEUDOSTATIC*</b></p>
*/
@Override public boolean canSeeField(DBSession dbSession, DBField field)
{
short fieldid = field.getID();
switch (fieldid)
{
case MAILUSER:
case MAILPASSWORD2:
case MAILEXPDATE:
return field.getObject().isSet(ALLOWEXTERNAL);
case OLDMAILUSER:
case OLDMAILPASSWORD2:
return false;
case EXCHANGESTORE:
String type = (String) field.getObject().getFieldValueLocal(EMAILACCOUNTTYPE);
if (type == null || !type.equals("Exchange"))
{
return false;
}
}
return super.canSeeField(dbSession, field);
}
/**
* <p>Controls whether change information for the field can be safely
* logged to the Ganymede log and emailed to the owner notification
* list for the field's containing object.</p>
*
* <p>If okToLogField() returns false for a field, the change to the
* field will be mentioned in email and the log, but the actual
* values will not be.</p>
*
* <p>With the exception of PasswordDBFields, you should consider any
* field for which okToLogField() returns true to be public data, as
* any user able to log into Ganymede may be able to see the data in
* the logs. PasswordDBFields contain special logic to prevent the
* data they contain from being revealed in logs or email.<p>
*
* <p><b>*PSEUDOSTATIC*</b></p>
*/
@Override public boolean okToLogField(DBField field)
{
short fieldid = field.getID();
switch (fieldid)
{
case MAILUSER:
case OLDMAILUSER:
return false;
}
// MAILPASSWORD2 and OLDMAILPASSWORD2 are password fields, and
// Ganymede will already keep that sensitive data from the logs
// and transaction email.
return super.okToLogField(field);
}
/**
* <p>Customization method to verify whether a specific field
* in object should be cloned using the basic field-clone
* logic.</p>
*
* <p>To be overridden in DBEditObject subclasses.</p>
*
* <p><b>*PSEUDOSTATIC*</b></p>
*/
@Override public boolean canCloneField(DBSession dbSession, DBObject object, DBField field)
{
short fieldid = field.getID();
switch (fieldid)
{
case USERNAME:
case UID:
case CATEGORY: // but see special logic in cloneFromObject()
case userSchema.PASSWORD:
case HOMEDIR:
case PERSONAE:
case ALIASES:
case SIGNATURE:
case EMAILTARGET:
case PASSWORDCHANGETIME:
case MAILUSER:
case MAILPASSWORD2:
case OLDMAILUSER:
case OLDMAILPASSWORD2:
return false;
}
// by default, all custom fields are cloneable, so this call will
// return true for all but the built-in fields.
return super.canCloneField(dbSession, object, field);
}
/**
* <p>Hook to allow the cloning of an object. If this object type
* supports cloning (which should be very much customized for this
* object type.. creation of the ancillary objects, which fields to
* clone, etc.), this customization method will actually do the
* work.</p>
*
* <p>This method is called on a newly created object, in order to
* clone the state of origObj into it. This method does not
* actually create a new object.. that is handled by {@link
* arlut.csd.ganymede.server.GanymedeSession#clone_db_object(arlut.csd.ganymede.common.Invid)
* clone_db_object()} before this method is called on the newly
* created object.</p>
*
* <p>The default (DBEditObject) implementation of this method will
* only clone fields for which {@link
* arlut.csd.ganymede.server.DBEditObject#canCloneField(arlut.csd.ganymede.server.DBSession,
* arlut.csd.ganymede.server.DBObject,
* arlut.csd.ganymede.server.DBField) canCloneField()} returns true,
* and which are not connected to a namespace (and thus could not
* possibly be cloned, because the values are constrained to be
* unique and non-duplicated).</p>
*
* <p>If one or more fields in the original object are unreadable by
* the cloning session, we will provide a list of fields that could
* not be cloned due to a lack of read permissions in a dialog in
* the ReturnVal. Such a problem will not result in a failure code
* being returned, however.. the clone will succeed, but an
* informative dialog will be provided to the user.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses, but
* this method's default logic will probably do what you need it to
* do. If you need to make changes, try to chain your subclassed
* method to this one via super.cloneFromObject().</p>
*
* @param session The DBSession that the new object is to be created in
* @param origObj The object we are cloning
* @param local If true, fields that have choice lists will not be checked against
* those choice lists and read permissions for each field will not be consulted.
* The canCloneField() method will still be consulted, however.
*
* @return A standard ReturnVal status object. May be null on success, or
* else may carry a dialog with information on problems and a success flag.
*/
@Override public ReturnVal cloneFromObject(DBSession dbSession, DBObject origObj, boolean local)
{
try
{
if (debug)
{
System.err.println("Attempting to clone User " + origObj.getLabel());
}
boolean problem = false;
ReturnVal tmpVal;
StringBuilder resultBuf = new StringBuilder();
// clone all of the fields that we don't inhibit in canCloneField().
ReturnVal retVal = super.cloneFromObject(dbSession, origObj, local);
if (retVal != null && retVal.getDialog() != null)
{
resultBuf.append(retVal.getDialog().getText());
problem = true;
}
// We have the default canCloneField() refuse to clone
// userSchema.CATEGORY to avoid dealing or bypassing with the
// wizard. If we are cloning a normal user, it is safe enough
// to copy that value. Else we'll leave it blank for the user
// to set.
Invid category = (Invid) origObj.getFieldValueLocal(userSchema.CATEGORY);
if (dbSession.getObjectLabel(category).equals("normal"))
{
try
{
getField(userSchema.CATEGORY).setValue(category, local, true);
}
catch (GanyPermissionsException ex)
{
return Ganymede.createErrorDialog(dbSession.getGSession(),
"permissions",
"permissions error setting category" + ex);
}
}
if (debug)
{
System.err.println("User " + origObj.getLabel() + " cloned, working on embeddeds");
}
// and clone the embedded objects
InvidDBField newVolumes = getInvidField(userSchema.VOLUMES);
InvidDBField oldVolumes = origObj.getInvidField(userSchema.VOLUMES);
Vector<Invid> newOnes = (Vector<Invid>) newVolumes.getValuesLocal();
Vector<Invid> oldOnes = (Vector<Invid>) oldVolumes.getValuesLocal();
DBObject origVolume;
DBEditObject workingVolume;
int i;
for (i = 0; i < newOnes.size(); i++)
{
if (debug)
{
System.err.println("User clone sub " + i);
}
workingVolume = (DBEditObject) dbSession.editDBObject(newOnes.get(i));
origVolume = dbSession.viewDBObject(oldOnes.get(i));
if (debug)
{
System.err.println("Attempting to clone user volume " + origVolume.getLabel());
}
tmpVal = workingVolume.cloneFromObject(dbSession, origVolume, local);
if (tmpVal != null && tmpVal.getDialog() != null)
{
if (resultBuf.length() != 0)
{
resultBuf.append("\n\n");
}
resultBuf.append(tmpVal.getDialog().getText());
problem = true;
}
}
Invid newInvid;
if (i < oldOnes.size())
{
for (; i < oldOnes.size(); i++)
{
if (debug)
{
System.err.println("User clone sub sub " + i);
}
try
{
tmpVal = newVolumes.createNewEmbedded(local);
}
catch (GanyPermissionsException ex)
{
tmpVal = Ganymede.createErrorDialog(dbSession.getGSession(),
"permissions",
"permissions error creating embedded object during user cloning" + ex);
}
if (!tmpVal.didSucceed())
{
if (debug)
{
System.err.println("User clone couldn't allocate new embedded");
}
if (tmpVal != null && tmpVal.getDialog() != null)
{
if (resultBuf.length() != 0)
{
resultBuf.append("\n\n");
}
resultBuf.append(tmpVal.getDialog().getText());
problem = true;
}
continue;
}
newInvid = tmpVal.getInvid();
workingVolume = (DBEditObject) dbSession.editDBObject(newInvid);
origVolume = dbSession.viewDBObject(oldOnes.get(i));
if (debug)
{
System.err.println("Attempting to clone user volume " + origVolume.getLabel());
}
tmpVal = workingVolume.cloneFromObject(dbSession, origVolume, local);
if (tmpVal != null && tmpVal.getDialog() != null)
{
if (resultBuf.length() != 0)
{
resultBuf.append("\n\n");
}
resultBuf.append(tmpVal.getDialog().getText());
problem = true;
}
}
}
retVal = new ReturnVal(true, !problem);
if (problem)
{
retVal.setDialog(new JDialogBuff("Possible Clone Problems", resultBuf.toString(),
"Ok", null, "ok.gif"));
}
return retVal;
}
catch (NotLoggedInException ex)
{
return Ganymede.loginError(ex);
}
}
/**
* <p>This method provides a hook to allow custom DBEditObject subclasses to
* indicate that the given object is interested in receiving notification
* when changes involving it occur, and can provide one or more addresses for
* such notification to go to.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*
* <p><b>*PSEUDOSTATIC*</b></p>
*/
@Override public boolean hasEmailTarget(DBObject object)
{
return true;
}
/**
* <p>This method provides a hook to allow custom DBEditObject subclasses to
* return a List of Strings comprising a list of addresses to be
* notified above and beyond the normal owner group notification when
* the given object is changed in a transaction. Used for letting end-users
* be notified of changes to their account, etc.</p>
*
* <p>If no email targets are present in this object, either a null value
* or an empty List may be returned.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*
* <p><b>*PSEUDOSTATIC*</b></p>
*/
@Override public List<String> getEmailTargets(DBObject object)
{
// don't tell this user's email address if this user is in the process
// of being created. this will avoid causing email to be sent to
// the newly created account, which would likely bounce at this point
// in time
if (object instanceof DBEditObject)
{
if (((DBEditObject) object).getStatus() == ObjectStatus.CREATING)
{
return null;
}
}
Vector<String> targets = new Vector<String>();
targets.add(object.getLabel()); // let our mail system handle routing.
return targets;
}
/**
* <p>This method provides a hook to allow custom DBEditObject
* subclasses to return a String containing a URL for an image to
* represent this object. Intended to be used for users, primarily.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*
* <p><b>*PSEUDOSTATIC*</b></p>
*/
@Override public String getImageURLForObject(DBObject object)
{
String badge = (String) object.getFieldValueLocal(userSchema.BADGE);
if (badge == null || badge.trim().equals(""))
{
return null;
}
return "http://csdsun9.arlut.utexas.edu/pictures/" + badge + ".jpg";
}
/**
* <p>This method provides a hook to allow custom DBEditObject
* subclasses to react to forthcoming object removal.</p>
*
* <p>This method will be called without benefit of an open DBEditSet,
* so any email generated will need to make use of the
* non-transactional mail methods in the Ganymede.log object.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*
* <p><b>*PSEUDOSTATIC*</b></p>
*
* @return true if the DBEditObject subclass wishes to completely
* handle the warning, or false if the default warning transmisssion
* logic should also be sent.
*/
@Override public boolean reactToRemovalWarning(DBObject object, int days)
{
StringDBField deliveryAddresses = null;
Vector<String> values = null;
deliveryAddresses = object.getStringField(userSchema.EMAILTARGET);
if (deliveryAddresses == null)
{
Ganymede.debug("Missing email target for user " + object.getLabel());
return false;
}
values = (Vector<String>) deliveryAddresses.getValuesLocal();
for (String x: values)
{
if (x.endsWith("@arlex.arlut.utexas.edu"))
{
Vector<String> toAddresses = new Vector<String>();
toAddresses.add("pcshelp@arlut.utexas.edu");
toAddresses.add("broccol@arlut.utexas.edu");
Ganymede.log.sendMail(toAddresses,
"Exchange User " + object.getLabel() + " Scheduled for Deletion",
"User " + object.getLabel() +
" is scheduled to be deleted from Ganymede in " + days +
" days.\n\nPCS will need to be prepared for clearing the account out of the Exchange server.\n");
return false;
}
}
return false;
}
/**
* <p>This method is used to control whether or not it is acceptable to
* make a link to the given field in this
* {@link arlut.csd.ganymede.server.DBObject DBObject} type when the
* user only has editing access for the source
* {@link arlut.csd.ganymede.server.InvidDBField InvidDBField} and not
* the target.</p>
*
* <p>This version of anonymousLinkOK takes additional parameters
* to allow an object type to decide that it does or does not want
* to allow a link based on what field of what object wants to link
* to it.</p>
*
* <p>By default, the 3 variants of the DBEditObject
* anonymousLinkOK() method are chained together, so that the
* customizer can choose which level of detail he is interested in.
* {@link arlut.csd.ganymede.server.InvidDBField InvidDBField}'s
* {@link
* arlut.csd.ganymede.server.InvidDBField#bind(arlut.csd.ganymede.common.Invid,arlut.csd.ganymede.common.Invid,boolean)
* bind()} method calls this version. This version calls the three
* parameter version, which calls the two parameter version, which
* returns false by default. Customizers can implement any of the
* three versions, but unless you maintain the version chaining
* yourself, there's no point to implementing more than one of
* them.</p>
*
* <p>Note that the {@link
* arlut.csd.ganymede.server.DBEditObject#choiceListHasExceptions(arlut.csd.ganymede.server.DBField)
* choiceListHasExceptions()} method will call this version of
* anonymousLinkOK() with a null targetObject, to determine that the
* client should not use its cache for an InvidDBField's choices.
* Any overriding done of this method must be able to handle a null
* targetObject, or else an exception will be thrown
* inappropriately.</p>
*
* <p>The only reason to consult targetObject in any case is to
* allow or disallow anonymous object linking to a field based on
* the current state of the target object. If you are just writing
* generic anonymous linking rules for a field in this object type,
* targetObject won't concern you anyway. If you do care about the
* targetObject's state, though, you have to be prepared to handle
* a null valued targetObject.</p>
*
* <p><b>*PSEUDOSTATIC*</b></p>
*
* @param targetObject The object that the link is to be created in (may be null)
* @param targetFieldID The field that the link is to be created in
* @param sourceObject The object on the other side of the proposed link
* @param sourceFieldID The field on the other side of the proposed link
* @param gsession Who is trying to do this linking?
*/
@Override public boolean anonymousLinkOK(DBObject targetObject, short targetFieldID,
DBObject sourceObject, short sourceFieldID,
GanymedeSession gsession)
{
// if they can edit the group, they can put us in it.. the
// gasharl schema specifies the mandatory type for the other
// end of the GROUPLIST field's link, so we don't have to
// check that here
if (targetFieldID == userSchema.GROUPLIST)
{
return true;
}
// go ahead and allow the same for netgroups
if (targetFieldID == userSchema.NETGROUPS)
{
return true;
}
// if someone tries to put this user in an email list, let them.
if ((targetFieldID == SchemaConstants.BackLinksField) &&
(sourceObject.getTypeID() == 274) && // email list
(sourceFieldID == 257)) // email list members
{
return true;
}
// the default anonymousLinkOK() method returns false
return super.anonymousLinkOK(targetObject, targetFieldID,
sourceObject, sourceFieldID, gsession);
}
/**
* <p>Customization method to control whether a specified field
* is required to be defined at commit time for a given object.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*
* <p>Note that this method will not be called if the controlling
* GanymedeSession's enableOversight is turned off, as in
* bulk loading.</p>
*
* <p>Note as well that the designated label field for objects are
* always required, whatever this method returns, and that this
* requirement holds without regard to the GanymedeSession's
* enableOversight value.</p>
*
* <p><b>*PSEUDOSTATIC*</b></p>
*/
@Override public boolean fieldRequired(DBObject object, short fieldid)
{
switch (fieldid)
{
case userSchema.USERNAME:
case userSchema.UID:
case userSchema.LOGINSHELL:
case userSchema.HOMEDIR:
case userSchema.VOLUMES:
case userSchema.CATEGORY:
case userSchema.HOMEGROUP:
case userSchema.SIGNATURE:
case userSchema.EMAILTARGET:
return true;
// the following fields are only necessary if the account has
// not been inactivated
case userSchema.PASSWORD:
case userSchema.PASSWORDCHANGETIME:
return !object.isInactivated();
case userSchema.MAILUSER:
case userSchema.MAILPASSWORD2:
case userSchema.MAILEXPDATE:
return object.isSet(ALLOWEXTERNAL);
case userSchema.EXCHANGESTORE:
return StringUtils.stringEquals((String) object.getFieldValueLocal(EMAILACCOUNTTYPE), "Exchange");
}
// Whether or not the Badge number field is required depends on
// the user category.
if (fieldid == userSchema.BADGE)
{
boolean needIdentifier = false;
try
{
Invid catInvid = (Invid) object.getFieldValueLocal(userSchema.CATEGORY);
// we're PSEUDOSTATIC, so we need to get ahold of the internal session
// so we can look up objects
DBObject category = internalSession().getDBSession().viewDBObject(catInvid);
needIdentifier = category.isSet(userCategorySchema.SSREQUIRED);
}
catch (NullPointerException ex)
{
// if we can't get the category reference, assume that we
// aren't gonna require the category.. the user will still
// be prompted to set a category, and once they go back
// and do that and try to re-commit, they'll hit us again
// and we can make the proper determination at that point.
return false;
}
return needIdentifier;
}
return false;
}
/**
* <p>Customization method to verify overall consistency of a
* DBObject. This method is intended to be overridden in
* DBEditObject subclasses, and will be called by {@link
* arlut.csd.ganymede.server.DBEditObject#commitPhase1()
* commitPhase1()} to verify the readiness of this object for
* commit. The DBObject passed to this method will be a
* DBEditObject, complete with that object's GanymedeSession
* reference if this method is called during transaction commit, and
* that session reference may be used by the verifying code if the
* code needs to access the database.</p>
*
* <p>This method is for custom checks specific to custom
* DBEditObject subclasses. Standard checking for missing fields
* for which fieldRequired() returns true is done by {@link
* arlut.csd.ganymede.server.DBEditSet#commit_checkObjectMissingFields(arlut.csd.ganymede.server.DBEditObject)}
* during {@link
* arlut.csd.ganymede.server.DBEditSet#commit_handlePhase1()}.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*
* <p><b>*PSEUDOSTATIC*</b></p>
*
* @return A ReturnVal indicating success or failure. May
* be simply 'null' to indicate success if no feedback need
* be provided.
*/
@Override public ReturnVal consistencyCheck(DBObject object)
{
GanymedeSession gSession = null;
DBObject categoryObj = null;
String categoryName = null;
/* -- */
Invid category = (Invid) object.getFieldValueLocal(userSchema.CATEGORY);
if (category == null)
{
return null;
}
if (object instanceof DBEditObject)
{
gSession = object.getGSession();
}
else
{
gSession = Ganymede.getInternalSession();
}
categoryObj = lookupInvid(category, false);
if (categoryObj == null)
{
// shouldn't happen, but if it does we'll assume something
// else will catch this
return null;
}
categoryName = categoryObj.getLabel();
if (categoryObj.isSet(userCategorySchema.EXPIRE))
{
if (!object.isDefined(SchemaConstants.ExpirationField) &&
!object.isDefined(SchemaConstants.RemovalField))
{
return Ganymede.createErrorDialog(object.getGSession(),
"Missing Expiration Field",
"User objects belonging to the " + categoryName +
" category require an expiration date to be set.");
}
}
// now let's make sure the signature alias is valid
String signature = (String) object.getFieldValueLocal(SIGNATURE);
String myUsername = (String) object.getLabel();
Vector<String> aliases = (Vector<String>) object.getFieldValuesLocal(ALIASES);
if (!StringUtils.stringEquals(signature, myUsername) &&
!aliases.contains(signature))
{
return Ganymede.createErrorDialog(object.getGSession(),
"Bad Signature Alias",
"Ganymede server configuration error. The signature alias (" + signature + ") for this user is " +
"not a valid choice.");
}
// and the home group as well
Invid homeGroupInvid = (Invid) object.getFieldValueLocal(HOMEGROUP);
Vector<Invid> myGroups = (Vector<Invid>) object.getFieldValuesLocal(GROUPLIST);
if (!myGroups.contains(homeGroupInvid))
{
DBObject homeGroupObj = object.lookupInvid(homeGroupInvid, false);
if (homeGroupObj != null)
{
return Ganymede.createErrorDialog(object.getGSession(),
"Bad Home Group",
"Ganymede server configuration error. The home group (" +
homeGroupObj.getLabel() + ") for this user is " +
"not a valid choice.");
}
else
{
return Ganymede.createErrorDialog(object.getGSession(),
"Bad Home Group",
"Ganymede server configuration error. The home group " +
"for this user does not point to a valid object.");
}
}
else if (myGroups.size() == 0)
{
return Ganymede.createErrorDialog(object.getGSession(),
"Missing Groups",
"This user is not a member of any groups.");
}
// and make sure that the badge number is unique, if we're a
// normal account and we don't have any attached admin personae.
if (object.isDefined(BADGE))
{
Vector<Invid> personaeList = (Vector<Invid>) object.getFieldValuesLocal(PERSONAE);
if (personaeList.size() == 0 && categoryName.equals("normal"))
{
String badge = (String) object.getFieldValueLocal(BADGE);
QueryResult qr = null;
try
{
qr = gSession.query("select object from 'User' where 'Badge' == '" + StringUtils.escape(badge) +
"' and (not 'Username' == '" + StringUtils.escape(myUsername) +
"') and ('Account Category' == 'normal') and (not 'Removal Date' defined)");
}
catch (NotLoggedInException ex)
{
throw new RuntimeException("Error in userObject.consistencyCheck(): query threw a NotLoggedInException.", ex);
}
catch (GanyParseException ex)
{
throw new RuntimeException("Error in userObject.consistencyCheck(): query could not be parsed correctly.", ex);
}
if (qr != null && qr.size() != 0)
{
boolean badge_is_admin = false;
String conflict_name = null;
for (int i = 0; !badge_is_admin && i < qr.size(); i++)
{
Invid matchInvid = qr.getInvid(i);
DBObject conflictUserObject = lookupInvid(matchInvid, false);
Vector<Invid> personae = (Vector<Invid>) conflictUserObject.getFieldValuesLocal(PERSONAE);
if (personae.size() > 0)
{
badge_is_admin = true;
}
else
{
conflict_name = conflictUserObject.getLabel();
}
}
if (!badge_is_admin)
{
return Ganymede.createErrorDialog(object.getGSession(),
"Duplicate Badge Number",
"This user object shares a badge number with the " + conflict_name + " user object.\n\n" +
"Since both of these user accounts are 'normal' accounts, Ganymede can't tell which one should " +
"be the account of record for information transfer to the HR database.");
}
}
}
}
return null;
}
/**
* <p>Customization method to verify whether this object type has an inactivation
* mechanism.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*
* <p><b>*PSEUDOSTATIC*</b></p>
*/
@Override public boolean canBeInactivated()
{
return true;
}
/**
* <p>Customization method to verify whether the user has permission
* to inactivate a given object. The client's
* {@link arlut.csd.ganymede.server.DBSession DBSession} object
* will call this per-class method to do an object type-
* sensitive check to see if this object feels like being
* available for inactivating by the client.</p>
*
* <p>Note that unlike canRemove(), canInactivate() takes a
* DBEditObject instead of a DBObject. This is because inactivating
* an object is based on editing the object, and so we have the
* GanymedeSession/DBSession classes go ahead and check the object
* out for editing before calling us. This serves to force the
* session classes to check for write permission before attempting
* inactivation.</p>
*
* <p>Use canBeInactivated() to test for the presence of an inactivation
* protocol outside of an edit context if needed.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*
* <p><b>*PSEUDOSTATIC*</b></p>
*/
@Override public boolean canInactivate(DBSession dbSession, DBEditObject object)
{
return true;
}
/**
* <p>This method handles inactivation logic for this object type.
* A DBEditObject must first be checked out for editing, then the
* inactivate() method can then be called on the object to put the
* object into inactive mode. inactivate() will set the object's
* removal date and fix up any other state information to reflect
* the object's inactive status.</p>
*
* <p>inactive() is designed to run synchronously with the user's
* request for inactivation. It can return a wizard reference in
* the ReturnVal object returned, to guide the user through a set of
* interactive dialogs to inactive the object.</p>
*
* <p>The inactive() method can cause other objects to be deleted,
* can cause strings to be removed from fields in other objects,
* whatever.</p>
*
* <p>If inactivate() returns a ReturnVal that has its success flag
* set to false and does not include a JDialogBuff for further
* interaction with the user, then DBSEssion.inactivateDBObject()
* method will rollback any changes made by this method.</p>
*
* <p>If inactivate() returns a success value, we expect that the
* object will have a removal date set.</p>
*
* <p>IMPORTANT NOTE 1: This method is intended to be called by the
* DBSession.inactivateDBObject() method, which establishes a
* checkpoint before calling inactivate. If this method is not
* called by DBSession.inactivateDBObject(), you need to push a
* checkpoint with the key 'inactivate'+label, where label is the
* returned name of this object.</p>
*
* <p>IMPORTANT NOTE 2: If a custom object's inactivate() logic
* decides to enter into a wizard interaction with the user, that
* logic is responsible for calling finalizeInactivate() with a
* boolean indicating ultimate success of the operation.</p>
*
* <p>Finally, it is up to commitPhase1() and commitPhase2() to
* handle any external actions related to object inactivation when
* the transaction is committed..</p>
*
* @see #commitPhase1()
* @see #commitPhase2()
*
* @param ckp_label The checkpoint label which should be popped or
* rolledback on necessity by the custom inactivate method.
*
* @return A ReturnVal indicating success or failure. May
* be simply 'null' to indicate success if no feedback need
* be provided.
*/
@Override public ReturnVal inactivate(String ckp_label)
{
return inactivate(null, false, ckp_label);
}
public ReturnVal inactivate(String forward, boolean calledByWizard, String ckp_label)
{
ReturnVal retVal;
StringDBField stringfield;
PasswordDBField passfield;
DateDBField date;
Calendar cal = Calendar.getInstance();
Date time;
/* -- */
if (!gSession.enableWizards || calledByWizard)
{
// ok, we want to null the password field and set the
// removal time to current time + 3 months.
passfield = getPassField(userSchema.PASSWORD);
retVal = passfield.setCryptPass(null); // we know our schema uses crypted pass'es
if (retVal != null && !retVal.didSucceed())
{
if (calledByWizard)
{
finalizeInactivate(false, ckp_label);
}
return retVal;
}
// we're not going to set the shell to /bin/false
// anymore.. we'll depend on our builder task to write it out
// as /bin/false for us.
if (false)
{
// set the shell to /bin/false
stringfield = getStringField(LOGINSHELL);
retVal = stringfield.setValueLocal("/bin/false");
if (retVal != null && !retVal.didSucceed())
{
if (calledByWizard)
{
finalizeInactivate(false, ckp_label);
}
return retVal;
}
}
// reset the forwarding address?
if (forward != null && !forward.equals(""))
{
stringfield = getStringField(EMAILTARGET);
while (stringfield.size() > 0)
{
retVal = stringfield.deleteElementLocal(0);
if (retVal != null && !retVal.didSucceed())
{
if (calledByWizard)
{
finalizeInactivate(false, ckp_label);
}
return retVal;
}
}
stringfield.addElementLocal(forward);
}
// determine what will be the date 3 months from now
time = new Date();
cal.setTime(time);
cal.add(Calendar.MONTH, 3);
// and set the removal date
date = getDateField(SchemaConstants.RemovalField);
retVal = date.setValueLocal(cal.getTime());
if (retVal != null && !retVal.didSucceed())
{
if (calledByWizard)
{
finalizeInactivate(false, ckp_label);
}
return retVal;
}
// make sure that the expiration date is cleared.. we're on
// the removal track now.
date = getDateField(SchemaConstants.ExpirationField);
retVal = date.setValueLocal(null);
if (retVal != null && !retVal.didSucceed())
{
if (calledByWizard)
{
finalizeInactivate(false, ckp_label);
}
return retVal;
}
// success, have our DBEditObject superclass clean up.
if (calledByWizard)
{
finalizeInactivate(true, ckp_label);
}
// ok, we succeeded, now we have to tell the client
// what to refresh to see the inactivation results
ReturnVal result = ReturnVal.success();
result.addRescanField(this.getInvid(), SchemaConstants.RemovalField);
result.addRescanField(this.getInvid(), userSchema.LOGINSHELL);
result.addRescanField(this.getInvid(), userSchema.EMAILTARGET);
return result;
}
else // interactive, but not called by wizard.. return a wizard
{
userInactivateWizard theWiz;
try
{
if (debug)
{
System.err.println("userCustom: creating inactivation wizard");
}
theWiz = new userInactivateWizard(this.gSession, this, ckp_label);
}
catch (RemoteException ex)
{
throw new RuntimeException("oops, userCustom couldn't create wizard for remote ex " + ex);
}
if (debug)
{
System.err.println("userCustom: returning inactivation wizard");
}
return theWiz.respond(null);
}
}
/**
* <p>This method handles reactivation logic for this object type.
* A DBEditObject must first be checked out for editing, then the
* reactivate() method can then be called on the object to make the
* object active again. reactivate() will clear the object's
* removal date and fix up any other state information to reflect
* the object's reactive status.</p>
*
* <p>reactive() is designed to run synchronously with the user's
* request for inactivation. It can return a wizard reference in
* the ReturnVal object returned, to guide the user through a set of
* interactive dialogs to reactive the object.</p>
*
* <p>If reactivate() returns a ReturnVal that has its success flag
* set to false and does not include a {@link
* arlut.csd.JDialog.JDialogBuff JDialogBuff} for further
* interaction with the user, then {@link
* arlut.csd.ganymede.server.DBSession#inactivateDBObject(arlut.csd.ganymede.server.DBEditObject)
* inactivateDBObject()} method will rollback any changes made by
* this method.</p>
*
* <p>IMPORTANT NOTE: If a custom object's inactivate() logic
* decides to enter into a wizard interaction with the user, that
* logic is responsible for calling finalizeInactivate() with a
* boolean indicating ultimate success of the operation.</p>
*
* <p>Finally, it is up to commitPhase1() and commitPhase2() to
* handle any external actions related to object reactivation when
* the transaction is committed..</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*
* @see arlut.csd.ganymede.server.DBEditObject#commitPhase1()
* @see arlut.csd.ganymede.server.DBEditObject#commitPhase2()
*
* @return A ReturnVal indicating success or failure. May
* be simply 'null' to indicate success if no feedback need
* be provided.
*/
@Override public ReturnVal reactivate(String ckp_label)
{
userReactivateWizard theWiz;
/* -- */
try
{
if (debug)
{
System.err.println("userCustom: creating reactivation wizard");
}
theWiz = new userReactivateWizard(this.gSession, this, ckp_label);
}
catch (RemoteException ex)
{
throw new RuntimeException("oops, userCustom couldn't create wizard for remote ex " + ex);
}
if (debug)
{
System.err.println("userCustom: returning reactivation wizard");
}
return theWiz.respond(null);
}
/**
* <p>This method is called by the userReactivateWizard on
* successfully obtaining the necessary information from the client
* on a reactivate operation. We then do the actual work to
* reactivate the user in this method.</p>
*
* @see arlut.csd.ganymede.gasharl.userReactivateWizard
*/
public ReturnVal reactivate(userReactivateWizard reactivateWizard, String ckp_label)
{
ReturnVal retVal = null;
StringDBField stringfield;
PasswordDBField passfield;
DateDBField date;
boolean success = false;
/* -- */
if (reactivateWizard == null)
{
return Ganymede.createErrorDialog(this.getGSession(),
"userCustom.reactivate() error",
"Error, reactivate() called without a valid user wizard");
}
try
{
// reset the password
if (reactivateWizard.password != null && reactivateWizard.password.length() != 0)
{
passfield = getPassField(userSchema.PASSWORD);
retVal = passfield.setPlainTextPass(reactivateWizard.password);
if (retVal != null && !retVal.didSucceed())
{
return retVal;
}
}
else
{
return Ganymede.createErrorDialog(this.getGSession(),
"userCustom.reactivate() error",
"Error, reactivate() called without a password selected");
}
// reset the shell
if (reactivateWizard.shell != null)
{
stringfield = getStringField(LOGINSHELL);
try
{
retVal = stringfield.setValue(reactivateWizard.shell);
}
catch (GanyPermissionsException ex)
{
return Ganymede.createErrorDialog(this.getGSession(),
"permissions",
"permissions error setting shell during reactivation" + ex);
}
if (retVal != null && !retVal.didSucceed())
{
return retVal;
}
}
// reset the forwarding address
if (reactivateWizard.forward != null && !reactivateWizard.forward.equals(""))
{
stringfield = getStringField(EMAILTARGET);
while (stringfield.size() > 0)
{
retVal = stringfield.deleteElementLocal(0);
if (retVal != null && !retVal.didSucceed())
{
return retVal;
}
}
String[] strings = arlut.csd.Util.StringUtils.split(reactivateWizard.forward, ",");
for (int i = 0; i < strings.length; i++)
{
stringfield.addElementLocal(strings[i]);
}
}
// make sure that the removal date is cleared..
date = getDateField(SchemaConstants.RemovalField);
retVal = date.setValueLocal(null);
if (retVal != null && !retVal.didSucceed())
{
return retVal;
}
finalizeReactivate(true, ckp_label);
success = true;
// ok, we succeeded, now we have to tell the client
// what to refresh to see the reactivation results
ReturnVal result = ReturnVal.success();
result.addRescanField(this.getInvid(), SchemaConstants.RemovalField);
result.addRescanField(this.getInvid(), userSchema.LOGINSHELL);
result.addRescanField(this.getInvid(), userSchema.EMAILTARGET);
result.addRescanField(this.getInvid(), userSchema.PASSWORDCHANGETIME);
return result;
}
finally
{
if (!success)
{
finalizeReactivate(false, ckp_label);
}
}
}
/**
* <p>This method handles removal logic for this object type. This method
* will be called immediately from DBSession.deleteDBObject().</p>
*
* <p>The remove() method can cause other objects to be deleted, can cause
* strings to be removed from fields in other objects, whatever.</p>
*
* <p>If remove() returns a ReturnVal that has its success flag set to false
* and does not include a JDialogBuff for further interaction with the
* user, the DBSession.deleteDBObject() method will roll back any changes
* made by this method.</p>
*
* <p>remove() is intended for subclassing, whereas finalizeRemove() is
* not. finalizeRemove() provides the standard logic for wiping out
* fields and what not to cause the object to be unlinked from
* other objects.</p>
*
* <p>IMPORTANT NOTE: If a custom object's remove() logic decides to
* enter into a wizard interaction with the user, that logic is
* responsible for calling finalizeRemove() on the object when
* it is determined that the object really should be removed,
* with a boolean indicating whether success was had.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*
* @return A ReturnVal indicating success or failure. May
* be simply 'null' to indicate success if no feedback need
* be provided.
*/
@Override public ReturnVal remove()
{
StringDBField deliveryAddresses = this.getStringField(userSchema.EMAILTARGET);
Vector<String> values = (Vector<String>) deliveryAddresses.getValuesLocal();
for (String x: values)
{
if (x.endsWith("@arlex.arlut.utexas.edu"))
{
Vector<String> toAddresses = new Vector<String>();
toAddresses.add("pcshelp@arlut.utexas.edu");
toAddresses.add("broccol@arlut.utexas.edu");
this.getEditSet().logMail(toAddresses,
"Exchange User " + this.getLabel() + " Deleted",
"User " + this.getLabel() +
" has been deleted from Ganymede, and will need to be cleared out of the Exchange server.\n");
return null;
}
}
return null;
}
/**
* <p>This method returns a key that can be used by the client
* to cache the value returned by choices(). If the client
* already has the key cached on the client side, it
* can provide the choice list from its cache rather than
* calling choices() on this object again.</p>
*
* <p>The default logic in this method is designed to cause the client
* to cache choice lists for invid fields in the 'all objects of
* invid target type' cache bucket. If your InvidDBField needs to
* provide a restricted subset of objects of the targeted type as
* the choice list, you'll need to override this method to either
* return null (to turn off choice list caching), or generate some
* kind of unique key that won't collide with the Short objects used
* to represent the default object list caches.</p>
*
* <p>See also the {@link
* arlut.csd.ganymede.server.DBEditObject#choiceListHasExceptions(arlut.csd.ganymede.server.DBField)}
* hook, which controls whether or not the default logic will
* encourage the client to cache a given InvidDBField's choice list.</p>
*
* <p>If there is no caching key, this method will return null.</p>
*
* <p>We don't want the HOMEGROUP field's choice list to be cached on
* the client because it is dynamically generated for this
* context, and doesn't make sense in other contexts.</p>
*/
@Override public Object obtainChoicesKey(DBField field)
{
if (field.getID() == HOMEGROUP)
{
return null;
}
else
{
return super.obtainChoicesKey(field);
}
}
/**
* <p>This method provides a hook that a DBEditObject subclass
* can use to indicate whether a given field can only
* choose from a choice provided by obtainChoiceList()</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses,
* particularly if you have a StringDBField that you want to force
* to pick from the list of choices provided by your DBEditObject
* subclass' obtainChoiceList() method.</p>
*/
@Override public boolean mustChoose(DBField field)
{
switch (field.getID())
{
case SIGNATURE:
// we want to force signature alias choosing
case EMAILACCOUNTTYPE:
return true;
}
return super.mustChoose(field);
}
/**
* <p>This method provides a hook that can be used to generate
* choice lists for invid and string fields that provide
* such. String and Invid DBFields will call their owner's
* obtainChoiceList() method to get a list of valid choices.</p>
*
* <p>This method will provide a reasonable default for targetted
* invid fields, filtered by the GanymedeSession's
* visibilityFilterInvids list.</p>
*
* <p>NOTE: This method does not need to be synchronized. Making this
* synchronized can lead to DBEditObject/DBSession nested monitor
* deadlocks.</p>
*
* <p>Notice that fields 263 (login shell) and 268 (signature alias)
* do not have their choice lists cached on the client, because
* they are custom generated without any kind of accompanying
* cache key.</p>
*/
@Override public QueryResult obtainChoiceList(DBField field) throws NotLoggedInException
{
switch (field.getID())
{
case LOGINSHELL: // login shell
updateShellChoiceList();
if (debug)
{
System.err.println("userCustom: obtainChoice returning " + shellChoices + " for shell field.");
}
return shellChoices;
case HOMEGROUP: // home group
updateGroupChoiceList();
return groupChoices;
case EMAILACCOUNTTYPE:
QueryResult typeResult = new QueryResult();
typeResult.addRow("IMAP");
typeResult.addRow("Exchange");
typeResult.addRow("Other");
return typeResult;
case SIGNATURE: // signature alias
QueryResult result = new QueryResult();
/* -- */
// our list of possible aliases includes the user's name
// note that we first check the new value, if any, for the
// user name.. this way the user rename code can change the
// signature alias without having the StringDBField for the
// signature alias reject the new name.
String name = newUsername;
if (name != null)
{
result.addRow(null, name, false);
}
else
{
name = (String) getField(USERNAME).getValueLocal();
if (name != null)
{
result.addRow(null, name, false);
}
}
// and any aliases defined
Vector<String> values = (Vector<String>) getField(ALIASES).getValuesLocal();
for (String str: values)
{
result.addRow(null, str, false);
}
return result;
default:
return super.obtainChoiceList(field);
}
}
/**
* We update the groupChoices list to contain all of the groups
* the user is currently in.
*/
void updateGroupChoiceList()
{
if (groupChoices == null)
{
groupChoices = new QueryResult();
Vector<Invid> invids = (Vector<Invid>) getFieldValuesLocal(GROUPLIST); // groups list
for (Invid invid: invids)
{
// must be editable because the client cares
groupChoices.addRow(invid, gSession.getDBSession().getObjectLabel(invid), true);
}
}
}
void updateShellChoiceList()
{
synchronized (shellChoices)
{
DBObjectBase base = Ganymede.db.getObjectBase("Shell Choice");
// just go ahead and throw the null pointer if we didn't get our base.
if (shellChoiceStamp == null || base.changedSince(shellChoiceStamp))
{
if (debug)
{
System.err.println("userCustom - updateShellChoiceList()");
}
shellChoices = new QueryResult();
Query query = new Query("Shell Choice", null, false);
// internalQuery doesn't care if the query has its filtered bit set
if (debug)
{
System.err.println("userCustom - issuing query");
}
Vector<Result> results = internalSession().internalQuery(query);
if (debug)
{
System.err.println("userCustom - processing query results");
}
for (Result result: results)
{
shellChoices.addRow(null, result.toString(), false); // no invid
}
if (shellChoiceStamp == null)
{
shellChoiceStamp = new Date();
}
else
{
shellChoiceStamp.setTime(System.currentTimeMillis());
}
}
}
}
/**
* <p>Customization method to allow this Ganymede object type to
* override the default permissions mechanism for special
* purposes.</p>
*
* <p>If this method returns null, the default permissions mechanism
* will be followed. If not, the permissions system will grant the
* permissions specified by this method for access to the given
* field, and no further elaboration of the permission will be
* performed. If permOverride() returns a non-null value for a
* given field, permExpand() will not be consulted for that field.
* Just as with permExpand(), this method can never cause greater
* permissions to be granted to a field than is available to the
* object as a whole, and this override capability does not
* apply to operations performed in supergash mode.</p>
*
* <p>This method should be used very sparingly.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*
* <p><b>*PSEUDOSTATIC*</b></p>
*/
@Override public PermEntry permOverride(GanymedeSession session, DBObject object, short fieldid)
{
if (fieldid != UID)
{
return null;
}
// we don't want to allow anyone other than supergash to change our
// uid once it is set.
if (object.isDefined(UID))
{
return PermEntry.getPermEntry(true, false, true, false);
}
else
{
return null;
}
}
/**
* <p>This method provides a hook that a DBEditObject subclass
* can use to indicate that a given
* {@link arlut.csd.ganymede.server.DateDBField DateDBField} has a restricted
* range of possibilities.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*/
@Override public boolean isDateLimited(DBField field)
{
if (field.getID() == SchemaConstants.ExpirationField)
{
return true;
}
if (field.getID() == userSchema.PASSWORDCHANGETIME)
{
return true;
}
return super.isDateLimited(field);
}
/**
* <p>This method is used to specify the earliest acceptable date
* for the specified {@link arlut.csd.ganymede.server.DateDBField DateDBField}.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*/
@Override public Date minDate(DBField field)
{
if (field.getID() == userSchema.PASSWORDCHANGETIME)
{
return new Date(); // no values in the past, thanks
}
return super.minDate(field);
}
/**
* <p>This method is used to specify the latest acceptable date
* for the specified {@link arlut.csd.ganymede.server.DateDBField DateDBField}.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*/
@Override public Date maxDate(DBField field)
{
if (field.getID() == SchemaConstants.ExpirationField)
{
// if we have a user category set, limit the acceptable date to
// current date + max days
Date currentDate = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(currentDate);
try
{
Invid catInvid = (Invid) this.getFieldValueLocal(userSchema.CATEGORY);
DBObject category = internalSession().getDBSession().viewDBObject(catInvid);
Integer maxDays = (Integer) category.getFieldValueLocal(userCategorySchema.LIMIT);
cal.add(Calendar.DATE, maxDays.intValue());
}
catch (NullPointerException ex)
{
// oops, no category set.. <shrug>
return super.maxDate(field);
}
return cal.getTime();
}
if (field.getID() == userSchema.PASSWORDCHANGETIME)
{
GanymedeSession mySession = this.getGSession();
// if we are supergash or we are reacting to a password change
// cascade, don't restrict what the date can be set to.
if (mySession == null || mySession.getPermManager().isSuperGash() || amChangingExpireDate)
{
return super.maxDate(field);
}
Date maxDate = getMaxPasswordExtension();
DateDBField passDateField = getDateField(userSchema.PASSWORDCHANGETIME);
if (passDateField != null)
{
Date currentDate = passDateField.value();
if (currentDate != null && currentDate.after(maxDate))
{
maxDate = currentDate;
}
}
return maxDate;
}
return super.maxDate(field);
}
/**
* <p>This method allows the DBEditObject to have executive approval
* of any scalar set operation, and to take any special actions in
* reaction to the set. When a scalar field has its value set, it
* will call its owners finalizeSetValue() method, passing itself as
* the <field> parameter, and passing the new value to be
* approved as the <value> parameter. A Ganymede customizer
* who creates custom subclasses of the DBEditObject class can
* override the finalizeSetValue() method and write his own logic
* to examine any change and either approve or reject the change.</p>
*
* <p>A custom finalizeSetValue() method will typically need to
* examine the field parameter to see which field is being changed,
* and then do the appropriate checking based on the value
* parameter. The finalizeSetValue() method can call the normal
* this.getFieldValueLocal() type calls to examine the current state
* of the object, if such information is necessary to make
* appropriate decisions.</p>
*
* <p>If finalizeSetValue() returns null or a ReturnVal object with
* a positive success value, the DBField that called us is
* guaranteed to proceed to make the change to its value. If this
* method returns a non-success code in its ReturnVal, as with the
* result of a call to Ganymede.createErrorDialog(), the DBField
* that called us will not make the change, and the field will be
* left unchanged. Any error dialog returned from finalizeSetValue()
* will be passed to the user.</p>
*
* <p>The DBField that called us will take care of all standard
* checks on the operation (including a call to our own
* verifyNewValue() method before calling this method. Under normal
* circumstances, we won't need to do anything here.
* finalizeSetValue() is useful when you need to do unusually
* involved checks, and for when you want a chance to trigger other
* changes in response to a particular field's value being
* changed.</p>
*
* @return A ReturnVal indicating success or failure. May
* be simply 'null' to indicate success if no feedback need
* be provided.
*/
@Override public synchronized ReturnVal finalizeSetValue(DBField field, Object value)
{
InvidDBField inv;
String oldName;
StringDBField sf;
/* -- */
// we don't want to allow the home directory to be changed except
// by when the username field is being changed.
if (field.getID() == HOMEDIR)
{
String dir = (String) value;
/* -- */
if (homedir == null)
{
homedir = System.getProperty("ganymede.homedirprefix");
}
// we will only check against a defined prefix if
// we have set one in our properties file.
if (homedir != null && homedir.length() != 0)
{
if (newUsername != null)
{
String expected = homedir + (String) newUsername;
if (!dir.equals(expected))
{
return Ganymede.createErrorDialog(this.getGSession(),
"Schema Error",
"Home directory should be " + expected + ".\n" +
"This is a restriction encoded in userCustom.java.");
}
}
}
return null;
}
if (field.getID() == userSchema.PASSWORD)
{
// the password is being changed, update the time that it will need to
// be changed again
DateDBField dateField = getDateField(userSchema.PASSWORDCHANGETIME);
if (dateField != null)
{
Date passwordDate = getNewPasswordExpirationDate();
Date currentDate = dateField.value();
// be sure and check to make sure we never pull a password
// expiration date backwards in time
if (currentDate == null || !currentDate.after(passwordDate))
{
// set the amChangingExpireDate flag to true so that we
// won't try and restrict the forward date when the date
// set operation cascades forward through our maxDate()
// method
ReturnVal result;
amChangingExpireDate = true;
try
{
result = dateField.setValueLocal(passwordDate);
}
finally
{
amChangingExpireDate = false;
}
if (result != null)
{
System.err.println("UserCustom: setValueLocal on PASSWORDCHANGETIME field failed: " + result);
}
}
}
else
{
System.err.println("UserCustom: can't find PASSWORDCHANGETIME field");
}
ReturnVal result = ReturnVal.success();
result.addRescanField(this.getInvid(), userSchema.PASSWORDCHANGETIME);
return result;
}
// our maxDate() and isDateLimited() methods have pre-filtered any
// non-null expiration date for us.. just need to check to see
// whether the field can be cleared here.
if ((field.getID() == SchemaConstants.ExpirationField) && value == null)
{
if (isDeleting())
{
// approve it, everything's being cleaned out.
return null;
}
if (willBeRemoved())
{
// it's okay for us to null the expiration date, since we already
// have a removal date set
return null;
}
// check to see if the user category doesn't mind not having an expiration
// or removal date set.
try
{
Invid catInvid = (Invid) this.getFieldValueLocal(userSchema.CATEGORY);
DBObject category = internalSession().getDBSession().viewDBObject(catInvid);
if (category.isSet(userCategorySchema.EXPIRE))
{
return Ganymede.createErrorDialog(this.getGSession(),
"Schema Error",
"This user requires an expiration date because of its " +
"user category.");
}
else
{
// ok, then
return null;
}
}
catch (NullPointerException ex)
{
// ah, no category or limit set.. go ahead and let em do
// it
return null;
}
}
// when we rename a user, we have lots to do.. a number of other
// fields in this object and others need to be updated to match.
if (field.getID() == USERNAME)
{
// remember the new user name we are changing to, so that the
// other fields that we will change as a result of the
// username change will be able to get the new name.
newUsername = (String) value;
try
{
// if we are being told to clear the user name field, go ahead and
// do it.. we assume this is being done by user removal logic,
// so we won't press the issue.
if (isDeleting() && (value == null))
{
return null;
}
// so we're renaming. rename the hidden label for any
// embedded automounter entries, please.
ReturnVal retVal = renameEntries(newUsername);
if (retVal != null && !retVal.didSucceed())
{
return retVal;
}
// signature alias field will need to be rescanned
sf = getStringField(USERNAME); // old user name
oldName = (String) sf.getValueLocal();
if (oldName != null)
{
sf = getStringField(SIGNATURE); // signature alias
// if the signature alias was the user's name, we'll want
// to continue that.
if (oldName.equals((String) sf.getValueLocal()))
{
sf.setValueLocal(value); // set the signature alias to the user's new name
}
}
// update the home directory location.. we assume that if
// the user has permission to rename the user, they can
// automatically execute this change to the home directory.
if (homedir == null)
{
homedir = System.getProperty("ganymede.homedirprefix");
}
// do we have a homedir prefix? if so, set the home dir here
if (homedir != null && homedir.length() != 0)
{
sf = getStringField(HOMEDIR);
sf.setValueLocal(homedir + (String) value); // ** ARL
}
// if we don't have a signature set, set it to the username.
sf = getStringField(SIGNATURE);
String sigVal = (String) sf.getValueLocal();
if (sigVal == null || sigVal.equals(oldName))
{
sf.setValueLocal(value);
}
// update the email target field. We want to look for
// oldName@arlut.utexas.edu and replace it if we find it.
sf = getStringField(EMAILTARGET);
if (mailsuffix == null)
{
mailsuffix = System.getProperty("ganymede.defaultmailsuffix");
}
if (mailsuffix == null)
{
Ganymede.debug("Error in userCustom: couldn't find property ganymede.defaultmailsuffix!");
}
String oldMail = oldName + mailsuffix;
if (sf.containsElementLocal(oldMail))
{
sf.deleteElementLocal(oldMail);
sf.addElementLocal(value + mailsuffix);
}
else if (sf.size() == 0)
{
sf.addElementLocal(value + mailsuffix);
}
inv = getInvidField(PERSONAE);
if (inv == null)
{
return null; // success
}
// rename all the associated personae with the new user name
Vector<Invid> personaeInvids = (Vector<Invid>) inv.getValuesLocal();
for (Invid invid: personaeInvids)
{
adminPersonaCustom adminObj = (adminPersonaCustom) getDBSession().editDBObject(invid);
adminObj.refreshLabelField(null, null, (String) value);
}
}
finally
{
newUsername = null;
}
}
return null; // success by default
}
/**
* <p>This method allows the DBEditObject to have executive approval of
* any vector delete operation, and to take any special actions in
* reaction to the delete.. if this method returns null or a success
* code in its ReturnVal, the {@link arlut.csd.ganymede.server.DBField DBField}
* that called us is guaranteed to proceed to
* make the change to its vector. If this method returns a
* non-success code in its ReturnVal, the DBField that called us
* will not make the change, and the field will be left
* unchanged.</p>
*
* <p>The <field> parameter identifies the field that is requesting
* approval for item deletion, and the <index> parameter identifies
* the element number that is to be deleted.</p>
*
* <p>The DBField that called us will take care of all standard
* checks on the operation (including vector bounds, etc.) before
* calling this method. Under normal circumstances, we won't need
* to do anything here.</p>
*
* @return A ReturnVal indicating success or failure. May
* be simply 'null' to indicate success if no feedback need
* be provided.
*/
@Override public ReturnVal finalizeDeleteElement(DBField field, int index)
{
if (field.getID() == ALIASES)
{
String goneAlias = (String) getFieldElementLocal(field, index);
String signatureAlias = (String) getFieldValueLocal(SIGNATURE);
if (!StringUtils.stringEquals(goneAlias, signatureAlias))
{
return null; // no worries
}
// okay, they're removing their signature alias from the
// aliases field. Let's force the signature alias back to
// their username.
String username = (String) getFieldValueLocal(USERNAME);
ReturnVal retVal = setFieldValueLocal(SIGNATURE, username);
return retVal;
}
return null;
}
/**
* <p>This method calculates what time the password expiration field should be set to
* if the password is being changed right now.</p>
*/
private Date getNewPasswordExpirationDate()
{
Calendar myCal = new GregorianCalendar();
myCal.add(Calendar.MONTH, 3);
// if the expiration date will fall between Dec 20
// and January 10, bump the date forward three
// weeks to skip over the year-end holidays
int month = myCal.get(Calendar.MONTH);
int day = myCal.get(Calendar.DATE);
if ((month == Calendar.DECEMBER && day >= 20) ||
(month == Calendar.JANUARY && day < 10))
{
myCal.add(Calendar.DATE, 21);
}
return myCal.getTime();
}
/**
* This method calculates what the maximum time the password
* expiration field may be set to by a ganymede admin.
*/
private Date getMaxPasswordExtension()
{
Calendar myCal = new GregorianCalendar();
myCal.add(Calendar.DATE, 14);
// if the maximum expiration date will fall between Dec 20
// and January 10, bump the date forward three
// weeks to skip over the year-end holidays
int month = myCal.get(Calendar.MONTH);
int day = myCal.get(Calendar.DATE);
if ((month == Calendar.DECEMBER && day >= 20) ||
(month == Calendar.JANUARY && day < 10))
{
myCal.add(Calendar.DATE, 21);
}
return myCal.getTime();
}
/**
* <p>This method is the hook that DBEditObject subclasses use to interpose
* {@link arlut.csd.ganymede.server.GanymediatorWizard wizards} when a field's
* value is being changed.</p>
*
* <p>Whenever a field is changed in this object, this method will be
* called with details about the change. This method can refuse to
* perform the operation, it can make changes to other objects in
* the database in response to the requested operation, or it can
* choose to allow the operation to continue as requested.</p>
*
* <p>In the latter two cases, the wizardHook code may specify a list
* of fields and/or objects that the client may need to update in
* order to maintain a consistent view of the database.</p>
*
* <p>If server-local code has called
* {@link arlut.csd.ganymede.server.GanymedeSession#enableOversight(boolean)
* enableOversight(false)},
* this method will never be
* called. This mode of operation is intended only for initial
* bulk-loading of the database.</p>
*
* <p>This method may also be bypassed when server-side code uses
* setValueLocal() and the like to make changes in the database.</p>
*
* <p>This method is called before the finalize*() methods.. the finalize*()
* methods is where last minute cascading changes should be performed..
* Note as well that wizardHook() is called before the namespace checking
* for the proposed value is performed, while the finalize*() methods are
* called after the namespace checking.</p>
*
* <p>The operation parameter will be a small integer, and should hold one of the
* following values:</p>
*
* <dl>
* <dt>1 - SETVAL</dt>
* <dd>This operation is used whenever a simple scalar field is having
* it's value set. param1 will be the value being placed into the field.</dd>
* <dt>2 - SETELEMENT</dt>
* <dd>This operation is used whenever a value in a vector field is being
* set. param1 will be an Integer holding the element index, and
* param2 will be the value being set.</dd>
* <dt>3 - ADDELEMENT</dt>
* <dd>This operation is used whenever a value is being added to the
* end of a vector field. param1 will be the value being added.</dd>
* <dt>4 - DELELEMENT</dt>
* <dd>This operation is used whenever a value in a vector field is being
* deleted. param1 will be an Integer holding the element index.</dd>
* <dt>5 - ADDELEMENTS</dt>
* <dd>This operation is used whenever a set of elements is being
* added to a vector field en masse. param1 will be a Vector containing
* the values that are being added.</dd>
* <dt>6 - DELELEMENTS</dt>
* <dd>This operation is used whenever a set of elements is being
* deleted from a vector field en masse. param1 will be a Vector containing
* the values that are being deleted.</dd>
* <dt>7 - SETPASSPLAIN</dt>
* <dd>This operation is used when a password field is having its password
* set using a plaintext source. param1 will be a String containing the
* submitted password, or null if the password is being cleared.</dd>
* <dt>8 - SETPASSCRYPT</dt>
* <dd>This operation is used when a password field is having its password
* set using a UNIX crypt() hashed source. param1 will be a String containing the
* submitted hashed password, or null if the password is being cleared.</dd>
* <dt>9 - SETPASSMD5</dt>
* <dd>This operation is used when a password field is having its password
* set using an md5Ccrypt() hashed source. param1 will be a String containing the
* submitted hashed password, or null if the password is being cleared.</dd>
* <dt>10 - SETPASSWINHASHES</dt>
* <dd>This operation is used when a password field is having its password
* set using Windows style password hashes. param1 will be the password in
* LANMAN hash form, param2 will be the password in NT Unicode MD4 hash
* form. Either or both of param1 and param2 may be null.</dd>
* <dt>11 - SETPASSAPACHEMD5</dt>
* <dd>This operation is used when a password field is having its
* password set using the Apache variant of the md5crypt algorithm.
* param1 will be the password in Apache md5crypt hash form, or null
* if the password hash is being cleared. param2 will be null.</dd>
* <dt>12 - SETPASSSSHA</dt>
* <dd>This operation is used when a password field is having its
* password set using the OpenLDAP-style SSHA password hash. param1
* will be the password in SSHA form, or null if the password is
* being cleared. param2 will be null.</dd>
* <dt>13 - SETPASS_SHAUNIXCRYPT</dt>
* <dd>This operation is used when a password field is having its
* password set using Ulrich Drepper's SHA256 or SHA512 Unix Crypt
* algorithms. param1 will be the password in SHA Unix Crypt form,
* or null if the password is being cleared. param2 will be
* null.</dd>
* <dt>14 - SETPASS_BCRYPT</dt>
* <dd>This operation is used when a password field is having its
* password set using the OpenBSD-style BCrypt password hash. param1
* will be the password in BCrypt form, or null if the password is
* being cleared. param2 will be null.</dd>
* </dl>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*
* @return null if the operation is approved without comment, or a
* ReturnVal object indicating success or failure, objects and
* fields to be rescanned by the client, and a doNormalProcessing
* flag that will indicate to the field code whether or not the
* operation should continue to completion using the field's
* standard logic. <b>It is very important that wizardHook return a
* new ReturnVal(true, true) if the wizardHook wishes to simply
* specify rescan information while having the field perform its
* standard operation.</b> wizardHook() may return new
* ReturnVal(true, false) if the wizardHook performs the operation
* (or a logically related operation) itself. The same holds true
* for the respond() method in GanymediatorWizard subclasses.
*/
@Override public ReturnVal wizardHook(DBField field, int operation, Object param1, Object param2)
{
userHomeGroupDelWizard groupWizard = null;
userRenameWizard renameWizard = null;
ReturnVal result;
/* -- */
// something's changed, forget that we've given a warning about
// username/badge issues in IRIS
this.IRISWarningGiven = false;
// if the groups field is being changed, we may need to intervene
if (debug)
{
System.err.println("userCustom ** entering wizardHook, field = " +
field.getName() + ", op= " + operation);
}
try
{
if (field.getID() == EMAILACCOUNTTYPE)
{
result = ReturnVal.success();
result.addRescanField(field.getObject().getInvid(), EXCHANGESTORE);
if (!"Exchange".equals(param1))
{
return result.merge(((DBEditObject) field.getObject()).setFieldValueLocal(EXCHANGESTORE, null));
}
else
{
return result;
}
}
if (field.getID() == ALLOWEXTERNAL)
{
// a success ReturnVal is used to tell the Ganymede logic
// to go ahead and complete the operation normally. In
// this case, it will take the rescan information as an
// extra to pass back to the client.
result = ReturnVal.success();
if (Boolean.TRUE.equals(param1))
{
StringDBField usernameField = getStringField(userSchema.MAILUSER);
PasswordDBField passField = getPassField(userSchema.MAILPASSWORD2);
DateDBField dateField = getDateField(userSchema.MAILEXPDATE);
result = ReturnVal.merge(result, usernameField.setValueLocal(RandomUtils.getRandomUsername()));
if (!ReturnVal.didSucceed(result))
{
return result; // random username collision?
}
result = ReturnVal.merge(result, passField.setPlainTextPass(RandomUtils.getRandomPassword(20)));
if (!ReturnVal.didSucceed(result))
{
return result;
}
Calendar myCal = new GregorianCalendar();
myCal.add(Calendar.DATE, 168); // 24 weeks
result = ReturnVal.merge(result, dateField.setValueLocal(myCal.getTime()));
if (!ReturnVal.didSucceed(result))
{
return result;
}
}
result.addRescanField(this.getInvid(), userSchema.MAILUSER);
result.addRescanField(this.getInvid(), userSchema.MAILPASSWORD2);
result.addRescanField(this.getInvid(), userSchema.MAILEXPDATE);
return result;
}
// if we are changing the list of email aliases, we'll want
// to update the list of choices for the signature field.
if (field.getID() == ALIASES)
{
// the second true in the ReturnVal constructor makes the
// Ganymede logic go ahead and complete the operation
// normally, just taking the rescan information as an
// extra to pass back to the client.
result = ReturnVal.success();
result.addRescanField(this.getInvid(), userSchema.SIGNATURE);
return result;
}
if (field.getID() == GROUPLIST)
{
switch (operation)
{
case ADDELEMENT:
case ADDELEMENTS:
// ok, no big deal, but we will need to have the client
// rescan the choice list for the home group field
result = ReturnVal.success();
result.addRescanField(this.getInvid(), HOMEGROUP);
groupChoices = null;
return result;
case DELELEMENT:
if (isDeleting())
{
return null;
}
// ok, this is more of a big deal.. first, see if the value
// being deleted is the home group. If not, still no big
// deal.
int index = ((Integer) param1).intValue();
Vector<Invid> valueAry = (Vector<Invid>) getFieldValuesLocal(GROUPLIST);
Invid delVal = valueAry.get(index);
if (debug)
{
System.err.println("userCustom: deleting group element " +
gSession.getDBSession().getObjectLabel(delVal));
}
if (!delVal.equals(getFieldValueLocal(HOMEGROUP)))
{
// whew, no big deal.. they are not removing the
// home group. The client will need to rescan,
// but no biggie.
if (debug)
{
System.err.println("userCustom: I don't think " +
gSession.getDBSession().getObjectLabel(delVal) +
" is the home group");
}
result = ReturnVal.success();
result.addRescanField(this.getInvid(), HOMEGROUP);
groupChoices = null;
return result;
}
if (gSession.isWizardActive() &&
gSession.getWizard() instanceof userHomeGroupDelWizard)
{
groupWizard = (userHomeGroupDelWizard) gSession.getWizard();
if (groupWizard.getState() == groupWizard.DONE)
{
// ok, assume the wizard has taken care of getting everything prepped and
// approved for us. An active wizard has approved the operation
groupWizard.unregister();
return null;
}
else
{
if (groupWizard.userObject != this)
{
System.err.println("userCustom.wizardHook(): bad object");
}
if (groupWizard.getState() != groupWizard.DONE)
{
System.err.println("userCustom.wizardHook(): bad state: " +
groupWizard.getState());
}
groupWizard.unregister();
return Ganymede.createErrorDialog(this.getGSession(),
"User Object Error",
"The client is attempting to do an operation on " +
"a user object with an active wizard.");
}
}
else if (gSession.isWizardActive() &&
!(gSession.getWizard() instanceof userHomeGroupDelWizard))
{
return Ganymede.createErrorDialog(this.getGSession(),
"User Object Error",
"The client is attempting to do an operation on " +
"a user object with mismatched active wizard.");
}
// eek. they are deleting the home group. Why Lord, why?!
try
{
groupWizard = new userHomeGroupDelWizard(this.gSession,
this,
param1);
}
catch (RemoteException ex)
{
throw new RuntimeException("Couldn't create userWizard " + ex.getMessage());
}
// if we get here, the wizard was able to register itself.. go ahead
// and return the initial dialog for the wizard. The ReturnVal code
// that wizard.getStartDialog() returns will have the success code
// set to false, so whatever triggered us will prematurely exit,
// returning the wizard's dialog.
return groupWizard.respond(null);
case DELELEMENTS:
// see if any of the values is the home group
Vector<Invid> valuesToDelete = (Vector<Invid>) param1;
if (!valuesToDelete.contains(getFieldValueLocal(HOMEGROUP)))
{
result = ReturnVal.success();
result.addRescanField(this.getInvid(), HOMEGROUP); // rebuild choice list
groupChoices = null;
return result;
}
else
{
return Ganymede.createErrorDialog(this.getGSession(),
"User Validation Error",
"Can't remove home group in bulk transfer.");
}
}
}
// if the user category is changed, we need to be sure and get
// the expiration date set..
if (field.getID() == CATEGORY)
{
if (gSession.isWizardActive() && gSession.getWizard() instanceof userCategoryWizard)
{
userCategoryWizard uw = (userCategoryWizard) gSession.getWizard();
if (uw.getState() == uw.DONE)
{
// ok, assume the wizard has taken care of getting everything prepped and
// approved for us. An active wizard has approved the operation
return null;
}
}
try
{
if (param1 != null || !isDeleting())
{
return new userCategoryWizard(getGSession(), this,
(Invid) getFieldValueLocal(userSchema.CATEGORY),
(Invid) param1).respond(null);
}
else
{
return null;
}
}
catch (RemoteException ex)
{
return Ganymede.createErrorDialog(this.getGSession(),
"Server error",
"userCustom.wizardHook(): can't initialize userCategoryWizard.");
}
}
if ((field.getID() != USERNAME) ||
(operation != SETVAL))
{
return null; // by default, we just ok whatever else
}
// ok, we're doing a user rename.. check to see if we need to do a
// wizard
// If this is a newly created user, we won't pester them about setting
// or changing the user name field.
if ((field.getValueLocal() == null) || (getStatus() == ObjectStatus.CREATING))
{
result = ReturnVal.success(); // have setValue() do the right thing
result.addRescanField(this.getInvid(), userSchema.HOMEDIR);
result.addRescanField(this.getInvid(), userSchema.ALIASES);
result.addRescanField(this.getInvid(), userSchema.SIGNATURE);
result.addRescanField(this.getInvid(), userSchema.VOLUMES);
result.addRescanField(this.getInvid(), userSchema.EMAILTARGET);
return result;
}
String oldname = (String) field.getValueLocal();
if (!gSession.enableWizards)
{
return null; // no wizards if the user is non-interactive.
}
// Huh! Wizard time! We'll check here to see if there is a
// registered userRenameWizard in the system taking care of us.
if (gSession.isWizardActive() && gSession.getWizard() instanceof userRenameWizard)
{
renameWizard = (userRenameWizard) gSession.getWizard();
if ((renameWizard.getState() == renameWizard.DONE) &&
(renameWizard.field == field) &&
(renameWizard.userObject == this) &&
(renameWizard.newname == param1))
{
// ok, assume the wizard has taken care of getting
// everything prepped and approved for us. An active
// wizard has approved the operation
renameWizard.unregister();
// note that we don't have to return the rescan fields
// directive here.. the active wizard is what is going to
// respond directly to the user, we are presumably just
// here because the wizard task-completion code went ahead
// and called setValue on the user's name.. we'll trust
// that code to return the rescan indicators.
return null;
}
else
{
if (renameWizard.field != field)
{
System.err.println("userCustom.wizardHook(): bad field");
}
if (renameWizard.userObject != this)
{
System.err.println("userCustom.wizardHook(): bad object");
}
if (renameWizard.newname != param1)
{
System.err.println("userCustom.wizardHook(): bad param");
}
if (renameWizard.getState() != renameWizard.DONE)
{
System.err.println("userCustom.wizardHook(): bad state: " +
renameWizard.getState());
}
renameWizard.unregister();
return Ganymede.createErrorDialog(this.getGSession(),
"User Object Error",
"The client is attempting to do an operation on " +
"a user object with an active wizard.");
}
}
else if (gSession.isWizardActive())
{
return Ganymede.createErrorDialog(this.getGSession(),
"User Object Error",
"The client is attempting to do an operation on " +
"a user object with mismatched active wizard.\n" +
"Wizard id: " + gSession.getWizard());
}
else
{
// there's no wizard active, and this operation has to be approved by one. Go ahead
// and set up the wizard and let the client play with it.
// if we're setting the field to null, don't need to pass it through
// a wizard.. we're probably just deleting this user.
if (isDeleting() && (param1 == null))
{
return null;
}
try
{
// Mike Jittlov is the Wizard of Speed and Time
renameWizard = new userRenameWizard(this.gSession,
this,
field,
(String) param1,
oldname);
}
catch (RemoteException ex)
{
throw new RuntimeException("Couldn't create userWizard " + ex.getMessage());
}
// if we get here, the wizard was able to register itself.. go ahead
// and return the initial dialog for the wizard. The ReturnVal code
// that wizard.respond() returns will have the success code
// set to false, so whatever triggered us will prematurely exit,
// returning the wizard's dialog.
return renameWizard.respond(null);
}
}
finally
{
if (debug)
{
System.err.println("userCustom ** exiting wizardHook");
}
}
}
/**
* <p>This method is a hook for subclasses to override to
* pass the phase-two commit command to external processes.</p>
*
* <p>For normal usage this method would not be overridden. For
* cases in which change to an object would result in an external
* process being initiated whose <b>success or failure would not
* affect the successful commit of this DBEditObject in the
* Ganymede server</b>, the process invocation should be placed here,
* rather than in
* {@link arlut.csd.ganymede.server.DBEditObject#commitPhase1() commitPhase1()}.</p>
*
* <p>commitPhase2() is generally the last method called on a
* DBEditObject before it is discarded by the server in the
* {@link arlut.csd.ganymede.server.DBEditSet DBEditSet}
* {@link arlut.csd.ganymede.server.DBEditSet#commit(java.lang.String) commit()} method.</p>
*
* <p>Subclasses that override this method may wish to make this method
* synchronized.</p>
*
* <p><b>WARNING!</b> this method is called at a time when portions
* of the database are locked for the transaction's integration into
* the database. You must not call methods that seek to gain a lock
* on the Ganymede database. At this point, this means no composite
* queries on embedded object types, where you seek an object based
* on a field in an embedded object and in the object itself, using
* the GanymedeSession query calls, or else you will lock the server.</p>
*
* <p>This method should NEVER try to edit or change any DBEditObject
* in the server.. at this point in the game, the server has fixed the
* transaction working set and is depending on commitPhase2() not trying
* to make changes internal to the server.</p>
*
* <p>To be overridden on necessity in DBEditObject subclasses.</p>
*/
@Override public void commitPhase2()
{
switch (getStatus())
{
case DROPPING:
// the user never really existed.. no external actions required.
break;
case CREATING:
// handle creating the user.. creating their home directory, setting
// up their mail spool, etc., etc.
createUserExternals();
break;
case DELETING:
deleteUserExternals();
break;
case EDITING:
// did the user's name change?
String name = getLabel();
String oldname = original.getLabel();
if (!name.equals(oldname))
{
handleUserRename(oldname, name);
}
// did we change home directory volumes?
handleVolumeChanges();
}
return;
}
/**
* This method runs from userCustom's commitPhase2() and runs an external
* script that can create the user's home directory, and anything else
* that might need doing.
*/
private void createUserExternals()
{
// if the system log is null, we're running in the direct loader, and we
// don't want to create anything external.
if (Ganymede.log == null)
{
return;
}
if (debug)
{
System.err.println("userCustom: " + getLabel() + ", in createUserExternals().");
}
// get the volumes defined for the user on auto.home.default
InvidDBField mapEntries = getInvidField(userSchema.VOLUMES);
Vector<Invid> entries = (Vector<Invid>) mapEntries.getValuesLocal();
if (entries.size() < 1)
{
System.err.println("Couldn't handle createUserExternals for user " + getLabel() +
", because we don't have a volume defined");
return;
}
for (Invid entry: entries)
{
user_added_to_vol(entry);
}
}
/**
* Helper method to create a directory for a user
*/
private void user_added_to_vol(Invid entryInvid)
{
// if the system log is null, we're running in the direct loader, and we
// don't want to create anything external.
if (Ganymede.log == null)
{
return;
}
DBObject entryObj = getDBSession().viewDBObject(entryInvid);
Invid volumeInvid = (Invid) entryObj.getFieldValueLocal(mapEntrySchema.VOLUME);
DBObject volumeObj = getDBSession().viewDBObject(volumeInvid);
String volName = (String) volumeObj.getFieldValueLocal(volumeSchema.LABEL);
Invid mapInvid = (Invid) entryObj.getFieldValueLocal(mapEntrySchema.MAP);
DBObject mapObj = getDBSession().viewDBObject(mapInvid);
String mapName = mapObj.getLabel();
Integer id = (Integer) getFieldValueLocal(userSchema.UID);
Invid homegroupInvid = (Invid) getFieldValueLocal(userSchema.HOMEGROUP);
Vector<Invid> ownerInvids = (Vector<Invid>) this.getFieldValuesLocal(SchemaConstants.OwnerListField);
String ownerName;
if (ownerInvids.size() > 0)
{
Invid ownerOne = ownerInvids.get(0);
DBObject ownerObj = getDBSession().viewDBObject(ownerOne);
ownerName = ownerObj.getLabel();
// we want underscores to separate words, not spaces
ownerName = ownerName.replace(' ', '_');
}
else
{
ownerName = "";
}
if (homegroupInvid == null)
{
// the user didn't completely fill out this user
// object.. return silently and let the transaction logic tell
// the user what the problem is.
return;
}
DBObject homeGroup = getDBSession().viewDBObject(homegroupInvid);
Integer gid = (Integer) homeGroup.getFieldValueLocal(groupSchema.GID);
boolean success = false;
try
{
if (createHandler == null)
{
if (debug)
{
System.err.println("userCustom: createUserExternals: getting createFilename");
}
createFilename = System.getProperty("ganymede.builder.scriptlocation");
if (createFilename == null)
{
Ganymede.debug("userCustom.createUserExternals(): Couldn't find " +
"ganymede.builder.scriptlocation property");
return;
}
// make sure we've got the path separator at the end of
// createFilename, add our script name
createFilename = PathComplete.completePath(createFilename) + "scripts/directory_maker";
if (debug)
{
System.err.println("userCustom: createUserExternals: createFilename = " +
createFilename);
}
createHandler = new File(createFilename);
}
if (createHandler.exists())
{
try
{
// we'll call our external script with the following
//
// parameters: <volumename/volume_directory> <username> <user id> <group id> <mapname> <owner>
String execLine = createFilename + " " + volName + " " +
getLabel() + " " + id + " " + gid + " " + mapName + " " + ownerName;
if (debug)
{
System.err.println("createUserExternals: running " + execLine);
}
try
{
if (debug)
{
System.err.println("createUserExternals: blocking ");
}
int result = FileOps.runProcess(execLine);
if (debug)
{
System.err.println("createUserExternals: done ");
}
if (result != 0)
{
Ganymede.debug("Couldn't handle externals for creating user " + getLabel() +
"\n" + createFilename + " returned a non-zero result: " + result);
}
else
{
success = true;
}
}
catch (InterruptedException ex)
{
Ganymede.debug("Couldn't handle externals for creating user " + getLabel() + "\n" +
ex.getMessage());
}
}
catch (IOException ex)
{
Ganymede.debug("Couldn't handle externals for creating user " + getLabel() + "\n" +
ex.getMessage());
}
}
}
finally
{
mail_user_added_to_vol(entryInvid, !success);
}
}
/**
* Helper method to send out mail to owners of the system that the
* user's home directory is being placed on.
*/
private void mail_user_added_to_vol(Invid entryInvid, boolean need_to_create)
{
// if the system log is null, we're running in the direct loader, and we
// don't want to create anything external.
if (Ganymede.log == null)
{
return;
}
StringBuilder buffer = new StringBuilder();
DBObject entryObj = getDBSession().viewDBObject(entryInvid);
Invid mapInvid = (Invid) entryObj.getFieldValueLocal(mapEntrySchema.MAP);
DBObject mapObj = getDBSession().viewDBObject(mapInvid);
String mapName = mapObj.getLabel();
Invid volumeInvid = (Invid) entryObj.getFieldValueLocal(mapEntrySchema.VOLUME);
DBObject volumeObj = getDBSession().viewDBObject(volumeInvid);
String volName = volumeObj.getLabel();
String volPath = (String) volumeObj.getFieldValueLocal(volumeSchema.PATH);
Invid sysInvid = (Invid) volumeObj.getFieldValueLocal(volumeSchema.HOST);
DBObject sysObj = getDBSession().viewDBObject(sysInvid);
String sysName = sysObj.getLabel();
List<Invid> objects = new ArrayList<Invid>();
objects.add(sysInvid);
Set<String> addresses = DBLog.calculateOwnerAddresses(objects, getDBSession());
String subject = null;
if (need_to_create)
{
buffer.append("Hi. User ");
buffer.append(getLabel());
buffer.append(" was added to volume ");
buffer.append(volName);
buffer.append(" in the ");
buffer.append(mapName);
buffer.append(" automounter home map.\n\nSince you are listed in the Ganymede");
buffer.append(" system database as an administrator for a system contained in");
buffer.append(" volume ");
buffer.append(volName);
buffer.append(", you need to take whatever action is appropriate to create a");
buffer.append(" home directory for this user on ");
buffer.append(sysName);
buffer.append(", if one does not already exist..\n\n");
buffer.append("Volume ");
buffer.append(volName);
buffer.append(" is currently defined as:\n");
buffer.append(sysName);
buffer.append(":");
buffer.append(volPath);
buffer.append("\n\nThanks for your cooperation.\nYour friend,\n\tGanymede.\n");
subject = "User " + getLabel() + " needs a home directory on " + sysName;
}
else
{
buffer.append("A home directory for user ");
buffer.append(getLabel());
buffer.append(" has been constructed on volume ");
buffer.append(volName);
buffer.append(". The user's home directory has been registered in the ");
buffer.append(mapName);
buffer.append(" automounter home map.\n");
subject = "User " + getLabel() + " home directory created";
}
editset.logMail(addresses, subject, buffer.toString());
}
/**
* This method runs from userCustom's commitPhase2() and runs an external
* script that can do whatever bookkeeping might be desired when a user
* is taken out of the passwd/user_info file generated by Ganymede. This
* may include removing the user's mailbox, home directory, and files, or
* simply notifying someone that the user is no longer valid.
*/
private void deleteUserExternals()
{
// if the system log is null, we're running in the direct loader, and we
// don't want to create anything external.
if (Ganymede.log == null)
{
return;
}
if (debug)
{
System.err.println("userCustom: " + getLabel() + ", in deleteUserExternals().");
}
handleUserDelete(getLabel());
// get the volumes defined for the user on auto.home.default
DBObject obj = getOriginal();
InvidDBField mapEntries = obj.getInvidField(userSchema.VOLUMES);
Vector<Invid> entries = (Vector<Invid>) mapEntries.getValuesLocal();
if (entries.size() < 1)
{
System.err.println("Couldn't handle deleteUserExternals for user " + getLabel() +
", because we don't have a volume defined");
return;
}
for (Invid entry: entries)
{
mail_user_removed_from_vol(entry);
}
}
/**
* Helper method to send out mail to owners of the system that the
* user's home directory is being scrubbed from.
*/
private void mail_user_removed_from_vol(Invid entryInvid)
{
// if the system log is null, we're running in the direct loader, and we
// don't want to create anything external.
if (Ganymede.log == null)
{
return;
}
StringBuilder buffer = new StringBuilder();
DBObject entryObj = getDBSession().viewDBObject(entryInvid, true);
Invid mapInvid = (Invid) entryObj.getFieldValueLocal(mapEntrySchema.MAP);
DBObject mapObj = getDBSession().viewDBObject(mapInvid, true);
String mapName = mapObj.getLabel();
Invid volumeInvid = (Invid) entryObj.getFieldValueLocal(mapEntrySchema.VOLUME);
DBObject volumeObj = getDBSession().viewDBObject(volumeInvid, true);
String volName = volumeObj.getLabel();
String volPath = (String) volumeObj.getFieldValueLocal(volumeSchema.PATH);
Invid sysInvid = (Invid) volumeObj.getFieldValueLocal(volumeSchema.HOST);
DBObject sysObj = getDBSession().viewDBObject(sysInvid, true);
String sysName = sysObj.getLabel();
List<Invid> objects = new ArrayList<Invid>();
objects.add(sysInvid);
Set<String> addresses = DBLog.calculateOwnerAddresses(objects, getDBSession());
String subject = null;
buffer.append("User ");
buffer.append(getLabel());
buffer.append(" has been removed from volume ");
buffer.append(volName);
buffer.append(" in the ");
buffer.append(mapName);
buffer.append(" automounter home map.\n\nSince you are listed in the Ganymede");
buffer.append(" system database as an administrator for a system contained in");
buffer.append(" volume ");
buffer.append(volName);
buffer.append(", you need to take whatever action is appropriate to remove this user");
buffer.append(" from ");
buffer.append(volName);
buffer.append(" if you are sure that the user will no longer be using his or her directory");
buffer.append(" on this volume.\n\n");
buffer.append("Volume ");
buffer.append(volName);
buffer.append(" is currently defined as:\n");
buffer.append(sysName);
buffer.append(":");
buffer.append(volPath);
buffer.append("\n\nThanks for your cooperation.\nYour friend,\n\tGanymede.\n");
subject = "User " + getLabel() + " needs to be removed on " + sysName;
editset.logMail(addresses, subject, buffer.toString());
}
/**
* This method handles external actions for deleting a user.
*/
private void handleUserDelete(String name)
{
// if the system log is null, we're running in the direct loader, and we
// don't want to create anything external.
// This would be unusual for a delete, but..
if (Ganymede.log == null)
{
return;
}
if (debug)
{
System.err.println("userCustom.handleUserDelete(): user " + name +
"is being deleted");
}
try
{
if (deleteHandler == null)
{
deleteFilename = System.getProperty("ganymede.builder.scriptlocation");
if (deleteFilename != null)
{
// make sure we've got the path separator at the end of
// deleteFilename, add our script name
deleteFilename = PathComplete.completePath(deleteFilename) + "scripts/user_deleter";
deleteHandler = new File(deleteFilename);
}
else
{
Ganymede.debug("userCustom.handleUserDelete(): Couldn't find " +
"ganymede.builder.scriptlocation property");
}
}
if (deleteHandler.exists())
{
try
{
String execLine = deleteFilename + " " + name;
if (debug)
{
System.err.println("handleUserDelete: running " + execLine);
}
try
{
if (debug)
{
System.err.println("handleUserDelete: blocking");
}
int result = FileOps.runProcess(execLine);
if (debug)
{
System.err.println("handleUserDelete: done");
}
if (result != 0)
{
Ganymede.debug("Couldn't handle externals for deleting user " + name +
"\n" + deleteFilename +
" returned a non-zero result: " + result);
}
}
catch (InterruptedException ex)
{
Ganymede.debug("Couldn't handle externals for deleting user " + name + ": " +
ex.getMessage());
}
}
catch (IOException ex)
{
Ganymede.debug("Couldn't handle externals for deleting user " + name + ": " +
ex.getMessage());
}
}
}
finally
{
Invid admin = getGSession().getPermManager().getPersonaInvid();
String adminName = getGSession().getPermManager().getUserName();
List<Invid> objects = new ArrayList<Invid>();
objects.add(getInvid());
StringBuilder buffer = new StringBuilder();
buffer.append("User ");
buffer.append(name);
buffer.append(" has been expunged from the Ganymede database.\n\n");
editset.logEvent("userdeleted",
buffer.toString(),
admin, adminName, objects, null);
}
}
/**
* This method is designed to send out mail notifying admins of changes
* made to a user's volume mappings, if any
*/
private void handleVolumeChanges()
{
// if the system log is null, we're running in the direct loader, and we
// don't want to create anything external.
if (Ganymede.log == null)
{
return;
}
Map<String, Invid> oldEntryMap = new HashMap<String, Invid>();
Map<String, Invid> newEntryMap = new HashMap<String, Invid>();
Map<Invid, Invid> oldVolMap = new HashMap<Invid, Invid>();
Map<Invid, Invid> newVolMap = new HashMap<Invid, Invid>();
List<String> oldMapNames = new ArrayList<String>();
List<String> newMapNames = new ArrayList<String>();
List<Invid> oldVolumes = new ArrayList<Invid>();
List<Invid> newVolumes = new ArrayList<Invid>();
Vector<Invid> oldEntries = (Vector<Invid>) original.getFieldValuesLocal(userSchema.VOLUMES);
for (Invid mapEntryInvid: oldEntries)
{
DBObject mapEntryObj = getDBSession().viewDBObject(mapEntryInvid);
if (mapEntryObj instanceof mapEntryCustom)
{
mapEntryCustom mapEntry = (mapEntryCustom) mapEntryObj;
String mapName = mapEntry.getOriginalMapName();
Invid volumeId = mapEntry.getOriginalVolumeInvid();
oldEntryMap.put(mapName, mapEntryInvid);
oldVolumes.add(volumeId);
oldMapNames.add(mapName);
// if we see the same volume in multiple maps, we'll just
// remember the last one seen.. doesn't matter much, for
// our purposes
oldVolMap.put(volumeId, mapEntryInvid);
if (debug)
{
System.err.println("Old entry.. " + mapName + ", " + volumeId);
}
}
}
Vector<Invid> newEntries = (Vector<Invid>) getFieldValuesLocal(userSchema.VOLUMES);
for (Invid mapEntryInvid: newEntries)
{
DBObject mapEntryObj = getDBSession().viewDBObject(mapEntryInvid);
if (mapEntryObj instanceof mapEntryCustom)
{
mapEntryCustom mapEntry = (mapEntryCustom) mapEntryObj;
String mapName = mapEntry.getMapName();
Invid volumeId = mapEntry.getVolumeInvid();
newEntryMap.put(mapName, mapEntryInvid);
newVolumes.add(volumeId);
newMapNames.add(mapName);
// if we see the same volume in multiple maps, we'll just
// remember the last one seen.. doesn't matter much, for
// our purposes
newVolMap.put(volumeId, mapEntryInvid);
if (debug)
{
System.err.println("New entry.. " + mapName + ", " + volumeId);
}
}
}
List<Invid> addedVolumes = VectorUtils.difference(newVolumes, oldVolumes);
List<Invid> deletedVolumes = VectorUtils.difference(oldVolumes, newVolumes);
List<String> keptMapNames = VectorUtils.intersection(newMapNames, oldMapNames);
for (String mapName: keptMapNames)
{
if (debug)
{
System.err.println("Checking map " + mapName + " for a volume change");
}
Invid oldMapEntryInvid = oldEntryMap.get(mapName);
Invid newMapEntryInvid = newEntryMap.get(mapName);
if (oldMapEntryInvid.equals(newMapEntryInvid))
{
// we know the map entry obj is an editing copy, don't
// need to check here
mapEntryCustom mapEntry = (mapEntryCustom) getDBSession().viewDBObject(oldMapEntryInvid);
Invid oldVolInvid = mapEntry.getOriginalVolumeInvid();
Invid newVolInvid = mapEntry.getVolumeInvid();
if (!oldVolInvid.equals(newVolInvid))
{
if (debug)
{
System.err.println("In map " + mapName + ", old vol was " + oldVolInvid +
", is now " + newVolInvid);
}
// we have moved the user's home directory on this map.. we won't
// try to create the new home directory ourselves
user_moved_from_vol_to_vol(oldVolInvid, newVolInvid, mapName);
// we've already handled notification for the moving
// between these volumes, don't need to do anything more
// for it
deletedVolumes.remove(oldVolInvid);
addedVolumes.remove(newVolInvid);
}
}
}
for (Invid volumeId: addedVolumes)
{
if (debug)
{
System.err.println("Gained volume " + volumeId);
}
// the user might have the same volume registered on multiple
// maps, but we don't care enough to send mail out for it
user_added_to_vol(newVolMap.get(volumeId));
}
for (Invid volumeId: deletedVolumes)
{
if (debug)
{
System.err.println("Lost volume " + volumeId);
}
// the user might have had the same volume registered on
// multiple maps, but we don't care enough to send mail out
// for it
mail_user_removed_from_vol(oldVolMap.get(volumeId));
}
}
/**
* <p>This method takes care of executing whatever external code is required
* to handle this user being moved from volume to volume</p>
*
* @param oldVolume Invid for old volume listed on a given map
* @param newVolume Invid for new volume listed on a given map
* @param mapName Name of the map this user is being moved on.
*/
private void user_moved_from_vol_to_vol(Invid oldVolume, Invid newVolume, String mapName)
{
// if the system log is null, we're running in the direct loader, and we
// don't want to create anything external.
if (Ganymede.log == null)
{
return;
}
DBObject volumeObj;
DBObject sysObj;
String oldVolName;
String oldVolPath;
Invid oldSysInvid;
String oldSysName;
String newVolName;
String newVolPath;
Invid newSysInvid;
String newSysName;
Vector<Invid> objects = new Vector<Invid>();
StringBuilder buffer = new StringBuilder();
/* -- */
volumeObj = getDBSession().viewDBObject(oldVolume, true);
oldVolName = volumeObj.getLabel();
oldVolPath = (String) volumeObj.getFieldValueLocal(volumeSchema.PATH);
oldSysInvid = (Invid) volumeObj.getFieldValueLocal(volumeSchema.HOST);
objects.add(oldSysInvid);
sysObj = getDBSession().viewDBObject(oldSysInvid, true);
oldSysName = sysObj.getLabel();
volumeObj = getDBSession().viewDBObject(newVolume);
newVolName = volumeObj.getLabel();
newVolPath = (String) volumeObj.getFieldValueLocal(volumeSchema.PATH);
newSysInvid = (Invid) volumeObj.getFieldValueLocal(volumeSchema.HOST);
objects.add(newSysInvid);
sysObj = getDBSession().viewDBObject(newSysInvid);
newSysName = sysObj.getLabel();
Set<String> addresses = DBLog.calculateOwnerAddresses(objects, getDBSession());
buffer.append("Hi. User ");
buffer.append(getLabel());
buffer.append(" was moved from volume ");
buffer.append(oldVolName);
buffer.append(" to volume ");
buffer.append(newVolName);
buffer.append(" in the ");
buffer.append(mapName);
buffer.append(" automounter home map.\n\nSince you are listed in the Ganymede system database");
buffer.append(" as an administrator for a system contained in volume ");
buffer.append(oldVolName);
buffer.append(", you need to take whatever action is appropriate to move this user's");
buffer.append(" directory from ");
buffer.append(oldVolName);
buffer.append(" if you are sure that the user will no longer be using his or her directory");
buffer.append(" on this volume.\n\n");
buffer.append("Volume ");
buffer.append(oldVolName);
buffer.append(" is currently defined as:\n\t");
buffer.append(oldSysName);
buffer.append(":");
buffer.append(oldVolPath);
buffer.append("\n\n");
buffer.append("Volume ");
buffer.append(newVolName);
buffer.append(" is currently defined as:\n\t");
buffer.append(newSysName);
buffer.append(":");
buffer.append(newVolPath);
buffer.append("\n\n");
buffer.append("Thanks for your cooperation.\nYour friend,\n\tGanymede.\n");
editset.logMail(addresses,
"User home directory on map " + mapName + " moved",
buffer.toString());
}
/**
* This method handles external actions for renaming a user.
*/
private void handleUserRename(String orig, String newname)
{
boolean success = false;
/* -- */
// if the system log is null, we're running in the direct loader, and we
// don't want to create anything external.
if (Ganymede.log == null)
{
return;
}
if (debug)
{
System.err.println("userCustom.handleUserRename(): user " + orig +
"is being renamed to " + newname);
}
try
{
if (renameHandler == null)
{
renameFilename = System.getProperty("ganymede.builder.scriptlocation");
if (renameFilename != null)
{
// make sure we've got the path separator at the end of
// renameFilename, add our script name
renameFilename = PathComplete.completePath(renameFilename) + "scripts/directory_namer";
renameHandler = new File(renameFilename);
}
else
{
Ganymede.debug("userCustom.handleUserRename(): Couldn't find " +
"ganymede.builder.scriptlocation property");
}
}
if (renameHandler.exists())
{
try
{
String execLine = renameFilename + " " + orig + " " + newname;
if (debug)
{
System.err.println("handleUserRename: running " + execLine);
}
try
{
if (debug)
{
System.err.println("handleUserRename: blocking");
}
int result = FileOps.runProcess(execLine);
if (debug)
{
System.err.println("handleUserRename: done");
}
if (result != 0)
{
Ganymede.debug("Couldn't handle externals for renaming user " + orig +
" to " + newname + "\n" + renameFilename +
" returned a non-zero result: " + result);
}
else
{
success = true;
}
}
catch (InterruptedException ex)
{
Ganymede.debug("Couldn't handle externals for renaming user " + orig +
" to " +
newname + "\n" +
ex.getMessage());
}
}
catch (IOException ex)
{
Ganymede.debug("Couldn't handle externals for renaming user " + orig +
" to " +
newname + "\n" +
ex.getMessage());
}
}
}
finally
{
Invid admin = getGSession().getPermManager().getPersonaInvid();
String adminName = getGSession().getPermManager().getUserName();
Vector<Invid> objects = new Vector<Invid>();
objects.add(getInvid());
StringBuilder buffer = new StringBuilder();
buffer.append("User ");
buffer.append(orig);
buffer.append(" has been renamed to ");
buffer.append(newname);
buffer.append(".\n\n");
if (success)
{
buffer.append("The user's main home directory has been renamed. You may need ");
buffer.append("to take some action to make sure that the user's account name change ");
buffer.append("doesn't cause problems in your local scripts, etc.\n\n");
}
else
{
buffer.append("The user's main home directory was not able to be properly renamed ");
buffer.append("by Ganymede. You should contact a systems administrator on the user's");
buffer.append("main server to make sure his or her home directory is renamed properly.\n\n");
buffer.append("In addition, you may need ");
buffer.append("to take some action to make sure that the user's account name change ");
buffer.append("doesn't cause problems in your local scripts, etc.\n\n");
}
editset.logEvent("userrenamed",
buffer.toString(),
admin, adminName, objects, null);
}
}
}