blob: a4aa01b14adf46ae05be63ea1b523f773a2d270c [file] [log] [blame]
// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed 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 com.google.enterprise.adaptor;
import com.google.enterprise.apis.client.GsaClient;
import com.google.gdata.util.AuthenticationException;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLException;
/**
* Require GSA-Administrator authentication before allowing requests.
*/
class AdministratorSecurityHandler implements HttpHandler {
private static final Logger log
= Logger.getLogger(AdministratorSecurityHandler.class.getName());
/** Key used to store the fact the user has been authenticated. */
private static final String SESSION_ATTR_NAME = "dashboard-authned";
/** Page to display when prompting for user credentials. */
private static final String LOGIN_PAGE = "resources/login.html";
/** Page to display when the user credentials are invalid. */
private static final String LOGIN_FAILED_PAGE = "resources/login-failed.html";
/** Page to display when the user credentials were not able to be verified. */
private static final String LOGIN_INDETERMINATE_PAGE
= "resources/login-indeterminate.html";
/** Wrapped handler, for when the user is authenticated. */
private final HttpHandler handler;
/** Manager that handles keeping track of authenticated users. */
private final SessionManager<HttpExchange> sessionManager;
/** Trusted entity for performing authentication of user credentials. */
private final AuthnClient authnClient;
AdministratorSecurityHandler(HttpHandler handler,
SessionManager<HttpExchange> sessionManager, AuthnClient authnClient) {
this.handler = handler;
this.sessionManager = sessionManager;
this.authnClient = authnClient;
}
public AdministratorSecurityHandler(HttpHandler handler,
SessionManager<HttpExchange> sessionManager, String gsaHostname,
boolean useHttps) {
this(handler, sessionManager, new GsaAuthnClient(gsaHostname, useHttps));
}
private void meteredHandle(HttpExchange ex) throws IOException {
String pageToDisplay = LOGIN_PAGE;
if ("POST".equals(ex.getRequestMethod())) {
AuthzStatus authn = validUsernameAndPassword(ex);
if (authn == AuthzStatus.PERMIT) {
// Need the client to access the page via GET since the only reason the
// request method was POST was because of submitting our login form.
HttpExchanges.sendRedirect(ex, HttpExchanges.getRequestUri(ex));
return;
} else if (authn == AuthzStatus.INDETERMINATE) {
pageToDisplay = LOGIN_INDETERMINATE_PAGE;
} else {
pageToDisplay = LOGIN_FAILED_PAGE;
}
}
// Send login page.
InputStream is = this.getClass().getResourceAsStream(pageToDisplay);
if (is == null) {
throw new IOException("Could not load login page");
}
byte[] page;
try {
page = IOHelper.readInputStreamToByteArray(is);
} finally {
is.close();
}
HttpExchanges.respond(
ex, HttpURLConnection.HTTP_FORBIDDEN, "text/html", page);
}
/**
* Check POST data to see if the user can be authenticated by the GSA. This
* abuses the {@code AuthzStatus} class, using it for Authn, but it was
* convenient.
*
* @return {@code PERMIT} if user authenticated, {@code DENY} if invalid
* credentials, and {@code INDETERMINATE} otherwise
*/
private AuthzStatus validUsernameAndPassword(HttpExchange ex)
throws IOException {
log.fine("Not already authenticated");
// Check to see if they provided a username and password.
String username = null;
String password = null;
try {
String request = IOHelper.readInputStreamToString(
ex.getRequestBody(), Charset.forName("US-ASCII"));
for (String pair : request.split("&")) {
String[] splitPair = pair.split("=", 2);
if (splitPair.length != 2) {
continue;
}
splitPair[0] = URLDecoder.decode(splitPair[0], "UTF-8");
splitPair[1] = URLDecoder.decode(splitPair[1], "UTF-8");
if ("username".equals(splitPair[0])) {
username = splitPair[1];
} else if ("password".equals(splitPair[0])) {
password = splitPair[1];
}
if (username != null && password != null) {
break;
}
}
} catch (Exception e) {
log.log(Level.FINE, "Processing POST caused exception", e);
// Assume that they were POSTing to a different page, since they didn't
// provide the expected input.
username = null;
password = null;
}
if (username == null || password == null) {
log.fine("Username or password is null. Not authenticated");
// Must not have been from our login page.
return AuthzStatus.INDETERMINATE;
}
// Check to see if provided username and password are valid.
AuthzStatus result = authnClient.authn(username, password);
if (result == AuthzStatus.INDETERMINATE) {
log.fine("Failed communicating with the GSA");
return result;
} else if (result != AuthzStatus.PERMIT) {
log.fine("GSA login was not successful");
return result;
}
// We have a winner. Store in the session that they are a valid user.
log.fine("GSA login successful");
Session session = sessionManager.getSession(ex);
session.setAttribute(SESSION_ATTR_NAME, true);
return AuthzStatus.PERMIT;
}
@Override
public void handle(HttpExchange ex) throws IOException {
// Clickjacking defence.
ex.getResponseHeaders().set("X-Frame-Options", "deny");
// This comment is no longer true and exists from before logging was done in
// a filter. TODO(ejona): split into a separate filter and handler.
// Perform fast-path checking here to prevent double-logging most requests.
Session session = sessionManager.getSession(ex, false);
if (session != null && session.getAttribute(SESSION_ATTR_NAME) != null) {
handler.handle(ex);
return;
}
meteredHandle(ex);
}
interface AuthnClient {
public AuthzStatus authn(String username, String password);
}
static class GsaAuthnClient implements AuthnClient {
private String gsaHostname;
private boolean useHttps;
public GsaAuthnClient(String gsaHostname, boolean useHttps) {
this.gsaHostname = gsaHostname;
this.useHttps = useHttps;
}
@Override
public AuthzStatus authn(String username, String password) {
String protocol = useHttps ? "https" : "http";
int port = useHttps ? 8443 : 8000;
try {
new GsaClient(protocol, gsaHostname, port, username, password);
} catch (AuthenticationException e) {
if (e.getCause() instanceof ConnectException) {
log.log(Level.WARNING, "Failed to connect to the GSA Administrative "
+ "Console at " + adminUrl() + " . If the GSA is online and the "
+ "GSA's dedicated administrative network interface is enabled, "
+ "please verify that the gsa.admin.hostname property is "
+ "properly configured.", e);
return AuthzStatus.INDETERMINATE;
} else if (e.getCause() instanceof UnknownHostException) {
log.log(Level.WARNING, "Failed to locate the GSA Administrative "
+ "Console at " + adminUrl() + " . Please verify that the "
+ "gsa.hostname and/or the gsa.admin.hostname configuration "
+ "properties are correct.", e);
return AuthzStatus.INDETERMINATE;
} else if (e.getCause() instanceof SSLException && useHttps) {
log.log(Level.WARNING, "Failed to connect to the GSA Administrative "
+ "Console at " + adminUrl() + " . Please verify that the your "
+ "SSL Certificates are properly configured for secure "
+ "communication with the GSA.", e);
} else {
log.log(Level.FINE, "AuthenticationException", e);
}
return AuthzStatus.DENY;
}
return AuthzStatus.PERMIT;
}
private String adminUrl() {
try {
String protocol = useHttps ? "https" : "http";
int port = useHttps ? 8443 : 8000;
return new URL(protocol, gsaHostname, port, "").toString();
} catch (MalformedURLException e) {
return e.toString();
}
}
}
}