| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| package com.cloud.api; |
| |
| import com.cloud.api.auth.ListUserTwoFactorAuthenticatorProvidersCmd; |
| import com.cloud.api.auth.SetupUserTwoFactorAuthenticationCmd; |
| import com.cloud.api.auth.ValidateUserTwoFactorAuthenticationCodeCmd; |
| import com.cloud.server.ManagementServer; |
| import com.cloud.user.Account; |
| import com.cloud.user.AccountManagerImpl; |
| import com.cloud.user.AccountService; |
| import com.cloud.user.User; |
| import com.cloud.user.UserAccount; |
| import com.cloud.utils.HttpUtils; |
| import com.cloud.vm.UserVmManager; |
| import org.apache.cloudstack.api.ApiConstants; |
| import org.apache.cloudstack.api.auth.APIAuthenticationManager; |
| import org.apache.cloudstack.api.auth.APIAuthenticationType; |
| import org.apache.cloudstack.api.auth.APIAuthenticator; |
| import org.apache.cloudstack.api.command.admin.config.ListCfgsByCmd; |
| import org.apache.cloudstack.framework.config.ConfigKey; |
| import org.junit.After; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.Mock; |
| import org.mockito.Mockito; |
| import org.mockito.junit.MockitoJUnitRunner; |
| |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| import javax.servlet.http.HttpSession; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.io.UnsupportedEncodingException; |
| import java.lang.reflect.Field; |
| import java.net.InetAddress; |
| import java.net.URLEncoder; |
| import java.net.UnknownHostException; |
| import java.util.HashMap; |
| import java.util.Map; |
| |
| import static org.mockito.ArgumentMatchers.nullable; |
| |
| @RunWith(MockitoJUnitRunner.class) |
| public class ApiServletTest { |
| |
| @Mock |
| ApiServer apiServer; |
| |
| @Mock |
| HttpServletRequest request; |
| |
| @Mock |
| HttpServletResponse response; |
| |
| @Mock |
| AccountService accountService; |
| |
| @Mock |
| APIAuthenticationManager authManager; |
| |
| @Mock |
| APIAuthenticator authenticator; |
| |
| @Mock |
| User user; |
| |
| @Mock |
| Account account; |
| |
| @Mock |
| HttpSession session; |
| |
| @Mock |
| ManagementServer managementServer; |
| |
| @Mock |
| UserAccount userAccount; |
| |
| @Mock |
| AccountService accountMgr; |
| |
| @Mock ConfigKey<Boolean> useForwardHeader; |
| StringWriter responseWriter; |
| |
| ApiServlet servlet; |
| ApiServlet spyServlet; |
| @SuppressWarnings("unchecked") |
| @Before |
| public void setup() throws SecurityException, NoSuchFieldException, |
| IllegalArgumentException, IllegalAccessException, IOException, UnknownHostException { |
| servlet = new ApiServlet(); |
| spyServlet = Mockito.spy(servlet); |
| responseWriter = new StringWriter(); |
| Mockito.when(response.getWriter()).thenReturn( |
| new PrintWriter(responseWriter)); |
| Mockito.when(request.getRemoteAddr()).thenReturn("127.0.0.1"); |
| Mockito.when(accountService.getSystemUser()).thenReturn(user); |
| Mockito.when(accountService.getSystemAccount()).thenReturn(account); |
| |
| Field accountMgrField = ApiServlet.class.getDeclaredField("accountMgr"); |
| accountMgrField.setAccessible(true); |
| accountMgrField.set(servlet, accountService); |
| |
| Mockito.when(authManager.getAPIAuthenticator(Mockito.anyString())).thenReturn(authenticator); |
| Mockito.lenient().when(authenticator.authenticate(Mockito.anyString(), Mockito.anyMap(), Mockito.isA(HttpSession.class), |
| Mockito.same(InetAddress.getByName("127.0.0.1")), Mockito.anyString(), Mockito.isA(StringBuilder.class), Mockito.isA(HttpServletRequest.class), Mockito.isA(HttpServletResponse.class))).thenReturn("{\"loginresponse\":{}"); |
| |
| Field authManagerField = ApiServlet.class.getDeclaredField("authManager"); |
| authManagerField.setAccessible(true); |
| authManagerField.set(servlet, authManager); |
| |
| Field apiServerField = ApiServlet.class.getDeclaredField("apiServer"); |
| apiServerField.setAccessible(true); |
| apiServerField.set(servlet, apiServer); |
| |
| } |
| |
| /** |
| * These are envinonment hacks, actually getting into the behavior of other |
| * classes, but there is no other way to run the test. |
| */ |
| @Before |
| public void hackEnvironment() throws Exception { |
| Field smsField = ApiDBUtils.class.getDeclaredField("s_ms"); |
| smsField.setAccessible(true); |
| smsField.set(null, managementServer); |
| Mockito.lenient().when(managementServer.getVersion()).thenReturn( |
| "LATEST-AND-GREATEST"); |
| } |
| |
| @After |
| public void cleanupEnvironmentHacks() throws Exception { |
| Field smsField = ApiDBUtils.class.getDeclaredField("s_ms"); |
| smsField.setAccessible(true); |
| smsField.set(null, null); |
| } |
| |
| @Test |
| public void utf8Fixup() { |
| Mockito.when(request.getQueryString()).thenReturn( |
| "foo=12345&bar=blah&baz=¶m=param"); |
| HashMap<String, Object[]> params = new HashMap<String, Object[]>(); |
| servlet.utf8Fixup(request, params); |
| Assert.assertEquals("12345", params.get("foo")[0]); |
| Assert.assertEquals("blah", params.get("bar")[0]); |
| } |
| |
| @Test |
| public void utf8FixupNull() { |
| Mockito.when(request.getQueryString()).thenReturn("&&=a&=&&a&a=a=a=a"); |
| servlet.utf8Fixup(request, new HashMap<String, Object[]>()); |
| } |
| |
| @Test |
| public void utf8FixupStrangeInputs() { |
| Mockito.when(request.getQueryString()).thenReturn("&&=a&=&&a&a=a=a=a"); |
| HashMap<String, Object[]> params = new HashMap<String, Object[]>(); |
| servlet.utf8Fixup(request, params); |
| Assert.assertTrue(params.containsKey("")); |
| } |
| |
| @Test |
| public void utf8FixupUtf() throws UnsupportedEncodingException { |
| Mockito.when(request.getQueryString()).thenReturn( |
| URLEncoder.encode("防水镜钻å”机", "UTF-8") + "=" |
| + URLEncoder.encode("árvÃztűrÅ‘tükörfúró", "UTF-8")); |
| HashMap<String, Object[]> params = new HashMap<String, Object[]>(); |
| servlet.utf8Fixup(request, params); |
| Assert.assertEquals("árvÃztűrÅ‘tükörfúró", params.get("防水镜钻å”机")[0]); |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Test |
| public void processRequestInContextUnauthorizedGET() { |
| Mockito.when(request.getMethod()).thenReturn("GET"); |
| Mockito.lenient().when( |
| apiServer.verifyRequest(Mockito.anyMap(), Mockito.anyLong(), Mockito.any(InetAddress.class))) |
| .thenReturn(false); |
| servlet.processRequestInContext(request, response); |
| Mockito.verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED); |
| Mockito.verify(apiServer, Mockito.never()).handleRequest( |
| Mockito.anyMap(), Mockito.anyString(), |
| Mockito.any(StringBuilder.class)); |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Test |
| public void processRequestInContextAuthorizedGet() { |
| Mockito.when(request.getMethod()).thenReturn("GET"); |
| Mockito.when( |
| apiServer.verifyRequest(nullable(Map.class), nullable(Long.class), nullable(InetAddress.class))) |
| .thenReturn(true); |
| servlet.processRequestInContext(request, response); |
| Mockito.verify(response).setStatus(HttpServletResponse.SC_OK); |
| Mockito.verify(apiServer, Mockito.times(1)).handleRequest( |
| Mockito.anyMap(), Mockito.anyString(), |
| Mockito.any(StringBuilder.class)); |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Test |
| public void processRequestInContextLogout() throws UnknownHostException { |
| Mockito.when(request.getMethod()).thenReturn("GET"); |
| Mockito.when(request.getSession(Mockito.anyBoolean())).thenReturn( |
| session); |
| Mockito.when(session.getAttribute("userid")).thenReturn(1l); |
| Mockito.when(session.getAttribute("accountobj")).thenReturn(account); |
| HashMap<String, String[]> params = new HashMap<String, String[]>(); |
| params.put(ApiConstants.COMMAND, new String[] { "logout" }); |
| Mockito.when(request.getParameterMap()).thenReturn(params); |
| |
| Mockito.when(authenticator.getAPIType()).thenReturn(APIAuthenticationType.LOGOUT_API); |
| |
| servlet.processRequestInContext(request, response); |
| |
| |
| Mockito.verify(authManager).getAPIAuthenticator("logout"); |
| Mockito.verify(authenticator).authenticate(Mockito.anyString(), Mockito.anyMap(), Mockito.isA(HttpSession.class), |
| Mockito.eq(InetAddress.getByName("127.0.0.1")), Mockito.anyString(), Mockito.isA(StringBuilder.class), Mockito.isA(HttpServletRequest.class), Mockito.isA(HttpServletResponse.class)); |
| Mockito.verify(session).invalidate(); |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Test |
| public void processRequestInContextLogin() throws UnknownHostException { |
| Mockito.when(request.getMethod()).thenReturn("GET"); |
| Mockito.when(request.getSession(Mockito.anyBoolean())).thenReturn( |
| session); |
| HashMap<String, String[]> params = new HashMap<String, String[]>(); |
| params.put(ApiConstants.COMMAND, new String[] { "login" }); |
| params.put(ApiConstants.USERNAME, new String[] { "TEST" }); |
| params.put(ApiConstants.PASSWORD, new String[] { "TEST-PWD" }); |
| params.put(ApiConstants.DOMAIN_ID, new String[] { "42" }); |
| params.put(ApiConstants.DOMAIN, new String[] { "TEST-DOMAIN" }); |
| Mockito.when(request.getParameterMap()).thenReturn(params); |
| |
| servlet.processRequestInContext(request, response); |
| |
| Mockito.verify(authManager).getAPIAuthenticator("login"); |
| Mockito.verify(authenticator).authenticate(Mockito.anyString(), Mockito.anyMap(), Mockito.isA(HttpSession.class), |
| Mockito.eq(InetAddress.getByName("127.0.0.1")), Mockito.anyString(), Mockito.isA(StringBuilder.class), Mockito.isA(HttpServletRequest.class), Mockito.isA(HttpServletResponse.class)); |
| } |
| |
| @Test |
| public void getClientAddressWithXForwardedFor() throws UnknownHostException { |
| String[] proxynet = {"127.0.0.0/8"}; |
| Mockito.when(spyServlet.proxyNets()).thenReturn(proxynet); |
| Mockito.when(spyServlet.doUseForwardHeaders()).thenReturn(true); |
| Mockito.when(request.getHeader(Mockito.eq("X-Forwarded-For"))).thenReturn("192.168.1.1"); |
| Assert.assertEquals(InetAddress.getByName("192.168.1.1"), spyServlet.getClientAddress(request)); |
| } |
| |
| @Test |
| public void getClientAddressWithHttpXForwardedFor() throws UnknownHostException { |
| String[] proxynet = {"127.0.0.0/8"}; |
| Mockito.when(spyServlet.proxyNets()).thenReturn(proxynet); |
| Mockito.when(spyServlet.doUseForwardHeaders()).thenReturn(true); |
| Mockito.when(request.getHeader(Mockito.eq("HTTP_X_FORWARDED_FOR"))).thenReturn("192.168.1.1"); |
| Assert.assertEquals(InetAddress.getByName("192.168.1.1"), spyServlet.getClientAddress(request)); |
| } |
| |
| @Test |
| public void getClientAddressWithRemoteAddr() throws UnknownHostException { |
| String[] proxynet = {"127.0.0.0/8"}; |
| Mockito.when(spyServlet.proxyNets()).thenReturn(proxynet); |
| Mockito.when(spyServlet.doUseForwardHeaders()).thenReturn(true); |
| Assert.assertEquals(InetAddress.getByName("127.0.0.1"), spyServlet.getClientAddress(request)); |
| } |
| |
| @Test |
| public void getClientAddressWithHttpClientIp() throws UnknownHostException { |
| String[] proxynet = {"127.0.0.0/8"}; |
| Mockito.when(spyServlet.proxyNets()).thenReturn(proxynet); |
| Mockito.when(spyServlet.doUseForwardHeaders()).thenReturn(true); |
| Mockito.when(request.getHeader(Mockito.eq("HTTP_CLIENT_IP"))).thenReturn("192.168.1.1"); |
| Assert.assertEquals(InetAddress.getByName("192.168.1.1"), spyServlet.getClientAddress(request)); |
| } |
| |
| @Test |
| public void getClientAddressDefault() throws UnknownHostException { |
| Mockito.when(request.getRemoteAddr()).thenReturn("127.0.0.1"); |
| Assert.assertEquals(InetAddress.getByName("127.0.0.1"), spyServlet.getClientAddress(request)); |
| } |
| |
| @Test |
| public void testSkip2FAcheckForAPIs() { |
| String command = "listZones"; |
| Map<String, Object[]> params = new HashMap<String, Object[]>(); |
| boolean result = servlet.skip2FAcheckForAPIs(command); |
| Assert.assertEquals(false, result); |
| |
| command = ListCfgsByCmd.APINAME; |
| params.put(ApiConstants.NAME, new String[] { UserVmManager.AllowUserExpungeRecoverVm.key() }); |
| result = servlet.skip2FAcheckForAPIs(command); |
| Assert.assertEquals(false, result); |
| } |
| |
| @Test |
| public void testDoNotSkip2FAcheckForAPIs() { |
| String[] commands = new String[] {ApiConstants.LIST_IDPS, ApiConstants.LIST_APIS, |
| ListUserTwoFactorAuthenticatorProvidersCmd.APINAME, SetupUserTwoFactorAuthenticationCmd.APINAME}; |
| Map<String, Object[]> params = new HashMap<String, Object[]>(); |
| for (String cmd: commands) { |
| boolean result = servlet.skip2FAcheckForAPIs(cmd); |
| Assert.assertEquals(true, result); |
| } |
| } |
| |
| @Test |
| public void testSkip2FAcheckForUserWhenAlreadyVerified() { |
| Mockito.when(session.getAttribute("userid")).thenReturn(1L); |
| Mockito.when(session.getAttribute(ApiConstants.IS_2FA_VERIFIED)).thenReturn(true); |
| |
| boolean result = servlet.skip2FAcheckForUser(session); |
| Assert.assertEquals(true, result); |
| } |
| |
| @Test |
| public void testDoNotSkip2FAcheckForUserWhen2FAEnabled() { |
| servlet.accountMgr = accountMgr; |
| HttpSession cuurentSession = Mockito.mock(HttpSession.class); |
| Mockito.when(cuurentSession.getAttribute("userid")).thenReturn(1L); |
| Mockito.when(cuurentSession.getAttribute(ApiConstants.IS_2FA_VERIFIED)).thenReturn(false); |
| Mockito.when(accountMgr.getUserAccountById(1L)).thenReturn(userAccount); |
| Mockito.when(userAccount.isUser2faEnabled()).thenReturn(true); |
| |
| boolean result = servlet.skip2FAcheckForUser(cuurentSession); |
| Assert.assertEquals(false, result); |
| } |
| |
| @Test |
| public void testDoNotSkip2FAcheckForUserWhen2FAMandated() { |
| servlet.accountMgr = accountMgr; |
| HttpSession cuurentSession = Mockito.mock(HttpSession.class); |
| Mockito.when(cuurentSession.getAttribute("userid")).thenReturn(1L); |
| Mockito.when(cuurentSession.getAttribute(ApiConstants.IS_2FA_VERIFIED)).thenReturn(false); |
| |
| Mockito.when(accountMgr.getUserAccountById(1L)).thenReturn(userAccount); |
| Mockito.when(userAccount.getDomainId()).thenReturn(1L); |
| Mockito.when(userAccount.isUser2faEnabled()).thenReturn(false); |
| |
| ConfigKey<Boolean> mandateUserTwoFactorAuthentication = Mockito.mock(ConfigKey.class); |
| AccountManagerImpl.mandateUserTwoFactorAuthentication = mandateUserTwoFactorAuthentication; |
| Mockito.when(mandateUserTwoFactorAuthentication.valueIn(1L)).thenReturn(false); |
| |
| boolean result = servlet.skip2FAcheckForUser(cuurentSession); |
| Assert.assertEquals(true, result); |
| } |
| |
| @Test |
| public void testSkip2FAcheckForUserWhen2FAisNotEnabledAndNotMandated() { |
| servlet.accountMgr = accountMgr; |
| HttpSession cuurentSession = Mockito.mock(HttpSession.class); |
| Mockito.when(cuurentSession.getAttribute("userid")).thenReturn(1L); |
| Mockito.when(cuurentSession.getAttribute(ApiConstants.IS_2FA_VERIFIED)).thenReturn(false); |
| |
| Mockito.when(accountMgr.getUserAccountById(1L)).thenReturn(userAccount); |
| Mockito.when(userAccount.getDomainId()).thenReturn(1L); |
| Mockito.when(userAccount.isUser2faEnabled()).thenReturn(false); |
| |
| ConfigKey<Boolean> enableUserTwoFactorAuthentication = Mockito.mock(ConfigKey.class); |
| AccountManagerImpl.enableUserTwoFactorAuthentication = enableUserTwoFactorAuthentication; |
| Mockito.when(enableUserTwoFactorAuthentication.valueIn(1L)).thenReturn(true); |
| |
| ConfigKey<Boolean> mandateUserTwoFactorAuthentication = Mockito.mock(ConfigKey.class); |
| AccountManagerImpl.mandateUserTwoFactorAuthentication = mandateUserTwoFactorAuthentication; |
| Mockito.when(mandateUserTwoFactorAuthentication.valueIn(1L)).thenReturn(true); |
| |
| boolean result = servlet.skip2FAcheckForUser(cuurentSession); |
| Assert.assertEquals(false, result); |
| } |
| |
| @Test |
| public void testVerify2FA() throws UnknownHostException { |
| String command = ValidateUserTwoFactorAuthenticationCodeCmd.APINAME; |
| Mockito.lenient().when(authenticator.authenticate(Mockito.anyString(), Mockito.anyMap(), Mockito.isA(HttpSession.class), |
| Mockito.same(InetAddress.getByName("127.0.0.1")), Mockito.anyString(), Mockito.isA(StringBuilder.class), Mockito.isA(HttpServletRequest.class), Mockito.isA(HttpServletResponse.class))).thenReturn("{\"Success\":{}"); |
| |
| StringBuilder auditTrailSb = new StringBuilder(); |
| Map<String, Object[]> params = new HashMap<String, Object[]>(); |
| String responseType = HttpUtils.RESPONSE_TYPE_XML; |
| boolean result = servlet.verify2FA(session, command, auditTrailSb, params, InetAddress.getByName("192.168.1.1"), |
| responseType, request, response); |
| |
| Assert.assertEquals(true, result); |
| } |
| |
| @Test |
| public void testVerify2FAWhenAuthenticatorNotFound() throws UnknownHostException { |
| String command = ValidateUserTwoFactorAuthenticationCodeCmd.APINAME; |
| Mockito.when(authManager.getAPIAuthenticator(command)).thenReturn(null); |
| StringBuilder auditTrailSb = new StringBuilder(); |
| Map<String, Object[]> params = new HashMap<String, Object[]>(); |
| String responseType = HttpUtils.RESPONSE_TYPE_XML; |
| boolean result = servlet.verify2FA(session, command, auditTrailSb, params, InetAddress.getByName("192.168.1.1"), |
| responseType, request, response); |
| |
| Assert.assertEquals(false, result); |
| } |
| |
| @Test |
| public void testVerify2FAWhenExpectedCommandIsNotCalled() throws UnknownHostException { |
| servlet.accountMgr = accountMgr; |
| String command = "listZones"; |
| Mockito.when(session.getAttribute("userid")).thenReturn(1L); |
| Mockito.when(accountMgr.getUserAccountById(1L)).thenReturn(userAccount); |
| Mockito.when(userAccount.isUser2faEnabled()).thenReturn(true); |
| |
| StringBuilder auditTrailSb = new StringBuilder(); |
| Map<String, Object[]> params = new HashMap<String, Object[]>(); |
| String responseType = HttpUtils.RESPONSE_TYPE_XML; |
| boolean result = servlet.verify2FA(session, command, auditTrailSb, params, InetAddress.getByName("192.168.1.1"), |
| responseType, request, response); |
| |
| Assert.assertEquals(false, result); |
| } |
| } |