You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by bh...@apache.org on 2017/10/25 05:19:50 UTC
[cloudstack] branch master updated: CLOUDSTACK-10103: Cloudian
Connector for CloudStack (#2284)
This is an automated email from the ASF dual-hosted git repository.
bhaisaab 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 b6dc40f CLOUDSTACK-10103: Cloudian Connector for CloudStack (#2284)
b6dc40f is described below
commit b6dc40faefdab52c2db35232fdab98e1c65c0b9e
Author: Rohit Yadav <bh...@apache.org>
AuthorDate: Wed Oct 25 10:49:45 2017 +0530
CLOUDSTACK-10103: Cloudian Connector for CloudStack (#2284)
Several organizations use Cloudian as S3 provider, this implements the
Cloudian Management Console connector for CloudStack that can do the
following:
- Provide ease in connector configuration using CloudStack global
settings
- Perform SSO from CloudStack UI into Cloudian Management Console (CMC)
when the connector is enabled
- Automatic provisioning and de-provisioning of CloudStack accounts and
domains as Cloudian users and groups respectively
- During CloudStack UI logout, logout user from CMC
- CloudStack account will be mapped to Cloudian Users, and CloudStack
domain will be mapped to Cloudian Groups.
- The CloudStack admin account is mapped to Cloudian admin (user name
configurable).
- The user/group provisioning will be from CloudStack to Cloudian only,
i.e. user/group addition/removal/updation/deactivation in Cloudian
portal (CMC) won't propagate the changes to CloudStack.
FS: https://cwiki.apache.org/confluence/display/CLOUDSTACK/Cloudian+Connector+for+CloudStack
New APIs:
- `cloudianIsEnabled`: API to check whether Cloudian Connector is enabled.
- `cloudianSsoLogin`: Performs SSO for the logged-in, requesting user
and returns the URL that can be used to perform
SSO and log into CMC.
New Global Settings:
- cloudian.connector.enabled (false)
If set to true, this enables the Cloudian Connector for CloudStack.
Restarting management server(s) is required.
- cloudian.admin.host (s3-admin.cloudian.com)
The host where Cloudian Admin services are accessible.
- cloudian.admin.port (19443)
The admin service port.
- cloudian.admin.protocol (https)
The admin service API scheme/protocol.
- cloudian.validate.ssl (true)
When set to true, this validates the certificate of the https-enabled
admin API service.
- cloudian.admin.user (sysadmin)
The admin user's name when making (admin) API calls.
- cloudian.admin.password (public)
The admin password used when making (admin) API calls.
- cloudian.api.request.timeout (5)
The API request timeout in seconds used by the internal HTTP/s client.
- cloudian.cmc.admin.user (admin)
The CMC admin user's name.
- cloudian.cmc.host (cmc.cloudian.com)
The CMC host.
- cloudian.cmc.port (8443)
The CMC service port.
- cloudian.cmc.protocol (https)
The CMC service scheme/protocol.
- cloudian.sso.key (ss0sh5r3dk3y)
The Single-Sign-On shared key.
Signed-off-by: Rohit Yadav <ro...@shapeblue.com>
---
client/pom.xml | 5 +
plugins/integrations/cloudian/pom.xml | 60 +++
.../META-INF/cloudstack/cloudian/module.properties | 18 +
.../cloudian/spring-cloudian-context.xml | 25 ++
.../cloudstack/cloudian/CloudianConnector.java | 82 ++++
.../cloudstack/cloudian/CloudianConnectorImpl.java | 345 +++++++++++++++++
.../cloudian/api/CloudianIsEnabledCmd.java | 65 ++++
.../cloudian/api/CloudianSsoLoginCmd.java | 70 ++++
.../cloudstack/cloudian/client/CloudianClient.java | 347 +++++++++++++++++
.../cloudstack/cloudian/client/CloudianGroup.java | 56 +++
.../cloudstack/cloudian/client/CloudianUser.java | 85 +++++
.../cloudstack/cloudian/client/CloudianUtils.java | 92 +++++
.../cloudian/response/CloudianEnabledResponse.java | 32 +-
.../response/CloudianSsoLoginResponse.java | 24 +-
.../cloudstack/cloudian/CloudianClientTest.java | 416 +++++++++++++++++++++
plugins/pom.xml | 1 +
pom.xml | 1 +
ui/plugins/cloudian/cloudian.css | 18 +
ui/plugins/cloudian/cloudian.js | 66 ++++
ui/plugins/{plugins.js => cloudian/config.js} | 15 +-
ui/plugins/cloudian/icon.png | Bin 0 -> 1099 bytes
ui/plugins/plugins.js | 1 +
22 files changed, 1806 insertions(+), 18 deletions(-)
diff --git a/client/pom.xml b/client/pom.xml
index ae0fcaa..2b52833 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 0000000..3e2b635
--- /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 0000000..762c636
--- /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 0000000..71ed52d
--- /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 0000000..c04d70c
--- /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 0000000..cfb23da
--- /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 0000000..fdca871
--- /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 0000000..7bdd7fd
--- /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 0000000..11f2055
--- /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 0000000..0a3c4e4
--- /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 0000000..88d45f9
--- /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 0000000..faca579
--- /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("×tamp=").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/ui/plugins/plugins.js b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/response/CloudianEnabledResponse.java
similarity index 52%
copy from ui/plugins/plugins.js
copy to plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/response/CloudianEnabledResponse.java
index 21da7a0..49d8cda 100644
--- a/ui/plugins/plugins.js
+++ b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/response/CloudianEnabledResponse.java
@@ -14,9 +14,29 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-(function($, cloudStack) {
- cloudStack.plugins = [
- //'testPlugin',
- 'quota'
- ];
-}(jQuery, cloudStack));
+
+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/ui/plugins/plugins.js b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/response/CloudianSsoLoginResponse.java
similarity index 59%
copy from ui/plugins/plugins.js
copy to plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/response/CloudianSsoLoginResponse.java
index 21da7a0..1731456 100644
--- a/ui/plugins/plugins.js
+++ b/plugins/integrations/cloudian/src/org/apache/cloudstack/cloudian/response/CloudianSsoLoginResponse.java
@@ -14,9 +14,21 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-(function($, cloudStack) {
- cloudStack.plugins = [
- //'testPlugin',
- 'quota'
- ];
-}(jQuery, cloudStack));
+
+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 0000000..23ba1e1
--- /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 1ee7af5..f57fbdc 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 2ff63e6..96fe925 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 0000000..447cf43
--- /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 0000000..1b8a35a
--- /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/plugins.js b/ui/plugins/cloudian/config.js
similarity index 75%
copy from ui/plugins/plugins.js
copy to ui/plugins/cloudian/config.js
index 21da7a0..b72cd5f 100644
--- a/ui/plugins/plugins.js
+++ b/ui/plugins/cloudian/config.js
@@ -14,9 +14,12 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
-(function($, cloudStack) {
- cloudStack.plugins = [
- //'testPlugin',
- 'quota'
- ];
-}(jQuery, cloudStack));
+(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 0000000..d18ec23
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 21da7a0..6edfe88 100644
--- a/ui/plugins/plugins.js
+++ b/ui/plugins/plugins.js
@@ -17,6 +17,7 @@
(function($, cloudStack) {
cloudStack.plugins = [
//'testPlugin',
+ 'cloudian',
'quota'
];
}(jQuery, cloudStack));
--
To stop receiving notification emails like this one, please contact
['"commits@cloudstack.apache.org" <co...@cloudstack.apache.org>'].