CLOUDSTACK-8457: SAML auth plugin improvements for production usage

* Move config options to SAML plugin
  This moves all configuration options from Config.java to SAML auth manager. This
  allows us to use the config framework.
* Make SAML2UserAuthenticator validate SAML token in httprequest
* Make logout API use ConfigKeys defined in saml auth manager
* Before doing SAML auth, cleanup local states and cookies
* Fix configurations in 4.5.1 to 4.5.2 upgrade path
* Fail if idp has no sso URL defined
* Add a default set of SAML SP cert for testing purposes
  Now to enable and use saml, one needs to do a deploydb-saml after doing a deploydb
* UI remembers login selections, IDP server

- CLOUDSTACK-8458:
    * On UI show dropdown list of discovered IdPs
    * Support SAML Federation, where there may be more than one IdP
        - New datastructure to hold metadata of SP or IdP
        - Recursive processing of IdP metadata
        - Fix login/logout APIs to get new interface and metadata data structure
        - Add org/contact information to metadata
        - Add new API: listIdps that returns list of all discovered IdPs
        - Refactor and cleanup code and tests

- CLOUDSTACK-8459:
    * Add HTTP-POST binding to SP metadata
    * Authn requests must use either HTTP POST/Artifact binding

- CLOUDSTACK-8461:
    * Use unspecified x509 cert as a fallback encryption/signing key
      In case a IDP's metadata does not clearly say if their certificates need to be
      used as signing or encryption and we don't find that, fallback to use the
      unspecified key itself.

- CLOUDSTACK-8462:
    * SAML Auth plugin should not do authorization
      This removes logic to create user if they don't exist. This strictly now
      assumes that users have been already created/imported/authorized by admins.
      As per SAML v2.0 spec section 4.1.2, the SP provider should create authn requests using
      either HTTP POST or HTTP Artifact binding to transfer the message through a
      user agent (browser in our case). The use of HTTP Redirect was one of the reasons
      why this plugin failed to work for some IdP servers that enforce this.
    * Add new User Source
      By reusing the source field, we can find if a user has been SAML enabled or not.
      The limitation is that, once say a user is imported by LDAP and then SAML
      enabled - they won't be able to use LDAP for authentication
    * UI should allow users to pass in domain they want to log into, though it is
      optional and needed only when a user has accounts across domains with same
      username and authorized IDP server
    * SAML users need to be authorized before they can authenticate
        - New column entity to track saml entity id for a user
        - Reusing source column to check if user is saml enabled or not
        - Add new source types, saml2 and saml2disabled
        - New table saml_token to solve the issue of multiple users across domains and
          to enforce security by tracking authn token and checking the samlresponse for
          the tokens
        - Implement API: authorizeSamlSso to enable/disable saml authentication for a
          user
        - Stubs to implement saml token flushing/expiry

- CLOUDSTACK-8463:
    * Use username attribute specified in global setting
      Use username attribute defined by admin from a global setting
      In case of encrypted assertion/attributes:
      - Decrypt them
      - Check signature if provided to check authenticity of message using IdP's
        public key and SP's private key
      - Loop through attributes to find the username

- CLOUDSTACK-8538:
    * Add new global config for SAML request sig algorithm

- CLOUDSTACK-8539:
    * Add metadata refresh timer task and token expiring
        - Fix domain path and save it to saml_tokens
        - Expire hour old saml tokens
        - Refresh metadata based on timer task
        - Fix unit tests

This closes #489

(cherry picked from commit 20ce346f3acb794b08a51841bab2188d426bf7dc)
Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>

Conflicts:
	client/WEB-INF/classes/resources/messages_hu.properties
	plugins/hypervisors/xenserver/src/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixCheckHealthCommandWrapper.java
	plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
	ui/scripts/ui-custom/login.js
This commit is contained in:
Rohit Yadav 2015-05-28 14:50:12 +02:00
parent 527d6ee77b
commit 107595a6a5
54 changed files with 2169 additions and 563 deletions

View File

@ -23,7 +23,7 @@ import org.apache.cloudstack.api.InternalIdentity;
public interface User extends OwnedBy, InternalIdentity {
public enum Source {
LDAP, UNKNOWN
LDAP, SAML2, SAML2DISABLED, UNKNOWN
}
public static final long UID_SYSTEM = 1;
@ -84,4 +84,9 @@ public interface User extends OwnedBy, InternalIdentity {
public Source getSource();
void setSource(Source source);
public String getExternalEntity();
public void setExternalEntity(String entity);
}

View File

@ -63,4 +63,8 @@ public interface UserAccount extends InternalIdentity {
int getLoginAttempts();
public User.Source getSource();
public String getExternalEntity();
public void setExternalEntity(String entity);
}

View File

@ -375,6 +375,7 @@ public class ApiConstants {
public static final String ISOLATION_METHODS = "isolationmethods";
public static final String PHYSICAL_NETWORK_ID = "physicalnetworkid";
public static final String DEST_PHYSICAL_NETWORK_ID = "destinationphysicalnetworkid";
public static final String ENABLE = "enable";
public static final String ENABLED = "enabled";
public static final String SERVICE_NAME = "servicename";
public static final String DHCP_RANGE = "dhcprange";
@ -518,7 +519,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 IDP_ID = "idpid";
public static final String SCALEUP_POLICY_IDS = "scaleuppolicyids";
public static final String SCALEDOWN_POLICY_IDS = "scaledownpolicyids";
public static final String SCALEUP_POLICIES = "scaleuppolicies";

View File

@ -115,6 +115,7 @@ label.action.attach.iso=Attach ISO
label.action.cancel.maintenance.mode.processing=Cancelling Maintenance Mode....
label.action.cancel.maintenance.mode=Cancel Maintenance Mode
label.action.change.password=Change Password
label.action.configure.samlauthorization=Configure SAML SSO Authorization
label.action.change.service.processing=Changing Service....
label.action.change.service=Change Service
label.action.copy.ISO.processing=Copying ISO....
@ -763,7 +764,9 @@ label.local.storage=Local Storage
label.local=Local
label.login=Login
label.logout=Logout
label.saml.login=SAML Login
label.saml.enable=Authorize SAML SSO
label.saml.entity=Identity Provider
label.add.LDAP.account=Add LDAP Account
label.LUN.number=LUN \#
label.lun=LUN
label.make.project.owner=Make account project owner

View File

@ -1289,7 +1289,6 @@ label.s3.nfs.server=Serveur NFS S3
label.s3.secret_key=Cl\u00e9 Priv\u00e9e
label.s3.socket_timeout=D\u00e9lai d\\'expiration de la socket
label.s3.use_https=Utiliser HTTPS
label.saml.login=Identifiant SAML
label.saturday=Samedi
label.save.and.continue=Enregistrer et continuer
label.save=Sauvegarder

View File

@ -1282,7 +1282,6 @@ label.s3.nfs.server=S3 NFS kiszolg\u00e1l\u00f3
label.s3.secret_key=Titkos kulcs
label.s3.socket_timeout=Kapcsolat id\u0151t\u00fall\u00e9p\u00e9s
label.s3.use_https=HTTPS haszn\u00e1lata
label.saml.login=SAML bejelentkez\u00e9s
label.saturday=Szombat
label.save.and.continue=Ment\u00e9s \u00e9s folytat\u00e1s
label.save=Ment\u00e9s

View File

@ -26,6 +26,9 @@ logout=15
samlSso=15
samlSlo=15
getSPMetadata=15
listIdps=15
authorizeSamlSso=7
listSamlAuthorization=7
### Account commands
createAccount=7

View File

@ -83,9 +83,4 @@ INSERT INTO `cloud`.`configuration` (category, instance, component, name, value)
VALUES ('Advanced', 'DEFAULT', 'management-server',
'developer', 'true');
-- Enable SAML plugin for developers by default
INSERT INTO `cloud`.`configuration` (category, instance, component, name, value)
VALUES ('Advanced', 'DEFAULT', 'management-server',
'saml2.enabled', 'true');
commit;

File diff suppressed because one or more lines are too long

View File

@ -157,6 +157,64 @@
</plugins>
</build>
</profile>
<profile>
<!-- saml deploydb property -->
<id>deploydb-saml</id>
<activation>
<property>
<name>deploydb-saml</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${cs.mysql.version}</version>
</dependency>
</dependencies>
<version>1.2.1</version>
<executions>
<execution>
<phase>process-resources</phase>
<id>create-schema-simulator</id>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>com.cloud.upgrade.DatabaseCreator</mainClass>
<includePluginDependencies>true</includePluginDependencies>
<arguments>
<!-- db properties file -->
<argument>${basedir}/../utils/conf/db.properties</argument>
<argument>${basedir}/../utils/conf/db.properties.override</argument>
<!-- simulator sql files -->
<argument>${basedir}/developer-saml.sql</argument>
<!-- upgrade -->
<argument>com.cloud.upgrade.DatabaseUpgradeChecker</argument>
<argument>--rootpassword=${db.root.password}</argument>
</arguments>
<systemProperties>
<systemProperty>
<key>catalina.home</key>
<value>${basedir}/../utils</value>
</systemProperty>
<systemProperty>
<key>paths.script</key>
<value>${basedir}/target/db</value>
</systemProperty>
</systemProperties>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<!-- simulator deploydb property -->
<id>deploydb-simulator</id>

View File

@ -105,6 +105,9 @@ public class UserAccountVO implements UserAccount, InternalIdentity {
@Enumerated(value = EnumType.STRING)
private User.Source source;
@Column(name = "external_entity", length = 65535)
private String externalEntity = null;
public UserAccountVO() {
}
@ -296,4 +299,12 @@ public class UserAccountVO implements UserAccount, InternalIdentity {
public void setSource(User.Source source) {
this.source = source;
}
public String getExternalEntity() {
return externalEntity;
}
public void setExternalEntity(String externalEntity) {
this.externalEntity = externalEntity;
}
}

View File

@ -101,6 +101,9 @@ public class UserVO implements User, Identity, InternalIdentity {
@Enumerated(value = EnumType.STRING)
private Source source;
@Column(name = "external_entity", length = 65535)
private String externalEntity;
public UserVO() {
this.uuid = UUID.randomUUID().toString();
}
@ -283,4 +286,11 @@ public class UserVO implements User, Identity, InternalIdentity {
this.source = source;
}
public String getExternalEntity() {
return externalEntity;
}
public void setExternalEntity(String externalEntity) {
this.externalEntity = externalEntity;
}
}

View File

@ -20,7 +20,11 @@ import com.cloud.user.UserAccount;
import com.cloud.user.UserAccountVO;
import com.cloud.utils.db.GenericDao;
import java.util.List;
public interface UserAccountDao extends GenericDao<UserAccountVO, Long> {
List<UserAccountVO> getAllUsersByNameAndEntity(String username, String entity);
UserAccount getUserAccount(String username, Long domainId);
boolean validateUsernameInDomain(String username, Long domainId);

View File

@ -16,15 +16,15 @@
// under the License.
package com.cloud.user.dao;
import javax.ejb.Local;
import org.springframework.stereotype.Component;
import com.cloud.user.UserAccount;
import com.cloud.user.UserAccountVO;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import org.springframework.stereotype.Component;
import javax.ejb.Local;
import java.util.List;
@Component
@Local(value = {UserAccountDao.class})
@ -38,6 +38,17 @@ public class UserAccountDaoImpl extends GenericDaoBase<UserAccountVO, Long> impl
userAccountSearch.done();
}
@Override
public List<UserAccountVO> getAllUsersByNameAndEntity(String username, String entity) {
if (username == null) {
return null;
}
SearchCriteria<UserAccountVO> sc = createSearchCriteria();
sc.addAnd("username", SearchCriteria.Op.EQ, username);
sc.addAnd("externalEntity", SearchCriteria.Op.EQ, entity);
return listBy(sc);
}
@Override
public UserAccount getUserAccount(String username, Long domainId) {
if ((username == null) || (domainId == null)) {

View File

@ -47,5 +47,10 @@
<artifactId>cloud-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-framework-config</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -33,4 +33,7 @@
<property name="name" value="SAML2Auth"/>
</bean>
<bean id="samlTokenDao" class="org.apache.cloudstack.saml.SAMLTokenDaoImpl">
</bean>
</beans>

View File

@ -0,0 +1,105 @@
// 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.domain.Domain;
import com.cloud.user.Account;
import com.cloud.user.UserAccount;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.api.APICommand;
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.ServerApiException;
import org.apache.cloudstack.api.response.IdpResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.api.response.UserResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.saml.SAML2AuthManager;
import org.apache.log4j.Logger;
import javax.inject.Inject;
@APICommand(name = "authorizeSamlSso", description = "Allow or disallow a user to use SAML SSO", responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
public class AuthorizeSAMLSSOCmd extends BaseCmd {
public static final Logger s_logger = Logger.getLogger(AuthorizeSAMLSSOCmd.class.getName());
private static final String s_name = "authorizesamlssoresponse";
@Inject
SAML2AuthManager _samlAuthManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.USER_ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "User uuid")
private Long id;
@Parameter(name = ApiConstants.ENABLE, type = CommandType.BOOLEAN, required = true, description = "If true, authorizes user to be able to use SAML for Single Sign. If False, disable user to user SAML SSO.")
private Boolean enable;
public Boolean getEnable() {
return enable;
}
public String getEntityId() {
return entityId;
}
@Parameter(name = ApiConstants.ENTITY_ID, type = CommandType.STRING, entityType = IdpResponse.class, description = "The Identity Provider ID the user is allowed to get single signed on from")
private String entityId;
public Long getId() {
return id;
}
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public void execute() {
// Check permissions
UserAccount userAccount = _accountService.getUserAccountById(getId());
if (userAccount == null) {
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR , "Unable to find a user account with the given ID");
}
Domain domain = _domainService.getDomain(userAccount.getDomainId());
Account account = _accountService.getAccount(userAccount.getAccountId());
_accountService.checkAccess(CallContext.current().getCallingAccount(), domain);
_accountService.checkAccess(CallContext.current().getCallingAccount(), SecurityChecker.AccessType.OperateEntry, true, account);
CallContext.current().setEventDetails("UserId: " + getId());
SuccessResponse response = new SuccessResponse();
Boolean status = false;
if (_samlAuthManager.authorizeUser(getId(), getEntityId(), getEnable())) {
status = true;
}
response.setResponseName(getCommandName());
response.setSuccess(status);
setResponseObject(response);
}
}

View File

@ -14,7 +14,6 @@
// 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;
@ -30,21 +29,36 @@ 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.cloudstack.saml.SAMLProviderMetadata;
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.ContactPerson;
import org.opensaml.saml2.metadata.ContactPersonTypeEnumeration;
import org.opensaml.saml2.metadata.EmailAddress;
import org.opensaml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml2.metadata.GivenName;
import org.opensaml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml2.metadata.LocalizedString;
import org.opensaml.saml2.metadata.NameIDFormat;
import org.opensaml.saml2.metadata.Organization;
import org.opensaml.saml2.metadata.OrganizationName;
import org.opensaml.saml2.metadata.OrganizationURL;
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.ContactPersonBuilder;
import org.opensaml.saml2.metadata.impl.EmailAddressBuilder;
import org.opensaml.saml2.metadata.impl.EntityDescriptorBuilder;
import org.opensaml.saml2.metadata.impl.GivenNameBuilder;
import org.opensaml.saml2.metadata.impl.KeyDescriptorBuilder;
import org.opensaml.saml2.metadata.impl.NameIDFormatBuilder;
import org.opensaml.saml2.metadata.impl.OrganizationBuilder;
import org.opensaml.saml2.metadata.impl.OrganizationNameBuilder;
import org.opensaml.saml2.metadata.impl.OrganizationURLBuilder;
import org.opensaml.saml2.metadata.impl.SPSSODescriptorBuilder;
import org.opensaml.saml2.metadata.impl.SingleLogoutServiceBuilder;
import org.opensaml.xml.ConfigurationException;
@ -73,6 +87,7 @@ import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.net.InetAddress;
@ -119,8 +134,10 @@ public class GetServiceProviderMetaDataCmd extends BaseCmd implements APIAuthent
params, responseType));
}
final SAMLProviderMetadata spMetadata = _samlAuthManager.getSPMetadata();
EntityDescriptor spEntityDescriptor = new EntityDescriptorBuilder().buildObject();
spEntityDescriptor.setEntityID(_samlAuthManager.getServiceProviderId());
spEntityDescriptor.setEntityID(spMetadata.getEntityId());
SPSSODescriptor spSSODescriptor = new SPSSODescriptorBuilder().buildObject();
spSSODescriptor.setWantAssertionsSigned(true);
@ -130,19 +147,23 @@ public class GetServiceProviderMetaDataCmd extends BaseCmd implements APIAuthent
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.getSpX509Certificate());
KeyDescriptor encKeyDescriptor = new KeyDescriptorBuilder().buildObject();
encKeyDescriptor.setUse(UsageType.ENCRYPTION);
BasicX509Credential signingCredential = new BasicX509Credential();
signingCredential.setEntityCertificate(spMetadata.getSigningCertificate());
BasicX509Credential encryptionCredential = new BasicX509Credential();
encryptionCredential.setEntityCertificate(spMetadata.getEncryptionCertificate());
try {
encKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(credential));
signKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(credential));
spSSODescriptor.getKeyDescriptors().add(encKeyDescriptor);
signKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(signingCredential));
encKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(encryptionCredential));
spSSODescriptor.getKeyDescriptors().add(signKeyDescriptor);
spSSODescriptor.getKeyDescriptors().add(encKeyDescriptor);
} catch (SecurityException e) {
s_logger.warn("Unable to add SP X509 descriptors:" + e.getMessage());
}
@ -160,19 +181,50 @@ public class GetServiceProviderMetaDataCmd extends BaseCmd implements APIAuthent
spSSODescriptor.getNameIDFormats().add(transientNameIDFormat);
AssertionConsumerService assertionConsumerService = new AssertionConsumerServiceBuilder().buildObject();
assertionConsumerService.setIndex(0);
assertionConsumerService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
assertionConsumerService.setLocation(_samlAuthManager.getSpSingleSignOnUrl());
assertionConsumerService.setIndex(1);
assertionConsumerService.setIsDefault(true);
assertionConsumerService.setBinding(SAMLConstants.SAML2_POST_BINDING_URI);
assertionConsumerService.setLocation(spMetadata.getSsoUrl());
spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService);
AssertionConsumerService assertionConsumerService2 = new AssertionConsumerServiceBuilder().buildObject();
assertionConsumerService2.setIndex(2);
assertionConsumerService2.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
assertionConsumerService2.setLocation(spMetadata.getSsoUrl());
spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService2);
SingleLogoutService ssoService = new SingleLogoutServiceBuilder().buildObject();
ssoService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
ssoService.setLocation(_samlAuthManager.getSpSingleLogOutUrl());
ssoService.setLocation(spMetadata.getSloUrl());
spSSODescriptor.getSingleLogoutServices().add(ssoService);
spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService);
SingleLogoutService ssoService2 = new SingleLogoutServiceBuilder().buildObject();
ssoService2.setBinding(SAMLConstants.SAML2_POST_BINDING_URI);
ssoService2.setLocation(spMetadata.getSloUrl());
spSSODescriptor.getSingleLogoutServices().add(ssoService2);
spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor);
ContactPerson contactPerson = new ContactPersonBuilder().buildObject();
GivenName givenName = new GivenNameBuilder().buildObject();
givenName.setName(spMetadata.getContactPersonName());
EmailAddress emailAddress = new EmailAddressBuilder().buildObject();
emailAddress.setAddress(spMetadata.getContactPersonEmail());
contactPerson.setType(ContactPersonTypeEnumeration.TECHNICAL);
contactPerson.setGivenName(givenName);
contactPerson.getEmailAddresses().add(emailAddress);
spEntityDescriptor.getContactPersons().add(contactPerson);
Organization organization = new OrganizationBuilder().buildObject();
OrganizationName organizationName = new OrganizationNameBuilder().buildObject();
organizationName.setName(new LocalizedString(spMetadata.getOrganizationName(), Locale.getDefault().getLanguage()));
OrganizationURL organizationURL = new OrganizationURLBuilder().buildObject();
organizationURL.setURL(new LocalizedString(spMetadata.getOrganizationUrl(), Locale.getDefault().getLanguage()));
organization.getOrganizationNames().add(organizationName);
organization.getURLs().add(organizationURL);
spEntityDescriptor.setOrganization(organization);
StringWriter stringWriter = new StringWriter();
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

