mirror of
				https://github.com/apache/cloudstack.git
				synced 2025-10-26 08:42:29 +01:00 
			
		
		
		
	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 <rohit.yadav@shapeblue.com>
This commit is contained in:
		
							parent
							
								
									bd953d811f
								
							
						
					
					
						commit
						b6dc40faef
					
				| @ -414,6 +414,11 @@ | |||||||
|       <artifactId>cloud-plugin-database-quota</artifactId> |       <artifactId>cloud-plugin-database-quota</artifactId> | ||||||
|       <version>${project.version}</version> |       <version>${project.version}</version> | ||||||
|     </dependency> |     </dependency> | ||||||
|  |     <dependency> | ||||||
|  |       <groupId>org.apache.cloudstack</groupId> | ||||||
|  |       <artifactId>cloud-plugin-integrations-cloudian-connector</artifactId> | ||||||
|  |       <version>${project.version}</version> | ||||||
|  |     </dependency> | ||||||
|     <dependency> |     <dependency> | ||||||
|       <groupId>org.apache.cloudstack</groupId> |       <groupId>org.apache.cloudstack</groupId> | ||||||
|       <artifactId>cloud-plugin-integrations-prometheus-exporter</artifactId> |       <artifactId>cloud-plugin-integrations-prometheus-exporter</artifactId> | ||||||
|  | |||||||
							
								
								
									
										60
									
								
								plugins/integrations/cloudian/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								plugins/integrations/cloudian/pom.xml
									
									
									
									
									
										Normal file
									
								
							| @ -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> | ||||||
| @ -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 | ||||||
| @ -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> | ||||||
| @ -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(); | ||||||
|  | } | ||||||
| @ -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 | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -107,6 +107,7 @@ | |||||||
|     <module>network-elements/vxlan</module> |     <module>network-elements/vxlan</module> | ||||||
|     <module>network-elements/globodns</module> |     <module>network-elements/globodns</module> | ||||||
|     <module>database/quota</module> |     <module>database/quota</module> | ||||||
|  |     <module>integrations/cloudian</module> | ||||||
|     <module>integrations/prometheus</module> |     <module>integrations/prometheus</module> | ||||||
|   </modules> |   </modules> | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								pom.xml
									
									
									
									
									
								
							| @ -124,6 +124,7 @@ | |||||||
|     <cs.cxf.version>3.1.4</cs.cxf.version> |     <cs.cxf.version>3.1.4</cs.cxf.version> | ||||||
|     <cs.groovy.version>2.4.7</cs.groovy.version> |     <cs.groovy.version>2.4.7</cs.groovy.version> | ||||||
|     <cs.nitro.version>10.1</cs.nitro.version> |     <cs.nitro.version>10.1</cs.nitro.version> | ||||||
|  |     <cs.wiremock.version>2.8.0</cs.wiremock.version> | ||||||
|   </properties> |   </properties> | ||||||
| 
 | 
 | ||||||
|   <distributionManagement> |   <distributionManagement> | ||||||
|  | |||||||
							
								
								
									
										18
									
								
								ui/plugins/cloudian/cloudian.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								ui/plugins/cloudian/cloudian.css
									
									
									
									
									
										Normal file
									
								
							| @ -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. | ||||||
|  | */ | ||||||
							
								
								
									
										66
									
								
								ui/plugins/cloudian/cloudian.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								ui/plugins/cloudian/cloudian.js
									
									
									
									
									
										Normal file
									
								
							| @ -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)); | ||||||
							
								
								
									
										25
									
								
								ui/plugins/cloudian/config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								ui/plugins/cloudian/config.js
									
									
									
									
									
										Normal file
									
								
							| @ -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)); | ||||||
							
								
								
									
										
											BIN
										
									
								
								ui/plugins/cloudian/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								ui/plugins/cloudian/icon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 KiB | 
| @ -17,6 +17,7 @@ | |||||||
| (function($, cloudStack) { | (function($, cloudStack) { | ||||||
|   cloudStack.plugins = [ |   cloudStack.plugins = [ | ||||||
|     //'testPlugin',
 |     //'testPlugin',
 | ||||||
|  |     'cloudian', | ||||||
|     'quota' |     'quota' | ||||||
|   ]; |   ]; | ||||||
| }(jQuery, cloudStack)); | }(jQuery, cloudStack)); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user