Created
June 27, 2012 13:31
Obtain additional user information from ActiveDirectory using Com4J
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// from http://waffle.codeplex.com/workitem/10034 | |
// | |
package waffle.servlet.spi; | |
import java.io.IOException; | |
import java.security.Principal; | |
import java.util.Date; | |
import java.util.Enumeration; | |
import java.util.HashMap; | |
import java.util.Iterator; | |
import java.util.Map; | |
import java.util.StringTokenizer; | |
import javax.servlet.Filter; | |
import javax.servlet.FilterChain; | |
import javax.servlet.FilterConfig; | |
import javax.servlet.ServletException; | |
import javax.servlet.ServletRequest; | |
import javax.servlet.ServletResponse; | |
import javax.servlet.http.HttpServletRequest; | |
import org.apache.commons.logging.Log; | |
import org.apache.commons.logging.LogFactory; | |
import com4j.COM4J; | |
import com4j.Com4jObject; | |
import com4j.Variant; | |
import com4j.typelibs.activeDirectory.IADs; | |
import com4j.typelibs.ado20.ClassFactory; | |
import com4j.typelibs.ado20.Field; | |
import com4j.typelibs.ado20.Fields; | |
import com4j.typelibs.ado20._Command; | |
import com4j.typelibs.ado20._Connection; | |
import com4j.typelibs.ado20._Recordset; | |
/* | |
* Decorates a Request with Active Directory data corresponding to the currently authenticated user. | |
* Requires COM4J. | |
* | |
* Developpers info: http://waffle.codeplex.com/workitem/10034 | |
* | |
* by Christophe Dupriez, DESTIN inc. SSEB. Contributed in 2010/12 to Waffle Project along its OpenSource licence. | |
*/ | |
public class ActiveDirectoryDecoratorFilter implements Filter { | |
protected Log _log = LogFactory.getLog(ActiveDirectoryDecoratorFilter.class); | |
/* | |
<filter> | |
<filter-name>ActiveDirectoryDecoratorFilter</filter-name> | |
<filter-class> | |
waffle.servlet.spi.ActiveDirectoryDecoratorFilter | |
</filter-class> | |
<init-param> | |
<param-name>peremption</param-name> | |
<param-value>1800</param-value> (number of seconds before user data is considered invalid) | |
</init-param> | |
<init-param> | |
<param-name>query</param-name> | |
<param-value>sAMAccountName</param-value> (fields where authentication principal name is searched in AD) | |
</init-param> | |
<init-param> | |
<param-name>query2</param-name> | |
<param-value>userPrincipalName</param-value> (secondary field to search name in AD) | |
</init-param> | |
<init-param> | |
<param-name>request-attribute-where-you-like-to-receive-userPrincipalName</param-name> | |
<param-value>userPrincipalName</param-value> | |
</init-param> | |
<init-param> | |
<param-name>request-attribute-where-you-like-to-receive-someOtherField</param-name> | |
<param-value>someOtherField</param-value> (typical AD fields are: sn,givenName,telephoneNumber...) | |
</init-param> | |
</filter> | |
<filter-mapping> | |
<filter-name>ActiveDirectoryDecoratorFilter</filter-name> | |
<url-pattern>/*</url-pattern> OR LIMITED TO URLs WHERE AD ADDITIONAL INFO IS USEFUL! | |
</filter-mapping> | |
*/ | |
private String defaultNamingContext = null; | |
private final String defaultQueryField = "sAMAccountName"; | |
private final String defaultQueryField2 = "userPrincipalName"; | |
private String queryField; | |
private String queryField2; | |
private final String defaultFields = "distinguishedName,userPrincipalName,sAMAccountName,sn,givenName,telephoneNumber"; | |
private String adFields; | |
private HashMap<String,String> AD2attribute; | |
private HashMap<String, HashMap<String,String>> knownUsersData; | |
private HashMap<String, Date> knownUsersCreated; | |
private long maxAge = 30 * 60 * 1000; // Default age for cached AD information entries is 30 minutes | |
public synchronized void init(FilterConfig filterConfig) throws ServletException { | |
IADs rootDSE = COM4J.getObject(IADs.class, "LDAP://RootDSE", null); | |
defaultNamingContext = (String)rootDSE.get("defaultNamingContext"); | |
knownUsersData = new HashMap<String, HashMap<String,String>>(); | |
knownUsersCreated = new HashMap<String, Date>(); | |
//Get init parameters | |
Enumeration<String> paramNames = filterConfig.getInitParameterNames(); | |
adFields = ""; | |
AD2attribute = new HashMap<String,String>(); | |
queryField = ""; | |
queryField2 = ""; | |
while (paramNames.hasMoreElements()) { | |
String aName = paramNames.nextElement(); | |
if ("peremption".equalsIgnoreCase(aName)) { | |
String value = filterConfig.getInitParameter(aName); // Caution: aName must be used as it may be not in full lowercase... | |
if (value != null && !value.isEmpty()) { | |
long peremption = Long.valueOf(value); | |
if (peremption > 0) { | |
maxAge = peremption*1000; // Cache time to live is in seconds... | |
} | |
} | |
} else if ("query".equalsIgnoreCase(aName)) { | |
String value = filterConfig.getInitParameter(aName); // Caution: aName must be used as it may be not in full lowercase... | |
if (value != null && !value.isEmpty()) { | |
queryField = value; | |
} | |
} else if ("query2".equalsIgnoreCase(aName)) { | |
String value = filterConfig.getInitParameter(aName); | |
if (value != null && !value.isEmpty()) { | |
queryField2 = value; | |
} | |
} else { | |
String value = filterConfig.getInitParameter(aName); | |
if (value != null && !value.isEmpty()) { | |
adFields = (adFields.isEmpty() ? "" : ',')+value; | |
AD2attribute.put(value,aName); | |
} | |
} | |
} | |
if (adFields.isEmpty()) { | |
adFields = defaultFields; | |
StringTokenizer st = new StringTokenizer (adFields,","); | |
while (st.hasMoreTokens()) { | |
String token = st.nextToken(); | |
AD2attribute.put(token, token); | |
} | |
} | |
if (queryField.isEmpty()) { | |
queryField = defaultQueryField; | |
queryField2 = defaultQueryField2; | |
} | |
_log.info("LDAP root="+defaultNamingContext+", query="+queryField+", query2="+queryField2+", AD fields="+adFields); | |
} | |
public synchronized void destroy() { | |
defaultNamingContext = null; | |
knownUsersData = null; | |
knownUsersCreated = null; | |
adFields = null; | |
_log.info("destroyed"); | |
} | |
private static void store (ServletRequest request, HashMap<String,String> attributes) { | |
for (Map.Entry<String,String> anAttr : attributes.entrySet()) { | |
request.setAttribute(anAttr.getKey(),anAttr.getValue()); | |
} | |
} | |
synchronized private void gatherUserData (ServletRequest req, String name) { | |
Date nameCreated = knownUsersCreated.get(name); | |
Date now = new Date(); | |
if (nameCreated != null && nameCreated.getTime() > (now.getTime() - maxAge) ) { | |
store (req,knownUsersData.get(name)); | |
} else { | |
knownUsersData.remove(name); | |
knownUsersCreated.remove(name); | |
_Connection con = ClassFactory.createConnection(); | |
con.provider("ADsDSOObject"); | |
con.open("Active Directory Provider",""/*default*/,""/*default*/,-1/*default*/); | |
// query LDAP to find out the LDAP DN and other info for the given user from the login ID | |
_Command cmd = ClassFactory.createCommand(); | |
cmd.activeConnection(con); | |
String command = "<LDAP://"+defaultNamingContext+">;("+queryField+"="+name+");"+adFields+";subTree"; | |
_log.debug("Command="+command); | |
cmd.commandText(command); | |
_Recordset rs = cmd.execute(null, Variant.MISSING, -1/*default*/); | |
if(rs.eof()) { // User not found! | |
command = "<LDAP://"+defaultNamingContext+">;("+queryField2+"="+name+");"+adFields+";subTree"; | |
_log.debug("Command="+command); | |
cmd.commandText(command); | |
rs = cmd.execute(null, Variant.MISSING, -1/*default*/); | |
} | |
if(rs.eof()) { // User not found! | |
_log.error(name+" not found."); | |
} | |
else { | |
Fields userData = rs.fields(); | |
if (userData != null) { | |
Iterator<Com4jObject> itCom = userData.iterator(); | |
int i=0; | |
HashMap<String,String> userDataAttributes = new HashMap<String,String>(); | |
while (itCom.hasNext()) { | |
Field comObj = (Field)itCom.next(); | |
String attribute = AD2attribute.get(comObj.name()); | |
if (attribute != null && !attribute.isEmpty()) { | |
_log.debug(i++ +") " +attribute+":"+comObj.name()+"="+comObj.value().toString()); | |
userDataAttributes.put(attribute, comObj.value().toString()); | |
} | |
} | |
store (req,userDataAttributes); | |
knownUsersData.put(name, userDataAttributes); | |
knownUsersCreated.put(name, now); | |
} | |
else { | |
_log.error("User "+name+" AD information is empty?"); | |
} | |
} | |
rs.close(); | |
con.close(); | |
} | |
} | |
public void doFilter(ServletRequest req, ServletResponse res, | |
FilterChain chain) throws IOException, ServletException { | |
if (req instanceof HttpServletRequest) { | |
HttpServletRequest request = (HttpServletRequest) req; | |
// initNamingContext(true); This could be added in case Filter init is not ALWAYS called: let's trust the spec for now! | |
if (defaultNamingContext != null) { | |
Principal currPrincipal = request.getUserPrincipal(); | |
if (currPrincipal != null) { | |
String name = currPrincipal.getName(); | |
if (name != null && !name.isEmpty()) { | |
gatherUserData(req,name); | |
} | |
} | |
} | |
} | |
chain.doFilter(req, res); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// from http://waffle.codeplex.com/workitem/10034 | |
// | |
import java.util.HashMap; | |
import org.apache.commons.logging.Log; | |
import org.apache.commons.logging.LogFactory; | |
import com4j.COM4J; | |
import com4j.ComException; | |
import com4j.Variant; | |
import com4j.typelibs.activeDirectory.IADs; | |
import com4j.typelibs.ado20.ClassFactory; | |
import com4j.typelibs.ado20.Fields; | |
import com4j.typelibs.ado20._Command; | |
import com4j.typelibs.ado20._Connection; | |
import com4j.typelibs.ado20._Recordset; | |
public class UserInfo { | |
protected Log _log = LogFactory.getLog(UserInfo.class); | |
static String defaultNamingContext = null; | |
static final String usefulFields = "distinguishedName,userPrincipalName,sAMAccountName,sn,givenName,telephoneNumber"; | |
String dn; // distinguishedName | |
String upn; // userPrincipalName | |
String fqn; // sAMAccountName; | |
String sn; // surname | |
String givenName; | |
String telephoneNumber; | |
static HashMap<String, UserInfo> knownUsers = new HashMap<String, UserInfo>(); | |
synchronized void initNamingContext() { | |
if (defaultNamingContext == null) { | |
IADs rootDSE = COM4J.getObject(IADs.class, "LDAP://RootDSE", null); | |
defaultNamingContext = (String)rootDSE.get("defaultNamingContext"); | |
_log.error("defaultNamingContext="+defaultNamingContext); | |
} | |
} | |
synchronized public static UserInfo getInstance(String username) { | |
UserInfo found = knownUsers.get(username); | |
if (found != null) return found; | |
return getInstanceNoCache(username); | |
} | |
synchronized public static UserInfo getInstanceNoCache(String username) { | |
UserInfo found = new UserInfo(username); | |
if (found.dn == null) { | |
return null; | |
} | |
knownUsers.put(username, found); | |
return found; | |
} | |
private UserInfo (String username) { | |
initNamingContext(); | |
if (defaultNamingContext == null) { | |
return; | |
} | |
// Searching LDAP requires ADO [8], so it's good to create a connection upfront for reuse. | |
_Connection con = ClassFactory.createConnection(); | |
con.provider("ADsDSOObject"); | |
con.open("Active Directory Provider",""/*default*/,""/*default*/,-1/*default*/); | |
// query LDAP to find out the LDAP DN and other info for the given user from the login ID | |
_Command cmd = ClassFactory.createCommand(); | |
cmd.activeConnection(con); | |
String searchField = "userPrincipalName"; | |
int pSlash = username.indexOf('\\'); | |
if (pSlash > 0) { | |
searchField = "sAMAccountName"; | |
username = username.substring(pSlash+1); | |
} | |
_log.error("Command="+"<LDAP://"+defaultNamingContext+">;("+searchField+"="+username+");"+usefulFields+";subTree"); | |
cmd.commandText("<LDAP://"+defaultNamingContext+">;("+searchField+"="+username+");"+usefulFields+";subTree"); | |
_Recordset rs = cmd.execute(null, Variant.MISSING, -1/*default*/); | |
if(rs.eof()) { // User not found! | |
_log.error(username+" not found."); | |
} | |
else { | |
Fields userData = rs.fields(); | |
if (userData != null) { | |
/* Iterator<Com4jObject> itCom = userData.iterator(); | |
int i=0; | |
while (itCom.hasNext()) { | |
Field comObj = (Field)itCom.next(); | |
_log.error(i++ +":"+comObj.name()+"="+comObj.value().toString()); | |
} */ | |
Object o; | |
try { | |
o = userData.item("distinguishedName").value(); | |
if (o != null) dn = o.toString(); | |
} catch (ComException ecom ) { | |
_log.error("distinguishedName not returned:"+ecom.getMessage()); | |
} | |
try { | |
o = userData.item("userPrincipalName").value(); | |
if (o != null) upn = o.toString(); | |
} catch (ComException ecom ) { | |
_log.error("userPrincipalName not returned:"+ecom.getMessage()); | |
} | |
try { | |
o = userData.item("sAMAccountName").value(); | |
if (o != null) fqn = o.toString(); | |
} catch (ComException ecom ) { | |
_log.error("sAMAccountName not returned:"+ecom.getMessage()); | |
} | |
try { | |
o = userData.item("sn").value(); | |
if (o != null) sn = o.toString(); | |
} catch (ComException ecom ) { | |
_log.error("sn not returned:"+ecom.getMessage()); | |
} | |
try { | |
o = userData.item("givenName").value(); | |
if (o != null) givenName = o.toString(); | |
} catch (ComException ecom ) { | |
_log.error("givenName not returned:"+ecom.getMessage()); | |
} | |
try { | |
o = userData.item("telephoneNumber").value(); | |
if (o != null) telephoneNumber = o.toString(); | |
} catch (ComException ecom ) { | |
_log.error("telephoneNumber not returned:"+ecom.getMessage()); | |
} | |
} | |
else { | |
_log.error("User "+username+" information is empty?"); | |
} | |
} | |
rs.close(); | |
con.close(); | |
} | |
public static String getDefaultNamingContext() { | |
return defaultNamingContext; | |
} | |
public String getDn() { | |
return dn; | |
} | |
public String getUpn() { | |
return upn; | |
} | |
public String getFqn() { | |
return fqn; | |
} | |
public String getSn() { | |
return sn; | |
} | |
public String getGivenName() { | |
return givenName; | |
} | |
public String getTelephoneNumber() { | |
return telephoneNumber; | |
} | |
} |
Thanks a lot Samoth for the advice, it really helped me!
Thanks for this solution! I'm still having one problem though. This is the exception that I'm getting: "java.lang.ClassCastException: $Proxy101 cannot be cast to com4j.typelibs.ado20.Field" at line 203 of the ActiveDirectoryDecoratorFilter. Any idea of what is happening here? I'm using the "20120426-2" version of com4j (because I'm on a 64 bit OS)
Thanks
update: the objects seem to be of the Wrapper instead of Field
I had to adapt the code of the filter in order to make it work on my end.
Fields userData = rs.fields();
if (userData == null || userData.count() <= 0) {
_log.error("User " + name + " AD information is empty?");
} else {
final Map<String, String> userDataAttributes = new HashMap<String, String>();
for (int i = 0; i < userData.count(); i++) {
final Field field = userData.item(i);
String attribute = AD2attribute.get(field.name());
if (attribute != null && !attribute.isEmpty()) {
_log.debug(i + ") " + field.name() + "=" + field.value().toString());
userDataAttributes.put(attribute, field.value().toString());
}
store(req, userDataAttributes);
knownUsersData.put(name, userDataAttributes);
knownUsersCreated.put(name, now);
}
}
@Samoth and @TimmyStorms so much thanks for this!
for those coming along here in 2024, it even works on x64 with those artifacts:
<dependency>
<groupId>org.jvnet.com4j</groupId>
<artifactId>com4j</artifactId>
<version>2.1</version>
</dependency>
<dependency>
<groupId>org.jvnet.com4j.typelibs</groupId>
<artifactId>active-directory</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.jvnet.com4j.typelibs</groupId>
<artifactId>ado20</artifactId>
<version>1.0</version>
</dependency>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Use
Variant.getMissing()
insteadVariant.MISSING
userinfo.java, line 85
Otherwise, you will receive exception:
com4j.ComException: 800a0e7c (Unknown error) : Parameter object is improperly defined. Inconsistent or incomplete information was provided.