| // 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); | |
| } | |
| } |