mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 01:32:18 +02:00
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:
parent
527d6ee77b
commit
107595a6a5
@ -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);
|
||||
}
|
||||
|
||||
@ -63,4 +63,8 @@ public interface UserAccount extends InternalIdentity {
|
||||
int getLoginAttempts();
|
||||
|
||||
public User.Source getSource();
|
||||
|
||||
public String getExternalEntity();
|
||||
|
||||
public void setExternalEntity(String entity);
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -26,6 +26,9 @@ logout=15
|
||||
samlSso=15
|
||||
samlSlo=15
|
||||
getSPMetadata=15
|
||||
listIdps=15
|
||||
authorizeSamlSso=7
|
||||
listSamlAuthorization=7
|
||||
|
||||
### Account commands
|
||||
createAccount=7
|
||||
|
||||
@ -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;
|
||||
|
||||
63
developer/developer-saml.sql
Normal file
63
developer/developer-saml.sql
Normal file
File diff suppressed because one or more lines are too long
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -33,4 +33,7 @@
|
||||
<property name="name" value="SAML2Auth"/>
|
||||
</bean>
|
||||
|
||||
<bean id="samlTokenDao" class="org.apache.cloudstack.saml.SAMLTokenDaoImpl">
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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";
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
@ -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
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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),
|
||||
|
||||
|
||||
20
setup/db/db/schema-451to452-cleanup.sql
Normal file
20
setup/db/db/schema-451to452-cleanup.sql
Normal 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;
|
||||
--;
|
||||
35
setup/db/db/schema-451to452.sql
Normal file
35
setup/db/db/schema-451to452.sql
Normal 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;
|
||||
@ -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',
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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" />',
|
||||
|
||||
49
ui/index.jsp
49
ui/index.jsp
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: ''
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user