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>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-plugin-integrations-cloudian-connector</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-plugin-integrations-prometheus-exporter</artifactId>
|
||||
|
||||
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/globodns</module>
|
||||
<module>database/quota</module>
|
||||
<module>integrations/cloudian</module>
|
||||
<module>integrations/prometheus</module>
|
||||
</modules>
|
||||
|
||||
|
||||
1
pom.xml
1
pom.xml
@ -124,6 +124,7 @@
|
||||
<cs.cxf.version>3.1.4</cs.cxf.version>
|
||||
<cs.groovy.version>2.4.7</cs.groovy.version>
|
||||
<cs.nitro.version>10.1</cs.nitro.version>
|
||||
<cs.wiremock.version>2.8.0</cs.wiremock.version>
|
||||
</properties>
|
||||
|
||||
<distributionManagement>
|
||||
|
||||
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) {
|
||||
cloudStack.plugins = [
|
||||
//'testPlugin',
|
||||
'cloudian',
|
||||
'quota'
|
||||
];
|
||||
}(jQuery, cloudStack));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user