/* $Id: CmisAuthorityConnector.java 1327112 2012-04-17 14:28:14Z ridder $ */
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.manifoldcf.crawler.connectors.cmis;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import org.apache.commons.lang.StringUtils;
import org.apache.manifoldcf.authorities.authorities.BaseAuthorityConnector;
import org.apache.manifoldcf.authorities.interfaces.AuthorizationResponse;
import org.apache.manifoldcf.core.interfaces.CacheManagerFactory;
import org.apache.manifoldcf.core.interfaces.ConfigParams;
import org.apache.manifoldcf.core.interfaces.ICacheCreateHandle;
import org.apache.manifoldcf.core.interfaces.ICacheDescription;
import org.apache.manifoldcf.core.interfaces.ICacheHandle;
import org.apache.manifoldcf.core.interfaces.ICacheManager;
import org.apache.manifoldcf.core.interfaces.IHTTPOutput;
import org.apache.manifoldcf.core.interfaces.IPostParameters;
import org.apache.manifoldcf.core.interfaces.IThreadContext;
import org.apache.manifoldcf.core.interfaces.ManifoldCFException;
import org.apache.manifoldcf.core.interfaces.StringSet;
import org.apache.manifoldcf.crawler.system.Logging;
import org.apache.manifoldcf.ui.util.Encoder;
/**
*
* The CMIS Authority Connector is based only on a regular expression checker, because the CMIS specification
* (and the Apache Chemistry implementation) doesn't have any exposed method to get info about users.
*
* For the configuration we can assume that we could have different users for any CMIS repository that
* could have the same endpoint. That's why for this connector is required the Repository ID.
*
* @author Piergiorgio Lucidi
*
*/
public class CmisAuthorityConnector extends BaseAuthorityConnector {
public static final String CONFIG_PARAM_USERNAME = "username";
public static final String CONFIG_PARAM_PASSWORD = "password";
public static final String CONFIG_PARAM_ENDPOINT = "endpoint";
public static final String CONFIG_PARAM_REPOSITORY_ID = "repositoryId";
/** User name mapping parameter */
protected static final String CONFIG_PARAM_USERMAPPING = "usermapping";
protected static final String CONFIG_PARAM_USERNAME_REGEXP = "usernameregexp";
protected static final String CONFIG_PARAM_USER_TRANSLATION = "usertranslation";
private static final String DEFAULT_VALUE_ENDPOINT = "http://localhost:8080/cmis/";
private static final String DEFAULT_VALUE_REPOSITORY_ID = "uuid";
protected String endpoint = null;
protected String repositoryId = null;
protected Map<String, String> parameters = new HashMap<String, String>();
/** The cache manager. */
protected ICacheManager cacheManager = null;
/** Match map for username mapping */
protected MatchMap matchMap = null;
protected static long responseLifetime = 60000L;
protected static int LRUsize = 1000;
protected static StringSet emptyStringSet = new StringSet();
/** This is the active directory global deny token. This should be ingested with all documents. */
public static final String GLOBAL_DENY_TOKEN = "DEAD_AUTHORITY";
/** Unreachable CMIS */
private static final AuthorizationResponse unreachableResponse = new AuthorizationResponse(
new String[]{GLOBAL_DENY_TOKEN},AuthorizationResponse.RESPONSE_UNREACHABLE);
/** User not found */
private static final AuthorizationResponse userNotFoundResponse = new AuthorizationResponse(
new String[]{GLOBAL_DENY_TOKEN},AuthorizationResponse.RESPONSE_USERNOTFOUND);
/** Set thread context.
*/
@Override
public void setThreadContext(IThreadContext tc)
throws ManifoldCFException {
super.setThreadContext(tc);
cacheManager = CacheManagerFactory.make(tc);
}
/** Clear thread context.
*/
@Override
public void clearThreadContext() {
super.clearThreadContext();
cacheManager = null;
}
public CmisAuthorityConnector() {
super();
}
/**
* Output the configuration body section. This method is called in the body
* section of the authority connector's configuration page. Its purpose is to
* present the required form elements for editing. The coder can presume that
* the HTML that is output from this configuration will be within appropriate
* <html>, <body>, and <form> tags. The name of the form is "editconnection".
*
* @param threadContext
* is the local thread context.
* @param out
* is the output to which any HTML should be sent.
* @param parameters
* are the configuration parameters, as they currently exist, for
* this connection being configured.
* @param tabName
* is the current tab name.
*/
@Override
public void outputConfigurationBody(IThreadContext threadContext,
IHTTPOutput out, Locale locale, ConfigParams parameters, String tabName)
throws ManifoldCFException, IOException {
String endpoint = parameters.getParameter(CONFIG_PARAM_ENDPOINT);
String repositoryId = parameters.getParameter(CONFIG_PARAM_REPOSITORY_ID);
if(StringUtils.isEmpty(endpoint))
endpoint = DEFAULT_VALUE_ENDPOINT;
if(StringUtils.isEmpty(repositoryId))
repositoryId = DEFAULT_VALUE_REPOSITORY_ID;
String userMappingString = parameters.getParameter(CONFIG_PARAM_USERMAPPING);
MatchMap localMap;
if (StringUtils.isNotEmpty(userMappingString)){
localMap = new MatchMap(userMappingString);
} else {
localMap = new MatchMap();
localMap.appendMatchPair("(.*)","$(1)");
}
String usernameRegexp = localMap.getMatchString(0);
String userTranslation = localMap.getReplaceString(0);
if (tabName.equals(Messages.getString(locale,"CmisAuthorityConnector.Repository")))
{
out.print("<table class=\"displaytable\">\n"
+ " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n");
out.print("<tr><td class=\"description\"><nobr>" + Messages.getBodyString(locale,"CmisAuthorityConnector.Endpoint") + "</nobr></td>" +
"<td class=\"value\"><input type=\"text\" name=\""
+ CONFIG_PARAM_ENDPOINT + "\" value=\""+Encoder.attributeEscape(endpoint)+"\" size=\"50\"/></td></tr>\n");
out.print("<tr><td class=\"description\"><nobr>" + Messages.getBodyString(locale,"CmisAuthorityConnector.RepositoryID") + "</nobr></td>" +
"<td class=\"value\"><input type=\"text\" name=\""
+ CONFIG_PARAM_REPOSITORY_ID + "\" value=\""+Encoder.attributeEscape(repositoryId)+"\"/></td></tr>\n");
out.print("</table>\n");
}
else
{
out.print("<input type=\"hidden\" name=\""+CONFIG_PARAM_ENDPOINT+"\" value=\""+Encoder.attributeEscape(endpoint)+"\"/>\n");
out.print("<input type=\"hidden\" name=\""+CONFIG_PARAM_REPOSITORY_ID+"\" value=\""+Encoder.attributeEscape(repositoryId)+"\"/>\n");
}
if (tabName.equals(Messages.getString(locale,"CmisAuthorityConnector.UserMapping")))
{
out.print(
"<table class=\"displaytable\">\n"+
" <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"+
" <tr>\n"+
" <td class=\"description\"><nobr>" + Messages.getBodyString(locale,"CmisAuthorityConnector.UserMapping") + "</nobr></td>\n"+
" <td class=\"value\">\n"+
" <input type=\"text\" size=\"32\" name=\""+CONFIG_PARAM_USERNAME_REGEXP+"\" value=\""+
Encoder.attributeEscape(usernameRegexp)+"\"/> ==> \n"+
" <input type=\"text\" size=\"32\" name=\""+CONFIG_PARAM_USER_TRANSLATION+"\" value=\""+
Encoder.attributeEscape(userTranslation)+"\"/>\n"+
" </td>\n"+
" </tr>\n"+
"</table>\n"
);
}
else
{
out.print(
"<input type=\"hidden\" name=\"usernameregexp\" value=\""+
Encoder.attributeEscape(usernameRegexp)+"\"/>\n"+
"<input type=\"hidden\" name=\"usertranslation\" value=\""+
Encoder.attributeEscape(userTranslation)+"\"/>\n"
);
}
}
/**
* Output the configuration header section. This method is called in the head
* section of the connector's configuration page. Its purpose is to add the
* required tabs to the list, and to output any javascript methods that might
* be needed by the configuration editing HTML.
*
* @param threadContext
* is the local thread context.
* @param out
* is the output to which any HTML should be sent.
* @param parameters
* are the configuration parameters, as they currently exist, for
* this connection being configured.
* @param tabsArray
* is an array of tab names. Add to this array any tab names that are
* specific to the connector.
*/
@Override
public void outputConfigurationHeader(IThreadContext threadContext,
IHTTPOutput out, Locale locale, ConfigParams parameters, List<String> tabsArray)
throws ManifoldCFException, IOException {
tabsArray.add(Messages.getString(locale,"CmisAuthorityConnector.Repository"));
tabsArray.add(Messages.getString(locale,"CmisAuthorityConnector.UserMapping"));
out.print("<script type=\"text/javascript\">\n" + "<!--\n"
+ "function checkConfig()\n" + "{\n"
+ " if (editconnection.endpoint.value == \"\")\n" + " {\n"
+ " alert(\"" + Messages.getBodyJavascriptString(locale,"CmisAuthorityConnector.TheEndpointMustBeNotNull") + "\");\n"
+ " editconnection.endpoint.focus();\n" + " return false;\n"
+ " }\n" + "\n" + " return true;\n" + "}\n" + " \n"
+ "function checkConfigForSave()\n" + "{\n"
+ " if (editconnection.endpoint.value == \"\")\n" + " {\n"
+ " alert(\"" + Messages.getBodyJavascriptString(locale,"CmisAuthorityConnector.TheEndpointMustBeNotNull") + "\");\n"
+ " editconnection.endpoint.focus();\n" + " return false;\n"
+ " }\n" + " if (editconnection.repositoryId.value == \"\")\n" + " {\n"
+ " alert(\"" + Messages.getBodyJavascriptString(locale,"CmisAuthorityConnector.TheEndpointMustBeNotNull") + "\");\n"
+ " editconnection.repositoryId.focus();\n" + " return false;\n"
+ " }\n" + " return true;\n" + "}\n" + "\n" + "//-->\n"
+ "</script>\n");
}
/**
* Process a configuration post. This method is called at the start of the
* connector's configuration page, whenever there is a possibility that form
* data for a connection has been posted. Its purpose is to gather form
* information and modify the configuration parameters accordingly. The name
* of the posted form is "editconnection".
*
* @param threadContext
* is the local thread context.
* @param variableContext
* is the set of variables available from the post, including binary
* file post information.
* @param parameters
* are the configuration parameters, as they currently exist, for
* this connection being configured.
* @return null if all is well, or a string error message if there is an error
* that should prevent saving of the connection (and cause a
* redirection to an error page).
*/
@Override
public String processConfigurationPost(IThreadContext threadContext,
IPostParameters variableContext, Locale locale, ConfigParams parameters)
throws ManifoldCFException {
//Repository
String endpoint = variableContext.getParameter(CONFIG_PARAM_ENDPOINT);
if (StringUtils.isNotEmpty(endpoint) && endpoint.length() > 0)
parameters.setParameter(CONFIG_PARAM_ENDPOINT, endpoint);
String repositoryId = variableContext
.getParameter(CONFIG_PARAM_REPOSITORY_ID);
if (StringUtils.isNotEmpty(repositoryId))
parameters.setParameter(CONFIG_PARAM_REPOSITORY_ID, repositoryId);
//User Mapping
String usernameRegexp = variableContext.getParameter(CONFIG_PARAM_USERNAME_REGEXP);
String userTranslation = variableContext.getParameter(CONFIG_PARAM_USER_TRANSLATION);
if (StringUtils.isNotEmpty(usernameRegexp) && StringUtils.isNotEmpty(userTranslation))
{
MatchMap localMap = new MatchMap();
localMap.appendMatchPair(usernameRegexp,userTranslation);
parameters.setParameter(CONFIG_PARAM_USERMAPPING,localMap.toString());
}
return null;
}
/**
* View configuration. This method is called in the body section of the
* connector's view configuration page. Its purpose is to present the
* connection information to the user. The coder can presume that the HTML that
* is output from this configuration will be within appropriate <html> and
* <body> tags.
*
* @param threadContext
* is the local thread context.
* @param out
* is the output to which any HTML should be sent.
* @param parameters
* are the configuration parameters, as they currently exist, for
* this connection being configured.
*/
@Override
public void viewConfiguration(IThreadContext threadContext, IHTTPOutput out,
Locale locale, ConfigParams parameters) throws ManifoldCFException, IOException {
out.print("<table class=\"displaytable\">\n"
+ " <tr>\n"
+ " <td class=\"description\" colspan=\"1\"><nobr>" + Messages.getBodyString(locale,"CmisAuthorityConnector.Parameters") + "</nobr></td>\n"
+ " <td class=\"value\" colspan=\"3\">\n");
Iterator iter = parameters.listParameters();
while (iter.hasNext()) {
String param = (String) iter.next();
String value = parameters.getParameter(param);
out.print(" <nobr>"
+ org.apache.manifoldcf.ui.util.Encoder.bodyEscape(param) + "="
+ org.apache.manifoldcf.ui.util.Encoder.bodyEscape(value)
+ "</nobr><br/>\n");
}
out.print("</td>\n" + " </tr>\n" + "</table>\n");
}
/** Obtain the default access tokens for a given user name.
*@param userName is the user name or identifier.
*@return the default response tokens, presuming that the connect method fails.
*/
@Override
public AuthorizationResponse getDefaultAuthorizationResponse(String userName)
{
return unreachableResponse;
}
/** Uncached version of the getAuthorizationResponse method.
*@param userName is the user name or identifier.
*@return the response tokens (according to the current authority).
* (Should throws an exception only when a condition cannot be properly described within the authorization response object.)
*/
protected AuthorizationResponse getAuthorizationResponseUncached(String userName)
throws ManifoldCFException
{
if (Logging.connectors.isDebugEnabled())
Logging.connectors.debug("CMIS: Calculating response access tokens for user '"+userName+"'");
// Map the user to the final value
String verifiedUserName = StringUtils.EMPTY;
try {
verifiedUserName = matchMap.translate(userName);
} catch (Exception e) {
return userNotFoundResponse;
}
if (Logging.connectors.isDebugEnabled())
Logging.connectors.debug("CMIS: Mapped user name is '"+verifiedUserName+"'");
String[] tokens = new String[1];
tokens[0] = verifiedUserName;
return new AuthorizationResponse(tokens,AuthorizationResponse.RESPONSE_OK);
}
/** Obtain the access tokens for a given user name.
*@param userName is the user name or identifier.
*@return the response tokens (according to the current authority).
* (Should throws an exception only when a condition cannot be properly described within the authorization response object.)
*/
@Override
public AuthorizationResponse getAuthorizationResponse(String userName)
throws ManifoldCFException
{
if (Logging.connectors.isDebugEnabled())
Logging.connectors.debug("CMIS: Received request for user '"+userName+"'");
ICacheDescription objectDescription = new AuthorizationResponseDescription(userName,endpoint,repositoryId,matchMap.toString());
// Enter the cache
ICacheHandle ch = cacheManager.enterCache(new ICacheDescription[]{objectDescription},null,null);
try
{
ICacheCreateHandle createHandle = cacheManager.enterCreateSection(ch);
try
{
// Lookup the object
AuthorizationResponse response = (AuthorizationResponse)cacheManager.lookupObject(createHandle,objectDescription);
if (response != null)
return response;
// Create the object.
response = getAuthorizationResponseUncached(userName);
// Save it in the cache
cacheManager.saveObject(createHandle,objectDescription,response);
// And return it...
return response;
}
finally
{
cacheManager.leaveCreateSection(createHandle);
}
}
finally
{
cacheManager.leaveCache(ch);
}
}
/** This is the cache object descriptor for cached access tokens from
* this connector.
*/
protected static class AuthorizationResponseDescription extends org.apache.manifoldcf.core.cachemanager.BaseDescription
{
/** The user name associated with the access tokens */
protected String userName;
/** The repository endpoint */
protected String endpoint;
/** The repository id */
protected String repositoryId;
/** The user mapping */
protected String userMapping;
/** The expiration time */
protected long expirationTime = -1;
/** Constructor. */
public AuthorizationResponseDescription(String userName, String endpoint, String repositoryId, String userMapping)
{
super("CMISAuthority",LRUsize);
this.userName = userName;
this.endpoint = endpoint;
this.repositoryId = repositoryId;
this.userMapping = userMapping;
}
/** Return the invalidation keys for this object. */
public StringSet getObjectKeys()
{
return emptyStringSet;
}
/** Get the critical section name, used for synchronizing the creation of the object */
public String getCriticalSectionName()
{
return getClass().getName() + "-" + userName + "-" + endpoint +
"-" + repositoryId + "_" + userMapping;
}
/** Return the object expiration interval */
public long getObjectExpirationTime(long currentTime)
{
if (expirationTime == -1)
expirationTime = currentTime + responseLifetime;
return expirationTime;
}
public int hashCode()
{
return userName.hashCode() + endpoint.hashCode() + repositoryId.hashCode() + userMapping.hashCode();
}
public boolean equals(Object o)
{
if (!(o instanceof AuthorizationResponseDescription))
return false;
AuthorizationResponseDescription ard = (AuthorizationResponseDescription)o;
return ard.userName.equals(userName) && ard.endpoint.equals(endpoint) &&
repositoryId.equals(repositoryId) && ard.userMapping.equals(userMapping);
}
}
}