Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions server/src/main/java/com/cloud/api/ApiServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,11 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp
if (!DefaultForgotPasswordAPIAuthenticatorCmd.APINAME.equalsIgnoreCase(command) || StringUtils.isNotBlank(username)) {
LOGGER.debug("Authentication failure: {}", e.getMessage());
}
} catch (RuntimeException e) {
if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGIN_API) {
invalidateHttpSession(session, "invalidating session after unexpected login failure");
}
throw e;
}

if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGOUT_API) {
Expand Down Expand Up @@ -426,6 +431,11 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp
} catch (final Exception ex) {
LOGGER.error("unknown exception writing api response", ex);
auditTrailSb.append(" unknown exception writing api response");
if (!resp.isCommitted()) {
final String serializedResponseText = apiServer.getSerializedApiError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Internal error processing the request", params, responseType);
HttpUtils.writeHttpResponse(resp, serializedResponseText, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, responseType, ApiServer.JSONContentType.value());
}
} finally {
ACCESSLOGGER.info(auditTrailSb.toString());
if (LOGGER.isDebugEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,10 @@ public String authenticate(String command, Map<String, Object[]> params, HttpSes
}
serializedResponse = ApiResponseSerializer.toSerializedString(_apiServer.loginUser(session, username[0], pwd, domainId, domain, remoteAddress, params),
responseType);
userDetailsDao.removeDetail(userAccount.getId(), UserDetailVO.OauthLogin);
final Long userId = (Long)session.getAttribute("userid");
if (userId != null) {
userDetailsDao.removeDetail(userId, UserDetailVO.OauthLogin);
}
return serializedResponse;
} catch (final CloudAuthenticationException ex) {
ApiServlet.invalidateHttpSession(session, "fall through to API key,");
Expand Down
25 changes: 25 additions & 0 deletions server/src/test/java/com/cloud/api/ApiServletTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,31 @@ public void processRequestInContextLogin() throws UnknownHostException {
Mockito.eq(InetAddress.getByName("127.0.0.1")), Mockito.anyString(), Mockito.isA(StringBuilder.class), Mockito.isA(HttpServletRequest.class), Mockito.isA(HttpServletResponse.class));
}

@SuppressWarnings("unchecked")
@Test
public void processRequestInContextLoginUnexpectedExceptionInvalidatesSessionAndReturnsError() throws UnknownHostException {
Mockito.when(request.getMethod()).thenReturn("POST");
Mockito.when(request.getSession(false)).thenReturn(null);
Mockito.when(request.getSession(true)).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" });
Mockito.when(request.getParameterMap()).thenReturn(params);
Mockito.when(authenticator.getAPIType()).thenReturn(APIAuthenticationType.LOGIN_API);
Mockito.when(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)))
.thenThrow(new NullPointerException("test"));
Mockito.when(apiServer.getSerializedApiError(Mockito.eq(HttpServletResponse.SC_INTERNAL_SERVER_ERROR), Mockito.anyString(), Mockito.anyMap(), Mockito.anyString()))
.thenReturn("{\"errorresponse\":{}}");

servlet.processRequestInContext(request, response);

Mockito.verify(session).invalidate();
Mockito.verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
Assert.assertEquals("{\"errorresponse\":{}}", responseWriter.toString());
}

