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:
Rohit Yadav 2017-10-25 10:49:45 +05:30 committed by GitHub
parent bd953d811f
commit b6dc40faef
22 changed files with 1854 additions and 0 deletions

View File

@ -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>

View 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>

View 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.
name=cloudian
parent=api

View 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.
-->
<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>

View File

@ -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();
}

View File

@ -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
};
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,92 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.cloudian.client;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.Logger;
import com.cloud.utils.HttpUtils;
import com.google.common.base.Strings;
public class CloudianUtils {
private static final Logger LOG = Logger.getLogger(CloudianUtils.class);
private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
/**
* Generates RFC-2104 compliant HMAC signature
* @param data
* @param key
* @return returns the generated signature or null on error
*/
public static String generateHMACSignature(final String data, final String key) {
if (Strings. isNullOrEmpty(data) || Strings.isNullOrEmpty(key)) {
return null;
}
try {
final SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM);
final Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(data.getBytes());
return Base64.encodeBase64String(rawHmac);
} catch (final Exception e) {
LOG.error("Failed to generate HMAC signature from provided data and key, due to: ", e);
}
return null;
}
/**
* Generates URL parameters for single-sign on URL
* @param user
* @param group
* @param ssoKey
* @return returns SSO URL parameters or null on error
*/
public static String generateSSOUrl(final String cmcUrlPath, final String user, final String group, final String ssoKey) {
final StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("user=").append(user);
stringBuilder.append("&group=").append(group);
stringBuilder.append("&timestamp=").append(System.currentTimeMillis());
final String signature = generateHMACSignature(stringBuilder.toString(), ssoKey);
if (Strings.isNullOrEmpty(signature)) {
return null;
}
try {
stringBuilder.append("&signature=").append(URLEncoder.encode(signature, HttpUtils.UTF_8));
} catch (final UnsupportedEncodingException e) {
return null;
}
stringBuilder.append("&redirect=");
if (group.equals("0")) {
stringBuilder.append("admin.htm");
} else {
stringBuilder.append("explorer.htm");
}
return cmcUrlPath + "ssosecurelogin.htm?" + stringBuilder.toString();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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>

View 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.
*/

View 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));

View 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));

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -17,6 +17,7 @@
(function($, cloudStack) {
cloudStack.plugins = [
//'testPlugin',
'cloudian',
'quota'
];
}(jQuery, cloudStack));