You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by da...@apache.org on 2017/12/30 11:40:20 UTC

[cloudstack] branch master updated: CLOUDSTACK-10121 moveUser (#2301)

This is an automated email from the ASF dual-hosted git repository.

dahn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/master by this push:
     new 6724a47  CLOUDSTACK-10121 moveUser (#2301)
6724a47 is described below

commit 6724a471222daec41c8c9bfff709aee9db0a9880
Author: dahn <da...@gmail.com>
AuthorDate: Sat Dec 30 11:40:14 2017 +0000

    CLOUDSTACK-10121 moveUser (#2301)
    
    * internal service call for moveUser
    * expose moveUser as API
    * move uuid to external entity
---
 api/pom.xml                                        |   5 +
 api/src/com/cloud/event/EventTypes.java            |   1 +
 .../api/command/admin/user/MoveUserCmd.java        | 126 +++++++++++++++++++
 .../apache/cloudstack/region/RegionService.java    |  10 +-
 engine/schema/src/com/cloud/user/UserVO.java       |  20 ++-
 .../contrail/management/MockAccountManager.java    |   5 +
 pom.xml                                            |   2 +-
 .../src/com/cloud/server/ManagementServerImpl.java |   2 +
 server/src/com/cloud/user/AccountManager.java      |  10 +-
 server/src/com/cloud/user/AccountManagerImpl.java  |  79 ++++++++++--
 .../apache/cloudstack/region/RegionManager.java    |  14 ++-
 .../cloudstack/region/RegionManagerImpl.java       |   9 ++
 .../cloudstack/region/RegionServiceImpl.java       |   9 ++
 .../com/cloud/user/MockAccountManagerImpl.java     |   5 +
 test/integration/smoke/test_accounts.py            | 137 +++++++++++++++++++++
 tools/marvin/marvin/config/test_data.py            |   9 ++
 tools/marvin/marvin/lib/base.py                    |  13 ++
 17 files changed, 440 insertions(+), 16 deletions(-)

diff --git a/api/pom.xml b/api/pom.xml
index 6352e11..ce90eea 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -56,6 +56,11 @@
       <artifactId>cloud-framework-ca</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+      <version>${cs.commons-lang3.version}</version>
+    </dependency>
   </dependencies>
   <build>
     <plugins>
diff --git a/api/src/com/cloud/event/EventTypes.java b/api/src/com/cloud/event/EventTypes.java
index 26b6922..b745469 100644
--- a/api/src/com/cloud/event/EventTypes.java
+++ b/api/src/com/cloud/event/EventTypes.java
@@ -198,6 +198,7 @@ public class EventTypes {
     public static final String EVENT_USER_CREATE = "USER.CREATE";
     public static final String EVENT_USER_DELETE = "USER.DELETE";
     public static final String EVENT_USER_DISABLE = "USER.DISABLE";
+    public static final String EVENT_USER_MOVE = "USER.MOVE";
     public static final String EVENT_USER_UPDATE = "USER.UPDATE";
     public static final String EVENT_USER_ENABLE = "USER.ENABLE";
     public static final String EVENT_USER_LOCK = "USER.LOCK";
diff --git a/api/src/org/apache/cloudstack/api/command/admin/user/MoveUserCmd.java b/api/src/org/apache/cloudstack/api/command/admin/user/MoveUserCmd.java
new file mode 100644
index 0000000..b32aa2f
--- /dev/null
+++ b/api/src/org/apache/cloudstack/api/command/admin/user/MoveUserCmd.java
@@ -0,0 +1,126 @@
+// 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 org.apache.cloudstack.api.command.admin.user;
+
+import com.cloud.user.Account;
+import com.cloud.user.User;
+import com.google.common.base.Preconditions;
+import org.apache.cloudstack.acl.RoleType;
+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.response.AccountResponse;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.apache.cloudstack.api.response.UserResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.region.RegionService;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.log4j.Logger;
+
+import javax.inject.Inject;
+
+@APICommand(name = "moveUser",
+        description = "Moves a user to another account",
+        responseObject = SuccessResponse.class,
+        requestHasSensitiveInfo = false,
+        responseHasSensitiveInfo = false,
+        since = "4.11",
+        authorized = {RoleType.Admin})
+public class MoveUserCmd extends BaseCmd {
+    public static final Logger s_logger = Logger.getLogger(UpdateUserCmd.class.getName());
+
+    public static final String APINAME = "moveUser";
+
+    /////////////////////////////////////////////////////
+    //////////////// API parameters /////////////////////
+    /////////////////////////////////////////////////////
+    @Parameter(name = ApiConstants.ID,
+            type = CommandType.UUID,
+            entityType = UserResponse.class,
+            required = true,
+            description = "id of the user to be deleted")
+    private Long id;
+
+    @Parameter(name = ApiConstants.ACCOUNT,
+            type = CommandType.STRING,
+            description = "Creates the user under the specified account. If no account is specified, the username will be used as the account name.")
+    private String accountName;
+
+    @Parameter(name = ApiConstants.ACCOUNT_ID,
+            type = CommandType.UUID,
+            entityType = AccountResponse.class,
+            description = "Creates the user under the specified domain. Has to be accompanied with the account parameter")
+    private Long accountId;
+
+    @Inject
+    RegionService _regionService;
+
+    /////////////////////////////////////////////////////
+    /////////////////// Accessors ///////////////////////
+    /////////////////////////////////////////////////////
+
+    public Long getId() {
+        return id;
+    }
+
+    public String getAccountName() {
+        return accountName;
+    }
+
+    public Long getAccountId() {
+        return accountId;
+    }
+
+    /////////////////////////////////////////////////////
+    /////////////// API Implementation///////////////////
+    /////////////////////////////////////////////////////
+
+    @Override
+    public String getCommandName() {
+        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+    }
+
+    @Override
+    public long getEntityOwnerId() {
+        User user = _entityMgr.findById(User.class, getId());
+        if (user != null) {
+            return user.getAccountId();
+        }
+
+        return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked
+    }
+
+    @Override
+    public void execute() {
+        Preconditions.checkNotNull(getId(),"I have to have an user to move!");
+        Preconditions.checkState(ObjectUtils.anyNotNull(getAccountId(),getAccountName()),"provide either an account name or an account id!");
+
+        CallContext.current().setEventDetails("UserId: " + getId());
+        boolean result =
+                _regionService.moveUser(this);
+        if (result) {
+            SuccessResponse response = new SuccessResponse(getCommandName());
+            this.setResponseObject(response);
+        } else {
+            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to move the user to a new account");
+        }
+    }
+
+}
diff --git a/api/src/org/apache/cloudstack/region/RegionService.java b/api/src/org/apache/cloudstack/region/RegionService.java
index afefcc7..bee6691 100644
--- a/api/src/org/apache/cloudstack/region/RegionService.java
+++ b/api/src/org/apache/cloudstack/region/RegionService.java
@@ -27,6 +27,7 @@ import org.apache.cloudstack.api.command.admin.domain.UpdateDomainCmd;
 import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd;
 import org.apache.cloudstack.api.command.admin.user.DisableUserCmd;
 import org.apache.cloudstack.api.command.admin.user.EnableUserCmd;
+import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
 import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd;
 import org.apache.cloudstack.api.command.user.region.ListRegionsCmd;
 
@@ -111,9 +112,16 @@ public interface RegionService {
     boolean deleteUser(DeleteUserCmd deleteUserCmd);
 
     /**
+     * Deletes user by Id
+     * @param moveUserCmd
+     * @return true if delete was successful, false otherwise
+     */
+    boolean moveUser(MoveUserCmd moveUserCmd);
+
+    /**
      * update an existing domain
      *
-     * @param cmd
+     * @param updateDomainCmd
      *            - the command containing domainId and new domainName
      * @return Domain object if the command succeeded
      */
diff --git a/engine/schema/src/com/cloud/user/UserVO.java b/engine/schema/src/com/cloud/user/UserVO.java
index d6ddb58..05655bf 100644
--- a/engine/schema/src/com/cloud/user/UserVO.java
+++ b/engine/schema/src/com/cloud/user/UserVO.java
@@ -127,7 +127,25 @@ public class UserVO implements User, Identity, InternalIdentity {
         this.source = source;
     }
 
-    @Override
+    public UserVO(UserVO user) {
+        this.setAccountId(user.getAccountId());
+        this.setUsername(user.getUsername());
+        this.setPassword(user.getPassword());
+        this.setFirstname(user.getFirstname());
+        this.setLastname(user.getLastname());
+        this.setEmail(user.getEmail());
+        this.setTimezone(user.getTimezone());
+        this.setUuid(user.getUuid());
+        this.setSource(user.getSource());
+        this.setApiKey(user.getApiKey());
+        this.setSecretKey(user.getSecretKey());
+        this.setExternalEntity(user.getExternalEntity());
+        this.setRegistered(user.isRegistered());
+        this.setRegistrationToken(user.getRegistrationToken());
+        this.setState(user.getState());
+    }
+
+        @Override
     public long getId() {
         return id;
     }
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 a617013..fa93324 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
@@ -25,6 +25,7 @@ import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
 import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd;
+import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
 import org.apache.cloudstack.framework.config.ConfigKey;
 import org.apache.log4j.Logger;
 
@@ -316,6 +317,10 @@ public class MockAccountManager extends ManagerBase implements AccountManager {
         return false;
     }
 
+    @Override public boolean moveUser(MoveUserCmd moveUserCmd) {
+        return false;
+    }
+
     @Override
     public boolean deleteUserAccount(long arg0) {
         // TODO Auto-generated method stub
diff --git a/pom.xml b/pom.xml
index 66cfd09..bc76817 100644
--- a/pom.xml
+++ b/pom.xml
@@ -94,7 +94,7 @@
     <cs.aws.sdk.version>1.11.213</cs.aws.sdk.version>
     <cs.jackson.version>2.9.2</cs.jackson.version>
     <cs.lang.version>2.6</cs.lang.version>
-    <cs.commons-lang3.version>3.4</cs.commons-lang3.version>
+    <cs.commons-lang3.version>3.6</cs.commons-lang3.version>
     <cs.commons-io.version>2.6</cs.commons-io.version>
     <cs.commons-fileupload.version>1.3.3</cs.commons-fileupload.version>
     <cs.commons-collections.version>4.1</cs.commons-collections.version>
diff --git a/server/src/com/cloud/server/ManagementServerImpl.java b/server/src/com/cloud/server/ManagementServerImpl.java
index 177342b..c855c34 100644
--- a/server/src/com/cloud/server/ManagementServerImpl.java
+++ b/server/src/com/cloud/server/ManagementServerImpl.java
@@ -229,6 +229,7 @@ import org.apache.cloudstack.api.command.admin.user.GetUserCmd;
 import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd;
 import org.apache.cloudstack.api.command.admin.user.ListUsersCmd;
 import org.apache.cloudstack.api.command.admin.user.LockUserCmd;
+import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
 import org.apache.cloudstack.api.command.admin.user.RegisterCmd;
 import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd;
 import org.apache.cloudstack.api.command.admin.vlan.CreateVlanIpRangeCmd;
@@ -2672,6 +2673,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
         cmdList.add(GetUserCmd.class);
         cmdList.add(ListUsersCmd.class);
         cmdList.add(LockUserCmd.class);
+        cmdList.add(MoveUserCmd.class);
         cmdList.add(RegisterCmd.class);
         cmdList.add(UpdateUserCmd.class);
         cmdList.add(CreateVlanIpRangeCmd.class);
diff --git a/server/src/com/cloud/user/AccountManager.java b/server/src/com/cloud/user/AccountManager.java
index 9e0dde2..e0e7d3b 100644
--- a/server/src/com/cloud/user/AccountManager.java
+++ b/server/src/com/cloud/user/AccountManager.java
@@ -23,6 +23,7 @@ import java.net.InetAddress;
 import org.apache.cloudstack.acl.ControlledEntity;
 import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd;
 import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd;
+import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
 import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd;
 
 import com.cloud.api.query.vo.ControlledViewEntity;
@@ -156,9 +157,16 @@ public interface AccountManager extends AccountService, Configurable{
     boolean deleteUser(DeleteUserCmd deleteUserCmd);
 
     /**
+     * moves a user to another account within the same domain
+     * @param moveUserCmd
+     * @return true if the user was successfully moved
+     */
+    boolean moveUser(MoveUserCmd moveUserCmd);
+
+    /**
      * Update a user by userId
      *
-     * @param userId
+     * @param cmd
      * @return UserAccount object
      */
     UserAccount updateUser(UpdateUserCmd cmd);
diff --git a/server/src/com/cloud/user/AccountManagerImpl.java b/server/src/com/cloud/user/AccountManagerImpl.java
index 294bc6e..aaaa92b 100644
--- a/server/src/com/cloud/user/AccountManagerImpl.java
+++ b/server/src/com/cloud/user/AccountManagerImpl.java
@@ -52,6 +52,7 @@ import org.apache.cloudstack.affinity.dao.AffinityGroupDao;
 import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd;
 import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd;
 import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd;
+import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
 import org.apache.cloudstack.api.command.admin.user.RegisterCmd;
 import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd;
 import org.apache.cloudstack.context.CallContext;
@@ -1686,29 +1687,89 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
     @Override
     @ActionEvent(eventType = EventTypes.EVENT_USER_DELETE, eventDescription = "deleting User")
     public boolean deleteUser(DeleteUserCmd deleteUserCmd) {
-        long id = deleteUserCmd.getId();
+        UserVO user = getValidUserVO(deleteUserCmd.getId());
 
-        UserVO user = _userDao.findById(id);
+        Account account = _accountDao.findById(user.getAccountId());
 
-        if (user == null) {
-            throw new InvalidParameterValueException("The specified user doesn't exist in the system");
+        // don't allow to delete the user from the account of type Project
+        checkAccountAndAccess(user, account);
+        return _userDao.remove(deleteUserCmd.getId());
+    }
+
+    @ActionEvent(eventType = EventTypes.EVENT_USER_MOVE, eventDescription = "moving User to a new account")
+    public boolean moveUser(MoveUserCmd cmd) {
+        UserVO user = getValidUserVO(cmd.getId());
+        Account oldAccount = _accountDao.findById(user.getAccountId());
+        checkAccountAndAccess(user, oldAccount);
+        long domainId = oldAccount.getDomainId();
+
+        long newAccountId = getNewAccountId(cmd, domainId);
+
+        if(newAccountId == user.getAccountId()) {
+            // could do a not silent fail but the objective of the user is reached
+            return true; // no need to create a new user object for this user
         }
+        return Transaction.execute(new TransactionCallback<Boolean>() {
+            @Override
+            public Boolean doInTransaction(TransactionStatus status) {
+                UserVO newUser = new UserVO(user);
+                user.setExternalEntity(user.getUuid());
+                user.setUuid(UUID.randomUUID().toString());
+                _userDao.update(user.getId(),user);
+                newUser.setAccountId(newAccountId);
+                boolean success = _userDao.remove(cmd.getId());
+                UserVO persisted = _userDao.persist(newUser);
+                return success && persisted.getUuid().equals(user.getExternalEntity());
+            }
+        });
+    }
 
-        Account account = _accountDao.findById(user.getAccountId());
+    private long getNewAccountId(MoveUserCmd cmd, long domainId) {
+        Account newAccount = null;
+        if (StringUtils.isNotBlank(cmd.getAccountName())) {
+            if(s_logger.isDebugEnabled()) {
+                s_logger.debug("Getting id for account by name '" + cmd.getAccountName() + "' in domain " + domainId);
+            }
+            newAccount = _accountDao.findEnabledAccount(cmd.getAccountName(), domainId);
+        }
+        if (newAccount == null && cmd.getAccountId() != null) {
+            newAccount = _accountDao.findById(cmd.getAccountId());
+        }
+        if (newAccount == null) {
+            throw new CloudRuntimeException("no account name or account id. this should have been caught before this point");
+        }
+        long newAccountId = newAccount.getAccountId();
+
+        if(newAccount.getDomainId() != domainId) {
+            // not in scope
+            throw new InvalidParameterValueException("moving a user from an account in one domain to an account in annother domain is not supported!");
+        }
+        return newAccountId;
+    }
 
+    private void checkAccountAndAccess(UserVO user, Account account) {
         // don't allow to delete the user from the account of type Project
         if (account.getType() == Account.ACCOUNT_TYPE_PROJECT) {
+            throw new InvalidParameterValueException("Project users cannot be deleted or moved.");
+        }
+
+        checkAccess(CallContext.current().getCallingAccount(), AccessType.OperateEntry, true, account);
+        CallContext.current().putContextParameter(User.class, user.getUuid());
+    }
+
+    private UserVO getValidUserVO(long id) {
+        UserVO user = _userDao.findById(id);
+
+        if (user == null || user.getRemoved() != null) {
             throw new InvalidParameterValueException("The specified user doesn't exist in the system");
         }
 
         // don't allow to delete default user (system and admin users)
         if (user.isDefault()) {
-            throw new InvalidParameterValueException("The user is default and can't be removed");
+            throw new InvalidParameterValueException("The user is default and can't be (re)moved");
         }
 
-        checkAccess(CallContext.current().getCallingAccount(), AccessType.OperateEntry, true, account);
-        CallContext.current().putContextParameter(User.class, user.getUuid());
-        return _userDao.remove(id);
+        return user;
     }
 
     protected class AccountCleanupTask extends ManagedContextRunnable {
diff --git a/server/src/org/apache/cloudstack/region/RegionManager.java b/server/src/org/apache/cloudstack/region/RegionManager.java
index 6f25481..f7d7c10 100644
--- a/server/src/org/apache/cloudstack/region/RegionManager.java
+++ b/server/src/org/apache/cloudstack/region/RegionManager.java
@@ -21,6 +21,7 @@ import java.util.List;
 import org.apache.cloudstack.api.command.admin.account.UpdateAccountCmd;
 import org.apache.cloudstack.api.command.admin.domain.UpdateDomainCmd;
 import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd;
+import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
 import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd;
 
 import com.cloud.domain.Domain;
@@ -123,9 +124,16 @@ public interface RegionManager {
     boolean deleteUser(DeleteUserCmd deleteUserCmd);
 
     /**
+     * Deletes user by Id
+     * @param moveUserCmd
+     * @return
+     */
+    boolean moveUser(MoveUserCmd moveUserCmd);
+
+    /**
      * update an existing domain
      *
-     * @param cmd
+     * @param updateDomainCmd
      *            - the command containing domainId and new domainName
      * @return Domain object if the command succeeded
      */
@@ -142,7 +150,7 @@ public interface RegionManager {
     /**
      * Update a user by userId
      *
-     * @param userId
+     * @param updateUserCmd
      * @return UserAccount object
      */
     UserAccount updateUser(UpdateUserCmd updateUserCmd);
@@ -150,7 +158,7 @@ public interface RegionManager {
     /**
      * Disables a user by userId
      *
-     * @param userId
+     * @param id
      *            - the userId
      * @return UserAccount object
      */
diff --git a/server/src/org/apache/cloudstack/region/RegionManagerImpl.java b/server/src/org/apache/cloudstack/region/RegionManagerImpl.java
index 7e7189e..0878eef 100644
--- a/server/src/org/apache/cloudstack/region/RegionManagerImpl.java
+++ b/server/src/org/apache/cloudstack/region/RegionManagerImpl.java
@@ -24,6 +24,7 @@ import java.util.Properties;
 import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
+import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
 import org.apache.log4j.Logger;
 import org.springframework.stereotype.Component;
 
@@ -228,6 +229,14 @@ public class RegionManagerImpl extends ManagerBase implements RegionManager, Man
      * {@inheritDoc}
      */
     @Override
+    public boolean moveUser(MoveUserCmd cmd) {
+        return _accountMgr.moveUser(cmd);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public Domain updateDomain(UpdateDomainCmd cmd) {
         return _domainMgr.updateDomain(cmd);
     }
diff --git a/server/src/org/apache/cloudstack/region/RegionServiceImpl.java b/server/src/org/apache/cloudstack/region/RegionServiceImpl.java
index cd3a147..5afafff 100644
--- a/server/src/org/apache/cloudstack/region/RegionServiceImpl.java
+++ b/server/src/org/apache/cloudstack/region/RegionServiceImpl.java
@@ -34,6 +34,7 @@ import org.apache.cloudstack.api.command.admin.domain.UpdateDomainCmd;
 import org.apache.cloudstack.api.command.admin.user.DeleteUserCmd;
 import org.apache.cloudstack.api.command.admin.user.DisableUserCmd;
 import org.apache.cloudstack.api.command.admin.user.EnableUserCmd;
+import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
 import org.apache.cloudstack.api.command.admin.user.UpdateUserCmd;
 import org.apache.cloudstack.api.command.user.region.ListRegionsCmd;
 
@@ -155,6 +156,14 @@ public class RegionServiceImpl extends ManagerBase implements RegionService, Man
      * {@inheritDoc}
      */
     @Override
+    public boolean moveUser(MoveUserCmd cmd) {
+        return _regionMgr.moveUser(cmd);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public Domain updateDomain(UpdateDomainCmd cmd) {
         return _regionMgr.updateDomain(cmd);
     }
diff --git a/server/test/com/cloud/user/MockAccountManagerImpl.java b/server/test/com/cloud/user/MockAccountManagerImpl.java
index 9429c86..8ea0473 100644
--- a/server/test/com/cloud/user/MockAccountManagerImpl.java
+++ b/server/test/com/cloud/user/MockAccountManagerImpl.java
@@ -23,6 +23,7 @@ import java.net.InetAddress;
 import javax.naming.ConfigurationException;
 
 import org.apache.cloudstack.api.command.admin.user.GetUserKeysCmd;
+import org.apache.cloudstack.api.command.admin.user.MoveUserCmd;
 import org.apache.cloudstack.framework.config.ConfigKey;
 import org.springframework.stereotype.Component;
 
@@ -122,6 +123,10 @@ public class MockAccountManagerImpl extends ManagerBase implements Manager, Acco
         return false;
     }
 
+    @Override public boolean moveUser(MoveUserCmd moveUserCmd) {
+        return false;
+    }
+
     @Override
     public boolean isAdmin(Long accountId) {
         // TODO Auto-generated method stub
diff --git a/test/integration/smoke/test_accounts.py b/test/integration/smoke/test_accounts.py
index 00047bf..dffb00a 100644
--- a/test/integration/smoke/test_accounts.py
+++ b/test/integration/smoke/test_accounts.py
@@ -2086,3 +2086,140 @@ class TestDomainForceRemove(cloudstackTestCase):
             domain.delete(self.apiclient, cleanup=False)
         return
 
+class TestMoveUser(cloudstackTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.testClient = super(TestMoveUser, cls).getClsTestClient()
+        cls.api_client = cls.testClient.getApiClient()
+        cls.testdata = cls.testClient.getParsedTestDataConfig()
+
+        cls.domain = get_domain(cls.api_client)
+        cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests())
+        cls.testdata['mode'] = cls.zone.networktype
+
+        cls.template = get_test_template(
+            cls.api_client,
+            cls.zone.id,
+            cls.testdata["ostype"]
+        )
+
+        cls.testdata["virtual_machine"]["zoneid"] = cls.zone.id
+        cls._cleanup = []
+        return
+
+    @classmethod
+    def tearDownClass(cls):
+        try:
+            # Clean up, terminate the created resources
+            cleanup_resources(cls.api_client, cls._cleanup)
+        except Exception as e:
+
+            raise Exception("Warning: Exception during cleanup : %s" % e)
+        return
+
+    def setUp(self):
+        self.apiclient = self.testClient.getApiClient()
+        self.dbclient = self.testClient.getDbConnection()
+        self.cleanup = []
+        self.testdata = self.testClient.getParsedTestDataConfig()
+        self.account1 = Account.create(
+            self.apiclient,
+            self.testdata["acl"]["accountD1"],
+            domainid=self.domain.id
+        )
+        self.cleanup.append(self.account1)
+
+        self.account2 = Account.create(
+            self.apiclient,
+            self.testdata["acl"]["accountD1A"],
+            domainid=self.domain.id
+        )
+        self.cleanup.append(self.account2)
+
+        self.user = User.create(
+            self.apiclient,
+            self.testdata["user"],
+            account=self.account1.name,
+            domainid=self.account1.domainid
+        )
+
+        return
+
+    def tearDown(self):
+        try:
+            # Clean up, terminate the created resources
+            cleanup_resources(self.apiclient, self.cleanup)
+        except Exception as e:
+            raise Exception("Warning: Exception during cleanup : %s" % e)
+        return
+
+    @attr(tags=["domains", "advanced", "advancedns", "simulator","dvs"], required_hardware="false")
+    def test_move_user_to_accountID(self):
+
+        self.user.move(self.api_client, dest_accountid=self.account2.id)
+
+        self.assertEqual(
+            self.account2.name,
+            self.user.list(self.apiclient, id=self.user.id)[0].account,
+            "Check user source of created user"
+        )
+        return
+
+    @attr(tags=["domains", "advanced", "advancedns", "simulator","dvs"], required_hardware="false")
+    def test_move_user_to_account_name(self):
+
+        self.user.move(self.api_client, dest_account=self.account2.name)
+
+        self.assertEqual(
+            self.account2.name,
+            self.user.list(self.apiclient, id=self.user.id)[0].account,
+            "Check user source of created user"
+        )
+        return
+
+    @attr(tags=["domains", "advanced", "advancedns", "simulator","dvs"], required_hardware="false")
+    def test_move_user_to_different_domain(self):
+        domain2 = Domain.create(self.api_client,
+                                self.testdata["domain"],
+                                parentdomainid=self.domain.id
+                                )
+        self.cleanup.append(domain2)
+
+        account_different_domain = Account.create(
+            self.apiclient,
+            self.testdata["acl"]["accountD1B"],
+            domainid=domain2.id
+        )
+        self.cleanup.append(account_different_domain)
+        try:
+            self.user.move(self.api_client, dest_account=account_different_domain.name)
+        except Exception:
+            pass
+        else:
+            self.fail("It should not be allowed to move users across accounts in different domains, failing")
+
+        account_different_domain.delete(self.api_client)
+        return
+
+    @attr(tags=["domains", "advanced", "advancedns", "simulator","dvs"], required_hardware="false")
+    def test_move_user_incorrect_account_id(self):
+
+        try:
+            self.user.move(self.api_client, dest_accountid='incorrect-account-id')
+        except Exception:
+            pass
+        else:
+            self.fail("moving to non-existing account should not be possible, failing")
+        return
+
+    @attr(tags=["domains", "advanced", "advancedns", "simulator","dvs"], required_hardware="false")
+    def test_move_user_incorrect_account_name(self):
+
+        try:
+            self.user.move(self.api_client, dest_account='incorrect-account-name')
+        except Exception:
+            pass
+        else:
+            self.fail("moving to non-existing account should not be possible, failing")
+        return
diff --git a/tools/marvin/marvin/config/test_data.py b/tools/marvin/marvin/config/test_data.py
index 64733f7..7e7a2fd 100644
--- a/tools/marvin/marvin/config/test_data.py
+++ b/tools/marvin/marvin/config/test_data.py
@@ -71,6 +71,15 @@ test_data = {
         "username": "test-account2",
         "password": "password"
     },
+    "user": {
+         "email": "user@test.com",
+         "firstname": "User",
+         "lastname": "User",
+         "username": "User",
+           # Random characters are appended for unique
+           # username
+         "password": "fr3sca",
+     },
     "small": {
         "displayname": "testserver",
         "username": "root",
diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py
index 4154e91..f66a209 100755
--- a/tools/marvin/marvin/lib/base.py
+++ b/tools/marvin/marvin/lib/base.py
@@ -267,6 +267,19 @@ class User:
         cmd.id = self.id
         apiclient.deleteUser(cmd)
 
+    def move(self, api_client, dest_accountid = None, dest_account = None, domain= None):
+
+        if all([dest_account, dest_accountid]) is None:
+            raise Exception("Please add either destination account or destination account ID.")
+
+        cmd = moveUser.moveUserCmd()
+        cmd.id = self.id
+        cmd.accountid = dest_accountid
+        cmd.account = dest_account
+        cmd.domain = domain
+
+        return api_client.moveUser(cmd)
+
     @classmethod
     def list(cls, apiclient, **kwargs):
         """Lists users and provides detailed account information for

-- 
To stop receiving notification emails like this one, please contact
['"commits@cloudstack.apache.org" <co...@cloudstack.apache.org>'].