mirror of https://github.com/apache/cloudstack.git
Merge branch 'saml2'
Implements CLOUDSTACK-7083 Branch: saml2 Proposal: http://markmail.org/message/4ba4ztmqpud3l4uo JIRA ticket: https://issues.apache.org/jira/browse/CLOUDSTACK-7083 FS: https://cwiki.apache.org/confluence/display/CLOUDSTACK/SAML+2.0+Plugin Unit tests: Tests for each auth cmd class, SAMLUtils and SAMLAuthenticator, fixes unit test for ApiServlet Build status: clean build works with unit tests, testing using mvn3.0.5 and jdk 1.7
This commit is contained in:
commit
97ed5ff636
|
|
@ -35,6 +35,11 @@
|
|||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>servlet-api</artifactId>
|
||||
<version>${cs.servlet.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-framework-db</artifactId>
|
||||
|
|
|
|||
|
|
@ -514,6 +514,7 @@ public class ApiConstants {
|
|||
public static final String VMPROFILE_ID = "vmprofileid";
|
||||
public static final String VMGROUP_ID = "vmgroupid";
|
||||
public static final String CS_URL = "csurl";
|
||||
public static final String IDP_URL = "idpurl";
|
||||
public static final String SCALEUP_POLICY_IDS = "scaleuppolicyids";
|
||||
public static final String SCALEDOWN_POLICY_IDS = "scaledownpolicyids";
|
||||
public static final String SCALEUP_POLICIES = "scaleuppolicies";
|
||||
|
|
|
|||
|
|
@ -14,23 +14,19 @@
|
|||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
package com.cloud.api;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
package org.apache.cloudstack.api;
|
||||
|
||||
import com.cloud.exception.CloudAuthenticationException;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.Map;
|
||||
|
||||
public interface ApiServerService {
|
||||
public boolean verifyRequest(Map<String, Object[]> requestParameters, Long userId) throws ServerApiException;
|
||||
|
||||
public Long fetchDomainId(String domainUUID);
|
||||
|
||||
public void loginUser(HttpSession session, String username, String password, Long domainId, String domainPath, String loginIpAddress,
|
||||
Map<String, Object[]> requestParameters) throws CloudAuthenticationException;
|
||||
public ResponseObject loginUser(HttpSession session, String username, String password, Long domainId, String domainPath, String loginIpAddress,
|
||||
Map<String, Object[]> requestParameters) throws CloudAuthenticationException;
|
||||
|
||||
public void logoutUser(long userId);
|
||||
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package com.cloud.api.auth;
|
||||
package org.apache.cloudstack.api.auth;
|
||||
|
||||
import com.cloud.utils.component.PluggableService;
|
||||
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
package com.cloud.api.auth;
|
||||
package org.apache.cloudstack.api.auth;
|
||||
|
||||
public enum APIAuthenticationType {
|
||||
LOGIN_API, LOGOUT_API
|
||||
|
|
@ -14,12 +14,13 @@
|
|||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
package com.cloud.api.auth;
|
||||
package org.apache.cloudstack.api.auth;
|
||||
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/*
|
||||
|
|
@ -36,6 +37,8 @@ public interface APIAuthenticator {
|
|||
public String authenticate(String command, Map<String, Object[]> params,
|
||||
HttpSession session, String remoteAddress, String responseType,
|
||||
StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException;
|
||||
|
||||
public APIAuthenticationType getAPIType();
|
||||
|
||||
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators);
|
||||
}
|
||||
|
|
@ -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.
|
||||
package org.apache.cloudstack.api.auth;
|
||||
|
||||
import com.cloud.utils.component.Adapter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PluggableAPIAuthenticator extends Adapter {
|
||||
public List<Class<?>> getAuthCommands();
|
||||
}
|
||||
|
|
@ -21,8 +21,11 @@
|
|||
### CloudStack authentication commands
|
||||
login=15
|
||||
logout=15
|
||||
|
||||
### SAML SSO/SLO commands
|
||||
samlsso=15
|
||||
samlslo=15
|
||||
getSPMetadata=15
|
||||
|
||||
### Account commands
|
||||
createAccount=7
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@
|
|||
value="com.cloud.server.auth.UserAuthenticator" />
|
||||
</bean>
|
||||
|
||||
<bean class="org.apache.cloudstack.spring.lifecycle.registry.RegistryLifecycle">
|
||||
<property name="registry" ref="pluggableAPIAuthenticatorsRegistry" />
|
||||
<property name="typeClass"
|
||||
value=" org.apache.cloudstack.api.auth.PluggableAPIAuthenticator" />
|
||||
</bean>
|
||||
|
||||
<bean class="org.apache.cloudstack.spring.lifecycle.registry.RegistryLifecycle">
|
||||
<property name="registry" ref="securityCheckersRegistry" />
|
||||
<property name="typeClass"
|
||||
|
|
|
|||
|
|
@ -33,7 +33,14 @@
|
|||
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
|
||||
<property name="orderConfigKey" value="user.authenticators.order" />
|
||||
<property name="excludeKey" value="user.authenticators.exclude" />
|
||||
<property name="orderConfigDefault" value="SHA256SALT,MD5,LDAP,PLAINTEXT" />
|
||||
<property name="orderConfigDefault" value="SHA256SALT,MD5,LDAP,SAML2,PLAINTEXT" />
|
||||
</bean>
|
||||
|
||||
<bean id="pluggableAPIAuthenticatorsRegistry"
|
||||
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
|
||||
<property name="orderConfigKey" value="pluggableApi.authenticators.order" />
|
||||
<property name="excludeKey" value="pluggableApi.authenticators.exclude" />
|
||||
<property name="orderConfigDefault" value="SAML2Auth" />
|
||||
</bean>
|
||||
|
||||
<bean id="userPasswordEncodersRegistry"
|
||||
|
|
|
|||
|
|
@ -35,7 +35,17 @@
|
|||
<dependency>
|
||||
<groupId>org.opensaml</groupId>
|
||||
<artifactId>opensaml</artifactId>
|
||||
<version>2.6.1</version>
|
||||
<version>${cs.opensaml.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-utils</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
|
|||
|
|
@ -25,8 +25,12 @@
|
|||
http://www.springframework.org/schema/context
|
||||
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
|
||||
|
||||
<bean id="SAML2UserAuthenticator" class="org.apache.cloudstack.SAML2UserAuthenticator">
|
||||
<bean id="SAML2UserAuthenticator" class="org.apache.cloudstack.saml.SAML2UserAuthenticator">
|
||||
<property name="name" value="SAML2"/>
|
||||
</bean>
|
||||
|
||||
<bean id="SAML2Manager" class="org.apache.cloudstack.saml.SAML2AuthManagerImpl">
|
||||
<property name="name" value="SAML2Auth"/>
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
// 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.api.command;
|
||||
|
||||
import com.cloud.api.response.ApiResponseSerializer;
|
||||
import com.cloud.user.Account;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.ApiServerService;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationType;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticator;
|
||||
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
|
||||
import org.apache.cloudstack.api.response.SAMLMetaDataResponse;
|
||||
import org.apache.cloudstack.saml.SAML2AuthManager;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.opensaml.Configuration;
|
||||
import org.opensaml.DefaultBootstrap;
|
||||
import org.opensaml.common.xml.SAMLConstants;
|
||||
import org.opensaml.saml2.core.NameIDType;
|
||||
import org.opensaml.saml2.metadata.AssertionConsumerService;
|
||||
import org.opensaml.saml2.metadata.EntityDescriptor;
|
||||
import org.opensaml.saml2.metadata.KeyDescriptor;
|
||||
import org.opensaml.saml2.metadata.NameIDFormat;
|
||||
import org.opensaml.saml2.metadata.SPSSODescriptor;
|
||||
import org.opensaml.saml2.metadata.SingleLogoutService;
|
||||
import org.opensaml.saml2.metadata.impl.AssertionConsumerServiceBuilder;
|
||||
import org.opensaml.saml2.metadata.impl.EntityDescriptorBuilder;
|
||||
import org.opensaml.saml2.metadata.impl.KeyDescriptorBuilder;
|
||||
import org.opensaml.saml2.metadata.impl.NameIDFormatBuilder;
|
||||
import org.opensaml.saml2.metadata.impl.SPSSODescriptorBuilder;
|
||||
import org.opensaml.saml2.metadata.impl.SingleLogoutServiceBuilder;
|
||||
import org.opensaml.xml.ConfigurationException;
|
||||
import org.opensaml.xml.io.Marshaller;
|
||||
import org.opensaml.xml.io.MarshallingException;
|
||||
import org.opensaml.xml.security.SecurityException;
|
||||
import org.opensaml.xml.security.credential.UsageType;
|
||||
import org.opensaml.xml.security.keyinfo.KeyInfoGenerator;
|
||||
import org.opensaml.xml.security.x509.BasicX509Credential;
|
||||
import org.opensaml.xml.security.x509.X509KeyInfoGeneratorFactory;
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.stream.FactoryConfigurationError;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerException;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@APICommand(name = "getSPMetadata", description = "Returns SAML2 CloudStack Service Provider MetaData", responseObject = SAMLMetaDataResponse.class, entityType = {})
|
||||
public class GetServiceProviderMetaDataCmd extends BaseCmd implements APIAuthenticator {
|
||||
public static final Logger s_logger = Logger.getLogger(GetServiceProviderMetaDataCmd.class.getName());
|
||||
private static final String s_name = "spmetadataresponse";
|
||||
|
||||
@Inject
|
||||
ApiServerService _apiServer;
|
||||
|
||||
SAML2AuthManager _samlAuthManager;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////// API Implementation///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return s_name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEntityOwnerId() {
|
||||
return Account.ACCOUNT_TYPE_NORMAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws ServerApiException {
|
||||
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication plugin api, cannot be used directly");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String authenticate(String command, Map<String, Object[]> params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, HttpServletResponse resp) throws ServerApiException {
|
||||
SAMLMetaDataResponse response = new SAMLMetaDataResponse();
|
||||
response.setResponseName(getCommandName());
|
||||
|
||||
try {
|
||||
DefaultBootstrap.bootstrap();
|
||||
} catch (ConfigurationException | FactoryConfigurationError e) {
|
||||
s_logger.error("OpenSAML Bootstrapping error: " + e.getMessage());
|
||||
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
|
||||
"OpenSAML Bootstrapping error while creating SP MetaData",
|
||||
params, responseType));
|
||||
}
|
||||
|
||||
EntityDescriptor spEntityDescriptor = new EntityDescriptorBuilder().buildObject();
|
||||
spEntityDescriptor.setEntityID(_samlAuthManager.getServiceProviderId());
|
||||
|
||||
SPSSODescriptor spSSODescriptor = new SPSSODescriptorBuilder().buildObject();
|
||||
spSSODescriptor.setWantAssertionsSigned(true);
|
||||
spSSODescriptor.setAuthnRequestsSigned(false);
|
||||
|
||||
X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory();
|
||||
keyInfoGeneratorFactory.setEmitEntityCertificate(true);
|
||||
KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance();
|
||||
|
||||
KeyDescriptor encKeyDescriptor = new KeyDescriptorBuilder().buildObject();
|
||||
encKeyDescriptor.setUse(UsageType.ENCRYPTION);
|
||||
|
||||
KeyDescriptor signKeyDescriptor = new KeyDescriptorBuilder().buildObject();
|
||||
signKeyDescriptor.setUse(UsageType.SIGNING);
|
||||
|
||||
BasicX509Credential credential = new BasicX509Credential();
|
||||
credential.setEntityCertificate(_samlAuthManager.getIdpSigningKey());
|
||||
try {
|
||||
encKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(credential));
|
||||
signKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(credential));
|
||||
//TODO: generate own pub/priv keys
|
||||
//spSSODescriptor.getKeyDescriptors().add(encKeyDescriptor);
|
||||
//spSSODescriptor.getKeyDescriptors().add(signKeyDescriptor);
|
||||
} catch (SecurityException ignored) {
|
||||
}
|
||||
|
||||
NameIDFormat nameIDFormat = new NameIDFormatBuilder().buildObject();
|
||||
nameIDFormat.setFormat(NameIDType.PERSISTENT);
|
||||
spSSODescriptor.getNameIDFormats().add(nameIDFormat);
|
||||
|
||||
AssertionConsumerService assertionConsumerService = new AssertionConsumerServiceBuilder().buildObject();
|
||||
assertionConsumerService.setIndex(0);
|
||||
assertionConsumerService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
|
||||
assertionConsumerService.setLocation(_samlAuthManager.getSpSingleSignOnUrl());
|
||||
|
||||
SingleLogoutService ssoService = new SingleLogoutServiceBuilder().buildObject();
|
||||
ssoService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
|
||||
ssoService.setLocation(_samlAuthManager.getSpSingleLogOutUrl());
|
||||
|
||||
spSSODescriptor.getSingleLogoutServices().add(ssoService);
|
||||
spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService);
|
||||
spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
|
||||
spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor);
|
||||
|
||||
try {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
Document document = builder.newDocument();
|
||||
Marshaller out = Configuration.getMarshallerFactory().getMarshaller(spEntityDescriptor);
|
||||
out.marshall(spEntityDescriptor, document);
|
||||
|
||||
Transformer transformer = TransformerFactory.newInstance().newTransformer();
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
StreamResult streamResult = new StreamResult(stringWriter);
|
||||
DOMSource source = new DOMSource(document);
|
||||
transformer.transform(source, streamResult);
|
||||
stringWriter.close();
|
||||
response.setMetadata(stringWriter.toString());
|
||||
} catch (ParserConfigurationException | IOException | MarshallingException | TransformerException e) {
|
||||
response.setMetadata("Error creating Service Provider MetaData XML: " + e.getMessage());
|
||||
}
|
||||
|
||||
return ApiResponseSerializer.toSerializedString(response, responseType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public APIAuthenticationType getAPIType() {
|
||||
return APIAuthenticationType.LOGIN_API;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
|
||||
for (PluggableAPIAuthenticator authManager: authenticators) {
|
||||
if (authManager instanceof SAML2AuthManager) {
|
||||
_samlAuthManager = (SAML2AuthManager) authManager;
|
||||
}
|
||||
}
|
||||
if (_samlAuthManager == null) {
|
||||
s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
// 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.api.command;
|
||||
|
||||
import com.cloud.api.response.ApiResponseSerializer;
|
||||
import com.cloud.configuration.Config;
|
||||
import com.cloud.domain.Domain;
|
||||
import com.cloud.exception.CloudAuthenticationException;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.DomainManager;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.utils.HttpUtils;
|
||||
import com.cloud.utils.db.EntityManager;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.ApiServerService;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
import org.apache.cloudstack.api.Parameter;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationType;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticator;
|
||||
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
|
||||
import org.apache.cloudstack.api.response.LoginCmdResponse;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
|
||||
import org.apache.cloudstack.saml.SAML2AuthManager;
|
||||
import org.apache.cloudstack.utils.auth.SAMLUtils;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.opensaml.DefaultBootstrap;
|
||||
import org.opensaml.saml2.core.Assertion;
|
||||
import org.opensaml.saml2.core.Attribute;
|
||||
import org.opensaml.saml2.core.AttributeStatement;
|
||||
import org.opensaml.saml2.core.AuthnRequest;
|
||||
import org.opensaml.saml2.core.NameID;
|
||||
import org.opensaml.saml2.core.NameIDType;
|
||||
import org.opensaml.saml2.core.Response;
|
||||
import org.opensaml.saml2.core.StatusCode;
|
||||
import org.opensaml.xml.ConfigurationException;
|
||||
import org.opensaml.xml.io.MarshallingException;
|
||||
import org.opensaml.xml.io.UnmarshallingException;
|
||||
import org.opensaml.xml.security.x509.BasicX509Credential;
|
||||
import org.opensaml.xml.signature.Signature;
|
||||
import org.opensaml.xml.signature.SignatureValidator;
|
||||
import org.opensaml.xml.validation.ValidationException;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.stream.FactoryConfigurationError;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@APICommand(name = "samlsso", description = "SP initiated SAML Single Sign On", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {})
|
||||
public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator {
|
||||
public static final Logger s_logger = Logger.getLogger(SAML2LoginAPIAuthenticatorCmd.class.getName());
|
||||
private static final String s_name = "loginresponse";
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
//////////////// API parameters /////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
@Parameter(name = ApiConstants.IDP_URL, type = CommandType.STRING, description = "Identity Provider SSO HTTP-Redirect binding URL", required = true)
|
||||
private String idpUrl;
|
||||
|
||||
@Inject
|
||||
ApiServerService _apiServer;
|
||||
@Inject
|
||||
EntityManager _entityMgr;
|
||||
@Inject
|
||||
ConfigurationDao _configDao;
|
||||
@Inject
|
||||
DomainManager _domainMgr;
|
||||
|
||||
SAML2AuthManager _samlAuthManager;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
public String getIdpUrl() {
|
||||
return idpUrl;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////// API Implementation///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return s_name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEntityOwnerId() {
|
||||
return Account.ACCOUNT_TYPE_NORMAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws ServerApiException {
|
||||
// We should never reach here
|
||||
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly");
|
||||
}
|
||||
|
||||
private String buildAuthnRequestUrl(String idpUrl) {
|
||||
String spId = _samlAuthManager.getServiceProviderId();
|
||||
String consumerUrl = _samlAuthManager.getSpSingleSignOnUrl();
|
||||
String identityProviderUrl = _samlAuthManager.getIdpSingleSignOnUrl();
|
||||
|
||||
if (idpUrl != null) {
|
||||
identityProviderUrl = idpUrl;
|
||||
}
|
||||
|
||||
String redirectUrl = "";
|
||||
try {
|
||||
DefaultBootstrap.bootstrap();
|
||||
AuthnRequest authnRequest = SAMLUtils.buildAuthnRequestObject(spId, identityProviderUrl, consumerUrl);
|
||||
redirectUrl = identityProviderUrl + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(authnRequest);
|
||||
} catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException e) {
|
||||
s_logger.error("SAML AuthnRequest message building error: " + e.getMessage());
|
||||
}
|
||||
return redirectUrl;
|
||||
}
|
||||
|
||||
public Response processSAMLResponse(String responseMessage) {
|
||||
Response responseObject = null;
|
||||
try {
|
||||
DefaultBootstrap.bootstrap();
|
||||
responseObject = SAMLUtils.decodeSAMLResponse(responseMessage);
|
||||
|
||||
} catch (ConfigurationException | FactoryConfigurationError | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) {
|
||||
s_logger.error("SAMLResponse processing error: " + e.getMessage());
|
||||
}
|
||||
return responseObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String authenticate(final String command, final Map<String, Object[]> params, final HttpSession session, final String remoteAddress, final String responseType, final StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException {
|
||||
try {
|
||||
if (!params.containsKey("SAMLResponse")) {
|
||||
String idpUrl = null;
|
||||
final String[] idps = (String[])params.get(ApiConstants.IDP_URL);
|
||||
if (idps != null && idps.length > 0) {
|
||||
idpUrl = idps[0];
|
||||
}
|
||||
String redirectUrl = this.buildAuthnRequestUrl(idpUrl);
|
||||
resp.sendRedirect(redirectUrl);
|
||||
return "";
|
||||
} else {
|
||||
final String samlResponse = ((String[])params.get(SAMLUtils.SAML_RESPONSE))[0];
|
||||
Response processedSAMLResponse = this.processSAMLResponse(samlResponse);
|
||||
String statusCode = processedSAMLResponse.getStatus().getStatusCode().getValue();
|
||||
if (!statusCode.equals(StatusCode.SUCCESS_URI)) {
|
||||
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
|
||||
"Identity Provider send a non-successful authentication status code",
|
||||
params, responseType));
|
||||
}
|
||||
|
||||
if (_samlAuthManager.getIdpSigningKey() != null) {
|
||||
Signature sig = processedSAMLResponse.getSignature();
|
||||
BasicX509Credential credential = new BasicX509Credential();
|
||||
credential.setEntityCertificate(_samlAuthManager.getIdpSigningKey());
|
||||
SignatureValidator validator = new SignatureValidator(credential);
|
||||
try {
|
||||
validator.validate(sig);
|
||||
} catch (ValidationException e) {
|
||||
s_logger.error("SAML Response's signature failed to be validated by IDP signing key:" + e.getMessage());
|
||||
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
|
||||
"SAML Response's signature failed to be validated by IDP signing key",
|
||||
params, responseType));
|
||||
}
|
||||
}
|
||||
|
||||
String uniqueUserId = null;
|
||||
String accountName = _configDao.getValue(Config.SAMLUserAccountName.key());
|
||||
String domainString = _configDao.getValue(Config.SAMLUserDomain.key());
|
||||
|
||||
Long domainId = -1L;
|
||||
Domain domain = _domainMgr.getDomain(domainString);
|
||||
if (domain != null) {
|
||||
domainId = domain.getId();
|
||||
} else {
|
||||
try {
|
||||
domainId = Long.parseLong(domainString);
|
||||
} catch (NumberFormatException ignore) {
|
||||
}
|
||||
}
|
||||
if (domainId == -1L) {
|
||||
s_logger.error("The default domain ID for SAML users is not set correct, it should be a UUID");
|
||||
}
|
||||
|
||||
String username = null;
|
||||
String password = SAMLUtils.generateSecureRandomId(); // Random password
|
||||
String firstName = "";
|
||||
String lastName = "";
|
||||
String timeZone = "";
|
||||
String email = "";
|
||||
|
||||
Assertion assertion = processedSAMLResponse.getAssertions().get(0);
|
||||
NameID nameId = assertion.getSubject().getNameID();
|
||||
String sessionIndex = assertion.getAuthnStatements().get(0).getSessionIndex();
|
||||
session.setAttribute(SAMLUtils.SAML_NAMEID, nameId);
|
||||
session.setAttribute(SAMLUtils.SAML_SESSION, sessionIndex);
|
||||
|
||||
if (nameId.getFormat().equals(NameIDType.PERSISTENT) || nameId.getFormat().equals(NameIDType.EMAIL)) {
|
||||
username = nameId.getValue();
|
||||
uniqueUserId = SAMLUtils.createSAMLId(username);
|
||||
if (nameId.getFormat().equals(NameIDType.EMAIL)) {
|
||||
email = username;
|
||||
}
|
||||
}
|
||||
|
||||
AttributeStatement attributeStatement = assertion.getAttributeStatements().get(0);
|
||||
List<Attribute> attributes = attributeStatement.getAttributes();
|
||||
|
||||
// Try capturing standard LDAP attributes
|
||||
for (Attribute attribute: attributes) {
|
||||
String attributeName = attribute.getName();
|
||||
String attributeValue = attribute.getAttributeValues().get(0).getDOM().getTextContent();
|
||||
if (attributeName.equalsIgnoreCase("uid") && uniqueUserId == null) {
|
||||
username = attributeValue;
|
||||
uniqueUserId = SAMLUtils.createSAMLId(username);
|
||||
} else if (attributeName.equalsIgnoreCase("givenName")) {
|
||||
firstName = attributeValue;
|
||||
} else if (attributeName.equalsIgnoreCase(("sn"))) {
|
||||
lastName = attributeValue;
|
||||
} else if (attributeName.equalsIgnoreCase("mail")) {
|
||||
email = attributeValue;
|
||||
}
|
||||
}
|
||||
|
||||
User user = _entityMgr.findByUuid(User.class, uniqueUserId);
|
||||
if (user == null && uniqueUserId != null && username != null
|
||||
&& accountName != null && domainId != null) {
|
||||
CallContext.current().setEventDetails("UserName: " + username + ", FirstName :" + password + ", LastName: " + lastName);
|
||||
user = _accountService.createUser(username, password, firstName, lastName, email, timeZone, accountName, domainId, uniqueUserId);
|
||||
}
|
||||
|
||||
if (user != null) {
|
||||
try {
|
||||
if (_apiServer.verifyUser(user.getId())) {
|
||||
LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, username, user.getPassword(), domainId, null, remoteAddress, params);
|
||||
resp.addCookie(new Cookie("userid", loginResponse.getUserId()));
|
||||
resp.addCookie(new Cookie("domainid", loginResponse.getDomainId()));
|
||||
resp.addCookie(new Cookie("role", loginResponse.getType()));
|
||||
resp.addCookie(new Cookie("username", URLEncoder.encode(loginResponse.getUsername(), HttpUtils.UTF_8)));
|
||||
resp.addCookie(new Cookie("sessionKey", URLEncoder.encode(loginResponse.getSessionKey(), HttpUtils.UTF_8)));
|
||||
resp.addCookie(new Cookie("account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8)));
|
||||
resp.addCookie(new Cookie("timezone", URLEncoder.encode(loginResponse.getTimeZone(), HttpUtils.UTF_8)));
|
||||
resp.addCookie(new Cookie("userfullname", loginResponse.getFirstName() + "%20" + loginResponse.getLastName()));
|
||||
resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
|
||||
return ApiResponseSerializer.toSerializedString(loginResponse, responseType);
|
||||
|
||||
}
|
||||
} catch (final CloudAuthenticationException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
auditTrailSb.append("SP initiated SAML authentication using HTTP redirection failed:");
|
||||
auditTrailSb.append(e.getMessage());
|
||||
}
|
||||
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
|
||||
"Unable to authenticate or retrieve user while performing SAML based SSO",
|
||||
params, responseType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public APIAuthenticationType getAPIType() {
|
||||
return APIAuthenticationType.LOGIN_API;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
|
||||
for (PluggableAPIAuthenticator authManager: authenticators) {
|
||||
if (authManager instanceof SAML2AuthManager) {
|
||||
_samlAuthManager = (SAML2AuthManager) authManager;
|
||||
}
|
||||
}
|
||||
if (_samlAuthManager == null) {
|
||||
s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
// 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.api.command;
|
||||
|
||||
import com.cloud.api.response.ApiResponseSerializer;
|
||||
import com.cloud.configuration.Config;
|
||||
import com.cloud.user.Account;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.ApiServerService;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationType;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticator;
|
||||
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
|
||||
import org.apache.cloudstack.api.response.LogoutCmdResponse;
|
||||
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
|
||||
import org.apache.cloudstack.saml.SAML2AuthManager;
|
||||
import org.apache.cloudstack.utils.auth.SAMLUtils;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.opensaml.DefaultBootstrap;
|
||||
import org.opensaml.saml2.core.LogoutRequest;
|
||||
import org.opensaml.saml2.core.NameID;
|
||||
import org.opensaml.saml2.core.Response;
|
||||
import org.opensaml.saml2.core.StatusCode;
|
||||
import org.opensaml.xml.ConfigurationException;
|
||||
import org.opensaml.xml.io.MarshallingException;
|
||||
import org.opensaml.xml.io.UnmarshallingException;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.stream.FactoryConfigurationError;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@APICommand(name = "samlslo", description = "SAML Global Log Out API", responseObject = LogoutCmdResponse.class, entityType = {})
|
||||
public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator {
|
||||
public static final Logger s_logger = Logger.getLogger(SAML2LogoutAPIAuthenticatorCmd.class.getName());
|
||||
private static final String s_name = "logoutresponse";
|
||||
|
||||
@Inject
|
||||
ApiServerService _apiServer;
|
||||
@Inject
|
||||
ConfigurationDao _configDao;
|
||||
SAML2AuthManager _samlAuthManager;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////// API Implementation///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return s_name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEntityOwnerId() {
|
||||
return Account.ACCOUNT_TYPE_NORMAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws ServerApiException {
|
||||
// We should never reach here
|
||||
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String authenticate(String command, Map<String, Object[]> params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException {
|
||||
auditTrailSb.append("=== SAML SLO Logging out ===");
|
||||
LogoutCmdResponse response = new LogoutCmdResponse();
|
||||
response.setDescription("success");
|
||||
response.setResponseName(getCommandName());
|
||||
String responseString = ApiResponseSerializer.toSerializedString(response, responseType);
|
||||
|
||||
if (session == null) {
|
||||
try {
|
||||
resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return responseString;
|
||||
}
|
||||
|
||||
try {
|
||||
DefaultBootstrap.bootstrap();
|
||||
} catch (ConfigurationException | FactoryConfigurationError e) {
|
||||
s_logger.error("OpenSAML Bootstrapping error: " + e.getMessage());
|
||||
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
|
||||
"OpenSAML Bootstrapping error while creating SP MetaData",
|
||||
params, responseType));
|
||||
}
|
||||
|
||||
if (params != null && params.containsKey("SAMLResponse")) {
|
||||
try {
|
||||
final String samlResponse = ((String[])params.get(SAMLUtils.SAML_RESPONSE))[0];
|
||||
Response processedSAMLResponse = SAMLUtils.decodeSAMLResponse(samlResponse);
|
||||
String statusCode = processedSAMLResponse.getStatus().getStatusCode().getValue();
|
||||
if (!statusCode.equals(StatusCode.SUCCESS_URI)) {
|
||||
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.INTERNAL_ERROR.getHttpCode(),
|
||||
"SAML SLO LogoutResponse status is not Success",
|
||||
params, responseType));
|
||||
}
|
||||
} catch (ConfigurationException | FactoryConfigurationError | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) {
|
||||
s_logger.error("SAMLResponse processing error: " + e.getMessage());
|
||||
}
|
||||
try {
|
||||
resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return responseString;
|
||||
}
|
||||
|
||||
NameID nameId = (NameID) session.getAttribute(SAMLUtils.SAML_NAMEID);
|
||||
String sessionIndex = (String) session.getAttribute(SAMLUtils.SAML_SESSION);
|
||||
if (nameId == null || sessionIndex == null) {
|
||||
try {
|
||||
resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return responseString;
|
||||
}
|
||||
LogoutRequest logoutRequest = SAMLUtils.buildLogoutRequest(_samlAuthManager.getIdpSingleLogOutUrl(), _samlAuthManager.getServiceProviderId(), nameId, sessionIndex);
|
||||
|
||||
try {
|
||||
String redirectUrl = _samlAuthManager.getIdpSingleLogOutUrl() + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(logoutRequest);
|
||||
resp.sendRedirect(redirectUrl);
|
||||
} catch (MarshallingException | IOException e) {
|
||||
s_logger.error("SAML SLO error: " + e.getMessage());
|
||||
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
|
||||
"SAML Single Logout Error",
|
||||
params, responseType));
|
||||
}
|
||||
return responseString;
|
||||
}
|
||||
|
||||
@Override
|
||||
public APIAuthenticationType getAPIType() {
|
||||
return APIAuthenticationType.LOGOUT_API;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
|
||||
for (PluggableAPIAuthenticator authManager: authenticators) {
|
||||
if (authManager instanceof SAML2AuthManager) {
|
||||
_samlAuthManager = (SAML2AuthManager) authManager;
|
||||
}
|
||||
}
|
||||
if (_samlAuthManager == null) {
|
||||
s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
// 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.api.response;
|
||||
|
||||
import com.cloud.serializer.Param;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.apache.cloudstack.api.BaseResponse;
|
||||
|
||||
public class SAMLMetaDataResponse extends BaseResponse {
|
||||
|
||||
@SerializedName("metadata")
|
||||
@Param(description = "The Metadata XML")
|
||||
private String metadata;
|
||||
|
||||
public SAMLMetaDataResponse() {
|
||||
super();
|
||||
}
|
||||
|
||||
public String getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public void setMetadata(String metadata) {
|
||||
this.metadata = metadata;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
// 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.saml;
|
||||
|
||||
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
public interface SAML2AuthManager extends PluggableAPIAuthenticator {
|
||||
public String getServiceProviderId();
|
||||
public String getIdentityProviderId();
|
||||
|
||||
public X509Certificate getIdpSigningKey();
|
||||
public X509Certificate getIdpEncryptionKey();
|
||||
|
||||
public String getSpSingleSignOnUrl();
|
||||
public String getIdpSingleSignOnUrl();
|
||||
|
||||
public String getSpSingleLogOutUrl();
|
||||
public String getIdpSingleLogOutUrl();
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
// 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.saml;
|
||||
|
||||
import com.cloud.configuration.Config;
|
||||
import com.cloud.utils.component.AdapterBase;
|
||||
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
|
||||
import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd;
|
||||
import org.apache.cloudstack.api.command.SAML2LoginAPIAuthenticatorCmd;
|
||||
import org.apache.cloudstack.api.command.SAML2LogoutAPIAuthenticatorCmd;
|
||||
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.opensaml.DefaultBootstrap;
|
||||
import org.opensaml.common.xml.SAMLConstants;
|
||||
import org.opensaml.saml2.metadata.EntityDescriptor;
|
||||
import org.opensaml.saml2.metadata.IDPSSODescriptor;
|
||||
import org.opensaml.saml2.metadata.KeyDescriptor;
|
||||
import org.opensaml.saml2.metadata.SingleLogoutService;
|
||||
import org.opensaml.saml2.metadata.SingleSignOnService;
|
||||
import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider;
|
||||
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
|
||||
import org.opensaml.xml.ConfigurationException;
|
||||
import org.opensaml.xml.parse.BasicParserPool;
|
||||
import org.opensaml.xml.security.credential.UsageType;
|
||||
import org.opensaml.xml.security.keyinfo.KeyInfoHelper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.ejb.Local;
|
||||
import javax.inject.Inject;
|
||||
import javax.xml.stream.FactoryConfigurationError;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Local(value = {SAML2AuthManager.class, PluggableAPIAuthenticator.class})
|
||||
public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager {
|
||||
private static final Logger s_logger = Logger.getLogger(SAML2AuthManagerImpl.class);
|
||||
|
||||
private String serviceProviderId;
|
||||
private String identityProviderId;
|
||||
|
||||
private X509Certificate idpSigningKey;
|
||||
private X509Certificate idpEncryptionKey;
|
||||
|
||||
private String spSingleSignOnUrl;
|
||||
private String idpSingleSignOnUrl;
|
||||
|
||||
private String spSingleLogOutUrl;
|
||||
private String idpSingleLogOutUrl;
|
||||
|
||||
private HTTPMetadataProvider idpMetaDataProvider;
|
||||
|
||||
@Inject
|
||||
ConfigurationDao _configDao;
|
||||
|
||||
@Override
|
||||
public boolean start() {
|
||||
return isSAMLPluginEnabled() && setup();
|
||||
}
|
||||
|
||||
private boolean setup() {
|
||||
// TODO: In future if need added logic to get SP X509 cert for Idps that need signed requests
|
||||
|
||||
this.serviceProviderId = _configDao.getValue(Config.SAMLServiceProviderID.key());
|
||||
this.identityProviderId = _configDao.getValue(Config.SAMLIdentityProviderID.key());
|
||||
|
||||
this.spSingleSignOnUrl = _configDao.getValue(Config.SAMLServiceProviderSingleSignOnURL.key());
|
||||
this.spSingleLogOutUrl = _configDao.getValue(Config.SAMLServiceProviderSingleLogOutURL.key());
|
||||
|
||||
String idpMetaDataUrl = _configDao.getValue(Config.SAMLIdentityProviderMetadataURL.key());
|
||||
|
||||
int tolerance = 30000;
|
||||
String timeout = _configDao.getValue(Config.SAMLTimeout.key());
|
||||
if (timeout != null) {
|
||||
tolerance = Integer.parseInt(timeout);
|
||||
}
|
||||
|
||||
try {
|
||||
DefaultBootstrap.bootstrap();
|
||||
idpMetaDataProvider = new HTTPMetadataProvider(idpMetaDataUrl, tolerance);
|
||||
idpMetaDataProvider.setRequireValidMetadata(true);
|
||||
idpMetaDataProvider.setParserPool(new BasicParserPool());
|
||||
idpMetaDataProvider.initialize();
|
||||
|
||||
EntityDescriptor idpEntityDescriptor = idpMetaDataProvider.getEntityDescriptor(this.identityProviderId);
|
||||
|
||||
IDPSSODescriptor idpssoDescriptor = idpEntityDescriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
|
||||
if (idpssoDescriptor != null) {
|
||||
for (SingleSignOnService ssos: idpssoDescriptor.getSingleSignOnServices()) {
|
||||
if (ssos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) {
|
||||
this.idpSingleSignOnUrl = ssos.getLocation();
|
||||
}
|
||||
}
|
||||
|
||||
for (SingleLogoutService slos: idpssoDescriptor.getSingleLogoutServices()) {
|
||||
if (slos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) {
|
||||
this.idpSingleLogOutUrl = slos.getLocation();
|
||||
}
|
||||
}
|
||||
|
||||
for (KeyDescriptor kd: idpssoDescriptor.getKeyDescriptors()) {
|
||||
if (kd.getUse() == UsageType.SIGNING) {
|
||||
try {
|
||||
this.idpSigningKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0);
|
||||
} catch (CertificateException ignored) {
|
||||
}
|
||||
}
|
||||
if (kd.getUse() == UsageType.ENCRYPTION) {
|
||||
try {
|
||||
this.idpEncryptionKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0);
|
||||
} catch (CertificateException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s_logger.warn("Provided IDP XML Metadata does not contain IDPSSODescriptor, SAML authentication may not work");
|
||||
}
|
||||
} catch (MetadataProviderException e) {
|
||||
s_logger.error("Unable to read SAML2 IDP MetaData URL, error:" + e.getMessage());
|
||||
s_logger.error("SAML2 Authentication may be unavailable");
|
||||
} catch (ConfigurationException | FactoryConfigurationError e) {
|
||||
s_logger.error("OpenSAML bootstrapping failed: error: " + e.getMessage());
|
||||
}
|
||||
|
||||
if (this.idpSingleLogOutUrl == null || this.idpSingleSignOnUrl == null) {
|
||||
s_logger.error("SAML based authentication won't work");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<?>> getAuthCommands() {
|
||||
if (!isSAMLPluginEnabled()) {
|
||||
return null;
|
||||
}
|
||||
List<Class<?>> cmdList = new ArrayList<Class<?>>();
|
||||
cmdList.add(SAML2LoginAPIAuthenticatorCmd.class);
|
||||
cmdList.add(SAML2LogoutAPIAuthenticatorCmd.class);
|
||||
cmdList.add(GetServiceProviderMetaDataCmd.class);
|
||||
return cmdList;
|
||||
}
|
||||
|
||||
public String getServiceProviderId() {
|
||||
return serviceProviderId;
|
||||
}
|
||||
|
||||
public String getIdpSingleSignOnUrl() {
|
||||
return this.idpSingleSignOnUrl;
|
||||
}
|
||||
|
||||
public String getIdpSingleLogOutUrl() {
|
||||
return this.idpSingleLogOutUrl;
|
||||
}
|
||||
|
||||
public String getSpSingleSignOnUrl() {
|
||||
return spSingleSignOnUrl;
|
||||
}
|
||||
|
||||
public String getSpSingleLogOutUrl() {
|
||||
return spSingleLogOutUrl;
|
||||
}
|
||||
|
||||
public String getIdentityProviderId() {
|
||||
return identityProviderId;
|
||||
}
|
||||
|
||||
public X509Certificate getIdpSigningKey() {
|
||||
return idpSigningKey;
|
||||
}
|
||||
|
||||
public X509Certificate getIdpEncryptionKey() {
|
||||
return idpEncryptionKey;
|
||||
}
|
||||
|
||||
public Boolean isSAMLPluginEnabled() {
|
||||
return Boolean.valueOf(_configDao.getValue(Config.SAMLIsPluginEnabled.key()));
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
// 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;
|
||||
package org.apache.cloudstack.saml;
|
||||
|
||||
import com.cloud.server.auth.DefaultUserAuthenticator;
|
||||
import com.cloud.server.auth.UserAuthenticator;
|
||||
|
|
@ -21,6 +21,7 @@ import com.cloud.user.UserAccount;
|
|||
import com.cloud.user.dao.UserAccountDao;
|
||||
import com.cloud.user.dao.UserDao;
|
||||
import com.cloud.utils.Pair;
|
||||
import org.apache.cloudstack.utils.auth.SAMLUtils;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import javax.ejb.Local;
|
||||
|
|
@ -47,8 +48,8 @@ public class SAML2UserAuthenticator extends DefaultUserAuthenticator {
|
|||
return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
|
||||
} else {
|
||||
User user = _userDao.getUser(userAccount.getId());
|
||||
// TODO: check SAMLRequest, signature etc. from requestParameters
|
||||
if (user != null && user.getUuid().startsWith("saml")) {
|
||||
if (user != null && SAMLUtils.checkSAMLUserId(user.getUuid()) &&
|
||||
requestParameters != null && requestParameters.containsKey(SAMLUtils.SAML_RESPONSE)) {
|
||||
return new Pair<Boolean, ActionOnFailedAuthentication>(true, null);
|
||||
}
|
||||
}
|
||||
|
|
@ -58,8 +59,6 @@ public class SAML2UserAuthenticator extends DefaultUserAuthenticator {
|
|||
|
||||
@Override
|
||||
public String encode(final String password) {
|
||||
// TODO: Complete method
|
||||
StringBuilder sb = new StringBuilder(32);
|
||||
return sb.toString();
|
||||
return SAMLUtils.generateSecureRandomId();
|
||||
}
|
||||
}
|
||||
|
|
@ -19,21 +19,68 @@
|
|||
|
||||
package org.apache.cloudstack;
|
||||
|
||||
import com.cloud.server.auth.UserAuthenticator.ActionOnFailedAuthentication;
|
||||
import com.cloud.user.UserAccountVO;
|
||||
import com.cloud.user.UserVO;
|
||||
import com.cloud.user.dao.UserAccountDao;
|
||||
import com.cloud.user.dao.UserDao;
|
||||
import com.cloud.utils.Pair;
|
||||
import org.apache.cloudstack.saml.SAML2UserAuthenticator;
|
||||
import org.apache.cloudstack.utils.auth.SAMLUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class SAML2UserAuthenticatorTest {
|
||||
|
||||
@Mock
|
||||
UserAccountDao userAccountDao;
|
||||
@Mock
|
||||
UserDao userDao;
|
||||
|
||||
@Test
|
||||
public void encode() {
|
||||
|
||||
Assert.assertTrue(new SAML2UserAuthenticator().encode("random String").length() == 32);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authenticate() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
|
||||
SAML2UserAuthenticator authenticator = new SAML2UserAuthenticator();
|
||||
|
||||
Field daoField = SAML2UserAuthenticator.class.getDeclaredField("_userAccountDao");
|
||||
daoField.setAccessible(true);
|
||||
daoField.set(authenticator, userAccountDao);
|
||||
|
||||
Field userDaoField = SAML2UserAuthenticator.class.getDeclaredField("_userDao");
|
||||
userDaoField.setAccessible(true);
|
||||
userDaoField.set(authenticator, userDao);
|
||||
|
||||
UserAccountVO account = new UserAccountVO();
|
||||
account.setPassword("5f4dcc3b5aa765d61d8327deb882cf99");
|
||||
account.setId(1L);
|
||||
|
||||
UserVO user = new UserVO();
|
||||
user.setUuid(SAMLUtils.createSAMLId("someUID"));
|
||||
|
||||
Mockito.when(userAccountDao.getUserAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(account);
|
||||
Mockito.when(userDao.getUser(Mockito.anyLong())).thenReturn(user);
|
||||
|
||||
// When there is no SAMLRequest in params
|
||||
Pair<Boolean, ActionOnFailedAuthentication> pair1 = authenticator.authenticate(SAMLUtils.createSAMLId("user1234"), "random", 1l, null);
|
||||
Assert.assertFalse(pair1.first());
|
||||
|
||||
// When there is SAMLRequest in params
|
||||
Map<String, Object[]> params = new HashMap<String, Object[]>();
|
||||
params.put(SAMLUtils.SAML_RESPONSE, new Object[]{});
|
||||
Pair<Boolean, ActionOnFailedAuthentication> pair2 = authenticator.authenticate(SAMLUtils.createSAMLId("user1234"), "random", 1l, params);
|
||||
Assert.assertTrue(pair2.first());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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.api.command;
|
||||
|
||||
import com.cloud.utils.HttpUtils;
|
||||
import org.apache.cloudstack.api.ApiServerService;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationType;
|
||||
import org.apache.cloudstack.saml.SAML2AuthManager;
|
||||
import org.apache.cloudstack.utils.auth.SAMLUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.lang.reflect.Field;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class GetServiceProviderMetaDataCmdTest {
|
||||
|
||||
@Mock
|
||||
ApiServerService apiServer;
|
||||
|
||||
@Mock
|
||||
SAML2AuthManager samlAuthManager;
|
||||
|
||||
@Mock
|
||||
HttpSession session;
|
||||
|
||||
@Mock
|
||||
HttpServletResponse resp;
|
||||
|
||||
@Test
|
||||
public void testAuthenticate() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, CertificateParsingException, CertificateEncodingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException {
|
||||
GetServiceProviderMetaDataCmd cmd = new GetServiceProviderMetaDataCmd();
|
||||
|
||||
Field apiServerField = GetServiceProviderMetaDataCmd.class.getDeclaredField("_apiServer");
|
||||
apiServerField.setAccessible(true);
|
||||
apiServerField.set(cmd, apiServer);
|
||||
|
||||
Field managerField = GetServiceProviderMetaDataCmd.class.getDeclaredField("_samlAuthManager");
|
||||
managerField.setAccessible(true);
|
||||
managerField.set(cmd, samlAuthManager);
|
||||
|
||||
String spId = "someSPID";
|
||||
String url = "someUrl";
|
||||
X509Certificate cert = SAMLUtils.generateRandomX509Certificate();
|
||||
Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId);
|
||||
Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(cert);
|
||||
Mockito.when(samlAuthManager.getIdpSingleLogOutUrl()).thenReturn(url);
|
||||
Mockito.when(samlAuthManager.getSpSingleLogOutUrl()).thenReturn(url);
|
||||
|
||||
String result = cmd.authenticate("command", null, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), resp);
|
||||
Assert.assertTrue(result.contains("md:EntityDescriptor"));
|
||||
|
||||
Mockito.verify(samlAuthManager, Mockito.atLeast(1)).getServiceProviderId();
|
||||
Mockito.verify(samlAuthManager, Mockito.atLeast(1)).getSpSingleSignOnUrl();
|
||||
Mockito.verify(samlAuthManager, Mockito.atLeast(1)).getSpSingleLogOutUrl();
|
||||
Mockito.verify(samlAuthManager, Mockito.never()).getIdpSingleSignOnUrl();
|
||||
Mockito.verify(samlAuthManager, Mockito.never()).getIdpSingleLogOutUrl();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAPIType() {
|
||||
Assert.assertTrue(new GetServiceProviderMetaDataCmd().getAPIType() == APIAuthenticationType.LOGIN_API);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
package org.apache.cloudstack.api.command;
|
||||
|
||||
import com.cloud.domain.Domain;
|
||||
import com.cloud.user.AccountService;
|
||||
import com.cloud.user.DomainManager;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.user.UserVO;
|
||||
import com.cloud.utils.HttpUtils;
|
||||
import com.cloud.utils.db.EntityManager;
|
||||
import org.apache.cloudstack.api.ApiServerService;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationType;
|
||||
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
|
||||
import org.apache.cloudstack.saml.SAML2AuthManager;
|
||||
import org.apache.cloudstack.utils.auth.SAMLUtils;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
import org.opensaml.common.SAMLVersion;
|
||||
import org.opensaml.saml2.core.Assertion;
|
||||
import org.opensaml.saml2.core.AttributeStatement;
|
||||
import org.opensaml.saml2.core.AuthnStatement;
|
||||
import org.opensaml.saml2.core.NameID;
|
||||
import org.opensaml.saml2.core.NameIDType;
|
||||
import org.opensaml.saml2.core.Response;
|
||||
import org.opensaml.saml2.core.Status;
|
||||
import org.opensaml.saml2.core.StatusCode;
|
||||
import org.opensaml.saml2.core.Subject;
|
||||
import org.opensaml.saml2.core.impl.AssertionBuilder;
|
||||
import org.opensaml.saml2.core.impl.AttributeStatementBuilder;
|
||||
import org.opensaml.saml2.core.impl.AuthnStatementBuilder;
|
||||
import org.opensaml.saml2.core.impl.NameIDBuilder;
|
||||
import org.opensaml.saml2.core.impl.ResponseBuilder;
|
||||
import org.opensaml.saml2.core.impl.StatusBuilder;
|
||||
import org.opensaml.saml2.core.impl.StatusCodeBuilder;
|
||||
import org.opensaml.saml2.core.impl.SubjectBuilder;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.lang.reflect.Field;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class SAML2LoginAPIAuthenticatorCmdTest {
|
||||
|
||||
@Mock
|
||||
ApiServerService apiServer;
|
||||
|
||||
@Mock
|
||||
SAML2AuthManager samlAuthManager;
|
||||
|
||||
@Mock
|
||||
ConfigurationDao configDao;
|
||||
|
||||
@Mock
|
||||
EntityManager entityMgr;
|
||||
|
||||
@Mock
|
||||
DomainManager domainMgr;
|
||||
|
||||
@Mock
|
||||
AccountService accountService;
|
||||
|
||||
@Mock
|
||||
Domain domain;
|
||||
|
||||
@Mock
|
||||
HttpSession session;
|
||||
|
||||
@Mock
|
||||
HttpServletResponse resp;
|
||||
|
||||
private Response buildMockResponse() throws Exception {
|
||||
Response samlMessage = new ResponseBuilder().buildObject();
|
||||
samlMessage.setID("foo");
|
||||
samlMessage.setVersion(SAMLVersion.VERSION_20);
|
||||
samlMessage.setIssueInstant(new DateTime(0));
|
||||
Status status = new StatusBuilder().buildObject();
|
||||
StatusCode statusCode = new StatusCodeBuilder().buildObject();
|
||||
statusCode.setValue(StatusCode.SUCCESS_URI);
|
||||
status.setStatusCode(statusCode);
|
||||
samlMessage.setStatus(status);
|
||||
Assertion assertion = new AssertionBuilder().buildObject();
|
||||
Subject subject = new SubjectBuilder().buildObject();
|
||||
NameID nameID = new NameIDBuilder().buildObject();
|
||||
nameID.setValue("SOME-UNIQUE-ID");
|
||||
nameID.setFormat(NameIDType.PERSISTENT);
|
||||
subject.setNameID(nameID);
|
||||
assertion.setSubject(subject);
|
||||
AuthnStatement authnStatement = new AuthnStatementBuilder().buildObject();
|
||||
authnStatement.setSessionIndex("Some Session String");
|
||||
assertion.getAuthnStatements().add(authnStatement);
|
||||
AttributeStatement attributeStatement = new AttributeStatementBuilder().buildObject();
|
||||
assertion.getAttributeStatements().add(attributeStatement);
|
||||
samlMessage.getAssertions().add(assertion);
|
||||
return samlMessage;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthenticate() throws Exception {
|
||||
SAML2LoginAPIAuthenticatorCmd cmd = Mockito.spy(new SAML2LoginAPIAuthenticatorCmd());
|
||||
|
||||
Field apiServerField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_apiServer");
|
||||
apiServerField.setAccessible(true);
|
||||
apiServerField.set(cmd, apiServer);
|
||||
|
||||
Field managerField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_samlAuthManager");
|
||||
managerField.setAccessible(true);
|
||||
managerField.set(cmd, samlAuthManager);
|
||||
|
||||
Field accountServiceField = BaseCmd.class.getDeclaredField("_accountService");
|
||||
accountServiceField.setAccessible(true);
|
||||
accountServiceField.set(cmd, accountService);
|
||||
|
||||
Field entityMgrField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_entityMgr");
|
||||
entityMgrField.setAccessible(true);
|
||||
entityMgrField.set(cmd, entityMgr);
|
||||
|
||||
Field domainMgrField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_domainMgr");
|
||||
domainMgrField.setAccessible(true);
|
||||
domainMgrField.set(cmd, domainMgr);
|
||||
|
||||
Field configDaoField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_configDao");
|
||||
configDaoField.setAccessible(true);
|
||||
configDaoField.set(cmd, configDao);
|
||||
|
||||
String spId = "someSPID";
|
||||
String url = "someUrl";
|
||||
X509Certificate cert = SAMLUtils.generateRandomX509Certificate();
|
||||
Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId);
|
||||
Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(null);
|
||||
Mockito.when(samlAuthManager.getIdpSingleSignOnUrl()).thenReturn(url);
|
||||
Mockito.when(samlAuthManager.getSpSingleSignOnUrl()).thenReturn(url);
|
||||
|
||||
Mockito.when(session.getAttribute(Mockito.anyString())).thenReturn(null);
|
||||
Mockito.when(configDao.getValue(Mockito.anyString())).thenReturn("someString");
|
||||
|
||||
Mockito.when(domain.getId()).thenReturn(1L);
|
||||
Mockito.when(domainMgr.getDomain(Mockito.anyString())).thenReturn(domain);
|
||||
UserVO user = new UserVO();
|
||||
user.setUuid(SAMLUtils.createSAMLId("someUID"));
|
||||
Mockito.when(entityMgr.findByUuid(Mockito.eq(User.class), Mockito.anyString())).thenReturn((User) user);
|
||||
Mockito.when(apiServer.verifyUser(Mockito.anyLong())).thenReturn(false);
|
||||
|
||||
Map<String, Object[]> params = new HashMap<String, Object[]>();
|
||||
|
||||
// SSO redirection test
|
||||
cmd.authenticate("command", params, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), resp);
|
||||
Mockito.verify(resp, Mockito.times(1)).sendRedirect(Mockito.anyString());
|
||||
|
||||
// SSO SAMLResponse verification test, this should throw ServerApiException for auth failure
|
||||
params.put(SAMLUtils.SAML_RESPONSE, new String[]{"Some String"});
|
||||
Mockito.stub(cmd.processSAMLResponse(Mockito.anyString())).toReturn(buildMockResponse());
|
||||
try {
|
||||
cmd.authenticate("command", params, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), resp);
|
||||
} catch (ServerApiException ignored) {
|
||||
}
|
||||
Mockito.verify(configDao, Mockito.atLeastOnce()).getValue(Mockito.anyString());
|
||||
Mockito.verify(domainMgr, Mockito.times(1)).getDomain(Mockito.anyString());
|
||||
Mockito.verify(entityMgr, Mockito.times(1)).findByUuid(Mockito.eq(User.class), Mockito.anyString());
|
||||
Mockito.verify(apiServer, Mockito.times(1)).verifyUser(Mockito.anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAPIType() {
|
||||
Assert.assertTrue(new GetServiceProviderMetaDataCmd().getAPIType() == APIAuthenticationType.LOGIN_API);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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.api.command;
|
||||
|
||||
import com.cloud.utils.HttpUtils;
|
||||
import org.apache.cloudstack.api.ApiServerService;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationType;
|
||||
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
|
||||
import org.apache.cloudstack.saml.SAML2AuthManager;
|
||||
import org.apache.cloudstack.utils.auth.SAMLUtils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.lang.reflect.Field;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class SAML2LogoutAPIAuthenticatorCmdTest {
|
||||
|
||||
@Mock
|
||||
ApiServerService apiServer;
|
||||
|
||||
@Mock
|
||||
SAML2AuthManager samlAuthManager;
|
||||
|
||||
@Mock
|
||||
ConfigurationDao configDao;
|
||||
|
||||
@Mock
|
||||
HttpSession session;
|
||||
|
||||
@Mock
|
||||
HttpServletResponse resp;
|
||||
|
||||
@Test
|
||||
public void testAuthenticate() throws Exception {
|
||||
SAML2LogoutAPIAuthenticatorCmd cmd = new SAML2LogoutAPIAuthenticatorCmd();
|
||||
|
||||
Field apiServerField = SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_apiServer");
|
||||
apiServerField.setAccessible(true);
|
||||
apiServerField.set(cmd, apiServer);
|
||||
|
||||
Field managerField = SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_samlAuthManager");
|
||||
managerField.setAccessible(true);
|
||||
managerField.set(cmd, samlAuthManager);
|
||||
|
||||
Field configDaoField = SAML2LogoutAPIAuthenticatorCmd.class.getDeclaredField("_configDao");
|
||||
configDaoField.setAccessible(true);
|
||||
configDaoField.set(cmd, configDao);
|
||||
|
||||
String spId = "someSPID";
|
||||
String url = "someUrl";
|
||||
X509Certificate cert = SAMLUtils.generateRandomX509Certificate();
|
||||
Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId);
|
||||
Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(cert);
|
||||
Mockito.when(samlAuthManager.getIdpSingleLogOutUrl()).thenReturn(url);
|
||||
Mockito.when(samlAuthManager.getSpSingleLogOutUrl()).thenReturn(url);
|
||||
Mockito.when(session.getAttribute(Mockito.anyString())).thenReturn(null);
|
||||
Mockito.when(configDao.getValue(Mockito.anyString())).thenReturn("someString");
|
||||
|
||||
cmd.authenticate("command", null, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), resp);
|
||||
Mockito.verify(resp, Mockito.times(1)).sendRedirect(Mockito.anyString());
|
||||
Mockito.verify(session, Mockito.atLeastOnce()).getAttribute(Mockito.anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAPIType() throws Exception {
|
||||
Assert.assertTrue(new SAML2LogoutAPIAuthenticatorCmd().getAPIType() == APIAuthenticationType.LOGOUT_API);
|
||||
}
|
||||
}
|
||||
1
pom.xml
1
pom.xml
|
|
@ -94,6 +94,7 @@
|
|||
<cs.mycila.license.version>2.5</cs.mycila.license.version>
|
||||
<cs.findbugs.version>2.5.3</cs.findbugs.version>
|
||||
<cs.javadoc.version>2.9.1</cs.javadoc.version>
|
||||
<cs.opensaml.version>2.6.1</cs.opensaml.version>
|
||||
</properties>
|
||||
|
||||
<distributionManagement>
|
||||
|
|
|
|||
|
|
@ -133,6 +133,11 @@
|
|||
<artifactId>cloud-engine-components-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.opensaml</groupId>
|
||||
<artifactId>opensaml</artifactId>
|
||||
<version>${cs.opensaml.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<testResources>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,10 @@
|
|||
http://www.springframework.org/schema/util/spring-util-3.0.xsd"
|
||||
>
|
||||
|
||||
<bean id="authenticationManagerImpl" class="com.cloud.api.auth.APIAuthenticationManagerImpl" />
|
||||
<bean id="authenticationManagerImpl" class="com.cloud.api.auth.APIAuthenticationManagerImpl">
|
||||
<property name="apiAuthenticators"
|
||||
value="#{pluggableAPIAuthenticatorsRegistry.registered}" />
|
||||
</bean>
|
||||
|
||||
<bean id="accountManagerImpl" class="com.cloud.user.AccountManagerImpl">
|
||||
<property name="userAuthenticators"
|
||||
|
|
|
|||
|
|
@ -16,80 +16,49 @@
|
|||
// under the License.
|
||||
package com.cloud.api;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.inject.Inject;
|
||||
import javax.naming.ConfigurationException;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import com.cloud.api.dispatch.DispatchChainFactory;
|
||||
import com.cloud.api.dispatch.DispatchTask;
|
||||
import com.cloud.api.response.ApiResponseSerializer;
|
||||
import com.cloud.configuration.Config;
|
||||
import com.cloud.domain.Domain;
|
||||
import com.cloud.domain.DomainVO;
|
||||
import com.cloud.domain.dao.DomainDao;
|
||||
import com.cloud.event.ActionEventUtils;
|
||||
import com.cloud.event.EventCategory;
|
||||
import com.cloud.event.EventTypes;
|
||||
import com.cloud.exception.AccountLimitException;
|
||||
import com.cloud.exception.CloudAuthenticationException;
|
||||
import com.cloud.exception.InsufficientCapacityException;
|
||||
import com.cloud.exception.InvalidParameterValueException;
|
||||
import com.cloud.exception.PermissionDeniedException;
|
||||
import com.cloud.exception.RequestLimitException;
|
||||
import com.cloud.exception.ResourceAllocationException;
|
||||
import com.cloud.exception.ResourceUnavailableException;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountManager;
|
||||
import com.cloud.user.DomainManager;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.user.UserAccount;
|
||||
import com.cloud.user.UserVO;
|
||||
import com.cloud.utils.HttpUtils;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.http.ConnectionClosedException;
|
||||
import org.apache.http.HttpException;
|
||||
import org.apache.http.HttpRequest;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.HttpServerConnection;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.utils.URLEncodedUtils;
|
||||
import org.apache.http.entity.BasicHttpEntity;
|
||||
import org.apache.http.impl.DefaultHttpResponseFactory;
|
||||
import org.apache.http.impl.DefaultHttpServerConnection;
|
||||
import org.apache.http.impl.NoConnectionReuseStrategy;
|
||||
import org.apache.http.impl.SocketHttpServerConnection;
|
||||
import org.apache.http.params.BasicHttpParams;
|
||||
import org.apache.http.params.CoreConnectionPNames;
|
||||
import org.apache.http.params.CoreProtocolPNames;
|
||||
import org.apache.http.params.HttpParams;
|
||||
import org.apache.http.protocol.BasicHttpContext;
|
||||
import org.apache.http.protocol.BasicHttpProcessor;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
import org.apache.http.protocol.HttpRequestHandler;
|
||||
import org.apache.http.protocol.HttpRequestHandlerRegistry;
|
||||
import org.apache.http.protocol.HttpService;
|
||||
import org.apache.http.protocol.ResponseConnControl;
|
||||
import org.apache.http.protocol.ResponseContent;
|
||||
import org.apache.http.protocol.ResponseDate;
|
||||
import org.apache.http.protocol.ResponseServer;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.cloud.utils.NumbersUtil;
|
||||
import com.cloud.utils.Pair;
|
||||
import com.cloud.utils.StringUtils;
|
||||
import com.cloud.utils.component.ComponentContext;
|
||||
import com.cloud.utils.component.ManagerBase;
|
||||
import com.cloud.utils.component.PluggableService;
|
||||
import com.cloud.utils.concurrency.NamedThreadFactory;
|
||||
import com.cloud.utils.db.EntityManager;
|
||||
import com.cloud.utils.db.SearchCriteria;
|
||||
import com.cloud.utils.db.TransactionLegacy;
|
||||
import com.cloud.utils.db.UUIDManager;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
import com.cloud.utils.exception.ExceptionProxyObject;
|
||||
import org.apache.cloudstack.acl.APIChecker;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.ApiServerService;
|
||||
import org.apache.cloudstack.api.BaseAsyncCmd;
|
||||
import org.apache.cloudstack.api.BaseAsyncCreateCmd;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
|
|
@ -122,6 +91,7 @@ import org.apache.cloudstack.api.response.AsyncJobResponse;
|
|||
import org.apache.cloudstack.api.response.CreateCmdResponse;
|
||||
import org.apache.cloudstack.api.response.ExceptionResponse;
|
||||
import org.apache.cloudstack.api.response.ListResponse;
|
||||
import org.apache.cloudstack.api.response.LoginCmdResponse;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
|
||||
import org.apache.cloudstack.framework.config.impl.ConfigurationVO;
|
||||
|
|
@ -134,44 +104,74 @@ import org.apache.cloudstack.framework.messagebus.MessageBus;
|
|||
import org.apache.cloudstack.framework.messagebus.MessageDispatcher;
|
||||
import org.apache.cloudstack.framework.messagebus.MessageHandler;
|
||||
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.http.ConnectionClosedException;
|
||||
import org.apache.http.HttpException;
|
||||
import org.apache.http.HttpRequest;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.HttpServerConnection;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.utils.URLEncodedUtils;
|
||||
import org.apache.http.entity.BasicHttpEntity;
|
||||
import org.apache.http.impl.DefaultHttpResponseFactory;
|
||||
import org.apache.http.impl.DefaultHttpServerConnection;
|
||||
import org.apache.http.impl.NoConnectionReuseStrategy;
|
||||
import org.apache.http.impl.SocketHttpServerConnection;
|
||||
import org.apache.http.params.BasicHttpParams;
|
||||
import org.apache.http.params.CoreConnectionPNames;
|
||||
import org.apache.http.params.CoreProtocolPNames;
|
||||
import org.apache.http.params.HttpParams;
|
||||
import org.apache.http.protocol.BasicHttpContext;
|
||||
import org.apache.http.protocol.BasicHttpProcessor;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
import org.apache.http.protocol.HttpRequestHandler;
|
||||
import org.apache.http.protocol.HttpRequestHandlerRegistry;
|
||||
import org.apache.http.protocol.HttpService;
|
||||
import org.apache.http.protocol.ResponseConnControl;
|
||||
import org.apache.http.protocol.ResponseContent;
|
||||
import org.apache.http.protocol.ResponseDate;
|
||||
import org.apache.http.protocol.ResponseServer;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.cloud.api.dispatch.DispatchChainFactory;
|
||||
import com.cloud.api.dispatch.DispatchTask;
|
||||
import com.cloud.api.response.ApiResponseSerializer;
|
||||
import com.cloud.configuration.Config;
|
||||
import com.cloud.domain.Domain;
|
||||
import com.cloud.domain.DomainVO;
|
||||
import com.cloud.domain.dao.DomainDao;
|
||||
import com.cloud.event.ActionEventUtils;
|
||||
import com.cloud.event.EventCategory;
|
||||
import com.cloud.event.EventTypes;
|
||||
import com.cloud.exception.AccountLimitException;
|
||||
import com.cloud.exception.CloudAuthenticationException;
|
||||
import com.cloud.exception.InsufficientCapacityException;
|
||||
import com.cloud.exception.InvalidParameterValueException;
|
||||
import com.cloud.exception.PermissionDeniedException;
|
||||
import com.cloud.exception.RequestLimitException;
|
||||
import com.cloud.exception.ResourceAllocationException;
|
||||
import com.cloud.exception.ResourceUnavailableException;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountManager;
|
||||
import com.cloud.user.DomainManager;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.user.UserAccount;
|
||||
import com.cloud.user.UserVO;
|
||||
import com.cloud.utils.NumbersUtil;
|
||||
import com.cloud.utils.Pair;
|
||||
import com.cloud.utils.StringUtils;
|
||||
import com.cloud.utils.component.ComponentContext;
|
||||
import com.cloud.utils.component.ManagerBase;
|
||||
import com.cloud.utils.component.PluggableService;
|
||||
import com.cloud.utils.concurrency.NamedThreadFactory;
|
||||
import com.cloud.utils.db.EntityManager;
|
||||
import com.cloud.utils.db.SearchCriteria;
|
||||
import com.cloud.utils.db.TransactionLegacy;
|
||||
import com.cloud.utils.db.UUIDManager;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
import com.cloud.utils.exception.ExceptionProxyObject;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.inject.Inject;
|
||||
import javax.naming.ConfigurationException;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Component
|
||||
public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiServerService {
|
||||
|
|
@ -932,8 +932,55 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
|
|||
return null;
|
||||
}
|
||||
|
||||
private ResponseObject createLoginResponse(HttpSession session) {
|
||||
LoginCmdResponse response = new LoginCmdResponse();
|
||||
response.setTimeout(session.getMaxInactiveInterval());
|
||||
|
||||
final String user_UUID = (String)session.getAttribute("user_UUID");
|
||||
session.removeAttribute("user_UUID");
|
||||
response.setUserId(user_UUID);
|
||||
|
||||
final String domain_UUID = (String)session.getAttribute("domain_UUID");
|
||||
session.removeAttribute("domain_UUID");
|
||||
response.setDomainId(domain_UUID);
|
||||
|
||||
final Enumeration attrNames = session.getAttributeNames();
|
||||
if (attrNames != null) {
|
||||
while (attrNames.hasMoreElements()) {
|
||||
final String attrName = (String) attrNames.nextElement();
|
||||
final Object attrObj = session.getAttribute(attrName);
|
||||
if (ApiConstants.USERNAME.equalsIgnoreCase(attrName)) {
|
||||
response.setUsername(attrObj.toString());
|
||||
}
|
||||
if (ApiConstants.ACCOUNT.equalsIgnoreCase(attrName)) {
|
||||
response.setAccount(attrObj.toString());
|
||||
}
|
||||
if (ApiConstants.FIRSTNAME.equalsIgnoreCase(attrName)) {
|
||||
response.setFirstName(attrObj.toString());
|
||||
}
|
||||
if (ApiConstants.LASTNAME.equalsIgnoreCase(attrName)) {
|
||||
response.setLastName(attrObj.toString());
|
||||
}
|
||||
if (ApiConstants.TYPE.equalsIgnoreCase(attrName)) {
|
||||
response.setType((attrObj.toString()));
|
||||
}
|
||||
if (ApiConstants.TIMEZONE.equalsIgnoreCase(attrName)) {
|
||||
response.setTimeZone(attrObj.toString());
|
||||
}
|
||||
if (ApiConstants.REGISTERED.equalsIgnoreCase(attrName)) {
|
||||
response.setRegistered(attrObj.toString());
|
||||
}
|
||||
if (ApiConstants.SESSIONKEY.equalsIgnoreCase(attrName)) {
|
||||
response.setSessionKey(attrObj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
response.setResponseName("loginresponse");
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loginUser(final HttpSession session, final String username, final String password, Long domainId, final String domainPath, final String loginIpAddress,
|
||||
public ResponseObject loginUser(final HttpSession session, final String username, final String password, Long domainId, final String domainPath, final String loginIpAddress,
|
||||
final Map<String, Object[]> requestParameters) throws CloudAuthenticationException {
|
||||
// We will always use domainId first. If that does not exist, we will use domain name. If THAT doesn't exist
|
||||
// we will default to ROOT
|
||||
|
|
@ -1003,7 +1050,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
|
|||
final String sessionKey = Base64.encodeBase64String(sessionKeyBytes);
|
||||
session.setAttribute("sessionkey", sessionKey);
|
||||
|
||||
return;
|
||||
return createLoginResponse(session);
|
||||
}
|
||||
throw new CloudAuthenticationException("Failed to authenticate user " + username + " in domain " + domainId + "; please provide valid credentials");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
// under the License.
|
||||
package com.cloud.api;
|
||||
|
||||
import com.cloud.api.auth.APIAuthenticationManager;
|
||||
import com.cloud.api.auth.APIAuthenticationType;
|
||||
import com.cloud.api.auth.APIAuthenticator;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationManager;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationType;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticator;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountService;
|
||||
import com.cloud.user.User;
|
||||
|
|
@ -26,6 +26,7 @@ import com.cloud.utils.HttpUtils;
|
|||
import com.cloud.utils.StringUtils;
|
||||
import com.cloud.utils.db.EntityManager;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.ApiServerService;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.managed.context.ManagedContext;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ package com.cloud.api.auth;
|
|||
import com.cloud.utils.component.ComponentContext;
|
||||
import com.cloud.utils.component.ManagerBase;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationManager;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticator;
|
||||
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import javax.ejb.Local;
|
||||
|
|
@ -32,12 +35,21 @@ import java.util.Map;
|
|||
public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuthenticationManager {
|
||||
public static final Logger s_logger = Logger.getLogger(APIAuthenticationManagerImpl.class.getName());
|
||||
|
||||
private List<PluggableAPIAuthenticator> _apiAuthenticators;
|
||||
|
||||
private static Map<String, Class<?>> s_authenticators = null;
|
||||
private static List<Class<?>> s_commandList = null;
|
||||
|
||||
public APIAuthenticationManagerImpl() {
|
||||
}
|
||||
|
||||
public List<PluggableAPIAuthenticator> getApiAuthenticators() {
|
||||
return _apiAuthenticators;
|
||||
}
|
||||
|
||||
public void setApiAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
|
||||
_apiAuthenticators = authenticators;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean start() {
|
||||
s_authenticators = new HashMap<String, Class<?>>();
|
||||
|
|
@ -53,12 +65,13 @@ public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuth
|
|||
|
||||
@Override
|
||||
public List<Class<?>> getCommands() {
|
||||
if (s_commandList == null) {
|
||||
s_commandList = new ArrayList<Class<?>>();
|
||||
s_commandList.add(DefaultLoginAPIAuthenticatorCmd.class);
|
||||
s_commandList.add(DefaultLogoutAPIAuthenticatorCmd.class);
|
||||
List<Class<?>> cmdList = new ArrayList<Class<?>>();
|
||||
cmdList.add(DefaultLoginAPIAuthenticatorCmd.class);
|
||||
cmdList.add(DefaultLogoutAPIAuthenticatorCmd.class);
|
||||
for (PluggableAPIAuthenticator apiAuthenticator: _apiAuthenticators) {
|
||||
cmdList.addAll(apiAuthenticator.getAuthCommands());
|
||||
}
|
||||
return s_commandList;
|
||||
return cmdList;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -68,6 +81,7 @@ public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuth
|
|||
try {
|
||||
apiAuthenticator = (APIAuthenticator) s_authenticators.get(name).newInstance();
|
||||
apiAuthenticator = ComponentContext.inject(apiAuthenticator);
|
||||
apiAuthenticator.setAuthenticators(_apiAuthenticators);
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
if (s_logger.isDebugEnabled()) {
|
||||
s_logger.debug("APIAuthenticationManagerImpl::getAPIAuthenticator failed: " + e.getMessage());
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
// under the License.
|
||||
package com.cloud.api.auth;
|
||||
|
||||
import com.cloud.api.ApiServerService;
|
||||
import org.apache.cloudstack.api.ApiServerService;
|
||||
import com.cloud.api.response.ApiResponseSerializer;
|
||||
import com.cloud.exception.CloudAuthenticationException;
|
||||
import com.cloud.user.Account;
|
||||
|
|
@ -25,15 +25,17 @@ import org.apache.cloudstack.api.ApiConstants;
|
|||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
import org.apache.cloudstack.api.Parameter;
|
||||
import org.apache.cloudstack.api.ResponseObject;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationType;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticator;
|
||||
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
|
||||
import org.apache.cloudstack.api.response.LoginCmdResponse;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@APICommand(name = "login", description = "Logs a user into the CloudStack. A successful login attempt will generate a JSESSIONID cookie value that can be passed in subsequent Query command calls until the \"logout\" command has been issued or the session has expired.", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {})
|
||||
|
|
@ -100,54 +102,6 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe
|
|||
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly");
|
||||
}
|
||||
|
||||
private String createLoginResponse(HttpSession session, String responseType) {
|
||||
LoginCmdResponse response = new LoginCmdResponse();
|
||||
response.setTimeout(session.getMaxInactiveInterval());
|
||||
|
||||
final String user_UUID = (String)session.getAttribute("user_UUID");
|
||||
session.removeAttribute("user_UUID");
|
||||
response.setUserId(user_UUID);
|
||||
|
||||
final String domain_UUID = (String)session.getAttribute("domain_UUID");
|
||||
session.removeAttribute("domain_UUID");
|
||||
response.setDomainId(domain_UUID);
|
||||
|
||||
// FIXME: the while loop mess
|
||||
final Enumeration attrNames = session.getAttributeNames();
|
||||
if (attrNames != null) {
|
||||
while (attrNames.hasMoreElements()) {
|
||||
final String attrName = (String) attrNames.nextElement();
|
||||
final Object attrObj = session.getAttribute(attrName);
|
||||
if (ApiConstants.USERNAME.equalsIgnoreCase(attrName)) {
|
||||
response.setUsername(attrObj.toString());
|
||||
}
|
||||
if (ApiConstants.ACCOUNT.equalsIgnoreCase(attrName)) {
|
||||
response.setAccount(attrObj.toString());
|
||||
}
|
||||
if (ApiConstants.FIRSTNAME.equalsIgnoreCase(attrName)) {
|
||||
response.setFirstName(attrObj.toString());
|
||||
}
|
||||
if (ApiConstants.LASTNAME.equalsIgnoreCase(attrName)) {
|
||||
response.setLastName(attrObj.toString());
|
||||
}
|
||||
if (ApiConstants.TYPE.equalsIgnoreCase(attrName)) {
|
||||
response.setType((attrObj.toString()));
|
||||
}
|
||||
if (ApiConstants.TIMEZONE.equalsIgnoreCase(attrName)) {
|
||||
response.setTimeZone(attrObj.toString());
|
||||
}
|
||||
if (ApiConstants.REGISTERED.equalsIgnoreCase(attrName)) {
|
||||
response.setRegistered(attrObj.toString());
|
||||
}
|
||||
if (ApiConstants.SESSIONKEY.equalsIgnoreCase(attrName)) {
|
||||
response.setSessionKey(attrObj.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
response.setResponseName(getCommandName());
|
||||
return ApiResponseSerializer.toSerializedString((ResponseObject) response, responseType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String authenticate(String command, Map<String, Object[]> params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException {
|
||||
|
||||
|
|
@ -197,10 +151,8 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe
|
|||
if (username != null) {
|
||||
final String pwd = ((password == null) ? null : password[0]);
|
||||
try {
|
||||
_apiServer.loginUser(session, username[0], pwd, domainId, domain, remoteAddress, params);
|
||||
auditTrailSb.insert(0, "(userId=" + session.getAttribute("userid") + " accountId=" + ((Account) session.getAttribute("accountobj")).getId() +
|
||||
" sessionId=" + session.getId() + ")");
|
||||
return createLoginResponse(session, responseType);
|
||||
return ApiResponseSerializer.toSerializedString(_apiServer.loginUser(session, username[0], pwd, domainId, domain, remoteAddress, params),
|
||||
responseType);
|
||||
} catch (final CloudAuthenticationException ex) {
|
||||
// TODO: fall through to API key, or just fail here w/ auth error? (HTTP 401)
|
||||
try {
|
||||
|
|
@ -222,4 +174,8 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe
|
|||
public APIAuthenticationType getAPIType() {
|
||||
return APIAuthenticationType.LOGIN_API;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,11 +22,15 @@ 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.api.auth.APIAuthenticationType;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticator;
|
||||
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
|
||||
import org.apache.cloudstack.api.response.LogoutCmdResponse;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@APICommand(name = "logout", description = "Logs out the user", responseObject = LogoutCmdResponse.class, entityType = {})
|
||||
|
|
@ -68,4 +72,8 @@ public class DefaultLogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuth
|
|||
public APIAuthenticationType getAPIType() {
|
||||
return APIAuthenticationType.LOGOUT_API;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1379,6 +1379,86 @@ public enum Config {
|
|||
"300000",
|
||||
"The allowable clock difference in milliseconds between when an SSO login request is made and when it is received.",
|
||||
null),
|
||||
SAMLIsPluginEnabled(
|
||||
"Advanced",
|
||||
ManagementServer.class,
|
||||
Boolean.class,
|
||||
"saml2.enabled",
|
||||
"false",
|
||||
"Set it to true to enable SAML SSO plugin",
|
||||
null),
|
||||
SAMLUserAccountName(
|
||||
"Advanced",
|
||||
ManagementServer.class,
|
||||
String.class,
|
||||
"saml2.default.accountname",
|
||||
"admin",
|
||||
"The name of the default account to use when creating users from SAML SSO",
|
||||
null),
|
||||
SAMLUserDomain(
|
||||
"Advanced",
|
||||
ManagementServer.class,
|
||||
String.class,
|
||||
"saml2.default.domainid",
|
||||
"1",
|
||||
"The default domain UUID to use when creating users from SAML SSO",
|
||||
null),
|
||||
SAMLCloudStackRedirectionUrl(
|
||||
"Advanced",
|
||||
ManagementServer.class,
|
||||
String.class,
|
||||
"saml2.redirect.url",
|
||||
"http://localhost:8080/client",
|
||||
"The CloudStack UI url the SSO should redirected to when successful",
|
||||
null),
|
||||
SAMLServiceProviderID(
|
||||
"Advanced",
|
||||
ManagementServer.class,
|
||||
String.class,
|
||||
"saml2.sp.id",
|
||||
"org.apache.cloudstack",
|
||||
"SAML2 Service Provider Identifier String",
|
||||
null),
|
||||
SAMLServiceProviderSingleSignOnURL(
|
||||
"Advanced",
|
||||
ManagementServer.class,
|
||||
String.class,
|
||||
"saml2.sp.sso.url",
|
||||
"http://localhost:8080/client/api?command=samlsso",
|
||||
"SAML2 CloudStack Service Provider Single Sign On URL",
|
||||
null),
|
||||
SAMLServiceProviderSingleLogOutURL(
|
||||
"Advanced",
|
||||
ManagementServer.class,
|
||||
String.class,
|
||||
"saml2.sp.slo.url",
|
||||
"http://localhost:8080/client/api?command=samlslo",
|
||||
"SAML2 CloudStack Service Provider Single Log Out URL",
|
||||
null),
|
||||
SAMLIdentityProviderID(
|
||||
"Advanced",
|
||||
ManagementServer.class,
|
||||
String.class,
|
||||
"saml2.idp.id",
|
||||
"https://openidp.feide.no",
|
||||
"SAML2 Identity Provider Identifier String",
|
||||
null),
|
||||
SAMLIdentityProviderMetadataURL(
|
||||
"Advanced",
|
||||
ManagementServer.class,
|
||||
String.class,
|
||||
"saml2.idp.metadata.url",
|
||||
"https://openidp.feide.no/simplesaml/saml2/idp/metadata.php",
|
||||
"SAML2 Identity Provider Metadata XML Url",
|
||||
null),
|
||||
SAMLTimeout(
|
||||
"Advanced",
|
||||
ManagementServer.class,
|
||||
Long.class,
|
||||
"saml2.timeout",
|
||||
"30000",
|
||||
"SAML2 IDP Metadata Downloading and parsing etc. activity timeout in milliseconds",
|
||||
null),
|
||||
//NetworkType("Hidden", ManagementServer.class, String.class, "network.type", "vlan", "The type of network that this deployment will use.", "vlan,direct"),
|
||||
RouterRamSize("Hidden", NetworkOrchestrationService.class, Integer.class, "router.ram.size", "128", "Default RAM for router VM (in MB).", null),
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
// under the License.
|
||||
package com.cloud.api;
|
||||
|
||||
import com.cloud.api.auth.APIAuthenticationManager;
|
||||
import com.cloud.api.auth.APIAuthenticationType;
|
||||
import com.cloud.api.auth.APIAuthenticator;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationManager;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationType;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticator;
|
||||
import com.cloud.server.ManagementServer;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountService;
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ known_categories = {
|
|||
'login': 'Authentication',
|
||||
'logout': 'Authentication',
|
||||
'saml': 'Authentication',
|
||||
'getSPMetadata': 'Authentication',
|
||||
'Capacity': 'System Capacity',
|
||||
'NetworkDevice': 'Network Device',
|
||||
'ExternalLoadBalancer': 'Ext Load Balancer',
|
||||
|
|
|
|||
|
|
@ -129,17 +129,25 @@
|
|||
i.e. calling listCapabilities API with g_sessionKey from $.cookie('sessionKey') will succeed,
|
||||
then userValid will be set to true, then an user object (instead of "false") will be returned, then login screen will be bypassed.
|
||||
*/
|
||||
var unBoxCookieValue = function (cookieName) {
|
||||
var cookieValue = $.cookie(cookieName);
|
||||
if (cookieValue && cookieValue.length > 2 && cookieValue[0] === '"' && cookieValue[cookieValue.length-1] === '"') {
|
||||
cookieValue = cookieValue.slice(1, cookieValue.length-1);
|
||||
$.cookie(cookieName, cookieValue, { expires: 1 });
|
||||
}
|
||||
return cookieValue;
|
||||
};
|
||||
g_mySession = $.cookie('JSESSIONID');
|
||||
g_sessionKey = $.cookie('sessionKey');
|
||||
g_role = $.cookie('role');
|
||||
g_username = $.cookie('username');
|
||||
g_userid = $.cookie('userid');
|
||||
g_account = $.cookie('account');
|
||||
g_domainid = $.cookie('domainid');
|
||||
g_userfullname = $.cookie('userfullname');
|
||||
g_timezone = $.cookie('timezone');
|
||||
g_sessionKey = unBoxCookieValue('sessionKey');
|
||||
g_role = unBoxCookieValue('role');
|
||||
g_userid = unBoxCookieValue('userid');
|
||||
g_domainid = unBoxCookieValue('domainid');
|
||||
g_account = unBoxCookieValue('account');
|
||||
g_username = unBoxCookieValue('username');
|
||||
g_userfullname = unBoxCookieValue('userfullname');
|
||||
g_timezone = unBoxCookieValue('timezone');
|
||||
if ($.cookie('timezoneoffset') != null)
|
||||
g_timezoneoffset = isNaN($.cookie('timezoneoffset')) ? null : parseFloat($.cookie('timezoneoffset'));
|
||||
g_timezoneoffset = isNaN(unBoxCookieValue('timezoneoffset')) ? null : parseFloat(unBoxCookieValue('timezoneoffset'));
|
||||
else
|
||||
g_timezoneoffset = null;
|
||||
} else { //single-sign-on (bypass login screen)
|
||||
|
|
|
|||
|
|
@ -143,6 +143,11 @@
|
|||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.opensaml</groupId>
|
||||
<artifactId>opensaml</artifactId>
|
||||
<version>${cs.opensaml.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-net</groupId>
|
||||
<artifactId>commons-net</artifactId>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,232 @@
|
|||
//
|
||||
// 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.utils.auth;
|
||||
|
||||
import com.cloud.utils.HttpUtils;
|
||||
import org.apache.log4j.Logger;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.x509.X509V1CertificateGenerator;
|
||||
import org.joda.time.DateTime;
|
||||
import org.opensaml.Configuration;
|
||||
import org.opensaml.common.SAMLVersion;
|
||||
import org.opensaml.common.xml.SAMLConstants;
|
||||
import org.opensaml.saml2.core.AuthnContextClassRef;
|
||||
import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration;
|
||||
import org.opensaml.saml2.core.AuthnRequest;
|
||||
import org.opensaml.saml2.core.Issuer;
|
||||
import org.opensaml.saml2.core.LogoutRequest;
|
||||
import org.opensaml.saml2.core.NameID;
|
||||
import org.opensaml.saml2.core.NameIDPolicy;
|
||||
import org.opensaml.saml2.core.NameIDType;
|
||||
import org.opensaml.saml2.core.RequestedAuthnContext;
|
||||
import org.opensaml.saml2.core.Response;
|
||||
import org.opensaml.saml2.core.SessionIndex;
|
||||
import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder;
|
||||
import org.opensaml.saml2.core.impl.AuthnRequestBuilder;
|
||||
import org.opensaml.saml2.core.impl.IssuerBuilder;
|
||||
import org.opensaml.saml2.core.impl.LogoutRequestBuilder;
|
||||
import org.opensaml.saml2.core.impl.NameIDBuilder;
|
||||
import org.opensaml.saml2.core.impl.NameIDPolicyBuilder;
|
||||
import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder;
|
||||
import org.opensaml.saml2.core.impl.SessionIndexBuilder;
|
||||
import org.opensaml.xml.ConfigurationException;
|
||||
import org.opensaml.xml.XMLObject;
|
||||
import org.opensaml.xml.io.Marshaller;
|
||||
import org.opensaml.xml.io.MarshallingException;
|
||||
import org.opensaml.xml.io.Unmarshaller;
|
||||
import org.opensaml.xml.io.UnmarshallerFactory;
|
||||
import org.opensaml.xml.io.UnmarshallingException;
|
||||
import org.opensaml.xml.util.Base64;
|
||||
import org.opensaml.xml.util.XMLHelper;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URLEncoder;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Security;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
|
||||
public class SAMLUtils {
|
||||
public static final Logger s_logger = Logger.getLogger(SAMLUtils.class);
|
||||
|
||||
public static final String SAML_RESPONSE = "SAMLResponse";
|
||||
public static final String SAML_NS = "saml://";
|
||||
public static final String SAML_NAMEID = "SAML_NAMEID";
|
||||
public static final String SAML_SESSION = "SAML_SESSION";
|
||||
public static final String CERTIFICATE_NAME = "SAMLSP_X509CERTIFICATE";
|
||||
|
||||
public static String createSAMLId(String uid) {
|
||||
return SAML_NS + uid;
|
||||
}
|
||||
|
||||
public static Boolean checkSAMLUserId(String uuid) {
|
||||
return uuid.startsWith(SAML_NS);
|
||||
}
|
||||
|
||||
public static String generateSecureRandomId() {
|
||||
return new BigInteger(160, new SecureRandom()).toString(32);
|
||||
}
|
||||
|
||||
public static AuthnRequest buildAuthnRequestObject(String spId, String idpUrl, String consumerUrl) {
|
||||
String authnId = generateSecureRandomId();
|
||||
// Issuer object
|
||||
IssuerBuilder issuerBuilder = new IssuerBuilder();
|
||||
Issuer issuer = issuerBuilder.buildObject();
|
||||
issuer.setValue(spId);
|
||||
|
||||
// NameIDPolicy
|
||||
NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder();
|
||||
NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject();
|
||||
nameIdPolicy.setFormat(NameIDType.PERSISTENT);
|
||||
nameIdPolicy.setSPNameQualifier(spId);
|
||||
nameIdPolicy.setAllowCreate(true);
|
||||
|
||||
// AuthnContextClass
|
||||
AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder();
|
||||
AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject(
|
||||
SAMLConstants.SAML20_NS,
|
||||
"AuthnContextClassRef", "saml");
|
||||
authnContextClassRef.setAuthnContextClassRef("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport");
|
||||
|
||||
// AuthnContex
|
||||
RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder();
|
||||
RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject();
|
||||
requestedAuthnContext
|
||||
.setComparison(AuthnContextComparisonTypeEnumeration.MINIMUM);
|
||||
requestedAuthnContext.getAuthnContextClassRefs().add(
|
||||
authnContextClassRef);
|
||||
|
||||
// Creation of AuthRequestObject
|
||||
AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder();
|
||||
AuthnRequest authnRequest = authRequestBuilder.buildObject();
|
||||
authnRequest.setID(authnId);
|
||||
authnRequest.setDestination(idpUrl);
|
||||
authnRequest.setVersion(SAMLVersion.VERSION_20);
|
||||
authnRequest.setForceAuthn(false);
|
||||
authnRequest.setIsPassive(false);
|
||||
authnRequest.setIssuer(issuer);
|
||||
authnRequest.setIssueInstant(new DateTime());
|
||||
authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
|
||||
authnRequest.setAssertionConsumerServiceURL(consumerUrl);
|
||||
//authnRequest.setProviderName(spId);
|
||||
//authnRequest.setNameIDPolicy(nameIdPolicy);
|
||||
//authnRequest.setRequestedAuthnContext(requestedAuthnContext);
|
||||
|
||||
return authnRequest;
|
||||
}
|
||||
|
||||
public static LogoutRequest buildLogoutRequest(String logoutUrl, String spId, NameID sessionNameId, String sessionIndex) {
|
||||
IssuerBuilder issuerBuilder = new IssuerBuilder();
|
||||
Issuer issuer = issuerBuilder.buildObject();
|
||||
issuer.setValue(spId);
|
||||
|
||||
SessionIndex sessionIndexElement = new SessionIndexBuilder().buildObject();
|
||||
sessionIndexElement.setSessionIndex(sessionIndex);
|
||||
|
||||
NameID nameID = new NameIDBuilder().buildObject();
|
||||
nameID.setValue(sessionNameId.getValue());
|
||||
nameID.setFormat(sessionNameId.getFormat());
|
||||
|
||||
LogoutRequest logoutRequest = new LogoutRequestBuilder().buildObject();
|
||||
logoutRequest.setID(generateSecureRandomId());
|
||||
logoutRequest.setDestination(logoutUrl);
|
||||
logoutRequest.setVersion(SAMLVersion.VERSION_20);
|
||||
logoutRequest.setIssueInstant(new DateTime());
|
||||
logoutRequest.setIssuer(issuer);
|
||||
logoutRequest.getSessionIndexes().add(sessionIndexElement);
|
||||
logoutRequest.setNameID(nameID);
|
||||
return logoutRequest;
|
||||
}
|
||||
|
||||
public static String encodeSAMLRequest(XMLObject authnRequest)
|
||||
throws MarshallingException, IOException {
|
||||
Marshaller marshaller = Configuration.getMarshallerFactory()
|
||||
.getMarshaller(authnRequest);
|
||||
Element authDOM = marshaller.marshall(authnRequest);
|
||||
StringWriter requestWriter = new StringWriter();
|
||||
XMLHelper.writeNode(authDOM, requestWriter);
|
||||
String requestMessage = requestWriter.toString();
|
||||
Deflater deflater = new Deflater(Deflater.DEFLATED, true);
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, deflater);
|
||||
deflaterOutputStream.write(requestMessage.getBytes());
|
||||
deflaterOutputStream.close();
|
||||
String encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream.toByteArray(), Base64.DONT_BREAK_LINES);
|
||||
encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, HttpUtils.UTF_8).trim();
|
||||
return encodedRequestMessage;
|
||||
}
|
||||
|
||||
public static Response decodeSAMLResponse(String responseMessage)
|
||||
throws ConfigurationException, ParserConfigurationException,
|
||||
SAXException, IOException, UnmarshallingException {
|
||||
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
|
||||
documentBuilderFactory.setNamespaceAware(true);
|
||||
DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
|
||||
byte[] base64DecodedResponse = Base64.decode(responseMessage);
|
||||
Document document = docBuilder.parse(new ByteArrayInputStream(base64DecodedResponse));
|
||||
Element element = document.getDocumentElement();
|
||||
UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory();
|
||||
Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element);
|
||||
return (Response) unmarshaller.unmarshall(element);
|
||||
}
|
||||
|
||||
public static X509Certificate generateRandomX509Certificate() throws NoSuchAlgorithmException, NoSuchProviderException, CertificateEncodingException, SignatureException, InvalidKeyException {
|
||||
Date validityBeginDate = new Date(System.currentTimeMillis() - 24 * 60 * 60 * 1000);
|
||||
Date validityEndDate = new Date(System.currentTimeMillis() + 365 * 24 * 60 * 60 * 1000);
|
||||
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||
keyPairGenerator.initialize(1024, new SecureRandom());
|
||||
KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||
|
||||
X500Principal dnName = new X500Principal("CN=Apache CloudStack");
|
||||
X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
|
||||
certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
|
||||
certGen.setSubjectDN(dnName);
|
||||
certGen.setIssuerDN(dnName);
|
||||
certGen.setNotBefore(validityBeginDate);
|
||||
certGen.setNotAfter(validityEndDate);
|
||||
certGen.setPublicKey(keyPair.getPublic());
|
||||
certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
|
||||
|
||||
return certGen.generate(keyPair.getPrivate(), "BC");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
//
|
||||
// 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.utils.auth;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
import org.junit.Test;
|
||||
import org.opensaml.saml2.core.AuthnRequest;
|
||||
import org.opensaml.saml2.core.LogoutRequest;
|
||||
import org.opensaml.saml2.core.NameID;
|
||||
import org.opensaml.saml2.core.impl.NameIDBuilder;
|
||||
|
||||
public class SAMLUtilsTest extends TestCase {
|
||||
|
||||
@Test
|
||||
public void testSAMLId() throws Exception {
|
||||
assertTrue(SAMLUtils.checkSAMLUserId(SAMLUtils.createSAMLId("someUID")));
|
||||
assertFalse(SAMLUtils.checkSAMLUserId("randomUID"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateSecureRandomId() throws Exception {
|
||||
assertTrue(SAMLUtils.generateSecureRandomId().length() == 32);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildAuthnRequestObject() throws Exception {
|
||||
String consumerUrl = "http://someurl.com";
|
||||
String idpUrl = "http://idp.domain.example";
|
||||
String spId = "cloudstack";
|
||||
AuthnRequest req = SAMLUtils.buildAuthnRequestObject(spId, idpUrl, consumerUrl);
|
||||
assertEquals(req.getAssertionConsumerServiceURL(), consumerUrl);
|
||||
assertEquals(req.getDestination(), idpUrl);
|
||||
assertEquals(req.getIssuer().getValue(), spId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildLogoutRequest() throws Exception {
|
||||
String logoutUrl = "http://logoutUrl";
|
||||
String spId = "cloudstack";
|
||||
String sessionIndex = "12345";
|
||||
String nameIdString = "someNameID";
|
||||
NameID sessionNameId = new NameIDBuilder().buildObject();
|
||||
sessionNameId.setValue(nameIdString);
|
||||
LogoutRequest req = SAMLUtils.buildLogoutRequest(logoutUrl, spId, sessionNameId, sessionIndex);
|
||||
assertEquals(req.getDestination(), logoutUrl);
|
||||
assertEquals(req.getIssuer().getValue(), spId);
|
||||
assertEquals(req.getNameID().getValue(), nameIdString);
|
||||
assertEquals(req.getSessionIndexes().get(0).getSessionIndex(), sessionIndex);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue