package com.ibm.sbt.opensocial.domino.servlets;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lotus.domino.Session;
import org.apache.commons.lang3.StringUtils;
import org.apache.shindig.auth.AbstractSecurityToken;
import org.apache.shindig.auth.AbstractSecurityToken.Keys;
import org.apache.shindig.auth.SecurityTokenCodec;
import org.apache.shindig.common.servlet.InjectedServlet;
import org.apache.shindig.config.ContainerConfig;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.ibm.commons.util.io.json.JsonException;
import com.ibm.commons.util.io.json.JsonGenerator;
import com.ibm.commons.util.io.json.JsonJavaFactory;
import com.ibm.domino.osgi.core.context.ContextInfo;
import com.ibm.fiesta.commons.IdUtil;
import com.ibm.fiesta.commons.security.SimpleSecurityToken;
import com.ibm.sbt.opensocial.domino.internal.OpenSocialPlugin;
/**
* This servlet acts to generate security tokens for authenticated users.
* Supports GET only. The desired security token contents should be defined as
* query parameters.
*
* Parameters in the request to this servlet should be keyed by
* link AbstractSecurityToken.Keys
*
* The response, if there was no error, will be JSON of the form
*
* <code>
* {
* "token":[encrypted token],
* "ttl":[the time to live of the token]
* }
* </code>
*/
public class SecurityTokenServlet extends InjectedServlet {
private static final long serialVersionUID = 1L;
private static final String RESP_TTL_KEY = "ttl"; //$NON-NLS-1$
private static final String RESP_TOKEN_KEY = "token"; //$NON-NLS-1$
private static final String METHOD = "doGet"; //$NON-NLS-1$
private static final String ANONYMOUS = "@anonymous";
private static final String CLASSNAME = SecurityTokenServlet.class.getName();
private final Logger logger = OpenSocialPlugin.getLogger();
private SecurityTokenCodec codec;
private ContainerConfig config;
@Inject
public void setSecurityTokenCodec(SecurityTokenCodec codec) {
// Call checkInitialized() as part of the contract with InjectedServlet
checkInitialized();
this.codec = codec;
}
@Inject
public void setContainerConfig(ContainerConfig config) {
this.config = config;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// Before we do anything let's log some information about the request
// for debugging purposes
logRequestInfo(req);
// Just in case
if (this.codec == null) {
sendError(
req,
resp,
"The codec is null", //$NON-NLS-1$
HttpServletResponse.SC_INTERNAL_SERVER_ERROR
);
return;
}
try {
String userId = null;
try {
userId = getUserId();
} catch (Throwable t) {
sendError(
req,
resp,
"There was an error getting the user id.",
HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
t
);
return;
}
SimpleSecurityToken token = generateSecurityToken(req, userId, userId);
if (this.logger.isLoggable(Level.FINEST)) {
// Rope this off with an isLoggable so we don't do the work of token.toString() all the time
this.logger.finest("SecurityTokenServlet - encoding values: " + token); //$NON-NLS-1$
}
String tokenString = this.codec.encodeToken(token);
this.logger.finest("SecurityTokenServlet - encoded token: " + tokenString); //$NON-NLS-1$
// It's just the string. No need for JSON or XML
resp.setContentType("application/json;charset=UTF-8"); //$NON-NLS-1$
// Keep people from using this outside of an XHR
resp.setHeader("Content-Disposition", "attachment;filename=foo.txt"); //$NON-NLS-1$ //$NON-NLS-2$
// The requests coming in will be identical so browsers MUST NOT
// cached
resp.setHeader("Pragma", "no-cache"); //$NON-NLS-1$ //$NON-NLS-2$
resp.setHeader("Cache-Control", "no-cache"); //$NON-NLS-1$ //$NON-NLS-2$
String responseJsonString = buildResponseString(token, tokenString);
// Last but not least, set the content length
resp.setContentLength(responseJsonString.length());
// Write the response
PrintWriter writer = resp.getWriter();
writer.print(responseJsonString);
writer.flush();
} catch (Exception e) {
sendError(req, resp, e.getMessage(), HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}
}
private String getUserId() throws Throwable {
if(ContextInfo.isAnonymous() || ContextInfo.getUserSession() == null) {
return ANONYMOUS;
} else {
return getUserIdFromSession(ContextInfo.getUserSession());
}
}
private String getUserIdFromSession(Session userSession) throws Throwable {
String userId = null;
String userName = userSession.getEffectiveUserName();
try {
if(StringUtils.containsIgnoreCase(userName, "CN=")) {
userId = getCanonicalShindigId(userName);
} else {
userId = IdUtil.getShindigId(userName);
}
} catch (Throwable t) {
throw t;
}
return userId;
}
private String getCanonicalShindigId(String userName) {
//An OpenSocial user id can only have alpha numeric characters and periods, underscores, and dashes
String id = userName.replace("=", ".");
id = id.replace("/", "_");
return id;
}
// Build a response that includes the token itself and some other metadata,
// e.g, ttl
private String buildResponseString(SimpleSecurityToken token, String tokenString) throws JsonException, IOException {
Map<String, Object> jsonMap = Maps.newHashMap();
jsonMap.put(RESP_TTL_KEY, token.getTokenTTL());
jsonMap.put(RESP_TOKEN_KEY, tokenString);
return JsonGenerator.toJson(JsonJavaFactory.instance, jsonMap, true);
}
private void sendError(HttpServletRequest req, HttpServletResponse resp, String message, int code)
throws IOException {
this.logger.logp(Level.WARNING, CLASSNAME, METHOD,
"warn.security.servlet.response", new Object[] { code, message }); //$NON-NLS-1$
resp.sendError(code, message);
}
private void sendError(HttpServletRequest req, HttpServletResponse resp, String message, int code, Throwable t)
throws IOException {
sendError(req, resp, message, code);
this.logger.logp(Level.WARNING, CLASSNAME, METHOD, t.getMessage(), t);
}
private SimpleSecurityToken generateSecurityToken(HttpServletRequest req, String viewerId, String ownerId) {
Map<String, String> sanitizedParameters = Maps.newHashMap();
@SuppressWarnings("unchecked")
Map<String, String[]> inputParameters = req.getParameterMap();
Set<String> inputKeySet = inputParameters.keySet();
String[] inputValue;
for (String inputKey : inputKeySet) {
inputValue = inputParameters.get(inputKey);
if (inputValue != null && inputValue.length > 0) {
sanitizedParameters.put(inputKey, inputValue[0]);
}
}
// Ignore the viewer and owner if they were passed on the query string
// and add it based on the user session
sanitizedParameters.put(Keys.VIEWER.getKey(), viewerId);
sanitizedParameters.put(Keys.OWNER.getKey(), ownerId);
return new DominoSecurityToken(sanitizedParameters, this.config);
}
private void logRequestInfo(HttpServletRequest req) {
this.logger.finest("SecurityTokenServlet - remote address: " + req.getRemoteAddr()); //$NON-NLS-1$
this.logger.finest("SecurityTokenServlet - query string: " + req.getQueryString()); //$NON-NLS-1$
}
private class DominoSecurityToken extends SimpleSecurityToken {
private boolean isAnonymous = false;
private ContainerConfig config;
public DominoSecurityToken(Map<String, String> params, ContainerConfig config) {
super(params);
if(ANONYMOUS.equals(params.get(Keys.VIEWER.getKey()))) {
isAnonymous = true;
}
this.config = config;
}
@Override
public boolean isAnonymous() {
return isAnonymous;
}
@Override
public int getTokenTTL() {
//Use the gadget security token ttl for the container security token ttl
return config.getInt(getContainer(), SecurityTokenCodec.SECURITY_TOKEN_TTL_CONFIG);
}
}
}