package org.dcache.webdav;
import com.google.common.base.Splitter;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.net.InetAddresses;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.security.UserAuthentication;
import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.auth.Subject;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.PrivilegedAction;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import diskCacheV111.util.CacheException;
import diskCacheV111.util.PermissionDeniedCacheException;
import org.dcache.auth.BearerTokenCredential;
import org.dcache.auth.LoginNamePrincipal;
import org.dcache.auth.LoginReply;
import org.dcache.auth.LoginStrategy;
import org.dcache.auth.Origin;
import org.dcache.auth.PasswordCredential;
import org.dcache.auth.Subjects;
import org.dcache.auth.attributes.Restriction;
import org.dcache.auth.attributes.Restrictions;
import org.dcache.util.CertificateFactories;
import org.dcache.util.NetLoggerBuilder;
import static com.google.common.base.Strings.nullToEmpty;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.reverse;
import static java.util.Arrays.asList;
public class AuthenticationHandler extends HandlerWrapper {
private final static Logger LOG = LoggerFactory.getLogger(AuthenticationHandler.class);
public static final String X509_CERTIFICATE_ATTRIBUTE =
"javax.servlet.request.X509Certificate";
public static final String DCACHE_SUBJECT_ATTRIBUTE =
"org.dcache.subject";
public static final String DCACHE_RESTRICTION_ATTRIBUTE =
"org.dcache.restriction";
public static final String DCACHE_LOGIN_ATTRIBUTES =
"org.dcache.login";
private static final InetAddress UNKNOWN_ADDRESS = InetAddresses.forString("0.0.0.0");
private String _realm;
private Restriction _doorRestriction;
private boolean _isBasicAuthenticationEnabled;
private boolean _isSpnegoAuthenticationEnabled;
private LoginStrategy _loginStrategy;
private CertificateFactory _cf = CertificateFactories.newX509CertificateFactory();
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse servletResponse)
throws IOException, ServletException {
if (isStarted() && !baseRequest.isHandled()) {
Subject subject = new Subject();
AuthHandlerResponse response = new AuthHandlerResponse(servletResponse);
try {
addX509ChainToSubject(request, subject);
addOriginToSubject(request, subject);
addAuthCredentialsToSubject(request, subject);
addSpnegoCredentialsToSubject(baseRequest, request, subject);
LoginReply login = _loginStrategy.login(subject);
subject = login.getSubject();
Restriction restriction = Restrictions.concat(_doorRestriction, login.getRestriction());
request.setAttribute(DCACHE_SUBJECT_ATTRIBUTE, subject);
request.setAttribute(DCACHE_RESTRICTION_ATTRIBUTE, restriction);
request.setAttribute(DCACHE_LOGIN_ATTRIBUTES, login.getLoginAttributes());
/* Process the request as the authenticated user.*/
Exception problem = Subject.doAs(subject, (PrivilegedAction<Exception>) () -> {
try {
AuthenticationHandler.super.handle(target, baseRequest, request, response);
} catch (IOException | ServletException e) {
return e;
}
return null;
});
if (problem != null) {
Throwables.propagateIfInstanceOf(problem, IOException.class);
Throwables.propagateIfInstanceOf(problem, ServletException.class);
throw Throwables.propagate(problem);
}
} catch (PermissionDeniedCacheException e) {
LOG.warn("{} for path {} and user {}", e.getMessage(), request.getPathInfo(),
NetLoggerBuilder.describeSubject(subject));
response.sendError((Subjects.isNobody(subject)) ? HttpServletResponse.SC_UNAUTHORIZED :
HttpServletResponse.SC_FORBIDDEN);
baseRequest.setHandled(true);
} catch (CacheException e) {
LOG.error("Internal server error: {}", e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
baseRequest.setHandled(true);
}
}
}
private void addSpnegoCredentialsToSubject(Request baseRequest,
HttpServletRequest request,
Subject subject)
{
if (_isSpnegoAuthenticationEnabled) {
Authentication spnegoAuth = baseRequest.getAuthentication();
if (spnegoAuth instanceof Authentication.Deferred) {
Authentication spnegoUser = ((Authentication.Deferred) spnegoAuth).authenticate(request);
if (spnegoUser instanceof UserAuthentication) {
UserIdentity identity = ((UserAuthentication) spnegoUser).getUserIdentity();
subject.getPrincipals().add(new KerberosPrincipal(identity.getUserPrincipal().getName()));
}
}
}
}
private void addX509ChainToSubject(HttpServletRequest request, Subject subject)
throws CacheException {
Object object = request.getAttribute(X509_CERTIFICATE_ATTRIBUTE);
if (object instanceof X509Certificate[]) {
try {
subject.getPublicCredentials().add(_cf.generateCertPath(asList((X509Certificate[]) object)));
} catch (CertificateException e) {
throw new CacheException("Failed to generate X.509 certificate path: " + e.getMessage(), e);
}
}
}
private void addXForwardForAddresses(ImmutableList.Builder<InetAddress> addresses,
HttpServletRequest request)
{
String xff = nullToEmpty(request.getHeader("X-Forwarded-For"));
List<String> ids = newArrayList(Splitter.on(',').trimResults().omitEmptyStrings().split(xff));
reverse(ids).stream().
map(id -> {
try {
return InetAddresses.forString(id);
} catch (IllegalArgumentException e) {
LOG.warn("Fail to parse \"{}\" in X-Forwarded-For " +
"header \"{}\": {}", id, xff, e.getMessage());
return UNKNOWN_ADDRESS;
}
}).
forEach(addresses::add);
}
private void addOriginToSubject(HttpServletRequest request, Subject subject)
{
ImmutableList.Builder<InetAddress> addresses = ImmutableList.builder();
String address = request.getRemoteAddr();
try {
addresses.add(InetAddress.getByName(address));
} catch (UnknownHostException e) {
LOG.warn("Failed to resolve " + address + ": " + e.getMessage());
return;
}
// REVISIT: although RFC 7239 specifies a more powerful format, it
// is currently not widely used; whereas X-Forward-For header, while not
// standardised is the de facto standard and widely supported.
addXForwardForAddresses(addresses, request);
subject.getPrincipals().add(new Origin(addresses.build()));
}
private void addAuthCredentialsToSubject(HttpServletRequest request, Subject subject) {
if (!_isBasicAuthenticationEnabled) {
return;
}
Optional<AuthInfo> optional = parseAuthenticationHeader(request);
if (optional.isPresent()) {
AuthInfo info = optional.get();
switch (info.getScheme()) {
case HttpServletRequest.BASIC_AUTH:
try {
byte[] bytes = Base64.getDecoder().decode(info.getData().getBytes(StandardCharsets.US_ASCII));
String credential = new String(bytes, StandardCharsets.UTF_8);
int colon = credential.indexOf(":");
if (colon >= 0) {
String user = credential.substring(0, colon);
String password = credential.substring(colon + 1);
subject.getPrivateCredentials().add(new PasswordCredential(user, password));
} else {
subject.getPrincipals().add(new LoginNamePrincipal(credential));
}
} catch (IllegalArgumentException e) {
LOG.warn("Authentication Data in the header received is not Base64 encoded {}",
request.getHeader("Authorization"));
}
break;
case "BEARER":
try {
subject.getPrivateCredentials().add(new BearerTokenCredential(info.getData()));
} catch (IllegalArgumentException e) {
LOG.info("Bearer Token in invalid {}",
request.getHeader("Authorization"));
}
break;
default:
LOG.debug("Unknown authentication scheme {}", info.getScheme());
}
}
}
public String getRealm() {
return _realm;
}
/**
* Sets the HTTP realm used for basic authentication.
*/
public void setRealm(String realm) {
_realm = realm;
}
/**
* Specifies whether the door is read only.
*/
public void setReadOnly(boolean isReadOnly) {
_doorRestriction = isReadOnly ? Restrictions.readOnly() : Restrictions.none();
}
public void setEnableBasicAuthentication(boolean isEnabled) {
_isBasicAuthenticationEnabled = isEnabled;
}
public void setEnableSpnegoAuthentication(boolean isEnabled) {
_isSpnegoAuthenticationEnabled = isEnabled;
}
public void setLoginStrategy(LoginStrategy loginStrategy) {
_loginStrategy = loginStrategy;
}
private class AuthHandlerResponse extends HttpServletResponseWrapper {
public AuthHandlerResponse(HttpServletResponse response) {
super(response);
}
@Override
public void setStatus(int code) {
addAuthenticationChallenges(code);
super.setStatus(code);
}
@Override
public void setStatus(int code, String message) {
addAuthenticationChallenges(code);
super.setStatus(code, message);
}
@Override
public void sendError(int code) throws IOException {
addAuthenticationChallenges(code);
super.sendError(code);
}
@Override
public void sendError(int code, String message) throws IOException {
addAuthenticationChallenges(code);
super.sendError(code, message);
}
private void addAuthenticationChallenges(int code) {
if (code == HttpServletResponse.SC_UNAUTHORIZED) {
if (_isSpnegoAuthenticationEnabled) {
// Firefox always defaults to the first available authentication mechanism
// Conversely, Chrome and Safari choose the strongest mechanism
setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), HttpHeader.NEGOTIATE.asString());
addHeader(HttpHeader.WWW_AUTHENTICATE.asString(), "Basic realm=\"" + getRealm() + "\"");
} else {
setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), "Basic realm=\"" + getRealm() + "\"");
}
}
}
}
private class AuthInfo {
private final String _scheme;
private final String _data;
AuthInfo(String scheme, String data)
{
_scheme = scheme;
_data = data;
}
public String getScheme()
{
return _scheme;
}
public String getData()
{
return _data;
}
}
private Optional<AuthInfo> parseAuthenticationHeader(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (header == null) {
LOG.debug("No credentials found in Authorization header");
return Optional.empty();
}
if (header.length() == 0) {
LOG.debug("Credentials in Authorization header are not-null, but are empty");
return Optional.empty();
}
int space = header.indexOf(" ");
String authScheme = space >= 0 ? header.substring(0, space).toUpperCase() : HttpServletRequest.BASIC_AUTH;
String authData = space >= 0 ? header.substring(space + 1) : header;
return Optional.of(new AuthInfo(authScheme, authData));
}
}