[ https://issues.apache.org/jira/browse/KNOX-3048?focusedWorklogId=970508&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-970508 ]
ASF GitHub Bot logged work on KNOX-3048: ---------------------------------------- Author: ASF GitHub Bot Created on: 23/May/25 14:47 Start Date: 23/May/25 14:47 Worklog Time Spent: 10m Work Description: zeroflag commented on code in PR #1043: URL: https://github.com/apache/knox/pull/1043#discussion_r2104470497 ########## gateway-provider-identity-assertion-proxygroups/src/main/java/org/apache/knox/gateway/identityasserter/proxygroups/filter/ProxygroupIdentityAssertionFilter.java: ########## @@ -0,0 +1,167 @@ +/* + * 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.identityasserter.proxygroups.filter; + +import org.apache.knox.gateway.i18n.messages.MessagesFactory; +import org.apache.knox.gateway.identityasserter.hadoop.groups.filter.HadoopGroupProviderFilter; +import org.apache.knox.gateway.security.SubjectUtils; +import org.apache.knox.gateway.services.GatewayServices; +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.util.Arrays; +import java.util.EnumSet; +import java.util.List; + +import static org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_IMPERSONATION_MODE; +import static org.apache.knox.gateway.util.AuthFilterUtils.IMPERSONATION_MODE; +import static org.apache.knox.gateway.util.AuthFilterUtils.ImpersonationFlags.GROUP_IMPERSONATION; + +public class ProxygroupIdentityAssertionFilter extends HadoopGroupProviderFilter { + private static final ProxygroupProviderMessages LOG = MessagesFactory.get(ProxygroupProviderMessages.class); + private String impersonationMode = DEFAULT_IMPERSONATION_MODE; + private EnumSet<AuthFilterUtils.ImpersonationFlags> impersonationFlags; + private String topologyName; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + super.init(filterConfig); + topologyName = (String) filterConfig.getServletContext().getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE); + impersonationFlags = AuthFilterUtils.getImpersonationEnabledFlags(filterConfig); + impersonationMode = filterConfig.getInitParameter(IMPERSONATION_MODE) != null ? filterConfig.getInitParameter(IMPERSONATION_MODE) : DEFAULT_IMPERSONATION_MODE; + + /* Add group impersonation provider */ + if (!impersonationFlags.isEmpty() && impersonationFlags.contains(GROUP_IMPERSONATION)) { Review Comment: Isn't this `isEmpty()` check redundant? ########## gateway-provider-identity-assertion-proxygroups/src/main/java/org/apache/knox/gateway/identityasserter/proxygroups/filter/ProxygroupIdentityAssertionFilter.java: ########## @@ -0,0 +1,167 @@ +/* + * 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.identityasserter.proxygroups.filter; + +import org.apache.knox.gateway.i18n.messages.MessagesFactory; +import org.apache.knox.gateway.identityasserter.hadoop.groups.filter.HadoopGroupProviderFilter; +import org.apache.knox.gateway.security.SubjectUtils; +import org.apache.knox.gateway.services.GatewayServices; +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.util.Arrays; +import java.util.EnumSet; +import java.util.List; + +import static org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_IMPERSONATION_MODE; +import static org.apache.knox.gateway.util.AuthFilterUtils.IMPERSONATION_MODE; +import static org.apache.knox.gateway.util.AuthFilterUtils.ImpersonationFlags.GROUP_IMPERSONATION; + +public class ProxygroupIdentityAssertionFilter extends HadoopGroupProviderFilter { + private static final ProxygroupProviderMessages LOG = MessagesFactory.get(ProxygroupProviderMessages.class); + private String impersonationMode = DEFAULT_IMPERSONATION_MODE; + private EnumSet<AuthFilterUtils.ImpersonationFlags> impersonationFlags; + private String topologyName; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + super.init(filterConfig); + topologyName = (String) filterConfig.getServletContext().getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE); + impersonationFlags = AuthFilterUtils.getImpersonationEnabledFlags(filterConfig); + impersonationMode = filterConfig.getInitParameter(IMPERSONATION_MODE) != null ? filterConfig.getInitParameter(IMPERSONATION_MODE) : DEFAULT_IMPERSONATION_MODE; + + /* Add group impersonation provider */ + if (!impersonationFlags.isEmpty() && impersonationFlags.contains(GROUP_IMPERSONATION)) { + final List<String> initParameterNames = AuthFilterUtils.getInitParameterNamesAsList(filterConfig); + String topologyName = (String) filterConfig.getServletContext().getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE); + AuthFilterUtils.refreshProxyGroupsConfiguration(filterConfig, initParameterNames, topologyName, ProxygroupsIdentityAsserterDeploymentContributor.NAME); + } + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + final Subject subject = getSubject(); + /* + * in case of user auth failure, mapped principal is same as real principal, this is so that + * group lookup works needed for group proxy auth check + */ + String mappedPrincipalName = SubjectUtils.getEffectivePrincipalName(subject); + + boolean isProxyUserAuthorized = false; + boolean isProxyGroupAuthorized = false; + + /* Save exceptions if needed */ + AuthorizationException userAuthorizationException = null; + AuthorizationException groupsAuthorizationException = null; + + if (impersonationFlags.contains(AuthFilterUtils.ImpersonationFlags.USER_IMPERSONATION)) { + try { + mappedPrincipalName = handleProxyUserImpersonation(request, subject); + isProxyUserAuthorized = true; + } catch (AuthorizationException e) { + LOG.hadoopAuthProxyUserFailed(e); + userAuthorizationException = e; + } + } + + mappedPrincipalName = getMappedPrincipalName(request, mappedPrincipalName, subject); + String[] groups = getMappedGroups(request, mappedPrincipalName, subject); + + if (impersonationFlags.contains(AuthFilterUtils.ImpersonationFlags.GROUP_IMPERSONATION)) { + try { + mappedPrincipalName = handleProxyGroupImpersonation(request, subject, groups); + isProxyGroupAuthorized = true; + } catch (AuthorizationException e) { + LOG.hadoopAuthProxyGroupFailed(e); + groupsAuthorizationException = e; + } + } + + // Determine if authorization succeeded based on the impersonation mode + /* + * Impersonation mode can only come into play when both proxy user and proxy group are enabled. + * For cases where only one of them is enabled, we set impersonation mode to OR and rely on either + * of the modes to succeed. + * + * This is the case where one of the impersonations is disabled but hadoop.impersonation.mode value is + * configured. + */ + if (impersonationFlags.isEmpty() || (!impersonationFlags.contains(AuthFilterUtils.ImpersonationFlags.USER_IMPERSONATION) || !impersonationFlags.contains(AuthFilterUtils.ImpersonationFlags.GROUP_IMPERSONATION))) { + impersonationMode = DEFAULT_IMPERSONATION_MODE; + } + + boolean isAuthorized = "AND".equalsIgnoreCase(impersonationMode) + ? (isProxyUserAuthorized && isProxyGroupAuthorized) + : (isProxyUserAuthorized || isProxyGroupAuthorized); + + if (isAuthorized) { + LOG.hadoopAuthProxyAccessSuccess(SubjectUtils.getEffectivePrincipalName(subject), request.getParameter(AuthFilterUtils.QUERY_PARAMETER_DOAS), impersonationMode); + } else { + // Handle authorization failures + if (groupsAuthorizationException != null) { + LOG.hadoopAuthProxyGroupFailed(groupsAuthorizationException); + HttpExceptionUtils.createServletExceptionResponse((HttpServletResponse) response, HttpServletResponse.SC_FORBIDDEN, groupsAuthorizationException); + return; + } else if (userAuthorizationException != null) { + LOG.hadoopAuthProxyGroupFailed(groupsAuthorizationException); + HttpExceptionUtils.createServletExceptionResponse((HttpServletResponse) response, HttpServletResponse.SC_FORBIDDEN, userAuthorizationException); + return; + } else { + AuthorizationException e = new AuthorizationException("User: " + SubjectUtils.getEffectivePrincipalName(subject) + " is not allowed to impersonate." + "Proxyuser auth result: " + isProxyUserAuthorized + " Proxygroup auth result: " + isProxyGroupAuthorized); + LOG.hadoopAuthProxyGroupFailed(e); + HttpExceptionUtils.createServletExceptionResponse((HttpServletResponse) response, HttpServletResponse.SC_FORBIDDEN, e); + return; + } + } + + HttpServletRequestWrapper wrapper = wrapHttpServletRequest(request, mappedPrincipalName); + continueChainAsPrincipal(wrapper, response, chain, mappedPrincipalName, unique(groups)); + } + + private String handleProxyGroupImpersonation(final ServletRequest request, final Subject subject, String[] groups) throws AuthorizationException { + String principalName = SubjectUtils.getEffectivePrincipalName(subject); + if (!impersonationFlags.isEmpty() && impersonationFlags.contains(GROUP_IMPERSONATION)) { Review Comment: Isn't this `isEmpty()` check redundant? ########## gateway-spi/src/main/java/org/apache/knox/gateway/util/GroupBasedImpersonationProvider.java: ########## @@ -0,0 +1,228 @@ +/* + * 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.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * 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<>(); + EnumSet<AuthFilterUtils.ImpersonationFlags> impersonationFlags = EnumSet.noneOf(AuthFilterUtils.ImpersonationFlags.class); + private Map<String, MachineList> groupProxyHosts = new HashMap<>(); + private String groupConfigPrefix; + + public GroupBasedImpersonationProvider(EnumSet<AuthFilterUtils.ImpersonationFlags> impersonationFlags) { + super(); + this.impersonationFlags = impersonationFlags; + } + + @Override + public Configuration getConf() { + return super.getConf(); + } + + @Override + public void setConf(Configuration conf) { + super.setConf(conf); + } + + @Override + public void init(String proxyGroupPrefix) { + initGroupBasedProvider(proxyGroupPrefix); + } + + private void initGroupBasedProvider(String proxyGroupPrefix) { + 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; + } + + /** + * Authorization based on groups that are already in Subject + * + * @param user the user information attempting the operation, which includes the real + * user and the effective impersonated user. + * @param remoteAddress the remote address from which the user is connecting. + * @throws AuthorizationException if the user is not authorized based on the + * configured impersonation and group policies. + */ + @Override + public void authorize(UserGroupInformation user, InetAddress remoteAddress) throws AuthorizationException { + // If both authorization methods are disabled, allow the operation to proceed + if (impersonationFlags.isEmpty() || !impersonationFlags.contains(AuthFilterUtils.ImpersonationFlags.GROUP_IMPERSONATION)) { + LOG.successfulImpersonation(user.getRealUser().getUserName(), user.getUserName()); + return; + } + checkProxyGroupAuthorization(user, remoteAddress, Collections.emptyList()); + } + + /** + * Authorization based on groups that are provided as a function agument + * + * @param user the user information attempting the operation, which includes the real + * user and the effective impersonated user. + * @param groups the list of groups to check for authorization. + * @param remoteAddress the remote address from which the user is connecting. + * @throws AuthorizationException if the user is not authorized based on the + * configured impersonation and group policies. + */ + public void authorize(UserGroupInformation user, InetAddress remoteAddress, List<String> groups) throws AuthorizationException { + // If both authorization methods are disabled, allow the operation to proceed + if (impersonationFlags.isEmpty() || !impersonationFlags.contains(AuthFilterUtils.ImpersonationFlags.GROUP_IMPERSONATION)) { Review Comment: Isn't this `isEmpty()` check redundant? ########## gateway-spi/src/main/java/org/apache/knox/gateway/util/GroupBasedImpersonationProvider.java: ########## @@ -0,0 +1,228 @@ +/* + * 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.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * 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<>(); + EnumSet<AuthFilterUtils.ImpersonationFlags> impersonationFlags = EnumSet.noneOf(AuthFilterUtils.ImpersonationFlags.class); + private Map<String, MachineList> groupProxyHosts = new HashMap<>(); + private String groupConfigPrefix; + + public GroupBasedImpersonationProvider(EnumSet<AuthFilterUtils.ImpersonationFlags> impersonationFlags) { + super(); + this.impersonationFlags = impersonationFlags; + } + + @Override + public Configuration getConf() { + return super.getConf(); + } + + @Override + public void setConf(Configuration conf) { + super.setConf(conf); + } + + @Override + public void init(String proxyGroupPrefix) { + initGroupBasedProvider(proxyGroupPrefix); + } + + private void initGroupBasedProvider(String proxyGroupPrefix) { + 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; + } + + /** + * Authorization based on groups that are already in Subject + * + * @param user the user information attempting the operation, which includes the real + * user and the effective impersonated user. + * @param remoteAddress the remote address from which the user is connecting. + * @throws AuthorizationException if the user is not authorized based on the + * configured impersonation and group policies. + */ + @Override + public void authorize(UserGroupInformation user, InetAddress remoteAddress) throws AuthorizationException { + // If both authorization methods are disabled, allow the operation to proceed + if (impersonationFlags.isEmpty() || !impersonationFlags.contains(AuthFilterUtils.ImpersonationFlags.GROUP_IMPERSONATION)) { Review Comment: Isn't this `isEmpty()` check redundant? ########## gateway-spi/src/main/java/org/apache/knox/gateway/util/GroupBasedImpersonationProvider.java: ########## @@ -0,0 +1,228 @@ +/* + * 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.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * 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<>(); + EnumSet<AuthFilterUtils.ImpersonationFlags> impersonationFlags = EnumSet.noneOf(AuthFilterUtils.ImpersonationFlags.class); + private Map<String, MachineList> groupProxyHosts = new HashMap<>(); + private String groupConfigPrefix; + + public GroupBasedImpersonationProvider(EnumSet<AuthFilterUtils.ImpersonationFlags> impersonationFlags) { + super(); + this.impersonationFlags = impersonationFlags; + } + + @Override + public Configuration getConf() { + return super.getConf(); + } + + @Override + public void setConf(Configuration conf) { + super.setConf(conf); + } + + @Override + public void init(String proxyGroupPrefix) { + initGroupBasedProvider(proxyGroupPrefix); + } + + private void initGroupBasedProvider(String proxyGroupPrefix) { + 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; + } + + /** + * Authorization based on groups that are already in Subject + * + * @param user the user information attempting the operation, which includes the real + * user and the effective impersonated user. + * @param remoteAddress the remote address from which the user is connecting. + * @throws AuthorizationException if the user is not authorized based on the + * configured impersonation and group policies. + */ + @Override + public void authorize(UserGroupInformation user, InetAddress remoteAddress) throws AuthorizationException { + // If both authorization methods are disabled, allow the operation to proceed + if (impersonationFlags.isEmpty() || !impersonationFlags.contains(AuthFilterUtils.ImpersonationFlags.GROUP_IMPERSONATION)) { + LOG.successfulImpersonation(user.getRealUser().getUserName(), user.getUserName()); + return; + } + checkProxyGroupAuthorization(user, remoteAddress, Collections.emptyList()); + } + + /** + * Authorization based on groups that are provided as a function agument + * + * @param user the user information attempting the operation, which includes the real + * user and the effective impersonated user. + * @param groups the list of groups to check for authorization. + * @param remoteAddress the remote address from which the user is connecting. + * @throws AuthorizationException if the user is not authorized based on the + * configured impersonation and group policies. + */ + public void authorize(UserGroupInformation user, InetAddress remoteAddress, List<String> groups) throws AuthorizationException { + // If both authorization methods are disabled, allow the operation to proceed + if (impersonationFlags.isEmpty() || !impersonationFlags.contains(AuthFilterUtils.ImpersonationFlags.GROUP_IMPERSONATION)) { + LOG.successfulImpersonation(user.getRealUser().getUserName(), user.getUserName()); + return; + } + checkProxyGroupAuthorization(user, remoteAddress, groups); + } + + /** + * 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 void checkProxyGroupAuthorization(final UserGroupInformation user, final InetAddress remoteAddress, List<String> groups) throws AuthorizationException { + if (user == null) { + throw new IllegalArgumentException("user is null."); + } + + final UserGroupInformation realUser = user.getRealUser(); + if (realUser == null) { + return; + } + + // Get the real user's groups (both real and virtual) + Set<String> realUserGroups = new HashSet<>(); + /* Add provided groups */ + if(groups != null && !groups.isEmpty()) { + realUserGroups.addAll(groups); + } + /* Add groups from subject */ + 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()); + throw new AuthorizationException("User: " + realUser.getUserName() + + " with groups " + realUserGroups.toString() + + " is not allowed to impersonate " + user.getUserName()); + } + + boolean proxyGroupHostFound = false; + for (final String group : realUserGroups) { + final MachineList machineList = groupProxyHosts.get(groupConfigPrefix + group + CONF_HOSTS); + + if (machineList == null || !machineList.includes(remoteAddress)) { + continue; + } else { + proxyGroupHostFound = true; + break; + } + } + + if (!proxyGroupHostFound) { + LOG.failedToImpersonateGroupsFromAddress(realUser.getUserName(), realUserGroups.toString(), user.getUserName(), remoteAddress.toString()); + throw new AuthorizationException("User: " + realUser.getUserName() + + "with groups " + realUserGroups.toString() Review Comment: Missing space before `"with groups` ########## gateway-spi/src/main/java/org/apache/knox/gateway/util/AuthFilterUtils.java: ########## @@ -42,212 +45,307 @@ 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(); - } - - @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; - } - - 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)); - } + 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"; + + 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 IMPERSONATION_ENABLED_PARAM = AuthFilterUtils.PROXYUSER_PREFIX + ".impersonation.enabled"; + public static final String GROUP_IMPERSONATION_ENABLED_PARAM = AuthFilterUtils.PROXYGROUP_PREFIX + ".impersonation.enabled"; + + 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(); + + /** + * Represents the modes of impersonation that can be configured and used + * within the authentication process. + * + * USER_IMPERSONATION: + * Indicates that the impersonation process is based on a user identity. + * This is typically used when one user needs to act on behalf of another. + * + * GROUP_IMPERSONATION: + * Represents group-based impersonation where actions can be performed + * based on group roles or permissions. + */ + public enum ImpersonationFlags { + USER_IMPERSONATION, GROUP_IMPERSONATION + } + + + /** + * 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, new DefaultImpersonationProvider(), PROXYUSER_PREFIX); + } + + /* For proxy groups */ + public static void refreshProxyGroupsConfiguration(FilterConfig filterConfig, List<String> initParameterNames, String topologyName, String role) { + if (filterConfig == null) { + throw new IllegalArgumentException("Cannot get proxyuser configuration from NULL filter config"); + } + refreshProxyGroupsConfiguration(null, filterConfig, initParameterNames, topologyName, role); + } + + private static void refreshProxyGroupsConfiguration(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(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); + }); + } + /* For proxy group use GroupBasedImpersonationProvider */ + saveImpersonationProvider(topologyName, role, conf, new GroupBasedImpersonationProvider(getImpersonationEnabledFlags(filterConfig)), PROXYGROUP_PREFIX); + } + + + private static void saveImpersonationProvider(String topologyName, String role, final Configuration conf, final ImpersonationProvider impersonationProvider, final String prefix) { + refreshSuperUserGroupsLock.lock(); + try { + impersonationProvider.setConf(conf); + impersonationProvider.init(prefix); + LOG.createImpersonationProvider(topologyName, role, prefix, conf.getPropsWithPrefix(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(); + } + + @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; + } + + public static void authorizeGroupImpersonationRequest(HttpServletRequest request, String remoteUser, String doAsUser, String topologyName, String role, List<String> groups) throws AuthorizationException { + final UserGroupInformation remoteRequestUgi = getRemoteRequestUgi(remoteUser, doAsUser); + + if (remoteRequestUgi != null) { + authorizeImpersonationRequest(request, remoteRequestUgi, topologyName, role, groups); + } + } + + 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 { + authorizeImpersonationRequest(request, remoteRequestUgi, topologyName, role, Collections.emptyList()); + } + + private static void authorizeImpersonationRequest(HttpServletRequest request, UserGroupInformation remoteRequestUgi, String topologyName, String role, List<String> groups) + throws AuthorizationException { + + final ImpersonationProvider impersonationProvider = getImpersonationProvider(topologyName, role); + + if (impersonationProvider != null) { + try { + if (impersonationProvider instanceof GroupBasedImpersonationProvider) { + ((GroupBasedImpersonationProvider) impersonationProvider).authorize(remoteRequestUgi, InetAddress.getByName(request.getRemoteAddr()), groups); + } else { + impersonationProvider.authorize(remoteRequestUgi, request.getRemoteAddr()); + } + + } catch (org.apache.hadoop.security.authorize.AuthorizationException | UnknownHostException 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)); + } + + /** + * 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 Review Comment: Stale javadoc comment. It no longer returns a boolean array. Issue Time Tracking ------------------- Worklog Id: (was: 970508) Time Spent: 2.5h (was: 2h 20m) > 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: 2.5h > 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)