From 107595a6a5c669c63838bb6c52a93853cbd3c855 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Thu, 28 May 2015 14:50:12 +0200 Subject: [PATCH] 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 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 --- api/src/com/cloud/user/User.java | 7 +- api/src/com/cloud/user/UserAccount.java | 4 + .../apache/cloudstack/api/ApiConstants.java | 3 +- .../classes/resources/messages.properties | 5 +- .../resources/messages_fr_FR.properties | 1 - .../classes/resources/messages_hu.properties | 1 - client/tomcatconf/commands.properties.in | 3 + developer/developer-prefill.sql | 5 - developer/developer-saml.sql | 63 +++ developer/pom.xml | 58 +++ .../src/com/cloud/user/UserAccountVO.java | 11 + engine/schema/src/com/cloud/user/UserVO.java | 10 + .../com/cloud/user/dao/UserAccountDao.java | 4 + .../cloud/user/dao/UserAccountDaoImpl.java | 19 +- plugins/user-authenticators/saml2/pom.xml | 5 + .../cloudstack/saml2/spring-saml2-context.xml | 3 + .../api/command/AuthorizeSAMLSSOCmd.java | 105 ++++ .../GetServiceProviderMetaDataCmd.java | 84 +++- .../cloudstack/api/command/ListIdpsCmd.java | 114 +++++ .../api/command/ListSamlAuthorizationCmd.java | 95 ++++ .../SAML2LoginAPIAuthenticatorCmd.java | 288 ++++++----- .../SAML2LogoutAPIAuthenticatorCmd.java | 29 +- .../cloudstack/api/response/IdpResponse.java | 62 +++ .../response/SamlAuthorizationResponse.java | 68 +++ .../cloudstack/saml/SAML2AuthManager.java | 67 ++- .../cloudstack/saml/SAML2AuthManagerImpl.java | 454 ++++++++++++++---- .../saml/SAML2UserAuthenticator.java | 28 +- .../cloudstack/saml/SAMLPluginConstants.java | 30 ++ .../cloudstack/saml/SAMLProviderMetadata.java | 122 +++++ .../apache/cloudstack/saml/SAMLTokenDao.java | 23 + .../cloudstack/saml/SAMLTokenDaoImpl.java | 51 ++ .../apache/cloudstack/saml/SAMLTokenVO.java | 97 ++++ .../apache/cloudstack/saml}/SAMLUtils.java | 148 +++--- .../GetServiceProviderMetaDataCmdTest.java | 30 +- .../SAML2UserAuthenticatorTest.java | 8 +- .../org/apache/cloudstack}/SAMLUtilsTest.java | 29 +- .../SAML2LoginAPIAuthenticatorCmdTest.java | 45 +- .../SAML2LogoutAPIAuthenticatorCmdTest.java | 15 +- server/src/com/cloud/api/ApiServer.java | 4 +- server/src/com/cloud/api/ApiServlet.java | 2 +- .../src/com/cloud/configuration/Config.java | 72 --- setup/db/db/schema-451to452-cleanup.sql | 20 + setup/db/db/schema-451to452.sql | 35 ++ tools/apidoc/gen_toc.py | 3 + ui/css/cloudstack3.css | 19 +- ui/dictionary.jsp | 5 +- ui/index.jsp | 49 +- ui/scripts/accounts.js | 102 ++++ ui/scripts/accountsWizard.js | 63 ++- ui/scripts/cloudStack.js | 22 +- ui/scripts/docs.js | 8 + ui/scripts/sharedFunctions.js | 1 + ui/scripts/ui-custom/accountsWizard.js | 5 + ui/scripts/ui-custom/login.js | 128 +++-- 54 files changed, 2169 insertions(+), 563 deletions(-) create mode 100644 developer/developer-saml.sql create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/AuthorizeSAMLSSOCmd.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListSamlAuthorizationCmd.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/IdpResponse.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlAuthorizationResponse.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLPluginConstants.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLProviderMetadata.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDao.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDaoImpl.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenVO.java rename {utils/src/org/apache/cloudstack/utils/auth => plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml}/SAMLUtils.java (72%) rename plugins/user-authenticators/saml2/test/org/apache/cloudstack/{api/command => }/GetServiceProviderMetaDataCmdTest.java (79%) rename {utils/test/org/apache/cloudstack/utils/auth => plugins/user-authenticators/saml2/test/org/apache/cloudstack}/SAMLUtilsTest.java (66%) create mode 100644 setup/db/db/schema-451to452-cleanup.sql create mode 100644 setup/db/db/schema-451to452.sql diff --git a/api/src/com/cloud/user/User.java b/api/src/com/cloud/user/User.java index 8095e239417..0ecdcfa58d4 100644 --- a/api/src/com/cloud/user/User.java +++ b/api/src/com/cloud/user/User.java @@ -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); } diff --git a/api/src/com/cloud/user/UserAccount.java b/api/src/com/cloud/user/UserAccount.java index d44fcf72a89..0449514cc19 100644 --- a/api/src/com/cloud/user/UserAccount.java +++ b/api/src/com/cloud/user/UserAccount.java @@ -63,4 +63,8 @@ public interface UserAccount extends InternalIdentity { int getLoginAttempts(); public User.Source getSource(); + + public String getExternalEntity(); + + public void setExternalEntity(String entity); } diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index 2b6425846e4..204f33b0f42 100644 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -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"; diff --git a/client/WEB-INF/classes/resources/messages.properties b/client/WEB-INF/classes/resources/messages.properties index 20970fea94d..701582cb132 100644 --- a/client/WEB-INF/classes/resources/messages.properties +++ b/client/WEB-INF/classes/resources/messages.properties @@ -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 diff --git a/client/WEB-INF/classes/resources/messages_fr_FR.properties b/client/WEB-INF/classes/resources/messages_fr_FR.properties index 118f7f936cc..8a8efb7f5c5 100644 --- a/client/WEB-INF/classes/resources/messages_fr_FR.properties +++ b/client/WEB-INF/classes/resources/messages_fr_FR.properties @@ -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 diff --git a/client/WEB-INF/classes/resources/messages_hu.properties b/client/WEB-INF/classes/resources/messages_hu.properties index 397c43f6b81..6482c9ca4ed 100644 --- a/client/WEB-INF/classes/resources/messages_hu.properties +++ b/client/WEB-INF/classes/resources/messages_hu.properties @@ -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 diff --git a/client/tomcatconf/commands.properties.in b/client/tomcatconf/commands.properties.in index f4ff1c9dbcd..29752c62995 100644 --- a/client/tomcatconf/commands.properties.in +++ b/client/tomcatconf/commands.properties.in @@ -26,6 +26,9 @@ logout=15 samlSso=15 samlSlo=15 getSPMetadata=15 +listIdps=15 +authorizeSamlSso=7 +listSamlAuthorization=7 ### Account commands createAccount=7 diff --git a/developer/developer-prefill.sql b/developer/developer-prefill.sql index 27b36e78581..3097203b41d 100644 --- a/developer/developer-prefill.sql +++ b/developer/developer-prefill.sql @@ -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; diff --git a/developer/developer-saml.sql b/developer/developer-saml.sql new file mode 100644 index 00000000000..18afb288785 --- /dev/null +++ b/developer/developer-saml.sql @@ -0,0 +1,63 @@ +-- 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. + +-- SAML keystore for testing, allows testing on ssocirlce and other public IdPs +-- with pre-seeded SP metadata +USE cloud; + +-- Enable SAML plugin for developers by default +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'SAML2-PLUGIN', + 'saml2.enabled', 'true') + ON DUPLICATE KEY UPDATE value=VALUES(value); + +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'SAML2-PLUGIN', + 'saml2.default.idpid', 'https://idp.bhaisaab.org/idp/shibboleth') + ON DUPLICATE KEY UPDATE value=VALUES(value); + +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'SAML2-PLUGIN', + 'saml2.idp.metadata.url', 'http://idp.bhaisaab.org/idp/shibboleth') + ON DUPLICATE KEY UPDATE value=VALUES(value); + +-- Enable LDAP source +INSERT INTO `cloud`.`ldap_configuration` (hostname, port) + VALUES ('idp.bhaisaab.org', 389); + +-- Fix ldap configs +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'management-server', + 'ldap.basedn', 'ou=people,dc=idp,dc=bhaisaab,dc=org') + ON DUPLICATE KEY UPDATE value=VALUES(value); + +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'management-server', + 'ldap.bind.principal', 'cn=admin,dc=idp,dc=bhaisaab,dc=org') + ON DUPLICATE KEY UPDATE value=VALUES(value); + +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'management-server', + 'ldap.bind.password', 'password') + ON DUPLICATE KEY UPDATE value=VALUES(value); + +-- Add default set of certificates for testing +LOCK TABLES `keystore` WRITE; +/*!40000 ALTER TABLE `keystore` DISABLE KEYS */; +INSERT INTO `keystore` VALUES (1,'SAMLSP_KEYPAIR','MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDQDirtAajTrScXCUgXrsOBgQ++y+IUcyzXwUGEYBlM9kBCmcdlbt5zuYQ8pOvoOQz6CAVqSjYNbnbg1ph37Zfv/tzGUg2V5cpB5BfEyt2KY0mBFNbwa0LKnCAYlBlarm4XZF+oZ0maH6cdboHdHiEqNRscylnXYA2zHKU3YoEfOR+9acl4z54PAyuPjr9SWUPTAyYf326i0q+h3J4nT6FwBFK+yKSC1PeVG/viYQ0otU1UUDkQ3pX81qfJfN/6Ih7W4v73LgUlZsTBMzlu/kJzMuP5Wc5IkU6Mt+8EHMZeLnN0ZErkMk0DCQE14hG8W7S8/inUJpwJlmb5E634Zq8u0I1zVUmSmAGqvvqKJBGnqY5X/j2bsA8B2qFsrcxasIlkKaWLvY+AvXD5X0OHwIZzRbuuCtguSz671C7Cwok8R3N+e9ATDHmG9gC10NJaB6dUBA9p2UdA82TR73x6nGe8pLJnGyecEQfxz1+ptPVAENj1Rl3Wrwu/dbPd/X6inlYpuwlsnWi3LYrkguT/9W3Z2uuq5PTVT04zcyev+50gVnDPzTrNCZfHpmMNQIqZGFmFKz4m+VUZmzZOdg1Vx51t9+t7iHGHqFk6/vnqqWiyEuTEFAaC9krm1VNzGvno5LyNm5Dk9JGAHFfjgNV++viGTpSPLeEeMnlvPuQJy5OJMwIDAQABAoICABME6Imn+C35izRA5fU8RaUGDlFrw+wIp1XF1d5rBoURkchE1ISCQRWlJOCCVwpwhK4qo4wW4qARtA5Tr7Zu4s/OpZH/mDxWuEmTt1SHEv9+mg6RwCBUPdPVt91nVHYEsg2zYEc9we2z7Qv0uSxkf7WjCypzmQjmP/paqQPKHnGjQDKJhCBmIlXO/WFvNDAr9tZIWGjbfPqndeS/DTocvm5GBuZn4xoOq99Woo0MQC6zfDEz8DOJlX56hPYXU0ZDbjxInfQsoc3MejoLG7n4xkxPn6WAvynFFsAoZFIk60Faz7UZIfuAWafoX9L0KpjkbT5Fob9CFEuQEzO7x9CIWoUr2PYn8HbThHDUOFAuVVpOLqleLPCrxkX/P01WTrLFuT6vSJKW2kxVwiHvZH6pNT01X/nlHDD6Jd9oWse2jIDBVor6fMnNDtgKl9azKgyakxoOGB7BMcb5u0Im8vFBCCRIyN3lrYjjR1F3H1tvY6Q0fEGLkilO334IyjC63he6lZ6NqslE/3QWEyyIiCL52rMzadN2SwVNawCa8YIR6+TpBjKyqY17LCP57v3UyM6J/kcUqXxDRcg1XnsjiWU+u0j9ZdlBgcbNuQeb1jD2QgICcyr/tWyJ2asyVfvARcD/xt5a9AnGjO0LnwMfw/DdBz1XCxz5uf3gOM69+nXk2gWhAoIBAQDr7NhlmVrASpOJHXXvqkpC2P4+hx7bmwKUZPbqm32pqCBypn6Qd2h4wdFzcP41wN6kpYqmmsPBoezctSgromfHeVjTyjhGku8b1HqtyRoX5sqIIPtv5pKKGS/tVWfyqQ8BspcdhZaR7+ZiRsLRwOXCscRq82+vbyq5Jd1bjsDGeLtcYyASv3II1xTBzSgNlvB+WiFXIlGWT9IPXwhv6IxZn7ME/y992d7bhgPxCcdTfKQNPBpcKerjcNxeDMto8vVijBDqujVpTf+3YvOOzWrcLn5UORFzpVho7oml5+5qnkFI/RZoiagUixFeQMv5+72qKJrxJu3VfI3mxlzZm5YjAoIBAQDhwjvzNWCQ2y7wwwnr88snVxAhd7kByvbKmnFVzHFTy2lheyWkiAqXj9juqsCtbkOK8a1ogmFAHz3i0/n+QhcJ20gudryniMt+u+wKxpmEKyqHKX3d4biPOvkKx7tdfwnlRKpSWXuynlDNVaQnJKUqBqDygLaE2s0LK3Fwdl+HN5ZPjRcuHkNpXA8t5lbm3tttMIs3JMneKAq77rodgRg+JcYhUNiybek3cZQcEiIGoh8UU6AhgQIOyMy5lkdG7XwZ2FEMQlqZo+T4HnkdTMU1CbTav1/ZzwDQP4/BJvKXhdRBuYHHAwhV6NIEMk5fzXcOoYmhfOMjvftrSxqUOImxAoIBAQDrhaEuJB8d0hVhD6EZ5kWGYHvHzjp2/1Ne80AwS5Pyl5309tNow1vvGYZQGaAd53Icqgo1clE0b8M3Pj5g+RtjXnfXzovJoIvFm6Pw887xx3uu1EZOmr710FkxNE62SCFsD26ekSsUe4rh10RMA6cbaz3riySW3YKoHO3Tpjo6qHJas7ZkIOzleFoHcximIGXrrWyVQPRz+zF4GOYiWeQq4KvltB8kIylAu5QZwCpV5Rsc/0BNe6c68QN9fIZgOhPQEoYc3lHN04kR+V2t1NH2BxAkYmhSq+ELt/6AOn6fv2brR4VkTPAXuhFXp5Y59B+OzESJs9RAiLxcgvBUaOdDAoIBAQCzlPJjUL5z/Cam1j76NoAP1y25saa1SmJuX9Rvz6UGZvR42qDi9GSYk5CYqbODQgbwa7bpP21kuHVeDgj6vE/fQ1NzwnfnPOXC9nGZUMmlXUEDK3o4GenZ5atda+wbP4b7nVdvEkdXmp/j9pARoxDPEV7OCJ0nqXUZwYEHWOI8iXdD6JPb168ADH72oBfYpsYdYVQclWMPGQMQ46Gg/qPuK9YjglAd/1hZBjwu6C2w4R2f6bWjcR/V6t0Pc/9W6GqjlHNEMTQoqzrkNDlbmUn2GraGm1z/wa5/+U+88eJfrdFeRtZ5HGxxCjalp+64PpTKSq1UjCeSsvlgK+oEpcTBAoIBAQCDDcS69BnjFWNa68FBrA2Uw6acQ6lnpXALohXCRL5qOTMe/FFDEBo0ADGrGpSo+cPaE2buNsYO79CafqTxPoZ38OAtTVmX3uL3z9+2ne2dc486gmAnKdJA8w9uawqMEkVpTA9f4WiBJJVzPwAv19AJCPKfUaB8IdNPV+HL8CdK+Dm+lZBADlB9RyvkJRLVJUAuK8/h9kbS3myKI6FIBeFFJpXRONkBSEkANknMqelvdf0GQsHliRslqIK2QVTIOmkJKecG35OhZ5WtU54oSxljlvmtvEKkEJAhEUyfFQRwQTTsDxkFFsfIVr9gv8K1RVEb4D00GUY7GSyAgPKPNsib','MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0A4q7QGo060nFwlIF67DgYEPvsviFHMs18FBhGAZTPZAQpnHZW7ec7mEPKTr6DkM+ggFako2DW524NaYd+2X7/7cxlINleXKQeQXxMrdimNJgRTW8GtCypwgGJQZWq5uF2RfqGdJmh+nHW6B3R4hKjUbHMpZ12ANsxylN2KBHzkfvWnJeM+eDwMrj46/UllD0wMmH99uotKvodyeJ0+hcARSvsikgtT3lRv74mENKLVNVFA5EN6V/NanyXzf+iIe1uL+9y4FJWbEwTM5bv5CczLj+VnOSJFOjLfvBBzGXi5zdGRK5DJNAwkBNeIRvFu0vP4p1CacCZZm+ROt+GavLtCNc1VJkpgBqr76iiQRp6mOV/49m7APAdqhbK3MWrCJZCmli72PgL1w+V9Dh8CGc0W7rgrYLks+u9QuwsKJPEdzfnvQEwx5hvYAtdDSWgenVAQPadlHQPNk0e98epxnvKSyZxsnnBEH8c9fqbT1QBDY9UZd1q8Lv3Wz3f1+op5WKbsJbJ1oty2K5ILk//Vt2drrquT01U9OM3Mnr/udIFZwz806zQmXx6ZjDUCKmRhZhSs+JvlVGZs2TnYNVcedbffre4hxh6hZOv756qloshLkxBQGgvZK5tVTcxr56OS8jZuQ5PSRgBxX44DVfvr4hk6Ujy3hHjJ5bz7kCcuTiTMCAwEAAQ==','samlsp-keypair',NULL),(2,'SAMLSP_X509CERT','rO0ABXNyAC1qYXZhLnNlY3VyaXR5LmNlcnQuQ2VydGlmaWNhdGUkQ2VydGlmaWNhdGVSZXCJJ2qdya48DAIAAlsABGRhdGF0AAJbQkwABHR5cGV0ABJMamF2YS9sYW5nL1N0cmluZzt4cHVyAAJbQqzzF/gGCFTgAgAAeHAAAASzMIIErzCCApcCBgFNmkdlAzANBgkqhkiG9w0BAQsFADAbMRkwFwYDVQQDExBBcGFjaGVDbG91ZFN0YWNrMB4XDTE1MDUyNzExMjc1OVoXDTE4MDUyODExMjc1OVowGzEZMBcGA1UEAxMQQXBhY2hlQ2xvdWRTdGFjazCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANAOKu0BqNOtJxcJSBeuw4GBD77L4hRzLNfBQYRgGUz2QEKZx2Vu3nO5hDyk6+g5DPoIBWpKNg1uduDWmHftl+/+3MZSDZXlykHkF8TK3YpjSYEU1vBrQsqcIBiUGVqubhdkX6hnSZofpx1ugd0eISo1GxzKWddgDbMcpTdigR85H71pyXjPng8DK4+Ov1JZQ9MDJh/fbqLSr6HcnidPoXAEUr7IpILU95Ub++JhDSi1TVRQORDelfzWp8l83/oiHtbi/vcuBSVmxMEzOW7+QnMy4/lZzkiRToy37wQcxl4uc3RkSuQyTQMJATXiEbxbtLz+KdQmnAmWZvkTrfhmry7QjXNVSZKYAaq++ookEaepjlf+PZuwDwHaoWytzFqwiWQppYu9j4C9cPlfQ4fAhnNFu64K2C5LPrvULsLCiTxHc3570BMMeYb2ALXQ0loHp1QED2nZR0DzZNHvfHqcZ7yksmcbJ5wRB/HPX6m09UAQ2PVGXdavC791s939fqKeVim7CWydaLctiuSC5P/1bdna66rk9NVPTjNzJ6/7nSBWcM/NOs0Jl8emYw1AipkYWYUrPib5VRmbNk52DVXHnW3363uIcYeoWTr++eqpaLIS5MQUBoL2SubVU3Ma+ejkvI2bkOT0kYAcV+OA1X76+IZOlI8t4R4yeW8+5AnLk4kzAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAHZWSGpypDmQLQWr2FCVQUnulbPuMMJ0sCH0rNLGLe8qNbZ0YeAuWFsg7+0kVGZ4OuDgioIhD0h3Q3huZtF/WF81eyZqPyVfkXG8egjK58AzMDPHZECeoSVGUCZuq3wjmbnT2sLLDvr8RrzMbbCEvkrYHWivQ18Lbd3eWYYnDbXZRy9GuSWrA9cMqXVYjSTxam9Kel33BIF6CAlMQN5o11oiAv+ciNoxHqGh+8xX3kFKP+x+SRt40NOEs537lEpj/6KdLvd/bP6J4K94jAX3lsdg6zDaBiQWl7P3t50AKtP384Qsb/33uXcbTyw/TkzvPcbmsgTbEUTZIOv44CxMstFrUCyT7ptrzLvDk7Iy2cMgWghULgDvKT3esPE9pleyHG8bkjGt9ypDF/Lmp7j/kILYbF7eq1wIbHOSam4p8WyddVsW4nesu6fqLiCGXum9paChIfvL3To/VHFFKduhJd0Y7LMgWO7pXxWh7XfgRmzQaEN1eJmj5315HEYTS2wXWjptwYDrhiobKuCbpADfOQks8xNKJFLMnXp+IvAqz+ZjkNOz60MLuQ3hvKLTo6nQcTYTfZZxo3Aap30/hA2GtxxSXK/xpBDm58jcVoudgCdxML/OqERBfcADBLvIw5h9+DlXjPUg25IefU0oA336YtnzftJ6cfQfatrc0tBqNEeXdAAFWC41MDk=','','samlsp-x509cert',NULL); +/*!40000 ALTER TABLE `keystore` ENABLE KEYS */; +UNLOCK TABLES; diff --git a/developer/pom.xml b/developer/pom.xml index d6dc6a1de54..ef38e8a992f 100644 --- a/developer/pom.xml +++ b/developer/pom.xml @@ -157,6 +157,64 @@ + + + deploydb-saml + + + deploydb-saml + + + + + + org.codehaus.mojo + exec-maven-plugin + + + mysql + mysql-connector-java + ${cs.mysql.version} + + + 1.2.1 + + + process-resources + create-schema-simulator + + java + + + + + com.cloud.upgrade.DatabaseCreator + true + + + ${basedir}/../utils/conf/db.properties + ${basedir}/../utils/conf/db.properties.override + + ${basedir}/developer-saml.sql + + com.cloud.upgrade.DatabaseUpgradeChecker + --rootpassword=${db.root.password} + + + + catalina.home + ${basedir}/../utils + + + paths.script + ${basedir}/target/db + + + + + + + deploydb-simulator diff --git a/engine/schema/src/com/cloud/user/UserAccountVO.java b/engine/schema/src/com/cloud/user/UserAccountVO.java index 5f33c47e649..80ee873f3e5 100644 --- a/engine/schema/src/com/cloud/user/UserAccountVO.java +++ b/engine/schema/src/com/cloud/user/UserAccountVO.java @@ -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; + } } diff --git a/engine/schema/src/com/cloud/user/UserVO.java b/engine/schema/src/com/cloud/user/UserVO.java index eb2813bf387..da7811ecc5b 100644 --- a/engine/schema/src/com/cloud/user/UserVO.java +++ b/engine/schema/src/com/cloud/user/UserVO.java @@ -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; + } } diff --git a/engine/schema/src/com/cloud/user/dao/UserAccountDao.java b/engine/schema/src/com/cloud/user/dao/UserAccountDao.java index a26ff7f85f9..1d005b2090f 100644 --- a/engine/schema/src/com/cloud/user/dao/UserAccountDao.java +++ b/engine/schema/src/com/cloud/user/dao/UserAccountDao.java @@ -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 { + List getAllUsersByNameAndEntity(String username, String entity); + UserAccount getUserAccount(String username, Long domainId); boolean validateUsernameInDomain(String username, Long domainId); diff --git a/engine/schema/src/com/cloud/user/dao/UserAccountDaoImpl.java b/engine/schema/src/com/cloud/user/dao/UserAccountDaoImpl.java index 1449e6b82ab..a8d9e39a63c 100644 --- a/engine/schema/src/com/cloud/user/dao/UserAccountDaoImpl.java +++ b/engine/schema/src/com/cloud/user/dao/UserAccountDaoImpl.java @@ -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 impl userAccountSearch.done(); } + @Override + public List getAllUsersByNameAndEntity(String username, String entity) { + if (username == null) { + return null; + } + SearchCriteria 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)) { diff --git a/plugins/user-authenticators/saml2/pom.xml b/plugins/user-authenticators/saml2/pom.xml index 834d5f085b2..6c6f5cad2ea 100644 --- a/plugins/user-authenticators/saml2/pom.xml +++ b/plugins/user-authenticators/saml2/pom.xml @@ -47,5 +47,10 @@ cloud-api ${project.version} + + org.apache.cloudstack + cloud-framework-config + ${project.version} + diff --git a/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml b/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml index 92f89b8dfbc..d3a21947a21 100644 --- a/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml +++ b/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml @@ -33,4 +33,7 @@ + + + diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/AuthorizeSAMLSSOCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/AuthorizeSAMLSSOCmd.java new file mode 100644 index 00000000000..54ce418b2cb --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/AuthorizeSAMLSSOCmd.java @@ -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); + } +} \ No newline at end of file diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java index 721b18e1d9e..466a3ddc533 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java @@ -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(); diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java new file mode 100644 index 00000000000..7d7c95ef599 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java @@ -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 params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException { + auditTrailSb.append("=== SAML List IdPs ==="); + ListResponse response = new ListResponse(); + List idpResponseList = new ArrayList(); + 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 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"); + } + } +} \ No newline at end of file diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListSamlAuthorizationCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListSamlAuthorizationCmd.java new file mode 100644 index 00000000000..be958a16280 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListSamlAuthorizationCmd.java @@ -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 users = new ArrayList(); + 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 response = new ListResponse(); + List authorizationResponses = new ArrayList(); + 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); + } +} \ No newline at end of file diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java index dda5876213e..26a4c9b4b58 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java @@ -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 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 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 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 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)); } diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java index 17a0a30ca53..817e62c3f35 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java @@ -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()); diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/IdpResponse.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/IdpResponse.java new file mode 100644 index 00000000000..d95cc3349c9 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/IdpResponse.java @@ -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; + } +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlAuthorizationResponse.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlAuthorizationResponse.java new file mode 100644 index 00000000000..445ee887af7 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlAuthorizationResponse.java @@ -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; + } +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java index 9c0d4b42fc6..fc9a6db4d81 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java @@ -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 SAMLIsPluginEnabled = new ConfigKey("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 SAMLServiceProviderID = new ConfigKey("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 SAMLServiceProviderContactPersonName = new ConfigKey("Advanced", String.class, "saml2.sp.contact.person", "CloudStack Developers", + "SAML2 Service Provider Contact Person Name", true); + + public static final ConfigKey SAMLServiceProviderContactEmail = new ConfigKey("Advanced", String.class, "saml2.sp.contact.email", "dev@cloudstack.apache.org", + "SAML2 Service Provider Contact Email Address", true); + + public static final ConfigKey SAMLServiceProviderOrgName = new ConfigKey("Advanced", String.class, "saml2.sp.org.name", "Apache CloudStack", + "SAML2 Service Provider Organization Name", true); + + public static final ConfigKey SAMLServiceProviderOrgUrl = new ConfigKey("Advanced", String.class, "saml2.sp.org.url", "http://cloudstack.apache.org", + "SAML2 Service Provider Organization URL", true); + + public static final ConfigKey SAMLServiceProviderSingleSignOnURL = new ConfigKey("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 SAMLServiceProviderSingleLogOutURL = new ConfigKey("Advanced", String.class, "saml2.sp.slo.url", "http://localhost:8080/client/", + "SAML2 CloudStack Service Provider Single Log Out URL", true); + + public static final ConfigKey SAMLCloudStackRedirectionUrl = new ConfigKey("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 SAMLUserAttributeName = new ConfigKey("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 SAMLIdentityProviderMetadataURL = new ConfigKey("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 SAMLDefaultIdentityProviderId = new ConfigKey("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 SAMLSignatureAlgorithm = new ConfigKey("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 SAMLTimeout = new ConfigKey("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 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(); } diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java index 36c9da5e1e0..185955c3384 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java @@ -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 _idpMetadataMap = new HashMap(); - 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 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 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 metadataMap = new HashMap(); + 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 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> getAuthCommands() { List> cmdList = new ArrayList>(); @@ -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> getCommands() { + List> cmdList = new ArrayList>(); + 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}; } } diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java index 68bd81c18ff..5c8a39088ae 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java @@ -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(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(false, null); + } + if (!responseObject.getStatus().getStatusCode().getValue().equals(StatusCode.SUCCESS_URI)) { + return new Pair(false, null); + } return new Pair(true, null); } } diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLPluginConstants.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLPluginConstants.java new file mode 100644 index 00000000000..5f806e2f69e --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLPluginConstants.java @@ -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"; +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLProviderMetadata.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLProviderMetadata.java new file mode 100644 index 00000000000..c7138a1d790 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLProviderMetadata.java @@ -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; + } +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDao.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDao.java new file mode 100644 index 00000000000..b045562009d --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDao.java @@ -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 { + public void expireTokens(); +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDaoImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDaoImpl.java new file mode 100644 index 00000000000..eb106d97ad0 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDaoImpl.java @@ -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 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); + } + } +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenVO.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenVO.java new file mode 100644 index 00000000000..c8ac2f1f437 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenVO.java @@ -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; + } +} diff --git a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java similarity index 72% rename from utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java rename to plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java index a6d2d347be4..0216ad7eb63 100644 --- a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java @@ -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 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 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); diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java similarity index 79% rename from plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java rename to plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java index e53e70193c0..db1323fa6df 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java @@ -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 diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAML2UserAuthenticatorTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAML2UserAuthenticatorTest.java index 83792c64d0f..5b373885603 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAML2UserAuthenticatorTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAML2UserAuthenticatorTest.java @@ -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); diff --git a/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java similarity index 66% rename from utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java rename to plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java index bebfd130441..bd87831913c 100644 --- a/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java @@ -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 diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java index 8fbed414363..6960b3bb502 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java @@ -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 params = new HashMap(); @@ -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 diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java index 4388b886c22..cbfcc55c540 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java @@ -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()); diff --git a/server/src/com/cloud/api/ApiServer.java b/server/src/com/cloud/api/ApiServer.java index cf719c07297..4da8b1e1be8 100644 --- a/server/src/com/cloud/api/ApiServer.java +++ b/server/src/com/cloud/api/ApiServer.java @@ -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); } diff --git a/server/src/com/cloud/api/ApiServlet.java b/server/src/com/cloud/api/ApiServlet.java index 3b3a0be7c46..2a2844ef015 100644 --- a/server/src/com/cloud/api/ApiServlet.java +++ b/server/src/com/cloud/api/ApiServlet.java @@ -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 { diff --git a/server/src/com/cloud/configuration/Config.java b/server/src/com/cloud/configuration/Config.java index 2352313e7bd..ca898819f6e 100644 --- a/server/src/com/cloud/configuration/Config.java +++ b/server/src/com/cloud/configuration/Config.java @@ -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), diff --git a/setup/db/db/schema-451to452-cleanup.sql b/setup/db/db/schema-451to452-cleanup.sql new file mode 100644 index 00000000000..9f5e62a3674 --- /dev/null +++ b/setup/db/db/schema-451to452-cleanup.sql @@ -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; +--; diff --git a/setup/db/db/schema-451to452.sql b/setup/db/db/schema-451to452.sql new file mode 100644 index 00000000000..5c89008a83e --- /dev/null +++ b/setup/db/db/schema-451to452.sql @@ -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; diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index e53d69d1570..cb26e2b23b9 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -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', diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index a4e2a2af5c5..2bcd5e5a158 100644 --- a/ui/css/cloudstack3.css +++ b/ui/css/cloudstack3.css @@ -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) { diff --git a/ui/dictionary.jsp b/ui/dictionary.jsp index 63d22bd7c17..7d172670f73 100644 --- a/ui/dictionary.jsp +++ b/ui/dictionary.jsp @@ -143,6 +143,7 @@ dictionary = { 'label.action.cancel.maintenance.mode': '', 'label.action.cancel.maintenance.mode.processing': '', 'label.action.change.password': '', +'label.action.configure.samlauthorization': '', 'label.action.change.service': '', 'label.action.change.service.processing': '', 'label.action.copy.ISO': '', @@ -764,7 +765,9 @@ dictionary = { 'label.local.storage': '', 'label.login': '', 'label.logout': '', -'label.saml.login': '', +'label.saml.enable': '', +'label.saml.entity': '', +'label.add.LDAP.account': '', 'label.lun': '', 'label.LUN.number': '', 'label.make.project.owner': '', diff --git a/ui/index.jsp b/ui/index.jsp index 19c2dbf0490..e062799618b 100644 --- a/ui/index.jsp +++ b/ui/index.jsp @@ -51,28 +51,45 @@
- -
- - +
+
- -
- - + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
- -
- - + +
+
+ + +
+
+ +
+ + " />
- - " /> -
"/>