Return-Path: X-Original-To: apmail-cloudstack-commits-archive@www.apache.org Delivered-To: apmail-cloudstack-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 6EA2A18983 for ; Mon, 29 Jun 2015 10:17:52 +0000 (UTC) Received: (qmail 30563 invoked by uid 500); 29 Jun 2015 10:17:52 -0000 Delivered-To: apmail-cloudstack-commits-archive@cloudstack.apache.org Received: (qmail 30414 invoked by uid 500); 29 Jun 2015 10:17:52 -0000 Mailing-List: contact commits-help@cloudstack.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@cloudstack.apache.org Delivered-To: mailing list commits@cloudstack.apache.org Received: (qmail 30389 invoked by uid 99); 29 Jun 2015 10:17:52 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Mon, 29 Jun 2015 10:17:52 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id 0821BDFFF0; Mon, 29 Jun 2015 10:17:52 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: bhaisaab@apache.org To: commits@cloudstack.apache.org Date: Mon, 29 Jun 2015 10:17:53 -0000 Message-Id: <45f7d3f6421242988b7a8928cbc08341@git.apache.org> In-Reply-To: <2bafd1596e3747cc96bfc6ea3fcfe849@git.apache.org> References: <2bafd1596e3747cc96bfc6ea3fcfe849@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [3/3] git commit: updated refs/heads/saml-pp-squashed to 5ba6b13 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 Signed-off-by: Rohit Yadav Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/5ba6b13d Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/5ba6b13d Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/5ba6b13d Branch: refs/heads/saml-pp-squashed Commit: 5ba6b13dd90c87681c336901475b6c6a83102b04 Parents: a8959bc Author: Rohit Yadav Authored: Thu May 28 14:50:12 2015 +0200 Committer: Rohit Yadav Committed: Mon Jun 29 12:17:32 2015 +0200 ---------------------------------------------------------------------- api/src/com/cloud/user/User.java | 7 +- api/src/com/cloud/user/UserAccount.java | 4 + .../org/apache/cloudstack/api/ApiConstants.java | 3 +- .../classes/resources/messages.properties | 6 +- client/tomcatconf/commands.properties.in | 3 + developer/developer-prefill.sql | 5 - developer/developer-saml.sql | 63 +++ developer/pom.xml | 58 +++ .../com/cloud/upgrade/dao/Upgrade451to452.java | 19 +- .../src/com/cloud/user/UserAccountVO.java | 11 + engine/schema/src/com/cloud/user/UserVO.java | 10 + .../src/com/cloud/user/dao/UserAccountDao.java | 4 + .../com/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 +++++ .../command/GetServiceProviderMetaDataCmd.java | 82 +++- .../cloudstack/api/command/ListIdpsCmd.java | 114 +++++ .../api/command/ListSamlAuthorizationCmd.java | 95 ++++ .../command/SAML2LoginAPIAuthenticatorCmd.java | 285 ++++++------ .../command/SAML2LogoutAPIAuthenticatorCmd.java | 29 +- .../cloudstack/api/response/IdpResponse.java | 62 +++ .../api/response/SamlAuthorizationResponse.java | 68 +++ .../cloudstack/saml/SAML2AuthManager.java | 67 ++- .../cloudstack/saml/SAML2AuthManagerImpl.java | 430 +++++++++++++++---- .../cloudstack/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 +++ .../org/apache/cloudstack/saml/SAMLTokenVO.java | 97 +++++ .../org/apache/cloudstack/saml/SAMLUtils.java | 354 +++++++++++++++ .../GetServiceProviderMetaDataCmdTest.java | 102 +++++ .../cloudstack/SAML2UserAuthenticatorTest.java | 8 +- .../org/apache/cloudstack/SAMLUtilsTest.java | 74 ++++ .../GetServiceProviderMetaDataCmdTest.java | 98 ----- .../SAML2LoginAPIAuthenticatorCmdTest.java | 45 +- .../SAML2LogoutAPIAuthenticatorCmdTest.java | 15 +- server/src/com/cloud/api/ApiServer.java | 4 +- server/src/com/cloud/api/ApiServlet.java | 2 +- server/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 | 20 +- ui/dictionary.jsp | 4 + 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 | 133 ++++-- .../apache/cloudstack/utils/auth/SAMLUtils.java | 330 -------------- .../cloudstack/utils/auth/SAMLUtilsTest.java | 91 ---- 56 files changed, 2594 insertions(+), 974 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/api/src/com/cloud/user/User.java ---------------------------------------------------------------------- diff --git a/api/src/com/cloud/user/User.java b/api/src/com/cloud/user/User.java index 33d6235..1f0dcfd 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; @@ -83,4 +83,9 @@ public interface User extends OwnedBy, InternalIdentity { public Source getSource(); + void setSource(Source source); + + public String getExternalEntity(); + + public void setExternalEntity(String entity); } http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/api/src/com/cloud/user/UserAccount.java ---------------------------------------------------------------------- diff --git a/api/src/com/cloud/user/UserAccount.java b/api/src/com/cloud/user/UserAccount.java index d44fcf7..0449514 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); } http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/api/src/org/apache/cloudstack/api/ApiConstants.java ---------------------------------------------------------------------- diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index b6aed6f..2471e08 100755 --- a/api/src/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/org/apache/cloudstack/api/ApiConstants.java @@ -373,6 +373,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"; @@ -515,7 +516,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"; http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/client/WEB-INF/classes/resources/messages.properties ---------------------------------------------------------------------- diff --git a/client/WEB-INF/classes/resources/messages.properties b/client/WEB-INF/classes/resources/messages.properties index 523c7cc..805f960 100644 --- a/client/WEB-INF/classes/resources/messages.properties +++ b/client/WEB-INF/classes/resources/messages.properties @@ -112,6 +112,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.... @@ -757,7 +758,10 @@ label.local.storage=Local Storage label.local=Local label.login=Login label.logout=Logout -label.saml.login=SAML Login +label.saml.login=CAFe Single Sign On +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 http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/client/tomcatconf/commands.properties.in ---------------------------------------------------------------------- diff --git a/client/tomcatconf/commands.properties.in b/client/tomcatconf/commands.properties.in index a87d167..a66a3dc 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 http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/developer/developer-prefill.sql ---------------------------------------------------------------------- diff --git a/developer/developer-prefill.sql b/developer/developer-prefill.sql index 27b36e7..3097203 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; http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/developer/developer-saml.sql ---------------------------------------------------------------------- diff --git a/developer/developer-saml.sql b/developer/developer-saml.sql new file mode 100644 index 0000000..18afb288 --- /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','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','samlsp-keypair',NULL),(2,'S AMLSP_X509CERT','rO0ABXNyAC1qYXZhLnNlY3VyaXR5LmNlcnQuQ2VydGlmaWNhdGUkQ2VydGlmaWNhdGVSZXCJJ2qdya48DAIAAlsABGRhdGF0AAJbQkwABHR5cGV0ABJMamF2YS9sYW5nL1N0cmluZzt4cHVyAAJbQqzzF/gGCFTgAgAAeHAAAASzMIIErzCCApcCBgFNmkdlAzANBgkqhkiG9w0BAQsFADAbMRkwFwYDVQQDExBBcGFjaGVDbG91ZFN0YWNrMB4XDTE1MDUyNzExMjc1OVoXDTE4MDUyODExMjc1OVowGzEZMBcGA1UEAxMQQXBhY2hlQ2xvdWRTdGFjazCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANAOKu0BqNOtJxcJSBeuw4GBD77L4hRzLNfBQYRgGUz2QEKZx2Vu3nO5hDyk6+g5DPoIBWpKNg1uduDWmHftl+/+3MZSDZXlykHkF8TK3YpjSYEU1vBrQsqcIBiUGVqubhdkX6hnSZofpx1ugd0eISo1GxzKWddgDbMcpTdigR85H71pyXjPng8DK4+Ov1JZQ9MDJh/fbqLSr6HcnidPoXAEUr7IpILU95Ub++JhDSi1TVRQORDelfzWp8l83/oiHtbi/vcuBSVmxMEzOW7+QnMy4/lZzkiRToy37wQcxl4uc3RkSuQyTQMJATXiEbxbtLz+KdQmnAmWZvkTrfhmry7QjXNVSZKYAaq++ookEaepjlf+PZuwDwHaoWytzFqwiWQppYu9j4C9cPlfQ4fAhnNFu64K2C5LPrvULsLCiTxHc3570BMMeYb2ALXQ0loHp1QED2nZR0DzZNHvfHqcZ7yksmcbJ5wRB/HPX6m09UAQ2PVGXdavC791s939fqKeVim7CWydaLctiuSC5P/1bdna66rk9NVPTjNzJ6/7nSBWcM/NOs0Jl8emYw1AipkYWYUrPib5VRmbNk52DVXHnW3363uI cYeoWTr++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; http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/developer/pom.xml ---------------------------------------------------------------------- diff --git a/developer/pom.xml b/developer/pom.xml index e39820b..6717a62 100644 --- a/developer/pom.xml +++ b/developer/pom.xml @@ -173,6 +173,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 http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/engine/schema/src/com/cloud/upgrade/dao/Upgrade451to452.java ---------------------------------------------------------------------- diff --git a/engine/schema/src/com/cloud/upgrade/dao/Upgrade451to452.java b/engine/schema/src/com/cloud/upgrade/dao/Upgrade451to452.java index 3b7b643..870e534 100644 --- a/engine/schema/src/com/cloud/upgrade/dao/Upgrade451to452.java +++ b/engine/schema/src/com/cloud/upgrade/dao/Upgrade451to452.java @@ -17,11 +17,13 @@ package com.cloud.upgrade.dao; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.Script; +import org.apache.log4j.Logger; + import java.io.File; import java.sql.Connection; -import org.apache.log4j.Logger; - public class Upgrade451to452 implements DbUpgrade { final static Logger s_logger = Logger.getLogger(Upgrade451to452.class); @@ -42,7 +44,11 @@ public class Upgrade451to452 implements DbUpgrade { @Override public File[] getPrepareScripts() { - return new File[] {}; + String script = Script.findScript("", "db/schema-451to452.sql"); + if (script == null) { + throw new CloudRuntimeException("Unable to find db/schema-451to452.sql"); + } + return new File[] {new File(script)}; } @Override @@ -51,6 +57,11 @@ public class Upgrade451to452 implements DbUpgrade { @Override public File[] getCleanupScripts() { - return null; + String script = Script.findScript("", "db/schema-451to452-cleanup.sql"); + if (script == null) { + throw new CloudRuntimeException("Unable to find db/schema-451to452-cleanup.sql"); + } + + return new File[] {new File(script)}; } } http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/engine/schema/src/com/cloud/user/UserAccountVO.java ---------------------------------------------------------------------- diff --git a/engine/schema/src/com/cloud/user/UserAccountVO.java b/engine/schema/src/com/cloud/user/UserAccountVO.java index 5f33c47..80ee873 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; + } } http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/engine/schema/src/com/cloud/user/UserVO.java ---------------------------------------------------------------------- diff --git a/engine/schema/src/com/cloud/user/UserVO.java b/engine/schema/src/com/cloud/user/UserVO.java index eb2813b..da7811e 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; + } } http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/engine/schema/src/com/cloud/user/dao/UserAccountDao.java ---------------------------------------------------------------------- diff --git a/engine/schema/src/com/cloud/user/dao/UserAccountDao.java b/engine/schema/src/com/cloud/user/dao/UserAccountDao.java index a26ff7f..1d005b2 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); http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/engine/schema/src/com/cloud/user/dao/UserAccountDaoImpl.java ---------------------------------------------------------------------- diff --git a/engine/schema/src/com/cloud/user/dao/UserAccountDaoImpl.java b/engine/schema/src/com/cloud/user/dao/UserAccountDaoImpl.java index 1449e6b..a8d9e39 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}) @@ -39,6 +39,17 @@ public class UserAccountDaoImpl extends GenericDaoBase impl } @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)) { return null; http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/plugins/user-authenticators/saml2/pom.xml ---------------------------------------------------------------------- diff --git a/plugins/user-authenticators/saml2/pom.xml b/plugins/user-authenticators/saml2/pom.xml index fed1a54..c83b190 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} + http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml ---------------------------------------------------------------------- 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 92f89b8..d3a2194 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 @@ + + + http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/AuthorizeSAMLSSOCmd.java ---------------------------------------------------------------------- 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 0000000..54ce418 --- /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 http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java ---------------------------------------------------------------------- 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 e730836..3a52151 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; @APICommand(name = "getSPMetadata", description = "Returns SAML2 CloudStack Service Provider MetaData", responseObject = SAMLMetaDataResponse.class, entityType = {}) @@ -118,8 +133,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); @@ -129,19 +146,23 @@ public class GetServiceProviderMetaDataCmd extends BaseCmd implements APIAuthent keyInfoGeneratorFactory.setEmitEntityCertificate(true); KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance(); + KeyDescriptor signKeyDescriptor = new KeyDescriptorBuilder().buildObject(); + signKeyDescriptor.setUse(UsageType.SIGNING); + KeyDescriptor encKeyDescriptor = new KeyDescriptorBuilder().buildObject(); encKeyDescriptor.setUse(UsageType.ENCRYPTION); - KeyDescriptor signKeyDescriptor = new KeyDescriptorBuilder().buildObject(); - signKeyDescriptor.setUse(UsageType.SIGNING); + BasicX509Credential signingCredential = new BasicX509Credential(); + signingCredential.setEntityCertificate(spMetadata.getSigningCertificate()); + + BasicX509Credential encryptionCredential = new BasicX509Credential(); + encryptionCredential.setEntityCertificate(spMetadata.getEncryptionCertificate()); - BasicX509Credential credential = new BasicX509Credential(); - credential.setEntityCertificate(_samlAuthManager.getSpX509Certificate()); 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()); } @@ -159,19 +180,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(); http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java ---------------------------------------------------------------------- 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 0000000..7d7c95e --- /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 http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListSamlAuthorizationCmd.java ---------------------------------------------------------------------- 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 0000000..be958a1 --- /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 http://git-wip-us.apache.org/repos/asf/cloudstack/blob/5ba6b13d/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java ---------------------------------------------------------------------- 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 a10afb6..b05ebf6 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; @@ -69,12 +72,8 @@ import javax.xml.parsers.ParserConfigurationException; import javax.xml.stream.FactoryConfigurationError; import java.io.IOException; import java.net.URLEncoder; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; 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 { @@ -84,16 +83,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; @@ -104,8 +101,8 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// - public String getIdpUrl() { - return idpUrl; + public String getIdpId() { + return idpId; } ///////////////////////////////////////////////////// @@ -128,30 +125,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 { @@ -167,13 +140,44 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent @Override public String authenticate(final String command, final Map params, final HttpSession session, final String 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]; + } + + 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 redirectUrl = this.buildAuthnRequestUrl(idpUrl); + 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")) { @@ -181,7 +185,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)) { @@ -190,10 +194,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); @@ -204,94 +235,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)); + } + + 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)); + } + } } - 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); + 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) { } @@ -302,7 +345,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)); }