blob: 25aede35d38d1da36e44b8fed9982e00d329753f [file] [log] [blame]
/**
* 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.atlas.web.filters;
import org.apache.atlas.ApplicationProperties;
import org.apache.atlas.RequestContext;
import org.apache.atlas.security.SecurityProperties;
import org.apache.atlas.utils.AuthenticationUtil;
import org.apache.atlas.web.security.AtlasAuthenticationProvider;
import org.apache.atlas.web.util.Servlets;
import org.apache.commons.collections.iterators.IteratorEnumeration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.security.authentication.client.AuthenticatedURL;
import org.apache.hadoop.security.authentication.client.AuthenticationException;
import org.apache.hadoop.security.authentication.client.KerberosAuthenticator;
import org.apache.hadoop.security.authentication.server.AuthenticationFilter;
import org.apache.hadoop.security.authentication.server.AuthenticationHandler;
import org.apache.hadoop.security.authentication.server.AuthenticationToken;
import org.apache.hadoop.security.authentication.server.KerberosAuthenticationHandler;
import org.apache.hadoop.security.authentication.util.Signer;
import org.apache.hadoop.security.authentication.util.SignerException;
import org.apache.hadoop.security.authentication.util.SignerSecretProvider;
import org.apache.log4j.NDC;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Component;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.Principal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This enforces authentication as part of the filter before processing the request.
* todo: Subclass of {@link org.apache.hadoop.security.authentication.server.AuthenticationFilter}.
*/
@Component
public class AtlasAuthenticationFilter extends AuthenticationFilter {
private static final Logger LOG = LoggerFactory.getLogger(AtlasAuthenticationFilter.class);
static final String PREFIX = "atlas.authentication.method";
protected static ServletContext nullContext = new NullServletContext();
private Signer signer;
private SignerSecretProvider secretProvider;
public final boolean isKerberos = AuthenticationUtil.isKerberosAuthenticationEnabled();
private boolean isInitializedByTomcat;
private Set<Pattern> browserUserAgents;
private boolean supportKeyTabBrowserLogin = false;
public AtlasAuthenticationFilter() {
try {
LOG.info("AtlasAuthenticationFilter initialization started");
init(null);
} catch (ServletException e) {
LOG.error("Error while initializing AtlasAuthenticationFilter : {}", e.getMessage());
}
}
private HttpServlet optionsServlet;
/**
* Initialize the filter.
*
* @param filterConfig filter configuration.
* @throws ServletException thrown if the filter could not be initialized.
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
LOG.info("AtlasAuthenticationFilter initialization started");
final FilterConfig globalConf = filterConfig;
final Map<String, String> params = new HashMap<>();
FilterConfig filterConfig1 = new FilterConfig() {
@Override
public ServletContext getServletContext() {
if (globalConf != null) {
return globalConf.getServletContext();
} else {
return nullContext;
}
}
@SuppressWarnings("unchecked")
@Override
public Enumeration<String> getInitParameterNames() {
return new IteratorEnumeration(params.keySet().iterator());
}
@Override
public String getInitParameter(String param) {
return params.get(param);
}
@Override
public String getFilterName() {
return "AtlasAuthenticationFilter";
}
};
super.init(filterConfig1);
optionsServlet = new HttpServlet() {
};
optionsServlet.init();
}
@Override
public void initializeSecretProvider(FilterConfig filterConfig)
throws ServletException {
LOG.debug("AtlasAuthenticationFilter :: initializeSecretProvider {}", filterConfig);
secretProvider = (SignerSecretProvider) filterConfig.getServletContext().
getAttribute(AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE);
if (secretProvider == null) {
// As tomcat cannot specify the provider object in the configuration.
// It'll go into this path
String configPrefix = filterConfig.getInitParameter(CONFIG_PREFIX);
configPrefix = (configPrefix != null) ? configPrefix + "." : "";
try {
secretProvider = AuthenticationFilter.constructSecretProvider(
filterConfig.getServletContext(),
super.getConfiguration(configPrefix, filterConfig), false);
this.isInitializedByTomcat = true;
} catch (Exception ex) {
throw new ServletException(ex);
}
}
signer = new Signer(secretProvider);
}
@Override
protected Properties getConfiguration(String configPrefix, FilterConfig filterConfig) throws ServletException {
Configuration configuration;
try {
configuration = ApplicationProperties.get();
} catch (Exception e) {
throw new ServletException(e);
}
Properties config = new Properties();
String kerberosAuthEnabled = configuration != null ? configuration.getString("atlas.authentication.method.kerberos") : null;
// getString may return null, and would like to log the nature of the default setting
String authMethod = "";
if (kerberosAuthEnabled == null || kerberosAuthEnabled.equalsIgnoreCase("false")) {
LOG.info("No authentication method configured. Defaulting to simple authentication");
authMethod = "simple";
} else if (kerberosAuthEnabled.equalsIgnoreCase("true")) {
authMethod = "kerberos";
}
if (configuration.getString("atlas.authentication.method.kerberos.name.rules") != null) {
config.put("kerberos.name.rules", configuration.getString("atlas.authentication.method.kerberos.name.rules"));
}
if (configuration.getString("atlas.authentication.method.kerberos.keytab") != null) {
config.put("kerberos.keytab", configuration.getString("atlas.authentication.method.kerberos.keytab"));
}
if (configuration.getString("atlas.authentication.method.kerberos.principal") != null) {
config.put("kerberos.principal", configuration.getString("atlas.authentication.method.kerberos.principal"));
}
config.put(AuthenticationFilter.AUTH_TYPE, authMethod);
config.put(AuthenticationFilter.COOKIE_PATH, "/");
// add any config passed in as init parameters
Enumeration<String> enumeration = filterConfig.getInitParameterNames();
while (enumeration.hasMoreElements()) {
String name = enumeration.nextElement();
config.put(name, filterConfig.getInitParameter(name));
}
//Resolve _HOST into bind address
String bindAddress = configuration.getString(SecurityProperties.BIND_ADDRESS);
if (bindAddress == null) {
LOG.info("No host name configured. Defaulting to local host name.");
try {
bindAddress = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
throw new ServletException("Unable to obtain host name", e);
}
}
String principal = config.getProperty(KerberosAuthenticationHandler.PRINCIPAL);
if (principal != null) {
try {
principal = SecurityUtil.getServerPrincipal(principal, bindAddress);
} catch (IOException ex) {
throw new RuntimeException("Could not resolve Kerberos principal name: " + ex.toString(), ex);
}
config.put(KerberosAuthenticationHandler.PRINCIPAL, principal);
}
LOG.debug(" AuthenticationFilterConfig: {}", config);
supportKeyTabBrowserLogin = configuration.getBoolean("atlas.authentication.method.kerberos.support.keytab.browser.login", false);
String agents = configuration.getString(AtlasCSRFPreventionFilter.BROWSER_USER_AGENT_PARAM, AtlasCSRFPreventionFilter.BROWSER_USER_AGENTS_DEFAULT);
if (agents == null) {
agents = AtlasCSRFPreventionFilter.BROWSER_USER_AGENTS_DEFAULT;
}
parseBrowserUserAgents(agents);
return config;
}
@Override
public void doFilter(final ServletRequest request, final ServletResponse response,
final FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest httpRequest = (HttpServletRequest) request;
FilterChain filterChainWrapper = new FilterChain() {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse)
throws IOException, ServletException {
final HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
final HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
if (isKerberos) {
Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
String userName = readUserFromCookie(httpResponse);
if (StringUtils.isEmpty(userName) && !StringUtils.isEmpty(httpRequest.getRemoteUser())) {
userName = httpRequest.getRemoteUser();
}
if ((existingAuth == null || !existingAuth.isAuthenticated()) && (!StringUtils.isEmpty(userName))) {
List<GrantedAuthority> grantedAuths = AtlasAuthenticationProvider.getAuthoritiesFromUGI(userName);
final UserDetails principal = new User(userName, "", grantedAuths);
final Authentication finalAuthentication = new UsernamePasswordAuthenticationToken(principal, "", grantedAuths);
WebAuthenticationDetails webDetails = new WebAuthenticationDetails(httpRequest);
((AbstractAuthenticationToken) finalAuthentication).setDetails(webDetails);
SecurityContextHolder.getContext().setAuthentication(finalAuthentication);
request.setAttribute("atlas.http.authentication.type", true);
LOG.info("Logged into Atlas as = {}", userName);
}
}
// OPTIONS method is sent from quick start jersey atlas client
if (httpRequest.getMethod().equals("OPTIONS")) {
optionsServlet.service(request, response);
} else {
try {
String requestUser = httpRequest.getRemoteUser();
NDC.push(requestUser + ":" + httpRequest.getMethod() + httpRequest.getRequestURI());
RequestContext requestContext = RequestContext.get();
if (requestContext != null) {
requestContext.setUser(requestUser);
}
LOG.info("Request from authenticated user: {}, URL={}", requestUser,
Servlets.getRequestURI(httpRequest));
filterChain.doFilter(servletRequest, servletResponse);
} finally {
NDC.pop();
}
}
}
};
try {
Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
HttpServletResponse httpResponse = (HttpServletResponse) response;
AtlasResponseRequestWrapper responseWrapper = new AtlasResponseRequestWrapper(httpResponse);
responseWrapper.setHeader("X-Frame-Options", "DENY");
if (existingAuth == null) {
String authHeader = httpRequest.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Basic")) {
filterChain.doFilter(request, response);
} else if (isKerberos) {
doKerberosAuth(request, response, filterChainWrapper, filterChain);
} else {
filterChain.doFilter(request, response);
}
} else {
filterChain.doFilter(request, response);
}
} catch (NullPointerException e) {
LOG.error("Exception in AtlasAuthenticationFilter ", e);
//PseudoAuthenticationHandler.getUserName() from hadoop-auth throws NPE if user name is not specified
((HttpServletResponse) response).sendError(Response.Status.BAD_REQUEST.getStatusCode(),
"Authentication is enabled and user is not specified. Specify user.name parameter");
}
}
/**
* This method is copied from hadoop auth lib, code added for error handling and fallback to other auth methods
*
* If the request has a valid authentication token it allows the request to continue to the target resource,
* otherwise it triggers an authentication sequence using the configured {@link org.apache.hadoop.security.authentication.server.AuthenticationHandler}.
*
* @param request the request object.
* @param response the response object.
* @param filterChain the filter chain object.
*
* @throws IOException thrown if an IO error occurred.
* @throws ServletException thrown if a processing error occurred.
*/
public void doKerberosAuth(ServletRequest request, ServletResponse response, FilterChain filterChainWrapper, FilterChain filterChain)
throws IOException, ServletException {
boolean unauthorizedResponse = true;
int errCode = HttpServletResponse.SC_UNAUTHORIZED;
AuthenticationException authenticationEx = null;
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
boolean isHttps = "https".equals(httpRequest.getScheme());
AuthenticationHandler authHandler = getAuthenticationHandler();
try {
boolean newToken = false;
AuthenticationToken token;
try {
token = getToken(httpRequest);
} catch (AuthenticationException ex) {
LOG.warn("AuthenticationToken ignored: {}", ex.getMessage());
// will be sent back in a 401 unless filter authenticates
authenticationEx = ex;
token = null;
}
if (authHandler.managementOperation(token, httpRequest, httpResponse)) {
if (token == null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Request [{}] triggering authentication", getRequestURL(httpRequest));
}
token = authHandler.authenticate(httpRequest, httpResponse);
if (token != null && token.getExpires() != 0 &&
token != AuthenticationToken.ANONYMOUS) {
token.setExpires(System.currentTimeMillis() + getValidity() * 1000);
}
newToken = true;
}
if (token != null) {
unauthorizedResponse = false;
if (LOG.isDebugEnabled()) {
LOG.debug("Request [{}] user [{}] authenticated", getRequestURL(httpRequest), token.getUserName());
}
final AuthenticationToken authToken = token;
httpRequest = new HttpServletRequestWrapper(httpRequest) {
@Override
public String getAuthType() {
return authToken.getType();
}
@Override
public String getRemoteUser() {
return authToken.getUserName();
}
@Override
public Principal getUserPrincipal() {
return (authToken != AuthenticationToken.ANONYMOUS) ? authToken : null;
}
};
if (newToken && !token.isExpired() && token != AuthenticationToken.ANONYMOUS) {
String signedToken = signer.sign(token.toString());
createAuthCookie(httpResponse, signedToken, getCookieDomain(),
getCookiePath(), token.getExpires(), isHttps);
}
filterChainWrapper.doFilter(httpRequest, httpResponse);
}
} else {
unauthorizedResponse = false;
}
} catch (AuthenticationException ex) {
// exception from the filter itself is fatal
errCode = HttpServletResponse.SC_FORBIDDEN;
authenticationEx = ex;
LOG.warn("Authentication exception: {}", ex.getMessage(), ex);
}
if (unauthorizedResponse) {
if (!httpResponse.isCommitted()) {
createAuthCookie(httpResponse, "", getCookieDomain(),
getCookiePath(), 0, isHttps);
// If response code is 401. Then WWW-Authenticate Header should be
// present.. reset to 403 if not found..
if ((errCode == HttpServletResponse.SC_UNAUTHORIZED)
&& (!httpResponse.containsHeader(
KerberosAuthenticator.WWW_AUTHENTICATE))) {
errCode = HttpServletResponse.SC_FORBIDDEN;
}
if (authenticationEx == null) { // added this code for atlas error handling and fallback
if (!supportKeyTabBrowserLogin && isBrowser(httpRequest.getHeader("User-Agent"))) {
filterChain.doFilter(request, response);
} else {
boolean chk = true;
Collection<String> headerNames = httpResponse.getHeaderNames();
for (String headerName : headerNames) {
String value = httpResponse.getHeader(headerName);
if (headerName.equalsIgnoreCase("Set-Cookie") && value.startsWith("ATLASSESSIONID")) {
chk = false;
break;
}
}
String authHeader = httpRequest.getHeader("Authorization");
if (authHeader == null && chk) {
filterChain.doFilter(request, response);
} else if (authHeader != null && authHeader.startsWith("Basic")) {
filterChain.doFilter(request, response);
}
}
} else {
httpResponse.sendError(errCode, authenticationEx.getMessage());
}
}
}
}
@Override
public void destroy() {
if ((this.secretProvider != null) && (this.isInitializedByTomcat)) {
this.secretProvider.destroy();
this.secretProvider = null;
}
optionsServlet.destroy();
super.destroy();
}
private static String readUserFromCookie(HttpServletResponse response1) {
String userName = null;
boolean isCookieSet = response1.containsHeader("Set-Cookie");
if (isCookieSet) {
Collection<String> authUserName = response1.getHeaders("Set-Cookie");
if (authUserName != null) {
for (String cookie : authUserName) {
if (!StringUtils.isEmpty(cookie)) {
if (cookie.toLowerCase().startsWith(AuthenticatedURL.AUTH_COOKIE.toLowerCase()) && cookie.contains("u=")) {
String[] split = cookie.split(";");
if (split != null) {
for (String s : split) {
if (!StringUtils.isEmpty(s) && s.toLowerCase().startsWith(AuthenticatedURL.AUTH_COOKIE.toLowerCase())) {
int ustr = s.indexOf("u=");
if (ustr != -1) {
int andStr = s.indexOf("&", ustr);
if (andStr != -1) {
try {
userName = s.substring(ustr + 2, andStr);
break;
} catch (Exception e) {
userName = null;
}
}
}
}
}
}
}
}
}
}
}
return userName;
}
public static void createAuthCookie(HttpServletResponse resp, String token, String domain, String path, long expires, boolean isSecure) {
StringBuilder sb = (new StringBuilder(AuthenticatedURL.AUTH_COOKIE)).append("=");
if (token != null && token.length() > 0) {
sb.append("\"").append(token).append("\"");
}
sb.append("; Version=1");
if (path != null) {
sb.append("; Path=").append(path);
}
if (domain != null) {
sb.append("; Domain=").append(domain);
}
if (expires >= 0L) {
Date date = new Date(expires);
SimpleDateFormat df = new SimpleDateFormat("EEE, dd-MMM-yyyy HH:mm:ss zzz");
df.setTimeZone(TimeZone.getTimeZone("GMT"));
sb.append("; Expires=").append(df.format(date));
}
if (isSecure) {
sb.append("; Secure");
}
sb.append("; HttpOnly");
resp.addHeader("Set-Cookie", sb.toString());
}
@Override
protected AuthenticationToken getToken(HttpServletRequest request)
throws IOException, AuthenticationException {
AuthenticationToken token = null;
String tokenStr = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(AuthenticatedURL.AUTH_COOKIE)) {
tokenStr = cookie.getValue();
try {
tokenStr = this.signer.verifyAndExtract(tokenStr);
} catch (SignerException ex) {
throw new AuthenticationException(ex);
}
}
}
}
if (tokenStr != null) {
token = AuthenticationToken.parse(tokenStr);
if (token != null) {
AuthenticationHandler authHandler = getAuthenticationHandler();
if (!token.getType().equals(authHandler.getType())) {
throw new AuthenticationException("Invalid AuthenticationToken type");
}
if (token.isExpired()) {
throw new AuthenticationException("AuthenticationToken expired");
}
}
}
return token;
}
void parseBrowserUserAgents(String userAgents) {
String[] agentsArray = userAgents.split(",");
browserUserAgents = new HashSet<>();
for (String patternString : agentsArray) {
browserUserAgents.add(Pattern.compile(patternString));
}
}
boolean isBrowser(String userAgent) {
if (userAgent == null) {
return false;
}
if (browserUserAgents != null) {
for (Pattern pattern : browserUserAgents) {
Matcher matcher = pattern.matcher(userAgent);
if (matcher.matches()) {
return true;
}
}
}
return false;
}
}