[ https://issues.apache.org/jira/browse/KNOX-3048?focusedWorklogId=969992&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-969992 ]
ASF GitHub Bot logged work on KNOX-3048: ---------------------------------------- Author: ASF GitHub Bot Created on: 20/May/25 12:37 Start Date: 20/May/25 12:37 Worklog Time Spent: 10m Work Description: zeroflag commented on code in PR #1043: URL: https://github.com/apache/knox/pull/1043#discussion_r2097694327 ########## gateway-provider-identity-assertion-common/src/main/java/org/apache/knox/gateway/identityasserter/common/filter/CommonIdentityAssertionFilter.java: ########## @@ -62,286 +35,318 @@ 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.ServletContext; +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.AccessController; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.stream.Collectors; + +import static org.apache.knox.gateway.identityasserter.common.filter.AbstractIdentityAsserterDeploymentContributor.IMPERSONATION_PARAMS; +import static org.apache.knox.gateway.identityasserter.common.filter.AbstractIdentityAsserterDeploymentContributor.ROLE; +import static org.apache.knox.gateway.identityasserter.common.filter.VirtualGroupMapper.addRequestFunctions; + public class CommonIdentityAssertionFilter extends AbstractIdentityAssertionFilter { - private static final IdentityAsserterMessages LOG = MessagesFactory.get(IdentityAsserterMessages.class); - - public static final String VIRTUAL_GROUP_MAPPING_PREFIX = "group.mapping."; - public static final String GROUP_PRINCIPAL_MAPPING = "group.principal.mapping"; - public static final String PRINCIPAL_MAPPING = "principal.mapping"; - public static final String ADVANCED_PRINCIPAL_MAPPING = "expression.principal.mapping"; - private static final String PRINCIPAL_PARAM = "user.name"; - private static final String DOAS_PRINCIPAL_PARAM = "doAs"; - static final String IMPERSONATION_ENABLED_PARAM = AuthFilterUtils.PROXYUSER_PREFIX + ".impersonation.enabled"; - - private SimplePrincipalMapper mapper = new SimplePrincipalMapper(); - private final Parser parser = new Parser(); - private VirtualGroupMapper virtualGroupMapper; - /* List of all default and configured impersonation params */ - protected final List<String> impersonationParamsList = new ArrayList<>(); - protected boolean impersonationEnabled; - private AbstractSyntaxTree expressionPrincipalMapping; - private String topologyName; - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - topologyName = (String) filterConfig.getServletContext().getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE); - String principalMapping = filterConfig.getInitParameter(PRINCIPAL_MAPPING); - if (principalMapping == null || principalMapping.isEmpty()) { - principalMapping = filterConfig.getServletContext().getInitParameter(PRINCIPAL_MAPPING); + public static final String VIRTUAL_GROUP_MAPPING_PREFIX = "group.mapping."; + public static final String GROUP_PRINCIPAL_MAPPING = "group.principal.mapping"; + public static final String PRINCIPAL_MAPPING = "principal.mapping"; + public static final String ADVANCED_PRINCIPAL_MAPPING = "expression.principal.mapping"; + private static final IdentityAsserterMessages LOG = MessagesFactory.get(IdentityAsserterMessages.class); + private static final String PRINCIPAL_PARAM = "user.name"; + private static final String DOAS_PRINCIPAL_PARAM = "doAs"; + /* List of all default and configured impersonation params */ + protected final List<String> impersonationParamsList = new ArrayList<>(); + private final Parser parser = new Parser(); + protected boolean impersonationEnabled; + private SimplePrincipalMapper mapper = new SimplePrincipalMapper(); + private VirtualGroupMapper virtualGroupMapper; + private AbstractSyntaxTree expressionPrincipalMapping; + private String topologyName; + + private static List<String> virtualGroupParameterNames(List<String> initParameterNames) { + return initParameterNames == null ? new ArrayList<>() + : initParameterNames.stream().filter(name -> name.startsWith(VIRTUAL_GROUP_MAPPING_PREFIX)).collect(Collectors.toList()); } - String groupPrincipalMapping = filterConfig.getInitParameter(GROUP_PRINCIPAL_MAPPING); - if (groupPrincipalMapping == null || groupPrincipalMapping.isEmpty()) { - groupPrincipalMapping = filterConfig.getServletContext().getInitParameter(GROUP_PRINCIPAL_MAPPING); - } - if (principalMapping != null && !principalMapping.isEmpty() || groupPrincipalMapping != null && !groupPrincipalMapping.isEmpty()) { - try { - mapper.loadMappingTable(principalMapping, groupPrincipalMapping); - } catch (PrincipalMappingException e) { - throw new ServletException("Unable to load principal mapping table.", e); - } + + private static String[] unique(String[] groups) { + return new HashSet<>(Arrays.asList(groups)).toArray(new String[0]); } - expressionPrincipalMapping = parseAdvancedPrincipalMapping(filterConfig); - final List<String> initParameterNames = AuthFilterUtils.getInitParameterNamesAsList(filterConfig); + @Override + public void init(FilterConfig filterConfig) throws ServletException { + topologyName = (String) filterConfig.getServletContext().getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE); + String principalMapping = filterConfig.getInitParameter(PRINCIPAL_MAPPING); + if (principalMapping == null || principalMapping.isEmpty()) { + principalMapping = filterConfig.getServletContext().getInitParameter(PRINCIPAL_MAPPING); + } + String groupPrincipalMapping = filterConfig.getInitParameter(GROUP_PRINCIPAL_MAPPING); + if (groupPrincipalMapping == null || groupPrincipalMapping.isEmpty()) { + groupPrincipalMapping = filterConfig.getServletContext().getInitParameter(GROUP_PRINCIPAL_MAPPING); + } + if (principalMapping != null && !principalMapping.isEmpty() || groupPrincipalMapping != null && !groupPrincipalMapping.isEmpty()) { + try { + mapper.loadMappingTable(principalMapping, groupPrincipalMapping); + } catch (PrincipalMappingException e) { + throw new ServletException("Unable to load principal mapping table.", e); + } + } + expressionPrincipalMapping = parseAdvancedPrincipalMapping(filterConfig); - virtualGroupMapper = new VirtualGroupMapper(loadVirtualGroups(filterConfig, initParameterNames)); + final List<String> initParameterNames = AuthFilterUtils.getInitParameterNamesAsList(filterConfig); - initImpersonationParamsList(filterConfig); - initProxyUserConfiguration(filterConfig, initParameterNames); - } + virtualGroupMapper = new VirtualGroupMapper(loadVirtualGroups(filterConfig, initParameterNames)); - private AbstractSyntaxTree parseAdvancedPrincipalMapping(FilterConfig filterConfig) { - String expression = filterConfig.getInitParameter(ADVANCED_PRINCIPAL_MAPPING); - if (StringUtils.isBlank(expression)) { - expression = filterConfig.getServletContext().getInitParameter(ADVANCED_PRINCIPAL_MAPPING); + initImpersonationParamsList(filterConfig); + initProxyUserConfiguration(filterConfig, initParameterNames); } - return StringUtils.isBlank(expression) ? null : parser.parse(expression); - } - - /* - * Initialize the impersonation params list. - * This list contains query params that needs to be scrubbed - * from the outgoing request. - */ - private void initImpersonationParamsList(FilterConfig filterConfig) { - String impersonationListFromConfig = filterConfig.getInitParameter(IMPERSONATION_PARAMS); - if (impersonationListFromConfig == null || impersonationListFromConfig.isEmpty()) { - impersonationListFromConfig = filterConfig.getServletContext().getInitParameter(IMPERSONATION_PARAMS); + + private AbstractSyntaxTree parseAdvancedPrincipalMapping(FilterConfig filterConfig) { + String expression = filterConfig.getInitParameter(ADVANCED_PRINCIPAL_MAPPING); + if (StringUtils.isBlank(expression)) { + expression = filterConfig.getServletContext().getInitParameter(ADVANCED_PRINCIPAL_MAPPING); + } + return StringUtils.isBlank(expression) ? null : parser.parse(expression); } - /* Add default impersonation params */ - impersonationParamsList.add(DOAS_PRINCIPAL_PARAM); - impersonationParamsList.add(PRINCIPAL_PARAM); - - if (impersonationListFromConfig != null && !impersonationListFromConfig.isEmpty()) { - /* Add configured impersonation params */ - LOG.impersonationConfig(impersonationListFromConfig); - final StringTokenizer t = new StringTokenizer(impersonationListFromConfig, ","); - while (t.hasMoreElements()) { - final String token = t.nextToken().trim(); - if (!impersonationParamsList.contains(token)) { - impersonationParamsList.add(token); + /* + * Initialize the impersonation params list. + * This list contains query params that needs to be scrubbed + * from the outgoing request. + */ + private void initImpersonationParamsList(FilterConfig filterConfig) { + String impersonationListFromConfig = filterConfig.getInitParameter(IMPERSONATION_PARAMS); + if (impersonationListFromConfig == null || impersonationListFromConfig.isEmpty()) { + impersonationListFromConfig = filterConfig.getServletContext().getInitParameter(IMPERSONATION_PARAMS); + } + + /* Add default impersonation params */ + impersonationParamsList.add(DOAS_PRINCIPAL_PARAM); + impersonationParamsList.add(PRINCIPAL_PARAM); + + if (impersonationListFromConfig != null && !impersonationListFromConfig.isEmpty()) { + /* Add configured impersonation params */ + LOG.impersonationConfig(impersonationListFromConfig); + final StringTokenizer t = new StringTokenizer(impersonationListFromConfig, ","); + while (t.hasMoreElements()) { + final String token = t.nextToken().trim(); + if (!impersonationParamsList.contains(token)) { + impersonationParamsList.add(token); + } + } } - } } - } - - private void initProxyUserConfiguration(FilterConfig filterConfig, List<String> initParameterNames) { - final String impersonationEnabledValue = filterConfig.getInitParameter(IMPERSONATION_ENABLED_PARAM); - impersonationEnabled = impersonationEnabledValue == null ? Boolean.FALSE : Boolean.parseBoolean(impersonationEnabledValue); - - if (impersonationEnabled) { - if (AuthFilterUtils.hasProxyConfig(topologyName, "HadoopAuth")) { - LOG.ignoreProxyuserConfig(); - impersonationEnabled = false; //explicitly set to false to avoid redundant authorization attempts at request processing time - } else { - AuthFilterUtils.refreshSuperUserGroupsConfiguration(filterConfig, initParameterNames, topologyName, ROLE); - filterConfig.getServletContext().setAttribute(ContextAttributes.IMPERSONATION_ENABLED_ATTRIBUTE, Boolean.TRUE); - } - } else { - filterConfig.getServletContext().setAttribute(ContextAttributes.IMPERSONATION_ENABLED_ATTRIBUTE, Boolean.FALSE); + + private void initProxyUserConfiguration(FilterConfig filterConfig, List<String> initParameterNames) { + impersonationEnabled = shouldAddImpersonationProvider(filterConfig); + if (impersonationEnabled) { + if (AuthFilterUtils.hasProxyConfig(topologyName, "HadoopAuth")) { + LOG.ignoreProxyuserConfig(); + impersonationEnabled = false; //explicitly set to false to avoid redundant authorization attempts at request processing time + } else { + AuthFilterUtils.refreshSuperUserGroupsConfiguration(filterConfig, initParameterNames, topologyName, ROLE); + filterConfig.getServletContext().setAttribute(ContextAttributes.IMPERSONATION_ENABLED_ATTRIBUTE, Boolean.TRUE); + } + } else { + filterConfig.getServletContext().setAttribute(ContextAttributes.IMPERSONATION_ENABLED_ATTRIBUTE, Boolean.FALSE); + } } - } - boolean isImpersonationEnabled() { - return impersonationEnabled; - } + /** + * Check if the impersonation provider should be added to the filter chain based on + * user and group impersonation settings. + * + * @param filterConfig + * @return + */ + boolean shouldAddImpersonationProvider(final FilterConfig filterConfig) { Review Comment: Hi Sandeep, if I am not mistaken, the only relevant change in this file is how the `impersonationEnabled` flag is determined. Are all the other changes just related to formatting? ########## gateway-spi/src/main/java/org/apache/knox/gateway/util/AuthFilterUtils.java: ########## @@ -27,227 +39,270 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import javax.servlet.FilterConfig; -import javax.servlet.ServletContext; -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; +public class AuthFilterUtils { + public static final String DEFAULT_AUTH_UNAUTHENTICATED_PATHS_PARAM = "/knoxtoken/api/v1/jwks.json"; + public static final String PROXYUSER_PREFIX = "hadoop.proxyuser"; + public static final String PROXYGROUP_PREFIX = "hadoop.proxygroup"; + public static final String IMPERSONATION_MODE = "hadoop.impersonation.mode"; + public static final String DEFAULT_IMPERSONATION_MODE = "OR"; + public static final String QUERY_PARAMETER_DOAS = "doAs"; + public static final String REAL_USER_NAME_ATTRIBUTE = "real.user.name"; + public static final String DO_GLOBAL_LOGOUT_ATTRIBUTE = "do.global.logout"; + public static final String IMPERSONATION_ENABLED_PARAM = AuthFilterUtils.PROXYUSER_PREFIX + ".impersonation.enabled"; + public static final String GROUP_IMPERSONATION_ENABLED_PARAM = AuthFilterUtils.PROXYGROUP_PREFIX + ".impersonation.enabled"; -import org.apache.commons.lang3.StringUtils; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.security.UserGroupInformation; -import org.apache.hadoop.security.authorize.DefaultImpersonationProvider; -import org.apache.hadoop.security.authorize.ImpersonationProvider; -import org.apache.knox.gateway.i18n.GatewaySpiMessages; -import org.apache.knox.gateway.i18n.messages.MessagesFactory; -public class AuthFilterUtils { - public static final String DEFAULT_AUTH_UNAUTHENTICATED_PATHS_PARAM = "/knoxtoken/api/v1/jwks.json"; - public static final String PROXYUSER_PREFIX = "hadoop.proxyuser"; - public static final String QUERY_PARAMETER_DOAS = "doAs"; - public static final String REAL_USER_NAME_ATTRIBUTE = "real.user.name"; - public static final String DO_GLOBAL_LOGOUT_ATTRIBUTE = "do.global.logout"; - - private static final GatewaySpiMessages LOG = MessagesFactory.get(GatewaySpiMessages.class); - private static final Map<String, Map<String, ImpersonationProvider>> TOPOLOGY_IMPERSONATION_PROVIDERS = new ConcurrentHashMap<>(); - private static final Lock refreshSuperUserGroupsLock = new ReentrantLock(); - - /** - * A helper method that checks whether request contains - * unauthenticated path - * @param request - * @return - */ - public static boolean doesRequestContainUnauthPath( - final Set<String> unAuthenticatedPaths, final ServletRequest request) { - /* make sure the path matches EXACTLY to prevent auth bypass */ - return unAuthenticatedPaths.contains(((HttpServletRequest) request).getPathInfo()); - } - - /** - * A helper method that parses a string and adds to the - * provided unauthenticated set. - * @param unAuthenticatedPaths - * @param list - */ - public static void parseStringThenAdd(final Set<String> unAuthenticatedPaths, final String list) { - final StringTokenizer tokenizer = new StringTokenizer(list, ";,"); - while (tokenizer.hasMoreTokens()) { - unAuthenticatedPaths.add(tokenizer.nextToken()); - } - } - - /** - * A method that parses a string (delimiters = ;,) and adds them to the - * provided un-authenticated path set. - * @param unAuthenticatedPaths - * @param list - * @param defaultList - */ - public static void addUnauthPaths(final Set<String> unAuthenticatedPaths, final String list, final String defaultList) { - /* add default unauthenticated paths list */ - parseStringThenAdd(unAuthenticatedPaths, defaultList); - /* add provided unauthenticated paths list if specified */ - if (!StringUtils.isBlank(list)) { - AuthFilterUtils.parseStringThenAdd(unAuthenticatedPaths, list); - } - } - - public static void refreshSuperUserGroupsConfiguration(ServletContext context, List<String> initParameterNames, String topologyName, String role) { - if (context == null) { - throw new IllegalArgumentException("Cannot get proxyuser configuration from NULL context"); - } - refreshSuperUserGroupsConfiguration(context, null, initParameterNames, topologyName, role); - } - - public static void refreshSuperUserGroupsConfiguration(FilterConfig filterConfig, List<String> initParameterNames, String topologyName, String role) { - if (filterConfig == null) { - throw new IllegalArgumentException("Cannot get proxyuser configuration from NULL filter config"); - } - refreshSuperUserGroupsConfiguration(null, filterConfig, initParameterNames, topologyName, role); - } - - private static void refreshSuperUserGroupsConfiguration(ServletContext context, FilterConfig filterConfig, List<String> initParameterNames, String topologyName, String role) { - final Configuration conf = new Configuration(false); - if (initParameterNames != null) { - initParameterNames.stream().filter(name -> name.startsWith(PROXYUSER_PREFIX + ".")).forEach(name -> { - String value = context == null ? filterConfig.getInitParameter(name) : context.getInitParameter(name); - conf.set(name, value); - }); - } - - saveImpersonationProvider(topologyName, role, conf); - } - - private static void saveImpersonationProvider(String topologyName, String role, final Configuration conf) { - refreshSuperUserGroupsLock.lock(); - try { - final ImpersonationProvider impersonationProvider = new DefaultImpersonationProvider(); - impersonationProvider.setConf(conf); - impersonationProvider.init(PROXYUSER_PREFIX); - LOG.createImpersonationProvider(topologyName, role, PROXYUSER_PREFIX, conf.getPropsWithPrefix(PROXYUSER_PREFIX + ".").toString()); - TOPOLOGY_IMPERSONATION_PROVIDERS.putIfAbsent(topologyName, new ConcurrentHashMap<String, ImpersonationProvider>()); - TOPOLOGY_IMPERSONATION_PROVIDERS.get(topologyName).put(role, impersonationProvider); - } finally { - refreshSuperUserGroupsLock.unlock(); - } - } - - public static HttpServletRequest getProxyRequest(HttpServletRequest request, String doAsUser, String topologyName, String role) throws AuthorizationException { - return getProxyRequest(request, request.getUserPrincipal().getName(), doAsUser, topologyName, role); - } - - public static HttpServletRequest getProxyRequest(HttpServletRequest request, String remoteUser, String doAsUser, String topologyName, String role) throws AuthorizationException { - final UserGroupInformation remoteRequestUgi = getRemoteRequestUgi(remoteUser, doAsUser); - if (remoteRequestUgi != null) { - authorizeImpersonationRequest(request, remoteRequestUgi, topologyName, role); - - return new HttpServletRequestWrapper(request) { - @Override - public String getRemoteUser() { - return remoteRequestUgi.getShortUserName(); + private static final GatewaySpiMessages LOG = MessagesFactory.get(GatewaySpiMessages.class); + private static final Map<String, Map<String, ImpersonationProvider>> TOPOLOGY_IMPERSONATION_PROVIDERS = new ConcurrentHashMap<>(); + private static final Lock refreshSuperUserGroupsLock = new ReentrantLock(); + + /** + * A helper method that checks whether request contains + * unauthenticated path + * + * @param request + * @return + */ + public static boolean doesRequestContainUnauthPath( + final Set<String> unAuthenticatedPaths, final ServletRequest request) { + /* make sure the path matches EXACTLY to prevent auth bypass */ + return unAuthenticatedPaths.contains(((HttpServletRequest) request).getPathInfo()); + } + + /** + * A helper method that parses a string and adds to the + * provided unauthenticated set. + * + * @param unAuthenticatedPaths + * @param list + */ + public static void parseStringThenAdd(final Set<String> unAuthenticatedPaths, final String list) { + final StringTokenizer tokenizer = new StringTokenizer(list, ";,"); + while (tokenizer.hasMoreTokens()) { + unAuthenticatedPaths.add(tokenizer.nextToken()); + } + } + + /** + * A method that parses a string (delimiters = ;,) and adds them to the + * provided un-authenticated path set. + * + * @param unAuthenticatedPaths + * @param list + * @param defaultList + */ + public static void addUnauthPaths(final Set<String> unAuthenticatedPaths, final String list, final String defaultList) { + /* add default unauthenticated paths list */ + parseStringThenAdd(unAuthenticatedPaths, defaultList); + /* add provided unauthenticated paths list if specified */ + if (!StringUtils.isBlank(list)) { + AuthFilterUtils.parseStringThenAdd(unAuthenticatedPaths, list); + } + } + public static void refreshSuperUserGroupsConfiguration(FilterConfig filterConfig, List<String> initParameterNames, String topologyName, String role) { + if (filterConfig == null) { + throw new IllegalArgumentException("Cannot get proxyuser configuration from NULL filter config"); + } + refreshSuperUserGroupsConfiguration(null, filterConfig, initParameterNames, topologyName, role); + } + + private static void refreshSuperUserGroupsConfiguration(ServletContext context, FilterConfig filterConfig, List<String> initParameterNames, String topologyName, String role) { + if (filterConfig == null) { + throw new IllegalArgumentException("Cannot get proxyuser configuration from NULL filter config"); + } + + final Configuration conf = new Configuration(false); + if (initParameterNames != null) { + initParameterNames.stream().filter(name -> name.startsWith(PROXYUSER_PREFIX + ".")).forEach(name -> { + String value = context == null ? filterConfig.getInitParameter(name) : context.getInitParameter(name); + conf.set(name, value); + }); + + initParameterNames.stream().filter(name -> name.startsWith(PROXYGROUP_PREFIX + ".")).forEach(name -> { + String value = context == null ? filterConfig.getInitParameter(name) : context.getInitParameter(name); + conf.set(name, value); + }); + + initParameterNames.stream().filter(name -> name.startsWith(IMPERSONATION_MODE + ".")).forEach(name -> { + String value = context == null ? filterConfig.getInitParameter(name) : context.getInitParameter(name); + conf.set(name, value); + }); + + + } + + saveImpersonationProvider(filterConfig, topologyName, role, conf); + } + + private static void saveImpersonationProvider(FilterConfig filterConfig, String topologyName, String role, final Configuration conf) { + refreshSuperUserGroupsLock.lock(); + try { + boolean[] impersonationFlags = getImpersonationEnabledFlags(filterConfig); + boolean isProxyUserEnabled = impersonationFlags[0]; + boolean isProxyGroupEnabled = impersonationFlags[1]; + + final GroupBasedImpersonationProvider groupBasedImpersonationProvider = new GroupBasedImpersonationProvider(isProxyUserEnabled, isProxyGroupEnabled); + groupBasedImpersonationProvider.setConf(conf); + groupBasedImpersonationProvider.init(PROXYUSER_PREFIX, PROXYGROUP_PREFIX); + LOG.createImpersonationProvider(topologyName, role, PROXYUSER_PREFIX, conf.getPropsWithPrefix(PROXYUSER_PREFIX + ".").toString()); + LOG.createImpersonationProvider(topologyName, role, PROXYGROUP_PREFIX, conf.getPropsWithPrefix(PROXYGROUP_PREFIX + ".").toString()); + + TOPOLOGY_IMPERSONATION_PROVIDERS.putIfAbsent(topologyName, new ConcurrentHashMap<String, ImpersonationProvider>()); + TOPOLOGY_IMPERSONATION_PROVIDERS.get(topologyName).put(role, groupBasedImpersonationProvider); + } finally { + refreshSuperUserGroupsLock.unlock(); + } + } + + public static HttpServletRequest getProxyRequest(HttpServletRequest request, String doAsUser, String topologyName, String role) throws AuthorizationException { + return getProxyRequest(request, request.getUserPrincipal().getName(), doAsUser, topologyName, role); + } + + public static HttpServletRequest getProxyRequest(HttpServletRequest request, String remoteUser, String doAsUser, String topologyName, String role) throws AuthorizationException { + final UserGroupInformation remoteRequestUgi = getRemoteRequestUgi(remoteUser, doAsUser); + if (remoteRequestUgi != null) { + authorizeImpersonationRequest(request, remoteRequestUgi, topologyName, role); + + return new HttpServletRequestWrapper(request) { + @Override + public String getRemoteUser() { + return remoteRequestUgi.getShortUserName(); + } + + @Override + public Principal getUserPrincipal() { + return remoteRequestUgi::getUserName; + } + + @Override + public Object getAttribute(String name) { + if (name != null && name.equals(REAL_USER_NAME_ATTRIBUTE)) { + return remoteRequestUgi.getRealUser().getShortUserName(); + } else { + return super.getAttribute(name); + } + } + }; + } + return null; + } - @Override - public Principal getUserPrincipal() { - return remoteRequestUgi::getUserName; + public static void authorizeImpersonationRequest(HttpServletRequest request, String remoteUser, String doAsUser, String topologyName, String role) throws AuthorizationException { + final UserGroupInformation remoteRequestUgi = getRemoteRequestUgi(remoteUser, doAsUser); + if (remoteRequestUgi != null) { + authorizeImpersonationRequest(request, remoteRequestUgi, topologyName, role); } + } + + private static void authorizeImpersonationRequest(HttpServletRequest request, UserGroupInformation remoteRequestUgi, String topologyName, String role) + throws AuthorizationException { - @Override - public Object getAttribute(String name) { - if (name != null && name.equals(REAL_USER_NAME_ATTRIBUTE)) { - return remoteRequestUgi.getRealUser().getShortUserName(); - } else { - return super.getAttribute(name); - } + final ImpersonationProvider impersonationProvider = getImpersonationProvider(topologyName, role); + + if (impersonationProvider != null) { + try { + impersonationProvider.authorize(remoteRequestUgi, request.getRemoteAddr()); + } catch (org.apache.hadoop.security.authorize.AuthorizationException e) { + throw new AuthorizationException(e); + } + } else { + throw new AuthorizationException("ImpersonationProvider for " + topologyName + " / " + role + " not found!"); } - }; - - } - return null; - } - - public static void authorizeImpersonationRequest(HttpServletRequest request, String remoteUser, String doAsUser, String topologyName, String role) throws AuthorizationException { - final UserGroupInformation remoteRequestUgi = getRemoteRequestUgi(remoteUser, doAsUser); - if (remoteRequestUgi != null) { - authorizeImpersonationRequest(request, remoteRequestUgi, topologyName, role); - } - } - - private static void authorizeImpersonationRequest(HttpServletRequest request, UserGroupInformation remoteRequestUgi, String topologyName, String role) - throws AuthorizationException { - - final ImpersonationProvider impersonationProvider = getImpersonationProvider(topologyName, role); - - if (impersonationProvider != null) { - try { - impersonationProvider.authorize(remoteRequestUgi, request.getRemoteAddr()); - } catch (org.apache.hadoop.security.authorize.AuthorizationException e) { - throw new AuthorizationException(e); - } - } else { - throw new AuthorizationException("ImpersonationProvider for " + topologyName + " / " + role + " not found!"); - } - } - - private static ImpersonationProvider getImpersonationProvider(String topologyName, String role) { - refreshSuperUserGroupsLock.lock(); - final ImpersonationProvider impersonationProvider; - try { - impersonationProvider = (TOPOLOGY_IMPERSONATION_PROVIDERS.getOrDefault(topologyName, Collections.emptyMap())).get(role); - } finally { - refreshSuperUserGroupsLock.unlock(); - } - return impersonationProvider; - } - - private static UserGroupInformation getRemoteRequestUgi(String remoteUser, String doAsUser) { - if (remoteUser != null) { - final UserGroupInformation remoteUserUgi = UserGroupInformation.createRemoteUser(remoteUser); - return UserGroupInformation.createProxyUser(doAsUser, remoteUserUgi); - } - return null; - } - - public static boolean hasProxyConfig(String topologyName, String role) { - return getImpersonationProvider(topologyName, role) != null; - } - - public static void removeProxyUserConfig(String topologyName, String role) { - if (hasProxyConfig(topologyName, role)) { - refreshSuperUserGroupsLock.lock(); - try { - TOPOLOGY_IMPERSONATION_PROVIDERS.get(topologyName).remove(role); - } finally { - refreshSuperUserGroupsLock.unlock(); - } - } - } - - /** - * FilterConfig.getInitParameters() returns an enumeration and the first time we - * iterate thru on its elements we can process the parameter names as desired - * (because hasMoreElements returns true). The subsequent calls, however, will not - * succeed because getInitParameters() returns the same object where the - * hasMoreElements returns false. - * <p> - * In classes where there are multiple iterations should be conducted, a - * collection should be used instead. - * - * @return the names of the filter's initialization parameters as a List of - * String objects, or an empty List if the filter has no initialization - * parameters. - */ - public static List<String> getInitParameterNamesAsList(FilterConfig filterConfig) { - return filterConfig.getInitParameterNames() == null ? Collections.emptyList() : Collections.list(filterConfig.getInitParameterNames()); - } - - public static void markDoGlobalLogoutInRequest(HttpServletRequest request) { - request.setAttribute(DO_GLOBAL_LOGOUT_ATTRIBUTE, "true"); - } - - public static boolean shouldDoGlobalLogout(HttpServletRequest request) { - return request.getAttribute(DO_GLOBAL_LOGOUT_ATTRIBUTE) == null ? false : Boolean.parseBoolean((String) request.getAttribute(DO_GLOBAL_LOGOUT_ATTRIBUTE)); - } + } + + private static ImpersonationProvider getImpersonationProvider(String topologyName, String role) { + refreshSuperUserGroupsLock.lock(); + final ImpersonationProvider impersonationProvider; + try { + impersonationProvider = (TOPOLOGY_IMPERSONATION_PROVIDERS.getOrDefault(topologyName, Collections.emptyMap())).get(role); + } finally { + refreshSuperUserGroupsLock.unlock(); + } + return impersonationProvider; + } + + private static UserGroupInformation getRemoteRequestUgi(String remoteUser, String doAsUser) { + if (remoteUser != null) { + final UserGroupInformation remoteUserUgi = UserGroupInformation.createRemoteUser(remoteUser); + return UserGroupInformation.createProxyUser(doAsUser, remoteUserUgi); + } + return null; + } + + public static boolean hasProxyConfig(String topologyName, String role) { + return getImpersonationProvider(topologyName, role) != null; + } + + public static void removeProxyUserConfig(String topologyName, String role) { + if (hasProxyConfig(topologyName, role)) { + refreshSuperUserGroupsLock.lock(); + try { + TOPOLOGY_IMPERSONATION_PROVIDERS.get(topologyName).remove(role); + } finally { + refreshSuperUserGroupsLock.unlock(); + } + } + } + + /** + * FilterConfig.getInitParameters() returns an enumeration and the first time we + * iterate thru on its elements we can process the parameter names as desired + * (because hasMoreElements returns true). The subsequent calls, however, will not + * succeed because getInitParameters() returns the same object where the + * hasMoreElements returns false. + * <p> + * In classes where there are multiple iterations should be conducted, a + * collection should be used instead. + * + * @return the names of the filter's initialization parameters as a List of + * String objects, or an empty List if the filter has no initialization + * parameters. + */ + public static List<String> getInitParameterNamesAsList(FilterConfig filterConfig) { + return filterConfig.getInitParameterNames() == null ? Collections.emptyList() : Collections.list(filterConfig.getInitParameterNames()); + } + + public static void markDoGlobalLogoutInRequest(HttpServletRequest request) { + request.setAttribute(DO_GLOBAL_LOGOUT_ATTRIBUTE, "true"); + } + + public static boolean shouldDoGlobalLogout(HttpServletRequest request) { + return request.getAttribute(DO_GLOBAL_LOGOUT_ATTRIBUTE) == null ? false : Boolean.parseBoolean((String) request.getAttribute(DO_GLOBAL_LOGOUT_ATTRIBUTE)); + } + + /** + * Check if user or group impersonation is enabled based on filter configuration. + * + * @param filterConfig The filter configuration + * @return A boolean array where the first element indicates if user impersonation is enabled + * and the second element indicates if group impersonation is enabled + */ + public static boolean[] getImpersonationEnabledFlags(final FilterConfig filterConfig) { Review Comment: Maybe using enums instead of the boolean array would be more robust. For example: ```java public enum ImperonationFlags { USER_IMPERSONATION, GROUP_IMPERSONATION } ``` Usage: ```java public static EnumSet<ImperonationFlags> getImpersonationEnabledFlags(final FilterConfig filterConfig) { ... return EnumSet.of(USER_IMPERSONATION); // or return EnumSet.of(GROUP_IMPERSONATION); // or return EnumSet.of(USER_IMPERSONATION, GROUP_IMPERSONATION); } ``` ########## gateway-spi/src/main/java/org/apache/knox/gateway/util/AuthFilterUtils.java: ########## @@ -27,227 +39,270 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import javax.servlet.FilterConfig; -import javax.servlet.ServletContext; -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; +public class AuthFilterUtils { + public static final String DEFAULT_AUTH_UNAUTHENTICATED_PATHS_PARAM = "/knoxtoken/api/v1/jwks.json"; + public static final String PROXYUSER_PREFIX = "hadoop.proxyuser"; + public static final String PROXYGROUP_PREFIX = "hadoop.proxygroup"; + public static final String IMPERSONATION_MODE = "hadoop.impersonation.mode"; + public static final String DEFAULT_IMPERSONATION_MODE = "OR"; Review Comment: Could illustrate it with an example, how the OR and AND impersonation modes are used? ########## gateway-spi/src/main/java/org/apache/knox/gateway/util/GroupBasedImpersonationProvider.java: ########## @@ -0,0 +1,252 @@ +/* + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.util; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.authorize.AccessControlList; +import org.apache.hadoop.security.authorize.AuthorizationException; +import org.apache.hadoop.security.authorize.DefaultImpersonationProvider; +import org.apache.hadoop.util.MachineList; +import org.apache.knox.gateway.i18n.GatewaySpiMessages; +import org.apache.knox.gateway.i18n.messages.MessagesFactory; + +import java.net.InetAddress; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import static org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_IMPERSONATION_MODE; +import static org.apache.knox.gateway.util.AuthFilterUtils.IMPERSONATION_MODE; + +/** + * An extension of Hadoop's DefaultImpersonationProvider that adds support for group-based impersonation. + * This provider allows users who belong to specific groups to impersonate other users. + */ +public class GroupBasedImpersonationProvider extends DefaultImpersonationProvider { + private static final GatewaySpiMessages LOG = MessagesFactory.get(GatewaySpiMessages.class); + private static final String CONF_HOSTS = ".hosts"; + private static final String CONF_USERS = ".users"; + private static final String CONF_GROUPS = ".groups"; + private final Map<String, AccessControlList> proxyGroupsAcls = new HashMap<>(); + /* is the proxy user configuration enabled */ + boolean isProxyUserEnabled = true; + /* is the proxy group configuration enabled, defaulting it to false for backwards compatibility */ + boolean isProxyGroupEnabled; + private Map<String, MachineList> groupProxyHosts = new HashMap<>(); + private String groupConfigPrefix; + private String impersonationMode = DEFAULT_IMPERSONATION_MODE; + + public GroupBasedImpersonationProvider() { + super(); + } + + public GroupBasedImpersonationProvider(boolean isProxyUserEnabled, boolean isProxyGroupEnabled) { + super(); + this.isProxyUserEnabled = isProxyUserEnabled; + this.isProxyGroupEnabled = isProxyGroupEnabled; + } + + @Override + public Configuration getConf() { + return super.getConf(); + } + + @Override + public void setConf(Configuration conf) { + super.setConf(conf); + } + + public void init(String configurationPrefix, String proxyGroupPrefix) { + super.init(configurationPrefix); + initGroupBasedProvider(proxyGroupPrefix); + } + + private void initGroupBasedProvider(String proxyGroupPrefix) { + impersonationMode = getConf().get(IMPERSONATION_MODE, DEFAULT_IMPERSONATION_MODE); + groupConfigPrefix = proxyGroupPrefix + + (proxyGroupPrefix.endsWith(".") ? "" : "."); + + // constructing regex to match the following patterns: + // $configPrefix.[ANY].users + // $configPrefix.[ANY].groups + // $configPrefix.[ANY].hosts + // + String prefixRegEx = groupConfigPrefix.replace(".", "\\."); + String usersGroupsRegEx = prefixRegEx + "[\\S]*(" + + Pattern.quote(CONF_USERS) + "|" + Pattern.quote(CONF_GROUPS) + ")"; + String hostsRegEx = prefixRegEx + "[\\S]*" + Pattern.quote(CONF_HOSTS); + + // get list of users and groups per proxygroup + // Map of <hadoop.proxygroup.[VIRTUAL_GROUP].users|groups, group1,group2> + Map<String, String> allMatchKeys = + getConf().getValByRegex(usersGroupsRegEx); + + for (Map.Entry<String, String> entry : allMatchKeys.entrySet()) { + //aclKey = hadoop.proxygroup.[VIRTUAL_GROUP] + String aclKey = getAclKey(entry.getKey()); + + if (!proxyGroupsAcls.containsKey(aclKey)) { + proxyGroupsAcls.put(aclKey, new AccessControlList( + allMatchKeys.get(aclKey + CONF_USERS), + allMatchKeys.get(aclKey + CONF_GROUPS))); + } + } + + // get hosts per proxygroup + allMatchKeys = getConf().getValByRegex(hostsRegEx); + for (Map.Entry<String, String> entry : allMatchKeys.entrySet()) { + groupProxyHosts.put(entry.getKey(), + new MachineList(entry.getValue())); + } + } + + private String getAclKey(String key) { + int endIndex = key.lastIndexOf('.'); + if (endIndex != -1) { + return key.substring(0, endIndex); + } + return key; + } + + @Override + public void authorize(UserGroupInformation user, InetAddress remoteAddress) throws AuthorizationException { + // If both authorization methods are disabled, allow the operation to proceed + if (!isProxyUserEnabled && !isProxyGroupEnabled) { + LOG.successfulImpersonation(user.getRealUser().getUserName(), user.getUserName()); + return; + } + + boolean isProxyUserAuthorized = false; + boolean isProxyGroupAuthorized = false; + if (isProxyUserEnabled) { + /* authorize proxy user */ + isProxyUserAuthorized = checkProxyUserAuthorization(user, remoteAddress); + /* In case of AND no reason to check for proxy groups if isProxyUserAuthorized=false */ + if("AND".equalsIgnoreCase(impersonationMode) && !isProxyUserAuthorized) { Review Comment: I would either extract this to a string constant, or introduce an enum. ########## gateway-spi/src/main/java/org/apache/knox/gateway/util/GroupBasedImpersonationProvider.java: ########## @@ -0,0 +1,252 @@ +/* + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.util; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.authorize.AccessControlList; +import org.apache.hadoop.security.authorize.AuthorizationException; +import org.apache.hadoop.security.authorize.DefaultImpersonationProvider; +import org.apache.hadoop.util.MachineList; +import org.apache.knox.gateway.i18n.GatewaySpiMessages; +import org.apache.knox.gateway.i18n.messages.MessagesFactory; + +import java.net.InetAddress; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import static org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_IMPERSONATION_MODE; +import static org.apache.knox.gateway.util.AuthFilterUtils.IMPERSONATION_MODE; + +/** + * An extension of Hadoop's DefaultImpersonationProvider that adds support for group-based impersonation. + * This provider allows users who belong to specific groups to impersonate other users. + */ +public class GroupBasedImpersonationProvider extends DefaultImpersonationProvider { + private static final GatewaySpiMessages LOG = MessagesFactory.get(GatewaySpiMessages.class); + private static final String CONF_HOSTS = ".hosts"; + private static final String CONF_USERS = ".users"; + private static final String CONF_GROUPS = ".groups"; + private final Map<String, AccessControlList> proxyGroupsAcls = new HashMap<>(); + /* is the proxy user configuration enabled */ + boolean isProxyUserEnabled = true; + /* is the proxy group configuration enabled, defaulting it to false for backwards compatibility */ + boolean isProxyGroupEnabled; + private Map<String, MachineList> groupProxyHosts = new HashMap<>(); + private String groupConfigPrefix; + private String impersonationMode = DEFAULT_IMPERSONATION_MODE; + + public GroupBasedImpersonationProvider() { + super(); + } + + public GroupBasedImpersonationProvider(boolean isProxyUserEnabled, boolean isProxyGroupEnabled) { + super(); + this.isProxyUserEnabled = isProxyUserEnabled; + this.isProxyGroupEnabled = isProxyGroupEnabled; + } + + @Override + public Configuration getConf() { + return super.getConf(); + } + + @Override + public void setConf(Configuration conf) { + super.setConf(conf); + } + + public void init(String configurationPrefix, String proxyGroupPrefix) { + super.init(configurationPrefix); + initGroupBasedProvider(proxyGroupPrefix); + } + + private void initGroupBasedProvider(String proxyGroupPrefix) { + impersonationMode = getConf().get(IMPERSONATION_MODE, DEFAULT_IMPERSONATION_MODE); + groupConfigPrefix = proxyGroupPrefix + + (proxyGroupPrefix.endsWith(".") ? "" : "."); + + // constructing regex to match the following patterns: + // $configPrefix.[ANY].users + // $configPrefix.[ANY].groups + // $configPrefix.[ANY].hosts + // + String prefixRegEx = groupConfigPrefix.replace(".", "\\."); + String usersGroupsRegEx = prefixRegEx + "[\\S]*(" + + Pattern.quote(CONF_USERS) + "|" + Pattern.quote(CONF_GROUPS) + ")"; + String hostsRegEx = prefixRegEx + "[\\S]*" + Pattern.quote(CONF_HOSTS); + + // get list of users and groups per proxygroup + // Map of <hadoop.proxygroup.[VIRTUAL_GROUP].users|groups, group1,group2> + Map<String, String> allMatchKeys = + getConf().getValByRegex(usersGroupsRegEx); + + for (Map.Entry<String, String> entry : allMatchKeys.entrySet()) { + //aclKey = hadoop.proxygroup.[VIRTUAL_GROUP] + String aclKey = getAclKey(entry.getKey()); + + if (!proxyGroupsAcls.containsKey(aclKey)) { + proxyGroupsAcls.put(aclKey, new AccessControlList( + allMatchKeys.get(aclKey + CONF_USERS), + allMatchKeys.get(aclKey + CONF_GROUPS))); + } + } + + // get hosts per proxygroup + allMatchKeys = getConf().getValByRegex(hostsRegEx); + for (Map.Entry<String, String> entry : allMatchKeys.entrySet()) { + groupProxyHosts.put(entry.getKey(), + new MachineList(entry.getValue())); + } + } + + private String getAclKey(String key) { + int endIndex = key.lastIndexOf('.'); + if (endIndex != -1) { + return key.substring(0, endIndex); + } + return key; + } + + @Override + public void authorize(UserGroupInformation user, InetAddress remoteAddress) throws AuthorizationException { + // If both authorization methods are disabled, allow the operation to proceed + if (!isProxyUserEnabled && !isProxyGroupEnabled) { + LOG.successfulImpersonation(user.getRealUser().getUserName(), user.getUserName()); + return; + } + + boolean isProxyUserAuthorized = false; + boolean isProxyGroupAuthorized = false; + if (isProxyUserEnabled) { + /* authorize proxy user */ + isProxyUserAuthorized = checkProxyUserAuthorization(user, remoteAddress); + /* In case of AND no reason to check for proxy groups if isProxyUserAuthorized=false */ + if("AND".equalsIgnoreCase(impersonationMode) && !isProxyUserAuthorized) { + throw new AuthorizationException("User: " + user.getRealUser().getUserName() + + " is not allowed to impersonate " + user.getUserName()); + } + } + + if (isProxyGroupEnabled) { + /* check for proxy group impersonation */ + isProxyGroupAuthorized = checkProxyGroupAuthorization(user, remoteAddress); + } + + if("AND".equalsIgnoreCase(impersonationMode)) { Review Comment: Same as above. ########## gateway-spi/src/main/java/org/apache/knox/gateway/util/GroupBasedImpersonationProvider.java: ########## @@ -0,0 +1,252 @@ +/* + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.util; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.authorize.AccessControlList; +import org.apache.hadoop.security.authorize.AuthorizationException; +import org.apache.hadoop.security.authorize.DefaultImpersonationProvider; +import org.apache.hadoop.util.MachineList; +import org.apache.knox.gateway.i18n.GatewaySpiMessages; +import org.apache.knox.gateway.i18n.messages.MessagesFactory; + +import java.net.InetAddress; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import static org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_IMPERSONATION_MODE; +import static org.apache.knox.gateway.util.AuthFilterUtils.IMPERSONATION_MODE; + +/** + * An extension of Hadoop's DefaultImpersonationProvider that adds support for group-based impersonation. + * This provider allows users who belong to specific groups to impersonate other users. + */ +public class GroupBasedImpersonationProvider extends DefaultImpersonationProvider { + private static final GatewaySpiMessages LOG = MessagesFactory.get(GatewaySpiMessages.class); + private static final String CONF_HOSTS = ".hosts"; + private static final String CONF_USERS = ".users"; + private static final String CONF_GROUPS = ".groups"; + private final Map<String, AccessControlList> proxyGroupsAcls = new HashMap<>(); + /* is the proxy user configuration enabled */ + boolean isProxyUserEnabled = true; + /* is the proxy group configuration enabled, defaulting it to false for backwards compatibility */ + boolean isProxyGroupEnabled; + private Map<String, MachineList> groupProxyHosts = new HashMap<>(); + private String groupConfigPrefix; + private String impersonationMode = DEFAULT_IMPERSONATION_MODE; + + public GroupBasedImpersonationProvider() { + super(); + } + + public GroupBasedImpersonationProvider(boolean isProxyUserEnabled, boolean isProxyGroupEnabled) { + super(); + this.isProxyUserEnabled = isProxyUserEnabled; + this.isProxyGroupEnabled = isProxyGroupEnabled; + } + + @Override + public Configuration getConf() { + return super.getConf(); + } + + @Override + public void setConf(Configuration conf) { + super.setConf(conf); + } + + public void init(String configurationPrefix, String proxyGroupPrefix) { + super.init(configurationPrefix); + initGroupBasedProvider(proxyGroupPrefix); + } + + private void initGroupBasedProvider(String proxyGroupPrefix) { + impersonationMode = getConf().get(IMPERSONATION_MODE, DEFAULT_IMPERSONATION_MODE); + groupConfigPrefix = proxyGroupPrefix + + (proxyGroupPrefix.endsWith(".") ? "" : "."); + + // constructing regex to match the following patterns: + // $configPrefix.[ANY].users + // $configPrefix.[ANY].groups + // $configPrefix.[ANY].hosts + // + String prefixRegEx = groupConfigPrefix.replace(".", "\\."); + String usersGroupsRegEx = prefixRegEx + "[\\S]*(" + + Pattern.quote(CONF_USERS) + "|" + Pattern.quote(CONF_GROUPS) + ")"; + String hostsRegEx = prefixRegEx + "[\\S]*" + Pattern.quote(CONF_HOSTS); + + // get list of users and groups per proxygroup + // Map of <hadoop.proxygroup.[VIRTUAL_GROUP].users|groups, group1,group2> + Map<String, String> allMatchKeys = + getConf().getValByRegex(usersGroupsRegEx); + + for (Map.Entry<String, String> entry : allMatchKeys.entrySet()) { + //aclKey = hadoop.proxygroup.[VIRTUAL_GROUP] + String aclKey = getAclKey(entry.getKey()); + + if (!proxyGroupsAcls.containsKey(aclKey)) { + proxyGroupsAcls.put(aclKey, new AccessControlList( + allMatchKeys.get(aclKey + CONF_USERS), + allMatchKeys.get(aclKey + CONF_GROUPS))); + } + } + + // get hosts per proxygroup + allMatchKeys = getConf().getValByRegex(hostsRegEx); + for (Map.Entry<String, String> entry : allMatchKeys.entrySet()) { + groupProxyHosts.put(entry.getKey(), + new MachineList(entry.getValue())); + } + } + + private String getAclKey(String key) { + int endIndex = key.lastIndexOf('.'); + if (endIndex != -1) { + return key.substring(0, endIndex); + } + return key; + } + + @Override + public void authorize(UserGroupInformation user, InetAddress remoteAddress) throws AuthorizationException { + // If both authorization methods are disabled, allow the operation to proceed + if (!isProxyUserEnabled && !isProxyGroupEnabled) { + LOG.successfulImpersonation(user.getRealUser().getUserName(), user.getUserName()); + return; + } + + boolean isProxyUserAuthorized = false; + boolean isProxyGroupAuthorized = false; + if (isProxyUserEnabled) { + /* authorize proxy user */ + isProxyUserAuthorized = checkProxyUserAuthorization(user, remoteAddress); + /* In case of AND no reason to check for proxy groups if isProxyUserAuthorized=false */ + if("AND".equalsIgnoreCase(impersonationMode) && !isProxyUserAuthorized) { + throw new AuthorizationException("User: " + user.getRealUser().getUserName() + + " is not allowed to impersonate " + user.getUserName()); + } + } + + if (isProxyGroupEnabled) { + /* check for proxy group impersonation */ + isProxyGroupAuthorized = checkProxyGroupAuthorization(user, remoteAddress); + } + + if("AND".equalsIgnoreCase(impersonationMode)) { + if (isProxyUserAuthorized && isProxyGroupAuthorized) { + LOG.successfulImpersonation(user.getRealUser().getUserName(), user.getUserName()); + } else { + throw new AuthorizationException("User: " + user.getRealUser().getUserName() + + " is not allowed to impersonate " + user.getUserName()); + } + + } else { + /* OR */ + if (isProxyUserAuthorized || isProxyGroupAuthorized) { + LOG.successfulImpersonation(user.getRealUser().getUserName(), user.getUserName()); + } else { + throw new AuthorizationException("User: " + user.getRealUser().getUserName() + + " is not allowed to impersonate " + user.getUserName()); + } + } + } + + /** + * Helper method to check if the user is authorized to impersonate + * Returns true if the user is authorized, false otherwise. + * @param user + * @param remoteAddress + * @return + */ + private boolean checkProxyUserAuthorization(final UserGroupInformation user, final InetAddress remoteAddress) { + try { + super.authorize(user, remoteAddress); + return true; + } catch (final AuthorizationException e) { + LOG.failedToImpersonateUser(user.getRealUser().getUserName(), remoteAddress.getHostAddress()); + return false; + } + } + + /** + * Helper method to check if the group a given user belongs to is authorized to impersonate + * Returns true if the user is authorized, false otherwise. + * @param user + * @param remoteAddress + * @return + */ + private boolean checkProxyGroupAuthorization(final UserGroupInformation user, final InetAddress remoteAddress) { + if (user == null) { + throw new IllegalArgumentException("user is null."); + } + + final UserGroupInformation realUser = user.getRealUser(); + if (realUser == null) { + return true; + } + + // Get the real user's groups (both real and virtual) + Set<String> realUserGroups = new HashSet<>(); + if (user.getRealUser().getGroupNames() != null) { + Collections.addAll(realUserGroups, user.getRealUser().getGroupNames()); + } + + boolean proxyGroupFound = false; Review Comment: This method is already fairly long, so maybe extracting this part below (`proxyGroupFound`) would make it readable. ########## gateway-spi/src/main/java/org/apache/knox/gateway/util/GroupBasedImpersonationProvider.java: ########## @@ -0,0 +1,252 @@ +/* + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.util; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.UserGroupInformation; +import org.apache.hadoop.security.authorize.AccessControlList; +import org.apache.hadoop.security.authorize.AuthorizationException; +import org.apache.hadoop.security.authorize.DefaultImpersonationProvider; +import org.apache.hadoop.util.MachineList; +import org.apache.knox.gateway.i18n.GatewaySpiMessages; +import org.apache.knox.gateway.i18n.messages.MessagesFactory; + +import java.net.InetAddress; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import static org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_IMPERSONATION_MODE; +import static org.apache.knox.gateway.util.AuthFilterUtils.IMPERSONATION_MODE; + +/** + * An extension of Hadoop's DefaultImpersonationProvider that adds support for group-based impersonation. + * This provider allows users who belong to specific groups to impersonate other users. + */ +public class GroupBasedImpersonationProvider extends DefaultImpersonationProvider { + private static final GatewaySpiMessages LOG = MessagesFactory.get(GatewaySpiMessages.class); + private static final String CONF_HOSTS = ".hosts"; + private static final String CONF_USERS = ".users"; + private static final String CONF_GROUPS = ".groups"; + private final Map<String, AccessControlList> proxyGroupsAcls = new HashMap<>(); + /* is the proxy user configuration enabled */ + boolean isProxyUserEnabled = true; + /* is the proxy group configuration enabled, defaulting it to false for backwards compatibility */ + boolean isProxyGroupEnabled; + private Map<String, MachineList> groupProxyHosts = new HashMap<>(); + private String groupConfigPrefix; + private String impersonationMode = DEFAULT_IMPERSONATION_MODE; + + public GroupBasedImpersonationProvider() { + super(); + } + + public GroupBasedImpersonationProvider(boolean isProxyUserEnabled, boolean isProxyGroupEnabled) { + super(); + this.isProxyUserEnabled = isProxyUserEnabled; + this.isProxyGroupEnabled = isProxyGroupEnabled; + } + + @Override + public Configuration getConf() { + return super.getConf(); + } + + @Override + public void setConf(Configuration conf) { + super.setConf(conf); + } + + public void init(String configurationPrefix, String proxyGroupPrefix) { + super.init(configurationPrefix); + initGroupBasedProvider(proxyGroupPrefix); + } + + private void initGroupBasedProvider(String proxyGroupPrefix) { + impersonationMode = getConf().get(IMPERSONATION_MODE, DEFAULT_IMPERSONATION_MODE); + groupConfigPrefix = proxyGroupPrefix + + (proxyGroupPrefix.endsWith(".") ? "" : "."); + + // constructing regex to match the following patterns: + // $configPrefix.[ANY].users + // $configPrefix.[ANY].groups + // $configPrefix.[ANY].hosts + // + String prefixRegEx = groupConfigPrefix.replace(".", "\\."); + String usersGroupsRegEx = prefixRegEx + "[\\S]*(" + + Pattern.quote(CONF_USERS) + "|" + Pattern.quote(CONF_GROUPS) + ")"; + String hostsRegEx = prefixRegEx + "[\\S]*" + Pattern.quote(CONF_HOSTS); + + // get list of users and groups per proxygroup + // Map of <hadoop.proxygroup.[VIRTUAL_GROUP].users|groups, group1,group2> + Map<String, String> allMatchKeys = + getConf().getValByRegex(usersGroupsRegEx); + + for (Map.Entry<String, String> entry : allMatchKeys.entrySet()) { + //aclKey = hadoop.proxygroup.[VIRTUAL_GROUP] + String aclKey = getAclKey(entry.getKey()); + + if (!proxyGroupsAcls.containsKey(aclKey)) { + proxyGroupsAcls.put(aclKey, new AccessControlList( + allMatchKeys.get(aclKey + CONF_USERS), + allMatchKeys.get(aclKey + CONF_GROUPS))); + } + } + + // get hosts per proxygroup + allMatchKeys = getConf().getValByRegex(hostsRegEx); + for (Map.Entry<String, String> entry : allMatchKeys.entrySet()) { + groupProxyHosts.put(entry.getKey(), + new MachineList(entry.getValue())); + } + } + + private String getAclKey(String key) { + int endIndex = key.lastIndexOf('.'); + if (endIndex != -1) { + return key.substring(0, endIndex); + } + return key; + } + + @Override + public void authorize(UserGroupInformation user, InetAddress remoteAddress) throws AuthorizationException { + // If both authorization methods are disabled, allow the operation to proceed + if (!isProxyUserEnabled && !isProxyGroupEnabled) { + LOG.successfulImpersonation(user.getRealUser().getUserName(), user.getUserName()); + return; + } + + boolean isProxyUserAuthorized = false; + boolean isProxyGroupAuthorized = false; + if (isProxyUserEnabled) { + /* authorize proxy user */ + isProxyUserAuthorized = checkProxyUserAuthorization(user, remoteAddress); + /* In case of AND no reason to check for proxy groups if isProxyUserAuthorized=false */ + if("AND".equalsIgnoreCase(impersonationMode) && !isProxyUserAuthorized) { + throw new AuthorizationException("User: " + user.getRealUser().getUserName() + + " is not allowed to impersonate " + user.getUserName()); + } + } + + if (isProxyGroupEnabled) { + /* check for proxy group impersonation */ + isProxyGroupAuthorized = checkProxyGroupAuthorization(user, remoteAddress); + } + + if("AND".equalsIgnoreCase(impersonationMode)) { + if (isProxyUserAuthorized && isProxyGroupAuthorized) { + LOG.successfulImpersonation(user.getRealUser().getUserName(), user.getUserName()); + } else { + throw new AuthorizationException("User: " + user.getRealUser().getUserName() + + " is not allowed to impersonate " + user.getUserName()); + } + + } else { + /* OR */ + if (isProxyUserAuthorized || isProxyGroupAuthorized) { + LOG.successfulImpersonation(user.getRealUser().getUserName(), user.getUserName()); + } else { + throw new AuthorizationException("User: " + user.getRealUser().getUserName() + + " is not allowed to impersonate " + user.getUserName()); + } + } + } + + /** + * Helper method to check if the user is authorized to impersonate + * Returns true if the user is authorized, false otherwise. + * @param user + * @param remoteAddress + * @return + */ + private boolean checkProxyUserAuthorization(final UserGroupInformation user, final InetAddress remoteAddress) { + try { + super.authorize(user, remoteAddress); + return true; + } catch (final AuthorizationException e) { + LOG.failedToImpersonateUser(user.getRealUser().getUserName(), remoteAddress.getHostAddress()); + return false; + } + } + + /** + * Helper method to check if the group a given user belongs to is authorized to impersonate + * Returns true if the user is authorized, false otherwise. + * @param user + * @param remoteAddress + * @return + */ + private boolean checkProxyGroupAuthorization(final UserGroupInformation user, final InetAddress remoteAddress) { + if (user == null) { + throw new IllegalArgumentException("user is null."); + } + + final UserGroupInformation realUser = user.getRealUser(); + if (realUser == null) { + return true; + } + + // Get the real user's groups (both real and virtual) + Set<String> realUserGroups = new HashSet<>(); + if (user.getRealUser().getGroupNames() != null) { + Collections.addAll(realUserGroups, user.getRealUser().getGroupNames()); + } + + boolean proxyGroupFound = false; + // Check if any of the real user's groups have permission to impersonate the proxy user + for (String group : realUserGroups) { + final AccessControlList acl = proxyGroupsAcls.get(groupConfigPrefix + + group); + + if (acl == null || !acl.isUserAllowed(user)) { + continue; + } else { + proxyGroupFound = true; + break; + } + } + + if (!proxyGroupFound) { + LOG.failedToImpersonateGroups(realUser.getUserName(), realUserGroups.toString(), user.getUserName()); + return false; + } + + boolean proxyGroupHostFound = false; Review Comment: That could be another candidate to extract into its own method. Issue Time Tracking ------------------- Worklog Id: (was: 969992) Time Spent: 1h 10m (was: 1h) > Surrogate proxy user configuration for user groups > -------------------------------------------------- > > Key: KNOX-3048 > URL: https://issues.apache.org/jira/browse/KNOX-3048 > Project: Apache Knox > Issue Type: Improvement > Components: Server > Affects Versions: 2.0.0 > Reporter: Philip Zampino > Assignee: Sandeep More > Priority: Major > Fix For: 2.1.0 > > Time Spent: 1h 10m > Remaining Estimate: 0h > > *Problem Statement:* > Currently Knox has the ability for specific users (say for e.g. {{sp_user}}) > to impersonate other users (say for e.g.{{ot_user}}). This is driven by > configs defined in a topology. Currently these configs are needed for each > user that impersonates other users (i.e. {{sp_user}}), this can get out of > hand quickly and can be difficult to maintain. > To solve this problem the proposed solution uses a group level impersonation > configuration. This configuration will be based on the virtual groups feature > that is already available in Knox. With this new configuration we can have > specific users who belong to a virtual group/s (based on conditions such as > {{(match groups 'analyst|scientist') }}) impersonate other users. This will > significantly cut down on the config properties and keep them readable and > maintainable. -- This message was sent by Atlassian Jira (v8.20.10#820010)