CLOUDSTACK-3049: Implemented role update for account. (#3058)

This commit is contained in:
Bitworks LLC 2019-01-25 19:02:56 +07:00 committed by Gabriel Beims Bräscher
parent 97ddd8dffd
commit d68712eb7b
8 changed files with 500 additions and 182 deletions

View File

@ -16,6 +16,7 @@
# under the License.
cloudstack/tools/docker/Dockerfile
cloudstack/tools/docker/Dockerfile.smokedev
.dockerignore
.idea
.git

View File

@ -21,6 +21,7 @@ import java.util.Map;
import javax.inject.Inject;
import org.apache.cloudstack.api.response.RoleResponse;
import org.apache.log4j.Logger;
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
@ -48,16 +49,19 @@ public class UpdateAccountCmd extends BaseCmd {
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@ACL(accessType = AccessType.OperateEntry)
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "Account id")
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "Account UUID")
private Long id;
@Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "the current account name")
@Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, description = "Current account name")
private String accountName;
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "the ID of the domain where the account exists")
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "The UUID of the domain where the account exists")
private Long domainId;
@Parameter(name = ApiConstants.NEW_NAME, type = CommandType.STRING, required = true, description = "new name for the account")
@Parameter(name = ApiConstants.ROLE_ID, type = CommandType.UUID, entityType = RoleResponse.class, description = "The UUID of the dynamic role to set for the account")
private Long roleId;
@Parameter(name = ApiConstants.NEW_NAME, type = CommandType.STRING, description = "New name for the account")
private String newName;
@Parameter(name = ApiConstants.NETWORK_DOMAIN,
@ -65,7 +69,7 @@ public class UpdateAccountCmd extends BaseCmd {
description = "Network domain for the account's networks; empty string will update domainName with NULL value")
private String networkDomain;
@Parameter(name = ApiConstants.ACCOUNT_DETAILS, type = CommandType.MAP, description = "details for account used to store specific parameters")
@Parameter(name = ApiConstants.ACCOUNT_DETAILS, type = CommandType.MAP, description = "Details for the account used to store specific parameters")
private Map details;
@Inject
@ -87,6 +91,8 @@ public class UpdateAccountCmd extends BaseCmd {
return domainId;
}
public Long getRoleId() { return roleId; }
public String getNewName() {
return newName;
}

View File

@ -37,8 +37,10 @@ import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.QuerySelector;
import org.apache.cloudstack.acl.Role;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
@ -1019,8 +1021,11 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
@DB
@ActionEvents({@ActionEvent(eventType = EventTypes.EVENT_ACCOUNT_CREATE, eventDescription = "creating Account"),
@ActionEvent(eventType = EventTypes.EVENT_USER_CREATE, eventDescription = "creating User")})
public UserAccount createUserAccount(final String userName, final String password, final String firstName, final String lastName, final String email, final String timezone, String accountName,
final short accountType, final Long roleId, Long domainId, final String networkDomain, final Map<String, String> details, String accountUUID, final String userUUID,
public UserAccount createUserAccount(final String userName, final String password, final String firstName,
final String lastName, final String email, final String timezone,
String accountName, final short accountType, final Long roleId, Long domainId,
final String networkDomain, final Map<String, String> details,
String accountUUID, final String userUUID,
final User.Source source) {
if (accountName == null) {
@ -1155,7 +1160,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
UserVO user = retrieveAndValidateUser(updateUserCmd);
s_logger.debug("Updating user with Id: " + user.getUuid());
validateAndUpdatApiAndSecretKeyIfNeeded(updateUserCmd, user);
validateAndUpdateApiAndSecretKeyIfNeeded(updateUserCmd, user);
Account account = retrieveAndValidateAccount(user);
validateAndUpdateFirstNameIfNeeded(updateUserCmd, user);
@ -1344,7 +1349,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
* <li>If a pair of keys is provided, we validate to see if there is an user already using the provided API key. If there is someone else using, we throw an {@link InvalidParameterValueException} because two users cannot have the same API key.
* </ul>
*/
protected void validateAndUpdatApiAndSecretKeyIfNeeded(UpdateUserCmd updateUserCmd, UserVO user) {
protected void validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmd updateUserCmd, UserVO user) {
String apiKey = updateUserCmd.getApiKey();
String secretKey = updateUserCmd.getSecretKey();
@ -1687,6 +1692,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
public AccountVO updateAccount(UpdateAccountCmd cmd) {
Long accountId = cmd.getId();
Long domainId = cmd.getDomainId();
Long roleId = cmd.getRoleId();
String accountName = cmd.getAccountName();
String newAccountName = cmd.getNewName();
String networkDomain = cmd.getNetworkDomain();
@ -1700,6 +1706,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
account = _accountDao.findEnabledAccount(accountName, domainId);
}
final AccountVO acctForUpdate = _accountDao.findById(account.getId());
// Check if account exists
if (account == null || account.getType() == Account.ACCOUNT_TYPE_PROJECT) {
s_logger.error("Unable to find account by accountId: " + accountId + " OR by name: " + accountName + " in domain " + domainId);
@ -1712,25 +1720,48 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
}
// Check if user performing the action is allowed to modify this account
checkAccess(getCurrentCallingAccount(), _domainMgr.getDomain(account.getDomainId()));
Account caller = getCurrentCallingAccount();
checkAccess(caller, _domainMgr.getDomain(account.getDomainId()));
// check if the given account name is unique in this domain for updating
Account duplicateAcccount = _accountDao.findActiveAccount(newAccountName, domainId);
if (duplicateAcccount != null && duplicateAcccount.getId() != account.getId()) {
throw new InvalidParameterValueException(
"There already exists an account with the name:" + newAccountName + " in the domain:" + domainId + " with existing account id:" + duplicateAcccount.getId());
if(newAccountName != null) {
if (newAccountName.isEmpty()) {
throw new InvalidParameterValueException("The new account name for account '" + account.getUuid() + "' " +
"within domain '" + domainId + "' is empty string. Account will be not renamed.");
}
// check if the new proposed account name is absent in the domain
Account existingAccount = _accountDao.findActiveAccount(newAccountName, domainId);
if (existingAccount != null && existingAccount.getId() != account.getId()) {
throw new InvalidParameterValueException("The account with the proposed name '" +
newAccountName + "' exists in the domain '" +
domainId + "' with existing account id '" + existingAccount.getId() + "'");
}
acctForUpdate.setAccountName(newAccountName);
}
if (networkDomain != null && !networkDomain.isEmpty()) {
if (!NetUtils.verifyDomainName(networkDomain)) {
throw new InvalidParameterValueException(
"Invalid network domain. Total length shouldn't exceed 190 chars. Each domain label must be between 1 and 63 characters long, can contain ASCII letters 'a' through 'z', the digits '0' through '9', "
throw new InvalidParameterValueException("Invalid network domain or format. " +
"Total length shouldn't exceed 190 chars. Every domain part must be between 1 and 63 " +
"characters long, can contain ASCII letters 'a' through 'z', the digits '0' through '9', "
+ "and the hyphen ('-'); can't start or end with \"-\"");
}
}
final AccountVO acctForUpdate = _accountDao.findById(account.getId());
acctForUpdate.setAccountName(newAccountName);
if (roleId != null) {
final List<Role> roles = cmd.roleService.listRoles();
final boolean roleNotFound = roles.stream().filter(r -> r.getId() == roleId).count() == 0;
if (roleNotFound) {
throw new InvalidParameterValueException("Role with ID '" + roleId.toString() + "' " +
"is not found or not available for the account '" + account.getUuid() + "' " +
"in the domain '" + domainId + "'.");
}
acctForUpdate.setRoleId(roleId);
}
if (networkDomain != null) {
if (networkDomain.isEmpty()) {
@ -1741,17 +1772,14 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
}
final Account accountFinal = account;
success = Transaction.execute(new TransactionCallback<Boolean>() {
@Override
public Boolean doInTransaction(TransactionStatus status) {
boolean success = _accountDao.update(accountFinal.getId(), acctForUpdate);
success = Transaction.execute((TransactionCallback<Boolean>) status -> {
boolean success1 = _accountDao.update(accountFinal.getId(), acctForUpdate);
if (details != null && success) {
_accountDetailsDao.update(accountFinal.getId(), details);
}
return success;
if (details != null && success1) {
_accountDetailsDao.update(accountFinal.getId(), details);
}
return success1;
});
if (success) {

View File

@ -226,7 +226,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
private void prepareMockAndExecuteUpdateUserTest(int numberOfExpectedCallsForSetEmailAndSetTimeZone) {
Mockito.doReturn(userVoMock).when(accountManagerImpl).retrieveAndValidateUser(UpdateUserCmdMock);
Mockito.doNothing().when(accountManagerImpl).validateAndUpdatApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock);
Mockito.doNothing().when(accountManagerImpl).validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock);
Mockito.doReturn(accountMock).when(accountManagerImpl).retrieveAndValidateAccount(userVoMock);
Mockito.doNothing().when(accountManagerImpl).validateAndUpdateFirstNameIfNeeded(UpdateUserCmdMock, userVoMock);
@ -242,7 +242,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
InOrder inOrder = Mockito.inOrder(userVoMock, accountManagerImpl, userDaoMock, userAccountDaoMock);
inOrder.verify(accountManagerImpl).retrieveAndValidateUser(UpdateUserCmdMock);
inOrder.verify(accountManagerImpl).validateAndUpdatApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock);
inOrder.verify(accountManagerImpl).validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock);
inOrder.verify(accountManagerImpl).retrieveAndValidateAccount(userVoMock);
inOrder.verify(accountManagerImpl).validateAndUpdateFirstNameIfNeeded(UpdateUserCmdMock, userVoMock);
@ -275,7 +275,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
@Test
public void validateAndUpdatApiAndSecretKeyIfNeededTestNoKeys() {
accountManagerImpl.validateAndUpdatApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock);
accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock);
Mockito.verify(accountDaoMock, Mockito.times(0)).findUserAccountByApiKey(Mockito.anyString());
}
@ -284,14 +284,14 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
public void validateAndUpdatApiAndSecretKeyIfNeededTestOnlyApiKeyInformed() {
Mockito.doReturn("apiKey").when(UpdateUserCmdMock).getApiKey();
accountManagerImpl.validateAndUpdatApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock);
accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock);
}
@Test(expected = InvalidParameterValueException.class)
public void validateAndUpdatApiAndSecretKeyIfNeededTestOnlySecretKeyInformed() {
Mockito.doReturn("secretKey").when(UpdateUserCmdMock).getSecretKey();
accountManagerImpl.validateAndUpdatApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock);
accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock);
}
@Test(expected = InvalidParameterValueException.class)
@ -308,7 +308,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
Pair<User, Account> pairUserAccountMock = new Pair<User, Account>(otherUserMock, Mockito.mock(Account.class));
Mockito.doReturn(pairUserAccountMock).when(accountDaoMock).findUserAccountByApiKey(apiKey);
accountManagerImpl.validateAndUpdatApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock);
accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock);
}
@Test
@ -327,7 +327,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
Pair<User, Account> pairUserAccountMock = new Pair<User, Account>(otherUserMock, Mockito.mock(Account.class));
Mockito.doReturn(pairUserAccountMock).when(accountDaoMock).findUserAccountByApiKey(apiKey);
accountManagerImpl.validateAndUpdatApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock);
accountManagerImpl.validateAndUpdateApiAndSecretKeyIfNeeded(UpdateUserCmdMock, userVoMock);
Mockito.verify(accountDaoMock).findUserAccountByApiKey(apiKey);
Mockito.verify(userVoMock).setApiKey(apiKey);

