blob: 3c30be605c8d9fc6262317a551195e2dfd39a824 [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.knox.gateway.hadoopauth.filter;
import org.apache.knox.gateway.GatewayServer;
import org.apache.knox.gateway.audit.api.Action;
import org.apache.knox.gateway.audit.api.ActionOutcome;
import org.apache.knox.gateway.audit.api.AuditContext;
import org.apache.knox.gateway.audit.api.AuditService;
import org.apache.knox.gateway.audit.api.AuditServiceFactory;
import org.apache.knox.gateway.audit.api.Auditor;
import org.apache.knox.gateway.audit.api.ResourceType;
import org.apache.knox.gateway.audit.log4j.audit.AuditConstants;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.context.ContextAttributes;
import org.apache.knox.gateway.filter.AbstractGatewayFilter;
import org.apache.knox.gateway.hadoopauth.HadoopAuthMessages;
import org.apache.knox.gateway.hadoopauth.deploy.HadoopAuthDeploymentContributor;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.provider.federation.jwt.filter.JWTFederationFilter;
import org.apache.knox.gateway.security.PrimaryPrincipal;
import org.apache.knox.gateway.services.GatewayServices;
import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.security.AliasService;
import org.apache.knox.gateway.services.security.AliasServiceException;
import org.apache.knox.gateway.util.AuthFilterUtils;
import org.apache.knox.gateway.util.AuthorizationException;
import org.apache.knox.gateway.util.HttpExceptionUtils;
import javax.security.auth.Subject;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.Principal;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;
import static org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_AUTH_UNAUTHENTICATED_PATHS_PARAM;
/*
* see http://hadoop.apache.org/docs/current/hadoop-auth/Configuration.html
*
* CONFIG_PREFIX = "config.prefix
* AUTH_TYPE = "type", AUTH_TOKEN_VALIDITY = "token.validity"
* COOKIE_DOMAIN = "cookie.domain", COOKIE_PATH = "cookie.path"
* SIGNATURE_SECRET = "signature.secret
* TYPE = "kerberos", PRINCIPAL = TYPE + ".principal", KEYTAB = TYPE + ".keytab"
* config.prefix=hadoop.auth.config (default: null)
* hadoop.auth.config.signature.secret=SECRET (default: a simple random number)
* hadoop.auth.config.type=simple|kerberos|CLASS (default: none, would throw exception)
* hadoop.auth.config.token.validity=SECONDS (default: 3600 seconds)
* hadoop.auth.config.cookie.domain=DOMAIN(default: null)
* hadoop.auth.config.cookie.path=PATH (default: null)
* hadoop.auth.config.kerberos.principal=HTTP/localhost@LOCALHOST (default: null)
* hadoop.auth.config.kerberos.keytab=/etc/knox/conf/knox.service.keytab (default: null)
*/
public class HadoopAuthFilter extends
org.apache.hadoop.security.authentication.server.AuthenticationFilter {
static final String SUPPORT_JWT = "support.jwt";
private static final HadoopAuthMessages LOG = MessagesFactory.get(HadoopAuthMessages.class);
/* A semicolon separated list of paths that need to bypass authentication */
private static final String HADOOP_AUTH_UNAUTHENTICATED_PATHS_PARAM = "hadoop.auth.unauthenticated.path.list";
private static AuditService auditService = AuditServiceFactory.getAuditService();
private static Auditor auditor = auditService.getAuditor(
AuditConstants.DEFAULT_AUDITOR_NAME, AuditConstants.KNOX_SERVICE_NAME,
AuditConstants.KNOX_COMPONENT_NAME );
private final Set<String> ignoreDoAs = new HashSet<>();
private JWTFederationFilter jwtFilter;
private Set<String> unAuthenticatedPaths = new HashSet<>(20);
private String topologyName;
@Override
protected Properties getConfiguration(String configPrefix, FilterConfig filterConfig) throws ServletException {
GatewayServices services = GatewayServer.getGatewayServices();
AliasService aliasService = services.getService(ServiceType.ALIAS_SERVICE);
return getConfiguration(aliasService, configPrefix, filterConfig);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.topologyName = (String) filterConfig.getInitParameter("clusterName");
final List<String> initParameterNames = AuthFilterUtils.getInitParameterNamesAsList(filterConfig);
AuthFilterUtils.refreshSuperUserGroupsConfiguration(filterConfig, initParameterNames, topologyName, HadoopAuthDeploymentContributor.NAME);
filterConfig.getServletContext().setAttribute(ContextAttributes.IMPERSONATION_ENABLED_ATTRIBUTE, Boolean.TRUE);
Collection<String> ignoredServices = null;
// Look for GatewayConfig.PROXYUSER_SERVICES_IGNORE_DOAS value in the filter context, which was created
// using the relevant topology file...
String configValue = filterConfig.getInitParameter(GatewayConfig.PROXYUSER_SERVICES_IGNORE_DOAS);
if (configValue != null) {
configValue = configValue.trim();
if (!configValue.isEmpty()) {
ignoredServices = Arrays.asList(configValue.toLowerCase(Locale.ROOT).split("\\s*,\\s*"));
}
}
// If not set in the topology, look for GatewayConfig.PROXYUSER_SERVICES_IGNORE_DOAS in the
// gateway site context
if (ignoredServices == null) {
Object attributeValue = filterConfig.getServletContext().getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
if (attributeValue instanceof GatewayConfig) {
ignoredServices = ((GatewayConfig) attributeValue).getServicesToIgnoreDoAs();
}
}
if (ignoredServices != null) {
ignoreDoAs.addAll(ignoredServices);
}
super.init(filterConfig);
final String supportJwt = filterConfig.getInitParameter(SUPPORT_JWT);
final boolean jwtSupported = Boolean.parseBoolean(supportJwt == null ? "false" : supportJwt);
if (jwtSupported) {
jwtFilter = new JWTFederationFilter();
jwtFilter.init(filterConfig);
LOG.initializedJwtFilter();
}
final String unAuthPathString = filterConfig
.getInitParameter(HADOOP_AUTH_UNAUTHENTICATED_PATHS_PARAM);
/* prepare a list of allowed unauthenticated paths */
AuthFilterUtils.addUnauthPaths(unAuthenticatedPaths, unAuthPathString, DEFAULT_AUTH_UNAUTHENTICATED_PATHS_PARAM);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
/* check for unauthenticated paths to bypass */
if(AuthFilterUtils.doesRequestContainUnauthPath(unAuthenticatedPaths, request)) {
continueWithAnonymousSubject(request, response, filterChain);
return;
}
if (shouldUseJwtFilter(jwtFilter, (HttpServletRequest) request)) {
LOG.useJwtFilter();
jwtFilter.doFilter(request, response, filterChain);
} else {
super.doFilter(request, response, filterChain);
}
}
@Override
protected void doFilter(FilterChain filterChain, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
/* check for unauthenticated paths to bypass */
if(AuthFilterUtils.doesRequestContainUnauthPath(unAuthenticatedPaths, request)) {
continueWithAnonymousSubject(request, response, filterChain);
return;
}
if (shouldUseJwtFilter(jwtFilter, request)) {
LOG.useJwtFilter();
jwtFilter.doFilter(request, response, filterChain);
return;
}
/*
* If impersonation is not ignored for the authenticated user, attempt to set a proxied user if
* one was specified in the doAs query parameter. A comma-delimited list of services/users to
* be ignored may be set in either the relevant topology file or the Gateway's gateway-site
* configuration file using a property named `gateway.proxyuser.services.ignore.doas`
*
* If setting a proxy user, proper authorization checks are made to ensure the authenticated user
* (proxy user) is allowed to set specified proxied user. It is expected that the relevant
* topology file has the required hadoop.proxyuser configurations set.
*/
HttpServletRequest proxyRequest = null;
final String remoteUser = request.getRemoteUser();
if (!ignoreDoAs(remoteUser)) {
final String doAsUser = request.getParameter(AuthFilterUtils.QUERY_PARAMETER_DOAS);
if (doAsUser != null && !doAsUser.equals(remoteUser)) {
LOG.hadoopAuthDoAsUser(doAsUser, remoteUser, request.getRemoteAddr());
if (request.getUserPrincipal() != null) {
try {
proxyRequest = AuthFilterUtils.getProxyRequest(request, doAsUser, topologyName, HadoopAuthDeploymentContributor.NAME);
LOG.hadoopAuthProxyUserSuccess();
} catch (AuthorizationException ex) {
HttpExceptionUtils.createServletExceptionResponse(response, HttpServletResponse.SC_FORBIDDEN, ex);
LOG.hadoopAuthProxyUserFailed(ex);
return;
}
}
}
}
super.doFilter(filterChain, proxyRequest == null ? request : proxyRequest, response);
}
/**
* A function that let's configured unauthenticated path requests to
* pass through without requiring authentication.
* An anonymous subject is created and the request is audited.
*
* Fail gracefully by logging error message.
* @param request
* @param response
* @param chain
*/
private void continueWithAnonymousSubject(final ServletRequest request,
final ServletResponse response, final FilterChain chain)
throws ServletException, IOException {
try {
/* This path is configured as an unauthenticated path let the request through */
final Subject sub = new Subject();
sub.getPrincipals().add(new PrimaryPrincipal("anonymous"));
LOG.unauthenticatedPathBypass(((HttpServletRequest) request).getRequestURI(), unAuthenticatedPaths.toString());
continueWithEstablishedSecurityContext(sub, (HttpServletRequest) request, (HttpServletResponse) response, chain);
} catch (final Exception e) {
LOG.unauthenticatedPathError(
((HttpServletRequest) request).getRequestURI(), e.toString());
throw e;
}
}
protected void continueWithEstablishedSecurityContext(final Subject subject, final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException {
Principal principal = (Principal) subject.getPrincipals(PrimaryPrincipal.class).toArray()[0];
AuditContext context = auditService.getContext();
if (context != null) {
context.setUsername( principal.getName() );
auditService.attachContext(context);
String sourceUri = (String)request.getAttribute( AbstractGatewayFilter.SOURCE_REQUEST_CONTEXT_URL_ATTRIBUTE_NAME );
if (sourceUri != null) {
auditor.audit( Action.AUTHENTICATION , sourceUri, ResourceType.URI, ActionOutcome.SUCCESS );
}
}
try {
Subject.doAs(
subject,
new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws Exception {
chain.doFilter(new AnonymousRequest(request, principal), response);
return null;
}
}
);
}
catch (PrivilegedActionException e) {
Throwable t = e.getCause();
if (t instanceof IOException) {
throw (IOException) t;
}
else if (t instanceof ServletException) {
throw (ServletException) t;
}
else {
throw new ServletException(t);
}
}
}
static boolean shouldUseJwtFilter(JWTFederationFilter jwtFilter, HttpServletRequest request)
throws IOException, ServletException {
return jwtFilter == null ? false : jwtFilter.getWireToken(request) != null;
}
/**
* Tests if the authenticated user/service has impersonation enabled based on previously calculated
* data (see {@link #init(FilterConfig)}.
* <p>
* By default, impersonation is enabled for all. However if an entry in the pre-calculated data
* declares that is it disabled, then return false.
*
* @param userName the user name to test
* @return <code>true</code>, if impersonation is enabled for the relative principal; otherwise, <code>false</code>
*/
boolean ignoreDoAs(String userName) {
// Return true if one the following conditions have been met:
// * the userPrincipal is null
// * the user principal exists on the ignoreDoAs set.
return (userName == null) || userName.isEmpty() || ignoreDoAs.contains(userName.toLowerCase(Locale.ROOT));
}
// Visible for testing
Properties getConfiguration(AliasService aliasService, String configPrefix, FilterConfig filterConfig) throws ServletException {
final Properties props = new Properties();
final Enumeration<String> names = filterConfig.getInitParameterNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
if (name.startsWith(configPrefix)) {
String value = handleAlias(aliasService, filterConfig, filterConfig.getInitParameter(name), name);
props.put(name.substring(configPrefix.length()), value);
}
}
return props;
}
private String handleAlias(AliasService aliasService, FilterConfig filterConfig, String value, String name) throws ServletException {
String result = value;
// Handle the case value is an alias
if (value.startsWith("${ALIAS=") && value.endsWith("}")) {
try {
final String clusterName = filterConfig.getInitParameter("clusterName");
final String alias = value.substring("${ALIAS=".length(), value.length() - 1);
final char[] topologyLevelAliasValue = aliasService.getPasswordFromAliasForCluster(clusterName, alias);
if (topologyLevelAliasValue == null) {
//try on gateway-level
final char[] gatewayLevelAliasValue = aliasService.getPasswordFromAliasForGateway(alias);
if (gatewayLevelAliasValue != null) {
result = String.valueOf(gatewayLevelAliasValue);
} else {
LOG.noAliasStored(clusterName, alias);
}
} else {
result = String.valueOf(topologyLevelAliasValue);
}
} catch (AliasServiceException e) {
throw new ServletException("Unable to retrieve alias for config: " + name, e);
}
}
return result;
}
boolean isJwtSupported() {
return jwtFilter != null;
}
/**
* A wrapper around the request that returns anonymous subject.
*/
private class AnonymousRequest extends HttpServletRequestWrapper {
private Principal principal;
AnonymousRequest(HttpServletRequest req, Principal principal) {
super(req);
this.principal = principal;
}
/**
* The default behavior of this method is to return getRemoteUser()
* on the wrapped request object.
*/
@Override
public String getRemoteUser() {
return principal.getName();
}
/**
* The default behavior of this method is to return getUserPrincipal()
* on the wrapped request object.
*/
@Override
public Principal getUserPrincipal() {
return principal;
}
}
}