@Test
public void getClientAddressWithXForwardedFor() throws UnknownHostException {
setConfigValue("proxy.header.verify", "true");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// 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.auth;

import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiServerService;
import org.apache.cloudstack.api.ResponseObject;
import org.apache.cloudstack.resourcedetail.UserDetailVO;
import org.apache.cloudstack.resourcedetail.dao.UserDetailsDao;
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.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;

import com.cloud.api.response.ApiResponseSerializer;
import com.cloud.domain.Domain;
import com.cloud.user.AccountService;
import com.cloud.user.DomainService;
import com.cloud.user.UserAccount;

@RunWith(MockitoJUnitRunner.class)
public class DefaultLoginAPIAuthenticatorCmdTest {

private static final String USERNAME = "user@example.com";
private static final long DOMAIN_ID = 1L;
private static final long USER_ID = 42L;
private static final String SERIALIZED_RESPONSE = "{\"loginresponse\":{}}";

@Mock
ApiServerService apiServer;
@Mock
UserDetailsDao userDetailsDao;
@Mock
AccountService accountService;
@Mock
DomainService domainService;
@Mock
HttpSession session;
@Mock
HttpServletRequest req;
@Mock
HttpServletResponse resp;
@Mock
Domain domain;
@Mock
ResponseObject loginResponse;

DefaultLoginAPIAuthenticatorCmd cmd = new DefaultLoginAPIAuthenticatorCmd();

private MockedStatic<ApiResponseSerializer> serializerMock;
private Map<String, Object[]> params;

@Before
public void setUp() throws Exception {
cmd._apiServer = apiServer;
cmd._accountService = accountService;
cmd._domainService = domainService;
cmd.userDetailsDao = userDetailsDao;

params = new HashMap<>();
params.put(ApiConstants.USERNAME, new String[] {USERNAME});
params.put(ApiConstants.PASSWORD, new String[] {"password"});
params.put(ApiConstants.DOMAIN, new String[] {"/"});

Mockito.when(req.getMethod()).thenReturn("POST");
Mockito.when(domain.getId()).thenReturn(DOMAIN_ID);
Mockito.when(domainService.findDomainByIdOrPath(Mockito.any(), Mockito.anyString())).thenReturn(domain);
Mockito.when(apiServer.loginUser(Mockito.eq(session), Mockito.eq(USERNAME), Mockito.anyString(), Mockito.eq(DOMAIN_ID),
Mockito.anyString(), Mockito.any(), Mockito.any())).thenReturn(loginResponse);
Mockito.when(session.getAttribute("userid")).thenReturn(USER_ID);

serializerMock = Mockito.mockStatic(ApiResponseSerializer.class);
serializerMock.when(() -> ApiResponseSerializer.toSerializedString(loginResponse, "json")).thenReturn(SERIALIZED_RESPONSE);
}

@After
public void tearDown() {
serializerMock.close();
}

private String authenticate() throws Exception {
return cmd.authenticate("login", params, session, InetAddress.getLoopbackAddress(), "json", new StringBuilder(), req, resp);
}

@Test
public void authenticateUserCreatedDuringLoginReturnsLoginResponse() throws Exception {
Mockito.when(accountService.getActiveUserAccount(USERNAME, DOMAIN_ID)).thenReturn(null);

Assert.assertEquals(SERIALIZED_RESPONSE, authenticate());
Mockito.verify(userDetailsDao).removeDetail(USER_ID, UserDetailVO.OauthLogin);
}

@Test
public void authenticateExistingUserRemovesOauthLoginDetail() throws Exception {
UserAccount userAccount = Mockito.mock(UserAccount.class);
Mockito.when(accountService.getActiveUserAccount(USERNAME, DOMAIN_ID)).thenReturn(userAccount);

Assert.assertEquals(SERIALIZED_RESPONSE, authenticate());
Mockito.verify(userDetailsDao).removeDetail(USER_ID, UserDetailVO.OauthLogin);
}
}
17 changes: 16 additions & 1 deletion ui/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,22 @@ vueApp.use(backupUtilPlugin)
vueApp.use(extensions)
vueApp.use(directives)

vueProps.$localStorage.keys().forEach(key => {
try {
vueProps.$localStorage.get(key)
} catch (e) {
console.warn('Removing unreadable local storage entry: ' + key)
vueProps.$localStorage.remove(key)
}
})

let configLoaded = false

const renderError = (err) => {
console.error('Fatal error during app initialization: ', err)
const details = configLoaded
? 'The application failed to start: ' + err + '. Check the browser console for more information.'
: 'Missing or malformed config.json. Please ensure the file is present, accessible, and contains valid JSON. Check the browser console for more information.'
const ErrorComponent = {
render: () => h(
'div',
Expand All @@ -81,7 +95,7 @@ const renderError = (err) => {
h('summary', { style: 'cursor: pointer;' }, 'Technical details'),
h('pre', {
style: 'text-align: left; display: inline-block; margin-top: 10px;'
}, 'Missing or malformed config.json. Please ensure the file is present, accessible, and contains valid JSON. Check the browser console for more information.')
}, details)
])
]
)
Expand All @@ -97,6 +111,7 @@ fetch('config.json?ts=' + Date.now())
return response.json()
})
.then(async config => {
configLoaded = true
vueProps.$config = config
let baseUrl = config.apiBase
if (config.multipleServer) {
Expand Down
5 changes: 4 additions & 1 deletion ui/src/store/modules/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ function setLoginCookies (result) {
}

function processLoginResponse (commit, response) {
const result = response.loginresponse || {}
const result = response?.loginresponse || {}
if (!result.sessionkey) {
throw new Error('Login response not valid.')
}
setLoginCookies(result)
vueProps.$localStorage.set(ACCESS_TOKEN, result.sessionkey, 24 * 60 * 60 * 1000)
commit('SET_TOKEN', result.sessionkey)
Expand Down
Loading