|
@@ -0,0 +1,443 @@
|
|
|
+/*
|
|
|
+ * 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.hadoop.hdfs.server.common;
|
|
|
+
|
|
|
+import org.apache.commons.io.FilenameUtils;
|
|
|
+import org.apache.commons.net.util.SubnetUtils;
|
|
|
+import org.apache.hadoop.conf.Configuration;
|
|
|
+import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier;
|
|
|
+import org.apache.hadoop.hdfs.web.WebHdfsFileSystem;
|
|
|
+import org.apache.hadoop.security.token.Token;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+
|
|
|
+import javax.servlet.Filter;
|
|
|
+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.HttpServletResponse;
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
+import java.io.DataInputStream;
|
|
|
+import java.io.IOException;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Optional;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
+import java.util.concurrent.CopyOnWriteArrayList;
|
|
|
+import java.util.function.BiFunction;
|
|
|
+import java.util.function.Predicate;
|
|
|
+import java.util.regex.Pattern;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+import java.util.stream.Stream;
|
|
|
+
|
|
|
+/**
|
|
|
+ * An HTTP filter that can filter requests based on Hosts.
|
|
|
+ */
|
|
|
+public class HostRestrictingAuthorizationFilter implements Filter {
|
|
|
+ public static final String HDFS_CONFIG_PREFIX = "dfs.web.authentication.";
|
|
|
+ public static final String RESTRICTION_CONFIG = "host.allow.rules";
|
|
|
+ // A Java Predicate for query string parameters on which to filter requests
|
|
|
+ public static final Predicate<String> RESTRICTED_OPERATIONS =
|
|
|
+ qStr -> (qStr.trim().equalsIgnoreCase("op=OPEN") ||
|
|
|
+ qStr.trim().equalsIgnoreCase("op=GETDELEGATIONTOKEN"));
|
|
|
+ private final Map<String, CopyOnWriteArrayList<Rule>> rulemap =
|
|
|
+ new ConcurrentHashMap<>();
|
|
|
+ private static final Logger LOG =
|
|
|
+ LoggerFactory.getLogger(HostRestrictingAuthorizationFilter.class);
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Constructs a mapping of configuration properties to be used for filter
|
|
|
+ * initialization. The mapping includes all properties that start with the
|
|
|
+ * specified configuration prefix. Property names in the mapping are trimmed
|
|
|
+ * to remove the configuration prefix.
|
|
|
+ *
|
|
|
+ * @param conf configuration to read
|
|
|
+ * @param confPrefix configuration prefix
|
|
|
+ * @return mapping of configuration properties to be used for filter
|
|
|
+ * initialization
|
|
|
+ */
|
|
|
+ public static Map<String, String> getFilterParams(Configuration conf,
|
|
|
+ String confPrefix) {
|
|
|
+ return conf.getPropsWithPrefix(confPrefix);
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Check all rules for this user to see if one matches for this host/path pair
|
|
|
+ *
|
|
|
+ * @param: user - user to check rules for
|
|
|
+ * @param: host - IP address (e.g. "192.168.0.1")
|
|
|
+ * @param: path - file path with no scheme (e.g. /path/foo)
|
|
|
+ * @returns: true if a rule matches this user, host, path tuple false if an
|
|
|
+ * error occurs or no match
|
|
|
+ */
|
|
|
+ private boolean matchRule(String user, String remoteIp, String path) {
|
|
|
+ // allow lookups for blank in the rules for user and path
|
|
|
+ user = (user != null ? user : "");
|
|
|
+ path = (path != null ? path : "");
|
|
|
+
|
|
|
+ LOG.trace("Got user: {}, remoteIp: {}, path: {}", user, remoteIp, path);
|
|
|
+
|
|
|
+ // isInRange fails for null/blank IPs, require an IP to approve
|
|
|
+ if (remoteIp == null) {
|
|
|
+ LOG.trace("Returned false due to null rempteIp");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ List<Rule> userRules = ((userRules = rulemap.get(user)) != null) ?
|
|
|
+ userRules : new ArrayList<Rule>();
|
|
|
+ List<Rule> anyRules = ((anyRules = rulemap.get("*")) != null) ?
|
|
|
+ anyRules : new ArrayList<Rule>();
|
|
|
+
|
|
|
+ List<Rule> rules = Stream.of(userRules, anyRules)
|
|
|
+ .flatMap(l -> l.stream()).collect(Collectors.toList());
|
|
|
+
|
|
|
+ for (Rule rule : rules) {
|
|
|
+ SubnetUtils.SubnetInfo subnet = rule.getSubnet();
|
|
|
+ String rulePath = rule.getPath();
|
|
|
+ LOG.trace("Evaluating rule, subnet: {}, path: {}",
|
|
|
+ subnet != null ? subnet.getCidrSignature() : "*", rulePath);
|
|
|
+ try {
|
|
|
+ if ((subnet == null || subnet.isInRange(remoteIp))
|
|
|
+ && FilenameUtils.directoryContains(rulePath, path)) {
|
|
|
+ LOG.debug("Found matching rule, subnet: {}, path: {}; returned true",
|
|
|
+ rule.getSubnet() != null ? subnet.getCidrSignature() : null,
|
|
|
+ rulePath);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ } catch (IOException e) {
|
|
|
+ LOG.warn("Got IOException {}; returned false", e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ LOG.trace("Found no rules for user");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void destroy() {
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void init(FilterConfig config) throws ServletException {
|
|
|
+ // Process dropbox rules
|
|
|
+ String dropboxRules = config.getInitParameter(RESTRICTION_CONFIG);
|
|
|
+ loadRuleMap(dropboxRules);
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Initializes the rule map state for the filter
|
|
|
+ *
|
|
|
+ * @param ruleString - a string of newline delineated, comma separated
|
|
|
+ * three field records
|
|
|
+ * @throws IllegalArgumentException - when a rule can not be properly parsed
|
|
|
+ * Postconditions:
|
|
|
+ * <ul>
|
|
|
+ * <li>The {@rulemap} hash will be populated with all parsed rules.</li>
|
|
|
+ * </ul>
|
|
|
+ */
|
|
|
+ private void loadRuleMap(String ruleString) throws IllegalArgumentException {
|
|
|
+ if (ruleString == null || ruleString.equals("")) {
|
|
|
+ LOG.debug("Got no rules - will disallow anyone access");
|
|
|
+ } else {
|
|
|
+ // value: user1,network/bits1,path_glob1|user2,network/bits2,path_glob2...
|
|
|
+ Pattern comma_split = Pattern.compile(",");
|
|
|
+ Pattern rule_split = Pattern.compile("\\||\n");
|
|
|
+ // split all rule lines
|
|
|
+ Map<Integer, List<String[]>> splits = rule_split.splitAsStream(ruleString)
|
|
|
+ .map(x -> comma_split.split(x, 3))
|
|
|
+ .collect(Collectors.groupingBy(x -> x.length));
|
|
|
+ // verify all rules have three parts
|
|
|
+ if (!splits.keySet().equals(Collections.singleton(3))) {
|
|
|
+ // instead of re-joining parts, re-materialize lines which do not split
|
|
|
+ // correctly for the exception
|
|
|
+ String bad_lines = rule_split.splitAsStream(ruleString)
|
|
|
+ .filter(x -> comma_split.split(x, 3).length != 3)
|
|
|
+ .collect(Collectors.joining("\n"));
|
|
|
+ throw new IllegalArgumentException("Bad rule definition: " + bad_lines);
|
|
|
+ }
|
|
|
+ // create a list of Rules
|
|
|
+ int user = 0;
|
|
|
+ int cidr = 1;
|
|
|
+ int path = 2;
|
|
|
+ BiFunction<CopyOnWriteArrayList<Rule>, CopyOnWriteArrayList<Rule>,
|
|
|
+ CopyOnWriteArrayList<Rule>> arrayListMerge = (v1, v2) -> {
|
|
|
+ v1.addAll(v2);
|
|
|
+ return v1;
|
|
|
+ };
|
|
|
+ for (String[] split : splits.get(3)) {
|
|
|
+ LOG.debug("Loaded rule: user: {}, network/bits: {} path: {}",
|
|
|
+ split[user], split[cidr], split[path]);
|
|
|
+ Rule rule = (split[cidr].trim().equals("*") ? new Rule(null,
|
|
|
+ split[path]) : new Rule(new SubnetUtils(split[cidr]).getInfo(),
|
|
|
+ split[path]));
|
|
|
+ // Rule map is {"user": [rule1, rule2, ...]}, update the user's array
|
|
|
+ CopyOnWriteArrayList<Rule> arrayListRule =
|
|
|
+ new CopyOnWriteArrayList<Rule>() {
|
|
|
+ {
|
|
|
+ add(rule);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ rulemap.merge(split[user], arrayListRule, arrayListMerge);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * doFilter() is a shim to create an HttpInteraction object and pass that to
|
|
|
+ * the actual processing logic
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public void doFilter(ServletRequest request, ServletResponse response,
|
|
|
+ FilterChain filterChain)
|
|
|
+ throws IOException, ServletException {
|
|
|
+ final HttpServletRequest httpRequest = (HttpServletRequest) request;
|
|
|
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
|
|
|
+
|
|
|
+ handleInteraction(new ServletFilterHttpInteraction(httpRequest,
|
|
|
+ httpResponse, filterChain));
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * The actual processing logic of the Filter
|
|
|
+ * Uses our {@HttpInteraction} shim which can be called from a variety of
|
|
|
+ * incoming request sources
|
|
|
+ * @param interaction - An HttpInteraction object from any of our callers
|
|
|
+ */
|
|
|
+ public void handleInteraction(HttpInteraction interaction)
|
|
|
+ throws IOException, ServletException {
|
|
|
+ final String address = interaction.getRemoteAddr();
|
|
|
+ final String query = interaction.getQueryString();
|
|
|
+ final String path =
|
|
|
+ interaction.getRequestURI()
|
|
|
+ .substring(WebHdfsFileSystem.PATH_PREFIX.length());
|
|
|
+ String user = interaction.getRemoteUser();
|
|
|
+
|
|
|
+ LOG.trace("Got request user: {}, remoteIp: {}, query: {}, path: {}",
|
|
|
+ user, address, query, path);
|
|
|
+ boolean authenticatedQuery =
|
|
|
+ Arrays.stream(Optional.ofNullable(query).orElse("")
|
|
|
+ .trim()
|
|
|
+ .split("&"))
|
|
|
+ .anyMatch(RESTRICTED_OPERATIONS);
|
|
|
+ if (!interaction.isCommitted() && authenticatedQuery) {
|
|
|
+ // loop over all query parts
|
|
|
+ String[] queryParts = query.split("&");
|
|
|
+
|
|
|
+ if (user == null) {
|
|
|
+ LOG.trace("Looking for delegation token to identify user");
|
|
|
+ for (String part : queryParts) {
|
|
|
+ if (part.trim().startsWith("delegation=")) {
|
|
|
+ Token t = new Token();
|
|
|
+ t.decodeFromUrlString(part.split("=", 2)[1]);
|
|
|
+ ByteArrayInputStream buf =
|
|
|
+ new ByteArrayInputStream(t.getIdentifier());
|
|
|
+ DelegationTokenIdentifier identifier =
|
|
|
+ new DelegationTokenIdentifier();
|
|
|
+ identifier.readFields(new DataInputStream(buf));
|
|
|
+ user = identifier.getUser().getUserName();
|
|
|
+ LOG.trace("Updated request user: {}, remoteIp: {}, query: {}, " +
|
|
|
+ "path: {}", user, address, query, path);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (authenticatedQuery && !(matchRule("*", address,
|
|
|
+ path) || matchRule(user, address, path))) {
|
|
|
+ LOG.trace("Rejecting interaction; no rule found");
|
|
|
+ interaction.sendError(HttpServletResponse.SC_FORBIDDEN,
|
|
|
+ "WebHDFS is configured write-only for " + user + "@" + address +
|
|
|
+ " for file: " + path);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ LOG.trace("Proceeding with interaction");
|
|
|
+ interaction.proceed();
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Defines the minimal API requirements for the filter to execute its
|
|
|
+ * filtering logic. This interface exists to facilitate integration in
|
|
|
+ * components that do not run within a servlet container and therefore cannot
|
|
|
+ * rely on a servlet container to dispatch to the {@link #doFilter} method.
|
|
|
+ * Applications that do run inside a servlet container will not need to write
|
|
|
+ * code that uses this interface. Instead, they can use typical servlet
|
|
|
+ * container configuration mechanisms to insert the filter.
|
|
|
+ */
|
|
|
+ public interface HttpInteraction {
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Returns if the request has been committed.
|
|
|
+ *
|
|
|
+ * @return boolean
|
|
|
+ */
|
|
|
+ boolean isCommitted();
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Returns the value of the requesting client address.
|
|
|
+ *
|
|
|
+ * @return the remote address
|
|
|
+ */
|
|
|
+ String getRemoteAddr();
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Returns the user ID making the request.
|
|
|
+ *
|
|
|
+ * @return the user
|
|
|
+ */
|
|
|
+ String getRemoteUser();
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Returns the value of the request URI.
|
|
|
+ *
|
|
|
+ * @return the request URI
|
|
|
+ */
|
|
|
+ String getRequestURI();
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Returns the value of the query string.
|
|
|
+ *
|
|
|
+ * @return an optional contianing the URL query string
|
|
|
+ */
|
|
|
+ String getQueryString();
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Returns the method.
|
|
|
+ *
|
|
|
+ * @return method
|
|
|
+ */
|
|
|
+ String getMethod();
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Called by the filter after it decides that the request may proceed.
|
|
|
+ *
|
|
|
+ * @throws IOException if there is an I/O error
|
|
|
+ * @throws ServletException if the implementation relies on the servlet API
|
|
|
+ * and a servlet API call has failed
|
|
|
+ */
|
|
|
+ void proceed() throws IOException, ServletException;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Called by the filter after it decides that the request is an
|
|
|
+ * unauthorized request and therefore must be rejected.
|
|
|
+ *
|
|
|
+ * @param code status code to send
|
|
|
+ * @param message response message
|
|
|
+ * @throws IOException if there is an I/O error
|
|
|
+ */
|
|
|
+ void sendError(int code, String message) throws IOException;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class Rule {
|
|
|
+ private final SubnetUtils.SubnetInfo subnet;
|
|
|
+ private final String path;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * A class for holding dropbox filter rules
|
|
|
+ *
|
|
|
+ * @param subnet - the IPv4 subnet for which this rule is valid (pass
|
|
|
+ * null for any network location)
|
|
|
+ * @param path - the HDFS path for which this rule is valid
|
|
|
+ */
|
|
|
+ Rule(SubnetUtils.SubnetInfo subnet, String path) {
|
|
|
+ this.subnet = subnet;
|
|
|
+ this.path = path;
|
|
|
+ }
|
|
|
+
|
|
|
+ public SubnetUtils.SubnetInfo getSubnet() {
|
|
|
+ return (subnet);
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getPath() {
|
|
|
+ return (path);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ * {@link HttpInteraction} implementation for use in the servlet filter.
|
|
|
+ */
|
|
|
+ private static final class ServletFilterHttpInteraction
|
|
|
+ implements HttpInteraction {
|
|
|
+
|
|
|
+ private final FilterChain chain;
|
|
|
+ private final HttpServletRequest httpRequest;
|
|
|
+ private final HttpServletResponse httpResponse;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Creates a new ServletFilterHttpInteraction.
|
|
|
+ *
|
|
|
+ * @param httpRequest request to process
|
|
|
+ * @param httpResponse response to process
|
|
|
+ * @param chain filter chain to forward to if HTTP interaction is allowed
|
|
|
+ */
|
|
|
+ public ServletFilterHttpInteraction(HttpServletRequest httpRequest,
|
|
|
+ HttpServletResponse httpResponse, FilterChain chain) {
|
|
|
+ this.httpRequest = httpRequest;
|
|
|
+ this.httpResponse = httpResponse;
|
|
|
+ this.chain = chain;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean isCommitted() {
|
|
|
+ return (httpResponse.isCommitted());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getRemoteAddr() {
|
|
|
+ return (httpRequest.getRemoteAddr());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getRemoteUser() {
|
|
|
+ return (httpRequest.getRemoteUser());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getRequestURI() {
|
|
|
+ return (httpRequest.getRequestURI());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getQueryString() {
|
|
|
+ return (httpRequest.getQueryString());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getMethod() {
|
|
|
+ return httpRequest.getMethod();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void proceed() throws IOException, ServletException {
|
|
|
+ chain.doFilter(httpRequest, httpResponse);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void sendError(int code, String message) throws IOException {
|
|
|
+ httpResponse.sendError(code, message);
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+}
|