blob: 237c49360098bf8842958bf14bea6ab3c7a45024 [file] [log] [blame]
/* $Id: UserACLServlet.java 988245 2010-08-23 18:39:35Z kwright $ */
/**
* 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.authorityservlet;
import org.apache.manifoldcf.core.interfaces.*;
import org.apache.manifoldcf.authorities.interfaces.*;
import org.apache.manifoldcf.authorities.system.ManifoldCF;
import org.apache.manifoldcf.authorities.system.Logging;
import org.apache.manifoldcf.authorities.system.RequestQueue;
import org.apache.manifoldcf.authorities.system.AuthRequest;
import org.apache.manifoldcf.authorities.system.MappingRequest;
import org.apache.manifoldcf.core.util.URLEncoder;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
/** This servlet class is meant to receive a user name and return a list of access tokens.
* The user name is expected to be sent as an argument on the url (the "username" argument), and the
* response will simply be a list of access tokens separated by newlines.
* This is guaranteed safe because the index system cannot work with access tokens that aren't 7-bit ascii that
* have any control characters in them.
*
* Errors will simply report back with an empty acl.
*
* The content type will always be text/plain.
*/
public class UserACLServlet extends HttpServlet
{
public static final String _rcsid = "@(#)$Id: UserACLServlet.java 988245 2010-08-23 18:39:35Z kwright $";
protected final static String AUTHORIZED_VALUE = "AUTHORIZED:";
protected final static String UNREACHABLE_VALUE = "UNREACHABLEAUTHORITY:";
protected final static String UNAUTHORIZED_VALUE = "UNAUTHORIZED:";
protected final static String USERNOTFOUND_VALUE = "USERNOTFOUND:";
protected final static String ID_PREFIX = "ID:";
protected final static String TOKEN_PREFIX = "TOKEN:";
/** The init method.
*/
public void init(ServletConfig config)
throws ServletException
{
super.init(config);
try
{
// Set up the environment
//ManifoldCF.initializeEnvironment();
IThreadContext itc = ThreadContextFactory.make();
ManifoldCF.startSystem(itc);
}
catch (ManifoldCFException e)
{
Logging.misc.error("Error starting authority service: "+e.getMessage(),e);
throw new ServletException("Error starting authority service: "+e.getMessage(),e);
}
}
/** The destroy method.
*/
public void destroy()
{
try
{
// Set up the environment
//ManifoldCF.initializeEnvironment();
IThreadContext itc = ThreadContextFactory.make();
ManifoldCF.stopSystem(itc);
}
catch (ManifoldCFException e)
{
Logging.misc.error("Error shutting down authority service: "+e.getMessage(),e);
}
super.destroy();
}
/** The get method.
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
try
{
// Set up the environment
//ManifoldCF.initializeEnvironment();
Logging.authorityService.debug("Received request");
Map<String,String> domainMap = new HashMap<String,String>();
// Legacy mode: single user name with optional domain
String userID = request.getParameter("username");
if (userID != null)
{
String domain = request.getParameter("domain");
if (domain == null)
domain = "";
domainMap.put(domain,userID);
}
// Now, go through enumerated username/domain pairs
int q = 0;
while (true)
{
String enumUserName = request.getParameter("username_"+q);
if (enumUserName == null)
break;
String enumDomain = request.getParameter("domain_"+q);
if (enumDomain == null)
enumDomain = "";
domainMap.put(enumDomain,enumUserName);
q++;
}
if (domainMap.size() == 0)
{
response.sendError(response.SC_BAD_REQUEST);
return;
}
boolean idneeded = false;
boolean aclneeded = true;
String idneededValue = request.getParameter("idneeded");
if (idneededValue != null)
{
if (idneededValue.equals("true"))
idneeded = true;
else if (idneededValue.equals("false"))
idneeded = false;
}
String aclneededValue = request.getParameter("aclneeded");
if (aclneededValue != null)
{
if (aclneededValue.equals("true"))
aclneeded = true;
else if (aclneededValue.equals("false"))
aclneeded = false;
}
if (Logging.authorityService.isDebugEnabled())
{
StringBuilder sb2 = new StringBuilder("[");
boolean first = true;
for (String domain : domainMap.keySet())
{
if (first)
first = false;
else
sb2.append(",");
sb2.append("'").append(domain).append("':'").append(domainMap.get(domain)).append("'");
}
sb2.append("]");
Logging.authorityService.debug("Received authority request for domain:user set "+sb2.toString());
}
RequestQueue<MappingRequest> mappingQueue = ManifoldCF.getMappingRequestQueue();
if (mappingQueue == null)
{
// System wasn't started; return unauthorized
throw new ManifoldCFException("System improperly initialized");
}
RequestQueue<AuthRequest> queue = ManifoldCF.getRequestQueue();
if (queue == null)
{
// System wasn't started; return unauthorized
throw new ManifoldCFException("System improperly initialized");
}
IThreadContext itc = ThreadContextFactory.make();
IMappingConnectionManager mappingConnManager = MappingConnectionManagerFactory.make(itc);
IAuthorityConnectionManager authConnManager = AuthorityConnectionManagerFactory.make(itc);
// Get all mapping connections; we may not need them all but we do need to be able to look them all up
IMappingConnection[] mappingConnections = mappingConnManager.getAllConnections();
// One thread per connection, which is responsible for starting the mapping process when it is ready.
List<MappingOrderThread> mappingThreads = new ArrayList<MappingOrderThread>();
// One thread per authority, which is responsible for starting the auth request when it is ready.
List<AuthOrderThread> authThreads = new ArrayList<AuthOrderThread>();
Map<MapperDescription,MappingRequest> mappingRequests = new HashMap<MapperDescription,MappingRequest>();
Map<String,AuthRequest> authRequests = new HashMap<String,AuthRequest>();
Map<String,IMappingConnection> mappingConnMap = new HashMap<String,IMappingConnection>();
// Fill in mappingConnMap, since we need to be able to find connections given connection names
for (IMappingConnection c : mappingConnections)
{
mappingConnMap.put(c.getName(),c);
}
// Set of connections we need to fire off
Set<MapperDescription> activeConnections = new HashSet<MapperDescription>();
// We do the minimal set of mapping requests and authorities. Since it is the authority tokens we are
// looking for, we start there, and build authority requests first, then mapping requests that support them,
// etc.
// Create auth requests
for (String authDomain : domainMap.keySet())
{
IAuthorityConnection[] connections = authConnManager.getDomainConnections(authDomain);
for (int i = 0; i < connections.length; i++)
{
IAuthorityConnection thisConnection = connections[i];
String identifyingString = thisConnection.getDescription();
if (identifyingString == null || identifyingString.length() == 0)
identifyingString = thisConnection.getName();
// Create a request
AuthRequest ar = new AuthRequest(thisConnection,identifyingString);
authRequests.put(thisConnection.getName(), ar);
// We create an auth thread if there are prerequisites to meet.
// Otherwise, we just fire off the request
String domainUserID = domainMap.get(authDomain);
if (thisConnection.getPrerequisiteMapping() == null)
{
ar.setUserID(domainUserID);
queue.addRequest(ar);
}
else
{
MapperDescription md = new MapperDescription(thisConnection.getPrerequisiteMapping(),authDomain);
AuthOrderThread thread = new AuthOrderThread(identifyingString,
ar, md,
queue, mappingRequests);
authThreads.add(thread);
// The same mapper can be used for multiple domains, although this is likely to be uncommon. Nevertheless,
// mapper invocations need to be segregated to prevent trouble
activeConnections.add(md);
}
}
}
// Create mapping requests
while (!activeConnections.isEmpty())
{
Iterator<MapperDescription> connectionIter = activeConnections.iterator();
MapperDescription mapperDesc = connectionIter.next();
String connectionName = mapperDesc.mapperName;
String authDomain = mapperDesc.authDomain;
IMappingConnection thisConnection = mappingConnMap.get(connectionName);
String identifyingString = thisConnection.getDescription();
if (identifyingString == null || identifyingString.length() == 0)
identifyingString = connectionName;
// Create a request
MappingRequest mr = new MappingRequest(thisConnection,identifyingString);
mappingRequests.put(mapperDesc, mr);
// Either start up a thread, or just fire it off immediately.
if (thisConnection.getPrerequisiteMapping() == null)
{
mr.setUserID(domainMap.get(authDomain));
mappingQueue.addRequest(mr);
}
else
{
//System.out.println("Mapper: prerequisite found: '"+thisConnection.getPrerequisiteMapping()+"'");
MapperDescription p = new MapperDescription(thisConnection.getPrerequisiteMapping(),authDomain);
MappingOrderThread thread = new MappingOrderThread(identifyingString,
mr, p, mappingQueue, mappingRequests);
mappingThreads.add(thread);
if (mappingRequests.get(p) == null)
activeConnections.add(p);
}
activeConnections.remove(mapperDesc);
}
// Start threads. We have to wait until all the requests have been
// at least created before we do this.
for (MappingOrderThread thread : mappingThreads)
{
thread.start();
}
for (AuthOrderThread thread : authThreads)
{
thread.start();
}
// Wait for the threads to finish up. This will guarantee that all entities have run to completion.
for (MappingOrderThread thread : mappingThreads)
{
thread.finishUp();
}
for (AuthOrderThread thread : authThreads)
{
thread.finishUp();
}
// This is probably unnecessary, but we do it anyway just to adhere to the contract
for (MappingRequest mr : mappingRequests.values())
{
mr.waitForComplete();
}
// Handle all exceptions thrown during mapping. In general this just means logging them, because
// the downstream authorities will presumably not find what they are looking for and error out that way.
for (MappingRequest mr : mappingRequests.values())
{
Throwable exception = mr.getAnswerException();
if (exception != null)
{
Logging.authorityService.warn("Mapping exception logged from "+mr.getIdentifyingString()+": "+exception.getMessage()+"; mapper aborted", exception);
}
}
// Now, work through the returning answers.
// Ask all the interrogated authorities for their ACLs, and merge the final list together.
StringBuilder sb = new StringBuilder();
// Set response mime type
response.setContentType("text/plain; charset=ISO8859-1");
ServletOutputStream out = response.getOutputStream();
try
{
for (String connectionName : authRequests.keySet())
{
AuthRequest ar = authRequests.get(connectionName);
if (Logging.authorityService.isDebugEnabled())
Logging.authorityService.debug("Waiting for answer from authority connection "+ar.getIdentifyingString()+" for user '"+ar.getUserID()+"'");
ar.waitForComplete();
if (Logging.authorityService.isDebugEnabled())
Logging.authorityService.debug("Received answer from authority connection "+ar.getIdentifyingString()+" for user '"+ar.getUserID()+"'");
Throwable exception = ar.getAnswerException();
AuthorizationResponse reply = ar.getAnswerResponse();
if (exception != null)
{
// Exceptions are always bad now
// The ManifoldCFException here must disable access to the UI without causing a generic badness thing to happen, so use 403.
if (exception instanceof ManifoldCFException)
response.sendError(response.SC_FORBIDDEN,"From "+ar.getIdentifyingString()+": "+exception.getMessage());
else
response.sendError(response.SC_INTERNAL_SERVER_ERROR,"From "+ar.getIdentifyingString()+": "+exception.getMessage());
return;
}
String authGroup = ar.getAuthorityConnection().getAuthGroup();
// A null reply means the same as USERNOTFOUND; it occurs because a user mapping failed somewhere.
if (reply == null)
{
if (Logging.authorityService.isDebugEnabled())
Logging.authorityService.debug("User '"+ar.getUserID()+"' mapping failed for authority '"+ar.getIdentifyingString()+"'");
sb.append(USERNOTFOUND_VALUE).append(URLEncoder.encode(ar.getIdentifyingString())).append("\n");
}
else if (reply.getResponseStatus() == AuthorizationResponse.RESPONSE_UNREACHABLE)
{
Logging.authorityService.warn("Authority '"+ar.getIdentifyingString()+"' is unreachable for user '"+ar.getUserID()+"'");
sb.append(UNREACHABLE_VALUE).append(URLEncoder.encode(ar.getIdentifyingString())).append("\n");
}
else if (reply.getResponseStatus() == AuthorizationResponse.RESPONSE_USERUNAUTHORIZED)
{
if (Logging.authorityService.isDebugEnabled())
Logging.authorityService.debug("Authority '"+ar.getIdentifyingString()+"' does not authorize user '"+ar.getUserID()+"'");
sb.append(UNAUTHORIZED_VALUE).append(URLEncoder.encode(ar.getIdentifyingString())).append("\n");
}
else if (reply.getResponseStatus() == AuthorizationResponse.RESPONSE_USERNOTFOUND)
{
if (Logging.authorityService.isDebugEnabled())
Logging.authorityService.debug("User '"+ar.getUserID()+"' unknown to authority '"+ar.getIdentifyingString()+"'");
sb.append(USERNOTFOUND_VALUE).append(URLEncoder.encode(ar.getIdentifyingString())).append("\n");
}
else
sb.append(AUTHORIZED_VALUE).append(URLEncoder.encode(ar.getIdentifyingString())).append("\n");
String[] acl = reply.getAccessTokens();
if (acl != null)
{
if (aclneeded)
{
int j = 0;
while (j < acl.length)
{
if (Logging.authorityService.isDebugEnabled())
Logging.authorityService.debug(" User '"+ar.getUserID()+"' has Acl = '"+acl[j]+"' from authority '"+ar.getIdentifyingString()+"'");
sb.append(TOKEN_PREFIX).append(URLEncoder.encode(authGroup)).append(":").append(URLEncoder.encode(acl[j++])).append("\n");
}
}
}
}
// Maintained for backwards compatibility only; no practical use that I can determine here
if (idneeded && userID != null)
sb.append(ID_PREFIX).append(URLEncoder.encode(userID)).append("\n");
byte[] responseValue = sb.toString().getBytes(StandardCharsets.ISO_8859_1);
response.setIntHeader("Content-Length", (int)responseValue.length);
out.write(responseValue,0,responseValue.length);
out.flush();
}
finally
{
out.close();
}
if (Logging.authorityService.isDebugEnabled())
{
StringBuilder sb2 = new StringBuilder("[");
boolean first = true;
for (String domain : domainMap.keySet())
{
if (first)
first = false;
else
sb2.append(",");
sb2.append("'").append(domain).append("':'").append(domainMap.get(domain)).append("'");
}
sb2.append("]");
Logging.authorityService.debug("Done with request for domain:user set "+sb2.toString());
}
}
catch (InterruptedException e)
{
// Shut down and don't bother to respond
}
catch (ManifoldCFException e)
{
Logging.authorityService.error("User ACL servlet error: "+e.getMessage(),e);
response.sendError(response.SC_INTERNAL_SERVER_ERROR,e.getMessage());
}
}
/** This class represents a tuple of (mapper_name, auth_domain).
*/
protected static class MapperDescription
{
public final String mapperName;
public final String authDomain;
public MapperDescription(String mapperName, String authDomain)
{
this.mapperName = mapperName;
this.authDomain = authDomain;
}
public int hashCode()
{
return mapperName.hashCode() + authDomain.hashCode();
}
public boolean equals(Object o)
{
if (!(o instanceof MapperDescription))
return false;
MapperDescription other = (MapperDescription)o;
return this.mapperName.equals(other.mapperName) &&
this.authDomain.equals(other.authDomain);
}
}
/** This thread is responsible for making sure that the constraints for a given mapping connection
* are met, and then when they are, firing off a MappingRequest. One of these threads is spun up
* for every IMappingConnection being handled.
* NOTE WELL: The number of threads this might require is worrisome. It is essentially
* {@literal<number_of_app_server_threads> * <number_of_mappers>}. I will try later to see if I can find
* a way of limiting this to sane numbers.
*/
protected static class MappingOrderThread extends Thread
{
protected final MappingRequest request;
protected final MapperDescription prerequisite;
protected final Map<MapperDescription,MappingRequest> requests;
protected final RequestQueue<MappingRequest> mappingRequestQueue;
protected Throwable exception = null;
public MappingOrderThread(
String identifyingString,
MappingRequest request,
MapperDescription prerequisite,
RequestQueue<MappingRequest> mappingRequestQueue,
Map<MapperDescription, MappingRequest> requests)
{
super();
this.request = request;
this.prerequisite = prerequisite;
this.mappingRequestQueue = mappingRequestQueue;
this.requests = requests;
setName("Constraint matcher for mapper '"+identifyingString+"'");
setDaemon(true);
}
public void run()
{
try
{
MappingRequest mappingRequest = requests.get(prerequisite);
mappingRequest.waitForComplete();
// Constraints are met. Fire off the request.
request.setUserID(mappingRequest.getAnswerResponse());
mappingRequestQueue.addRequest(request);
}
catch (Throwable e)
{
exception = e;
}
}
public void finishUp()
throws InterruptedException
{
join();
if (exception != null)
{
if (exception instanceof Error)
throw (Error)exception;
else if (exception instanceof RuntimeException)
throw (RuntimeException)exception;
}
}
}
/** This thread is responsible for making sure that the constraints for a given authority connection
* are met, and then when they are, firing off an AuthRequest. One of these threads is spun up
* for every IAuthorityConnection being handled.
* NOTE WELL: The number of threads this might require is worrisome. It is essentially
* {@literal<number_of_app_server_threads> * <number_of_authorities>}. I will try later to see if I can find
* a way of limiting this to sane numbers.
*/
protected static class AuthOrderThread extends Thread
{
protected final AuthRequest request;
protected final MapperDescription prerequisite;
protected final Map<MapperDescription,MappingRequest> mappingRequests;
protected final RequestQueue<AuthRequest> authRequestQueue;
protected Throwable exception = null;
public AuthOrderThread(
String identifyingString,
AuthRequest request,
MapperDescription prerequisite,
RequestQueue<AuthRequest> authRequestQueue,
Map<MapperDescription, MappingRequest> mappingRequests)
{
super();
this.request = request;
this.prerequisite = prerequisite;
this.authRequestQueue = authRequestQueue;
this.mappingRequests = mappingRequests;
setName("Constraint matcher for authority '"+identifyingString+"'");
setDaemon(true);
}
public void run()
{
try
{
MappingRequest mappingRequest = mappingRequests.get(prerequisite);
mappingRequest.waitForComplete();
// Constraints are met. Fire off the request. User may be null if mapper failed!!
request.setUserID(mappingRequest.getAnswerResponse());
authRequestQueue.addRequest(request);
}
catch (Throwable e)
{
exception = e;
}
}
public void finishUp()
throws InterruptedException
{
join();
if (exception != null)
{
if (exception instanceof Error)
throw (Error)exception;
else if (exception instanceof RuntimeException)
throw (RuntimeException)exception;
}
}
}
}