View File

@ -30,7 +30,7 @@ from marvin.lib.base import (Domain,
User,
NATRule,
Template,
PublicIPAddress)
PublicIPAddress, Role)
from marvin.lib.common import (get_domain,
get_zone,
get_test_template,
@ -67,6 +67,11 @@ class Services:
# username
"password": "fr3sca",
},
"role": {
"name": "MarvinFake Role",
"type": "User",
"description": "Fake Role created by Marvin test"
},
"user": {
"email": "user@test.com",
"firstname": "User",
@ -261,6 +266,53 @@ class TestAccounts(cloudstackTestCase):
return
@attr(tags=["advanced", "basic", "eip", "advancedns", "sg"],
required_hardware="false")
def test_02_update_account(self):
"""
Tests that accounts can be updated with new name, network domain, dynamic role
:return:
"""
dynamic_roles_active = self.apiclient.listCapabilities(listCapabilities.listCapabilitiesCmd()).dynamicrolesenabled
if not dynamic_roles_active:
self.skipTest("Dynamic Role-Based API checker not enabled, skipping test")
ts = str(time.time())
network_domain = 'mycloud.com'
account = Account.create(self.apiclient, self.services['account'])
self.cleanup.append(account)
role = Role.create(self.apiclient, self.services['role'])
self.cleanup.append(role)
account.update(self.apiclient, newname=account.name + ts)
account.update(self.apiclient, roleid=role.id)
account.update(self.apiclient, networkdomain=network_domain)
list_accounts_response = list_accounts(self.apiclient, id=account.id)
test_account = list_accounts_response[0]
self.assertEqual(
test_account.roleid, role.id,
"Check the role for the account is changed")
self.assertEqual(
test_account.networkdomain, network_domain,
"Check the domain for the account is changed")
self.assertEqual(
test_account.name, account.name + ts,
"Check the name for the account is changed")
try:
account.update(self.apiclient, newname="")
self.fail("Account name change to empty name succeeded. Must be error.")
except CloudstackAPIException:
pass
class TestRemoveUserFromAccount(cloudstackTestCase):
@classmethod

View File

@ -0,0 +1,143 @@
# 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.
#
# CloudStack-simulator build
FROM ubuntu:16.04
MAINTAINER "Apache CloudStack" <dev@cloudstack.apache.org>
LABEL Vendor="Apache.org" License="ApacheV2" Version="4.12.0-SNAPSHOT"
RUN apt-get -y update && apt-get install -y \
genisoimage \
libffi-dev \
libssl-dev \
git \
sudo \
ipmitool \
maven \
openjdk-8-jdk \
python-dev \
python-setuptools \
python-pip \
python-mysql.connector \
supervisor \
python-crypto \
python-openssl
RUN echo 'mysql-server mysql-server/root_password password root' | debconf-set-selections; \
echo 'mysql-server mysql-server/root_password_again password root' | debconf-set-selections;
RUN apt-get install -qqy mysql-server && \
apt-get clean all && \
mkdir /var/run/mysqld; \
chown mysql /var/run/mysqld
#
# this package is needed if one wants to run marvin tests from
# inside the running simulator.
#
RUN pip install pyOpenSSL
RUN echo '''sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"''' >> /etc/mysql/mysql.conf.d/mysqld.cnf
RUN (/usr/bin/mysqld_safe &); sleep 5; mysqladmin -u root -proot password ''
COPY agent /root/agent
COPY api /root/api
COPY build /root/build
COPY client /root/client
COPY cloud-cli /root/cloud-cli
COPY cloudstack.iml /root/cloudstack.iml
COPY core /root/core
COPY debian /root/debian
COPY deps /root/deps
COPY developer /root/developer
COPY engine /root/engine
COPY framework /root/framework
COPY LICENSE.header /root/LICENSE.header
COPY LICENSE /root/LICENSE
COPY maven-standard /root/maven-standard
COPY NOTICE /root/NOTICE
COPY packaging /root/packaging
COPY plugins /root/plugins
COPY pom.xml /root/pom.xml
COPY python /root/python
COPY quickcloud /root/quickcloud
COPY requirements.txt /root/requirements.txt
COPY scripts /root/scripts
COPY server /root/server
COPY services /root/services
COPY setup /root/setup
COPY systemvm /root/systemvm
COPY target /root/target
COPY test/bindirbak /root/test/bindirbak
COPY test/conf /root/test/conf
COPY test/metadata /root/test/metadata
COPY test/pom.xml /root/test/pom.xml
COPY test/scripts /root/test/scripts
COPY test/selenium /root/test/selenium
COPY test/systemvm /root/test/systemvm
COPY test/target /root/test/target
COPY tools/pom.xml /root/tools/pom.xml
COPY tools/apidoc /root/tools/apidoc
COPY tools/checkstyle /root/tools/checkstyle
COPY tools/devcloud4/pom.xml /root/tools/devcloud4/pom.xml
COPY tools/devcloud-kvm/pom.xml /root/tools/devcloud-kvm/pom.xml
COPY tools/marvin/pom.xml /root/tools/marvin/pom.xml
COPY tools/pom.xml /root/tools/pom.xml
COPY ui /root/ui
COPY usage /root/usage
COPY utils /root/utils
COPY vmware-base /root/vmware-base
RUN cd /root && mvn -Pdeveloper -Dsimulator -DskipTests -pl "!:cloud-marvin" install
RUN (/usr/bin/mysqld_safe &) && \
sleep 5 && \
cd /root && \
mvn -Pdeveloper -pl developer -Ddeploydb && \
mvn -Pdeveloper -pl developer -Ddeploydb-simulator
COPY tools/marvin /root/tools/marvin
COPY tools/docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY tools/docker/docker_run_tests.sh /root
RUN cd /root && mvn -Pdeveloper -Dsimulator -DskipTests -pl ":cloud-marvin"
RUN MARVIN_FILE=`find /root/tools/marvin/dist/ -name "Marvin*.tar.gz"` && pip install $MARVIN_FILE
COPY test/integration /root/test/integration
COPY tools /root/tools
RUN pip install --upgrade pyOpenSSL
EXPOSE 8080 8096
WORKDIR /root
CMD ["/usr/bin/supervisord"]
# --------------------------------
#
# docker run -v ~/dev/tmp:/tmp -v ~/IdeaProjects/cloudstack/test/integration/smoke:/root/test/integration/smoke -it
# --name simulator -p 8080:8080 -p8096:8096 simulator:4.12
#
# docker exec -it simulator bash
#
# cat /root/docker_run_tests.sh
# for instructions
#

View File

@ -0,0 +1,50 @@
#!/usr/bin/env bash
# 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.
MODE=${1:-advanced}
SUITE=${2:-smoke}
export MARVIN_CONFIG=setup/dev/$MODE.cfg
export TEST_SUITE=test/integration/$SUITE
export ZONE_NAME=Sandbox-simulator
cd /root
python tools/marvin/marvin/deployDataCenter.py -i setup/dev/$MODE.cfg
cat <<EOF
RUN WHOLE '$SUITE' SUITE
--------------------------
nosetests-2.7 \
--with-marvin \
--marvin-config=${MARVIN_CONFIG} \
-w ${TEST_SUITE} \
--with-xunit \
--xunit-file=/tmp/bvt_selfservice_cases.xml \
--zone=${ZONE_NAME} \
--hypervisor=simulator \
-a tags=$MODE,required_hardware=false
--------------------------
OR INDIVIDUAL TEST LIKE
--------------------------
nosetests-2.7 -s --with-marvin --marvin-config=${MARVIN_CONFIG} --zone=${ZONE_NAME} \
--hypervisor=simulator -a tags=$MODE,required_hardware=false \
test/integration/smoke/test_accounts.py:TestAccounts
--------------------------
EOF

File diff suppressed because it is too large Load Diff