diff --git a/.gitignore b/.gitignore index f15690a..c1ed8f4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,9 @@ *~ deploy.sh .gradle +.DS_Store +.classpath +.metadata/ +.project +.settings/ +data/ diff --git a/README.md b/README.md index 444bec2..cf79356 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,25 @@ + +**IMPORTANT NOTICE**: versions 1.0.4 is *insecure and should not be used*. +They have a bug that allows an attacker to get ip authentication by setting +its ip on the 'Host' header. A fix is provided for now for versions v1.1.1, v1.2.0 and +v.1.3.0 of the plugin. + # HTTP Basic auth for ElasticSearch -This plugin provides an extension of ElasticSearchs HTTP Transport module to enable HTTP Basic authorization. +This plugin provides an extension of ElasticSearchs HTTP Transport module to enable HTTP Basic authorization and +Ip based authorization. -Requesting / does not request authentication to simplify health heck configuration. +Requesting `/` does not request authentication to simplify health check configuration. There is no way to configure this on a per index basis. + ## Version Mapping | Http Basic Plugin | elasticsearch | |-----------------------------|-----------------------| -| 1.1.0 | 1.0.0 | -| 1.0.4(master) | 0.90.7 | +| 1.1.0(master) | 1.0.0 | +| 1.0.4 | 0.90.7 | ## Installation @@ -19,12 +27,96 @@ Download the current version from https://github.com/Asquera/elasticsearch-http- ## Configuration -The plugin is disabled by default. Enabling basic authorization will disable the default HTTP Transport module. +Once the plugin is installed it can be configured in the [elasticsearch modules configuration file](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/setup-configuration.html#settings). See the [elasticserach directory layout information](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/setup-dir-layout.html) for more information about the default paths of an ES installation. + +| Setting key | Default value | Notes | +|-----------------------------------|------------------------------|-------------------------------------------------------------------------| +| `http.basic.enabled` | true | **true** disables the default ES HTTP Transport module | +| `http.basic.user` | "admin" | | +| `http.basic.password` | "admin_pw" | | +| `http.basic.ipwhitelist` | ["localhost", "127.0.0.1"] | uses Host Name Resolution from [java.net.InetAddress](http://docs.oracle.com/javase/7/docs/api/java/net/InetAddress.html) | +| `http.basic.trusted_proxy_chains` | [] | Set an array of trusted proxies ips chains | +| `http.basic.log` | false | enables plugin logging to ES log. Unauthenticated requests are always logged. | +| `http.basic.xforward` | "" | most common is [X-Forwarded-For](http://en.wikipedia.org/wiki/X-Forwarded-For) | + +Be aware that the password is stored in plain text. + +## Ip based authentication + +A client is **authenticated iff** its **request** is **trusted** and its **ip is whitelisted**. +A Request from a client connected *directly* (direct client) is **trusted**. Its ip is the request ip. +A Request form a client connected *via proxies* (remote client) is **trusted iff** there is a tail +subchain of the request chain that matches a tail subchain of the trusted proxy chains. + +**A tail subchain** of a chain "*A,B,C*" is a subchain that matches it by the end. +Example: the 3 tail subchains of the ip chain *A,B,C* are: + + (pseudo code) tailSubchains("A,B,C") --> ["A,B,C", "B,C", "C"] + +The request chain of a remote client is obtained following these steps: + +- read the request's xforward configured header field. +- remove the xforwarded defined client's ip (first listed ip as defined by X-Forwarded-For) from it. +- append the request ip to it. + +The ip chain of a remote client is the ip previous to the longest trusted tail subchain .Is the ip used to check + against the whitelist. + + +### Request chain checks + +Having the following configuration: + + http.basic.xforward = 'X-Forwarded-For' + http.basic.trusted_proxy_chains = ["B,C", "Z"] + +#### Trusted cases: + +- A remote client with ip *A* connects to [server] via proxies with ips *B* and *C*. *X-Forwarded-For* header has "*A,B*", removing the client's ip "*A*" and adding the request ip *C*, the resulting chain *B,C* matches a trusted tail subchain. Client's ip is A. + + [A] --> B --> C --> [server] + +- A remote client with ip *A* connects to [server] via proxies with ips *R*, *P*, *B* and *C*. *X-Forwarded-For* header has "*A,R,P,B*". + Removing the client's ip "*A*" and adding the request ip *C* , the resulting chain ** matches a trusted tail subchain. **note**: in this case "*P*" is taken as the client's ip, and checked against the white list. Client's ip is P. + + [A] --> R --> P --> B --> C --> [server] + +- A remote client with ip *A* connects to [server] via *C*. *X-Forwarded-For* header has + *A*, removing the client's ip *A* and adding the request ip *C*, the resulting chain *C* matches a trusted tail subchain. Client's ip is A. + + [A] --> C --> [server] + +- client *A* connects directly to [server]. *X-Forwarded-For* header is not set. Client's ip is A. + + [A] --> [server] + +#### Untrusted cases: + +- A remote client with ip *A* connects to [server] via *D*. *X-Forwarded-For* header has + "*A*", removing the client's ip "*A*" and adding the request ip *D*, the resulting chain *D* doesn't match any trusted sub ip chain. + + [A] --> D --> [server] + +- A remote client with ip *X* connects to proxy with ip *C* passing a faked *X-Forwarded-For* header "*R*". *C* will check the IP of the request and add it to the *X-Forwarded-For* field. the server will receive and *X-Forwarded-For* header + as: "*R,X*", remove the client's ip "*R*", add the request ip "*C*" and finally drop the request, as "*X,C*" doesn't match the trusted ip. + + [X] -- R --> C --> [server] + + +### configuration example + +The following code enables plugin logging, sets user and password, sets chain +"1.1.1.1,2.2.2.2" as trusted , whitelists ip 3.3.3.3 and defines xforward +header as the common 'X-Forwarded-For': ``` -http.basic.enabled: true -http.basic.user: "my_username" -http.basic.password: "my_password" +http.basic.log: true +http.basic.user: "some_user" +http.basic.password: "some_password" +http.basic.ipwhitelist: ["3.3.3.3"] +http.basic.xforward: "X-Forwarded-For" +http.basic.trusted_proxy_chains: ["1.1.1.1,2.2.2.2"] +>>>>>>> 8f3012f... fixed security problem in ip authentication. ``` Be aware that the password is stored in plain text. @@ -34,12 +126,21 @@ Be aware that the password is stored in plain text. ``` $ curl -v localhost:9200 # works $ curl -v --user my_username:my_password localhost:9200/foo # works +``` + +**note:** localhost is a whitelisted ip as default. +``` $ curl -v --user my_username:password localhost:9200/foo # sends 401 ``` -## Problems +## Development + +### Testing + Maven is configured to run the unit and integration tests. This plugin makes + use of [ES Integration Tests](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/integration-tests.html) -This will not send WWW-Authorize headers - this is due to elasticsearch not allowing to add custom headers to responses. + `mvn test` test runs all tests + `mvn integration-test` test runs integration tests only ## Issues diff --git a/pom.xml b/pom.xml index 9c5fd6d..9d4cd6c 100644 --- a/pom.xml +++ b/pom.xml @@ -13,9 +13,25 @@ UTF-8 1.0.0 + 4.6.1 + + + org.apache.lucene + lucene-test-framework + ${lucene.version} + test + + + + org.apache.httpcomponents + httpclient + 4.3.5 + test + + org.elasticsearch elasticsearch @@ -23,21 +39,27 @@ - org.testng - testng - 6.8 - test - - - org.hamcrest - hamcrest-core - - - junit - junit - - + org.elasticsearch + elasticsearch + ${elasticsearch.version} + test-jar + test + + + org.hamcrest + hamcrest-all + 1.3 + test + + + + junit + junit + 4.10 + test + + diff --git a/src/main/java/com/asquera/elasticsearch/plugins/http/HttpBasicServer.java b/src/main/java/com/asquera/elasticsearch/plugins/http/HttpBasicServer.java index 091f110..f8f7d94 100644 --- a/src/main/java/com/asquera/elasticsearch/plugins/http/HttpBasicServer.java +++ b/src/main/java/com/asquera/elasticsearch/plugins/http/HttpBasicServer.java @@ -13,13 +13,17 @@ import static org.elasticsearch.rest.RestStatus.*; import java.io.IOException; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; +import java.net.InetAddress; +import java.net.InetSocketAddress; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.rest.RestRequest.Method; import org.elasticsearch.rest.StringRestResponse; +import com.asquera.elasticsearch.plugins.http.auth.Client; +import com.asquera.elasticsearch.plugins.http.auth.InetAddressWhitelist; +import com.asquera.elasticsearch.plugins.http.auth.ProxyChains; +import com.asquera.elasticsearch.plugins.http.auth.XForwardedFor; + // # possible http config // http.basic.user: admin // http.basic.password: password @@ -29,7 +33,7 @@ // # EITHER $.ajaxSetup({ headers: { 'Authorization': "Basic " + credentials }}); // # OR use beforeSend in $.ajax({ // http.cors.allow-headers: "X-Requested-With, Content-Type, Content-Length, Authorization" -// +// /** * @author Florian Gilcher (florian.gilcher@asquera.de) * @author Peter Karich @@ -38,8 +42,9 @@ public class HttpBasicServer extends HttpServer { private final String user; private final String password; - private final Set whitelist; - private final String xForwardFor; + private final InetAddressWhitelist whitelist; + private final ProxyChains proxyChains; + private final String xForwardHeader; private final boolean log; @Inject public HttpBasicServer(Settings settings, Environment environment, HttpServerTransport transport, @@ -49,38 +54,35 @@ public class HttpBasicServer extends HttpServer { this.user = settings.get("http.basic.user", "admin"); this.password = settings.get("http.basic.password", "admin_pw"); - this.whitelist = new HashSet(Arrays.asList( + this.whitelist = new InetAddressWhitelist( settings.getAsArray("http.basic.ipwhitelist", - new String[]{"localhost", "127.0.0.1"}))); - - // for AWS load balancers it is X-Forwarded-For -> hmmh does not work - this.xForwardFor = settings.get("http.basic.xforward", ""); - this.log = settings.getAsBoolean("http.basic.log", false); - Loggers.getLogger(getClass()).info("using {}:{} with whitelist {}, xforward {}", - user, password, whitelist, xForwardFor); + new String[]{"localhost", "127.0.0.1"})); + this.proxyChains = new ProxyChains( + settings.getAsArray( + "http.basic.trusted_proxy_chains", new String[]{""})); + + // for AWS load balancers it is X-Forwarded-For -> hmmh does not work + this.xForwardHeader = settings.get("http.basic.xforward", ""); + this.log = settings.getAsBoolean("http.basic.log", true); + Loggers.getLogger(getClass()).info("using {}:{} with whitelist: {}, xforward header field: {}, trusted proxy chain: {}", + user, password, whitelist, xForwardHeader, proxyChains); } @Override public void internalDispatchRequest(final HttpRequest request, final HttpChannel channel) { - if (log) - logger.info("Authorization:{}, host:{}, xforward:{}, path:{}, isInWhitelist:{}, Client-IP:{}, X-Client-IP:{}", - request.header("Authorization"), request.header("host"), - request.header(xForwardFor), request.path(), isInIPWhitelist(request), - request.header("X-Client-IP"), request.header("Client-IP")); - + if (log) { + logRequest(request); + } // allow health check even without authorization if (healthCheck(request)) { channel.sendResponse(new StringRestResponse(OK, "{\"OK\":{}}")); - } else if (allowOptionsForCORS(request) || authBasic(request) || isInIPWhitelist(request)) { + } else if (authorized(request)) { super.internalDispatchRequest(request, channel); } else { - String addr = getAddress(request); - Loggers.getLogger(getClass()).error("UNAUTHORIZED type:{}, address:{}, path:{}, request:{}, content:{}, credentials:{}", - request.method(), addr, request.path(), request.params(), request.content().toUtf8(), getDecoded(request)); - - StringRestResponse response = new StringRestResponse(UNAUTHORIZED, "Authentication Required"); - response.addHeader("WWW-Authenticate", "Basic realm=\"Restricted\""); - channel.sendResponse(response); + logUnAuthorizedRequest(request); + StringRestResponse response = new StringRestResponse(UNAUTHORIZED, "Authentication Required"); + response.addHeader("WWW-Authenticate", "Basic realm=\"Restricted\""); + channel.sendResponse(response); } } @@ -89,6 +91,43 @@ private boolean healthCheck(final HttpRequest request) { return (request.method() == RestRequest.Method.GET) && path.equals("/"); } + /** + * + * + * @param request + * @return true if the request is authorized + */ + private boolean authorized(final HttpRequest request) { + return allowOptionsForCORS(request) || + authBasic(request) || ipAuthorized(request); + } + + /** + * + * + * @param request + * @return true iff the client is authorized by ip + */ + private boolean ipAuthorized(final HttpRequest request) { + boolean ipAuthorized = false; + String xForwardedFor = request.header(xForwardHeader); + Client client = new Client(getAddress(request), + whitelist, + new XForwardedFor(xForwardedFor), + proxyChains); + ipAuthorized = client.isAuthorized(); + if (ipAuthorized) { + if (log) { + String template = "Ip Authorized client: {}"; + Loggers.getLogger(getClass()).info(template, client); + } + } else { + String template = "Ip Unauthorized client: {}"; + Loggers.getLogger(getClass()).error(template, client); + } + return ipAuthorized; + } + public String getDecoded(HttpRequest request) { String authHeader = request.header("Authorization"); if (authHeader == null) @@ -121,33 +160,17 @@ private boolean authBasic(final HttpRequest request) { return false; } - private String getAddress(HttpRequest request) { - String addr; - if (xForwardFor.isEmpty()) { - addr = request.header("Host"); - addr = addr == null ? "" : addr; - } else { - addr = request.header(xForwardFor); - addr = addr == null ? "" : addr; - int addrIndex = addr.indexOf(','); - if (addrIndex >= 0) - addr = addr.substring(0, addrIndex); - } - int portIndex = addr.indexOf(":"); - if (portIndex >= 0) - addr = addr.substring(0, portIndex); - return addr; + /** + * + * + * @param request + * @return the IP adress of the direct client + */ + private InetAddress getAddress(HttpRequest request) { + return ((InetSocketAddress) request.getRemoteAddress()).getAddress(); } - private boolean isInIPWhitelist(HttpRequest request) { - String addr = getAddress(request); -// Loggers.getLogger(getClass()).info("address {}, path {}, request {}", -// addr, request.path(), request.params()); - if (whitelist.isEmpty() || addr.isEmpty()) - return false; - return whitelist.contains(addr); - } /** * https://en.wikipedia.org/wiki/Cross-origin_resource_sharing the @@ -164,4 +187,29 @@ private boolean allowOptionsForCORS(HttpRequest request) { } return false; } + + public void logRequest(final HttpRequest request) { + String addr = getAddress(request).getHostAddress(); + String t = "Authorization:{}, Host:{}, Path:{}, {}:{}, Request-IP:{}, " + + "Client-IP:{}, X-Client-IP{}"; + logger.info(t, + request.header("Authorization"), + request.header("Host"), + request.path(), + xForwardHeader, + request.header(xForwardHeader), + addr, + request.header("X-Client-IP"), + request.header("Client-IP")); + } + + public void logUnAuthorizedRequest(final HttpRequest request) { + String addr = getAddress(request).getHostAddress(); + String t = "UNAUTHORIZED type:{}, address:{}, path:{}, request:{}," + + "content:{}, credentials:{}"; + Loggers.getLogger(getClass()).error(t, + request.method(), addr, request.path(), request.params(), + request.content().toUtf8(), getDecoded(request)); + } + } diff --git a/src/main/java/com/asquera/elasticsearch/plugins/http/auth/Client.java b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/Client.java new file mode 100644 index 0000000..436ab33 --- /dev/null +++ b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/Client.java @@ -0,0 +1,209 @@ +package com.asquera.elasticsearch.plugins.http.auth; +import java.net.InetAddress; +import java.util.List; +import java.util.ArrayList; + +/** + * This class is responsible for determining the ip of the + * remote client and if the client is authorized based on its ip. + * a client is authorized iff it's ip is whitelisted and the path to the + * server is trusted + *

