azkaban-developers

Add support for upstream gateways or proxies in front of Azkaban

11/6/2016 8:57:38 PM
3.8.0

Details

diff --git a/azkaban-common/src/main/java/azkaban/utils/WebUtils.java b/azkaban-common/src/main/java/azkaban/utils/WebUtils.java
index 55891b2..916e441 100644
--- a/azkaban-common/src/main/java/azkaban/utils/WebUtils.java
+++ b/azkaban-common/src/main/java/azkaban/utils/WebUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2012 LinkedIn Corp.
+ * Copyright 2016 LinkedIn Corp.
  *
  * Licensed 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
@@ -17,6 +17,7 @@
 package azkaban.utils;
 
 import java.text.NumberFormat;
+import java.util.Map;
 
 import org.joda.time.DateTime;
 import org.joda.time.DurationFieldType;
@@ -33,6 +34,8 @@ public class WebUtils {
   private static final long ONE_GB = 1024 * ONE_MB;
   private static final long ONE_TB = 1024 * ONE_GB;
 
+  public static final String X_FORWARDED_FOR_HEADER = "X-Forwarded-For";
+
   public String formatDate(long timeMS) {
     if (timeMS == -1) {
       return "-";
@@ -164,4 +167,41 @@ public class WebUtils {
     else
       return sizeBytes + " B";
   }
+
+  /**
+   * Gets the actual client IP address inspecting the X-Forwarded-For
+   * HTTP header or using the provided 'remote IP address' from the
+   * low level TCP connection from the client.
+   *
+   * If multiple IP addresses are provided in the X-Forwarded-For header
+   * then the first one (first hop) is used
+   *
+   * @param httpHeaders List of HTTP headers for the current request
+   * @param remoteAddr The client IP address and port from the current request's TCP connection
+   * @return The actual client IP address
+   */
+  public String getRealClientIpAddr(Map<String, String> httpHeaders, String remoteAddr){
+
+    // If some upstream device added an X-Forwarded-For header
+    // use it for the client ip
+    // This will support scenarios where load balancers or gateways
+    // front the Azkaban web server and a changing Ip address invalidates
+    // the session
+
+    String clientIp = httpHeaders.getOrDefault(X_FORWARDED_FOR_HEADER, null);
+    if(clientIp == null){
+      clientIp = remoteAddr;
+    }
+    else{
+      // header can contain comma separated list of upstream servers - get the first one
+      String ips[] = clientIp.split(",");
+      clientIp = ips[0];
+    }
+
+    // Strip off port and only get IP address
+    String parts[] = clientIp.split(":");
+    clientIp = parts[0];
+
+    return clientIp;
+  }
 }
diff --git a/azkaban-common/src/test/java/azkaban/utils/WebUtilsTest.java b/azkaban-common/src/test/java/azkaban/utils/WebUtilsTest.java
new file mode 100644
index 0000000..39cb66e
--- /dev/null
+++ b/azkaban-common/src/test/java/azkaban/utils/WebUtilsTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2014 LinkedIn Corp.
+ *
+ * Licensed 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 azkaban.utils;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Test class for azkaban.utils.WebUtils
+ */
+public class WebUtilsTest {
+
+  @Test
+  public void testWhenNoXForwardedForHeaderUseClientIp(){
+
+    String clientIp = "127.0.0.1:10000";
+    Map<String, String> headers = new HashMap<>();
+
+    WebUtils utils = new WebUtils();
+
+    String ip = utils.getRealClientIpAddr(headers, clientIp);
+
+    assertEquals(ip, "127.0.0.1");
+  }
+
+  @Test
+  public void testWhenClientIpNoPort(){
+
+    String clientIp = "192.168.1.1";
+    Map<String, String> headers = new HashMap<>();
+
+    WebUtils utils = new WebUtils();
+
+    String ip = utils.getRealClientIpAddr(headers, clientIp);
+
+    assertEquals(ip, "192.168.1.1");
+  }
+
+  @Test
+  public void testWhenXForwardedForHeaderUseHeader(){
+
+    String clientIp = "127.0.0.1:10000";
+    String upstreamIp = "192.168.1.1:10000";
+    Map<String, String> headers = new HashMap<>();
+
+    headers.put("X-Forwarded-For", upstreamIp);
+
+    WebUtils utils = new WebUtils();
+
+    String ip = utils.getRealClientIpAddr(headers, clientIp);
+
+    assertEquals(ip, "192.168.1.1");
+  }
+
+  @Test
+  public void testWhenXForwardedForHeaderMultipleUpstreamsUseHeader(){
+
+    String clientIp = "127.0.0.1:10000";
+    String upstreamIp = "192.168.1.1:10000";
+    Map<String, String> headers = new HashMap<>();
+
+    headers.put("X-Forwarded-For", upstreamIp + ",127.0.0.1,55.55.55.55");
+
+    WebUtils utils = new WebUtils();
+
+    String ip = utils.getRealClientIpAddr(headers, clientIp);
+
+    assertEquals(ip, "192.168.1.1");
+  }
+
+}
diff --git a/azkaban-web-server/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java b/azkaban-web-server/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
index b19021d..39cc07d 100644
--- a/azkaban-web-server/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
+++ b/azkaban-web-server/src/main/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
@@ -330,7 +330,7 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
    */
   protected Page newPage(HttpServletRequest req, HttpServletResponse resp,
       Session session, String template) {
-    Page page = new Page(req, resp, application.getVelocityEngine(), template);
+    Page page = new Page(req, resp, getApplication().getVelocityEngine(), template);
     page.add("azkaban_name", name);
     page.add("azkaban_label", label);
     page.add("azkaban_color", color);
@@ -381,7 +381,7 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
    */
   protected Page newPage(HttpServletRequest req, HttpServletResponse resp,
       String template) {
-    Page page = new Page(req, resp, application.getVelocityEngine(), template);
+    Page page = new Page(req, resp, getApplication().getVelocityEngine(), template);
     page.add("azkaban_name", name);
     page.add("azkaban_label", label);
     page.add("azkaban_color", color);
diff --git a/azkaban-web-server/src/main/java/azkaban/webapp/servlet/LoginAbstractAzkabanServlet.java b/azkaban-web-server/src/main/java/azkaban/webapp/servlet/LoginAbstractAzkabanServlet.java
index 1d9f315..f2a4603 100644
--- a/azkaban-web-server/src/main/java/azkaban/webapp/servlet/LoginAbstractAzkabanServlet.java
+++ b/azkaban-web-server/src/main/java/azkaban/webapp/servlet/LoginAbstractAzkabanServlet.java
@@ -32,6 +32,7 @@ import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import azkaban.utils.WebUtils;
 import org.apache.commons.fileupload.servlet.ServletFileUpload;
 import org.apache.commons.io.IOUtils;
 import org.apache.log4j.Logger;
@@ -138,7 +139,7 @@ public abstract class LoginAbstractAzkabanServlet extends
    */
   private void logRequest(HttpServletRequest req, Session session) {
     StringBuilder buf = new StringBuilder();
-    buf.append(req.getRemoteAddr()).append(" ");
+    buf.append(getRealClientIpAddr(req)).append(" ");
     if (session != null && session.getUser() != null) {
       buf.append(session.getUser().getUserId()).append(" ");
     } else {
@@ -166,7 +167,7 @@ public abstract class LoginAbstractAzkabanServlet extends
         buf.append("not-browser");
       }
     }
-
+    
     logger.info(buf.toString());
   }
 
@@ -210,9 +211,25 @@ public abstract class LoginAbstractAzkabanServlet extends
     return false;
   }
 
+  private String getRealClientIpAddr(HttpServletRequest req){
+
+    // If some upstream device added an X-Forwarded-For header
+    // use it for the client ip
+    // This will support scenarios where load balancers or gateways
+    // front the Azkaban web server and a changing Ip address invalidates
+    // the session
+    HashMap<String, String> headers = new HashMap<>();
+    headers.put(WebUtils.X_FORWARDED_FOR_HEADER,
+            req.getHeader(WebUtils.X_FORWARDED_FOR_HEADER.toLowerCase()));
+
+    WebUtils utils = new WebUtils();
+
+    return utils.getRealClientIpAddr(headers, req.getRemoteAddr());
+  }
+
   private Session getSessionFromRequest(HttpServletRequest req)
       throws ServletException {
-    String remoteIp = req.getRemoteAddr();
+    String remoteIp = getRealClientIpAddr(req);
     Cookie cookie = getCookieByName(req, SESSION_ID_NAME);
     String sessionId = null;
 
@@ -269,7 +286,7 @@ public abstract class LoginAbstractAzkabanServlet extends
         // See if the session id is properly set.
         if (params.containsKey("session.id")) {
           String sessionId = (String) params.get("session.id");
-          String ip = req.getRemoteAddr();
+          String ip = getRealClientIpAddr(req);
 
           session = getSessionFromSessionId(sessionId, ip);
           if (session != null) {
@@ -286,7 +303,7 @@ public abstract class LoginAbstractAzkabanServlet extends
 
         String username = (String) params.get("username");
         String password = (String) params.get("password");
-        String ip = req.getRemoteAddr();
+        String ip = getRealClientIpAddr(req);
 
         try {
           session = createSession(username, password, ip);
@@ -333,7 +350,7 @@ public abstract class LoginAbstractAzkabanServlet extends
       throws UserManagerException, ServletException {
     String username = getParam(req, "username");
     String password = getParam(req, "password");
-    String ip = req.getRemoteAddr();
+    String ip = getRealClientIpAddr(req);
 
     return createSession(username, password, ip);
   }
diff --git a/azkaban-web-server/src/restli/java/azkaban/restli/ProjectManagerResource.java b/azkaban-web-server/src/restli/java/azkaban/restli/ProjectManagerResource.java
index 4633496..8447ac3 100644
--- a/azkaban-web-server/src/restli/java/azkaban/restli/ProjectManagerResource.java
+++ b/azkaban-web-server/src/restli/java/azkaban/restli/ProjectManagerResource.java
@@ -62,9 +62,7 @@ public class ProjectManagerResource extends ResourceContextHolder {
     logger.info("Deploy called. {sessionId: " + sessionId + ", projectName: "
         + projectName + ", packageUrl:" + packageUrl + "}");
 
-    String ip =
-        (String) this.getContext().getRawRequestContext()
-            .getLocalAttr("REMOTE_ADDR");
+    String ip = ResourceUtils.getRealClientIpAddr(this.getContext());
     User user = ResourceUtils.getUserFromSessionId(sessionId, ip);
     ProjectManager projectManager = getAzkaban().getProjectManager();
     Project project = projectManager.getProject(projectName);
diff --git a/azkaban-web-server/src/restli/java/azkaban/restli/ResourceUtils.java b/azkaban-web-server/src/restli/java/azkaban/restli/ResourceUtils.java
index 36be301..ccb0a18 100644
--- a/azkaban-web-server/src/restli/java/azkaban/restli/ResourceUtils.java
+++ b/azkaban-web-server/src/restli/java/azkaban/restli/ResourceUtils.java
@@ -21,8 +21,12 @@ import azkaban.user.Role;
 import azkaban.user.User;
 import azkaban.user.UserManager;
 import azkaban.user.UserManagerException;
+import azkaban.utils.WebUtils;
 import azkaban.webapp.AzkabanWebServer;
 import azkaban.server.session.Session;
+import com.linkedin.restli.server.ResourceContext;
+
+import java.util.Map;
 
 public class ResourceUtils {
 
@@ -56,4 +60,19 @@ public class ResourceUtils {
 
     return session.getUser();
   }
+
+  public static String getRealClientIpAddr(ResourceContext context){
+
+    // If some upstream device added an X-Forwarded-For header
+    // use it for the client ip
+    // This will support scenarios where load balancers or gateways
+    // front the Azkaban web server and a changing Ip address invalidates
+    // the session
+    Map<String, String> headers = context.getRequestHeaders();
+
+    WebUtils utils = new WebUtils();
+
+    return utils.getRealClientIpAddr(headers,
+            (String) context.getRawRequestContext().getLocalAttr("REMOTE_ADDR"));
+  }
 }
diff --git a/azkaban-web-server/src/restli/java/azkaban/restli/UserManagerResource.java b/azkaban-web-server/src/restli/java/azkaban/restli/UserManagerResource.java
index d3c0c33..ed995c3 100644
--- a/azkaban-web-server/src/restli/java/azkaban/restli/UserManagerResource.java
+++ b/azkaban-web-server/src/restli/java/azkaban/restli/UserManagerResource.java
@@ -44,9 +44,7 @@ public class UserManagerResource extends ResourceContextHolder {
   public String login(@ActionParam("username") String username,
       @ActionParam("password") String password) throws UserManagerException,
       ServletException {
-    String ip =
-        (String) this.getContext().getRawRequestContext()
-            .getLocalAttr("REMOTE_ADDR");
+    String ip = ResourceUtils.getRealClientIpAddr(this.getContext());
     logger
         .info("Attempting to login for " + username + " from ip '" + ip + "'");
 
@@ -59,9 +57,7 @@ public class UserManagerResource extends ResourceContextHolder {
 
   @Action(name = "getUserFromSessionId")
   public User getUserFromSessionId(@ActionParam("sessionId") String sessionId) {
-    String ip =
-        (String) this.getContext().getRawRequestContext()
-            .getLocalAttr("REMOTE_ADDR");
+    String ip = ResourceUtils.getRealClientIpAddr(this.getContext());
     Session session = getSessionFromSessionId(sessionId, ip);
     azkaban.user.User azUser = session.getUser();
 
diff --git a/azkaban-web-server/src/test/java/azkaban/fixture/MockLoginAzkabanServlet.java b/azkaban-web-server/src/test/java/azkaban/fixture/MockLoginAzkabanServlet.java
new file mode 100644
index 0000000..e266711
--- /dev/null
+++ b/azkaban-web-server/src/test/java/azkaban/fixture/MockLoginAzkabanServlet.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2016 LinkedIn Corp.
+ *
+ * Licensed 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 azkaban.fixture;
+
+
+import azkaban.server.AzkabanServer;
+import azkaban.server.session.Session;
+import azkaban.server.session.SessionCache;
+import azkaban.user.UserManager;
+import azkaban.utils.Props;
+import azkaban.webapp.AzkabanWebServer;
+import azkaban.webapp.servlet.LoginAbstractAzkabanServlet;
+import azkaban.user.User;
+import org.apache.velocity.app.VelocityEngine;
+import org.mockito.Spy;
+import org.mortbay.jetty.Server;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.UUID;
+
+import static org.mockito.Mockito.*;
+
+public class MockLoginAzkabanServlet extends LoginAbstractAzkabanServlet {
+
+    private static final String SESSION_ID_NAME = "azkaban.browser.session.id";
+
+    public static HttpServletRequest getRequestWithNoUpstream(String clientIp, String sessionId, String requestMethod){
+
+        HttpServletRequest req = mock(HttpServletRequest.class);
+
+        when(req.getRemoteAddr()).thenReturn(clientIp);
+        when(req.getHeader("x-forwarded-for")).thenReturn(null);
+        when(req.getMethod()).thenReturn(requestMethod);
+        when(req.getContentType()).thenReturn("application/x-www-form-urlencoded");
+
+        // Requires sessionId to be passed that is in the application's session cache
+        when(req.getParameter("session.id")).thenReturn(sessionId);
+
+        return req;
+    }
+
+    public static HttpServletRequest getRequestWithUpstream(String clientIp, String upstreamIp, String sessionId, String requestMethod){
+
+        HttpServletRequest req = mock(HttpServletRequest.class);
+
+        when(req.getRemoteAddr()).thenReturn("2.2.2.2:9999");
+        when(req.getHeader("x-forwarded-for")).thenReturn(upstreamIp);
+        when(req.getMethod()).thenReturn(requestMethod);
+        when(req.getContentType()).thenReturn("application/x-www-form-urlencoded");
+
+        // Requires sessionId to be passed that is in the application's session cache
+        when(req.getParameter("session.id")).thenReturn(sessionId);
+
+        return req;
+    }
+
+    public static HttpServletRequest getRequestWithMultipleUpstreams(String clientIp, String upstreamIp, String sessionId, String requestMethod){
+
+        HttpServletRequest req = mock(HttpServletRequest.class);
+
+        when(req.getRemoteAddr()).thenReturn("2.2.2.2:9999");
+        when(req.getHeader("x-forwarded-for")).thenReturn(upstreamIp + ",1.1.1.1,3.3.3.3:33333");
+        when(req.getMethod()).thenReturn(requestMethod);
+        when(req.getContentType()).thenReturn("application/x-www-form-urlencoded");
+
+        // Requires sessionId to be passed that is in the application's session cache
+        when(req.getParameter("session.id")).thenReturn(sessionId);
+
+        return req;
+    }
+
+    public static MockLoginAzkabanServlet getServletWithSession(String sessionId,
+                                                                String username, String clientIp)
+            throws Exception{
+
+        MockLoginAzkabanServlet servlet = new MockLoginAzkabanServlet();
+
+        Server server = mock(Server.class);
+        Props props = new Props();
+        UserManager userManager = mock(UserManager.class);
+
+        // Need to mock and inject an application instance into the servlet
+        AzkabanWebServer app = mock(AzkabanWebServer.class);
+
+        MockLoginAzkabanServlet servletSpy = spy(servlet);
+
+        when(servletSpy.getApplication()).thenReturn(app);
+
+        // Create a concrete SessionCache so a session will get persisted
+        // and can get looked up
+        SessionCache cache = new SessionCache(props);
+        when(app.getSessionCache()).thenReturn(cache);
+
+        // Need a valid object here when processing a request
+        when(app.getVelocityEngine()).thenReturn(mock(VelocityEngine.class));
+
+        // Construct and store a session in the servlet
+        azkaban.user.User user = mock(azkaban.user.User.class);
+        when(user.getEmail()).thenReturn(username + "@mail.com");
+        when(user.getUserId()).thenReturn(username);
+
+        Session session = new Session(sessionId, user, clientIp);
+        servletSpy.getApplication().getSessionCache().addSession(session);
+
+
+        // Return the servletSpy since we replaced implementation for 'getApplication'
+        return servletSpy;
+    }
+
+    @Override
+    protected void handleGet(HttpServletRequest req, HttpServletResponse resp, Session session)
+            throws ServletException, IOException {
+
+        resp.getWriter().write("SUCCESS_MOCK_LOGIN_SERVLET");
+    }
+
+    @Override
+    protected void handlePost(HttpServletRequest req, HttpServletResponse resp, Session session)
+            throws ServletException, IOException {
+
+        resp.getWriter().write("SUCCESS_MOCK_LOGIN_SERVLET");
+    }
+}
diff --git a/azkaban-web-server/src/test/java/azkaban/fixture/MockResourceContext.java b/azkaban-web-server/src/test/java/azkaban/fixture/MockResourceContext.java
new file mode 100644
index 0000000..bce0e90
--- /dev/null
+++ b/azkaban-web-server/src/test/java/azkaban/fixture/MockResourceContext.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2016 LinkedIn Corp.
+ *
+ * Licensed 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 azkaban.fixture;
+
+import com.linkedin.data.transform.filter.request.MaskTree;
+import com.linkedin.r2.message.RequestContext;
+import com.linkedin.r2.message.rest.RestRequest;
+import com.linkedin.restli.server.PathKeys;
+import com.linkedin.restli.server.ProjectionMode;
+import com.linkedin.restli.server.ResourceContext;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+public class MockResourceContext implements ResourceContext {
+
+    Map<String, String> requestHeaders;
+    RequestContext requestContext;
+
+    @Override
+    public RestRequest getRawRequest() {
+        return null;
+    }
+
+    @Override
+    public String getRequestMethod() {
+        return null;
+    }
+
+    @Override
+    public PathKeys getPathKeys() {
+        return null;
+    }
+
+    @Override
+    public MaskTree getProjectionMask() {
+        return null;
+    }
+
+    @Override
+    public boolean hasParameter(String key) {
+        return false;
+    }
+
+    @Override
+    public String getParameter(String key) {
+        return null;
+    }
+
+    @Override
+    public Object getStructuredParameter(String key) {
+        return null;
+    }
+
+    @Override
+    public List<String> getParameterValues(String key) {
+        return null;
+    }
+
+    public void setRequestHeader(String name, String value){
+
+        this.requestHeaders.put(name, value);
+    }
+
+    @Override
+    public Map<String, String> getRequestHeaders() {
+        return this.requestHeaders;
+    }
+
+    @Override
+    public void setResponseHeader(String name, String value) {
+
+    }
+
+    @Override
+    public RequestContext getRawRequestContext() {
+        return this.requestContext;
+    }
+
+    @Override
+    public ProjectionMode getProjectionMode() {
+        return null;
+    }
+
+    @Override
+    public void setProjectionMode(ProjectionMode mode) {
+
+    }
+
+    public void setLocalAttr(String name, String value){
+        requestContext.putLocalAttr(name, value);
+    }
+
+    public MockResourceContext(){
+        requestHeaders = new HashMap<>();
+        requestContext = new RequestContext();
+    }
+
+    public static MockResourceContext getResourceContextWithUpstream(String clientIp, String upstream){
+        MockResourceContext ctx = new MockResourceContext();
+
+        ctx.setLocalAttr("REMOTE_ADDR", clientIp);
+        ctx.setRequestHeader("X-Forwarded-For", upstream);
+
+        return ctx;
+    }
+
+    public static MockResourceContext getResourceContextWithMultipleUpstreams(String clientIp,
+                                                                              String firstUpstream){
+        MockResourceContext ctx = new MockResourceContext();
+
+        ctx.setLocalAttr("REMOTE_ADDR", clientIp);
+        ctx.setRequestHeader("X-Forwarded-For", firstUpstream + ",55.55.55.55:55555,1.1.1.1:9999");
+
+        return ctx;
+    }
+
+    public static MockResourceContext getResourceContext(String clientIp){
+        MockResourceContext ctx = new MockResourceContext();
+
+        ctx.setLocalAttr("REMOTE_ADDR", clientIp);
+
+        return ctx;
+    }
+
+}
diff --git a/azkaban-web-server/src/test/java/azkaban/restli/ResourceUtilsTest.java b/azkaban-web-server/src/test/java/azkaban/restli/ResourceUtilsTest.java
new file mode 100644
index 0000000..c6203b5
--- /dev/null
+++ b/azkaban-web-server/src/test/java/azkaban/restli/ResourceUtilsTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016 LinkedIn Corp.
+ *
+ * Licensed 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 azkaban.restli;
+
+import azkaban.fixture.MockResourceContext;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class ResourceUtilsTest {
+
+    @Test
+    public void testWhenNoXForwardedForHeaderUseClientIp(){
+
+        String clientIp = "127.0.0.1:10000";
+        MockResourceContext ctx = MockResourceContext.getResourceContext(clientIp);
+        assertNotNull(ctx);
+
+        String ip = ResourceUtils.getRealClientIpAddr(ctx);
+
+        assertEquals(ip, "127.0.0.1");
+    }
+
+    @Test
+    public void testWhenClientIpNoPort(){
+
+        String clientIp = "192.168.1.1";
+        MockResourceContext ctx = MockResourceContext.getResourceContext(clientIp);
+        assertNotNull(ctx);
+
+        String ip = ResourceUtils.getRealClientIpAddr(ctx);
+
+        assertEquals(ip, "192.168.1.1");
+    }
+
+    @Test
+    public void testWhenXForwardedForHeaderUseHeader(){
+
+        String clientIp = "127.0.0.1:10000";
+        String upstreamIp = "192.168.1.1:10000";
+        MockResourceContext ctx = MockResourceContext.getResourceContextWithUpstream(clientIp, upstreamIp);
+        assertNotNull(ctx);
+
+        String ip = ResourceUtils.getRealClientIpAddr(ctx);
+
+        assertEquals(ip, "192.168.1.1");
+    }
+
+    @Test
+    public void testWhenXForwardedForHeaderMultipleUpstreamsUseHeader(){
+
+        String clientIp = "127.0.0.1:10000";
+        String upstreamIp = "192.168.1.1:10000";
+        MockResourceContext ctx = MockResourceContext.getResourceContextWithMultipleUpstreams(clientIp, upstreamIp);
+        assertNotNull(ctx);
+
+        String ip = ResourceUtils.getRealClientIpAddr(ctx);
+
+        assertEquals(ip, "192.168.1.1");
+    }
+}
diff --git a/azkaban-web-server/src/test/java/azkaban/webapp/servlet/LoginAbstractAzkabanServletTest.java b/azkaban-web-server/src/test/java/azkaban/webapp/servlet/LoginAbstractAzkabanServletTest.java
new file mode 100644
index 0000000..564ba8b
--- /dev/null
+++ b/azkaban-web-server/src/test/java/azkaban/webapp/servlet/LoginAbstractAzkabanServletTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2016 LinkedIn Corp.
+ *
+ * Licensed 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 azkaban.webapp.servlet;
+
+
+import azkaban.fixture.MockLoginAzkabanServlet;
+import azkaban.server.session.Session;
+import azkaban.server.session.SessionCache;
+import org.junit.Test;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNotSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class LoginAbstractAzkabanServletTest {
+
+    private HttpServletResponse getResponse(StringWriter stringWriter){
+        HttpServletResponse resp = mock(HttpServletResponse.class);
+        PrintWriter writer = new PrintWriter(stringWriter);
+
+        try{
+            when(resp.getWriter()).thenReturn(writer);
+        }
+        catch(IOException ex){
+            System.out.println(ex);
+        }
+
+        return resp;
+    }
+
+    @Test
+    public void testWhenGetRequestSessionIsValid() throws Exception, IOException, ServletException {
+
+        String clientIp = "127.0.0.1:10000";
+        String sessionId = "111";
+
+
+        HttpServletRequest req = MockLoginAzkabanServlet.getRequestWithNoUpstream(clientIp, sessionId, "GET");
+
+        StringWriter writer = new StringWriter();
+        HttpServletResponse resp = getResponse(writer);
+
+        MockLoginAzkabanServlet servlet = MockLoginAzkabanServlet.getServletWithSession(sessionId,
+                "user", "127.0.0.1");
+
+        servlet.doGet(req, resp);
+
+        // Assert that our response was written (we have a valid session)
+        assertEquals("SUCCESS_MOCK_LOGIN_SERVLET", writer.toString());
+    }
+
+    @Test
+    public void testWhenPostRequestSessionIsValid() throws Exception{
+
+        String clientIp = "127.0.0.1:10000";
+        String sessionId = "111";
+
+
+        HttpServletRequest req = MockLoginAzkabanServlet.getRequestWithNoUpstream(clientIp, sessionId, "POST");
+        StringWriter writer = new StringWriter();
+        HttpServletResponse resp = getResponse(writer);
+
+        MockLoginAzkabanServlet servlet = MockLoginAzkabanServlet.getServletWithSession(sessionId,
+                "user", "127.0.0.1");
+
+
+        servlet.doPost(req, resp);
+
+        // Assert that our response was written (we have a valid session)
+        assertEquals("SUCCESS_MOCK_LOGIN_SERVLET", writer.toString());
+    }
+
+    @Test
+    public void testWhenPostRequestChangedClientIpSessionIsInvalid() throws Exception{
+
+        String clientIp = "127.0.0.2:10000";
+        String sessionId = "111";
+
+
+        HttpServletRequest req = MockLoginAzkabanServlet.getRequestWithNoUpstream(clientIp, sessionId, "POST");
+
+        StringWriter writer = new StringWriter();
+        HttpServletResponse resp = getResponse(writer);
+
+
+        MockLoginAzkabanServlet servlet = MockLoginAzkabanServlet.getServletWithSession(sessionId,
+                "user", "127.0.0.1");
+
+
+        servlet.doPost(req, resp);
+
+        // Assert that our response was written (we have a valid session)
+        assertNotSame("SUCCESS_MOCK_LOGIN_SERVLET", writer.toString());
+    }
+
+    @Test
+    public void testWhenPostRequestChangedClientPortSessionIsValid() throws Exception{
+
+        String clientIp = "127.0.0.1:10000";
+        String sessionId = "111";
+
+
+        HttpServletRequest req = MockLoginAzkabanServlet.getRequestWithNoUpstream(clientIp, sessionId, "POST");
+
+        StringWriter writer = new StringWriter();
+        HttpServletResponse resp = getResponse(writer);
+
+
+        MockLoginAzkabanServlet servlet = MockLoginAzkabanServlet.getServletWithSession(sessionId,
+                "user", "127.0.0.1");
+
+
+        servlet.doPost(req, resp);
+
+        // Assert that our response was written (we have a valid session)
+        assertEquals("SUCCESS_MOCK_LOGIN_SERVLET", writer.toString());
+    }
+
+    @Test
+    public void testWhenPostRequestWithUpstreamSessionIsValid() throws Exception{
+
+        String clientIp = "127.0.0.1:10000";
+        String upstreamIp = "192.168.1.1:11111";
+        String sessionId = "111";
+
+
+        HttpServletRequest req = MockLoginAzkabanServlet.getRequestWithUpstream(clientIp, upstreamIp,
+                sessionId, "POST");
+
+        StringWriter writer = new StringWriter();
+        HttpServletResponse resp = getResponse(writer);
+
+
+        MockLoginAzkabanServlet servlet = MockLoginAzkabanServlet.getServletWithSession(sessionId,
+                "user", "192.168.1.1");
+
+
+        servlet.doPost(req, resp);
+
+        // Assert that our response was written (we have a valid session)
+        assertEquals("SUCCESS_MOCK_LOGIN_SERVLET", writer.toString());
+    }
+
+    @Test
+    public void testWhenPostRequestWithMultipleUpstreamsSessionIsValid() throws Exception{
+
+        String clientIp = "127.0.0.1:10000";
+        String upstreamIp = "192.168.1.1:11111,888.8.8.8:2222,5.5.5.5:5555";
+        String sessionId = "111";
+
+
+        HttpServletRequest req = MockLoginAzkabanServlet.getRequestWithUpstream(clientIp, upstreamIp,
+                sessionId, "POST");
+
+        StringWriter writer = new StringWriter();
+        HttpServletResponse resp = getResponse(writer);
+
+
+        MockLoginAzkabanServlet servlet = MockLoginAzkabanServlet.getServletWithSession(sessionId,
+                "user", "192.168.1.1");
+
+
+        servlet.doPost(req, resp);
+
+        // Assert that our response was written (we have a valid session)
+        assertEquals("SUCCESS_MOCK_LOGIN_SERVLET", writer.toString());
+    }
+}