From e71b05e1970542713ff37f7ba8c91195580fffa2 Mon Sep 17 00:00:00 2001 From: Bhargavi Karampudi Date: Thu, 12 Oct 2023 12:59:16 -0500 Subject: [PATCH 1/4] AC-143 NewApi end point to impersonate user --- .../cloudstack/api/ApiServerService.java | 2 + .../api/response/LoginCmdResponse.java | 9 + .../management/MockAccountManager.java | 5 + server/src/com/cloud/api/ApiServer.java | 90 +++++++++ .../cloud/api/auth/ImpersonateUserCmd.java | 186 ++++++++++++++++++ .../cloud/server/ManagementServerImpl.java | 4 + server/src/com/cloud/user/AccountManager.java | 3 + .../com/cloud/user/AccountManagerImpl.java | 46 +++++ .../cloud/user/MockAccountManagerImpl.java | 5 + 9 files changed, 350 insertions(+) create mode 100644 server/src/com/cloud/api/auth/ImpersonateUserCmd.java diff --git a/api/src/org/apache/cloudstack/api/ApiServerService.java b/api/src/org/apache/cloudstack/api/ApiServerService.java index 382b48a5e026..c67aeee643ad 100644 --- a/api/src/org/apache/cloudstack/api/ApiServerService.java +++ b/api/src/org/apache/cloudstack/api/ApiServerService.java @@ -31,6 +31,8 @@ public interface ApiServerService { public ResponseObject loginUser(HttpSession session, String username, String password, Long domainId, String domainPath, InetAddress loginIpAddress, Map requestParameters) throws CloudAuthenticationException; + public ResponseObject impersonateUser(HttpSession session, String username, Long domainId, String domainPath, InetAddress loginIpAddress, Map requestParameters) throws CloudAuthenticationException; + public void logoutUser(long userId); public boolean verifyUser(Long userId); diff --git a/api/src/org/apache/cloudstack/api/response/LoginCmdResponse.java b/api/src/org/apache/cloudstack/api/response/LoginCmdResponse.java index d2d122efb665..28bafde2ce1d 100644 --- a/api/src/org/apache/cloudstack/api/response/LoginCmdResponse.java +++ b/api/src/org/apache/cloudstack/api/response/LoginCmdResponse.java @@ -69,6 +69,9 @@ public class LoginCmdResponse extends AuthenticationCmdResponse { @SerializedName(value = ApiConstants.SESSIONKEY) @Param(description = "Session key that can be passed in subsequent Query command calls", isSensitive = true) private String sessionKey; + @SerializedName("cookie") + @Param(description = "Cookie value") + private String cookie; public String getUsername() { return username; @@ -163,4 +166,10 @@ public String getSessionKey() { public void setSessionKey(String sessionKey) { this.sessionKey = sessionKey; } + + public String getCookie(){ return cookie; } + + public void setCookie(String cookie) { + this.cookie = cookie; + } } diff --git a/plugins/network-elements/juniper-contrail/test/org/apache/cloudstack/network/contrail/management/MockAccountManager.java b/plugins/network-elements/juniper-contrail/test/org/apache/cloudstack/network/contrail/management/MockAccountManager.java index 37ca2bccff48..b448e2ee6a1d 100644 --- a/plugins/network-elements/juniper-contrail/test/org/apache/cloudstack/network/contrail/management/MockAccountManager.java +++ b/plugins/network-elements/juniper-contrail/test/org/apache/cloudstack/network/contrail/management/MockAccountManager.java @@ -261,6 +261,11 @@ public UserAccount authenticateUser(String arg0, String arg1, Long arg2, InetAdd return null; } + @Override + public UserAccount impersonateUser(String username, Long domainId, InetAddress loginIpAddress, Map requestParameters) { + return null; + } + @Override public void buildACLSearchBuilder( SearchBuilder arg0, Long arg1, diff --git a/server/src/com/cloud/api/ApiServer.java b/server/src/com/cloud/api/ApiServer.java index a97984a2be21..92cf6fb2b85c 100644 --- a/server/src/com/cloud/api/ApiServer.java +++ b/server/src/com/cloud/api/ApiServer.java @@ -994,6 +994,15 @@ private ResponseObject createLoginResponse(HttpSession session) { session.removeAttribute("user_UUID"); session.removeAttribute("domain_UUID"); } + String username = null; + String account = null; + String firstname = null; + String lastname = null; + String type = null; + String timezone = null; + String timezoneoffset = null; + String registered = null; + String sessionkey = null; final Enumeration attrNames = session.getAttributeNames(); if (attrNames != null) { @@ -1002,32 +1011,43 @@ private ResponseObject createLoginResponse(HttpSession session) { final Object attrObj = session.getAttribute(attrName); if (ApiConstants.USERNAME.equalsIgnoreCase(attrName)) { response.setUsername(attrObj.toString()); + username = attrObj.toString(); } if (ApiConstants.ACCOUNT.equalsIgnoreCase(attrName)) { response.setAccount(attrObj.toString()); + account = attrObj.toString(); } if (ApiConstants.FIRSTNAME.equalsIgnoreCase(attrName)) { response.setFirstName(attrObj.toString()); + firstname = attrObj.toString(); } if (ApiConstants.LASTNAME.equalsIgnoreCase(attrName)) { response.setLastName(attrObj.toString()); + lastname = attrObj.toString(); } if (ApiConstants.TYPE.equalsIgnoreCase(attrName)) { response.setType((attrObj.toString())); + type = attrObj.toString(); } if (ApiConstants.TIMEZONE.equalsIgnoreCase(attrName)) { response.setTimeZone(attrObj.toString()); + timezone = attrObj.toString(); } if (ApiConstants.TIMEZONEOFFSET.equalsIgnoreCase(attrName)) { response.setTimeZoneOffset(attrObj.toString()); + timezoneoffset = attrObj.toString(); } if (ApiConstants.REGISTERED.equalsIgnoreCase(attrName)) { response.setRegistered(attrObj.toString()); + registered = attrObj.toString(); } if (ApiConstants.SESSIONKEY.equalsIgnoreCase(attrName)) { response.setSessionKey(attrObj.toString()); + sessionkey = attrObj.toString(); } } + response.setCookie(String.format("JSESSIONID=%s;account=%s;domainid=%s;role=%s;sessionkey=%s;timezone=%s;timezoneoffset=%s;userfullname=%s;userid=%s;username=%s", + session.getId(), account, domain_UUID, type, sessionkey, timezone, timezoneoffset, firstname + " " + lastname, user_UUID, username)); } response.setResponseName("loginresponse"); return response; @@ -1103,6 +1123,76 @@ public ResponseObject loginUser(final HttpSession session, final String username throw new CloudAuthenticationException("Failed to authenticate user " + username + " in domain " + domainId + "; please provide valid credentials"); } + @Override + public ResponseObject impersonateUser(final HttpSession session, final String username, Long domainId, final String domainPath, final InetAddress loginIpAddress, + final Map requestParameters) throws CloudAuthenticationException { + // We will always use domainId first. If that does not exist, we will use domain name. If THAT doesn't exist + // we will default to ROOT + final Domain userDomain = domainMgr.findDomainByIdOrPath(domainId, domainPath); + if (userDomain == null || userDomain.getId() < 1L) { + throw new CloudAuthenticationException("Unable to find the domain from the path " + domainPath); + } else { + domainId = userDomain.getId(); + } + + final UserAccount userAcct = accountMgr.impersonateUser(username, domainId, loginIpAddress, requestParameters); + if (userAcct != null) { + final String timezone = userAcct.getTimezone(); + float offsetInHrs = 0f; + if (timezone != null) { + final TimeZone t = TimeZone.getTimeZone(timezone); + s_logger.info("Current user logged in under " + timezone + " timezone"); + + final java.util.Date date = new java.util.Date(); + final long longDate = date.getTime(); + final float offsetInMs = (t.getOffset(longDate)); + offsetInHrs = offsetInMs / (1000 * 60 * 60); + s_logger.info("Timezone offset from UTC is: " + offsetInHrs); + } + + final Account account = accountMgr.getAccount(userAcct.getAccountId()); + + // set the userId and account object for everyone + session.setAttribute("userid", userAcct.getId()); + final UserVO user = (UserVO)accountMgr.getActiveUser(userAcct.getId()); + if (user.getUuid() != null) { + session.setAttribute("user_UUID", user.getUuid()); + } + + session.setAttribute("username", userAcct.getUsername()); + session.setAttribute("firstname", userAcct.getFirstname()); + session.setAttribute("lastname", userAcct.getLastname()); + session.setAttribute("accountobj", account); + session.setAttribute("account", account.getAccountName()); + + session.setAttribute("domainid", account.getDomainId()); + final DomainVO domain = (DomainVO)domainMgr.getDomain(account.getDomainId()); + if (domain.getUuid() != null) { + session.setAttribute("domain_UUID", domain.getUuid()); + } + + session.setAttribute("type", Short.valueOf(account.getType()).toString()); + session.setAttribute("registrationtoken", userAcct.getRegistrationToken()); + session.setAttribute("registered", Boolean.toString(userAcct.isRegistered())); + + if (timezone != null) { + session.setAttribute("timezone", timezone); + session.setAttribute("timezoneoffset", Float.valueOf(offsetInHrs).toString()); + } + + // (bug 5483) generate a session key that the user must submit on every request to prevent CSRF, add that + // to the login response so that session-based authenticators know to send the key back + final SecureRandom sesssionKeyRandom = new SecureRandom(); + final byte sessionKeyBytes[] = new byte[20]; + sesssionKeyRandom.nextBytes(sessionKeyBytes); + final String sessionKey = Base64.encodeBase64URLSafeString(sessionKeyBytes); + session.setAttribute(ApiConstants.SESSIONKEY, sessionKey); + + return createLoginResponse(session); + } + throw new CloudAuthenticationException("Failed to authenticate user " + username + " in domain " + domainId + "; please provide valid credentials"); + } + @Override public void logoutUser(final long userId) { accountMgr.logoutUser(userId); diff --git a/server/src/com/cloud/api/auth/ImpersonateUserCmd.java b/server/src/com/cloud/api/auth/ImpersonateUserCmd.java new file mode 100644 index 000000000000..040d2d2fe191 --- /dev/null +++ b/server/src/com/cloud/api/auth/ImpersonateUserCmd.java @@ -0,0 +1,186 @@ +// 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 com.cloud.domain.Domain; +import com.cloud.user.User; +import com.cloud.user.UserAccount; +import org.apache.cloudstack.api.ApiServerService; +import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.exception.CloudAuthenticationException; +import com.cloud.user.Account; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.auth.APIAuthenticator; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.api.response.LoginCmdResponse; +import org.apache.log4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.List; +import java.util.Map; +import java.net.InetAddress; + +@APICommand(name = "impersonateUser", description = "Logs a user into the CloudStack. A successful login attempt will generate a JSESSIONID cookie value that can be passed in subsequent Query command calls until the \"logout\" command has been issued or the session has expired.", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {}) +public class ImpersonateUserCmd extends BaseCmd implements APIAuthenticator { + + public static final Logger s_logger = Logger.getLogger(ImpersonateUserCmd.class.getName()); + private static final String s_name = "loginresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.USERNAME, type = CommandType.STRING, description = "Username", required = true) + private String username; + + @Parameter(name = ApiConstants.DOMAIN, type = CommandType.STRING, description = "Path of the domain that the user belongs to. Example: domain=/com/cloud/internal. If no domain is passed in, the ROOT (/) domain is assumed.") + private String domain; + + @Parameter(name = ApiConstants.DOMAIN__ID, type = CommandType.LONG, description = "The id of the domain that the user belongs to. If both domain and domainId are passed in, \"domainId\" parameter takes precendence") + private Long domainId; + + @Inject + ApiServerService _apiServer; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getUsername() { + return username; + } + + public String getDomain() { + return domain; + } + + public Long getDomainId() { + return domainId; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_TYPE_NORMAL; + } + + @Override + public void execute() throws ServerApiException { + // We should never reach here + throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); + } + + @Override + public String authenticate(String command, Map params, HttpSession session, InetAddress remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException { + // FIXME: ported from ApiServlet, refactor and cleanup + final String[] username = (String[])params.get(ApiConstants.USERNAME); + String[] domainIdArr = (String[])params.get(ApiConstants.DOMAIN_ID); + + if (domainIdArr == null) { + domainIdArr = (String[])params.get(ApiConstants.DOMAIN__ID); + } + final String[] domainName = (String[])params.get(ApiConstants.DOMAIN); + Long domainId = null; + if ((domainIdArr != null) && (domainIdArr.length > 0)) { + try { + //check if UUID is passed in for domain + domainId = _apiServer.fetchDomainId(domainIdArr[0]); + if (domainId == null) { + domainId = Long.parseLong(domainIdArr[0]); + } + auditTrailSb.append(" domainid=" + domainId);// building the params for POST call + } catch (final NumberFormatException e) { + s_logger.warn("Invalid domain id entered by user"); + auditTrailSb.append(" " + HttpServletResponse.SC_UNAUTHORIZED + " " + "Invalid domain id entered, please enter a valid one"); + throw new ServerApiException(ApiErrorCode.UNAUTHORIZED, + _apiServer.getSerializedApiError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid domain id entered, please enter a valid one", params, + responseType)); + } + } + + String domain = null; + if (domainName != null) { + domain = domainName[0]; + auditTrailSb.append(" domain=" + domain); + if (domain != null) { + // ensure domain starts with '/' and ends with '/' + if (!domain.endsWith("/")) { + domain += '/'; + } + if (!domain.startsWith("/")) { + domain = "/" + domain; + } + } + } + + String serializedResponse = null; + if (username != null) { + try { + final Domain userDomain = _domainService.findDomainByIdOrPath(domainId, domain); + if (userDomain != null) { + domainId = userDomain.getId(); + } else { + throw new CloudAuthenticationException("Unable to find the domain from the path " + domain); + } + final UserAccount userAccount = _accountService.getActiveUserAccount(username[0], domainId); + if (userAccount != null && User.Source.SAML2 == userAccount.getSource()) { + throw new CloudAuthenticationException("User is not allowed CloudStack login"); + } + return ApiResponseSerializer.toSerializedString(_apiServer.impersonateUser(session, username[0], domainId, domain, remoteAddress, params), + responseType); + } catch (final CloudAuthenticationException ex) { + // TODO: fall through to API key, or just fail here w/ auth error? (HTTP 401) + try { + session.invalidate(); + } catch (final IllegalStateException ise) { + } + auditTrailSb.append(" " + ApiErrorCode.ACCOUNT_ERROR + " " + ex.getMessage() != null ? ex.getMessage() + : "failed to authenticate user, check if username/password are correct"); + serializedResponse = + _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), ex.getMessage() != null ? ex.getMessage() + : "failed to authenticate user, check if username/password are correct", params, responseType); + } + } + // We should not reach here and if we do we throw an exception + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, serializedResponse); + } + + @Override + public APIAuthenticationType getAPIType() { + return APIAuthenticationType.LOGIN_API; + } + + @Override + public void setAuthenticators(List authenticators) { + } +} diff --git a/server/src/com/cloud/server/ManagementServerImpl.java b/server/src/com/cloud/server/ManagementServerImpl.java index a7dfc205b735..37033d40b0ef 100644 --- a/server/src/com/cloud/server/ManagementServerImpl.java +++ b/server/src/com/cloud/server/ManagementServerImpl.java @@ -37,6 +37,9 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; + +import com.cloud.api.auth.ImpersonateUserCmd; + import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.affinity.AffinityGroupProcessor; import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; @@ -3096,6 +3099,7 @@ public List> getCommands() { cmdList.add(IssueOutOfBandManagementPowerActionCmd.class); cmdList.add(ChangeOutOfBandManagementPasswordCmd.class); cmdList.add(GetUserKeysCmd.class); + cmdList.add(ImpersonateUserCmd.class); return cmdList; } diff --git a/server/src/com/cloud/user/AccountManager.java b/server/src/com/cloud/user/AccountManager.java index e708b040ed92..b9b42a5a1f84 100644 --- a/server/src/com/cloud/user/AccountManager.java +++ b/server/src/com/cloud/user/AccountManager.java @@ -77,6 +77,9 @@ public interface AccountManager extends AccountService, Configurable{ */ UserAccount authenticateUser(String username, String password, Long domainId, InetAddress loginIpAddress, Map requestParameters); + + UserAccount impersonateUser(String username, Long domainId, InetAddress loginIpAddress, Map requestParameters); + /** * Locate a user by their apiKey * diff --git a/server/src/com/cloud/user/AccountManagerImpl.java b/server/src/com/cloud/user/AccountManagerImpl.java index 9129976900d3..bed7c116e053 100644 --- a/server/src/com/cloud/user/AccountManagerImpl.java +++ b/server/src/com/cloud/user/AccountManagerImpl.java @@ -2248,6 +2248,52 @@ public UserAccount authenticateUser(final String username, final String password } } + @Override + public UserAccount impersonateUser(final String username, final Long domainId, final InetAddress loginIpAddress, final Map + requestParameters) { + UserAccount user = _userAccountDao.getUserAccount(username, domainId); + if (user != null) { + // don't allow to authenticate system user + if (user.getId() == User.UID_SYSTEM) { + s_logger.error("Failed to authenticate user: " + username + " in domain " + domainId); + return null; + } + // don't allow baremetal system user + if (BaremetalUtils.BAREMETAL_SYSTEM_ACCOUNT_NAME.equals(user.getUsername())) { + s_logger.error("Won't authenticate user: " + username + " in domain " + domainId); + return null; + } + // We authenticated successfully by now, let's check if we are allowed to login from the ip address the reqest comes from + final Account account = _accountMgr.getAccount(user.getAccountId()); + final DomainVO domain = (DomainVO) _domainMgr.getDomain(account.getDomainId()); + // Get the CIDRs from where this account is allowed to make calls + final String accessAllowedCidrs = ApiServiceConfiguration.ApiAllowedSourceCidrList.valueIn(account.getId()).replaceAll("\\s",""); + final Boolean ApiSourceCidrChecksEnabled = ApiServiceConfiguration.ApiSourceCidrChecksEnabled.value(); + if (ApiSourceCidrChecksEnabled) { + s_logger.debug("CIDRs from which account '" + account.toString() + "' is allowed to perform API calls: " + accessAllowedCidrs); + // Block when is not in the list of allowed IPs + if (!NetUtils.isIpInCidrList(loginIpAddress, accessAllowedCidrs.split(","))) { + s_logger.warn("Request by account '" + account.toString() + "' was denied since " + loginIpAddress.toString().replaceAll("/","") + + " does not match " + accessAllowedCidrs); + throw new CloudAuthenticationException("Failed to authenticate user '" + username + "' in domain '" + domain.getPath() + "' from ip " + + loginIpAddress.toString().replaceAll("/","") + "; please provide valid credentials"); + } + } + // Here all is fine! + if (s_logger.isDebugEnabled()) { + s_logger.debug("User: " + username + " in domain " + domainId + " has successfully logged in"); + } + ActionEventUtils.onActionEvent(user.getId(), user.getAccountId(), user.getDomainId(), EventTypes.EVENT_USER_LOGIN, + "user has logged in from IP Address " + loginIpAddress); + return user; + } else { + if (s_logger.isDebugEnabled()) { + s_logger.debug("User: " + username + " in domain " + domainId + " has failed to log in"); + } + return null; + } + } + private UserAccount getUserAccount(String username, String password, Long domainId, Map requestParameters) { if (s_logger.isDebugEnabled()) { s_logger.debug("Attempting to log in user: " + username + " in domain " + domainId); diff --git a/server/test/com/cloud/user/MockAccountManagerImpl.java b/server/test/com/cloud/user/MockAccountManagerImpl.java index 0a92c1446dbc..97b91d47a8cc 100644 --- a/server/test/com/cloud/user/MockAccountManagerImpl.java +++ b/server/test/com/cloud/user/MockAccountManagerImpl.java @@ -278,6 +278,11 @@ public UserAccount authenticateUser(String username, String password, Long domai return null; } + @Override + public UserAccount impersonateUser(String username, Long domainId, InetAddress loginIpAddress, Map requestParameters) { + return null; + } + @Override public Pair findUserByApiKey(String apiKey) { return null; From 5898f42f8667d293478f2d2ccab23e507e7f6e2d Mon Sep 17 00:00:00 2001 From: Bhargavi Karampudi Date: Thu, 26 Oct 2023 15:23:17 -0500 Subject: [PATCH 2/4] API Access to admin changes --- server/src/com/cloud/api/ApiServlet.java | 13 +++++++++++++ .../api/auth/APIAuthenticationManagerImpl.java | 1 + .../src/com/cloud/api/auth/ImpersonateUserCmd.java | 1 + 3 files changed, 15 insertions(+) diff --git a/server/src/com/cloud/api/ApiServlet.java b/server/src/com/cloud/api/ApiServlet.java index 4002ff8d99b1..c85a68a16ed3 100644 --- a/server/src/com/cloud/api/ApiServlet.java +++ b/server/src/com/cloud/api/ApiServlet.java @@ -41,6 +41,7 @@ import org.apache.cloudstack.api.auth.APIAuthenticationManager; import org.apache.cloudstack.api.auth.APIAuthenticationType; import org.apache.cloudstack.api.auth.APIAuthenticator; +import com.cloud.api.auth.ImpersonateUserCmd; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.managed.context.ManagedContext; import org.apache.log4j.Logger; @@ -185,6 +186,18 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp final Object[] commandObj = params.get(ApiConstants.COMMAND); if (commandObj != null) { final String command = (String) commandObj[0]; + if (command.equals(ImpersonateUserCmd.APINAME)) { + final Long userId = (Long) session.getAttribute("userid"); + final User user = ApiDBUtils.findUserById(userId); + final Account account = ApiDBUtils.findAccountById(user.getAccountId()); + if (account.getType() != (Account.ACCOUNT_TYPE_ADMIN)) { + s_logger.info(String.format("User does not have permission to access the API")); + auditTrailSb.append(" " + HttpServletResponse.SC_UNAUTHORIZED + " " + "User does not have permission to access the API"); + final String serializedResponse = apiServer.getSerializedApiError(HttpServletResponse.SC_UNAUTHORIZED, "User does not have permission to access the API", params, responseType); + HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_UNAUTHORIZED, responseType, ApiServer.JSONcontentType.value()); + return; + } + } APIAuthenticator apiAuthenticator = authManager.getAPIAuthenticator(command); if (apiAuthenticator != null) { diff --git a/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java b/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java index c304a10d917f..766d184f3167 100644 --- a/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java +++ b/server/src/com/cloud/api/auth/APIAuthenticationManagerImpl.java @@ -77,6 +77,7 @@ public List> getCommands() { List> cmdList = new ArrayList>(); cmdList.add(DefaultLoginAPIAuthenticatorCmd.class); cmdList.add(DefaultLogoutAPIAuthenticatorCmd.class); + cmdList.add(ImpersonateUserCmd.class); for (PluggableAPIAuthenticator apiAuthenticator: _apiAuthenticators) { List> commands = apiAuthenticator.getAuthCommands(); if (commands != null) { diff --git a/server/src/com/cloud/api/auth/ImpersonateUserCmd.java b/server/src/com/cloud/api/auth/ImpersonateUserCmd.java index 040d2d2fe191..bfec4dee43cf 100644 --- a/server/src/com/cloud/api/auth/ImpersonateUserCmd.java +++ b/server/src/com/cloud/api/auth/ImpersonateUserCmd.java @@ -47,6 +47,7 @@ public class ImpersonateUserCmd extends BaseCmd implements APIAuthenticator { public static final Logger s_logger = Logger.getLogger(ImpersonateUserCmd.class.getName()); + public static final String APINAME = "impersonateUser"; private static final String s_name = "loginresponse"; ///////////////////////////////////////////////////// From 8231553e7b916f95558ec8db5e61ad02f97bad18 Mon Sep 17 00:00:00 2001 From: bkarampudi Date: Wed, 29 Nov 2023 15:55:58 -0500 Subject: [PATCH 3/4] PR comment fixes --- .../cloudstack/api/response/LoginCmdResponse.java | 12 ++++++++++++ server/src/com/cloud/api/ApiServlet.java | 2 +- .../src/com/cloud/api/auth/ImpersonateUserCmd.java | 11 +++++++++-- server/src/com/cloud/user/AccountManagerImpl.java | 14 +------------- server/src/com/cloud/user/DomainManagerImpl.java | 2 +- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/api/src/org/apache/cloudstack/api/response/LoginCmdResponse.java b/api/src/org/apache/cloudstack/api/response/LoginCmdResponse.java index 28bafde2ce1d..e8d980394893 100644 --- a/api/src/org/apache/cloudstack/api/response/LoginCmdResponse.java +++ b/api/src/org/apache/cloudstack/api/response/LoginCmdResponse.java @@ -73,6 +73,10 @@ public class LoginCmdResponse extends AuthenticationCmdResponse { @Param(description = "Cookie value") private String cookie; + @SerializedName("description") + @Param(description = "Description for the response") + private String description; + public String getUsername() { return username; } @@ -172,4 +176,12 @@ public void setSessionKey(String sessionKey) { public void setCookie(String cookie) { this.cookie = cookie; } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } } diff --git a/server/src/com/cloud/api/ApiServlet.java b/server/src/com/cloud/api/ApiServlet.java index c85a68a16ed3..c4de33da9046 100644 --- a/server/src/com/cloud/api/ApiServlet.java +++ b/server/src/com/cloud/api/ApiServlet.java @@ -207,7 +207,7 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp int httpResponseCode = HttpServletResponse.SC_OK; String responseString = null; - if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGIN_API) { + if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGIN_API && !command.equals(ImpersonateUserCmd.APINAME)) { if (session != null) { try { session.invalidate(); diff --git a/server/src/com/cloud/api/auth/ImpersonateUserCmd.java b/server/src/com/cloud/api/auth/ImpersonateUserCmd.java index bfec4dee43cf..e13730b6b1cc 100644 --- a/server/src/com/cloud/api/auth/ImpersonateUserCmd.java +++ b/server/src/com/cloud/api/auth/ImpersonateUserCmd.java @@ -151,12 +151,19 @@ public String authenticate(String command, Map params, HttpSes if (userDomain != null) { domainId = userDomain.getId(); } else { - throw new CloudAuthenticationException("Unable to find the domain from the path " + domain); + LoginCmdResponse response = new LoginCmdResponse(); + response.setResponseName("loginresponse"); + response.setDescription("Unable to find the domain from the path " + domain); + return ApiResponseSerializer.toSerializedString(response, responseType); } final UserAccount userAccount = _accountService.getActiveUserAccount(username[0], domainId); if (userAccount != null && User.Source.SAML2 == userAccount.getSource()) { - throw new CloudAuthenticationException("User is not allowed CloudStack login"); + LoginCmdResponse response = new LoginCmdResponse(); + response.setResponseName("loginresponse"); + response.setDescription("User is not allowed CloudStack login"); + return ApiResponseSerializer.toSerializedString(response, responseType); } + return ApiResponseSerializer.toSerializedString(_apiServer.impersonateUser(session, username[0], domainId, domain, remoteAddress, params), responseType); } catch (final CloudAuthenticationException ex) { diff --git a/server/src/com/cloud/user/AccountManagerImpl.java b/server/src/com/cloud/user/AccountManagerImpl.java index bed7c116e053..96884f457c99 100644 --- a/server/src/com/cloud/user/AccountManagerImpl.java +++ b/server/src/com/cloud/user/AccountManagerImpl.java @@ -2266,19 +2266,7 @@ public UserAccount impersonateUser(final String username, final Long domainId, f // We authenticated successfully by now, let's check if we are allowed to login from the ip address the reqest comes from final Account account = _accountMgr.getAccount(user.getAccountId()); final DomainVO domain = (DomainVO) _domainMgr.getDomain(account.getDomainId()); - // Get the CIDRs from where this account is allowed to make calls - final String accessAllowedCidrs = ApiServiceConfiguration.ApiAllowedSourceCidrList.valueIn(account.getId()).replaceAll("\\s",""); - final Boolean ApiSourceCidrChecksEnabled = ApiServiceConfiguration.ApiSourceCidrChecksEnabled.value(); - if (ApiSourceCidrChecksEnabled) { - s_logger.debug("CIDRs from which account '" + account.toString() + "' is allowed to perform API calls: " + accessAllowedCidrs); - // Block when is not in the list of allowed IPs - if (!NetUtils.isIpInCidrList(loginIpAddress, accessAllowedCidrs.split(","))) { - s_logger.warn("Request by account '" + account.toString() + "' was denied since " + loginIpAddress.toString().replaceAll("/","") - + " does not match " + accessAllowedCidrs); - throw new CloudAuthenticationException("Failed to authenticate user '" + username + "' in domain '" + domain.getPath() + "' from ip " - + loginIpAddress.toString().replaceAll("/","") + "; please provide valid credentials"); - } - } + // Here all is fine! if (s_logger.isDebugEnabled()) { s_logger.debug("User: " + username + " in domain " + domainId + " has successfully logged in"); diff --git a/server/src/com/cloud/user/DomainManagerImpl.java b/server/src/com/cloud/user/DomainManagerImpl.java index 60c48fa574e8..583e53e144de 100644 --- a/server/src/com/cloud/user/DomainManagerImpl.java +++ b/server/src/com/cloud/user/DomainManagerImpl.java @@ -236,7 +236,7 @@ public DomainVO findDomainByPath(String domainPath) { public Domain findDomainByIdOrPath(final Long id, final String domainPath) { Long domainId = id; if (domainId == null || domainId < 1L) { - if (Strings.isNullOrEmpty(domainPath) || domainPath.trim().isEmpty()) { + if (Strings.isNullOrEmpty(domainPath) || domainPath.trim().isEmpty() || domainPath.trim().equalsIgnoreCase("/ROOT/")) { domainId = Domain.ROOT_DOMAIN; } else { final Domain domainVO = findDomainByPath(domainPath.trim()); From dfa1f34c65db4b91918e250d54b5a308feaa7104 Mon Sep 17 00:00:00 2001 From: bkarampudi Date: Fri, 1 Dec 2023 15:02:30 -0500 Subject: [PATCH 4/4] removing unused code --- api/src/org/apache/cloudstack/api/ApiServerService.java | 2 +- .../network/contrail/management/MockAccountManager.java | 2 +- server/src/com/cloud/api/ApiServer.java | 5 ++--- server/src/com/cloud/api/auth/ImpersonateUserCmd.java | 2 +- server/src/com/cloud/user/AccountManager.java | 2 +- server/src/com/cloud/user/AccountManagerImpl.java | 3 +-- server/test/com/cloud/user/MockAccountManagerImpl.java | 2 +- 7 files changed, 8 insertions(+), 10 deletions(-) diff --git a/api/src/org/apache/cloudstack/api/ApiServerService.java b/api/src/org/apache/cloudstack/api/ApiServerService.java index c67aeee643ad..bd2b184eb60a 100644 --- a/api/src/org/apache/cloudstack/api/ApiServerService.java +++ b/api/src/org/apache/cloudstack/api/ApiServerService.java @@ -31,7 +31,7 @@ public interface ApiServerService { public ResponseObject loginUser(HttpSession session, String username, String password, Long domainId, String domainPath, InetAddress loginIpAddress, Map requestParameters) throws CloudAuthenticationException; - public ResponseObject impersonateUser(HttpSession session, String username, Long domainId, String domainPath, InetAddress loginIpAddress, Map requestParameters) throws CloudAuthenticationException; + public ResponseObject impersonateUser(HttpSession session, String username, Long domainId, String domainPath, InetAddress loginIpAddress) throws CloudAuthenticationException; public void logoutUser(long userId); diff --git a/plugins/network-elements/juniper-contrail/test/org/apache/cloudstack/network/contrail/management/MockAccountManager.java b/plugins/network-elements/juniper-contrail/test/org/apache/cloudstack/network/contrail/management/MockAccountManager.java index b448e2ee6a1d..a3a2964e5d2b 100644 --- a/plugins/network-elements/juniper-contrail/test/org/apache/cloudstack/network/contrail/management/MockAccountManager.java +++ b/plugins/network-elements/juniper-contrail/test/org/apache/cloudstack/network/contrail/management/MockAccountManager.java @@ -262,7 +262,7 @@ public UserAccount authenticateUser(String arg0, String arg1, Long arg2, InetAdd } @Override - public UserAccount impersonateUser(String username, Long domainId, InetAddress loginIpAddress, Map requestParameters) { + public UserAccount impersonateUser(String username, Long domainId, InetAddress loginIpAddress) { return null; } diff --git a/server/src/com/cloud/api/ApiServer.java b/server/src/com/cloud/api/ApiServer.java index 92cf6fb2b85c..3f34e804eb5c 100644 --- a/server/src/com/cloud/api/ApiServer.java +++ b/server/src/com/cloud/api/ApiServer.java @@ -1124,8 +1124,7 @@ public ResponseObject loginUser(final HttpSession session, final String username } @Override - public ResponseObject impersonateUser(final HttpSession session, final String username, Long domainId, final String domainPath, final InetAddress loginIpAddress, - final Map requestParameters) throws CloudAuthenticationException { + public ResponseObject impersonateUser(final HttpSession session, final String username, Long domainId, final String domainPath, final InetAddress loginIpAddress) throws CloudAuthenticationException { // We will always use domainId first. If that does not exist, we will use domain name. If THAT doesn't exist // we will default to ROOT final Domain userDomain = domainMgr.findDomainByIdOrPath(domainId, domainPath); @@ -1135,7 +1134,7 @@ public ResponseObject impersonateUser(final HttpSession session, final String us domainId = userDomain.getId(); } - final UserAccount userAcct = accountMgr.impersonateUser(username, domainId, loginIpAddress, requestParameters); + final UserAccount userAcct = accountMgr.impersonateUser(username, domainId, loginIpAddress); if (userAcct != null) { final String timezone = userAcct.getTimezone(); float offsetInHrs = 0f; diff --git a/server/src/com/cloud/api/auth/ImpersonateUserCmd.java b/server/src/com/cloud/api/auth/ImpersonateUserCmd.java index e13730b6b1cc..4a65f18d2518 100644 --- a/server/src/com/cloud/api/auth/ImpersonateUserCmd.java +++ b/server/src/com/cloud/api/auth/ImpersonateUserCmd.java @@ -164,7 +164,7 @@ public String authenticate(String command, Map params, HttpSes return ApiResponseSerializer.toSerializedString(response, responseType); } - return ApiResponseSerializer.toSerializedString(_apiServer.impersonateUser(session, username[0], domainId, domain, remoteAddress, params), + return ApiResponseSerializer.toSerializedString(_apiServer.impersonateUser(session, username[0], domainId, domain, remoteAddress), responseType); } catch (final CloudAuthenticationException ex) { // TODO: fall through to API key, or just fail here w/ auth error? (HTTP 401) diff --git a/server/src/com/cloud/user/AccountManager.java b/server/src/com/cloud/user/AccountManager.java index b9b42a5a1f84..fe10cddb0167 100644 --- a/server/src/com/cloud/user/AccountManager.java +++ b/server/src/com/cloud/user/AccountManager.java @@ -78,7 +78,7 @@ public interface AccountManager extends AccountService, Configurable{ UserAccount authenticateUser(String username, String password, Long domainId, InetAddress loginIpAddress, Map requestParameters); - UserAccount impersonateUser(String username, Long domainId, InetAddress loginIpAddress, Map requestParameters); + UserAccount impersonateUser(String username, Long domainId, InetAddress loginIpAddress); /** * Locate a user by their apiKey diff --git a/server/src/com/cloud/user/AccountManagerImpl.java b/server/src/com/cloud/user/AccountManagerImpl.java index 96884f457c99..7bb8d83165eb 100644 --- a/server/src/com/cloud/user/AccountManagerImpl.java +++ b/server/src/com/cloud/user/AccountManagerImpl.java @@ -2249,8 +2249,7 @@ public UserAccount authenticateUser(final String username, final String password } @Override - public UserAccount impersonateUser(final String username, final Long domainId, final InetAddress loginIpAddress, final Map - requestParameters) { + public UserAccount impersonateUser(final String username, final Long domainId, final InetAddress loginIpAddress) { UserAccount user = _userAccountDao.getUserAccount(username, domainId); if (user != null) { // don't allow to authenticate system user diff --git a/server/test/com/cloud/user/MockAccountManagerImpl.java b/server/test/com/cloud/user/MockAccountManagerImpl.java index 97b91d47a8cc..2be330176e95 100644 --- a/server/test/com/cloud/user/MockAccountManagerImpl.java +++ b/server/test/com/cloud/user/MockAccountManagerImpl.java @@ -279,7 +279,7 @@ public UserAccount authenticateUser(String username, String password, Long domai } @Override - public UserAccount impersonateUser(String username, Long domainId, InetAddress loginIpAddress, Map requestParameters) { + public UserAccount impersonateUser(String username, Long domainId, InetAddress loginIpAddress) { return null; }