+ * a client can be trusted in this cases: + *

    + *
  • client is directly connected to the server and its request ip is + * whitelisted + *
  • client is connected to the server via a chain of proxies trusted by + * the server, and its ip is whitelisted + * + * @author Ernesto Miguez (ernesto.miguez@asquera.de) + */ +public class Client { + private final InetAddress requestIp; + private final InetAddressWhitelist whitelist; + private final XForwardedFor xForwardedFor; + private final ProxyChains trustedProxyChains; + /** + * the trusted state of the client. A client is trusted if it is connected + * directly or if it is connected via a trusted ip chain. + */ + private boolean trusted; + /** + * the whitelisted state of the client. + */ + private boolean whitelisted; + /** + * the authorize state of the client. + * true iff the client it is conected via a trusted proxy chain and + * its client ip is whitelisted: + */ + private boolean authorized; + + /** + * @param requestIp + * @param whitelist + * @param xForwardedFor + * @param trustedProxyChains + * @param authorized + * @param trusted + * @param whitelisted + */ + public Client(InetAddress requestIp, InetAddressWhitelist whitelist, + XForwardedFor xForwardedFor, ProxyChains trustedProxyChains) + { + this.requestIp = requestIp; + this.whitelist = whitelist; + this.xForwardedFor = xForwardedFor; + this.trustedProxyChains = trustedProxyChains; + trusted = checkTrusted(); + whitelisted = checkWhitelisted(); + authorized = trusted && whitelisted; + } + + /** + * + * @return the String representation of the client's ip + * from the point of view of the server + *

    + * this can be one of: + *

      + *
    • client ip from X-Forwarded-For + *
    • a proxy ip in the proxy chain defined in X-Forwarded-For + *
    • the request ip + * + * + */ + public String ip() { + String ip = requestIp.getHostAddress(); + if (xForwardedFor.isSet()) { + ip = remoteClientIp(); + } + return ip; + } + + /** + * + * determines the trust state of the client. + *

      + * The client is trusted when: + *

        + *
      • it is not connected via proxy + *
      • it is connected via proxies and least one of the proxies subchains is trusted + * + * @return true if the client's proxy chain is trusted or if the client is + * not connected via proxy, false otherwise. + */ + + private boolean checkTrusted() { + boolean trusted = true; + if (xForwardedFor.isSet()) { + trusted = trustedProxyChains.trusts(requestChain()); + } + return trusted; + } + + /** + * If the client conects via proxy its ip can be any of the listed + * in the X-Forwarded-For field; the trusted subchain determines which is the + * client's remote ip, as the ip previous to the first item of the trusted + * subchain. + *

        + * If the client doesn't connect via proxy its ip is the request ip + * + * @return true iff clients ip is whitelisted + * + */ + private boolean checkWhitelisted() { + boolean whitelisted = false; + if (xForwardedFor.isSet()) { + whitelisted = whitelist.contains(remoteClientIp()); + } else { + whitelisted = whitelist.contains(requestIp); + } + return whitelisted; + } + + /** + * @return the trusted ip chain or null if none + */ + private ProxyChain trustedChain() { + return trustedProxyChains.trustedSubchain(requestChain()); + } + + /** + * @param request_ip + * @return an request chain in the form of [proxy-1, .., proxy-n, request] + */ + private ProxyChain requestChain() { + List ipsChain = new ArrayList(); + ipsChain.addAll(xForwardedFor.proxies()); + ipsChain.add(requestIp.getHostAddress()); + return new ProxyChain(ipsChain); + } + + /** + * + * In case of X-Forwarded-For set, the ip of the remote client relative + * to the server is defined as follows: + *

        + * by default the remote client ip is the first ip of the X-Forwarded-For. + * If there is a sub proxy chain in the X-Forwarded-For that is trusted, the + * client ip is the ip that precedes the starting of the trusted subchain.

        + * example:

        + * + * a X-Forwarded-For value "1.1.1.1,2.2.2.2,3.3.3.3" with "3.3.3.3" as + * trusted proxy chain will have the "3.3.3.3" subchain trusted. This + * determines "2.2.2.2" as the server's remote client + * + * @return the remote client's ip relative to the server + */ + private String remoteClientIp() { + String clientIp = xForwardedFor.client(); + if (trustedChain() != null) { + List trustedProxies = trustedChain().getProxyChain(); + List proxies = xForwardedFor.proxies(); + proxies.removeAll(trustedProxies); + if (proxies.size() > 0) { + clientIp = proxies.get(proxies.size() - 1); + } + } + return clientIp; + } + + @Override + public String toString() { + String addr = requestIp.getHostAddress(); + String s = "client with request ip " + addr + + (xForwardedFor.isSet() ? ", remoteIp: " + remoteClientIp() : "") + + " is:" + + (authorized ? "Authorized" : "NotAuthorized") + + ", " + + (trusted ? "Trusted" : "NotTrusted") + + " (X-Forwarded-For: " + + xForwardedFor + + "), " + + (whitelisted ? "Whitelisted" : "NotWhitelisted"); + return s; + } + + /** + * @return the trusted + */ + public boolean isTrusted() { + return trusted; + } + + /** + * @return the whitelisted + */ + public boolean isWhitelisted() { + return whitelisted; + } + + /** + * @return the authorized + */ + public boolean isAuthorized() { + return authorized; + } +} diff --git a/src/main/java/com/asquera/elasticsearch/plugins/http/auth/InetAddressWhitelist.java b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/InetAddressWhitelist.java new file mode 100644 index 0000000..dd416f2 --- /dev/null +++ b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/InetAddressWhitelist.java @@ -0,0 +1,116 @@ +package com.asquera.elasticsearch.plugins.http.auth; +import org.elasticsearch.common.logging.Loggers; + +import java.util.ArrayList; +import java.util.List; +import java.util.HashSet; +import java.util.Set; +import java.util.Iterator; +import java.util.Arrays; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * + * Wraps the configured whitelisted ips. + * It uses a set of {@link InetAddress} internally. + *

        + * + * + * + * @author Ernesto Miguez (ernesto.miguez@asquera.de) + */ + +public class InetAddressWhitelist { + private Set whitelist; + /** + * + * + * @param whitelist + */ + public InetAddressWhitelist(Set whitelist) { + this.whitelist = whitelist; + } + + /** + * + * + * @param sWhitelist + * + */ + public InetAddressWhitelist(String[] sWhitelist) { + this(toInetAddress(Arrays.asList(sWhitelist))); + } + + /** + * Checks the request ip for inclusion. + * Since that ip comes in a {@link InetAddress} representation, it is checked + * against the whitelist. + * + * @param candidate + * @return if the ip is included in the whitelist + */ + public Boolean contains(InetAddress candidate) { + return this.whitelist.contains(candidate); + } + + /** + * + * Checks the xForwardedFor defined client ip for inclusion. + * Since that ip comes in a String representation, it is checked against + * the String representation of the defined whitelist. + * + * @param candidate + * @return if the ip is included in the String representation of the + * whitelist ips + */ + public Boolean contains(String candidate) { + return getStringWhitelist().contains(candidate); + } + + /** + * @return set of the string representations of the whitelist + */ + Set getStringWhitelist() { + Iterator iterator = this.whitelist.iterator(); + Set set = new HashSet(); + while (iterator.hasNext()) { + InetAddress next = iterator.next(); + set.add(next.getHostAddress()); + } + return set; + } + + /** + * when an configured InetAddress is Unkown or Invalid it is dropped from the + * whitelist + * + * @param ips a list of string ips + * @return a list of {@link InetAddress} objects + * + */ + static Set toInetAddress(List ips) { + List listIps = new ArrayList(); + Iterator iterator = ips.iterator(); + while (iterator.hasNext()) { + String next = iterator.next(); + try { + listIps.add(InetAddress.getByName(next)); + } catch (UnknownHostException e) { + String template = "an ip set in the whitelist settings raised an " + + "UnknownHostException: {}, dropping it"; + Loggers.getLogger(InetAddressWhitelist.class).info(template, e.getMessage()); + } + } + return new HashSet(listIps); + } + + /** + * delegate method + */ + @Override + public String toString() { + return whitelist.toString(); + } + +} diff --git a/src/main/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChain.java b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChain.java new file mode 100644 index 0000000..34ae434 --- /dev/null +++ b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChain.java @@ -0,0 +1,104 @@ +package com.asquera.elasticsearch.plugins.http.auth; + +import java.util.*; + +/** + * + * + * This class wraps an ip chain (an ordered list of ips). + * + * @author Ernesto Miguez (ernesto.miguez@asquera.de) + * + */ +public class ProxyChain { + private List proxyChain; + + public ProxyChain() { + this.proxyChain = new ArrayList(); + } + + public ProxyChain(List proxyChain) { + this.proxyChain = proxyChain; + } + + public ProxyChain(String proxyChain) { + this(new ArrayList(Arrays.asList(proxyChain.split(",")))); + } + + + /** + * @return the proxy chain + */ + public List getProxyChain() { + return proxyChain; + } + + /** + * A subchain is every segment of the list matching by the tail, included + * itself.

        example: + * "1.1.1.1,2.2.2.2" is trusted by trusted list "3.3.3.3,4.4.4.4,2.2.2.2" since the + * subchain "2.2.2.2" is included in a subchain of the trusted list. + * + * @return a new {@link ProxyChain} instance having all the subchains of the + * present instance + */ + public ProxyChains subchains() { + List reversedIps = new ArrayList(proxyChain); + Collections.reverse(reversedIps); + ListIterator iterator = reversedIps.listIterator(); + ProxyChains subchains = new ProxyChains((Set)new HashSet()); + ProxyChain subChain = new ProxyChain(new ArrayList()); + while (iterator.hasNext()) { + String next = iterator.next(); + subChain.add(next); + List r = new ArrayList(subChain.getProxyChain()); + Collections.reverse(r); + subchains.add( new ProxyChain(r)); + } + return subchains; + } + + /** + * delegated method + * @param o + * @see List#add(Object o); + */ + public void add (Object o) { + proxyChain.add((String)o); + } + + /** + * delegated method + * @see List#toString(); + */ + @Override + public String toString() { + return proxyChain.toString(); + } + + /** + * delegated method + * @see List#equals(); + */ + @Override + public boolean equals(Object c) { + return proxyChain.equals(((ProxyChain)c).getProxyChain()); + } + + /** + * delegated method + * @see List#hashCode(); + */ + @Override + public int hashCode() { + return proxyChain.hashCode(); + } + + /** + * delegated method + * @see List#size(); + */ + public int size() { + return proxyChain.size(); + } +} diff --git a/src/main/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChains.java b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChains.java new file mode 100644 index 0000000..5a23363 --- /dev/null +++ b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChains.java @@ -0,0 +1,144 @@ +package com.asquera.elasticsearch.plugins.http.auth; + +import java.util.*; + +/** + * This class wraps a set of {@link ProxyChain} + * + * @author Ernesto Miguez (ernesto.miguez@asquera.de) + **/ + +public class ProxyChains { + + private Set proxyChains; + + public ProxyChains(Set proxyChains) { + this.proxyChains = proxyChains; + } + + public ProxyChains(String[] proxyChains) { + this(getProxies(proxyChains)); + } + + /** + * + * An ip chain is trusted iff any of it subchains is contained in + * any of the instance subchains + * + * @param candidate the ip list to check + * @return true iff the candidate is included + */ + public Boolean trusts(ProxyChain candidate) { + return trustedSubchain(candidate) != null; + } + + /** + * + * Find the trusted subchain if any.

        note: Any chain is included in its subchains + * + * @param candidate + * @return the trusted subchain or nil if none is trusted. + * If more than one is trusted, the longuest will be returned + */ + public ProxyChain trustedSubchain(ProxyChain candidate) { + Set sub = subchains(); + sub.retainAll(candidate.subchains().getProxyChains()); + ProxyChain trusted = null; + if (!sub.isEmpty()) { + trusted = Collections.max(sub, new InetAddressChainComparator()); + } + return trusted; + } + + /** + * a comparator that uses ip chain size + */ + class InetAddressChainComparator implements Comparator { + @Override + public int compare(ProxyChain a, ProxyChain b) { + return a.size() < b.size() ? -1 : a.size() == b.size() ? 0 : 1; + } + } + + /** + * + * @return the set of subchains of the trusted ip proxy chains + * @see {@link ProxyChain#subchains()} + */ + + public Set subchains() { + Iterator iterator = proxyChains.iterator(); + Set set = new HashSet(); + while (iterator.hasNext()) { + ProxyChain next = iterator.next(); + set.addAll(next.subchains().getProxyChains()); + } + return set; + } + + /** + * + * delegated method + * + */ + public boolean contains(Object c) { + return proxyChains.contains(c); + } + + + /** + * + * delegated method + * + * @param chain + * @return if it is empty + */ + public boolean isEmpty() { + return proxyChains.isEmpty(); + } + + /** + * + * delegated method + * + * @param chain + * @return true if it could be added + */ + public boolean add(ProxyChain chain) { + return proxyChains.add(chain); + } + + + /** + * + * delegated method + */ + @Override + public String toString() { + return proxyChains.toString(); + } + + /** + * + * @param array of proxies represented as comma separated strings + * @return a {@link ProxyChain} object representing the passed proxies + * + */ + + private static Set getProxies(String[] ips) { + Set pChainSet = new HashSet(); + Iterator iterator = (Arrays.asList(ips)).iterator(); + while (iterator.hasNext()) { + String next = iterator.next(); + pChainSet.add(new ProxyChain(next)); + } + return pChainSet; + } + + /** + * @return the proxyChains + */ + public Set getProxyChains() { + return proxyChains; + } +} diff --git a/src/main/java/com/asquera/elasticsearch/plugins/http/auth/XForwardedFor.java b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/XForwardedFor.java new file mode 100644 index 0000000..16a4b4e --- /dev/null +++ b/src/main/java/com/asquera/elasticsearch/plugins/http/auth/XForwardedFor.java @@ -0,0 +1,92 @@ +package com.asquera.elasticsearch.plugins.http.auth; + +import java.util.List; +import java.util.Arrays; +import java.util.ArrayList; + +/** + * + * Class that handles the values obtained from the X-Forwarded-For (XFF) HTTP Header + * field. + *

        + * The X-Forwarded-For (XFF) HTTP header field is a de facto standard for + * identifying the originating IP address of a client connecting to a web + * server through an HTTP proxy or load balancer. + *

        + * The usefulness of XFF depends on the proxy server truthfully reporting the + * original host's IP address; for this reason, effective use of XFF requires + * knowledge of which proxies are trustworthy, for instance by looking them + * up in a whitelist of servers whose maintainers can be trusted. + * + * @see X-Forwarded-For + * + * + * + * @author Ernesto Miguez (ernesto.miguez@asquera.de) + */ + +public class XForwardedFor { + /** + * + * The X-Forwarded-For Header value as received in the request + * The general format of the field is: + * + * X-Forwarded-For: client, proxy1, proxy2 + */ + private final String xForwardedFor; + + /** + * + * @param xForwardedFor + */ + public XForwardedFor(String xForwardedFor) { + this.xForwardedFor = xForwardedFor != null ? xForwardedFor : ""; + } + + /** + * @return the ip of the client as defined by the X-Forwarded-For Header + */ + public String client() { + ArrayList splitted_ips = new ArrayList( + Arrays.asList(xForwardedFor.split(","))); + return splitted_ips.remove(0); + } + + /** + * + * @return true if the X-Forwarded-For header was set + */ + public boolean isSet() { + return ! xForwardedFor.equals(""); + } + + /** + * delegate method + */ + @Override + public String toString() { + String s = "not used"; + if (isSet()) { + s = xForwardedFor; + } + return s; + } + + /** + * @return the ips of the proxies between the client(as defined by the + * X-Forwarded-For Header) and the * server + */ + protected List proxies() { + ArrayList splitted_ips = new ArrayList( + Arrays.asList(xForwardedFor.split(","))); + splitted_ips.remove(0); + return splitted_ips; + } + + /** + * @return the xForwardedFor + */ + public String getxForwardedFor() { + return xForwardedFor; + } +} diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ClientTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ClientTest.java new file mode 100644 index 0000000..f698884 --- /dev/null +++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ClientTest.java @@ -0,0 +1,229 @@ +package com.asquera.elasticsearch.plugins.http.auth; + +import static org.junit.Assert.*; +import static org.hamcrest.CoreMatchers.is; + +import org.junit.Test; + +import java.net.UnknownHostException; +import java.net.InetAddress; + + +public class ClientTest{ + + private final String whitelistedIp = "8.8.8.8"; + private final String notWhitelistedIp = "42.42.42.42"; + private final String untrustedRequestIp = "50.50.50.50"; + private final String trustedRequestIp = "6.6.6.6"; + private String[] trustedIps = {"7.7.7.7,"+ trustedRequestIp}; + private String[] whitelist = { whitelistedIp }; + private String xForwardedFor = "9.9.9.9,8.8.8.8,7.7.7.7"; + + @Test + public void authorizedWhitelistedRequestNilXForwardedFor() throws UnknownHostException { + Client c = new Client( + InetAddress.getByName(whitelistedIp), + new InetAddressWhitelist(whitelist), + new XForwardedFor(null), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is(whitelistedIp)); + assertTrue(c.isTrusted()); + assertTrue(c.isWhitelisted()); + assertTrue(c.isAuthorized()); + } + + public void authorizedWhitelistedRequestUnsetProxies() throws UnknownHostException { + Client c = new Client( + InetAddress.getByName(whitelistedIp), + new InetAddressWhitelist(whitelist), + new XForwardedFor(""), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is(whitelistedIp)); + assertTrue(c.isTrusted()); + assertTrue(c.isWhitelisted()); + assertTrue(c.isAuthorized()); + } + + @Test + public void unauthorizedWhitelistedRequestUnsetProxies() throws UnknownHostException { + Client c = new Client( + InetAddress.getByName(notWhitelistedIp), + new InetAddressWhitelist(whitelist), + new XForwardedFor(""), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is(notWhitelistedIp)); + assertTrue(c.isTrusted()); + assertFalse(c.isWhitelisted()); + assertFalse(c.isAuthorized()); + } + + @Test + public void ipOfUntrustedRequestViaProxiesIsFirstOfXForwardedFor() throws UnknownHostException { + Client c = new Client( + InetAddress.getByName(untrustedRequestIp), + new InetAddressWhitelist(whitelist), + new XForwardedFor(xForwardedFor), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is("9.9.9.9")); + assertFalse(c.isTrusted()); + assertFalse(c.isWhitelisted()); + assertFalse(c.isAuthorized()); + } + + @Test + public void ipOfTrustedRequestViaProxiesIsInXForwardedFor() throws UnknownHostException { + Client c = new Client( + InetAddress.getByName(trustedRequestIp), + new InetAddressWhitelist(whitelist), + new XForwardedFor(xForwardedFor), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is("8.8.8.8")); + assertTrue(c.isTrusted()); + assertTrue(c.isWhitelisted()); + assertTrue(c.isAuthorized()); + } + + @Test + public void ipOfNotWhitelistedIpViaTrustedProxiesIsFirstOfXForwardedFor() throws UnknownHostException { + String[] whitelist = {"10.10.10.10"}; + Client c = new Client( + InetAddress.getByName(trustedRequestIp), + new InetAddressWhitelist(whitelist), + new XForwardedFor(xForwardedFor), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is("8.8.8.8")); + assertTrue(c.isTrusted()); + assertFalse(c.isWhitelisted()); + assertFalse(c.isAuthorized()); + } + + @Test + public void noXForwardedSetRequestIpWhitelisted() throws UnknownHostException { + Client c = new Client( + InetAddress.getByName(whitelistedIp), + new InetAddressWhitelist(whitelist), + new XForwardedFor(""), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is(whitelistedIp)); + assertTrue(c.isTrusted()); + assertTrue(c.isWhitelisted()); + assertTrue(c.isAuthorized()); + } + + @Test + public void noXForwardedSetRequestIpNotWhitelisted() throws UnknownHostException { + Client c = new Client( + InetAddress.getByName(notWhitelistedIp), + new InetAddressWhitelist(whitelist), + new XForwardedFor(""), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is(notWhitelistedIp)); + assertTrue(c.isTrusted()); + assertFalse(c.isWhitelisted()); + assertFalse(c.isAuthorized()); + } + + @Test + public void clientIsTrustedBySeveralProxyChains() throws UnknownHostException { + + String[] trustedIps = {"1.1.1.1,2.2.2.2,3.3.3.3","4.4.4.4,2.2.2.2,3.3.3.3"}; + String xForwardedFor = whitelistedIp + ",2.2.2.2"; + Client c = new Client( + InetAddress.getByName("3.3.3.3"), + new InetAddressWhitelist(whitelist), + new XForwardedFor(xForwardedFor), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is(whitelistedIp)); + assertTrue(c.isTrusted()); + assertTrue(c.isWhitelisted()); + assertTrue(c.isAuthorized()); + } + + @Test + public void clientIsUntrustedAndInWhitelist() throws UnknownHostException { + String xForwardedFor = whitelistedIp + ",2.2.2.2"; + Client c = new Client( + InetAddress.getByName(untrustedRequestIp), + new InetAddressWhitelist(whitelist), + new XForwardedFor(xForwardedFor), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is(whitelistedIp)); + assertFalse(c.isTrusted()); + assertTrue(c.isWhitelisted()); + assertFalse(c.isAuthorized()); + } + + @Test + public void clientIsTrustedAndInWhitelistViaOneProxy() throws UnknownHostException { + String xForwardedFor = whitelistedIp; + Client c = new Client( + InetAddress.getByName(trustedRequestIp), + new InetAddressWhitelist(whitelist), + new XForwardedFor(xForwardedFor), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is(whitelistedIp)); + assertTrue(c.isTrusted()); + assertTrue(c.isWhitelisted()); + assertTrue(c.isAuthorized()); + } + + @Test + public void clientIsUntrustedAndInWhitelistViaOneProxy() throws UnknownHostException { + String xForwardedFor = whitelistedIp; + Client c = new Client( + InetAddress.getByName(untrustedRequestIp), + new InetAddressWhitelist(whitelist), + new XForwardedFor(xForwardedFor), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is(whitelistedIp)); + assertFalse(c.isTrusted()); + assertTrue(c.isWhitelisted()); + assertFalse(c.isAuthorized()); + } + + @Test + public void lastXForwardTrustedProxyIsNotwhitelistedClient() throws UnknownHostException { + String[] trustedIps = {"3.3.3.3,2.2.2.2,1.1.1.1," + trustedRequestIp}; + String xForwardedFor = notWhitelistedIp + "," + whitelistedIp + ",3.3.3.3,2.2.2.2"; + Client c = new Client( + InetAddress.getByName(trustedRequestIp), + new InetAddressWhitelist(whitelist), + new XForwardedFor(xForwardedFor), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is("2.2.2.2")); + assertTrue(c.isTrusted()); + assertFalse(c.isWhitelisted()); + assertFalse(c.isAuthorized()); + } + + @Test + public void lastXForwardTrustedProxyIsWhitelistedClient() throws UnknownHostException { + String[] trustedIps = {"3.3.3.3,2.2.2.2," + trustedRequestIp}; + String xForwardedFor = notWhitelistedIp + "," + whitelistedIp + ",2.2.2.2"; + Client c = new Client( + InetAddress.getByName(trustedRequestIp), + new InetAddressWhitelist(whitelist), + new XForwardedFor(xForwardedFor), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is(whitelistedIp)); + assertTrue(c.isTrusted()); + assertTrue(c.isWhitelisted()); + assertTrue(c.isAuthorized()); + } + + + @Test + public void longestTrustedChainDefinesClientNontInWhitelist() throws UnknownHostException { + String xForwardedFor = notWhitelistedIp + "," + whitelistedIp + ",2.2.2.2"; + String[] trustedIps = { xForwardedFor + "," + trustedRequestIp}; + Client c = new Client( + InetAddress.getByName(trustedRequestIp), + new InetAddressWhitelist(whitelist), + new XForwardedFor(xForwardedFor), + new ProxyChains(trustedIps)); + assertThat(c.ip(), is(notWhitelistedIp)); + assertTrue(c.isTrusted()); + assertFalse(c.isWhitelisted()); + assertFalse(c.isAuthorized()); + } +} diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/InetAddressWhitelistTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/InetAddressWhitelistTest.java new file mode 100644 index 0000000..8112a9a --- /dev/null +++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/InetAddressWhitelistTest.java @@ -0,0 +1,55 @@ +package com.asquera.elasticsearch.plugins.http.auth; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class InetAddressWhitelistTest { + + static final String localhost = "localhost"; + static final String containedIp = "1.1.1.1"; + static String notContainedIp = "2.2.2.2"; + private InetAddressWhitelist whitelist(String ip) { + String[] w = { ip }; + return new InetAddressWhitelist(w); + } + + @Test + public void testInnetLocalhost() throws UnknownHostException { + assertTrue(whitelist(localhost).contains(InetAddress.getByName(localhost))); + } + @Test + public void testInnetNullDefaultsToLocalhost() throws UnknownHostException { + assertTrue(whitelist(null).contains(InetAddress.getByName(localhost))); + } + @Test + public void testStringLocalhostNotMatched() throws UnknownHostException { + // the ip that "localhost" resolves to its matched ip and not the string + // "localhost" itself + assertFalse(whitelist(localhost).contains(localhost)); + } + + @Test + public void testIpContained() throws UnknownHostException { + assertTrue(whitelist(containedIp).contains(containedIp)); + } + + @Test + public void testEmptyWhitelist() throws UnknownHostException { + assertFalse(whitelist("").contains(notContainedIp)); + } + + @Test + public void testNotContained() throws UnknownHostException { + assertFalse(whitelist(containedIp).contains(notContainedIp)); + } + + @Test + public void invalidIpIsDropped() throws UnknownHostException { + String invalidIp = "555.555.555.555"; + assertFalse(whitelist(invalidIp).contains(invalidIp)); + } +} diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChainTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChainTest.java new file mode 100644 index 0000000..8d58107 --- /dev/null +++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChainTest.java @@ -0,0 +1,61 @@ +package com.asquera.elasticsearch.plugins.http.auth; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import java.util.ArrayList; + +public class ProxyChainTest { + + + @Test + public void emptyProxyChainGeneratesEmptySubchain() { + assertTrue(new ProxyChain().subchains().isEmpty()); + } + + @Test + public void notEmptyIsIncludedInSubchain() { + ArrayList c = new ArrayList(); + c.add( "123.134.123.213" ); + ProxyChain r = new ProxyChain(c); + assertTrue(r.subchains().contains(r)); + } + + @Test + public void lastTwoIncludedInSubchain() { + ArrayList c = new ArrayList(); + c.add( "1.1.1.1" ); + c.add( "3.3.3.3" ); + ProxyChain r = new ProxyChain(c); + ProxyChain s = new ProxyChain(); + s.add( "1.1.1.1" ); + s.add( "3.3.3.3" ); + assertTrue(r.subchains().contains(s)); + } + + @Test + public void firstTwoNotIncludedInSubchain() { + ArrayList c = new ArrayList(); + c.add( "123.134.123.213" ); + c.add( "1.1.1.1" ); + c.add( "3.3.3.3" ); + ProxyChain r = new ProxyChain(c); + ProxyChain s = new ProxyChain(); + s.add(c.get(0)); + s.add(c.get(1)); + assertFalse(r.subchains().contains(s)); + } + + @Test + public void middleNotIncludedInSubchain() { + ArrayList c = new ArrayList(); + c.add( "123.134.123.213" ); + c.add( "1.1.1.1" ); + c.add( "3.3.3.3" ); + ProxyChain r = new ProxyChain(c); + ProxyChain s = new ProxyChain(); + s.add(c.get(1)); + assertFalse(r.subchains().contains(s)); + } +} diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChainsTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChainsTest.java new file mode 100644 index 0000000..c580c5f --- /dev/null +++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/ProxyChainsTest.java @@ -0,0 +1,60 @@ +package com.asquera.elasticsearch.plugins.http.auth; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.junit.Before; + +public class ProxyChainsTest { + + private ProxyChains trustedChains; + private ProxyChain trustedCandidateChain; + private ProxyChain unTrustedCandidateChain; + private final String untrustedChain1 = "50.50.50.50"; + private final String trustedChain1 = "7.7.7.7"; + private final String trustedChain2 = "5.5.5.5,6.6.6.6"; + private final String trustedChain3 = "8.8.8.8,9.9.9.9,10.10.10.10"; + private final String trustedChain4 = "2.2.2.2" + "," + trustedChain2; + private final String[] t = { trustedChain1, trustedChain2, trustedChain3, trustedChain4 }; + + @Before public void initialize() { + trustedCandidateChain = new ProxyChain(trustedChain1); + unTrustedCandidateChain = new ProxyChain(untrustedChain1); + trustedChains = new ProxyChains(t); + } + + @Test(expected=NullPointerException.class) + public void NullPointerExceptionInNullTrustedIps() { + String[] t = null; + trustedChains = new ProxyChains(t); + assertFalse(trustedChains.trusts(trustedCandidateChain)); + } + + @Test + public void unTrustsEmptyCandidate() { + assertFalse(trustedChains.trusts(new ProxyChain(""))); + } + + @Test + public void unTrustsAnyCandidateWithEmptyTrustedChain() { + String[] t = {""}; + trustedChains = new ProxyChains(t); + assertFalse(trustedChains.trusts(trustedCandidateChain)); + } + + @Test + public void trustsCandidatesInProxyChain() { + assertTrue(trustedChains.trusts(trustedCandidateChain)); + } + + @Test + public void unTrustedCandidateNotInProxyChain() { + assertFalse(trustedChains.trusts(unTrustedCandidateChain)); + } + + @Test + public void trustCandidateContainedInTwoTrustedChains() { + trustedCandidateChain = new ProxyChain(trustedChain2); + assertTrue(trustedChains.trusts(trustedCandidateChain)); + } +} diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/XForwardedForTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/XForwardedForTest.java new file mode 100644 index 0000000..cb38637 --- /dev/null +++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/XForwardedForTest.java @@ -0,0 +1,36 @@ +package com.asquera.elasticsearch.plugins.http.auth; + +import static org.junit.Assert.*; +import static org.hamcrest.CoreMatchers.is; +import org.junit.Test; + +public class XForwardedForTest { + + @Test + public void returnsClientWithClientAndProxy() { + // It seems the getName lookup for empty string is localhost + String xForwardedFor = "123.123.123.123,122.122.12.1" ; + assertThat(new XForwardedFor(xForwardedFor).client(), + is(xForwardedFor.split(",")[0])); + } + + @Test + public void returnsClientWithClient() { + // It seems the getName lookup for empty string is localhost + String xForwardedFor = "123.123.123.123" ; + assertThat(new XForwardedFor(xForwardedFor).client(), + is(xForwardedFor.split(",")[0])); + } + + @Test + public void returnsClientWithNil() { + assertThat(new XForwardedFor(null).client(), is("")); + } + + @Test + public void unsetHeaderReturnsEmptyClient() { + // It seems the getName lookup for empty string is localhost + String xForwardedFor = "" ; + assertFalse(new XForwardedFor(xForwardedFor).isSet()); + } +} diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/DefaultConfigurationIntegrationTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/DefaultConfigurationIntegrationTest.java new file mode 100644 index 0000000..d1bcbf8 --- /dev/null +++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/DefaultConfigurationIntegrationTest.java @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 com.asquera.elasticsearch.plugins.http.auth.integration; + + +import org.apache.http.impl.client.HttpClients; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.ElasticsearchIntegrationTest; +import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope; +import org.elasticsearch.test.ElasticsearchIntegrationTest.Scope; +import org.elasticsearch.test.rest.client.http.HttpRequestBuilder; +import org.elasticsearch.test.rest.client.http.HttpResponse; +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; + +/** + * Test a rest action that sets special response headers + */ +@ClusterScope(transportClientRatio = 0.0, scope = Scope.SUITE) +public class DefaultConfigurationIntegrationTest extends ElasticsearchIntegrationTest { + + @Override + protected Settings nodeSettings(int nodeOrdinal) { + return ImmutableSettings.settingsBuilder() + .build(); + } + + @Test + public void testHealthCheck() throws Exception { + HttpResponse response = httpClient().path("/").execute(); + assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus())); + } + + @Test + public void localhostClientIsAuthenticated() throws Exception { + HttpResponse response = httpClient().path("/_status").execute(); + assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus())); + } + + public static HttpRequestBuilder httpClient() { + return new HttpRequestBuilder(HttpClients.createDefault()).host("localhost").port(9200); + } +} diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/EmptyWhitelistIntegrationTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/EmptyWhitelistIntegrationTest.java new file mode 100644 index 0000000..3635e34 --- /dev/null +++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/EmptyWhitelistIntegrationTest.java @@ -0,0 +1,112 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 com.asquera.elasticsearch.plugins.http.auth.integration; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.Base64; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.ElasticsearchIntegrationTest; +import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope; +import org.elasticsearch.test.rest.client.http.HttpGetWithEntity; +import org.elasticsearch.test.rest.client.http.HttpRequestBuilder; +import org.elasticsearch.test.rest.client.http.HttpResponse; +import org.junit.Test; + +import java.net.URI; +import java.net.URISyntaxException; + +import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope; +import static org.hamcrest.Matchers.equalTo; + +/** + * Test a rest action that sets special response headers + */ +@ClusterScope(transportClientRatio = 0.0, scope = Scope.SUITE) +public class EmptyWhitelistIntegrationTest extends ElasticsearchIntegrationTest { + + @Override + protected Settings nodeSettings(int nodeOrdinal) { + return ImmutableSettings.settingsBuilder().putArray("http.basic.ipwhitelist", "unkown") + .build(); + } + + @Test + public void testHealthCheck() throws Exception { + HttpResponse response = httpClient().path("/").execute(); + assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus())); + } + + @Test + public void localhostClientIsNotIpAuthenticated() throws Exception { + HttpResponse response = httpClient().path("/_status").execute(); + assertThat(response.getStatusCode(), equalTo(RestStatus.UNAUTHORIZED.getStatus())); + } + + @Test + public void localhostClientIsBasicAuthenticated() throws Exception { + HttpUriRequest request = httpRequest(); + String credentials = "admin:admin_pw"; + request.setHeader("Authorization", "Basic " + Base64.encodeBytes(credentials.getBytes())); + CloseableHttpResponse response = closeableHttpClient().execute(request); + assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus())); + } + + + @Test + public void localhostClientIsBasicAuthenticatedPassingXForward() throws Exception { + HttpUriRequest request = httpRequest(); + String credentials = "admin:admin_pw"; + request.setHeader("Authorization", "Basic " + Base64.encodeBytes(credentials.getBytes())); + request.setHeader("X-Forwarded-For", "1.1.1.1" ); + CloseableHttpResponse response = closeableHttpClient().execute(request); + assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus())); + } + @Test + public void localhostClientNotBasicAuthenticated() throws Exception { + HttpUriRequest request = httpRequest(); + String credentials = "admin:wrong"; + request.setHeader("Authorization", "Basic " + Base64.encodeBytes(credentials.getBytes())); + CloseableHttpResponse response = closeableHttpClient().execute(request); + assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.UNAUTHORIZED.getStatus())); + } + + public static HttpRequestBuilder httpClient() { + return new HttpRequestBuilder(HttpClients.createDefault()).host("localhost").port(9200); + } + + public static HttpUriRequest httpRequest() { + HttpUriRequest httpUriRequest = null; + try { + httpUriRequest = new HttpGetWithEntity(new URI("http", null, "localhost", 9200, "/_status", null, null)); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + return httpUriRequest; + } + + public static CloseableHttpClient closeableHttpClient() { + return HttpClients.createDefault(); + } + +} diff --git a/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/IpAuthenticationIntegrationTest.java b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/IpAuthenticationIntegrationTest.java new file mode 100644 index 0000000..41895bb --- /dev/null +++ b/src/test/java/com/asquera/elasticsearch/plugins/http/auth/integration/IpAuthenticationIntegrationTest.java @@ -0,0 +1,130 @@ + +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 com.asquera.elasticsearch.plugins.http.auth.integration; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.Base64; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.ElasticsearchIntegrationTest; +import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope; +import org.elasticsearch.test.rest.client.http.HttpGetWithEntity; +import org.elasticsearch.test.rest.client.http.HttpRequestBuilder; +import org.elasticsearch.test.rest.client.http.HttpResponse; +import org.junit.Test; + +import java.net.URI; +import java.net.URISyntaxException; + +import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope; +import static org.hamcrest.Matchers.equalTo; + +/** + * Test a rest action that sets special response headers + */ +@ClusterScope(transportClientRatio = 0.0, scope = Scope.SUITE) +public class IpAuthenticationIntegrationTest extends ElasticsearchIntegrationTest { + + protected final String localhost = "127.0.0.1"; + protected final String whitelistedIp = "2.2.2.2"; + protected final String notWhitelistedIp = "3.3.3.3"; + protected final String trustedIp = "4.4.4.4"; + + @Override + protected Settings nodeSettings(int nodeOrdinal) { + return ImmutableSettings.settingsBuilder() + .putArray("http.basic.ipwhitelist", whitelistedIp) + .putArray("http.basic.trusted_proxy_chains", trustedIp + "," + localhost) + .put("http.basic.xforward", "X-Forwarded-For") + .build(); + } + + @Test + public void testHealthCheck() throws Exception { + HttpResponse response = httpClient().path("/").execute(); + assertThat(response.getStatusCode(), equalTo(RestStatus.OK.getStatus())); + } + + @Test + public void localhostClientIsBasicAuthenticated() throws Exception { + HttpUriRequest request = httpRequest(); + String credentials = "admin:admin_pw"; + request.setHeader("Authorization", "Basic " + Base64.encodeBytes(credentials.getBytes())); + CloseableHttpResponse response = closeableHttpClient().execute(request); + assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus())); + } + + @Test + public void proxyViaLocalhostIpAuthenticatesWhitelistedClients() throws Exception { + HttpUriRequest request = httpRequest(); + request.setHeader("X-Forwarded-For", whitelistedIp ); + CloseableHttpResponse response = closeableHttpClient().execute(request); + assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus())); + request = httpRequest(); + request.setHeader("X-Forwarded-For", notWhitelistedIp + "," + whitelistedIp); + response = closeableHttpClient().execute(request); + assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus())); + request = httpRequest(); + request.setHeader("X-Forwarded-For", notWhitelistedIp + "," + whitelistedIp + "," + trustedIp); + response = closeableHttpClient().execute(request); + assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.OK.getStatus())); + } + + @Test + public void proxyViaLocalhostIpUnauthenticatesNonWhitelistedClients() throws Exception { + HttpUriRequest request = httpRequest(); + request.setHeader("X-Forwarded-For", notWhitelistedIp); + CloseableHttpResponse response = closeableHttpClient().execute(request); + assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.UNAUTHORIZED.getStatus())); + request = httpRequest(); + request.setHeader("X-Forwarded-For", whitelistedIp + "," + notWhitelistedIp + "," + trustedIp); + response = closeableHttpClient().execute(request); + assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.UNAUTHORIZED.getStatus())); + request = httpRequest(); + request.setHeader("X-Forwarded-For", ""); + response = closeableHttpClient().execute(request); + assertThat(response.getStatusLine().getStatusCode(), equalTo(RestStatus.UNAUTHORIZED.getStatus())); + } + + public static HttpRequestBuilder httpClient() { + return new HttpRequestBuilder(HttpClients.createDefault()) + .host("localhost").port(9200); + } + + public static HttpUriRequest httpRequest() { + HttpUriRequest httpUriRequest = null; + try { + httpUriRequest = new HttpGetWithEntity(new URI("http", + null, "localhost", 9200, "/_status", null, null)); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + return httpUriRequest; + } + + public static CloseableHttpClient closeableHttpClient() { + return HttpClients.createDefault(); + } + +}