View File

@ -0,0 +1,114 @@
// 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.IdpResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.saml.SAML2AuthManager;
import org.apache.cloudstack.saml.SAMLProviderMetadata;
import org.apache.log4j.Logger;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@APICommand(name = "listIdps", description = "Returns list of discovered SAML Identity Providers", responseObject = IdpResponse.class, entityType = {})
public class ListIdpsCmd extends BaseCmd implements APIAuthenticator {
public static final Logger s_logger = Logger.getLogger(ListIdpsCmd.class.getName());
private static final String s_name = "listidpsresponse";
@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 {
// 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 HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException {
auditTrailSb.append("=== SAML List IdPs ===");
ListResponse<IdpResponse> response = new ListResponse<IdpResponse>();
List<IdpResponse> idpResponseList = new ArrayList<IdpResponse>();
for (SAMLProviderMetadata metadata: _samlAuthManager.getAllIdPMetadata()) {
if (metadata == null) {
continue;
}
IdpResponse idpResponse = new IdpResponse();
idpResponse.setId(metadata.getEntityId());
if (metadata.getOrganizationName() == null || metadata.getOrganizationName().isEmpty()) {
idpResponse.setOrgName(metadata.getEntityId());
} else {
idpResponse.setOrgName(metadata.getOrganizationName());
}
idpResponse.setOrgUrl(metadata.getOrganizationUrl());
idpResponse.setObjectName("idp");
idpResponseList.add(idpResponse);
}
response.setResponses(idpResponseList, idpResponseList.size());
response.setResponseName(getCommandName());
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 != null && authManager instanceof SAML2AuthManager) {
_samlAuthManager = (SAML2AuthManager) authManager;
}
}
if (_samlAuthManager == null) {
s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd");
}
}
}

View File

@ -0,0 +1,95 @@
// 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.user.Account;
import com.cloud.user.User;
import com.cloud.user.UserVO;
import com.cloud.user.dao.UserDao;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.SamlAuthorizationResponse;
import org.apache.cloudstack.api.response.UserResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.log4j.Logger;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
@APICommand(name = "listSamlAuthorization", description = "Lists authorized users who can used SAML SSO", responseObject = SamlAuthorizationResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
public class ListSamlAuthorizationCmd extends BaseListCmd {
public static final Logger s_logger = Logger.getLogger(ListSamlAuthorizationCmd.class.getName());
private static final String s_name = "listsamlauthorizationsresponse";
@Inject
private UserDao _userDao;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.USER_ID, type = CommandType.UUID, entityType = UserResponse.class, required = false, description = "User uuid")
private Long userId;
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
public Long getUserId() {
return userId;
}
@Override
public String getCommandName() {
return s_name;
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public void execute() {
List<UserVO> users = new ArrayList<UserVO>();
if (getUserId() != null) {
UserVO user = _userDao.getUser(getUserId());
if (user != null) {
Account account = _accountService.getAccount(user.getAccountId());
_accountService.checkAccess(CallContext.current().getCallingAccount(), SecurityChecker.AccessType.ListEntry, true, account);
users.add(user);
}
} else if (CallContext.current().getCallingAccount().getType() == Account.ACCOUNT_TYPE_ADMIN) {
users = _userDao.listAll();
}
ListResponse<SamlAuthorizationResponse> response = new ListResponse<SamlAuthorizationResponse>();
List<SamlAuthorizationResponse> authorizationResponses = new ArrayList<SamlAuthorizationResponse>();
for (User user: users) {
SamlAuthorizationResponse authorizationResponse = new SamlAuthorizationResponse(user.getUuid(), user.getSource().equals(User.Source.SAML2), user.getExternalEntity());
authorizationResponse.setObjectName("samlauthorization");
authorizationResponses.add(authorizationResponse);
}
response.setResponses(authorizationResponses);
response.setResponseName(getCommandName());
setResponseObject(response);
}
}

View File

@ -14,16 +14,14 @@
// 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.UserAccount;
import com.cloud.user.UserAccountVO;
import com.cloud.user.dao.UserAccountDao;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.db.EntityManager;
@ -38,24 +36,29 @@ 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.cloudstack.saml.SAMLPluginConstants;
import org.apache.cloudstack.saml.SAMLProviderMetadata;
import org.apache.cloudstack.saml.SAMLTokenVO;
import org.apache.cloudstack.saml.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.EncryptedAssertion;
import org.opensaml.saml2.core.Issuer;
import org.opensaml.saml2.core.Response;
import org.opensaml.saml2.core.StatusCode;
import org.opensaml.saml2.encryption.Decrypter;
import org.opensaml.xml.ConfigurationException;
import org.opensaml.xml.io.MarshallingException;
import org.opensaml.xml.encryption.DecryptionException;
import org.opensaml.xml.encryption.EncryptedKeyResolver;
import org.opensaml.xml.encryption.InlineEncryptedKeyResolver;
import org.opensaml.xml.io.UnmarshallingException;
import org.opensaml.xml.security.SecurityHelper;
import org.opensaml.xml.security.credential.Credential;
import org.opensaml.xml.security.keyinfo.StaticKeyInfoCredentialResolver;
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;
@ -68,14 +71,11 @@ 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.net.InetAddress;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@APICommand(name = "samlSso", description = "SP initiated SAML Single Sign On", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {})
public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator {
@ -85,16 +85,14 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.IDP_URL, type = CommandType.STRING, description = "Identity Provider SSO HTTP-Redirect binding URL", required = true)
private String idpUrl;
@Parameter(name = ApiConstants.IDP_ID, type = CommandType.STRING, description = "Identity Provider Entity ID", required = true)
private String idpId;
@Inject
ApiServerService _apiServer;
@Inject
EntityManager _entityMgr;
@Inject
ConfigurationDao _configDao;
@Inject
DomainManager _domainMgr;
@Inject
private UserAccountDao _userAccountDao;
@ -105,8 +103,8 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public String getIdpUrl() {
return idpUrl;
public String getIdpId() {
return idpId;
}
/////////////////////////////////////////////////////
@ -129,30 +127,6 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
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);
PrivateKey privateKey = null;
if (_samlAuthManager.getSpKeyPair() != null) {
privateKey = _samlAuthManager.getSpKeyPair().getPrivate();
}
redirectUrl = identityProviderUrl + "?" + SAMLUtils.generateSAMLRequestSignature("SAMLRequest=" + SAMLUtils.encodeSAMLRequest(authnRequest), privateKey);
} catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException | NoSuchAlgorithmException | InvalidKeyException | java.security.SignatureException e) {
s_logger.error("SAML AuthnRequest message building error: " + e.getMessage());
}
return redirectUrl;
}
public Response processSAMLResponse(String responseMessage) {
Response responseObject = null;
try {
@ -168,13 +142,44 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
@Override
public String authenticate(final String command, final Map<String, Object[]> params, final HttpSession session, final InetAddress remoteAddress, final String responseType, final StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException {
try {
if (!params.containsKey("SAMLResponse") && !params.containsKey("SAMLart")) {
String idpUrl = null;
final String[] idps = (String[])params.get(ApiConstants.IDP_URL);
if (idps != null && idps.length > 0) {
idpUrl = idps[0];
if (!params.containsKey(SAMLPluginConstants.SAML_RESPONSE) && !params.containsKey("SAMLart")) {
String idpId = null;
String domainPath = null;
if (params.containsKey(ApiConstants.IDP_ID)) {
idpId = ((String[])params.get(ApiConstants.IDP_ID))[0];
}
String redirectUrl = this.buildAuthnRequestUrl(idpUrl);
if (params.containsKey(ApiConstants.DOMAIN)) {
domainPath = ((String[])params.get(ApiConstants.DOMAIN))[0];
}
if (domainPath != null && !domainPath.isEmpty()) {
if (!domainPath.startsWith("/")) {
domainPath = "/" + domainPath;
}
if (!domainPath.endsWith("/")) {
domainPath = domainPath + "/";
}
}
SAMLProviderMetadata spMetadata = _samlAuthManager.getSPMetadata();
SAMLProviderMetadata idpMetadata = _samlAuthManager.getIdPMetadata(idpId);
if (idpMetadata == null) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.PARAM_ERROR.getHttpCode(),
"IdP ID (" + idpId + ") is not found in our list of supported IdPs, cannot proceed.",
params, responseType));
}
if (idpMetadata.getSsoUrl() == null || idpMetadata.getSsoUrl().isEmpty()) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.PARAM_ERROR.getHttpCode(),
"IdP ID (" + idpId + ") has no Single Sign On URL defined please contact "
+ idpMetadata.getContactPersonName() + " <" + idpMetadata.getContactPersonEmail() + ">, cannot proceed.",
params, responseType));
}
String authnId = SAMLUtils.generateSecureRandomId();
_samlAuthManager.saveToken(authnId, domainPath, idpMetadata.getEntityId());
s_logger.debug("Sending SAMLRequest id=" + authnId);
String redirectUrl = SAMLUtils.buildAuthnRequestUrl(authnId, spMetadata, idpMetadata, SAML2AuthManager.SAMLSignatureAlgorithm.value());
resp.sendRedirect(redirectUrl);
return "";
} if (params.containsKey("SAMLart")) {
@ -182,7 +187,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
"SAML2 HTTP Artifact Binding is not supported",
params, responseType));
} else {
final String samlResponse = ((String[])params.get(SAMLUtils.SAML_RESPONSE))[0];
final String samlResponse = ((String[])params.get(SAMLPluginConstants.SAML_RESPONSE))[0];
Response processedSAMLResponse = this.processSAMLResponse(samlResponse);
String statusCode = processedSAMLResponse.getStatus().getStatusCode().getValue();
if (!statusCode.equals(StatusCode.SUCCESS_URI)) {
@ -191,10 +196,37 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
params, responseType));
}
if (_samlAuthManager.getIdpSigningKey() != null) {
org.opensaml.xml.signature.Signature sig = processedSAMLResponse.getSignature();
String username = null;
Long domainId = null;
Issuer issuer = processedSAMLResponse.getIssuer();
SAMLProviderMetadata spMetadata = _samlAuthManager.getSPMetadata();
SAMLProviderMetadata idpMetadata = _samlAuthManager.getIdPMetadata(issuer.getValue());
String responseToId = processedSAMLResponse.getInResponseTo();
s_logger.debug("Received SAMLResponse in response to id=" + responseToId);
SAMLTokenVO token = _samlAuthManager.getToken(responseToId);
if (token != null) {
if (token.getDomainId() != null) {
domainId = token.getDomainId();
}
if (!(token.getEntity().equalsIgnoreCase(issuer.getValue()))) {
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
"The SAML response contains Issuer Entity ID that is different from the original SAML request",
params, responseType));
}
} else {
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
"Received SAML response for a SSO request that we may not have made or has expired, please try logging in again",
params, responseType));
}
// Set IdpId for this session
session.setAttribute(SAMLPluginConstants.SAML_IDPID, issuer.getValue());
Signature sig = processedSAMLResponse.getSignature();
if (idpMetadata.getSigningCertificate() != null && sig != null) {
BasicX509Credential credential = new BasicX509Credential();
credential.setEntityCertificate(_samlAuthManager.getIdpSigningKey());
credential.setEntityCertificate(idpMetadata.getSigningCertificate());
SignatureValidator validator = new SignatureValidator(credential);
try {
validator.validate(sig);
@ -205,94 +237,106 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
params, responseType));
}
}
String domainString = _configDao.getValue(Config.SAMLUserDomain.key());
Long domainId = null;
Domain domain = _domainMgr.getDomain(domainString);
if (domain != null) {
domainId = domain.getId();
} else {
try {
domainId = Long.parseLong(domainString);
} catch (NumberFormatException ignore) {
}
}
if (domainId == null) {
s_logger.error("The default domain ID for SAML users is not set correct, it should be a UUID. ROOT domain will be used.");
if (username == null) {
username = SAMLUtils.getValueFromAssertions(processedSAMLResponse.getAssertions(), SAML2AuthManager.SAMLUserAttributeName.value());
}
String username = null;
String password = SAMLUtils.generateSecureRandomId(); // Random password
String firstName = "";
String lastName = "";
String timeZone = "GMT";
String email = "";
short accountType = 0; // User account
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();
if (nameId.getFormat().equals(NameIDType.EMAIL)) {
email = username;
for (Assertion assertion: processedSAMLResponse.getAssertions()) {
if (assertion!= null && assertion.getSubject() != null && assertion.getSubject().getNameID() != null) {
session.setAttribute(SAMLPluginConstants.SAML_NAMEID, assertion.getSubject().getNameID().getValue());
break;
}
}
List<AttributeStatement> attributeStatements = assertion.getAttributeStatements();
if (attributeStatements != null && attributeStatements.size() > 0) {
for (AttributeStatement attributeStatement: attributeStatements) {
if (attributeStatement == null) {
continue;
}
// Try capturing standard LDAP attributes
for (Attribute attribute: attributeStatement.getAttributes()) {
String attributeName = attribute.getName();
String attributeValue = attribute.getAttributeValues().get(0).getDOM().getTextContent();
if (attributeName.equalsIgnoreCase("uid") && username == null) {
username = attributeValue;
} else if (attributeName.equalsIgnoreCase("givenName")) {
firstName = attributeValue;
} else if (attributeName.equalsIgnoreCase(("sn"))) {
lastName = attributeValue;
} else if (attributeName.equalsIgnoreCase("mail")) {
email = attributeValue;
if (idpMetadata.getEncryptionCertificate() != null && spMetadata != null
&& spMetadata.getKeyPair() != null && spMetadata.getKeyPair().getPrivate() != null) {
Credential credential = SecurityHelper.getSimpleCredential(idpMetadata.getEncryptionCertificate().getPublicKey(),
spMetadata.getKeyPair().getPrivate());
StaticKeyInfoCredentialResolver keyInfoResolver = new StaticKeyInfoCredentialResolver(credential);
EncryptedKeyResolver keyResolver = new InlineEncryptedKeyResolver();
Decrypter decrypter = new Decrypter(null, keyInfoResolver, keyResolver);
decrypter.setRootInNewDocument(true);
List<EncryptedAssertion> encryptedAssertions = processedSAMLResponse.getEncryptedAssertions();
if (encryptedAssertions != null) {
for (EncryptedAssertion encryptedAssertion : encryptedAssertions) {
Assertion assertion = null;
try {
assertion = decrypter.decrypt(encryptedAssertion);
} catch (DecryptionException e) {
s_logger.warn("SAML EncryptedAssertion error: " + e.toString());
}
if (assertion == null) {
continue;
}
Signature encSig = assertion.getSignature();
if (idpMetadata.getSigningCertificate() != null && encSig != null) {
BasicX509Credential sigCredential = new BasicX509Credential();
sigCredential.setEntityCertificate(idpMetadata.getSigningCertificate());
SignatureValidator validator = new SignatureValidator(sigCredential);
try {
validator.validate(encSig);
} 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));
}
}
if (assertion.getSubject() != null && assertion.getSubject().getNameID() != null) {
session.setAttribute(SAMLPluginConstants.SAML_NAMEID, assertion.getSubject().getNameID().getValue());
}
if (username == null) {
username = SAMLUtils.getValueFromAttributeStatements(assertion.getAttributeStatements(), SAML2AuthManager.SAMLUserAttributeName.value());
}
}
}
}
if (username == null && email != null) {
username = email;
if (username == null) {
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
"Failed to find admin configured username attribute in the SAML Response. Please ask your administrator to check SAML user attribute name.", params, responseType));
}
final String uniqueUserId = SAMLUtils.createSAMLId(username);
UserAccount userAccount = _userAccountDao.getUserAccount(username, domainId);
if (userAccount == null && uniqueUserId != null && username != null) {
CallContext.current().setEventDetails("SAML Account/User with UserName: " + username + ", FirstName :" + password + ", LastName: " + lastName);
userAccount = _accountService.createUserAccount(username, password, firstName, lastName, email, timeZone,
username, (short) accountType, domainId, null, null, UUID.randomUUID().toString(), uniqueUserId);
UserAccount userAccount = null;
List<UserAccountVO> possibleUserAccounts = _userAccountDao.getAllUsersByNameAndEntity(username, issuer.getValue());
if (possibleUserAccounts != null && possibleUserAccounts.size() > 0) {
if (possibleUserAccounts.size() == 1) {
userAccount = possibleUserAccounts.get(0);
} else if (possibleUserAccounts.size() > 1) {
if (domainId != null) {
userAccount = _userAccountDao.getUserAccount(username, domainId);
} else {
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
"You have accounts in multiple domains, please re-login by specifying the domain you want to log into.",
params, responseType));
}
}
}
if (userAccount == null || userAccount.getExternalEntity() == null || !_samlAuthManager.isUserAuthorized(userAccount.getId(), issuer.getValue())) {
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
"Your authenticated user is not authorized, please contact your administrator",
params, responseType));
}
if (userAccount != null) {
try {
if (_apiServer.verifyUser(userAccount.getId())) {
LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, username, userAccount.getPassword(), domainId, null, remoteAddress, params);
LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, userAccount.getUsername(), userAccount.getUsername() + userAccount.getSource().toString(),
userAccount.getDomainId(), null, remoteAddress, params);
resp.addCookie(new Cookie("userid", URLEncoder.encode(loginResponse.getUserId(), HttpUtils.UTF_8)));
resp.addCookie(new Cookie("domainid", URLEncoder.encode(loginResponse.getDomainId(), HttpUtils.UTF_8)));
resp.addCookie(new Cookie("role", URLEncoder.encode(loginResponse.getType(), HttpUtils.UTF_8)));
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(ApiConstants.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)));
String timezone = loginResponse.getTimeZone();
if (timezone != null) {
resp.addCookie(new Cookie("timezone", URLEncoder.encode(timezone, HttpUtils.UTF_8)));
}
resp.addCookie(new Cookie("userfullname", URLEncoder.encode(loginResponse.getFirstName() + " " + loginResponse.getLastName(), HttpUtils.UTF_8).replace("+", "%20")));
resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
return ApiResponseSerializer.toSerializedString(loginResponse, responseType);
}
} catch (final CloudAuthenticationException ignored) {
}
@ -303,7 +347,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
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",
"Unable to authenticate user while performing SAML based SSO. Please make sure your user/account has been added, enable and authorized by the admin before you can authenticate. Please contact your administrator.",
params, responseType));
}

View File

@ -17,7 +17,6 @@
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;
@ -28,13 +27,13 @@ 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.cloudstack.saml.SAMLPluginConstants;
import org.apache.cloudstack.saml.SAMLProviderMetadata;
import org.apache.cloudstack.saml.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;
@ -60,8 +59,7 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen
@Inject
ApiServerService _apiServer;
@Inject
ConfigurationDao _configDao;
SAML2AuthManager _samlAuthManager;
/////////////////////////////////////////////////////
@ -94,7 +92,7 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen
if (session == null) {
try {
resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
} catch (IOException ignored) {
}
return responseString;
@ -111,7 +109,7 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen
if (params != null && params.containsKey("SAMLResponse")) {
try {
final String samlResponse = ((String[])params.get(SAMLUtils.SAML_RESPONSE))[0];
final String samlResponse = ((String[])params.get(SAMLPluginConstants.SAML_RESPONSE))[0];
Response processedSAMLResponse = SAMLUtils.decodeSAMLResponse(samlResponse);
String statusCode = processedSAMLResponse.getStatus().getStatusCode().getValue();
if (!statusCode.equals(StatusCode.SUCCESS_URI)) {
@ -123,25 +121,26 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen
s_logger.error("SAMLResponse processing error: " + e.getMessage());
}
try {
resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
} 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) {
String idpId = (String) session.getAttribute(SAMLPluginConstants.SAML_IDPID);
SAMLProviderMetadata idpMetadata = _samlAuthManager.getIdPMetadata(idpId);
String nameId = (String) session.getAttribute(SAMLPluginConstants.SAML_NAMEID);
if (idpMetadata == null || nameId == null || nameId.isEmpty()) {
try {
resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key()));
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
} catch (IOException ignored) {
}
return responseString;
}
LogoutRequest logoutRequest = SAMLUtils.buildLogoutRequest(_samlAuthManager.getIdpSingleLogOutUrl(), _samlAuthManager.getServiceProviderId(), nameId, sessionIndex);
LogoutRequest logoutRequest = SAMLUtils.buildLogoutRequest(idpMetadata.getSloUrl(), _samlAuthManager.getSPMetadata().getEntityId(), nameId);
try {
String redirectUrl = _samlAuthManager.getIdpSingleLogOutUrl() + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(logoutRequest);
String redirectUrl = idpMetadata.getSloUrl() + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(logoutRequest);
resp.sendRedirect(redirectUrl);
} catch (MarshallingException | IOException e) {
s_logger.error("SAML SLO error: " + e.getMessage());

View File

@ -0,0 +1,62 @@
// 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;
public class IdpResponse extends AuthenticationCmdResponse {
@SerializedName("id")
@Param(description = "The IdP Entity ID")
private String id;
@SerializedName("orgName")
@Param(description = "The IdP Organization Name")
private String orgName;
@SerializedName("orgUrl")
@Param(description = "The IdP Organization URL")
private String orgUrl;
public IdpResponse() {
super();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getOrgName() {
return orgName;
}
public void setOrgName(String orgName) {
this.orgName = orgName;
}
public String getOrgUrl() {
return orgUrl;
}
public void setOrgUrl(String orgUrl) {
this.orgUrl = orgUrl;
}
}

View File

@ -0,0 +1,68 @@
// 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.cloud.user.User;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.api.EntityReference;
@EntityReference(value = User.class)
public class SamlAuthorizationResponse extends BaseResponse {
@SerializedName("userid")
@Param(description = "the user ID")
private String userId;
@SerializedName("status")
@Param(description = "the SAML authorization status")
private Boolean status;
@SerializedName("idpid")
@Param(description = "the authorized Identity Provider ID")
private String idpId;
public SamlAuthorizationResponse(String userId, Boolean status, String idpId) {
this.userId = userId;
this.status = status;
this.idpId = idpId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public Boolean getStatus() {
return status;
}
public void setStatus(Boolean status) {
this.status = status;
}
public String getIdpId() {
return idpId;
}
public void setIdpId(String idpId) {
this.idpId = idpId;
}
}

View File

@ -17,23 +17,64 @@
package org.apache.cloudstack.saml;
import com.cloud.utils.component.PluggableService;
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
import org.apache.cloudstack.framework.config.ConfigKey;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.Collection;
public interface SAML2AuthManager extends PluggableAPIAuthenticator {
public String getServiceProviderId();
public String getIdentityProviderId();
public interface SAML2AuthManager extends PluggableAPIAuthenticator, PluggableService {
public X509Certificate getIdpSigningKey();
public X509Certificate getIdpEncryptionKey();
public X509Certificate getSpX509Certificate();
public KeyPair getSpKeyPair();
public static final ConfigKey<Boolean> SAMLIsPluginEnabled = new ConfigKey<Boolean>("Advanced", Boolean.class, "saml2.enabled", "false",
"Indicates whether SAML SSO plugin is enabled or not", true);
public String getSpSingleSignOnUrl();
public String getIdpSingleSignOnUrl();
public static final ConfigKey<String> SAMLServiceProviderID = new ConfigKey<String>("Advanced", String.class, "saml2.sp.id", "org.apache.cloudstack",
"SAML2 Service Provider Identifier String", true);
public String getSpSingleLogOutUrl();
public String getIdpSingleLogOutUrl();
public static final ConfigKey<String> SAMLServiceProviderContactPersonName = new ConfigKey<String>("Advanced", String.class, "saml2.sp.contact.person", "CloudStack Developers",
"SAML2 Service Provider Contact Person Name", true);
public static final ConfigKey<String> SAMLServiceProviderContactEmail = new ConfigKey<String>("Advanced", String.class, "saml2.sp.contact.email", "dev@cloudstack.apache.org",
"SAML2 Service Provider Contact Email Address", true);
public static final ConfigKey<String> SAMLServiceProviderOrgName = new ConfigKey<String>("Advanced", String.class, "saml2.sp.org.name", "Apache CloudStack",
"SAML2 Service Provider Organization Name", true);
public static final ConfigKey<String> SAMLServiceProviderOrgUrl = new ConfigKey<String>("Advanced", String.class, "saml2.sp.org.url", "http://cloudstack.apache.org",
"SAML2 Service Provider Organization URL", true);
public static final ConfigKey<String> SAMLServiceProviderSingleSignOnURL = new ConfigKey<String>("Advanced", String.class, "saml2.sp.sso.url", "http://localhost:8080/client/api?command=samlSso",
"SAML2 CloudStack Service Provider Single Sign On URL", true);
public static final ConfigKey<String> SAMLServiceProviderSingleLogOutURL = new ConfigKey<String>("Advanced", String.class, "saml2.sp.slo.url", "http://localhost:8080/client/",
"SAML2 CloudStack Service Provider Single Log Out URL", true);
public static final ConfigKey<String> SAMLCloudStackRedirectionUrl = new ConfigKey<String>("Advanced", String.class, "saml2.redirect.url", "http://localhost:8080/client",
"The CloudStack UI url the SSO should redirected to when successful", true);
public static final ConfigKey<String> SAMLUserAttributeName = new ConfigKey<String>("Advanced", String.class, "saml2.user.attribute", "uid",
"Attribute name to be looked for in SAML response that will contain the username", true);
public static final ConfigKey<String> SAMLIdentityProviderMetadataURL = new ConfigKey<String>("Advanced", String.class, "saml2.idp.metadata.url", "https://openidp.feide.no/simplesaml/saml2/idp/metadata.php",
"SAML2 Identity Provider Metadata XML Url", true);
public static final ConfigKey<String> SAMLDefaultIdentityProviderId = new ConfigKey<String>("Advanced", String.class, "saml2.default.idpid", "https://openidp.feide.no",
"The default IdP entity ID to use only in case of multiple IdPs", true);
public static final ConfigKey<String> SAMLSignatureAlgorithm = new ConfigKey<String>("Advanced", String.class, "saml2.sigalg", "SHA1",
"The algorithm to use to when signing a SAML request. Default is SHA1, allowed algorithms: SHA1, SHA256, SHA384, SHA512", true);
public static final ConfigKey<Integer> SAMLTimeout = new ConfigKey<Integer>("Advanced", Integer.class, "saml2.timeout", "1800",
"SAML2 IDP Metadata refresh interval in seconds, minimum value is set to 300", true);
public SAMLProviderMetadata getSPMetadata();
public SAMLProviderMetadata getIdPMetadata(String entityId);
public Collection<SAMLProviderMetadata> getAllIdPMetadata();
public boolean isUserAuthorized(Long userId, String entityId);
public boolean authorizeUser(Long userId, String entityId, boolean enable);
public void saveToken(String authnId, String domain, String entity);
public SAMLTokenVO getToken(String authnId);
public void expireTokens();
}

View File

@ -16,28 +16,46 @@
// under the License.
package org.apache.cloudstack.saml;
import com.cloud.configuration.Config;
import com.cloud.domain.Domain;
import com.cloud.user.DomainManager;
import com.cloud.user.User;
import com.cloud.user.UserVO;
import com.cloud.user.dao.UserDao;
import com.cloud.utils.PropertiesUtil;
import com.cloud.utils.component.AdapterBase;
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
import org.apache.cloudstack.api.command.AuthorizeSAMLSSOCmd;
import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd;
import org.apache.cloudstack.api.command.ListIdpsCmd;
import org.apache.cloudstack.api.command.ListSamlAuthorizationCmd;
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.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.security.keystore.KeystoreDao;
import org.apache.cloudstack.framework.security.keystore.KeystoreVO;
import org.apache.cloudstack.utils.auth.SAMLUtils;
import org.apache.log4j.Logger;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.HttpClient;
import org.apache.log4j.Logger;
import org.opensaml.DefaultBootstrap;
import org.opensaml.common.xml.SAMLConstants;
import org.opensaml.saml2.metadata.ContactPerson;
import org.opensaml.saml2.metadata.EmailAddress;
import org.opensaml.saml2.metadata.EntitiesDescriptor;
import org.opensaml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml2.metadata.OrganizationDisplayName;
import org.opensaml.saml2.metadata.OrganizationName;
import org.opensaml.saml2.metadata.OrganizationURL;
import org.opensaml.saml2.metadata.SingleLogoutService;
import org.opensaml.saml2.metadata.SingleSignOnService;
import org.opensaml.saml2.metadata.provider.AbstractReloadingMetadataProvider;
import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider;
import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.xml.ConfigurationException;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.parse.BasicParserPool;
import org.opensaml.xml.security.credential.UsageType;
import org.opensaml.xml.security.keyinfo.KeyInfoHelper;
@ -48,6 +66,7 @@ import javax.inject.Inject;
import javax.xml.stream.FactoryConfigurationError;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
@ -63,61 +82,87 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
@Component
@Local(value = {SAML2AuthManager.class, PluggableAPIAuthenticator.class})
public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager {
public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager, Configurable {
private static final Logger s_logger = Logger.getLogger(SAML2AuthManagerImpl.class);
private String serviceProviderId;
private String identityProviderId;
private SAMLProviderMetadata _spMetadata = new SAMLProviderMetadata();
private Map<String, SAMLProviderMetadata> _idpMetadataMap = new HashMap<String, SAMLProviderMetadata>();
private X509Certificate idpSigningKey;
private X509Certificate idpEncryptionKey;
private X509Certificate spX509Key;
private KeyPair spKeyPair;
private String spSingleSignOnUrl;
private String idpSingleSignOnUrl;
private String spSingleLogOutUrl;
private String idpSingleLogOutUrl;
private HTTPMetadataProvider idpMetaDataProvider;
@Inject
ConfigurationDao _configDao;
private Timer _timer;
private int _refreshInterval = SAMLPluginConstants.SAML_REFRESH_INTERVAL;
private AbstractReloadingMetadataProvider _idpMetaDataProvider;
@Inject
private KeystoreDao _ksDao;
@Inject
private SAMLTokenDao _samlTokenDao;
@Inject
private UserDao _userDao;
@Inject
DomainManager _domainMgr;
@Override
public boolean start() {
if (isSAMLPluginEnabled()) {
setup();
s_logger.info("SAML auth plugin loaded");
} else {
s_logger.info("SAML auth plugin not enabled so not loading");
}
return super.start();
}
private boolean setup() {
KeystoreVO keyStoreVO = _ksDao.findByName(SAMLUtils.SAMLSP_KEYPAIR);
@Override
public boolean stop() {
if (_timer != null) {
_timer.cancel();
}
return super.stop();
}
private boolean initSP() {
KeystoreVO keyStoreVO = _ksDao.findByName(SAMLPluginConstants.SAMLSP_KEYPAIR);
if (keyStoreVO == null) {
try {
KeyPair keyPair = SAMLUtils.generateRandomKeyPair();
_ksDao.save(SAMLUtils.SAMLSP_KEYPAIR, SAMLUtils.savePrivateKey(keyPair.getPrivate()), SAMLUtils.savePublicKey(keyPair.getPublic()), "samlsp-keypair");
keyStoreVO = _ksDao.findByName(SAMLUtils.SAMLSP_KEYPAIR);
_ksDao.save(SAMLPluginConstants.SAMLSP_KEYPAIR, SAMLUtils.savePrivateKey(keyPair.getPrivate()), SAMLUtils.savePublicKey(keyPair.getPublic()), "samlsp-keypair");
keyStoreVO = _ksDao.findByName(SAMLPluginConstants.SAMLSP_KEYPAIR);
s_logger.info("No SAML keystore found, created and saved a new Service Provider keypair");
} catch (NoSuchProviderException | NoSuchAlgorithmException e) {
s_logger.error("Unable to create and save SAML keypair");
s_logger.error("Unable to create and save SAML keypair: " + e.toString());
}
}
String spId = SAMLServiceProviderID.value();
String spSsoUrl = SAMLServiceProviderSingleSignOnURL.value();
String spSloUrl = SAMLServiceProviderSingleLogOutURL.value();
String spOrgName = SAMLServiceProviderOrgName.value();
String spOrgUrl = SAMLServiceProviderOrgUrl.value();
String spContactPersonName = SAMLServiceProviderContactPersonName.value();
String spContactPersonEmail = SAMLServiceProviderContactEmail.value();
KeyPair spKeyPair = null;
X509Certificate spX509Key = null;
if (keyStoreVO != null) {
PrivateKey privateKey = SAMLUtils.loadPrivateKey(keyStoreVO.getCertificate());
PublicKey publicKey = SAMLUtils.loadPublicKey(keyStoreVO.getKey());
if (privateKey != null && publicKey != null) {
spKeyPair = new KeyPair(publicKey, privateKey);
KeystoreVO x509VO = _ksDao.findByName(SAMLUtils.SAMLSP_X509CERT);
KeystoreVO x509VO = _ksDao.findByName(SAMLPluginConstants.SAMLSP_X509CERT);
if (x509VO == null) {
try {
spX509Key = SAMLUtils.generateRandomX509Certificate(spKeyPair);
@ -125,7 +170,7 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage
ObjectOutput out = new ObjectOutputStream(bos);
out.writeObject(spX509Key);
out.flush();
_ksDao.save(SAMLUtils.SAMLSP_X509CERT, Base64.encodeBase64String(bos.toByteArray()), "", "samlsp-x509cert");
_ksDao.save(SAMLPluginConstants.SAMLSP_X509CERT, Base64.encodeBase64String(bos.toByteArray()), "", "samlsp-x509cert");
bos.close();
} catch (NoSuchAlgorithmException | NoSuchProviderException | CertificateEncodingException | SignatureException | InvalidKeyException | IOException e) {
s_logger.error("SAML Plugin won't be able to use X509 signed authentication");
@ -142,61 +187,194 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage
}
}
}
if (spKeyPair != null && spX509Key != null
&& spId != null && spSsoUrl != null && spSloUrl != null
&& spOrgName != null && spOrgUrl != null
&& spContactPersonName != null && spContactPersonEmail != null) {
_spMetadata.setEntityId(spId);
_spMetadata.setOrganizationName(spOrgName);
_spMetadata.setOrganizationUrl(spOrgUrl);
_spMetadata.setContactPersonName(spContactPersonName);
_spMetadata.setContactPersonEmail(spContactPersonEmail);
_spMetadata.setSsoUrl(spSsoUrl);
_spMetadata.setSloUrl(spSloUrl);
_spMetadata.setKeyPair(spKeyPair);
_spMetadata.setSigningCertificate(spX509Key);
_spMetadata.setEncryptionCertificate(spX509Key);
return true;
}
return false;
}
this.serviceProviderId = _configDao.getValue(Config.SAMLServiceProviderID.key());
this.identityProviderId = _configDao.getValue(Config.SAMLIdentityProviderID.key());
private void addIdpToMap(EntityDescriptor descriptor, Map<String, SAMLProviderMetadata> idpMap) {
SAMLProviderMetadata idpMetadata = new SAMLProviderMetadata();
idpMetadata.setEntityId(descriptor.getEntityID());
s_logger.debug("Adding IdP to the list of discovered IdPs: " + descriptor.getEntityID());
if (descriptor.getOrganization() != null) {
if (descriptor.getOrganization().getDisplayNames() != null) {
for (OrganizationDisplayName orgName : descriptor.getOrganization().getDisplayNames()) {
if (orgName != null && orgName.getName() != null) {
idpMetadata.setOrganizationName(orgName.getName().getLocalString());
break;
}
}
}
if (idpMetadata.getOrganizationName() == null && descriptor.getOrganization().getOrganizationNames() != null) {
for (OrganizationName orgName : descriptor.getOrganization().getOrganizationNames()) {
if (orgName != null && orgName.getName() != null) {
idpMetadata.setOrganizationName(orgName.getName().getLocalString());
break;
}
}
}
if (descriptor.getOrganization().getURLs() != null) {
for (OrganizationURL organizationURL : descriptor.getOrganization().getURLs()) {
if (organizationURL != null && organizationURL.getURL() != null) {
idpMetadata.setOrganizationUrl(organizationURL.getURL().getLocalString());
break;
}
}
}
}
if (descriptor.getContactPersons() != null) {
for (ContactPerson person : descriptor.getContactPersons()) {
if (person == null || (person.getGivenName() == null && person.getSurName() == null)
|| person.getEmailAddresses() == null) {
continue;
}
if (person.getGivenName() != null) {
idpMetadata.setContactPersonName(person.getGivenName().getName());
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);
} else if (person.getSurName() != null) {
idpMetadata.setContactPersonName(person.getSurName().getName());
}
for (EmailAddress emailAddress : person.getEmailAddresses()) {
if (emailAddress != null && emailAddress.getAddress() != null) {
idpMetadata.setContactPersonEmail(emailAddress.getAddress());
}
}
if (idpMetadata.getContactPersonName() != null && idpMetadata.getContactPersonEmail() != null) {
break;
}
}
}
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()) {
IDPSSODescriptor idpDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
if (idpDescriptor != null) {
if (idpDescriptor.getSingleSignOnServices() != null) {
for (SingleSignOnService ssos : idpDescriptor.getSingleSignOnServices()) {
if (ssos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) {
this.idpSingleSignOnUrl = ssos.getLocation();
idpMetadata.setSsoUrl(ssos.getLocation());
}
}
for (SingleLogoutService slos: idpssoDescriptor.getSingleLogoutServices()) {
}
if (idpDescriptor.getSingleLogoutServices() != null) {
for (SingleLogoutService slos : idpDescriptor.getSingleLogoutServices()) {
if (slos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) {
this.idpSingleLogOutUrl = slos.getLocation();
idpMetadata.setSloUrl(slos.getLocation());
}
}
}
for (KeyDescriptor kd: idpssoDescriptor.getKeyDescriptors()) {
X509Certificate unspecifiedKey = null;
if (idpDescriptor.getKeyDescriptors() != null) {
for (KeyDescriptor kd : idpDescriptor.getKeyDescriptors()) {
if (kd.getUse() == UsageType.SIGNING) {
try {
this.idpSigningKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0);
idpMetadata.setSigningCertificate(KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0));
} catch (CertificateException ignored) {
}
}
if (kd.getUse() == UsageType.ENCRYPTION) {
try {
this.idpEncryptionKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0);
idpMetadata.setEncryptionCertificate(KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0));
} catch (CertificateException ignored) {
}
}
if (kd.getUse() == UsageType.UNSPECIFIED) {
try {
unspecifiedKey = 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");
}
if (idpMetadata.getSigningCertificate() == null && unspecifiedKey != null) {
idpMetadata.setSigningCertificate(unspecifiedKey);
}
if (idpMetadata.getEncryptionCertificate() == null && unspecifiedKey != null) {
idpMetadata.setEncryptionCertificate(unspecifiedKey);
}
if (idpMap.containsKey(idpMetadata.getEntityId())) {
s_logger.warn("Duplicate IdP metadata found with entity Id: " + idpMetadata.getEntityId());
}
idpMap.put(idpMetadata.getEntityId(), idpMetadata);
}
}
private void discoverAndAddIdp(XMLObject metadata, Map<String, SAMLProviderMetadata> idpMap) {
if (metadata instanceof EntityDescriptor) {
EntityDescriptor entityDescriptor = (EntityDescriptor) metadata;
addIdpToMap(entityDescriptor, idpMap);
} else if (metadata instanceof EntitiesDescriptor) {
EntitiesDescriptor entitiesDescriptor = (EntitiesDescriptor) metadata;
if (entitiesDescriptor.getEntityDescriptors() != null) {
for (EntityDescriptor entityDescriptor: entitiesDescriptor.getEntityDescriptors()) {
addIdpToMap(entityDescriptor, idpMap);
}
}
if (entitiesDescriptor.getEntitiesDescriptors() != null) {
for (EntitiesDescriptor entitiesDescriptorInner: entitiesDescriptor.getEntitiesDescriptors()) {
discoverAndAddIdp(entitiesDescriptorInner, idpMap);
}
}
}
}
class MetadataRefreshTask extends TimerTask {
@Override
public void run() {
if (_idpMetaDataProvider == null) {
return;
}
s_logger.debug("Starting SAML IDP Metadata Refresh Task");
Map <String, SAMLProviderMetadata> metadataMap = new HashMap<String, SAMLProviderMetadata>();
try {
discoverAndAddIdp(_idpMetaDataProvider.getMetadata(), metadataMap);
_idpMetadataMap = metadataMap;
expireTokens();
s_logger.debug("Finished refreshing SAML Metadata and expiring old auth tokens");
} catch (MetadataProviderException e) {
s_logger.warn("SAML Metadata Refresh task failed with exception: " + e.getMessage());
}
}
}
private boolean setup() {
if (!initSP()) {
s_logger.error("SAML Plugin failed to initialize, please fix the configuration and restart management server");
return false;
}
_timer = new Timer();
final HttpClient client = new HttpClient();
final String idpMetaDataUrl = SAMLIdentityProviderMetadataURL.value();
if (SAMLTimeout.value() != null && SAMLTimeout.value() > SAMLPluginConstants.SAML_REFRESH_INTERVAL) {
_refreshInterval = SAMLTimeout.value();
}
try {
DefaultBootstrap.bootstrap();
if (idpMetaDataUrl.startsWith("http")) {
_idpMetaDataProvider = new HTTPMetadataProvider(_timer, client, idpMetaDataUrl);
} else {
File metadataFile = PropertiesUtil.findConfigFile(idpMetaDataUrl);
s_logger.debug("Provided Metadata is not a URL, trying to read metadata file from local path: " + metadataFile.getAbsolutePath());
_idpMetaDataProvider = new FilesystemMetadataProvider(_timer, metadataFile);
}
_idpMetaDataProvider.setRequireValidMetadata(true);
_idpMetaDataProvider.setParserPool(new BasicParserPool());
_idpMetaDataProvider.initialize();
_timer.scheduleAtFixedRate(new MetadataRefreshTask(), 0, _refreshInterval * 1000);
} catch (MetadataProviderException e) {
s_logger.error("Unable to read SAML2 IDP MetaData URL, error:" + e.getMessage());
s_logger.error("SAML2 Authentication may be unavailable");
@ -204,16 +382,105 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage
s_logger.error("OpenSAML bootstrapping failed: error: " + e.getMessage());
} catch (NullPointerException e) {
s_logger.error("Unable to setup SAML Auth Plugin due to NullPointerException" +
" please check the SAML IDP metadata URL and entity ID in global settings: " + e.getMessage());
" please check the SAML global settings: " + e.getMessage());
}
if (this.idpSingleLogOutUrl == null || this.idpSingleSignOnUrl == null) {
s_logger.error("SAML based authentication won't work");
}
return true;
}
@Override
public SAMLProviderMetadata getSPMetadata() {
return _spMetadata;
}
@Override
public SAMLProviderMetadata getIdPMetadata(String entityId) {
if (entityId != null && _idpMetadataMap.containsKey(entityId)) {
return _idpMetadataMap.get(entityId);
}
String defaultIdpId = SAMLDefaultIdentityProviderId.value();
if (defaultIdpId != null && _idpMetadataMap.containsKey(defaultIdpId)) {
return _idpMetadataMap.get(defaultIdpId);
}
// In case of a single IdP, return that as default
if (_idpMetadataMap.size() == 1) {
return _idpMetadataMap.values().iterator().next();
}
return null;
}
@Override
public Collection<SAMLProviderMetadata> getAllIdPMetadata() {
return _idpMetadataMap.values();
}
@Override
public boolean isUserAuthorized(Long userId, String entityId) {
UserVO user = _userDao.getUser(userId);
if (user != null) {
if (user.getSource().equals(User.Source.SAML2) &&
user.getExternalEntity().equalsIgnoreCase(entityId)) {
return true;
}
}
return false;
}
@Override
public boolean authorizeUser(Long userId, String entityId, boolean enable) {
UserVO user = _userDao.getUser(userId);
if (user != null) {
if (enable) {
user.setExternalEntity(entityId);
user.setSource(User.Source.SAML2);
} else {
if (user.getSource().equals(User.Source.SAML2)) {
user.setSource(User.Source.SAML2DISABLED);
} else {
return false;
}
}
_userDao.update(user.getId(), user);
return true;
}
return false;
}
@Override
public void saveToken(String authnId, String domainPath, String entity) {
Long domainId = null;
if (domainPath != null) {
Domain domain = _domainMgr.findDomainByPath(domainPath);
if (domain != null) {
domainId = domain.getId();
}
}
SAMLTokenVO token = new SAMLTokenVO(authnId, domainId, entity);
if (_samlTokenDao.findByUuid(authnId) == null) {
_samlTokenDao.persist(token);
} else {
s_logger.warn("Duplicate SAML token for entity=" + entity + " token id=" + authnId + " domain=" + domainPath);
}
}
@Override
public SAMLTokenVO getToken(String authnId) {
return _samlTokenDao.findByUuid(authnId);
}
@Override
public void expireTokens() {
_samlTokenDao.expireTokens();
}
public Boolean isSAMLPluginEnabled() {
return SAMLIsPluginEnabled.value();
}
@Override
public String getConfigComponentName() {
return "SAML2-PLUGIN";
}
@Override
public List<Class<?>> getAuthCommands() {
List<Class<?>> cmdList = new ArrayList<Class<?>>();
@ -223,51 +490,30 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage
cmdList.add(SAML2LoginAPIAuthenticatorCmd.class);
cmdList.add(SAML2LogoutAPIAuthenticatorCmd.class);
cmdList.add(GetServiceProviderMetaDataCmd.class);
cmdList.add(ListIdpsCmd.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()));
}
public X509Certificate getSpX509Certificate() {
return spX509Key;
@Override
public List<Class<?>> getCommands() {
List<Class<?>> cmdList = new ArrayList<Class<?>>();
if (!isSAMLPluginEnabled()) {
return cmdList;
}
cmdList.add(AuthorizeSAMLSSOCmd.class);
cmdList.add(ListSamlAuthorizationCmd.class);
return cmdList;
}
@Override
public KeyPair getSpKeyPair() {
return spKeyPair;
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[] {
SAMLIsPluginEnabled, SAMLServiceProviderID,
SAMLServiceProviderContactPersonName, SAMLServiceProviderContactEmail,
SAMLServiceProviderOrgName, SAMLServiceProviderOrgUrl,
SAMLServiceProviderSingleSignOnURL, SAMLServiceProviderSingleLogOutURL,
SAMLCloudStackRedirectionUrl, SAMLUserAttributeName,
SAMLIdentityProviderMetadataURL, SAMLDefaultIdentityProviderId,
SAMLSignatureAlgorithm, SAMLTimeout};
}
}

View File

@ -21,12 +21,20 @@ 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.cxf.common.util.StringUtils;
import org.apache.log4j.Logger;
import org.opensaml.DefaultBootstrap;
import org.opensaml.saml2.core.Response;
import org.opensaml.saml2.core.StatusCode;
import org.opensaml.xml.ConfigurationException;
import org.opensaml.xml.io.UnmarshallingException;
import org.xml.sax.SAXException;
import javax.ejb.Local;
import javax.inject.Inject;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.FactoryConfigurationError;
import java.io.IOException;
import java.util.Map;
@Local(value = {UserAuthenticator.class})
@ -50,13 +58,23 @@ public class SAML2UserAuthenticator extends DefaultUserAuthenticator {
}
final UserAccount userAccount = _userAccountDao.getUserAccount(username, domainId);
if (userAccount == null) {
s_logger.debug("Unable to find user with " + username + " in domain " + domainId);
if (userAccount == null || userAccount.getSource() != User.Source.SAML2) {
s_logger.debug("Unable to find user with " + username + " in domain " + domainId + ", or user source is not SAML2");
return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
} else {
User user = _userDao.getUser(userAccount.getId());
if (user != null && SAMLUtils.checkSAMLUser(user.getUuid(), username) &&
requestParameters != null && requestParameters.containsKey(SAMLUtils.SAML_RESPONSE)) {
if (user != null && requestParameters != null && requestParameters.containsKey(SAMLPluginConstants.SAML_RESPONSE)) {
final String samlResponse = ((String[])requestParameters.get(SAMLPluginConstants.SAML_RESPONSE))[0];
Response responseObject = null;
try {
DefaultBootstrap.bootstrap();
responseObject = SAMLUtils.decodeSAMLResponse(samlResponse);
} catch (ConfigurationException | FactoryConfigurationError | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) {
return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
}
if (!responseObject.getStatus().getStatusCode().getValue().equals(StatusCode.SUCCESS_URI)) {
return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
}
return new Pair<Boolean, ActionOnFailedAuthentication>(true, null);
}
}

View File

@ -0,0 +1,30 @@
//
// 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;
public class SAMLPluginConstants {
public static final int SAML_REFRESH_INTERVAL = 300;
public static final String SAML_RESPONSE = "SAMLResponse";
public static final String SAML_IDPID = "SAML_IDPID";
public static final String SAML_SESSIONID = "SAML_SESSIONID";
public static final String SAML_NAMEID = "SAML_NAMEID";
public static final String SAMLSP_KEYPAIR = "SAMLSP_KEYPAIR";
public static final String SAMLSP_X509CERT = "SAMLSP_X509CERT";
}

View File

@ -0,0 +1,122 @@
// 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 java.security.KeyPair;
import java.security.cert.X509Certificate;
public class SAMLProviderMetadata {
private String entityId;
private String organizationName;
private String organizationUrl;
private String contactPersonName;
private String contactPersonEmail;
private String ssoUrl;
private String sloUrl;
private KeyPair keyPair;
private X509Certificate signingCertificate;
private X509Certificate encryptionCertificate;
public SAMLProviderMetadata() {
}
public void setCommonCertificate(X509Certificate certificate) {
this.signingCertificate = certificate;
this.encryptionCertificate = certificate;
}
public String getEntityId() {
return entityId;
}
public void setEntityId(String entityId) {
this.entityId = entityId;
}
public String getContactPersonName() {
return contactPersonName;
}
public void setContactPersonName(String contactPersonName) {
this.contactPersonName = contactPersonName;
}
public String getContactPersonEmail() {
return contactPersonEmail;
}
public void setContactPersonEmail(String contactPersonEmail) {
this.contactPersonEmail = contactPersonEmail;
}
public String getOrganizationName() {
return organizationName;
}
public void setOrganizationName(String organizationName) {
this.organizationName = organizationName;
}
public String getOrganizationUrl() {
return organizationUrl;
}
public void setOrganizationUrl(String organizationUrl) {
this.organizationUrl = organizationUrl;
}
public KeyPair getKeyPair() {
return keyPair;
}
public void setKeyPair(KeyPair keyPair) {
this.keyPair = keyPair;
}
public X509Certificate getSigningCertificate() {
return signingCertificate;
}
public void setSigningCertificate(X509Certificate signingCertificate) {
this.signingCertificate = signingCertificate;
}
public X509Certificate getEncryptionCertificate() {
return encryptionCertificate;
}
public void setEncryptionCertificate(X509Certificate encryptionCertificate) {
this.encryptionCertificate = encryptionCertificate;
}
public String getSsoUrl() {
return ssoUrl;
}
public void setSsoUrl(String ssoUrl) {
this.ssoUrl = ssoUrl;
}
public String getSloUrl() {
return sloUrl;
}
public void setSloUrl(String sloUrl) {
this.sloUrl = sloUrl;
}
}

View File

@ -0,0 +1,23 @@
// 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.utils.db.GenericDao;
public interface SAMLTokenDao extends GenericDao<SAMLTokenVO, Long> {
public void expireTokens();
}

View File

@ -0,0 +1,51 @@
// 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.utils.db.DB;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.exception.CloudRuntimeException;
import org.springframework.stereotype.Component;
import javax.ejb.Local;
import java.sql.PreparedStatement;
@DB
@Component
@Local(value = {SAMLTokenDao.class})
public class SAMLTokenDaoImpl extends GenericDaoBase<SAMLTokenVO, Long> implements SAMLTokenDao {
public SAMLTokenDaoImpl() {
super();
}
@Override
public void expireTokens() {
TransactionLegacy txn = TransactionLegacy.currentTxn();
try {
txn.start();
String sql = "DELETE FROM `saml_token` WHERE `created` < (NOW() - INTERVAL 1 HOUR)";
PreparedStatement pstmt = txn.prepareAutoCloseStatement(sql);
pstmt.executeUpdate();
txn.commit();
} catch (Exception e) {
txn.rollback();
throw new CloudRuntimeException("Unable to flush old SAML tokens due to exception", e);
}
}
}

View File

@ -0,0 +1,97 @@
// 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.utils.db.GenericDao;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
@Entity
@Table(name = "saml_token")
public class SAMLTokenVO implements Identity, InternalIdentity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private long id;
@Column(name = "uuid")
private String uuid;
@Column(name = "domain_id")
private Long domainId = null;
@Column(name = "entity")
private String entity = null;
@Column(name = GenericDao.CREATED_COLUMN)
private Date created;
public SAMLTokenVO() {
}
public SAMLTokenVO(String uuid, Long domainId, String entity) {
this.uuid = uuid;
this.domainId = domainId;
this.entity = entity;
}
@Override
public long getId() {
return id;
}
@Override
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public Long getDomainId() {
return domainId;
}
public void setDomainId(long domainId) {
this.domainId = domainId;
}
public String getEntity() {
return entity;
}
public void setEntity(String entity) {
this.entity = entity;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
}

View File

@ -17,37 +17,36 @@
// under the License.
//
package org.apache.cloudstack.utils.auth;
package org.apache.cloudstack.saml;
import com.cloud.utils.HttpUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.x509.X509V1CertificateGenerator;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.opensaml.Configuration;
import org.opensaml.DefaultBootstrap;
import org.opensaml.common.SAMLVersion;
import org.opensaml.common.xml.SAMLConstants;
import org.opensaml.saml2.core.Assertion;
import org.opensaml.saml2.core.Attribute;
import org.opensaml.saml2.core.AttributeStatement;
import org.opensaml.saml2.core.AuthnContext;
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;
@ -66,6 +65,7 @@ import javax.security.auth.x500.X500Principal;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.FactoryConfigurationError;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -90,67 +90,85 @@ import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.List;
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 SAMLSP_KEYPAIR = "SAMLSP_KEYPAIR";
public static final String SAMLSP_X509CERT = "SAMLSP_X509CERT";
public static String createSAMLId(String uid) {
if (uid == null) {
return null;
}
String hash = DigestUtils.sha256Hex(uid);
String samlUuid = SAML_NS + hash;
return samlUuid.substring(0, 40);
}
public static boolean checkSAMLUser(String uuid, String username) {
if (uuid == null || uuid.isEmpty() || username == null || username.isEmpty()) {
return false;
}
return uuid.startsWith(SAML_NS) && createSAMLId(username).equals(uuid);
}
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();
public static String getValueFromAttributeStatements(final List<AttributeStatement> attributeStatements, final String attributeKey) {
if (attributeStatements == null || attributeStatements.size() < 1 || attributeKey == null) {
return null;
}
for (AttributeStatement attributeStatement : attributeStatements) {
if (attributeStatement == null || attributeStatements.size() < 1) {
continue;
}
for (Attribute attribute : attributeStatement.getAttributes()) {
if (attribute.getAttributeValues() != null && attribute.getAttributeValues().size() > 0) {
String value = attribute.getAttributeValues().get(0).getDOM().getTextContent();
s_logger.debug("SAML attribute name: " + attribute.getName() + " friendly-name:" + attribute.getFriendlyName() + " value:" + value);
if (attributeKey.equals(attribute.getName()) || attributeKey.equals(attribute.getFriendlyName())) {
return value;
}
}
}
}
return null;
}
public static String getValueFromAssertions(final List<Assertion> assertions, final String attributeKey) {
if (assertions == null || attributeKey == null) {
return null;
}
for (Assertion assertion : assertions) {
String value = getValueFromAttributeStatements(assertion.getAttributeStatements(), attributeKey);
if (value != null) {
return value;
}
}
return null;
}
public static String buildAuthnRequestUrl(final String authnId, final SAMLProviderMetadata spMetadata, final SAMLProviderMetadata idpMetadata, final String signatureAlgorithm) {
String redirectUrl = "";
try {
DefaultBootstrap.bootstrap();
AuthnRequest authnRequest = SAMLUtils.buildAuthnRequestObject(authnId, spMetadata.getEntityId(), idpMetadata.getSsoUrl(), spMetadata.getSsoUrl());
PrivateKey privateKey = null;
if (spMetadata.getKeyPair() != null) {
privateKey = spMetadata.getKeyPair().getPrivate();
}
redirectUrl = idpMetadata.getSsoUrl() + "?" + SAMLUtils.generateSAMLRequestSignature("SAMLRequest=" + SAMLUtils.encodeSAMLRequest(authnRequest), privateKey, signatureAlgorithm);
} catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException | NoSuchAlgorithmException | InvalidKeyException | java.security.SignatureException e) {
s_logger.error("SAML AuthnRequest message building error: " + e.getMessage());
}
return redirectUrl;
}
public static AuthnRequest buildAuthnRequestObject(final String authnId, final String spId, final String idpUrl, final String consumerUrl) {
// 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");
authnContextClassRef.setAuthnContextClassRef(AuthnContext.PPT_AUTHN_CTX);
// AuthnContex
// AuthnContext
RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder();
RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject();
requestedAuthnContext
.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
requestedAuthnContext.getAuthnContextClassRefs().add(
authnContextClassRef);
requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef);
// Creation of AuthRequestObject
AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder();
@ -160,36 +178,27 @@ public class SAMLUtils {
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.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
authnRequest.setAssertionConsumerServiceURL(consumerUrl);
authnRequest.setProviderName(spId);
authnRequest.setNameIDPolicy(nameIdPolicy);
authnRequest.setIssuer(issuer);
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();
public static LogoutRequest buildLogoutRequest(String logoutUrl, String spId, String nameIdString) {
Issuer issuer = new 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());
nameID.setValue(nameIdString);
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;
}
@ -226,13 +235,28 @@ public class SAMLUtils {
return (Response) unmarshaller.unmarshall(element);
}
public static String generateSAMLRequestSignature(String urlEncodedString, PrivateKey signingKey)
public static String generateSAMLRequestSignature(final String urlEncodedString, final PrivateKey signingKey, final String sigAlgorithmName)
throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, UnsupportedEncodingException {
if (signingKey == null) {
return urlEncodedString;
}
String url = urlEncodedString + "&SigAlg=" + URLEncoder.encode(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1, HttpUtils.UTF_8);
Signature signature = Signature.getInstance("SHA1withRSA");
String opensamlAlgoIdSignature = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1;
String javaSignatureAlgorithmName = "SHA1withRSA";
if (sigAlgorithmName.equalsIgnoreCase("SHA256")) {
opensamlAlgoIdSignature = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256;
javaSignatureAlgorithmName = "SHA256withRSA";
} else if (sigAlgorithmName.equalsIgnoreCase("SHA384")) {
opensamlAlgoIdSignature = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA384;
javaSignatureAlgorithmName = "SHA384withRSA";
} else if (sigAlgorithmName.equalsIgnoreCase("SHA512")) {
opensamlAlgoIdSignature = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512;
javaSignatureAlgorithmName = "SHA512withRSA";
}
String url = urlEncodedString + "&SigAlg=" + URLEncoder.encode(opensamlAlgoIdSignature, HttpUtils.UTF_8);
Signature signature = Signature.getInstance(javaSignatureAlgorithmName);
signature.initSign(signingKey);
signature.update(url.getBytes());
String signatureString = Base64.encodeBytes(signature.sign(), Base64.DONT_BREAK_LINES);

View File

@ -17,13 +17,15 @@
* under the License.
*/
package org.apache.cloudstack.api.command;
package org.apache.cloudstack;
import com.cloud.utils.HttpUtils;
import org.apache.cloudstack.api.ApiServerService;
import org.apache.cloudstack.api.auth.APIAuthenticationType;
import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd;
import org.apache.cloudstack.saml.SAML2AuthManager;
import org.apache.cloudstack.utils.auth.SAMLUtils;
import org.apache.cloudstack.saml.SAMLProviderMetadata;
import org.apache.cloudstack.saml.SAMLUtils;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -36,6 +38,7 @@ import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Field;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
@ -77,20 +80,21 @@ public class GetServiceProviderMetaDataCmdTest {
String spId = "someSPID";
String url = "someUrl";
X509Certificate cert = SAMLUtils.generateRandomX509Certificate(SAMLUtils.generateRandomKeyPair());
Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId);
Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(cert);
Mockito.when(samlAuthManager.getIdpSingleLogOutUrl()).thenReturn(url);
Mockito.when(samlAuthManager.getSpSingleLogOutUrl()).thenReturn(url);
KeyPair kp = SAMLUtils.generateRandomKeyPair();
X509Certificate cert = SAMLUtils.generateRandomX509Certificate(kp);
SAMLProviderMetadata providerMetadata = new SAMLProviderMetadata();
providerMetadata.setEntityId("random");
providerMetadata.setSigningCertificate(cert);
providerMetadata.setEncryptionCertificate(cert);
providerMetadata.setKeyPair(kp);
providerMetadata.setSsoUrl("http://test.local");
providerMetadata.setSloUrl("http://test.local");
Mockito.when(samlAuthManager.getSPMetadata()).thenReturn(providerMetadata);
String result = cmd.authenticate("command", null, session, InetAddress.getByName("127.0.0.1"), HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, 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

View File

@ -25,8 +25,8 @@ 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.SAMLPluginConstants;
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;
@ -68,8 +68,6 @@ public class SAML2UserAuthenticatorTest {
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);
@ -81,9 +79,9 @@ public class SAML2UserAuthenticatorTest {
Assert.assertFalse(pair.first());
// When there is SAMLRequest in params and user is same as the mocked one
params.put(SAMLUtils.SAML_RESPONSE, new Object[]{});
params.put(SAMLPluginConstants.SAML_RESPONSE, new String[]{"RandomString"});
pair = authenticator.authenticate("someUID", "random", 1l, params);
Assert.assertTrue(pair.first());
Assert.assertFalse(pair.first());
// When there is SAMLRequest in params but username is null
pair = authenticator.authenticate(null, "random", 1l, params);

View File

@ -17,14 +17,13 @@
// under the License.
//
package org.apache.cloudstack.utils.auth;
package org.apache.cloudstack;
import junit.framework.TestCase;
import org.apache.cloudstack.saml.SAMLUtils;
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;
import java.security.KeyPair;
import java.security.PrivateKey;
@ -32,18 +31,6 @@ import java.security.PublicKey;
public class SAMLUtilsTest extends TestCase {
@Test
public void testSAMLId() throws Exception {
assertEquals(SAMLUtils.createSAMLId(null), null);
assertEquals(SAMLUtils.createSAMLId("someUserName"), "SAML-305e19dd2581f33fd90b3949298ec8b17de");
assertTrue(SAMLUtils.checkSAMLUser(SAMLUtils.createSAMLId("someUserName"), "someUserName"));
assertFalse(SAMLUtils.checkSAMLUser(SAMLUtils.createSAMLId("someUserName"), "someOtherUserName"));
assertFalse(SAMLUtils.checkSAMLUser(SAMLUtils.createSAMLId(null), "someOtherUserName"));
assertFalse(SAMLUtils.checkSAMLUser("randomUID", "randomUID"));
assertFalse(SAMLUtils.checkSAMLUser(null, null));
}
@Test
public void testGenerateSecureRandomId() throws Exception {
assertTrue(SAMLUtils.generateSecureRandomId().length() > 0);
@ -54,7 +41,8 @@ public class SAMLUtilsTest extends TestCase {
String consumerUrl = "http://someurl.com";
String idpUrl = "http://idp.domain.example";
String spId = "cloudstack";
AuthnRequest req = SAMLUtils.buildAuthnRequestObject(spId, idpUrl, consumerUrl);
String authnId = SAMLUtils.generateSecureRandomId();
AuthnRequest req = SAMLUtils.buildAuthnRequestObject(authnId, spId, idpUrl, consumerUrl);
assertEquals(req.getAssertionConsumerServiceURL(), consumerUrl);
assertEquals(req.getDestination(), idpUrl);
assertEquals(req.getIssuer().getValue(), spId);
@ -64,15 +52,10 @@ public class SAMLUtilsTest extends TestCase {
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);
String nameId = "_12345";
LogoutRequest req = SAMLUtils.buildLogoutRequest(logoutUrl, spId, nameId);
assertEquals(req.getDestination(), logoutUrl);
assertEquals(req.getIssuer().getValue(), spId);
assertEquals(req.getNameID().getValue(), nameIdString);
assertEquals(req.getSessionIndexes().get(0).getSessionIndex(), sessionIndex);
}
@Test

View File

@ -29,9 +29,10 @@ 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.apache.cloudstack.saml.SAMLPluginConstants;
import org.apache.cloudstack.saml.SAMLProviderMetadata;
import org.apache.cloudstack.saml.SAMLUtils;
import org.joda.time.DateTime;
import org.junit.Assert;
import org.junit.Test;
@ -43,6 +44,7 @@ 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.Issuer;
import org.opensaml.saml2.core.NameID;
import org.opensaml.saml2.core.NameIDType;
import org.opensaml.saml2.core.Response;
@ -52,6 +54,7 @@ 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.IssuerBuilder;
import org.opensaml.saml2.core.impl.NameIDBuilder;
import org.opensaml.saml2.core.impl.ResponseBuilder;
import org.opensaml.saml2.core.impl.StatusBuilder;
@ -62,6 +65,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
@ -76,9 +80,6 @@ public class SAML2LoginAPIAuthenticatorCmdTest {
@Mock
SAML2AuthManager samlAuthManager;
@Mock
ConfigurationDao configDao;
@Mock
DomainManager domainMgr;
@ -105,6 +106,9 @@ public class SAML2LoginAPIAuthenticatorCmdTest {
samlMessage.setID("foo");
samlMessage.setVersion(SAMLVersion.VERSION_20);
samlMessage.setIssueInstant(new DateTime(0));
Issuer issuer = new IssuerBuilder().buildObject();
issuer.setValue("MockedIssuer");
samlMessage.setIssuer(issuer);
Status status = new StatusBuilder().buildObject();
StatusCode statusCode = new StatusCodeBuilder().buildObject();
statusCode.setValue(StatusCode.SUCCESS_URI);
@ -146,32 +150,33 @@ public class SAML2LoginAPIAuthenticatorCmdTest {
domainMgrField.setAccessible(true);
domainMgrField.set(cmd, domainMgr);
Field configDaoField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_configDao");
configDaoField.setAccessible(true);
configDaoField.set(cmd, configDao);
Field userAccountDaoField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_userAccountDao");
userAccountDaoField.setAccessible(true);
userAccountDaoField.set(cmd, userAccountDao);
String spId = "someSPID";
String url = "someUrl";
X509Certificate cert = SAMLUtils.generateRandomX509Certificate(SAMLUtils.generateRandomKeyPair());
Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId);
Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(null);
Mockito.when(samlAuthManager.getIdpSingleSignOnUrl()).thenReturn(url);
Mockito.when(samlAuthManager.getSpSingleSignOnUrl()).thenReturn(url);
KeyPair kp = SAMLUtils.generateRandomKeyPair();
X509Certificate cert = SAMLUtils.generateRandomX509Certificate(kp);
SAMLProviderMetadata providerMetadata = new SAMLProviderMetadata();
providerMetadata.setEntityId("random");
providerMetadata.setSigningCertificate(cert);
providerMetadata.setEncryptionCertificate(cert);
providerMetadata.setKeyPair(kp);
providerMetadata.setSsoUrl("http://test.local");
providerMetadata.setSloUrl("http://test.local");
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);
UserAccountVO user = new UserAccountVO();
user.setUsername(SAMLUtils.createSAMLId("someUID"));
user.setId(1000L);
Mockito.when(userAccountDao.getUserAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(user);
Mockito.when(apiServer.verifyUser(Mockito.anyLong())).thenReturn(false);
Mockito.when(samlAuthManager.getSPMetadata()).thenReturn(providerMetadata);
Mockito.when(samlAuthManager.getIdPMetadata(Mockito.anyString())).thenReturn(providerMetadata);
Map<String, Object[]> params = new HashMap<String, Object[]>();
@ -180,16 +185,14 @@ public class SAML2LoginAPIAuthenticatorCmdTest {
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"});
params.put(SAMLPluginConstants.SAML_RESPONSE, new String[]{"Some String"});
Mockito.stub(cmd.processSAMLResponse(Mockito.anyString())).toReturn(buildMockResponse());
try {
cmd.authenticate("command", params, session, InetAddress.getByName("127.0.0.1"), HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
} catch (ServerApiException ignored) {
}
Mockito.verify(configDao, Mockito.atLeastOnce()).getValue(Mockito.anyString());
Mockito.verify(domainMgr, Mockito.times(1)).getDomain(Mockito.anyString());
Mockito.verify(userAccountDao, Mockito.times(1)).getUserAccount(Mockito.anyString(), Mockito.anyLong());
Mockito.verify(apiServer, Mockito.times(1)).verifyUser(Mockito.anyLong());
Mockito.verify(userAccountDao, Mockito.times(0)).getUserAccount(Mockito.anyString(), Mockito.anyLong());
Mockito.verify(apiServer, Mockito.times(0)).verifyUser(Mockito.anyLong());
}
@Test

View File

@ -22,9 +22,8 @@ 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.apache.cloudstack.saml.SAMLUtils;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -48,9 +47,6 @@ public class SAML2LogoutAPIAuthenticatorCmdTest {
@Mock
SAML2AuthManager samlAuthManager;
@Mock
ConfigurationDao configDao;
@Mock
HttpSession session;
@ -72,19 +68,10 @@ public class SAML2LogoutAPIAuthenticatorCmdTest {
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(SAMLUtils.generateRandomKeyPair());
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, InetAddress.getByName("127.0.0.1"), HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
Mockito.verify(resp, Mockito.times(1)).sendRedirect(Mockito.anyString());

View File

@ -1062,8 +1062,8 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
final SecureRandom sesssionKeyRandom = new SecureRandom();
final byte sessionKeyBytes[] = new byte[20];
sesssionKeyRandom.nextBytes(sessionKeyBytes);
final String sessionKey = Base64.encodeBase64String(sessionKeyBytes);
session.setAttribute("sessionkey", sessionKey);
final String sessionKey = Base64.encodeBase64URLSafeString(sessionKeyBytes);
session.setAttribute(ApiConstants.SESSIONKEY, sessionKey);
return createLoginResponse(session);
}

View File

@ -238,7 +238,7 @@ public class ApiServlet extends HttpServlet {
userId = (Long)session.getAttribute("userid");
final String account = (String)session.getAttribute("account");
final Object accountObj = session.getAttribute("accountobj");
final String sessionKey = (String)session.getAttribute("sessionkey");
final String sessionKey = (String)session.getAttribute(ApiConstants.SESSIONKEY);
final String[] sessionKeyParam = (String[])params.get(ApiConstants.SESSIONKEY);
if ((sessionKeyParam == null) || (sessionKey == null) || !sessionKey.equals(sessionKeyParam[0])) {
try {

View File

@ -1385,78 +1385,6 @@ 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),
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", "256", "Default RAM for router VM (in MB).", null),

View File

@ -0,0 +1,20 @@
-- 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.
--;
-- Schema cleanup from 4.5.1 to 4.5.2;
--;

View File

@ -0,0 +1,35 @@
-- 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.
--;
-- Schema upgrade from 4.5.1 to 4.5.2;
--;
DELETE FROM `cloud`.`configuration` WHERE name like 'saml%';
ALTER TABLE `cloud`.`user` ADD COLUMN `external_entity` text DEFAULT NULL COMMENT "reference to external federation entity";
DROP TABLE IF EXISTS `cloud`.`saml_token`;
CREATE TABLE `cloud`.`saml_token` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`uuid` varchar(255) UNIQUE NOT NULL COMMENT 'The Authn Unique Id',
`domain_id` bigint unsigned DEFAULT NULL,
`entity` text NOT NULL COMMENT 'Identity Provider Entity Id',
`created` DATETIME NOT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_saml_token__domain_id` FOREIGN KEY(`domain_id`) REFERENCES `domain`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View File

@ -115,6 +115,9 @@ known_categories = {
'logout': 'Authentication',
'saml': 'Authentication',
'getSPMetadata': 'Authentication',
'listIdps': 'Authentication',
'authorizeSamlSso': 'Authentication',
'listSamlAuthorization': 'Authentication',
'Capacity': 'System Capacity',
'NetworkDevice': 'Network Device',
'ExternalLoadBalancer': 'Ext Load Balancer',

View File

@ -369,7 +369,7 @@ body.login {
.login .select-language select {
width: 260px;
border: 1px solid #808080;
margin-top: 30px;
margin-top: 20px;
/*+border-radius:4px;*/
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
@ -460,14 +460,12 @@ body.login {
background: transparent url(../images/sprites.png) -563px -747px;
cursor: pointer;
border: none;
margin: 7px 120px 0 -1px;
text-align: center;
width: 60px;
height: 15px;
display: block;
color: #FFFFFF;
font-weight: bold;
float: left;
text-indent: -1px;
/*+text-shadow:0px 1px 2px #000000;*/
-moz-text-shadow: 0px 1px 2px #000000;
@ -12749,6 +12747,14 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it
background-position: -196px -704px;
}
.configureSamlAuthorization .icon {
background-position: -165px -122px;
}
.configureSamlAuthorization:hover .icon {
background-position: -165px -704px;
}
.viewConsole .icon {
background-position: -231px -2px;
}
@ -12972,13 +12978,6 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it
border-radius: 4px;
border-radius: 4px 4px 4px 4px;
border: 1px solid #AFAFAF;
-moz-box-shadow: inset 0px 1px #727272;
-webkit-box-shadow: inset 0px 1px #727272;
-o-box-shadow: inset 0px 1px #727272;
box-shadow: inset 0px 1px #727272;
-moz-box-shadow: inset 0px 1px 0px #727272;
-webkit-box-shadow: inset 0px 1px 0px #727272;
-o-box-shadow: inset 0px 1px 0px #727272;
}
.manual-account-details > *:nth-child(even) {

View File

@ -143,6 +143,7 @@ dictionary = {
'label.action.cancel.maintenance.mode': '<fmt:message key="label.action.cancel.maintenance.mode" />',
'label.action.cancel.maintenance.mode.processing': '<fmt:message key="label.action.cancel.maintenance.mode.processing" />',
'label.action.change.password': '<fmt:message key="label.action.change.password" />',
'label.action.configure.samlauthorization': '<fmt:message key="label.action.configure.samlauthorization" />',
'label.action.change.service': '<fmt:message key="label.action.change.service" />',
'label.action.change.service.processing': '<fmt:message key="label.action.change.service.processing" />',
'label.action.copy.ISO': '<fmt:message key="label.action.copy.ISO" />',
@ -764,7 +765,9 @@ dictionary = {
'label.local.storage': '<fmt:message key="label.local.storage" />',
'label.login': '<fmt:message key="label.login" />',
'label.logout': '<fmt:message key="label.logout" />',
'label.saml.login': '<fmt:message key="label.saml.login" />',
'label.saml.enable': '<fmt:message key="label.saml.enable" />',
'label.saml.entity': '<fmt:message key="label.saml.entity" />',
'label.add.LDAP.account': '<fmt:message key="label.add.LDAP.account" />',
'label.lun': '<fmt:message key="label.lun" />',
'label.LUN.number': '<fmt:message key="label.LUN.number" />',
'label.make.project.owner': '<fmt:message key="label.make.project.owner" />',

View File

@ -51,28 +51,45 @@
<form>
<div class="logo"></div>
<div class="fields">
<!-- User name -->
<div class="field username">
<label for="username"><fmt:message key="label.username"/></label>
<input type="text" name="username" class="required" />
<div id="login-dropdown">
<select id="login-options" style="width: 260px">
<option value="cloudstack-login">Local <fmt:message key="label.login"/></option>
</select>
</div>
<!-- Password -->
<div class="field password">
<label for="password"><fmt:message key="label.password"/></label>
<input type="password" name="password" class="required" autocomplete="off" />
<div id="cloudstack-login">
<!-- User name -->
<div class="field username">
<label for="username"><fmt:message key="label.username"/></label>
<input type="text" name="username" class="required" />
</div>
<!-- Password -->
<div class="field password">
<label for="password"><fmt:message key="label.password"/></label>
<input type="password" name="password" class="required" autocomplete="off" />
</div>
<!-- Domain -->
<div class="field domain">
<label for="domain"><fmt:message key="label.domain"/></label>
<input type="text" name="domain" />
</div>
</div>
<!-- Domain -->
<div class="field domain">
<label for="domain"><fmt:message key="label.domain"/></label>
<input type="text" name="domain" />
<div id="saml-login">
<div class="field domain">
<label for="saml-domain"><fmt:message key="label.domain"/></label>
<input id="saml-domain" type="text" name="saml-domain" />
</div>
</div>
<div id="login-submit">
<!-- Submit (login) -->
<input id="login-submit" type="submit" value="<fmt:message key="label.login"/>" />
</div>
<!-- Submit (login) -->
<input type="submit" value="<fmt:message key="label.login"/>" />
<div id="saml-login"><input type="samlsubmit" value="<fmt:message key="label.saml.login"/>"/></div>
<!-- Select language -->
<div class="select-language">
<select name="language">
<option value=""></option> <!-- when this blank option is selected, browser's default language will be used -->
<option value=""></option> <!-- when this blank option is selected, default language of the browser will be used -->
<option value="en"><fmt:message key="label.lang.english"/></option>
<option value="ja_JP"><fmt:message key="label.lang.japanese"/></option>
<option value="zh_CN"><fmt:message key="label.lang.chinese"/></option>

View File

@ -1223,6 +1223,102 @@
}
},
configureSamlAuthorization: {
label: 'label.action.configure.samlauthorization',
messages: {
notification: function(args) {
return 'label.action.configure.samlauthorization';
}
},
action: {
custom: function(args) {
var start = args.start;
var complete = args.complete;
var context = args.context;
if (g_idpList) {
$.ajax({
url: createURL('listSamlAuthorization'),
data: {
userid: context.users[0].id,
},
success: function(json) {
var authorization = json.listsamlauthorizationsresponse.samlauthorization[0];
cloudStack.dialog.createForm({
form: {
title: 'label.action.configure.samlauthorization',
fields: {
samlEnable: {
label: 'label.saml.enable',
docID: 'helpSamlEnable',
isBoolean: true,
isChecked: authorization.status,
validation: {
required: false
}
},
samlEntity: {
label: 'label.saml.entity',
docID: 'helpSamlEntity',
validation: {
required: false
},
select: function(args) {
var items = [];
$(g_idpList).each(function() {
items.push({
id: this.id,
description: this.orgName
});
});
args.response.success({
data: items
});
args.$select.change(function() {
$('select[name="samlEntity"] option[value="' + authorization.idpid + '"]').attr("selected", "selected");
});
}
}
}
},
after: function(args) {
start();
var enableSaml = false;
var idpId = '';
if (args.data.hasOwnProperty('samlEnable')) {
enableSaml = (args.data.samlEnable === 'on');
}
if (args.data.hasOwnProperty('samlEntity')) {
idpId = args.data.samlEntity;
}
$.ajax({
url: createURL('authorizeSamlSso'),
data: {
userid: context.users[0].id,
enable: enableSaml,
entityid: idpId
},
type: "POST",
success: function(json) {
complete();
},
error: function(json) {
complete({ error: parseXMLHttpResponse(json) });
}
});
}
});
},
error: function(json) {
complete({ error: parseXMLHttpResponse(json) });
}
});
}
}
}
},
generateKeys: {
label: 'label.action.generate.keys',
messages: {
@ -1797,6 +1893,9 @@
allowedActions.push("edit");
allowedActions.push("changePassword");
allowedActions.push("generateKeys");
if (g_idpList) {
allowedActions.push("configureSamlAuthorization");
}
if (!(jsonObj.domain == "ROOT" && jsonObj.account == "admin" && jsonObj.accounttype == 1)) { //if not system-generated default admin account user
if (jsonObj.state == "enabled")
allowedActions.push("disable");
@ -1818,6 +1917,9 @@
allowedActions.push("changePassword");
allowedActions.push("generateKeys");
if (g_idpList) {
allowedActions.push("configureSamlAuthorization");
}
}
}
return allowedActions;

View File

@ -162,8 +162,34 @@
validation: {
required: false
}
},
samlEnable: {
label: 'label.saml.enable',
docID: 'helpSamlEnable',
isBoolean: true,
validation: {
required: false
}
},
samlEntity: {
label: 'label.saml.entity',
docID: 'helpSamlEntity',
validation: {
required: false
},
select: function(args) {
var items = [];
$(g_idpList).each(function() {
items.push({
id: this.id,
description: this.orgName
});
});
args.response.success({
data: items
});
}
}
},
action: function(args) {
@ -218,6 +244,18 @@
array1.push("&group=" + args.groupname);
}
var authorizeUsersForSamlSSO = function (users, entity) {
for (var i = 0; i < users.length; i++) {
$.ajax({
url: createURL('authorizeSamlSso&enable=true&userid=' + users[i].id + "&entityid=" + entity),
error: function(XMLHttpResponse) {
args.response.error(parseXMLHttpResponse(XMLHttpResponse));
}
});
}
return;
};
if (ldapStatus) {
if (args.groupname) {
$.ajax({
@ -225,6 +263,13 @@
dataType: "json",
type: "POST",
async: false,
success: function (json) {
if (json.ldapuserresponse && args.data.samlEnable && args.data.samlEnable === 'on') {
cloudStack.dialog.notice({
message: "Unable to find users IDs to enable SAML Single Sign On, kindly enable it manually."
});
}
},
error: function(XMLHttpResponse) {
args.response.error(parseXMLHttpResponse(XMLHttpResponse));
}
@ -235,6 +280,14 @@
dataType: "json",
type: "POST",
async: false,
success: function(json) {
if (args.data.samlEnable && args.data.samlEnable === 'on') {
var users = json.createaccountresponse.account.user;
var entity = args.data.samlEntity;
if (users && entity)
authorizeUsersForSamlSSO(users, entity);
}
},
error: function(XMLHttpResponse) {
args.response.error(parseXMLHttpResponse(XMLHttpResponse));
}
@ -246,6 +299,14 @@
dataType: "json",
type: "POST",
async: false,
success: function(json) {
if (args.data.samlEnable && args.data.samlEnable === 'on') {
var users = json.createaccountresponse.account.user;
var entity = args.data.samlEntity;
if (users && entity)
authorizeUsersForSamlSSO(users, entity);
}
},
error: function(XMLHttpResponse) {
args.response.error(parseXMLHttpResponse(XMLHttpResponse));
}

View File

@ -115,7 +115,7 @@
cookieValue = cookieValue.slice(1, cookieValue.length-1);
$.cookie(cookieName, cookieValue, { expires: 1 });
}
return cookieValue;
return decodeURIComponent(cookieValue);
};
unBoxCookieValue('sessionkey');
// if sessionkey cookie exists use this to set g_sessionKey
@ -353,6 +353,17 @@
},
samlLoginAction: function(args) {
g_sessionKey = null;
g_username = null;
g_account = null;
g_domainid = null;
g_timezoneoffset = null;
g_timezone = null;
g_supportELB = null;
g_kvmsnapshotenabled = null;
g_regionsecondaryenabled = null;
g_loginCmdText = null;
$.cookie('JSESSIONID', null);
$.cookie('sessionkey', null);
$.cookie('username', null);
@ -360,7 +371,14 @@
$.cookie('domainid', null);
$.cookie('role', null);
$.cookie('timezone', null);
window.location.href = createURL('samlSso');
var url = 'samlSso';
if (args.data.idpid) {
url = url + '&idpid=' + args.data.idpid;
}
if (args.data.domain) {
url = url + '&domain=' + args.data.domain;
}
window.location.href = createURL(url);
},
// Show cloudStack main UI widget

View File

@ -1261,6 +1261,14 @@ cloudStack.docs = {
desc: 'The group name from which you want to import LDAP users',
externalLink: ''
},
helpSamlEnable: {
desc: 'Enable SAML Single Sign On for the user(s)',
externalLink: ''
},
helpSamlEntity: {
desc: 'Choose the SAML Identity Provider Entity ID with which you want to enable the Single Sign On for the user(s)',
externalLink: ''
},
helpVpcOfferingName: {
desc: 'Any desired name for the VPC offering',
externalLink: ''

View File

@ -32,6 +32,7 @@ var g_regionsecondaryenabled = null;
var g_userPublicTemplateEnabled = "true";
var g_cloudstackversion = null;
var g_queryAsyncJobResultInterval = 3000;
var g_idpList = null;
//keyboard keycode
var keycode_Enter = 13;

View File

@ -271,6 +271,11 @@
delete args.informationNotInLdap.ldapGroupName;
}
if (g_idpList == null) {
delete args.informationNotInLdap.samlEnable;
delete args.informationNotInLdap.samlEntity;
}
var informationNotInLdap = cloudStack.dialog.createForm({
context: context,
noDialog: true,

View File

@ -94,54 +94,120 @@
$inputs.filter(':first').addClass('first-input').focus();
// Login action
$login.find('input[type=submit]').click(function() {
if (!$form.valid()) return false;
var selectedLogin = 'cloudstack';
$login.find('#login-submit').click(function() {
if (selectedLogin === 'cloudstack') {
// CloudStack Local Login
if (!$form.valid()) return false;
var data = cloudStack.serializeForm($form);
var data = cloudStack.serializeForm($form);
args.loginAction({
data: data,
response: {
success: function(args) {
$login.remove();
$('html body').removeClass('login');
complete({
user: args.data.user
});
},
error: function(args) {
cloudStack.dialog.notice({
message: args
});
args.loginAction({
data: data,
response: {
success: function(args) {
$login.remove();
$('html body').removeClass('login');
complete({
user: args.data.user
});
},
error: function(args) {
cloudStack.dialog.notice({
message: args
});
}
}
}
});
});
} else if (selectedLogin === 'saml') {
// SAML
args.samlLoginAction({
data: {'idpid': $login.find('#login-options').find(':selected').val(),
'domain': $login.find('#saml-domain').val()}
});
}
return false;
});
// SAML Login action
$login.find('input[type=samlsubmit]').click(function() {
args.samlLoginAction({
});
// Show SAML button if only SP is configured
$login.find('#login-dropdown').hide();
$login.find('#saml-login').hide();
$login.find('#cloudstack-login').hide();
var toggleLoginView = function (selectedOption) {
$login.find('#login-submit').show();
if (selectedOption === '') {
$login.find('#saml-login').hide();
$login.find('#cloudstack-login').hide();
$login.find('#login-submit').hide();
selectedLogin = 'none';
} else if (selectedOption === 'cloudstack-login') {
$login.find('#saml-login').hide();
$login.find('#cloudstack-login').show();
selectedLogin = 'cloudstack';
} else {
$login.find('#saml-login').show();
$login.find('#cloudstack-login').hide();
selectedLogin = 'saml';
}
};
$login.find('#login-options').change(function() {
var selectedOption = $login.find('#login-options').find(':selected').val();
toggleLoginView(selectedOption);
if (selectedOption && selectedOption !== '') {
$.cookie('login-option', selectedOption);
}
});
// Show SAML button if only SP is configured
$login.find("#saml-login").hide();
$.ajax({
type: "GET",
url: createURL("getSPMetadata"),
dataType: "json",
type: 'GET',
url: createURL('listIdps'),
dataType: 'json',
async: false,
success: function(data, textStatus, xhr) {
if (xhr.status === 200) {
$login.find('#saml-login').show();
$login.find('#login-dropdown').show();
$login.find('#login-submit').hide();
} else {
$login.find('#saml-login').hide();
$login.find('#cloudstack-login').show();
$login.find('#login-submit').show();
return;
}
$login.find('#login-options')
.append($('<option>', {
value: '',
text: '--- Select Identity Provider -- ',
selected: true
}));
if (data.listidpsresponse && data.listidpsresponse.idp) {
var idpList = data.listidpsresponse.idp.sort(function (a, b) {
return a.orgName.localeCompare(b.orgName);
});
g_idpList = idpList;
$.each(idpList, function(index, idp) {
$login.find('#login-options')
.append($('<option>', {
value: idp.id,
text: idp.orgName
}));
});
}
var loginOption = $.cookie('login-option');
if (loginOption) {
var option = $login.find('#login-options option[value="' + loginOption + '"]');
if (option.length > 0) {
option.prop('selected', true);
toggleLoginView(loginOption);
}
}
},
error: function(xhr) {
$login.find('#saml-login').hide();
$login.find('#cloudstack-login').show();
}
});