You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by GitBox <gi...@apache.org> on 2017/10/25 05:19:51 UTC

[GitHub] rhtyd closed pull request #2284: CLOUDSTACK-10103: Cloudian Connector for CloudStack

rhtyd closed pull request #2284: CLOUDSTACK-10103: Cloudian Connector for CloudStack
URL: https://github.com/apache/cloudstack/pull/2284
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/client/pom.xml b/client/pom.xml
index ae0fcaa20a5..2b52833c74a 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -416,6 +416,11 @@
     </dependency>
     <dependency>
       <groupId>org.apache.cloudstack</groupId>
+      <artifactId>cloud-plugin-integrations-cloudian-connector</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.cloudstack</groupId>
       <artifactId>cloud-plugin-integrations-prometheus-exporter</artifactId>
       <version>${project.version}</version>
     </dependency>
diff --git a/plugins/integrations/cloudian/pom.xml b/plugins/integrations/cloudian/pom.xml
new file mode 100644
index 00000000000..3e2b63562fb
--- /dev/null
+++ b/plugins/integrations/cloudian/pom.xml
@@ -0,0 +1,60 @@
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+  http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <artifactId>cloud-plugin-integrations-cloudian-connector</artifactId>
+  <name>Apache CloudStack Plugin - Cloudian Connector</name>
+  <parent>
+    <groupId>org.apache.cloudstack</groupId>
+    <artifactId>cloudstack-plugins</artifactId>
+    <version>4.11.0.0-SNAPSHOT</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.cloudstack</groupId>
+      <artifactId>cloud-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.cloudstack</groupId>
+      <artifactId>cloud-utils</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpclient</artifactId>
+      <version>${cs.httpclient.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>${cs.jackson.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.github.tomakehurst</groupId>
+      <artifactId>wiremock</artifactId>
+      <version>${cs.wiremock.version}</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/plugins/integrations/cloudian/resources/META-INF/cloudstack/cloudian/module.properties b/plugins/integrations/cloudian/resources/META-INF/cloudstack/cloudian/module.properties
new file mode 100644
index 00000000000..762c636cd3b
--- /dev/null
+++ b/plugins/integrations/cloudian/resources/META-INF/cloudstack/cloudian/module.properties
@@ -0,0 +1,18 @@
+# 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.
+name=cloudian
+parent=api
diff --git a/plugins/integrations/cloudian/resources/META-INF/cloudstack/cloudian/spring-cloudian-context.xml b/plugins/integrations/cloudian/resources/META-INF/cloudstack/cloudian/spring-cloudian-context.xml
new file mode 100644
index 00000000000..71ed52dd701
--- /dev/null
+++ b/plugins/integrations/cloudian/resources/META-INF/cloudstack/cloudian/spring-cloudian-context.xml
@@ -0,0 +1,25 @@
+<!--
+  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.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans
+                      http://www.springframework.org/schema/beans/spring-beans.xsd">
+    <bean id="cloudianConnector" class="org.apache.cloudstack.cloudian.CloudianConnectorImpl" >
+    </bean>
+</beans>
diff --git a/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/CloudianConnector.java b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/CloudianConnector.java
new file mode 100644
index 00000000000..c04d70c2601
--- /dev/null
+++ b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/CloudianConnector.java
@@ -0,0 +1,82 @@
+// 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.cloudian;
+
+import org.apache.cloudstack.framework.config.ConfigKey;
+
+import com.cloud.utils.component.PluggableService;
+
+public interface CloudianConnector extends PluggableService {
+
+    ConfigKey<Boolean> CloudianConnectorEnabled = new ConfigKey<>("Advanced", Boolean.class, "cloudian.connector.enabled", "false",
+            "If set to true, this enables the Cloudian Connector for CloudStack.", true);
+
+    ConfigKey<String> CloudianAdminHost = new ConfigKey<>("Advanced", String.class, "cloudian.admin.host", "s3-admin.cloudian.com",
+            "The hostname of the Cloudian Admin server.", true);
+
+    ConfigKey<Integer> CloudianAdminPort = new ConfigKey<>("Advanced", Integer.class, "cloudian.admin.port", "19443",
+            "The port of the Cloudian Admin server.", true);
+
+    ConfigKey<String> CloudianAdminProtocol = new ConfigKey<>("Advanced", String.class, "cloudian.admin.protocol", "https",
+            "The protocol of the Cloudian Admin server.", true);
+
+    ConfigKey<Boolean> CloudianValidateSSLSecurity = new ConfigKey<>("Advanced", Boolean.class, "cloudian.validate.ssl", "true",
+            "When set to true, this will validate the SSL certificate when connecting to https/ssl enabled admin host.", true);
+
+    ConfigKey<String> CloudianAdminUser = new ConfigKey<>("Advanced", String.class, "cloudian.admin.user", "sysadmin",
+            "The system admin user for accessing the Cloudian Admin server.", true);
+
+    ConfigKey<String> CloudianAdminPassword = new ConfigKey<>("Advanced", String.class, "cloudian.admin.password", "public",
+            "The system admin password for the Cloudian Admin server.", true);
+
+    ConfigKey<Integer> CloudianAdminApiRequestTimeout = new ConfigKey<>("Advanced", Integer.class, "cloudian.api.request.timeout", "5",
+            "The admin API request timeout in seconds.", true);
+
+    ConfigKey<String> CloudianCmcAdminUser = new ConfigKey<>("Advanced", String.class, "cloudian.cmc.admin.user", "admin",
+            "The admin user name for accessing the Cloudian Management Console.", true);
+
+    ConfigKey<String> CloudianCmcHost = new ConfigKey<>("Advanced", String.class, "cloudian.cmc.host", "cmc.cloudian.com",
+            "The hostname of the Cloudian Management Console.", true);
+
+    ConfigKey<String> CloudianCmcPort = new ConfigKey<>("Advanced", String.class, "cloudian.cmc.port", "8443",
+            "The port of the Cloudian Management Console.", true);
+
+    ConfigKey<String> CloudianCmcProtocol = new ConfigKey<>("Advanced", String.class, "cloudian.cmc.protocol", "https",
+            "The protocol of the Cloudian Management Console.", true);
+
+    ConfigKey<String> CloudianSsoKey = new ConfigKey<>("Advanced", String.class, "cloudian.sso.key", "ss0sh5r3dk3y",
+            "The shared single sign-on key as configured in Cloudian CMC.", true);
+
+    /**
+     * Returns the base Cloudian Management Console URL
+     * @return returns the url string
+     */
+    String getCmcUrl();
+
+    /**
+     * Checks if the Cloudian Connector is enabled
+     * @return returns true is connector is enabled
+     */
+    boolean isEnabled();
+
+    /**
+     * Generates single-sign on URL for logged in user
+     * @return returns the SSO URL string
+     */
+    String generateSsoUrl();
+}
diff --git a/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/CloudianConnectorImpl.java b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/CloudianConnectorImpl.java
new file mode 100644
index 00000000000..cfb23da2846
--- /dev/null
+++ b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/CloudianConnectorImpl.java
@@ -0,0 +1,345 @@
+// 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.cloudian;
+
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.naming.ConfigurationException;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.cloudian.api.CloudianIsEnabledCmd;
+import org.apache.cloudstack.cloudian.api.CloudianSsoLoginCmd;
+import org.apache.cloudstack.cloudian.client.CloudianClient;
+import org.apache.cloudstack.cloudian.client.CloudianGroup;
+import org.apache.cloudstack.cloudian.client.CloudianUser;
+import org.apache.cloudstack.cloudian.client.CloudianUtils;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.framework.config.ConfigKey;
+import org.apache.cloudstack.framework.config.Configurable;
+import org.apache.cloudstack.framework.messagebus.MessageBus;
+import org.apache.cloudstack.framework.messagebus.MessageSubscriber;
+import org.apache.log4j.Logger;
+
+import com.cloud.domain.Domain;
+import com.cloud.domain.DomainVO;
+import com.cloud.domain.dao.DomainDao;
+import com.cloud.user.Account;
+import com.cloud.user.AccountManager;
+import com.cloud.user.DomainManager;
+import com.cloud.user.User;
+import com.cloud.user.dao.AccountDao;
+import com.cloud.user.dao.UserDao;
+import com.cloud.utils.component.ComponentLifecycleBase;
+import com.cloud.utils.exception.CloudRuntimeException;
+
+public class CloudianConnectorImpl extends ComponentLifecycleBase implements CloudianConnector, Configurable {
+    private static final Logger LOG = Logger.getLogger(CloudianConnectorImpl.class);
+
+    @Inject
+    private UserDao userDao;
+
+    @Inject
+    private AccountDao accountDao;
+
+    @Inject
+    private DomainDao domainDao;
+
+    @Inject
+    private MessageBus messageBus;
+
+    /////////////////////////////////////////////////////
+    //////////////// Plugin Methods /////////////////////
+    /////////////////////////////////////////////////////
+
+    private CloudianClient getClient() {
+        try {
+            return new CloudianClient(CloudianAdminHost.value(), CloudianAdminPort.value(), CloudianAdminProtocol.value(),
+                    CloudianAdminUser.value(), CloudianAdminPassword.value(),
+                    CloudianValidateSSLSecurity.value(), CloudianAdminApiRequestTimeout.value());
+        } catch (final KeyStoreException | NoSuchAlgorithmException | KeyManagementException e) {
+            LOG.error("Failed to create Cloudian API client due to: ", e);
+        }
+        throw new CloudRuntimeException("Failed to create and return Cloudian API client instance");
+    }
+
+    private boolean addGroup(final Domain domain) {
+        if (domain == null || !isEnabled()) {
+            return false;
+        }
+        final CloudianClient client = getClient();
+        final CloudianGroup group = new CloudianGroup();
+        group.setGroupId(domain.getUuid());
+        group.setGroupName(domain.getPath());
+        group.setActive(true);
+        return client.addGroup(group);
+    }
+
+    private boolean removeGroup(final Domain domain) {
+        if (domain == null || !isEnabled()) {
+            return false;
+        }
+        final CloudianClient client = getClient();
+        for (final CloudianUser user: client.listUsers(domain.getUuid())) {
+            if (client.removeUser(user.getUserId(), domain.getUuid())) {
+                LOG.error(String.format("Failed to remove Cloudian user id=%s, while removing Cloudian group id=%s", user.getUserId(), domain.getUuid()));
+            }
+        }
+        for (int retry = 0; retry < 3; retry++) {
+            if (client.removeGroup(domain.getUuid())) {
+                return true;
+            } else {
+                LOG.warn("Failed to remove Cloudian group id=" + domain.getUuid() + ", retrying count=" + retry+1);
+            }
+        }
+        LOG.warn("Failed to remove Cloudian group id=" + domain.getUuid() + ", please remove manually");
+        return false;
+    }
+
+    private boolean addUserAccount(final Account account, final Domain domain) {
+        if (account == null || domain == null || !isEnabled()) {
+            return false;
+        }
+        final User accountUser = userDao.listByAccount(account.getId()).get(0);
+        final CloudianClient client = getClient();
+        final String fullName = String.format("%s %s (%s)", accountUser.getFirstname(), accountUser.getLastname(), account.getAccountName());
+        final CloudianUser user = new CloudianUser();
+        user.setUserId(account.getUuid());
+        user.setGroupId(domain.getUuid());
+        user.setFullName(fullName);
+        user.setEmailAddr(accountUser.getEmail());
+        user.setUserType(CloudianUser.USER);
+        user.setActive(true);
+        return client.addUser(user);
+    }
+
+    private boolean updateUserAccount(final Account account, final Domain domain, final CloudianUser existingUser) {
+        if (account == null || domain == null || !isEnabled()) {
+            return false;
+        }
+        final CloudianClient client = getClient();
+        if (existingUser != null) {
+            final User accountUser = userDao.listByAccount(account.getId()).get(0);
+            final String fullName = String.format("%s %s (%s)", accountUser.getFirstname(), accountUser.getLastname(), account.getAccountName());
+            if (!existingUser.getActive() || !existingUser.getFullName().equals(fullName) || !existingUser.getEmailAddr().equals(accountUser.getEmail())) {
+                existingUser.setActive(true);
+                existingUser.setFullName(fullName);
+                existingUser.setEmailAddr(accountUser.getEmail());
+                return client.updateUser(existingUser);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private boolean removeUserAccount(final Account account) {
+        if (account == null || !isEnabled()) {
+            return false;
+        }
+        final CloudianClient client = getClient();
+        final Domain domain = domainDao.findById(account.getDomainId());
+        for (int retry = 0; retry < 3; retry++) {
+            if (client.removeUser(account.getUuid(), domain.getUuid())) {
+                return true;
+            } else {
+                LOG.warn("Failed to remove Cloudian user id=" + account.getUuid() + " in group id=" + domain.getUuid() + ", retrying count=" + retry+1);
+            }
+        }
+        LOG.warn("Failed to remove Cloudian user id=" + account.getUuid() + " in group id=" + domain.getUuid() + ", please remove manually");
+        return false;
+    }
+
+    //////////////////////////////////////////////////
+    //////////////// Plugin APIs /////////////////////
+    //////////////////////////////////////////////////
+
+    @Override
+    public String getCmcUrl() {
+        return String.format("%s://%s:%s/Cloudian/", CloudianCmcProtocol.value(),
+                CloudianCmcHost.value(), CloudianCmcPort.value());
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return CloudianConnectorEnabled.value();
+    }
+
+    @Override
+    public String generateSsoUrl() {
+        final Account caller = CallContext.current().getCallingAccount();
+        final Domain domain = domainDao.findById(caller.getDomainId());
+
+        String user = caller.getUuid();
+        String group = domain.getUuid();
+
+        if (caller.getAccountName().equals("admin") && caller.getRoleId() == RoleType.Admin.getId()) {
+            user = CloudianCmcAdminUser.value();
+            group = "0";
+        }
+
+        LOG.debug(String.format("Attempting Cloudian SSO with user id=%s, group id=%s", user, group));
+
+        final CloudianUser ssoUser = getClient().listUser(user, group);
+        if (ssoUser == null || !ssoUser.getActive()) {
+            LOG.debug(String.format("Failed to find existing Cloudian user id=%s in group id=%s", user, group));
+            final CloudianGroup ssoGroup = getClient().listGroup(group);
+            if (ssoGroup == null) {
+                LOG.debug(String.format("Failed to find existing Cloudian group id=%s, trying to add it", group));
+                if (!addGroup(domain)) {
+                    LOG.error("Failed to add missing Cloudian group id=" + group);
+                    throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Aborting Cloudian SSO, failed to add group to Cloudian.");
+                }
+            }
+            if (!addUserAccount(caller, domain)) {
+                LOG.error("Failed to add missing Cloudian group id=" + group);
+                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Aborting Cloudian SSO, failed to add user to Cloudian.");
+            }
+            final CloudianUser addedSsoUser = getClient().listUser(user, group);
+            if (addedSsoUser == null || !addedSsoUser.getActive()) {
+                throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Aborting Cloudian SSO, failed to find mapped Cloudian user, please fix integration issues.");
+            }
+        } else if (!group.equals("0")) {
+            updateUserAccount(caller, domain, ssoUser);
+        }
+
+        LOG.debug(String.format("Validated Cloudian SSO for Cloudian user id=%s, group id=%s", user, group));
+        return CloudianUtils.generateSSOUrl(getCmcUrl(), user, group, CloudianSsoKey.value());
+    }
+
+    ///////////////////////////////////////////////////////////
+    //////////////// Plugin Configuration /////////////////////
+    ///////////////////////////////////////////////////////////
+
+    @Override
+    public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
+        super.configure(name, params);
+
+        if (!isEnabled()) {
+            LOG.debug("Cloudian connector is disabled, skipping configuration");
+            return true;
+        }
+
+        LOG.debug(String.format("Cloudian connector is enabled, completed configuration, integration is ready. " +
+                        "Cloudian admin host:%s, port:%s, user:%s",
+                CloudianAdminHost.value(), CloudianAdminPort.value(), CloudianAdminUser.value()));
+
+        messageBus.subscribe(AccountManager.MESSAGE_ADD_ACCOUNT_EVENT, new MessageSubscriber() {
+            @Override
+            public void onPublishMessage(String senderAddress, String subject, Object args) {
+                try {
+                    final Map<Long, Long> accountGroupMap = (Map<Long, Long>) args;
+                    final Long accountId = accountGroupMap.keySet().iterator().next();
+                    final Account account = accountDao.findById(accountId);
+                    final Domain domain = domainDao.findById(account.getDomainId());
+
+                    if (!addUserAccount(account, domain)) {
+                        LOG.warn(String.format("Failed to add account in Cloudian while adding CloudStack account=%s in domain=%s", account.getAccountName(), domain.getPath()));
+                    }
+                } catch (final Exception e) {
+                    LOG.error("Caught exception while adding account in Cloudian: ", e);
+                }
+            }
+        });
+
+        messageBus.subscribe(AccountManager.MESSAGE_REMOVE_ACCOUNT_EVENT, new MessageSubscriber() {
+            @Override
+            public void onPublishMessage(String senderAddress, String subject, Object args) {
+                try {
+                    final Account account = accountDao.findByIdIncludingRemoved((Long) args);
+                    if(!removeUserAccount(account))    {
+                        LOG.warn(String.format("Failed to remove account to Cloudian while removing CloudStack account=%s, id=%s", account.getAccountName(), account.getId()));
+                    }
+                } catch (final Exception e) {
+                    LOG.error("Caught exception while removing account in Cloudian: ", e);
+                }
+            }
+        });
+
+        messageBus.subscribe(DomainManager.MESSAGE_ADD_DOMAIN_EVENT, new MessageSubscriber() {
+            @Override
+            public void onPublishMessage(String senderAddress, String subject, Object args) {
+                try {
+                    final Domain domain = domainDao.findById((Long) args);
+                    if (!addGroup(domain)) {
+                        LOG.warn(String.format("Failed to add group in Cloudian while adding CloudStack domain=%s id=%s", domain.getPath(), domain.getId()));
+                    }
+                } catch (final Exception e) {
+                    LOG.error("Caught exception adding domain/group in Cloudian: ", e);
+                }
+            }
+        });
+
+        messageBus.subscribe(DomainManager.MESSAGE_REMOVE_DOMAIN_EVENT, new MessageSubscriber() {
+            @Override
+            public void onPublishMessage(String senderAddress, String subject, Object args) {
+                try {
+                    final DomainVO domain = (DomainVO) args;
+                    if (!removeGroup(domain)) {
+                        LOG.warn(String.format("Failed to remove group in Cloudian while removing CloudStack domain=%s id=%s", domain.getPath(), domain.getId()));
+                    }
+                } catch (final Exception e) {
+                    LOG.error("Caught exception while removing domain/group in Cloudian: ", e);
+                }
+            }
+        });
+
+        return true;
+    }
+
+    @Override
+    public List<Class<?>> getCommands() {
+        final List<Class<?>> cmdList = new ArrayList<Class<?>>();
+        cmdList.add(CloudianIsEnabledCmd.class);
+        if (!isEnabled()) {
+            return cmdList;
+        }
+        cmdList.add(CloudianSsoLoginCmd.class);
+        return cmdList;
+    }
+
+    @Override
+    public String getConfigComponentName() {
+        return CloudianConnector.class.getSimpleName();
+    }
+
+    @Override
+    public ConfigKey<?>[] getConfigKeys() {
+        return new ConfigKey<?>[] {
+                CloudianConnectorEnabled,
+                CloudianAdminHost,
+                CloudianAdminPort,
+                CloudianAdminUser,
+                CloudianAdminPassword,
+                CloudianAdminProtocol,
+                CloudianAdminApiRequestTimeout,
+                CloudianValidateSSLSecurity,
+                CloudianCmcAdminUser,
+                CloudianCmcHost,
+                CloudianCmcPort,
+                CloudianCmcProtocol,
+                CloudianSsoKey
+        };
+    }
+}
diff --git a/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/api/CloudianIsEnabledCmd.java b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/api/CloudianIsEnabledCmd.java
new file mode 100644
index 00000000000..fdca87185d0
--- /dev/null
+++ b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/api/CloudianIsEnabledCmd.java
@@ -0,0 +1,65 @@
+// 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.cloudian.api;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.cloudian.CloudianConnector;
+import org.apache.cloudstack.cloudian.response.CloudianEnabledResponse;
+
+import com.cloud.user.Account;
+
+@APICommand(name = CloudianIsEnabledCmd.APINAME, description = "Checks if the Cloudian Connector is enabled",
+        responseObject = CloudianEnabledResponse.class,
+        requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
+        since = "4.11.0",
+        authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class CloudianIsEnabledCmd extends BaseCmd {
+    public static final String APINAME = "cloudianIsEnabled";
+
+    @Inject
+    private CloudianConnector connector;
+
+    /////////////////////////////////////////////////////
+    /////////////// API Implementation///////////////////
+    /////////////////////////////////////////////////////
+
+    @Override
+    public String getCommandName() {
+        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+    }
+
+    @Override
+    public long getEntityOwnerId() {
+        return Account.ACCOUNT_ID_SYSTEM;
+    }
+
+
+    @Override
+    public void execute() {
+        final CloudianEnabledResponse response = new CloudianEnabledResponse();
+        response.setEnabled(connector.isEnabled());
+        response.setCmcUrl(connector.getCmcUrl());
+        response.setObjectName(APINAME.toLowerCase());
+        response.setResponseName(getCommandName());
+        setResponseObject(response);
+    }
+}
diff --git a/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/api/CloudianSsoLoginCmd.java b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/api/CloudianSsoLoginCmd.java
new file mode 100644
index 00000000000..7bdd7fda5e1
--- /dev/null
+++ b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/api/CloudianSsoLoginCmd.java
@@ -0,0 +1,70 @@
+// 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.cloudian.api;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.cloudian.CloudianConnector;
+import org.apache.cloudstack.cloudian.response.CloudianSsoLoginResponse;
+
+import com.cloud.user.Account;
+import com.google.common.base.Strings;
+
+@APICommand(name = CloudianSsoLoginCmd.APINAME, description = "Generates single-sign-on login url for logged-in CloudStack user to access the Cloudian Management Console",
+        responseObject = CloudianSsoLoginResponse.class,
+        since = "4.11.0",
+        authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
+public class CloudianSsoLoginCmd extends BaseCmd {
+    public static final String APINAME = "cloudianSsoLogin";
+
+    @Inject
+    private CloudianConnector connector;
+
+    /////////////////////////////////////////////////////
+    /////////////// API Implementation///////////////////
+    /////////////////////////////////////////////////////
+
+    @Override
+    public String getCommandName() {
+        return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+    }
+
+    @Override
+    public long getEntityOwnerId() {
+        return Account.ACCOUNT_ID_SYSTEM;
+    }
+
+
+    @Override
+    public void execute() {
+        final String ssoUrl = connector.generateSsoUrl();
+        if (Strings.isNullOrEmpty(ssoUrl)) {
+            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to generate Cloudian single-sign on URL for the user");
+        }
+        final CloudianSsoLoginResponse response = new CloudianSsoLoginResponse();
+        response.setSsoRedirectUrl(ssoUrl);
+        response.setResponseName(getCommandName());
+        response.setObjectName(APINAME.toLowerCase());
+        setResponseObject(response);
+    }
+}
diff --git a/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/client/CloudianClient.java b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/client/CloudianClient.java
new file mode 100644
index 00000000000..11f2055fef8
--- /dev/null
+++ b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/client/CloudianClient.java
@@ -0,0 +1,347 @@
+// 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.cloudian.client;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.utils.security.SSLUtils;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.AuthCache;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.impl.client.BasicAuthCache;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.log4j.Logger;
+
+import com.cloud.utils.nio.TrustAllManager;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Strings;
+
+public class CloudianClient {
+    private static final Logger LOG = Logger.getLogger(CloudianClient.class);
+
+    private final HttpClient httpClient;
+    private final HttpClientContext httpContext;
+    private final String adminApiUrl;
+
+    public CloudianClient(final String host, final Integer port, final String scheme, final String username, final String password, final boolean validateSSlCertificate, final int timeout) throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
+        final CredentialsProvider provider = new BasicCredentialsProvider();
+        provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));
+        final HttpHost adminHost = new HttpHost(host, port, scheme);
+        final AuthCache authCache = new BasicAuthCache();
+        authCache.put(adminHost, new BasicScheme());
+
+        this.adminApiUrl = adminHost.toURI();
+        this.httpContext = HttpClientContext.create();
+        this.httpContext.setCredentialsProvider(provider);
+        this.httpContext.setAuthCache(authCache);
+
+        final RequestConfig config = RequestConfig.custom()
+                .setConnectTimeout(timeout * 1000)
+                .setConnectionRequestTimeout(timeout * 1000)
+                .setSocketTimeout(timeout * 1000)
+                .build();
+
+        if (!validateSSlCertificate) {
+            final SSLContext sslcontext = SSLUtils.getSSLContext();
+            sslcontext.init(null, new X509TrustManager[]{new TrustAllManager()}, new SecureRandom());
+            final SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(sslcontext, NoopHostnameVerifier.INSTANCE);
+            this.httpClient = HttpClientBuilder.create()
+                    .setDefaultCredentialsProvider(provider)
+                    .setDefaultRequestConfig(config)
+                    .setSSLSocketFactory(factory)
+                    .build();
+        } else {
+            this.httpClient = HttpClientBuilder.create()
+                    .setDefaultCredentialsProvider(provider)
+                    .setDefaultRequestConfig(config)
+                    .build();
+        }
+    }
+
+    private void checkAuthFailure(final HttpResponse response) {
+        if (response != null && response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
+            final Credentials credentials = httpContext.getCredentialsProvider().getCredentials(AuthScope.ANY);
+            LOG.error("Cloudian admin API authentication failed, please check Cloudian configuration. Admin auth principal=" + credentials.getUserPrincipal() + ", password=" + credentials.getPassword() + ", API url=" + adminApiUrl);
+            throw new ServerApiException(ApiErrorCode.UNAUTHORIZED, "Cloudian backend API call unauthorized, please ask your administrator to fix integration issues.");
+        }
+    }
+
+    private void checkResponseOK(final HttpResponse response) {
+        if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT) {
+            LOG.debug("Requested Cloudian resource does not exist");
+            return;
+        }
+        if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK && response.getStatusLine().getStatusCode() != HttpStatus.SC_NO_CONTENT) {
+            throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to find the requested resource and get valid response from Cloudian backend API call, please ask your administrator to diagnose and fix issues.");
+        }
+    }
+
+    private boolean checkEmptyResponse(final HttpResponse response) throws IOException {
+        return response != null && (response.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT ||
+                response.getEntity() == null ||
+                response.getEntity().getContent() == null);
+    }
+
+    private void checkResponseTimeOut(final Exception e) {
+        if (e instanceof ConnectTimeoutException || e instanceof SocketTimeoutException) {
+            throw new ServerApiException(ApiErrorCode.RESOURCE_UNAVAILABLE_ERROR, "Operation timed out, please try again.");
+        }
+    }
+
+    private HttpResponse delete(final String path) throws IOException {
+        final HttpResponse response = httpClient.execute(new HttpDelete(adminApiUrl + path), httpContext);
+        checkAuthFailure(response);
+        return response;
+    }
+
+    private HttpResponse get(final String path) throws IOException {
+        final HttpResponse response = httpClient.execute(new HttpGet(adminApiUrl + path), httpContext);
+        checkAuthFailure(response);
+        return response;
+    }
+
+    private HttpResponse post(final String path, final Object item) throws IOException {
+        final ObjectMapper mapper = new ObjectMapper();
+        final String json = mapper.writeValueAsString(item);
+        final StringEntity entity = new StringEntity(json);
+        final HttpPost request = new HttpPost(adminApiUrl + path);
+        request.setHeader("Content-type", "application/json");
+        request.setEntity(entity);
+        final HttpResponse response = httpClient.execute(request, httpContext);
+        checkAuthFailure(response);
+        return response;
+    }
+
+    private HttpResponse put(final String path, final Object item) throws IOException {
+        final ObjectMapper mapper = new ObjectMapper();
+        final String json = mapper.writeValueAsString(item);
+        final StringEntity entity = new StringEntity(json);
+        final HttpPut request = new HttpPut(adminApiUrl + path);
+        request.setHeader("Content-type", "application/json");
+        request.setEntity(entity);
+        final HttpResponse response = httpClient.execute(request, httpContext);
+        checkAuthFailure(response);
+        return response;
+    }
+
+    ////////////////////////////////////////////////////////
+    //////////////// Public APIs: User /////////////////////
+    ////////////////////////////////////////////////////////
+
+    public boolean addUser(final CloudianUser user) {
+        if (user == null) {
+            return false;
+        }
+        LOG.debug("Adding Cloudian user: " + user);
+        try {
+            final HttpResponse response = put("/user", user);
+            return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
+        } catch (final IOException e) {
+            LOG.error("Failed to add Cloudian user due to:", e);
+            checkResponseTimeOut(e);
+        }
+        return false;
+    }
+
+    public CloudianUser listUser(final String userId, final String groupId) {
+        if (Strings.isNullOrEmpty(userId) || Strings.isNullOrEmpty(groupId)) {
+            return null;
+        }
+        LOG.debug("Trying to find Cloudian user with id=" + userId + " and group id=" + groupId);
+        try {
+            final HttpResponse response = get(String.format("/user?userId=%s&groupId=%s", userId, groupId));
+            checkResponseOK(response);
+            if (checkEmptyResponse(response)) {
+                return null;
+            }
+            final ObjectMapper mapper = new ObjectMapper();
+            return mapper.readValue(response.getEntity().getContent(), CloudianUser.class);
+        } catch (final IOException e) {
+            LOG.error("Failed to list Cloudian user due to:", e);
+            checkResponseTimeOut(e);
+        }
+        return null;
+    }
+
+    public List<CloudianUser> listUsers(final String groupId) {
+        if (Strings.isNullOrEmpty(groupId)) {
+            return new ArrayList<>();
+        }
+        LOG.debug("Trying to list Cloudian users in group id=" + groupId);
+        try {
+            final HttpResponse response = get(String.format("/user/list?groupId=%s&userType=all&userStatus=active", groupId));
+            checkResponseOK(response);
+            if (checkEmptyResponse(response)) {
+                return new ArrayList<>();
+            }
+            final ObjectMapper mapper = new ObjectMapper();
+            return Arrays.asList(mapper.readValue(response.getEntity().getContent(), CloudianUser[].class));
+        } catch (final IOException e) {
+            LOG.error("Failed to list Cloudian users due to:", e);
+            checkResponseTimeOut(e);
+        }
+        return new ArrayList<>();
+    }
+
+    public boolean updateUser(final CloudianUser user) {
+        if (user == null) {
+            return false;
+        }
+        LOG.debug("Updating Cloudian user: " + user);
+        try {
+            final HttpResponse response = post("/user", user);
+            return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
+        } catch (final IOException e) {
+            LOG.error("Failed to update Cloudian user due to:", e);
+            checkResponseTimeOut(e);
+        }
+        return false;
+    }
+
+    public boolean removeUser(final String userId, final String groupId) {
+        if (Strings.isNullOrEmpty(userId) || Strings.isNullOrEmpty(groupId)) {
+            return false;
+        }
+        LOG.debug("Removing Cloudian user with user id=" + userId + " in group id=" + groupId);
+        try {
+            final HttpResponse response = delete(String.format("/user?userId=%s&groupId=%s", userId, groupId));
+            return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
+        } catch (final IOException e) {
+            LOG.error("Failed to remove Cloudian user due to:", e);
+            checkResponseTimeOut(e);
+        }
+        return false;
+    }
+
+    /////////////////////////////////////////////////////////
+    //////////////// Public APIs: Group /////////////////////
+    /////////////////////////////////////////////////////////
+
+    public boolean addGroup(final CloudianGroup group) {
+        if (group == null) {
+            return false;
+        }
+        LOG.debug("Adding Cloudian group: " + group);
+        try {
+            final HttpResponse response = put("/group", group);
+            return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
+        } catch (final IOException e) {
+            LOG.error("Failed to add Cloudian group due to:", e);
+            checkResponseTimeOut(e);
+        }
+        return false;
+    }
+
+    public CloudianGroup listGroup(final String groupId) {
+        if (Strings.isNullOrEmpty(groupId)) {
+            return null;
+        }
+        LOG.debug("Trying to find Cloudian group with id=" + groupId);
+        try {
+            final HttpResponse response = get(String.format("/group?groupId=%s", groupId));
+            checkResponseOK(response);
+            if (checkEmptyResponse(response)) {
+                return null;
+            }
+            final ObjectMapper mapper = new ObjectMapper();
+            return mapper.readValue(response.getEntity().getContent(), CloudianGroup.class);
+        } catch (final IOException e) {
+            LOG.error("Failed to list Cloudian group due to:", e);
+            checkResponseTimeOut(e);
+        }
+        return null;
+    }
+
+    public List<CloudianGroup> listGroups() {
+        LOG.debug("Trying to list Cloudian groups");
+        try {
+            final HttpResponse response = get("/group/list");
+            checkResponseOK(response);
+            if (checkEmptyResponse(response)) {
+                return new ArrayList<>();
+            }
+            final ObjectMapper mapper = new ObjectMapper();
+            return Arrays.asList(mapper.readValue(response.getEntity().getContent(), CloudianGroup[].class));
+        } catch (final IOException e) {
+            LOG.error("Failed to list Cloudian groups due to:", e);
+            checkResponseTimeOut(e);
+        }
+        return new ArrayList<>();
+    }
+
+    public boolean updateGroup(final CloudianGroup group) {
+        if (group == null) {
+            return false;
+        }
+        LOG.debug("Updating Cloudian group: " + group);
+        try {
+            final HttpResponse response = post("/group", group);
+            return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
+        } catch (final IOException e) {
+            LOG.error("Failed to remove group due to:", e);
+            checkResponseTimeOut(e);
+        }
+        return false;
+    }
+
+    public boolean removeGroup(final String groupId) {
+        if (Strings.isNullOrEmpty(groupId)) {
+            return false;
+        }
+        LOG.debug("Removing Cloudian group id=" + groupId);
+        try {
+            final HttpResponse response = delete(String.format("/group?groupId=%s", groupId));
+            return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
+        } catch (final IOException e) {
+            LOG.error("Failed to remove group due to:", e);
+            checkResponseTimeOut(e);
+        }
+        return false;
+    }
+}
diff --git a/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/client/CloudianGroup.java b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/client/CloudianGroup.java
new file mode 100644
index 00000000000..0a3c4e475a1
--- /dev/null
+++ b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/client/CloudianGroup.java
@@ -0,0 +1,56 @@
+// 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.cloudian.client;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CloudianGroup {
+    String groupId;
+    String groupName;
+    Boolean active;
+
+    @Override
+    public String toString() {
+        return String.format("Cloudian Group [id=%s, name=%s, active=%s]", groupId, groupName, active);
+    }
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(String groupId) {
+        this.groupId = groupId;
+    }
+
+    public String getGroupName() {
+        return groupName;
+    }
+
+    public void setGroupName(String groupName) {
+        this.groupName = groupName;
+    }
+
+    public Boolean getActive() {
+        return active;
+    }
+
+    public void setActive(Boolean active) {
+        this.active = active;
+    }
+}
diff --git a/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/client/CloudianUser.java b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/client/CloudianUser.java
new file mode 100644
index 00000000000..88d45f90976
--- /dev/null
+++ b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/client/CloudianUser.java
@@ -0,0 +1,85 @@
+// 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.cloudian.client;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CloudianUser {
+    public static final String USER = "User";
+
+    String userId;
+    String groupId;
+    String userType;
+    String fullName;
+    String emailAddr;
+    Boolean active;
+
+    @Override
+    public String toString() {
+        return String.format("Cloudian User [id=%s, group id=%s, type=%s, active=%s, name=%s]", userId, groupId, userType, active, fullName);
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(String groupId) {
+        this.groupId = groupId;
+    }
+
+    public String getUserType() {
+        return userType;
+    }
+
+    public void setUserType(String userType) {
+        this.userType = userType;
+    }
+
+    public String getFullName() {
+        return fullName;
+    }
+
+    public void setFullName(String fullName) {
+        this.fullName = fullName;
+    }
+
+    public String getEmailAddr() {
+        return emailAddr;
+    }
+
+    public void setEmailAddr(String emailAddr) {
+        this.emailAddr = emailAddr;
+    }
+
+    public Boolean getActive() {
+        return active;
+    }
+
+    public void setActive(Boolean active) {
+        this.active = active;
+    }
+}
diff --git a/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/client/CloudianUtils.java b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/client/CloudianUtils.java
new file mode 100644
index 00000000000..faca579886e
--- /dev/null
+++ b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/client/CloudianUtils.java
@@ -0,0 +1,92 @@
+// 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.cloudian.client;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.log4j.Logger;
+
+import com.cloud.utils.HttpUtils;
+import com.google.common.base.Strings;
+
+public class CloudianUtils {
+
+    private static final Logger LOG = Logger.getLogger(CloudianUtils.class);
+    private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
+
+    /**
+     * Generates RFC-2104 compliant HMAC signature
+     * @param data
+     * @param key
+     * @return returns the generated signature or null on error
+     */
+    public static String generateHMACSignature(final String data, final String key) {
+        if (Strings. isNullOrEmpty(data) || Strings.isNullOrEmpty(key)) {
+            return null;
+        }
+        try {
+            final SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM);
+            final Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
+            mac.init(signingKey);
+            byte[] rawHmac = mac.doFinal(data.getBytes());
+            return Base64.encodeBase64String(rawHmac);
+        } catch (final Exception e) {
+            LOG.error("Failed to generate HMAC signature from provided data and key, due to: ", e);
+        }
+        return null;
+    }
+
+    /**
+     * Generates URL parameters for single-sign on URL
+     * @param user
+     * @param group
+     * @param ssoKey
+     * @return returns SSO URL parameters or null on error
+     */
+    public static String generateSSOUrl(final String cmcUrlPath, final String user, final String group, final String ssoKey) {
+        final StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append("user=").append(user);
+        stringBuilder.append("&group=").append(group);
+        stringBuilder.append("&timestamp=").append(System.currentTimeMillis());
+
+        final String signature = generateHMACSignature(stringBuilder.toString(), ssoKey);
+        if (Strings.isNullOrEmpty(signature)) {
+            return null;
+        }
+
+        try {
+            stringBuilder.append("&signature=").append(URLEncoder.encode(signature, HttpUtils.UTF_8));
+        } catch (final UnsupportedEncodingException e) {
+            return null;
+        }
+
+        stringBuilder.append("&redirect=");
+        if (group.equals("0")) {
+            stringBuilder.append("admin.htm");
+        } else {
+            stringBuilder.append("explorer.htm");
+        }
+
+        return cmcUrlPath + "ssosecurelogin.htm?" + stringBuilder.toString();
+    }
+}
diff --git a/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/response/CloudianEnabledResponse.java b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/response/CloudianEnabledResponse.java
new file mode 100644
index 00000000000..49d8cda2eaf
--- /dev/null
+++ b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/response/CloudianEnabledResponse.java
@@ -0,0 +1,42 @@
+// 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.cloudian.response;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+
+public class CloudianEnabledResponse extends BaseResponse {
+    @SerializedName(ApiConstants.ENABLED)
+    @Param(description = "the Cloudian connector enabled state")
+    private Boolean enabled;
+
+    @SerializedName(ApiConstants.URL)
+    @Param(description = "the Cloudian Management Console base URL")
+    private String cmcUrl;
+
+    public void setEnabled(Boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    public void setCmcUrl(String cmcUrl) {
+        this.cmcUrl = cmcUrl;
+    }
+}
diff --git a/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/response/CloudianSsoLoginResponse.java b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/response/CloudianSsoLoginResponse.java
new file mode 100644
index 00000000000..1731456e197
--- /dev/null
+++ b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/response/CloudianSsoLoginResponse.java
@@ -0,0 +1,34 @@
+// 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.cloudian.response;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+
+public class CloudianSsoLoginResponse extends BaseResponse {
+    @SerializedName(ApiConstants.URL)
+    @Param(description = "the sso redirect url")
+    private String ssoRedirectUrl;
+
+    public void setSsoRedirectUrl(final String ssoRedirectUrl) {
+        this.ssoRedirectUrl = ssoRedirectUrl;
+    }
+}
diff --git a/plugins/integrations/cloudian/test/org/apache/cloudstack/cloudian/CloudianClientTest.java b/plugins/integrations/cloudian/test/org/apache/cloudstack/cloudian/CloudianClientTest.java
new file mode 100644
index 00000000000..23ba1e1294b
--- /dev/null
+++ b/plugins/integrations/cloudian/test/org/apache/cloudstack/cloudian/CloudianClientTest.java
@@ -0,0 +1,416 @@
+// 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.cloudian;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.containing;
+import static com.github.tomakehurst.wiremock.client.WireMock.delete;
+import static com.github.tomakehurst.wiremock.client.WireMock.deleteRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.put;
+import static com.github.tomakehurst.wiremock.client.WireMock.putRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+
+import java.util.List;
+
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.cloudian.client.CloudianClient;
+import org.apache.cloudstack.cloudian.client.CloudianGroup;
+import org.apache.cloudstack.cloudian.client.CloudianUser;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.github.tomakehurst.wiremock.client.BasicCredentials;
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+
+public class CloudianClientTest {
+    private final int port = 14333;
+    private final int timeout = 2;
+    private final String adminUsername = "admin";
+    private final String adminPassword = "public";
+    private CloudianClient client;
+
+    @Rule
+    public WireMockRule wireMockRule = new WireMockRule(port);
+
+    @Before
+    public void setUp() throws Exception {
+        client = new CloudianClient("localhost", port, "http", adminUsername, adminPassword, false, timeout);
+    }
+
+    private CloudianUser getTestUser() {
+        final CloudianUser user = new CloudianUser();
+        user.setActive(true);
+        user.setUserId("someUserId");
+        user.setGroupId("someGroupId");
+        user.setUserType(CloudianUser.USER);
+        user.setFullName("John Doe");
+        return user;
+    }
+
+    private CloudianGroup getTestGroup() {
+        final CloudianGroup group = new CloudianGroup();
+        group.setActive(true);
+        group.setGroupId("someGroupId");
+        group.setGroupName("someGroupName");
+        return group;
+    }
+
+    ////////////////////////////////////////////////////////
+    //////////////// General API tests /////////////////////
+    ////////////////////////////////////////////////////////
+
+    @Test(expected = CloudRuntimeException.class)
+    public void testRequestTimeout() {
+        wireMockRule.stubFor(get(urlEqualTo("/group/list"))
+                .willReturn(aResponse()
+                        .withHeader("Content-Type", "application/json")
+                        .withStatus(200)
+                        .withFixedDelay(2 * timeout * 1000)
+                        .withBody("")));
+        client.listGroups();
+    }
+
+    @Test
+    public void testBasicAuth() {
+        wireMockRule.stubFor(get(urlEqualTo("/group/list"))
+                .willReturn(aResponse()
+                        .withStatus(200)
+                        .withBody("[]")));
+        client.listGroups();
+        verify(getRequestedFor(urlEqualTo("/group/list"))
+                .withBasicAuth(new BasicCredentials(adminUsername, adminPassword)));
+    }
+
+    @Test(expected = ServerApiException.class)
+    public void testBasicAuthFailure() {
+        wireMockRule.stubFor(get(urlPathMatching("/user"))
+                .willReturn(aResponse()
+                        .withStatus(401)
+                        .withBody("")));
+        client.listUser("someUserId", "somegGroupId");
+    }
+
+    /////////////////////////////////////////////////////
+    //////////////// User API tests /////////////////////
+    /////////////////////////////////////////////////////
+
+    @Test
+    public void addUserAccount() {
+        wireMockRule.stubFor(put(urlEqualTo("/user"))
+                .willReturn(aResponse()
+                        .withStatus(200)
+                        .withBody("")));
+
+        final CloudianUser user = getTestUser();
+        boolean result = client.addUser(user);
+        Assert.assertTrue(result);
+        verify(putRequestedFor(urlEqualTo("/user"))
+                .withRequestBody(containing("userId\":\"" + user.getUserId()))
+                .withHeader("Content-Type", equalTo("application/json")));
+    }
+
+    @Test
+    public void addUserAccountFail() {
+        wireMockRule.stubFor(put(urlEqualTo("/user"))
+                .willReturn(aResponse()
+                        .withStatus(400)
+                        .withBody("")));
+
+        final CloudianUser user = getTestUser();
+        boolean result = client.addUser(user);
+        Assert.assertFalse(result);
+    }
+
+    @Test
+    public void listUserAccount() {
+        final String userId = "someUser";
+        final String groupId = "someGroup";
+        wireMockRule.stubFor(get(urlPathMatching("/user?.*"))
+                .willReturn(aResponse()
+                        .withHeader("Content-Type", "application/json")
+                        .withBody("{\"userId\":\"someUser\",\"userType\":\"User\",\"fullName\":\"John Doe (jdoe)\",\"emailAddr\":\"j@doe.com\",\"address1\":null,\"address2\":null,\"city\":null,\"state\":null,\"zip\":null,\"country\":null,\"phone\":null,\"groupId\":\"someGroup\",\"website\":null,\"active\":\"true\",\"canonicalUserId\":\"b3940886468689d375ebf8747b151c37\",\"ldapEnabled\":false}")));
+
+        final CloudianUser user = client.listUser(userId, groupId);
+        Assert.assertEquals(user.getActive(), true);
+        Assert.assertEquals(user.getUserId(), userId);
+        Assert.assertEquals(user.getGroupId(), groupId);
+        Assert.assertEquals(user.getUserType(), "User");
+    }
+
+    @Test
+    public void listUserAccountFail() {
+        wireMockRule.stubFor(get(urlPathMatching("/user?.*"))
+                .willReturn(aResponse()
+                        .withHeader("Content-Type", "application/json")
+                        .withBody("")));
+
+        final CloudianUser user = client.listUser("abc", "xyz");
+        Assert.assertNull(user);
+    }
+
+    @Test
+    public void listUserAccounts() {
+        final String groupId = "someGroup";
+        wireMockRule.stubFor(get(urlPathMatching("/user/list?.*"))
+                .willReturn(aResponse()
+                        .withHeader("Content-Type", "application/json")
+                        .withBody("[{\"userId\":\"someUser\",\"userType\":\"User\",\"fullName\":\"John Doe (jdoe)\",\"emailAddr\":\"j@doe.com\",\"address1\":null,\"address2\":null,\"city\":null,\"state\":null,\"zip\":null,\"country\":null,\"phone\":null,\"groupId\":\"someGroup\",\"website\":null,\"active\":\"true\",\"canonicalUserId\":\"b3940886468689d375ebf8747b151c37\",\"ldapEnabled\":false}]")));
+
+        final List<CloudianUser> users = client.listUsers(groupId);
+        Assert.assertEquals(users.size(), 1);
+        Assert.assertEquals(users.get(0).getActive(), true);
+        Assert.assertEquals(users.get(0).getGroupId(), groupId);
+    }
+
+    @Test
+    public void testEmptyListUsersResponse() {
+        wireMockRule.stubFor(get(urlPathMatching("/user/list"))
+                .willReturn(aResponse()
+                        .withHeader("Content-Type", "application/json")
+                        .withStatus(204)
+                        .withBody("")));
+        Assert.assertTrue(client.listUsers("someGroup").size() == 0);
+
+        wireMockRule.stubFor(get(urlPathMatching("/user"))
+                .willReturn(aResponse()
+                        .withHeader("Content-Type", "application/json")
+                        .withStatus(204)
+                        .withBody("")));
+        Assert.assertNull(client.listUser("someUserId", "someGroupId"));
+    }
+
+    @Test
+    public void listUserAccountsFail() {
+        wireMockRule.stubFor(get(urlPathMatching("/user/list?.*"))
+                .willReturn(aResponse()
+                        .withHeader("Content-Type", "application/json")
+                        .withBody("")));
+
+        final List<CloudianUser> users = client.listUsers("xyz");
+        Assert.assertEquals(users.size(), 0);
+    }
+
+    @Test
+    public void updateUserAccount() {
+        wireMockRule.stubFor(post(urlEqualTo("/user"))
+                .willReturn(aResponse()
+                        .withStatus(200)
+                        .withBody("")));
+
+        final CloudianUser user = getTestUser();
+        boolean result = client.updateUser(user);
+        Assert.assertTrue(result);
+        verify(postRequestedFor(urlEqualTo("/user"))
+                .withRequestBody(containing("userId\":\"" + user.getUserId()))
+                .withHeader("Content-Type", equalTo("application/json")));
+    }
+
+    @Test
+    public void updateUserAccountFail() {
+        wireMockRule.stubFor(post(urlEqualTo("/user"))
+                .willReturn(aResponse()
+                        .withStatus(400)
+                        .withBody("")));
+
+        boolean result = client.updateUser(getTestUser());
+        Assert.assertFalse(result);
+    }
+
+    @Test
+    public void removeUserAccount() {
+        wireMockRule.stubFor(delete(urlPathMatching("/user.*"))
+                .willReturn(aResponse()
+                        .withStatus(200)
+                        .withBody("")));
+        final CloudianUser user = getTestUser();
+        boolean result = client.removeUser(user.getUserId(), user.getGroupId());
+        Assert.assertTrue(result);
+        verify(deleteRequestedFor(urlPathMatching("/user.*"))
+                .withQueryParam("userId", equalTo(user.getUserId())));
+    }
+
+    @Test
+    public void removeUserAccountFail() {
+        wireMockRule.stubFor(delete(urlPathMatching("/user.*"))
+                .willReturn(aResponse()
+                        .withStatus(400)
+                        .withBody("")));
+        final CloudianUser user = getTestUser();
+        boolean result = client.removeUser(user.getUserId(), user.getGroupId());
+        Assert.assertFalse(result);
+    }
+
+    //////////////////////////////////////////////////////
+    //////////////// Group API tests /////////////////////
+    //////////////////////////////////////////////////////
+
+    @Test
+    public void addGroup() {
+        wireMockRule.stubFor(put(urlEqualTo("/group"))
+                .willReturn(aResponse()
+                        .withStatus(200)
+                        .withBody("")));
+
+        final CloudianGroup group = getTestGroup();
+        boolean result = client.addGroup(group);
+        Assert.assertTrue(result);
+        verify(putRequestedFor(urlEqualTo("/group"))
+                .withRequestBody(containing("groupId\":\"someGroupId"))
+                .withHeader("Content-Type", equalTo("application/json")));
+    }
+
+    @Test
+    public void addGroupFail() throws Exception {
+        wireMockRule.stubFor(put(urlEqualTo("/group"))
+                .willReturn(aResponse()
+                        .withStatus(400)
+                        .withBody("")));
+
+        final CloudianGroup group = getTestGroup();
+        boolean result = client.addGroup(group);
+        Assert.assertFalse(result);
+    }
+
+    @Test
+    public void listGroup() {
+        final String groupId = "someGroup";
+        wireMockRule.stubFor(get(urlPathMatching("/group.*"))
+                .willReturn(aResponse()
+                        .withHeader("Content-Type", "application/json")
+                        .withBody("{\"groupId\":\"someGroup\",\"groupName\":\"/someDomain\",\"ldapGroup\":null,\"active\":\"true\",\"ldapEnabled\":false,\"ldapServerURL\":null,\"ldapUserDNTemplate\":null,\"ldapSearch\":null,\"ldapSearchUserBase\":null,\"ldapMatchAttribute\":null}")));
+
+        final CloudianGroup group = client.listGroup(groupId);
+        Assert.assertEquals(group.getActive(), true);
+        Assert.assertEquals(group.getGroupId(), groupId);
+    }
+
+    @Test
+    public void listGroupFail() {
+        wireMockRule.stubFor(get(urlPathMatching("/group.*"))
+                .willReturn(aResponse()
+                        .withHeader("Content-Type", "application/json")
+                        .withBody("")));
+
+        final CloudianGroup group = client.listGroup("xyz");
+        Assert.assertNull(group);
+    }
+
+    @Test
+    public void listGroups() {
+        final String groupId = "someGroup";
+        wireMockRule.stubFor(get(urlEqualTo("/group/list"))
+                .willReturn(aResponse()
+                        .withHeader("Content-Type", "application/json")
+                        .withBody("[{\"groupId\":\"someGroup\",\"groupName\":\"/someDomain\",\"ldapGroup\":null,\"active\":\"true\",\"ldapEnabled\":false,\"ldapServerURL\":null,\"ldapUserDNTemplate\":null,\"ldapSearch\":null,\"ldapSearchUserBase\":null,\"ldapMatchAttribute\":null}]")));
+
+        final List<CloudianGroup> groups = client.listGroups();
+        Assert.assertEquals(groups.size(), 1);
+        Assert.assertEquals(groups.get(0).getActive(), true);
+        Assert.assertEquals(groups.get(0).getGroupId(), groupId);
+    }
+
+    @Test
+    public void listGroupsFail() {
+        wireMockRule.stubFor(get(urlEqualTo("/group/list"))
+                .willReturn(aResponse()
+                        .withHeader("Content-Type", "application/json")
+                        .withBody("")));
+
+        final List<CloudianGroup> groups = client.listGroups();
+        Assert.assertEquals(groups.size(), 0);
+    }
+
+    @Test
+    public void testEmptyListGroupResponse() {
+        wireMockRule.stubFor(get(urlEqualTo("/group/list"))
+                .willReturn(aResponse()
+                        .withHeader("Content-Type", "application/json")
+                        .withStatus(204)
+                        .withBody("")));
+
+        Assert.assertTrue(client.listGroups().size() == 0);
+
+
+        wireMockRule.stubFor(get(urlPathMatching("/group"))
+                .willReturn(aResponse()
+                        .withHeader("Content-Type", "application/json")
+                        .withStatus(204)
+                        .withBody("")));
+        Assert.assertNull(client.listGroup("someGroup"));
+    }
+
+    @Test
+    public void updateGroup() {
+        wireMockRule.stubFor(post(urlEqualTo("/group"))
+                .willReturn(aResponse()
+                        .withStatus(200)
+                        .withBody("")));
+
+        final CloudianGroup group = getTestGroup();
+        boolean result = client.updateGroup(group);
+        Assert.assertTrue(result);
+        verify(postRequestedFor(urlEqualTo("/group"))
+                .withRequestBody(containing("groupId\":\"" + group.getGroupId()))
+                .withHeader("Content-Type", equalTo("application/json")));
+    }
+
+    @Test
+    public void updateGroupFail() {
+        wireMockRule.stubFor(post(urlEqualTo("/group"))
+                .willReturn(aResponse()
+                        .withStatus(400)
+                        .withBody("")));
+
+        boolean result = client.updateGroup(getTestGroup());
+        Assert.assertFalse(result);
+    }
+
+    @Test
+    public void removeGroup() {
+        wireMockRule.stubFor(delete(urlPathMatching("/group.*"))
+                .willReturn(aResponse()
+                        .withStatus(200)
+                        .withBody("")));
+        final CloudianGroup group = getTestGroup();
+        boolean result = client.removeGroup(group.getGroupId());
+        Assert.assertTrue(result);
+        verify(deleteRequestedFor(urlPathMatching("/group.*"))
+                .withQueryParam("groupId", equalTo(group.getGroupId())));
+    }
+
+    @Test
+    public void removeGroupFail() {
+        wireMockRule.stubFor(delete(urlPathMatching("/group.*"))
+                .willReturn(aResponse()
+                        .withStatus(400)
+                        .withBody("")));
+        final CloudianGroup group = getTestGroup();
+        boolean result = client.removeGroup(group.getGroupId());
+        Assert.assertFalse(result);
+    }
+}
\ No newline at end of file
diff --git a/plugins/pom.xml b/plugins/pom.xml
index 1ee7af58c60..f57fbdc126c 100755
--- a/plugins/pom.xml
+++ b/plugins/pom.xml
@@ -107,6 +107,7 @@
     <module>network-elements/vxlan</module>
     <module>network-elements/globodns</module>
     <module>database/quota</module>
+    <module>integrations/cloudian</module>
     <module>integrations/prometheus</module>
   </modules>
 
diff --git a/pom.xml b/pom.xml
index 2ff63e6f455..96fe92503e2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -124,6 +124,7 @@
     <cs.cxf.version>3.1.4</cs.cxf.version>
     <cs.groovy.version>2.4.7</cs.groovy.version>
     <cs.nitro.version>10.1</cs.nitro.version>
+    <cs.wiremock.version>2.8.0</cs.wiremock.version>
   </properties>
 
   <distributionManagement>
diff --git a/ui/plugins/cloudian/cloudian.css b/ui/plugins/cloudian/cloudian.css
new file mode 100644
index 00000000000..447cf434b32
--- /dev/null
+++ b/ui/plugins/cloudian/cloudian.css
@@ -0,0 +1,18 @@
+/*
+* 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.
+*/
diff --git a/ui/plugins/cloudian/cloudian.js b/ui/plugins/cloudian/cloudian.js
new file mode 100644
index 00000000000..1b8a35ab1e9
--- /dev/null
+++ b/ui/plugins/cloudian/cloudian.js
@@ -0,0 +1,66 @@
+// 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.
+
+(function (cloudStack) {
+  cloudStack.plugins.cloudian = function(plugin) {
+
+    plugin.ui.addSection({
+      id: 'cloudian',
+      title: 'Cloudian Storage',
+      showOnNavigation: true,
+      preFilter: function(args) {
+        var pluginEnabled = false;
+        $.ajax({
+            url: createURL('cloudianIsEnabled'),
+            async: false,
+            success: function(json) {
+                var response = json.cloudianisenabledresponse.cloudianisenabled;
+                pluginEnabled = response.enabled;
+                if (pluginEnabled) {
+                    var cloudianLogoutUrl = response.url + "logout.htm?";
+                    onLogoutCallback = function() {
+                        g_loginResponse = null;
+                        var csUrl = window.location.href;
+                        var redirect = "redirect=" + encodeURIComponent(csUrl);
+                        window.location.replace(cloudianLogoutUrl + redirect);
+                        return false;
+                    };
+                }
+            }
+        });
+        return pluginEnabled;
+      },
+
+      show: function() {
+        var description = 'Cloudian Management Console should open in another window.';
+        $.ajax({
+            url: createURL('cloudianSsoLogin'),
+            async: false,
+            success: function(json) {
+                var response = json.cloudianssologinresponse.cloudianssologin;
+                var cmcWindow = window.open(response.url, "CMCWindow");
+                cmcWindow.focus();
+            },
+            error: function(data) {
+                description = 'Single-Sign-On failed for Cloudian Management Console. Please ask your administrator to fix integration issues.';
+            }
+        });
+        return $('<div style="margin: 20px;">').html(description);
+      }
+    });
+  };
+}(cloudStack));
diff --git a/ui/plugins/cloudian/config.js b/ui/plugins/cloudian/config.js
new file mode 100644
index 00000000000..b72cd5f2ff9
--- /dev/null
+++ b/ui/plugins/cloudian/config.js
@@ -0,0 +1,25 @@
+// 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.
+(function (cloudStack) {
+  cloudStack.plugins.cloudian.config = {
+    title: 'Cloudian Storage',
+    desc: 'Cloudian Storage',
+    externalLink: 'https://cloudian.com/',
+    authorName: 'Cloudian Inc.',
+    authorEmail: 'info@cloudian.com '
+  };
+}(cloudStack));
diff --git a/ui/plugins/cloudian/icon.png b/ui/plugins/cloudian/icon.png
new file mode 100644
index 00000000000..d18ec237678
Binary files /dev/null and b/ui/plugins/cloudian/icon.png differ
diff --git a/ui/plugins/plugins.js b/ui/plugins/plugins.js
index 21da7a07f4d..6edfe88fe1d 100644
--- a/ui/plugins/plugins.js
+++ b/ui/plugins/plugins.js
@@ -17,6 +17,7 @@
 (function($, cloudStack) {
   cloudStack.plugins = [
     //'testPlugin',
+    'cloudian',
     'quota'
   ];
 }(jQuery, cloudStack));


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services