You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by bh...@apache.org on 2015/06/25 19:29:07 UTC
[1/3] git commit: updated refs/heads/saml-pp-squashed to 595efe8
Repository: cloudstack
Updated Branches:
refs/heads/saml-pp-squashed faa02452b -> 595efe88b (forced update)
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmdTest.java
----------------------------------------------------------------------
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 e9834c9..eff4b29 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 {
SAML2AuthManager samlAuthManager;
@Mock
- ConfigurationDao configDao;
-
- @Mock
HttpSession session;
@Mock
@@ -71,19 +67,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, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
Mockito.verify(resp, Mockito.times(1)).sendRedirect(Mockito.anyString());
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/server/src/com/cloud/api/ApiServer.java
----------------------------------------------------------------------
diff --git a/server/src/com/cloud/api/ApiServer.java b/server/src/com/cloud/api/ApiServer.java
index 6dcf48a..2ab1f7c 100755
--- 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);
}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/server/src/com/cloud/api/ApiServlet.java
----------------------------------------------------------------------
diff --git a/server/src/com/cloud/api/ApiServlet.java b/server/src/com/cloud/api/ApiServlet.java
index 8d34dfe..2bffc77 100644
--- a/server/src/com/cloud/api/ApiServlet.java
+++ b/server/src/com/cloud/api/ApiServlet.java
@@ -231,7 +231,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 {
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/server/src/com/cloud/configuration/Config.java
----------------------------------------------------------------------
diff --git a/server/src/com/cloud/configuration/Config.java b/server/src/com/cloud/configuration/Config.java
index fc529b5..0d6a4e6 100755
--- a/server/src/com/cloud/configuration/Config.java
+++ b/server/src/com/cloud/configuration/Config.java
@@ -1362,78 +1362,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),
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/setup/db/db/schema-451to452-cleanup.sql
----------------------------------------------------------------------
diff --git a/setup/db/db/schema-451to452-cleanup.sql b/setup/db/db/schema-451to452-cleanup.sql
new file mode 100644
index 0000000..9f5e62a
--- /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;
+--;
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/setup/db/db/schema-451to452.sql
----------------------------------------------------------------------
diff --git a/setup/db/db/schema-451to452.sql b/setup/db/db/schema-451to452.sql
new file mode 100644
index 0000000..5c89008
--- /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;
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/tools/apidoc/gen_toc.py
----------------------------------------------------------------------
diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py
index 95f06c8..7eca3ef 100644
--- a/tools/apidoc/gen_toc.py
+++ b/tools/apidoc/gen_toc.py
@@ -115,6 +115,8 @@ known_categories = {
'logout': 'Authentication',
'saml': 'Authentication',
'getSPMetadata': 'Authentication',
+ 'listIdps': 'Authentication',
+ 'authorizeSamlSso': 'Authentication',
'Capacity': 'System Capacity',
'NetworkDevice': 'Network Device',
'ExternalLoadBalancer': 'Ext Load Balancer',
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/ui/css/cloudstack3.css
----------------------------------------------------------------------
diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css
index 0a2c57f..999fce2 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;
@@ -13039,3 +13037,4 @@ div.gpugroups div.list-view {
background: transparent url("../images/icons.png") no-repeat -626px -209px;
padding: 0 0 3px 18px;
}
+
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/ui/dictionary.jsp
----------------------------------------------------------------------
diff --git a/ui/dictionary.jsp b/ui/dictionary.jsp
index 317acfa..8481653 100644
--- a/ui/dictionary.jsp
+++ b/ui/dictionary.jsp
@@ -762,6 +762,9 @@ dictionary = {
'label.login': '<fmt:message key="label.login" />',
'label.logout': '<fmt:message key="label.logout" />',
'label.saml.login': '<fmt:message key="label.saml.login" />',
+'label.saml.enable': '<fmt:message key="label.saml.enable" />',
+'label.saml.entity': '<fmt:message key="label.saml.entity" />',
+'label.add.LDAP.account': '<fmt:message key="label.add.LDAP.account" />',
'label.lun': '<fmt:message key="label.lun" />',
'label.LUN.number': '<fmt:message key="label.LUN.number" />',
'label.make.project.owner': '<fmt:message key="label.make.project.owner" />',
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/ui/index.jsp
----------------------------------------------------------------------
diff --git a/ui/index.jsp b/ui/index.jsp
index 4b601a5..0f8ef94 100644
--- a/ui/index.jsp
+++ b/ui/index.jsp
@@ -51,28 +51,44 @@
<form>
<div class="logo"></div>
<div class="fields">
- <!-- User name -->
- <div class="field username">
- <label for="username"><fmt:message key="label.username"/></label>
- <input type="text" name="username" class="required" />
+ <div id="login-dropdown">
+ <select id="login-options" style="width: 260px">
+ <option value="cloudstack-login">Local <fmt:message key="label.login"/></option>
+ </select>
</div>
- <!-- Password -->
- <div class="field password">
- <label for="password"><fmt:message key="label.password"/></label>
- <input type="password" name="password" class="required" autocomplete="off" />
+
+ <div id="saml-login">
+ <div class="field domain">
+ <label for="saml-domain"><fmt:message key="label.domain"/></label>
+ <input id="saml-domain" type="text" name="saml-domain" />
+ <input id="saml-login-submit" type="submit" value="<fmt:message key="label.login"/>" />
+ </div>
</div>
- <!-- Domain -->
- <div class="field domain">
- <label for="domain"><fmt:message key="label.domain"/></label>
- <input type="text" name="domain" />
+
+ <div id="cloudstack-login">
+ <!-- User name -->
+ <div class="field username">
+ <label for="username"><fmt:message key="label.username"/></label>
+ <input type="text" name="username" class="required" />
+ </div>
+ <!-- Password -->
+ <div class="field password">
+ <label for="password"><fmt:message key="label.password"/></label>
+ <input type="password" name="password" class="required" autocomplete="off" />
+ </div>
+ <!-- Domain -->
+ <div class="field domain">
+ <label for="domain"><fmt:message key="label.domain"/></label>
+ <input type="text" name="domain" />
+ </div>
+ <!-- Submit (login) -->
+ <input id="cloudstack-login-submit" type="submit" value="<fmt:message key="label.login"/>" />
</div>
- <!-- Submit (login) -->
- <input type="submit" value="<fmt:message key="label.login"/>" />
- <div id="saml-login"><input type="samlsubmit" value="<fmt:message key="label.saml.login"/>"/></div>
+
<!-- Select language -->
<div class="select-language">
<select name="language">
- <option value=""></option> <!-- when this blank option is selected, browser's default language will be used -->
+ <option value=""></option> <!-- when this blank option is selected, default language of the browser will be used -->
<option value="en"><fmt:message key="label.lang.english"/></option>
<option value="ja_JP"><fmt:message key="label.lang.japanese"/></option>
<option value="zh_CN"><fmt:message key="label.lang.chinese"/></option>
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/ui/scripts/accountsWizard.js
----------------------------------------------------------------------
diff --git a/ui/scripts/accountsWizard.js b/ui/scripts/accountsWizard.js
index 82e7eab..7ea5eaa 100644
--- a/ui/scripts/accountsWizard.js
+++ b/ui/scripts/accountsWizard.js
@@ -162,8 +162,34 @@
validation: {
required: false
}
+ },
+ samlEnable: {
+ label: 'label.saml.enable',
+ docID: 'helpSamlEnable',
+ isBoolean: true,
+ validation: {
+ required: false
+ }
+ },
+ samlEntity: {
+ label: 'label.saml.entity',
+ docID: 'helpSamlEntity',
+ validation: {
+ required: false
+ },
+ select: function(args) {
+ var items = [];
+ $(g_idpList).each(function() {
+ items.push({
+ id: this.id,
+ description: this.orgName
+ });
+ });
+ args.response.success({
+ data: items
+ });
+ }
}
-
},
action: function(args) {
@@ -218,6 +244,18 @@
array1.push("&group=" + args.groupname);
}
+ var authorizeUsersForSamlSSO = function (users, entity) {
+ for (var i = 0; i < users.length; i++) {
+ $.ajax({
+ url: createURL('authorizeSamlSso&enable=true&userid=' + users[i].id + "&entityid=" + entity),
+ error: function(XMLHttpResponse) {
+ args.response.error(parseXMLHttpResponse(XMLHttpResponse));
+ }
+ });
+ }
+ return;
+ };
+
if (ldapStatus) {
if (args.groupname) {
$.ajax({
@@ -225,6 +263,13 @@
dataType: "json",
type: "POST",
async: false,
+ success: function (json) {
+ if (json.ldapuserresponse && args.data.samlEnable && args.data.samlEnable === 'on') {
+ cloudStack.dialog.notice({
+ message: "Unable to find users IDs to enable SAML Single Sign On, kindly enable it manually."
+ });
+ }
+ },
error: function(XMLHttpResponse) {
args.response.error(parseXMLHttpResponse(XMLHttpResponse));
}
@@ -235,6 +280,14 @@
dataType: "json",
type: "POST",
async: false,
+ success: function(json) {
+ if (args.data.samlEnable && args.data.samlEnable === 'on') {
+ var users = json.createaccountresponse.account.user;
+ var entity = args.data.samlEntity;
+ if (users && entity)
+ authorizeUsersForSamlSSO(users, entity);
+ }
+ },
error: function(XMLHttpResponse) {
args.response.error(parseXMLHttpResponse(XMLHttpResponse));
}
@@ -246,6 +299,14 @@
dataType: "json",
type: "POST",
async: false,
+ success: function(json) {
+ if (args.data.samlEnable && args.data.samlEnable === 'on') {
+ var users = json.createaccountresponse.account.user;
+ var entity = args.data.samlEntity;
+ if (users && entity)
+ authorizeUsersForSamlSSO(users, entity);
+ }
+ },
error: function(XMLHttpResponse) {
args.response.error(parseXMLHttpResponse(XMLHttpResponse));
}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/ui/scripts/cloudStack.js
----------------------------------------------------------------------
diff --git a/ui/scripts/cloudStack.js b/ui/scripts/cloudStack.js
index 479b162..a701c41 100644
--- a/ui/scripts/cloudStack.js
+++ b/ui/scripts/cloudStack.js
@@ -115,7 +115,7 @@
cookieValue = cookieValue.slice(1, cookieValue.length-1);
$.cookie(cookieName, cookieValue, { expires: 1 });
}
- return cookieValue;
+ return decodeURIComponent(cookieValue);
};
unBoxCookieValue('sessionkey');
// if sessionkey cookie exists use this to set g_sessionKey
@@ -353,6 +353,17 @@
},
samlLoginAction: function(args) {
+ g_sessionKey = null;
+ g_username = null;
+ g_account = null;
+ g_domainid = null;
+ g_timezoneoffset = null;
+ g_timezone = null;
+ g_supportELB = null;
+ g_kvmsnapshotenabled = null;
+ g_regionsecondaryenabled = null;
+ g_loginCmdText = null;
+
$.cookie('JSESSIONID', null);
$.cookie('sessionkey', null);
$.cookie('username', null);
@@ -360,7 +371,14 @@
$.cookie('domainid', null);
$.cookie('role', null);
$.cookie('timezone', null);
- window.location.href = createURL('samlSso');
+ var url = 'samlSso';
+ if (args.data.idpid) {
+ url = url + '&idpid=' + args.data.idpid;
+ }
+ if (args.data.domain) {
+ url = url + '&domain=' + args.data.domain;
+ }
+ window.location.href = createURL(url);
},
// Show cloudStack main UI widget
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/ui/scripts/docs.js
----------------------------------------------------------------------
diff --git a/ui/scripts/docs.js b/ui/scripts/docs.js
index d38bcf7..ed94ccc 100755
--- a/ui/scripts/docs.js
+++ b/ui/scripts/docs.js
@@ -1247,6 +1247,14 @@ cloudStack.docs = {
desc: 'The group name from which you want to import LDAP users',
externalLink: ''
},
+ helpSamlEnable: {
+ desc: 'Enable SAML Single Sign On for the user(s)',
+ externalLink: ''
+ },
+ helpSamlEntity: {
+ desc: 'Choose the SAML Identity Provider Entity ID with which you want to enable the Single Sign On for the user(s)',
+ externalLink: ''
+ },
helpVpcOfferingName: {
desc: 'Any desired name for the VPC offering',
externalLink: ''
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/ui/scripts/sharedFunctions.js
----------------------------------------------------------------------
diff --git a/ui/scripts/sharedFunctions.js b/ui/scripts/sharedFunctions.js
index 1e1514b..75860dc 100644
--- a/ui/scripts/sharedFunctions.js
+++ b/ui/scripts/sharedFunctions.js
@@ -32,6 +32,7 @@ var g_regionsecondaryenabled = null;
var g_userPublicTemplateEnabled = "true";
var g_cloudstackversion = null;
var g_queryAsyncJobResultInterval = 3000;
+var g_idpList = null;
//keyboard keycode
var keycode_Enter = 13;
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/ui/scripts/ui-custom/accountsWizard.js
----------------------------------------------------------------------
diff --git a/ui/scripts/ui-custom/accountsWizard.js b/ui/scripts/ui-custom/accountsWizard.js
index 3259227..cfbe930 100644
--- a/ui/scripts/ui-custom/accountsWizard.js
+++ b/ui/scripts/ui-custom/accountsWizard.js
@@ -271,6 +271,11 @@
delete args.informationNotInLdap.ldapGroupName;
}
+ if (g_idpList == null) {
+ delete args.informationNotInLdap.samlEnable;
+ delete args.informationNotInLdap.samlEntity;
+ }
+
var informationNotInLdap = cloudStack.dialog.createForm({
context: context,
noDialog: true,
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/ui/scripts/ui-custom/login.js
----------------------------------------------------------------------
diff --git a/ui/scripts/ui-custom/login.js b/ui/scripts/ui-custom/login.js
index 1f82c82..128eac4 100644
--- a/ui/scripts/ui-custom/login.js
+++ b/ui/scripts/ui-custom/login.js
@@ -94,7 +94,7 @@
$inputs.filter(':first').addClass('first-input').focus();
// Login action
- $login.find('input[type=submit]').click(function() {
+ $login.find('#cloudstack-login-submit').click(function() {
if (!$form.valid()) return false;
var data = cloudStack.serializeForm($form);
@@ -121,27 +121,81 @@
});
// SAML Login action
- $login.find('input[type=samlsubmit]').click(function() {
+ $login.find('#saml-login-submit').click(function() {
args.samlLoginAction({
+ data: {'idpid': $login.find('#login-options').find(':selected').val(),
+ 'domain': $login.find('#saml-domain').val()}
});
+ return false;
});
// Show SAML button if only SP is configured
- $login.find("#saml-login").hide();
+ $login.find('#login-dropdown').hide();
+ $login.find('#saml-login').hide();
+ $login.find('#cloudstack-login').hide();
+
+ var toggleLoginView = function (selectedOption) {
+ if (selectedOption === '') {
+ $login.find('#saml-login').hide();
+ $login.find('#cloudstack-login').hide();
+ } else if (selectedOption === 'cloudstack-login') {
+ $login.find('#saml-login').hide();
+ $login.find('#cloudstack-login').show();
+ } else {
+ $login.find('#saml-login').show();
+ $login.find('#cloudstack-login').hide();
+ }
+ };
+
+ $login.find('#login-options').change(function() {
+ var selectedOption = $login.find('#login-options').find(':selected').val();
+ toggleLoginView(selectedOption);
+ if (selectedOption && selectedOption !== '') {
+ $.cookie('login-option', selectedOption);
+ }
+ });
+
$.ajax({
- type: "GET",
- url: createURL("getSPMetadata"),
- dataType: "json",
+ type: 'GET',
+ url: createURL('listIdps'),
+ dataType: 'json',
async: false,
success: function(data, textStatus, xhr) {
if (xhr.status === 200) {
- $login.find('#saml-login').show();
+ $login.find('#login-dropdown').show();
} else {
- $login.find('#saml-login').hide();
+ $login.find('#cloudstack-login').show();
+ return;
+ }
+ var idpList = data.listidpsresponse.idp.sort(function (a, b) {
+ return a.orgName.localeCompare(b.orgName);
+ });
+ g_idpList = idpList;
+ $login.find('#login-options')
+ .append($('<option>', {
+ value: '',
+ text: '--- Select Identity Provider -- ',
+ selected: true
+ }));
+ $.each(idpList, function(index, idp) {
+ $login.find('#login-options')
+ .append($('<option>', {
+ value: idp.id,
+ text: idp.orgName
+ }));
+ });
+
+ var loginOption = $.cookie('login-option');
+ if (loginOption) {
+ var option = $login.find('#login-options option[value="' + loginOption + '"]');
+ if (option.length > 0) {
+ option.prop('selected', true);
+ toggleLoginView(loginOption);
+ }
}
},
error: function(xhr) {
- $login.find('#saml-login').hide();
+ $login.find('#cloudstack-login').show();
},
});
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java
----------------------------------------------------------------------
diff --git a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java b/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java
deleted file mode 100644
index a6d2d34..0000000
--- a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java
+++ /dev/null
@@ -1,330 +0,0 @@
-//
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-//
-
-package org.apache.cloudstack.utils.auth;
-
-import com.cloud.utils.HttpUtils;
-import org.apache.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.common.SAMLVersion;
-import org.opensaml.common.xml.SAMLConstants;
-import org.opensaml.saml2.core.AuthnContextClassRef;
-import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration;
-import org.opensaml.saml2.core.AuthnRequest;
-import org.opensaml.saml2.core.Issuer;
-import org.opensaml.saml2.core.LogoutRequest;
-import org.opensaml.saml2.core.NameID;
-import org.opensaml.saml2.core.NameIDPolicy;
-import org.opensaml.saml2.core.NameIDType;
-import org.opensaml.saml2.core.RequestedAuthnContext;
-import org.opensaml.saml2.core.Response;
-import org.opensaml.saml2.core.SessionIndex;
-import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder;
-import org.opensaml.saml2.core.impl.AuthnRequestBuilder;
-import org.opensaml.saml2.core.impl.IssuerBuilder;
-import org.opensaml.saml2.core.impl.LogoutRequestBuilder;
-import org.opensaml.saml2.core.impl.NameIDBuilder;
-import org.opensaml.saml2.core.impl.NameIDPolicyBuilder;
-import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder;
-import org.opensaml.saml2.core.impl.SessionIndexBuilder;
-import org.opensaml.xml.ConfigurationException;
-import org.opensaml.xml.XMLObject;
-import org.opensaml.xml.io.Marshaller;
-import org.opensaml.xml.io.MarshallingException;
-import org.opensaml.xml.io.Unmarshaller;
-import org.opensaml.xml.io.UnmarshallerFactory;
-import org.opensaml.xml.io.UnmarshallingException;
-import org.opensaml.xml.signature.SignatureConstants;
-import org.opensaml.xml.util.Base64;
-import org.opensaml.xml.util.XMLHelper;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.xml.sax.SAXException;
-
-import javax.security.auth.x500.X500Principal;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.io.UnsupportedEncodingException;
-import java.math.BigInteger;
-import java.net.URLEncoder;
-import java.security.InvalidKeyException;
-import java.security.KeyFactory;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.SecureRandom;
-import java.security.Security;
-import java.security.Signature;
-import java.security.SignatureException;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.X509Certificate;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.PKCS8EncodedKeySpec;
-import java.security.spec.X509EncodedKeySpec;
-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();
- // Issuer object
- IssuerBuilder issuerBuilder = new IssuerBuilder();
- Issuer issuer = issuerBuilder.buildObject();
- issuer.setValue(spId);
-
- // NameIDPolicy
- NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder();
- NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject();
- nameIdPolicy.setFormat(NameIDType.PERSISTENT);
- nameIdPolicy.setSPNameQualifier(spId);
- nameIdPolicy.setAllowCreate(true);
-
- // AuthnContextClass
- AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder();
- AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject(
- SAMLConstants.SAML20_NS,
- "AuthnContextClassRef", "saml");
- authnContextClassRef.setAuthnContextClassRef("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport");
-
- // AuthnContex
- RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder();
- RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject();
- requestedAuthnContext
- .setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
- requestedAuthnContext.getAuthnContextClassRefs().add(
- authnContextClassRef);
-
- // Creation of AuthRequestObject
- AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder();
- AuthnRequest authnRequest = authRequestBuilder.buildObject();
- authnRequest.setID(authnId);
- authnRequest.setDestination(idpUrl);
- authnRequest.setVersion(SAMLVersion.VERSION_20);
- authnRequest.setForceAuthn(false);
- authnRequest.setIsPassive(false);
- authnRequest.setIssuer(issuer);
- authnRequest.setIssueInstant(new DateTime());
- authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
- authnRequest.setAssertionConsumerServiceURL(consumerUrl);
- authnRequest.setProviderName(spId);
- authnRequest.setNameIDPolicy(nameIdPolicy);
- authnRequest.setRequestedAuthnContext(requestedAuthnContext);
-
- return authnRequest;
- }
-
- public static LogoutRequest buildLogoutRequest(String logoutUrl, String spId, NameID sessionNameId, String sessionIndex) {
- IssuerBuilder issuerBuilder = new IssuerBuilder();
- Issuer issuer = issuerBuilder.buildObject();
- issuer.setValue(spId);
-
- SessionIndex sessionIndexElement = new SessionIndexBuilder().buildObject();
- sessionIndexElement.setSessionIndex(sessionIndex);
-
- NameID nameID = new NameIDBuilder().buildObject();
- nameID.setValue(sessionNameId.getValue());
- nameID.setFormat(sessionNameId.getFormat());
-
- LogoutRequest logoutRequest = new LogoutRequestBuilder().buildObject();
- logoutRequest.setID(generateSecureRandomId());
- logoutRequest.setDestination(logoutUrl);
- logoutRequest.setVersion(SAMLVersion.VERSION_20);
- logoutRequest.setIssueInstant(new DateTime());
- logoutRequest.setIssuer(issuer);
- logoutRequest.getSessionIndexes().add(sessionIndexElement);
- logoutRequest.setNameID(nameID);
- return logoutRequest;
- }
-
- public static String encodeSAMLRequest(XMLObject authnRequest)
- throws MarshallingException, IOException {
- Marshaller marshaller = Configuration.getMarshallerFactory()
- .getMarshaller(authnRequest);
- Element authDOM = marshaller.marshall(authnRequest);
- StringWriter requestWriter = new StringWriter();
- XMLHelper.writeNode(authDOM, requestWriter);
- String requestMessage = requestWriter.toString();
- Deflater deflater = new Deflater(Deflater.DEFLATED, true);
- ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
- DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, deflater);
- deflaterOutputStream.write(requestMessage.getBytes());
- deflaterOutputStream.close();
- String encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream.toByteArray(), Base64.DONT_BREAK_LINES);
- encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, HttpUtils.UTF_8).trim();
- return encodedRequestMessage;
- }
-
- public static Response decodeSAMLResponse(String responseMessage)
- throws ConfigurationException, ParserConfigurationException,
- SAXException, IOException, UnmarshallingException {
- DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
- documentBuilderFactory.setNamespaceAware(true);
- DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
- byte[] base64DecodedResponse = Base64.decode(responseMessage);
- Document document = docBuilder.parse(new ByteArrayInputStream(base64DecodedResponse));
- Element element = document.getDocumentElement();
- UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory();
- Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element);
- return (Response) unmarshaller.unmarshall(element);
- }
-
- public static String generateSAMLRequestSignature(String urlEncodedString, PrivateKey signingKey)
- 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");
- signature.initSign(signingKey);
- signature.update(url.getBytes());
- String signatureString = Base64.encodeBytes(signature.sign(), Base64.DONT_BREAK_LINES);
- if (signatureString != null) {
- return url + "&Signature=" + URLEncoder.encode(signatureString, HttpUtils.UTF_8);
- }
- return url;
- }
-
- public static KeyFactory getKeyFactory() {
- KeyFactory keyFactory = null;
- try {
- Security.addProvider(new BouncyCastleProvider());
- keyFactory = KeyFactory.getInstance("RSA", "BC");
- } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
- s_logger.error("Unable to create KeyFactory:" + e.getMessage());
- }
- return keyFactory;
- }
-
- public static String savePublicKey(PublicKey key) {
- try {
- KeyFactory keyFactory = SAMLUtils.getKeyFactory();
- if (keyFactory == null) return null;
- X509EncodedKeySpec spec = keyFactory.getKeySpec(key, X509EncodedKeySpec.class);
- return new String(org.bouncycastle.util.encoders.Base64.encode(spec.getEncoded()));
- } catch (InvalidKeySpecException e) {
- s_logger.error("Unable to create KeyFactory:" + e.getMessage());
- }
- return null;
- }
-
- public static String savePrivateKey(PrivateKey key) {
- try {
- KeyFactory keyFactory = SAMLUtils.getKeyFactory();
- if (keyFactory == null) return null;
- PKCS8EncodedKeySpec spec = keyFactory.getKeySpec(key,
- PKCS8EncodedKeySpec.class);
- return new String(org.bouncycastle.util.encoders.Base64.encode(spec.getEncoded()));
- } catch (InvalidKeySpecException e) {
- s_logger.error("Unable to create KeyFactory:" + e.getMessage());
- }
- return null;
- }
-
- public static PublicKey loadPublicKey(String publicKey) {
- byte[] sigBytes = org.bouncycastle.util.encoders.Base64.decode(publicKey);
- X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(sigBytes);
- KeyFactory keyFact = SAMLUtils.getKeyFactory();
- if (keyFact == null)
- return null;
- try {
- return keyFact.generatePublic(x509KeySpec);
- } catch (InvalidKeySpecException e) {
- s_logger.error("Unable to create PrivateKey from privateKey string:" + e.getMessage());
- }
- return null;
- }
-
- public static PrivateKey loadPrivateKey(String privateKey) {
- byte[] sigBytes = org.bouncycastle.util.encoders.Base64.decode(privateKey);
- PKCS8EncodedKeySpec pkscs8KeySpec = new PKCS8EncodedKeySpec(sigBytes);
- KeyFactory keyFact = SAMLUtils.getKeyFactory();
- if (keyFact == null)
- return null;
- try {
- return keyFact.generatePrivate(pkscs8KeySpec);
- } catch (InvalidKeySpecException e) {
- s_logger.error("Unable to create PrivateKey from privateKey string:" + e.getMessage());
- }
- return null;
- }
-
- public static KeyPair generateRandomKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException {
- Security.addProvider(new BouncyCastleProvider());
- KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
- keyPairGenerator.initialize(4096, new SecureRandom());
- return keyPairGenerator.generateKeyPair();
- }
-
- public static X509Certificate generateRandomX509Certificate(KeyPair keyPair) throws NoSuchAlgorithmException, NoSuchProviderException, CertificateEncodingException, SignatureException, InvalidKeyException {
- DateTime now = DateTime.now(DateTimeZone.UTC);
- X500Principal dnName = new X500Principal("CN=ApacheCloudStack");
- X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
- certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
- certGen.setSubjectDN(dnName);
- certGen.setIssuerDN(dnName);
- certGen.setNotBefore(now.minusDays(1).toDate());
- certGen.setNotAfter(now.plusYears(3).toDate());
- certGen.setPublicKey(keyPair.getPublic());
- certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
- return certGen.generate(keyPair.getPrivate(), "BC");
- }
-
-}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java
----------------------------------------------------------------------
diff --git a/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java b/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java
deleted file mode 100644
index bebfd13..0000000
--- a/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java
+++ /dev/null
@@ -1,91 +0,0 @@
-//
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-//
-
-package org.apache.cloudstack.utils.auth;
-
-import junit.framework.TestCase;
-import org.junit.Test;
-import org.opensaml.saml2.core.AuthnRequest;
-import org.opensaml.saml2.core.LogoutRequest;
-import org.opensaml.saml2.core.NameID;
-import org.opensaml.saml2.core.impl.NameIDBuilder;
-
-import java.security.KeyPair;
-import java.security.PrivateKey;
-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);
- }
-
- @Test
- public void testBuildAuthnRequestObject() throws Exception {
- String consumerUrl = "http://someurl.com";
- String idpUrl = "http://idp.domain.example";
- String spId = "cloudstack";
- AuthnRequest req = SAMLUtils.buildAuthnRequestObject(spId, idpUrl, consumerUrl);
- assertEquals(req.getAssertionConsumerServiceURL(), consumerUrl);
- assertEquals(req.getDestination(), idpUrl);
- assertEquals(req.getIssuer().getValue(), spId);
- }
-
- @Test
- public void testBuildLogoutRequest() throws Exception {
- String logoutUrl = "http://logoutUrl";
- String spId = "cloudstack";
- String sessionIndex = "12345";
- String nameIdString = "someNameID";
- NameID sessionNameId = new NameIDBuilder().buildObject();
- sessionNameId.setValue(nameIdString);
- LogoutRequest req = SAMLUtils.buildLogoutRequest(logoutUrl, spId, sessionNameId, sessionIndex);
- assertEquals(req.getDestination(), logoutUrl);
- assertEquals(req.getIssuer().getValue(), spId);
- assertEquals(req.getNameID().getValue(), nameIdString);
- assertEquals(req.getSessionIndexes().get(0).getSessionIndex(), sessionIndex);
- }
-
- @Test
- public void testX509Helpers() throws Exception {
- KeyPair keyPair = SAMLUtils.generateRandomKeyPair();
-
- String privateKeyString = SAMLUtils.savePrivateKey(keyPair.getPrivate());
- String publicKeyString = SAMLUtils.savePublicKey(keyPair.getPublic());
-
- PrivateKey privateKey = SAMLUtils.loadPrivateKey(privateKeyString);
- PublicKey publicKey = SAMLUtils.loadPublicKey(publicKeyString);
-
- assertTrue(privateKey.equals(keyPair.getPrivate()));
- assertTrue(publicKey.equals(keyPair.getPublic()));
- }
-}
\ No newline at end of file
[2/3] git commit: updated refs/heads/saml-pp-squashed to 595efe8
Posted by bh...@apache.org.
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java
----------------------------------------------------------------------
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 9c0d4b4..1bdd25d 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<Boolean> SAMLIsPluginEnabled = new ConfigKey<Boolean>("Advanced", Boolean.class, "saml2.enabled", "false",
+ "Indicates whether SAML SSO plugin is enabled or not", true);
- public String getSpSingleSignOnUrl();
- public String getIdpSingleSignOnUrl();
+ public static final ConfigKey<String> SAMLServiceProviderID = new ConfigKey<String>("Advanced", String.class, "saml2.sp.id", "org.apache.cloudstack",
+ "SAML2 Service Provider Identifier String", true);
- public String getSpSingleLogOutUrl();
- public String getIdpSingleLogOutUrl();
+ public static final ConfigKey<String> SAMLServiceProviderContactPersonName = new ConfigKey<String>("Advanced", String.class, "saml2.sp.contact.person", "CloudStack Developers",
+ "SAML2 Service Provider Contact Person Name", true);
+
+ public static final ConfigKey<String> SAMLServiceProviderContactEmail = new ConfigKey<String>("Advanced", String.class, "saml2.sp.contact.email", "dev@cloudstack.apache.org",
+ "SAML2 Service Provider Contact Email Address", true);
+
+ public static final ConfigKey<String> SAMLServiceProviderOrgName = new ConfigKey<String>("Advanced", String.class, "saml2.sp.org.name", "Apache CloudStack",
+ "SAML2 Service Provider Organization Name", true);
+
+ public static final ConfigKey<String> SAMLServiceProviderOrgUrl = new ConfigKey<String>("Advanced", String.class, "saml2.sp.org.url", "http://cloudstack.apache.org",
+ "SAML2 Service Provider Organization URL", true);
+
+ public static final ConfigKey<String> SAMLServiceProviderSingleSignOnURL = new ConfigKey<String>("Advanced", String.class, "saml2.sp.sso.url", "http://localhost:8080/client/api?command=samlSso",
+ "SAML2 CloudStack Service Provider Single Sign On URL", true);
+
+ public static final ConfigKey<String> SAMLServiceProviderSingleLogOutURL = new ConfigKey<String>("Advanced", String.class, "saml2.sp.slo.url", "http://localhost:8080/client/api?command=samlSlo",
+ "SAML2 CloudStack Service Provider Single Log Out URL", true);
+
+ public static final ConfigKey<String> SAMLCloudStackRedirectionUrl = new ConfigKey<String>("Advanced", String.class, "saml2.redirect.url", "http://localhost:8080/client",
+ "The CloudStack UI url the SSO should redirected to when successful", true);
+
+ public static final ConfigKey<String> SAMLUserAttributeName = new ConfigKey<String>("Advanced", String.class, "saml2.user.attribute", "uid",
+ "Attribute name to be looked for in SAML response that will contain the username", true);
+
+ public static final ConfigKey<String> SAMLIdentityProviderMetadataURL = new ConfigKey<String>("Advanced", String.class, "saml2.idp.metadata.url", "https://openidp.feide.no/simplesaml/saml2/idp/metadata.php",
+ "SAML2 Identity Provider Metadata XML Url", true);
+
+ public static final ConfigKey<String> SAMLDefaultIdentityProviderId = new ConfigKey<String>("Advanced", String.class, "saml2.default.idpid", "https://openidp.feide.no",
+ "The default IdP entity ID to use only in case of multiple IdPs", true);
+
+ public static final ConfigKey<String> SAMLSignatureAlgorithm = new ConfigKey<String>("Advanced", String.class, "saml2.sigalg", "SHA1",
+ "The algorithm to use to when signing a SAML request. Default is SHA1, allowed algorithms: SHA1, SHA256, SHA384, SHA512", true);
+
+ public static final ConfigKey<Integer> SAMLTimeout = new ConfigKey<Integer>("Advanced", Integer.class, "saml2.timeout", "1800",
+ "SAML2 IDP Metadata refresh interval in seconds, minimum value is set to 300", true);
+
+ public SAMLProviderMetadata getSPMetadata();
+ public SAMLProviderMetadata getIdPMetadata(String entityId);
+ public Collection<SAMLProviderMetadata> getAllIdPMetadata();
+
+ public boolean isUserAuthorized(Long userId, String entityId);
+ public boolean authorizeUser(Long userId, String entityId, boolean enable);
+
+ public void saveToken(String authnId, String domain, String entity);
+ public SAMLTokenVO getToken(String authnId);
+ public void expireTokens();
}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java
----------------------------------------------------------------------
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 36c9da5..9d4a01a 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,42 @@
// 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.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.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.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;
@@ -63,61 +77,87 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
@Component
@Local(value = {SAML2AuthManager.class, PluggableAPIAuthenticator.class})
-public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager {
+public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager, Configurable {
private static final Logger s_logger = Logger.getLogger(SAML2AuthManagerImpl.class);
- private String serviceProviderId;
- private String identityProviderId;
+ private SAMLProviderMetadata _spMetadata = new SAMLProviderMetadata();
+ private Map<String, SAMLProviderMetadata> _idpMetadataMap = new HashMap<String, SAMLProviderMetadata>();
- private X509Certificate idpSigningKey;
- private X509Certificate idpEncryptionKey;
- private X509Certificate spX509Key;
- private KeyPair spKeyPair;
-
- private String spSingleSignOnUrl;
private String idpSingleSignOnUrl;
-
- private String spSingleLogOutUrl;
private String idpSingleLogOutUrl;
- private HTTPMetadataProvider idpMetaDataProvider;
+ private Timer _timer;
+ private int _refreshInterval = SAMLPluginConstants.SAML_REFRESH_INTERVAL;
+ private HTTPMetadataProvider _idpMetaDataProvider;
@Inject
- ConfigurationDao _configDao;
+ private KeystoreDao _ksDao;
@Inject
- private KeystoreDao _ksDao;
+ 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 +165,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 +182,188 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage
}
}
}
-
- this.serviceProviderId = _configDao.getValue(Config.SAMLServiceProviderID.key());
- this.identityProviderId = _configDao.getValue(Config.SAMLIdentityProviderID.key());
-
- this.spSingleSignOnUrl = _configDao.getValue(Config.SAMLServiceProviderSingleSignOnURL.key());
- this.spSingleLogOutUrl = _configDao.getValue(Config.SAMLServiceProviderSingleLogOutURL.key());
-
- String idpMetaDataUrl = _configDao.getValue(Config.SAMLIdentityProviderMetadataURL.key());
-
- int tolerance = 30000;
- String timeout = _configDao.getValue(Config.SAMLTimeout.key());
- if (timeout != null) {
- tolerance = Integer.parseInt(timeout);
+ 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;
+ }
- try {
- DefaultBootstrap.bootstrap();
- idpMetaDataProvider = new HTTPMetadataProvider(idpMetaDataUrl, tolerance);
- idpMetaDataProvider.setRequireValidMetadata(true);
- idpMetaDataProvider.setParserPool(new BasicParserPool());
- idpMetaDataProvider.initialize();
+ private void addIdpToMap(EntityDescriptor descriptor, Map<String, SAMLProviderMetadata> idpMap) {
+ SAMLProviderMetadata idpMetadata = new SAMLProviderMetadata();
+ idpMetadata.setEntityId(descriptor.getEntityID());
+ s_logger.debug("Adding IdP to the list of discovered IdPs: " + descriptor.getEntityID());
+ if (descriptor.getOrganization() != null) {
+ if (descriptor.getOrganization().getDisplayNames() != null) {
+ for (OrganizationDisplayName orgName : descriptor.getOrganization().getDisplayNames()) {
+ if (orgName != null && orgName.getName() != null) {
+ idpMetadata.setOrganizationName(orgName.getName().getLocalString());
+ break;
+ }
+ }
+ }
+ if (idpMetadata.getOrganizationName() == null && descriptor.getOrganization().getOrganizationNames() != null) {
+ for (OrganizationName orgName : descriptor.getOrganization().getOrganizationNames()) {
+ if (orgName != null && orgName.getName() != null) {
+ idpMetadata.setOrganizationName(orgName.getName().getLocalString());
+ break;
+ }
+ }
+ }
+ if (descriptor.getOrganization().getURLs() != null) {
+ for (OrganizationURL organizationURL : descriptor.getOrganization().getURLs()) {
+ if (organizationURL != null && organizationURL.getURL() != null) {
+ idpMetadata.setOrganizationUrl(organizationURL.getURL().getLocalString());
+ break;
+ }
+ }
+ }
+ }
+ if (descriptor.getContactPersons() != null) {
+ for (ContactPerson person : descriptor.getContactPersons()) {
+ if (person == null || (person.getGivenName() == null && person.getSurName() == null)
+ || person.getEmailAddresses() == null) {
+ continue;
+ }
+ if (person.getGivenName() != null) {
+ idpMetadata.setContactPersonName(person.getGivenName().getName());
- EntityDescriptor idpEntityDescriptor = idpMetaDataProvider.getEntityDescriptor(this.identityProviderId);
+ } 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;
+ }
+ }
+ }
- IDPSSODescriptor idpssoDescriptor = idpEntityDescriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
- if (idpssoDescriptor != null) {
- for (SingleSignOnService ssos: idpssoDescriptor.getSingleSignOnServices()) {
+ IDPSSODescriptor idpDescriptor = descriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
+ if (idpDescriptor != null) {
+ if (idpDescriptor.getSingleSignOnServices() != null) {
+ for (SingleSignOnService ssos : idpDescriptor.getSingleSignOnServices()) {
if (ssos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) {
- this.idpSingleSignOnUrl = ssos.getLocation();
+ idpMetadata.setSsoUrl(ssos.getLocation());
}
}
-
- for (SingleLogoutService slos: idpssoDescriptor.getSingleLogoutServices()) {
+ }
+ if (idpDescriptor.getSingleLogoutServices() != null) {
+ for (SingleLogoutService slos : idpDescriptor.getSingleLogoutServices()) {
if (slos.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI)) {
- this.idpSingleLogOutUrl = slos.getLocation();
+ idpMetadata.setSloUrl(slos.getLocation());
}
}
+ }
- for (KeyDescriptor kd: idpssoDescriptor.getKeyDescriptors()) {
+ X509Certificate unspecifiedKey = null;
+ if (idpDescriptor.getKeyDescriptors() != null) {
+ for (KeyDescriptor kd : idpDescriptor.getKeyDescriptors()) {
if (kd.getUse() == UsageType.SIGNING) {
try {
- this.idpSigningKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0);
+ idpMetadata.setSigningCertificate(KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0));
} catch (CertificateException ignored) {
}
}
if (kd.getUse() == UsageType.ENCRYPTION) {
try {
- this.idpEncryptionKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0);
+ idpMetadata.setEncryptionCertificate(KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0));
+ } catch (CertificateException ignored) {
+ }
+ }
+ if (kd.getUse() == UsageType.UNSPECIFIED) {
+ try {
+ unspecifiedKey = KeyInfoHelper.getCertificates(kd.getKeyInfo()).get(0);
} catch (CertificateException ignored) {
}
}
}
- } else {
- s_logger.warn("Provided IDP XML Metadata does not contain IDPSSODescriptor, SAML authentication may not work");
}
+ if (idpMetadata.getSigningCertificate() == null && unspecifiedKey != null) {
+ idpMetadata.setSigningCertificate(unspecifiedKey);
+ }
+ if (idpMetadata.getEncryptionCertificate() == null && unspecifiedKey != null) {
+ idpMetadata.setEncryptionCertificate(unspecifiedKey);
+ }
+ if (idpMap.containsKey(idpMetadata.getEntityId())) {
+ s_logger.warn("Duplicate IdP metadata found with entity Id: " + idpMetadata.getEntityId());
+ }
+ idpMap.put(idpMetadata.getEntityId(), idpMetadata);
+ }
+ }
+
+ private void discoverAndAddIdp(XMLObject metadata, Map<String, SAMLProviderMetadata> idpMap) {
+ if (metadata instanceof EntityDescriptor) {
+ EntityDescriptor entityDescriptor = (EntityDescriptor) metadata;
+ addIdpToMap(entityDescriptor, idpMap);
+ } else if (metadata instanceof EntitiesDescriptor) {
+ EntitiesDescriptor entitiesDescriptor = (EntitiesDescriptor) metadata;
+ if (entitiesDescriptor.getEntityDescriptors() != null) {
+ for (EntityDescriptor entityDescriptor: entitiesDescriptor.getEntityDescriptors()) {
+ addIdpToMap(entityDescriptor, idpMap);
+ }
+ }
+ if (entitiesDescriptor.getEntitiesDescriptors() != null) {
+ for (EntitiesDescriptor entitiesDescriptorInner: entitiesDescriptor.getEntitiesDescriptors()) {
+ discoverAndAddIdp(entitiesDescriptorInner, idpMap);
+ }
+ }
+ }
+ }
+
+ class MetadataRefreshTask extends TimerTask {
+ @Override
+ public void run() {
+ if (_idpMetaDataProvider == null) {
+ return;
+ }
+ s_logger.debug("Starting SAML IDP Metadata Refresh Task");
+ Map <String, SAMLProviderMetadata> metadataMap = new HashMap<String, SAMLProviderMetadata>();
+ try {
+ discoverAndAddIdp(_idpMetaDataProvider.getMetadata(), metadataMap);
+ _idpMetadataMap = metadataMap;
+ expireTokens();
+ s_logger.debug("Finished refreshing SAML Metadata and expiring old auth tokens");
+ } catch (MetadataProviderException e) {
+ s_logger.warn("SAML Metadata Refresh task failed with exception: " + e.getMessage());
+ }
+
+ }
+ }
+
+ private boolean setup() {
+ if (!initSP()) {
+ s_logger.error("SAML Plugin failed to initialize, please fix the configuration and restart management server");
+ return false;
+ }
+ _timer = new Timer();
+ final HttpClient client = new HttpClient();
+ final String idpMetaDataUrl = SAMLIdentityProviderMetadataURL.value();
+ if (SAMLTimeout.value() != null && SAMLTimeout.value() > SAMLPluginConstants.SAML_REFRESH_INTERVAL) {
+ _refreshInterval = SAMLTimeout.value();
+ }
+ try {
+ DefaultBootstrap.bootstrap();
+ _idpMetaDataProvider = new HTTPMetadataProvider(_timer, client, idpMetaDataUrl);
+ _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,70 +371,137 @@ 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());
- }
-
- if (this.idpSingleLogOutUrl == null || this.idpSingleSignOnUrl == null) {
- s_logger.error("SAML based authentication won't work");
+ " please check the SAML global settings: " + e.getMessage());
}
-
return true;
}
@Override
- public List<Class<?>> getAuthCommands() {
- List<Class<?>> cmdList = new ArrayList<Class<?>>();
- if (!isSAMLPluginEnabled()) {
- return cmdList;
+ public SAMLProviderMetadata getSPMetadata() {
+ return _spMetadata;
+ }
+
+ @Override
+ public SAMLProviderMetadata getIdPMetadata(String entityId) {
+ if (entityId != null && _idpMetadataMap.containsKey(entityId)) {
+ return _idpMetadataMap.get(entityId);
}
- cmdList.add(SAML2LoginAPIAuthenticatorCmd.class);
- cmdList.add(SAML2LogoutAPIAuthenticatorCmd.class);
- cmdList.add(GetServiceProviderMetaDataCmd.class);
- return cmdList;
+ 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;
}
- public String getServiceProviderId() {
- return serviceProviderId;
+ @Override
+ public Collection<SAMLProviderMetadata> getAllIdPMetadata() {
+ return _idpMetadataMap.values();
}
- public String getIdpSingleSignOnUrl() {
- return this.idpSingleSignOnUrl;
+ @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;
}
- public String getIdpSingleLogOutUrl() {
- return this.idpSingleLogOutUrl;
+ @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;
}
- public String getSpSingleSignOnUrl() {
- return spSingleSignOnUrl;
+ @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);
+ }
}
- public String getSpSingleLogOutUrl() {
- return spSingleLogOutUrl;
+ @Override
+ public SAMLTokenVO getToken(String authnId) {
+ return _samlTokenDao.findByUuid(authnId);
}
- public String getIdentityProviderId() {
- return identityProviderId;
+ @Override
+ public void expireTokens() {
+ _samlTokenDao.expireTokens();
}
- public X509Certificate getIdpSigningKey() {
- return idpSigningKey;
+ public Boolean isSAMLPluginEnabled() {
+ return SAMLIsPluginEnabled.value();
}
- public X509Certificate getIdpEncryptionKey() {
- return idpEncryptionKey;
+ @Override
+ public String getConfigComponentName() {
+ return "SAML2-PLUGIN";
}
- public Boolean isSAMLPluginEnabled() {
- return Boolean.valueOf(_configDao.getValue(Config.SAMLIsPluginEnabled.key()));
+ @Override
+ public List<Class<?>> getAuthCommands() {
+ List<Class<?>> cmdList = new ArrayList<Class<?>>();
+ if (!isSAMLPluginEnabled()) {
+ return cmdList;
+ }
+ cmdList.add(SAML2LoginAPIAuthenticatorCmd.class);
+ cmdList.add(SAML2LogoutAPIAuthenticatorCmd.class);
+ cmdList.add(GetServiceProviderMetaDataCmd.class);
+ cmdList.add(ListIdpsCmd.class);
+ return cmdList;
}
- public X509Certificate getSpX509Certificate() {
- return spX509Key;
+ @Override
+ public List<Class<?>> getCommands() {
+ List<Class<?>> cmdList = new ArrayList<Class<?>>();
+ if (!isSAMLPluginEnabled()) {
+ return cmdList;
+ }
+ cmdList.add(AuthorizeSAMLSSOCmd.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};
}
}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java
----------------------------------------------------------------------
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 68bd81c..5c8a390 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<Boolean, ActionOnFailedAuthentication>(false, null);
} else {
User user = _userDao.getUser(userAccount.getId());
- if (user != null && SAMLUtils.checkSAMLUser(user.getUuid(), username) &&
- requestParameters != null && requestParameters.containsKey(SAMLUtils.SAML_RESPONSE)) {
+ if (user != null && requestParameters != null && requestParameters.containsKey(SAMLPluginConstants.SAML_RESPONSE)) {
+ final String samlResponse = ((String[])requestParameters.get(SAMLPluginConstants.SAML_RESPONSE))[0];
+ Response responseObject = null;
+ try {
+ DefaultBootstrap.bootstrap();
+ responseObject = SAMLUtils.decodeSAMLResponse(samlResponse);
+ } catch (ConfigurationException | FactoryConfigurationError | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) {
+ return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
+ }
+ if (!responseObject.getStatus().getStatusCode().getValue().equals(StatusCode.SUCCESS_URI)) {
+ return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
+ }
return new Pair<Boolean, ActionOnFailedAuthentication>(true, null);
}
}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLPluginConstants.java
----------------------------------------------------------------------
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 0000000..dfd8447
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLPluginConstants.java
@@ -0,0 +1,29 @@
+//
+// 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 SAMLSP_KEYPAIR = "SAMLSP_KEYPAIR";
+ public static final String SAMLSP_X509CERT = "SAMLSP_X509CERT";
+}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLProviderMetadata.java
----------------------------------------------------------------------
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 0000000..c7138a1
--- /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;
+ }
+}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDao.java
----------------------------------------------------------------------
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 0000000..b045562
--- /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<SAMLTokenVO, Long> {
+ public void expireTokens();
+}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDaoImpl.java
----------------------------------------------------------------------
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 0000000..eb106d9
--- /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<SAMLTokenVO, Long> implements SAMLTokenDao {
+
+ public SAMLTokenDaoImpl() {
+ super();
+ }
+
+ @Override
+ public void expireTokens() {
+ TransactionLegacy txn = TransactionLegacy.currentTxn();
+ try {
+ txn.start();
+ String sql = "DELETE FROM `saml_token` WHERE `created` < (NOW() - INTERVAL 1 HOUR)";
+ PreparedStatement pstmt = txn.prepareAutoCloseStatement(sql);
+ pstmt.executeUpdate();
+ txn.commit();
+ } catch (Exception e) {
+ txn.rollback();
+ throw new CloudRuntimeException("Unable to flush old SAML tokens due to exception", e);
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenVO.java
----------------------------------------------------------------------
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 0000000..c8ac2f1
--- /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;
+ }
+}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java
new file mode 100644
index 0000000..5b42fef
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java
@@ -0,0 +1,351 @@
+//
+// 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.HttpUtils;
+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.RequestedAuthnContext;
+import org.opensaml.saml2.core.Response;
+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.RequestedAuthnContextBuilder;
+import org.opensaml.xml.ConfigurationException;
+import org.opensaml.xml.XMLObject;
+import org.opensaml.xml.io.Marshaller;
+import org.opensaml.xml.io.MarshallingException;
+import org.opensaml.xml.io.Unmarshaller;
+import org.opensaml.xml.io.UnmarshallerFactory;
+import org.opensaml.xml.io.UnmarshallingException;
+import org.opensaml.xml.signature.SignatureConstants;
+import org.opensaml.xml.util.Base64;
+import org.opensaml.xml.util.XMLHelper;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import javax.security.auth.x500.X500Principal;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.FactoryConfigurationError;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.net.URLEncoder;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+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 String generateSecureRandomId() {
+ return new BigInteger(160, new SecureRandom()).toString(32);
+ }
+
+ public static String getValueFromAttributeStatements(final List<AttributeStatement> attributeStatements, final String attributeKey) {
+ if (attributeStatements == null || attributeStatements.size() < 1 || attributeKey == null) {
+ return null;
+ }
+ for (AttributeStatement attributeStatement : attributeStatements) {
+ if (attributeStatement == null || attributeStatements.size() < 1) {
+ continue;
+ }
+ for (Attribute attribute : attributeStatement.getAttributes()) {
+ if (attribute.getAttributeValues() != null && attribute.getAttributeValues().size() > 0) {
+ String value = attribute.getAttributeValues().get(0).getDOM().getTextContent();
+ s_logger.debug("SAML attribute name: " + attribute.getName() + " friendly-name:" + attribute.getFriendlyName() + " value:" + value);
+ if (attributeKey.equals(attribute.getName()) || attributeKey.equals(attribute.getFriendlyName())) {
+ return value;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ public static String getValueFromAssertions(final List<Assertion> assertions, final String attributeKey) {
+ if (assertions == null || attributeKey == null) {
+ return null;
+ }
+ for (Assertion assertion : assertions) {
+ String value = getValueFromAttributeStatements(assertion.getAttributeStatements(), attributeKey);
+ if (value != null) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ public static String buildAuthnRequestUrl(final String authnId, final SAMLProviderMetadata spMetadata, final SAMLProviderMetadata idpMetadata, final String signatureAlgorithm) {
+ String redirectUrl = "";
+ try {
+ DefaultBootstrap.bootstrap();
+ AuthnRequest authnRequest = SAMLUtils.buildAuthnRequestObject(authnId, spMetadata.getEntityId(), idpMetadata.getSsoUrl(), spMetadata.getSsoUrl());
+ PrivateKey privateKey = null;
+ if (spMetadata.getKeyPair() != null) {
+ privateKey = spMetadata.getKeyPair().getPrivate();
+ }
+ redirectUrl = idpMetadata.getSsoUrl() + "?" + SAMLUtils.generateSAMLRequestSignature("SAMLRequest=" + SAMLUtils.encodeSAMLRequest(authnRequest), privateKey, signatureAlgorithm);
+ } catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException | NoSuchAlgorithmException | InvalidKeyException | java.security.SignatureException e) {
+ s_logger.error("SAML AuthnRequest message building error: " + e.getMessage());
+ }
+ return redirectUrl;
+ }
+
+ public static AuthnRequest buildAuthnRequestObject(final String authnId, final String spId, final String idpUrl, final String consumerUrl) {
+ // Issuer object
+ IssuerBuilder issuerBuilder = new IssuerBuilder();
+ Issuer issuer = issuerBuilder.buildObject();
+ issuer.setValue(spId);
+
+ // AuthnContextClass
+ AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder();
+ AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject(
+ SAMLConstants.SAML20_NS,
+ "AuthnContextClassRef", "saml");
+ authnContextClassRef.setAuthnContextClassRef(AuthnContext.PPT_AUTHN_CTX);
+
+ // AuthnContext
+ RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder();
+ RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject();
+ requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
+ requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef);
+
+ // Creation of AuthRequestObject
+ AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder();
+ AuthnRequest authnRequest = authRequestBuilder.buildObject();
+ authnRequest.setID(authnId);
+ authnRequest.setDestination(idpUrl);
+ authnRequest.setVersion(SAMLVersion.VERSION_20);
+ authnRequest.setForceAuthn(false);
+ authnRequest.setIsPassive(false);
+ authnRequest.setIssueInstant(new DateTime());
+ authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
+ authnRequest.setAssertionConsumerServiceURL(consumerUrl);
+ authnRequest.setProviderName(spId);
+ authnRequest.setIssuer(issuer);
+ authnRequest.setRequestedAuthnContext(requestedAuthnContext);
+
+ return authnRequest;
+ }
+
+ public static LogoutRequest buildLogoutRequest(String logoutUrl, String spId) {
+ IssuerBuilder issuerBuilder = new IssuerBuilder();
+ Issuer issuer = issuerBuilder.buildObject();
+ issuer.setValue(spId);
+
+ LogoutRequest logoutRequest = new LogoutRequestBuilder().buildObject();
+ logoutRequest.setID(generateSecureRandomId());
+ logoutRequest.setDestination(logoutUrl);
+ logoutRequest.setVersion(SAMLVersion.VERSION_20);
+ logoutRequest.setIssueInstant(new DateTime());
+ logoutRequest.setIssuer(issuer);
+ return logoutRequest;
+ }
+
+ public static String encodeSAMLRequest(XMLObject authnRequest)
+ throws MarshallingException, IOException {
+ Marshaller marshaller = Configuration.getMarshallerFactory()
+ .getMarshaller(authnRequest);
+ Element authDOM = marshaller.marshall(authnRequest);
+ StringWriter requestWriter = new StringWriter();
+ XMLHelper.writeNode(authDOM, requestWriter);
+ String requestMessage = requestWriter.toString();
+ Deflater deflater = new Deflater(Deflater.DEFLATED, true);
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, deflater);
+ deflaterOutputStream.write(requestMessage.getBytes());
+ deflaterOutputStream.close();
+ String encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream.toByteArray(), Base64.DONT_BREAK_LINES);
+ encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, HttpUtils.UTF_8).trim();
+ return encodedRequestMessage;
+ }
+
+ public static Response decodeSAMLResponse(String responseMessage)
+ throws ConfigurationException, ParserConfigurationException,
+ SAXException, IOException, UnmarshallingException {
+ DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
+ documentBuilderFactory.setNamespaceAware(true);
+ DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
+ byte[] base64DecodedResponse = Base64.decode(responseMessage);
+ Document document = docBuilder.parse(new ByteArrayInputStream(base64DecodedResponse));
+ Element element = document.getDocumentElement();
+ UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory();
+ Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element);
+ return (Response) unmarshaller.unmarshall(element);
+ }
+
+ public static String generateSAMLRequestSignature(final String urlEncodedString, final PrivateKey signingKey, final String sigAlgorithmName)
+ throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, UnsupportedEncodingException {
+ if (signingKey == null) {
+ return urlEncodedString;
+ }
+
+ 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);
+ if (signatureString != null) {
+ return url + "&Signature=" + URLEncoder.encode(signatureString, HttpUtils.UTF_8);
+ }
+ return url;
+ }
+
+ public static KeyFactory getKeyFactory() {
+ KeyFactory keyFactory = null;
+ try {
+ Security.addProvider(new BouncyCastleProvider());
+ keyFactory = KeyFactory.getInstance("RSA", "BC");
+ } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
+ s_logger.error("Unable to create KeyFactory:" + e.getMessage());
+ }
+ return keyFactory;
+ }
+
+ public static String savePublicKey(PublicKey key) {
+ try {
+ KeyFactory keyFactory = SAMLUtils.getKeyFactory();
+ if (keyFactory == null) return null;
+ X509EncodedKeySpec spec = keyFactory.getKeySpec(key, X509EncodedKeySpec.class);
+ return new String(org.bouncycastle.util.encoders.Base64.encode(spec.getEncoded()));
+ } catch (InvalidKeySpecException e) {
+ s_logger.error("Unable to create KeyFactory:" + e.getMessage());
+ }
+ return null;
+ }
+
+ public static String savePrivateKey(PrivateKey key) {
+ try {
+ KeyFactory keyFactory = SAMLUtils.getKeyFactory();
+ if (keyFactory == null) return null;
+ PKCS8EncodedKeySpec spec = keyFactory.getKeySpec(key,
+ PKCS8EncodedKeySpec.class);
+ return new String(org.bouncycastle.util.encoders.Base64.encode(spec.getEncoded()));
+ } catch (InvalidKeySpecException e) {
+ s_logger.error("Unable to create KeyFactory:" + e.getMessage());
+ }
+ return null;
+ }
+
+ public static PublicKey loadPublicKey(String publicKey) {
+ byte[] sigBytes = org.bouncycastle.util.encoders.Base64.decode(publicKey);
+ X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(sigBytes);
+ KeyFactory keyFact = SAMLUtils.getKeyFactory();
+ if (keyFact == null)
+ return null;
+ try {
+ return keyFact.generatePublic(x509KeySpec);
+ } catch (InvalidKeySpecException e) {
+ s_logger.error("Unable to create PrivateKey from privateKey string:" + e.getMessage());
+ }
+ return null;
+ }
+
+ public static PrivateKey loadPrivateKey(String privateKey) {
+ byte[] sigBytes = org.bouncycastle.util.encoders.Base64.decode(privateKey);
+ PKCS8EncodedKeySpec pkscs8KeySpec = new PKCS8EncodedKeySpec(sigBytes);
+ KeyFactory keyFact = SAMLUtils.getKeyFactory();
+ if (keyFact == null)
+ return null;
+ try {
+ return keyFact.generatePrivate(pkscs8KeySpec);
+ } catch (InvalidKeySpecException e) {
+ s_logger.error("Unable to create PrivateKey from privateKey string:" + e.getMessage());
+ }
+ return null;
+ }
+
+ public static KeyPair generateRandomKeyPair() throws NoSuchProviderException, NoSuchAlgorithmException {
+ Security.addProvider(new BouncyCastleProvider());
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
+ keyPairGenerator.initialize(4096, new SecureRandom());
+ return keyPairGenerator.generateKeyPair();
+ }
+
+ public static X509Certificate generateRandomX509Certificate(KeyPair keyPair) throws NoSuchAlgorithmException, NoSuchProviderException, CertificateEncodingException, SignatureException, InvalidKeyException {
+ DateTime now = DateTime.now(DateTimeZone.UTC);
+ X500Principal dnName = new X500Principal("CN=ApacheCloudStack");
+ X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
+ certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
+ certGen.setSubjectDN(dnName);
+ certGen.setIssuerDN(dnName);
+ certGen.setNotBefore(now.minusDays(1).toDate());
+ certGen.setNotAfter(now.plusYears(3).toDate());
+ certGen.setPublicKey(keyPair.getPublic());
+ certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
+ return certGen.generate(keyPair.getPrivate(), "BC");
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java
new file mode 100644
index 0000000..5b4d552
--- /dev/null
+++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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;
+
+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.saml.SAMLProviderMetadata;
+import org.apache.cloudstack.saml.SAMLUtils;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import javax.servlet.http.HttpServletRequest;
+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;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+
+@RunWith(MockitoJUnitRunner.class)
+public class GetServiceProviderMetaDataCmdTest {
+
+ @Mock
+ ApiServerService apiServer;
+
+ @Mock
+ SAML2AuthManager samlAuthManager;
+
+ @Mock
+ HttpSession session;
+
+ @Mock
+ HttpServletResponse resp;
+
+ @Mock
+ HttpServletRequest req;
+
+ @Test
+ public void testAuthenticate() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, CertificateParsingException, CertificateEncodingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException {
+ GetServiceProviderMetaDataCmd cmd = new GetServiceProviderMetaDataCmd();
+
+ Field apiServerField = GetServiceProviderMetaDataCmd.class.getDeclaredField("_apiServer");
+ apiServerField.setAccessible(true);
+ apiServerField.set(cmd, apiServer);
+
+ Field managerField = GetServiceProviderMetaDataCmd.class.getDeclaredField("_samlAuthManager");
+ managerField.setAccessible(true);
+ managerField.set(cmd, samlAuthManager);
+
+ String spId = "someSPID";
+ String url = "someUrl";
+ 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, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
+ Assert.assertTrue(result.contains("md:EntityDescriptor"));
+ }
+
+ @Test
+ public void testGetAPIType() {
+ Assert.assertTrue(new GetServiceProviderMetaDataCmd().getAPIType() == APIAuthenticationType.LOGIN_API);
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAML2UserAuthenticatorTest.java
----------------------------------------------------------------------
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 83792c6..5b37388 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);
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java
new file mode 100644
index 0000000..0f68e76
--- /dev/null
+++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java
@@ -0,0 +1,74 @@
+//
+// 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;
+
+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 java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+
+public class SAMLUtilsTest extends TestCase {
+
+ @Test
+ public void testGenerateSecureRandomId() throws Exception {
+ assertTrue(SAMLUtils.generateSecureRandomId().length() > 0);
+ }
+
+ @Test
+ public void testBuildAuthnRequestObject() throws Exception {
+ String consumerUrl = "http://someurl.com";
+ String idpUrl = "http://idp.domain.example";
+ String spId = "cloudstack";
+ 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);
+ }
+
+ @Test
+ public void testBuildLogoutRequest() throws Exception {
+ String logoutUrl = "http://logoutUrl";
+ String spId = "cloudstack";
+ String sessionIndex = "12345";
+ LogoutRequest req = SAMLUtils.buildLogoutRequest(logoutUrl, spId);
+ assertEquals(req.getDestination(), logoutUrl);
+ assertEquals(req.getIssuer().getValue(), spId);
+ }
+
+ @Test
+ public void testX509Helpers() throws Exception {
+ KeyPair keyPair = SAMLUtils.generateRandomKeyPair();
+
+ String privateKeyString = SAMLUtils.savePrivateKey(keyPair.getPrivate());
+ String publicKeyString = SAMLUtils.savePublicKey(keyPair.getPublic());
+
+ PrivateKey privateKey = SAMLUtils.loadPrivateKey(privateKeyString);
+ PublicKey publicKey = SAMLUtils.loadPublicKey(publicKeyString);
+
+ assertTrue(privateKey.equals(keyPair.getPrivate()));
+ assertTrue(publicKey.equals(keyPair.getPublic()));
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java
deleted file mode 100644
index cb16f0c..0000000
--- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.cloudstack.api.command;
-
-import com.cloud.utils.HttpUtils;
-import org.apache.cloudstack.api.ApiServerService;
-import org.apache.cloudstack.api.auth.APIAuthenticationType;
-import org.apache.cloudstack.saml.SAML2AuthManager;
-import org.apache.cloudstack.utils.auth.SAMLUtils;
-import org.junit.Assert;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.runners.MockitoJUnitRunner;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
-import java.lang.reflect.Field;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.SignatureException;
-import java.security.cert.CertificateEncodingException;
-import java.security.cert.CertificateParsingException;
-import java.security.cert.X509Certificate;
-
-@RunWith(MockitoJUnitRunner.class)
-public class GetServiceProviderMetaDataCmdTest {
-
- @Mock
- ApiServerService apiServer;
-
- @Mock
- SAML2AuthManager samlAuthManager;
-
- @Mock
- HttpSession session;
-
- @Mock
- HttpServletResponse resp;
-
- @Mock
- HttpServletRequest req;
-
- @Test
- public void testAuthenticate() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, CertificateParsingException, CertificateEncodingException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException {
- GetServiceProviderMetaDataCmd cmd = new GetServiceProviderMetaDataCmd();
-
- Field apiServerField = GetServiceProviderMetaDataCmd.class.getDeclaredField("_apiServer");
- apiServerField.setAccessible(true);
- apiServerField.set(cmd, apiServer);
-
- Field managerField = GetServiceProviderMetaDataCmd.class.getDeclaredField("_samlAuthManager");
- managerField.setAccessible(true);
- managerField.set(cmd, samlAuthManager);
-
- String spId = "someSPID";
- String url = "someUrl";
- X509Certificate cert = SAMLUtils.generateRandomX509Certificate(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);
-
- String result = cmd.authenticate("command", null, session, "random", 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
- public void testGetAPIType() {
- Assert.assertTrue(new GetServiceProviderMetaDataCmd().getAPIType() == APIAuthenticationType.LOGIN_API);
- }
-}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java
----------------------------------------------------------------------
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 30ecc93..00455b9 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 {
SAML2AuthManager samlAuthManager;
@Mock
- ConfigurationDao configDao;
-
- @Mock
DomainManager domainMgr;
@Mock
@@ -104,6 +105,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);
@@ -145,32 +149,33 @@ public class SAML2LoginAPIAuthenticatorCmdTest {
domainMgrField.setAccessible(true);
domainMgrField.set(cmd, domainMgr);
- Field configDaoField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_configDao");
- configDaoField.setAccessible(true);
- configDaoField.set(cmd, configDao);
-
Field userAccountDaoField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_userAccountDao");
userAccountDaoField.setAccessible(true);
userAccountDaoField.set(cmd, userAccountDao);
String spId = "someSPID";
String url = "someUrl";
- X509Certificate cert = SAMLUtils.generateRandomX509Certificate(SAMLUtils.generateRandomKeyPair());
- Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId);
- Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(null);
- Mockito.when(samlAuthManager.getIdpSingleSignOnUrl()).thenReturn(url);
- Mockito.when(samlAuthManager.getSpSingleSignOnUrl()).thenReturn(url);
+ KeyPair kp = SAMLUtils.generateRandomKeyPair();
+ X509Certificate cert = SAMLUtils.generateRandomX509Certificate(kp);
+
+ SAMLProviderMetadata providerMetadata = new SAMLProviderMetadata();
+ providerMetadata.setEntityId("random");
+ providerMetadata.setSigningCertificate(cert);
+ providerMetadata.setEncryptionCertificate(cert);
+ providerMetadata.setKeyPair(kp);
+ providerMetadata.setSsoUrl("http://test.local");
+ providerMetadata.setSloUrl("http://test.local");
Mockito.when(session.getAttribute(Mockito.anyString())).thenReturn(null);
- Mockito.when(configDao.getValue(Mockito.anyString())).thenReturn("someString");
Mockito.when(domain.getId()).thenReturn(1L);
Mockito.when(domainMgr.getDomain(Mockito.anyString())).thenReturn(domain);
UserAccountVO user = new UserAccountVO();
- user.setUsername(SAMLUtils.createSAMLId("someUID"));
user.setId(1000L);
Mockito.when(userAccountDao.getUserAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(user);
Mockito.when(apiServer.verifyUser(Mockito.anyLong())).thenReturn(false);
+ Mockito.when(samlAuthManager.getSPMetadata()).thenReturn(providerMetadata);
+ Mockito.when(samlAuthManager.getIdPMetadata(Mockito.anyString())).thenReturn(providerMetadata);
Map<String, Object[]> params = new HashMap<String, Object[]>();
@@ -179,16 +184,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, "random", 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
[3/3] git commit: updated refs/heads/saml-pp-squashed to 595efe8
Posted by bh...@apache.org.
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 <ro...@shapeblue.com>
Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo
Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/595efe88
Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/595efe88
Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/595efe88
Branch: refs/heads/saml-pp-squashed
Commit: 595efe88bf2c42f47292bbec47b2ed1e3c8e812f
Parents: eb904cd
Author: Rohit Yadav <ro...@shapeblue.com>
Authored: Thu May 28 14:50:12 2015 +0200
Committer: Rohit Yadav <ro...@shapeblue.com>
Committed: Thu Jun 25 19:25:48 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 | 5 +-
client/tomcatconf/commands.properties.in | 2 +
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 | 89 ++++
.../command/GetServiceProviderMetaDataCmd.java | 81 +++-
.../cloudstack/api/command/ListIdpsCmd.java | 114 +++++
.../command/SAML2LoginAPIAuthenticatorCmd.java | 279 ++++++------
.../command/SAML2LogoutAPIAuthenticatorCmd.java | 28 +-
.../cloudstack/api/response/IdpResponse.java | 62 +++
.../cloudstack/saml/SAML2AuthManager.java | 67 ++-
.../cloudstack/saml/SAML2AuthManagerImpl.java | 420 +++++++++++++++----
.../cloudstack/saml/SAML2UserAuthenticator.java | 28 +-
.../cloudstack/saml/SAMLPluginConstants.java | 29 ++
.../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 | 351 ++++++++++++++++
.../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 | 2 +
ui/css/cloudstack3.css | 5 +-
ui/dictionary.jsp | 3 +
ui/index.jsp | 48 ++-
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 | 72 +++-
.../apache/cloudstack/utils/auth/SAMLUtils.java | 330 ---------------
.../cloudstack/utils/auth/SAMLUtilsTest.java | 91 ----
53 files changed, 2240 insertions(+), 944 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/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/595efe88/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/595efe88/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/595efe88/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..f5989d0 100644
--- a/client/WEB-INF/classes/resources/messages.properties
+++ b/client/WEB-INF/classes/resources/messages.properties
@@ -757,7 +757,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=Enable 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/595efe88/client/tomcatconf/commands.properties.in
----------------------------------------------------------------------
diff --git a/client/tomcatconf/commands.properties.in b/client/tomcatconf/commands.properties.in
index a87d167..c6d068c 100644
--- a/client/tomcatconf/commands.properties.in
+++ b/client/tomcatconf/commands.properties.in
@@ -26,6 +26,8 @@ logout=15
samlSso=15
samlSlo=15
getSPMetadata=15
+listIdps=15
+authorizeSamlSso=7
### Account commands
createAccount=7
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/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/595efe88/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','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/WFvNDAr9tZIWGjbfP
qndeS/DTocvm5GBuZn4xoOq99Woo0MQC6zfDEz8DOJlX56hPYXU0ZDbjxInfQsoc3MejoLG7n4xkxPn6WAvynFFsAoZFIk60Faz7UZIfuAWafoX9L0KpjkbT5Fob9CFEuQEzO7x9CIWoUr2PYn8HbThHDUOFAuVVpOLqleLPCrxkX/P01WTrLFuT6vSJKW2kxVwiHvZH6pNT01X/nlHDD6Jd9oWse2jIDBVor6fMnNDtgKl9azKgyakxoOGB7BMcb5u0Im8vFBCCRIyN3lrYjjR1F3H1tvY6Q0fEGLkilO334IyjC63he6lZ6NqslE/3QWEyyIiCL52rMzadN2SwVNawCa8YIR6+TpBjKyqY17LCP57v3UyM6J/kcUqXxDRcg1XnsjiWU+u0j9ZdlBgcbNuQeb1jD2QgICcyr/tWyJ2asyVfvARcD/xt5a9AnGjO0LnwMfw/DdBz1XCxz5uf3gOM69+nXk2gWhAoIBAQDr7NhlmVrASpOJHXXvqkpC2P4+hx7bmwKUZPbqm32pqCBypn6Qd2h4wdFzcP41wN6kpYqmmsPBoezctSgromfHeVjTyjhGku8b1HqtyRoX5sqIIPtv5pKKGS/tVWfyqQ8BspcdhZaR7+ZiRsLRwOXCscRq82+vbyq5Jd1bjsDGeLtcYyASv3II1xTBzSgNlvB+WiFXIlGWT9IPXwhv6IxZn7ME/y992d7bhgPxCcdTfKQNPBpcKerjcNxeDMto8vVijBDqujVpTf+3YvOOzWrcLn5UORFzpVho7oml5+5qnkFI/RZoiagUixFeQMv5+72qKJrxJu3VfI3mxlzZm5YjAoIBAQDhwjvzNWCQ2y7wwwnr88snVxAhd7kByvbKmnFVzHFTy2lheyWkiAqXj9juqsCtbkOK8a1ogmFAHz3i0/n+QhcJ20gudryniMt+u+wKxpmEKyqHKX3d4biPOvkKx7tdfwnlRKpSWXuynlDNVaQnJKUqBqDygLaE2s0
LK3Fwdl+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+2ne2dc486gmAn
KdJA8w9uawqMEkVpTA9f4WiBJJVzPwAv19AJCPKfUaB8IdNPV+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,'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/595efe88/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 @@
</build>
</profile>
<profile>
+ <!-- saml deploydb property -->
+ <id>deploydb-saml</id>
+ <activation>
+ <property>
+ <name>deploydb-saml</name>
+ </property>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <dependencies>
+ <dependency>
+ <groupId>mysql</groupId>
+ <artifactId>mysql-connector-java</artifactId>
+ <version>${cs.mysql.version}</version>
+ </dependency>
+ </dependencies>
+ <version>1.2.1</version>
+ <executions>
+ <execution>
+ <phase>process-resources</phase>
+ <id>create-schema-simulator</id>
+ <goals>
+ <goal>java</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <mainClass>com.cloud.upgrade.DatabaseCreator</mainClass>
+ <includePluginDependencies>true</includePluginDependencies>
+ <arguments>
+ <!-- db properties file -->
+ <argument>${basedir}/../utils/conf/db.properties</argument>
+ <argument>${basedir}/../utils/conf/db.properties.override</argument>
+ <!-- simulator sql files -->
+ <argument>${basedir}/developer-saml.sql</argument>
+ <!-- upgrade -->
+ <argument>com.cloud.upgrade.DatabaseUpgradeChecker</argument>
+ <argument>--rootpassword=${db.root.password}</argument>
+ </arguments>
+ <systemProperties>
+ <systemProperty>
+ <key>catalina.home</key>
+ <value>${basedir}/../utils</value>
+ </systemProperty>
+ <systemProperty>
+ <key>paths.script</key>
+ <value>${basedir}/target/db</value>
+ </systemProperty>
+ </systemProperties>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
<!-- simulator deploydb property -->
<id>deploydb-simulator</id>
<activation>
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/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/595efe88/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/595efe88/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/595efe88/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<UserAccountVO, Long> {
+ List<UserAccountVO> 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/595efe88/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<UserAccountVO, Long> impl
}
@Override
+ public List<UserAccountVO> getAllUsersByNameAndEntity(String username, String entity) {
+ if (username == null) {
+ return null;
+ }
+ SearchCriteria<UserAccountVO> sc = createSearchCriteria();
+ sc.addAnd("username", SearchCriteria.Op.EQ, username);
+ sc.addAnd("externalEntity", SearchCriteria.Op.EQ, entity);
+ return listBy(sc);
+ }
+
+ @Override
public UserAccount getUserAccount(String username, Long domainId) {
if ((username == null) || (domainId == null)) {
return null;
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/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 @@
<artifactId>cloud-api</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.cloudstack</groupId>
+ <artifactId>cloud-framework-config</artifactId>
+ <version>${project.version}</version>
+ </dependency>
</dependencies>
</project>
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/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 @@
<property name="name" value="SAML2Auth"/>
</bean>
+ <bean id="samlTokenDao" class="org.apache.cloudstack.saml.SAMLTokenDaoImpl">
+ </bean>
+
</beans>
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/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..aeecbef
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/AuthorizeSAMLSSOCmd.java
@@ -0,0 +1,89 @@
+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 = true, responseHasSensitiveInfo = true)
+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/595efe88/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..0af02d5 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
@@ -30,21 +30,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 +88,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 +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);
@@ -129,19 +147,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 +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();
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/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<String, Object[]> params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException {
+ auditTrailSb.append("=== SAML List IdPs ===");
+ ListResponse<IdpResponse> response = new ListResponse<IdpResponse>();
+ List<IdpResponse> idpResponseList = new ArrayList<IdpResponse>();
+ for (SAMLProviderMetadata metadata: _samlAuthManager.getAllIdPMetadata()) {
+ if (metadata == null) {
+ continue;
+ }
+ IdpResponse idpResponse = new IdpResponse();
+ idpResponse.setId(metadata.getEntityId());
+ if (metadata.getOrganizationName() == null || metadata.getOrganizationName().isEmpty()) {
+ idpResponse.setOrgName(metadata.getEntityId());
+ } else {
+ idpResponse.setOrgName(metadata.getOrganizationName());
+ }
+ idpResponse.setOrgUrl(metadata.getOrganizationUrl());
+ idpResponse.setObjectName("idp");
+ idpResponseList.add(idpResponse);
+ }
+ response.setResponses(idpResponseList, idpResponseList.size());
+ response.setResponseName(getCommandName());
+ return ApiResponseSerializer.toSerializedString(response, responseType);
+ }
+
+ @Override
+ public APIAuthenticationType getAPIType() {
+ return APIAuthenticationType.LOGIN_API;
+ }
+
+ @Override
+ public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
+ for (PluggableAPIAuthenticator authManager: authenticators) {
+ if (authManager != null && authManager instanceof SAML2AuthManager) {
+ _samlAuthManager = (SAML2AuthManager) authManager;
+ }
+ }
+ if (_samlAuthManager == null) {
+ s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd");
+ }
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/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..2549b05 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
@@ -18,12 +18,11 @@
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 +37,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 +73,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 +84,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 +102,8 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
- public String getIdpUrl() {
- return idpUrl;
+ public String getIdpId() {
+ return idpId;
}
/////////////////////////////////////////////////////
@@ -128,30 +126,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 +141,44 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
@Override
public String authenticate(final String command, final Map<String, Object[]> params, final HttpSession session, final String remoteAddress, final String responseType, final StringBuilder auditTrailSb, final 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 +186,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 +195,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 +236,95 @@ 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 (username == null) {
+ username = SAMLUtils.getValueFromAssertions(processedSAMLResponse.getAssertions(), SAML2AuthManager.SAMLUserAttributeName.value());
}
- 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 (idpMetadata.getEncryptionCertificate() != null && spMetadata != null
+ && spMetadata.getKeyPair() != null && spMetadata.getKeyPair().getPrivate() != null) {
+ Credential credential = SecurityHelper.getSimpleCredential(idpMetadata.getEncryptionCertificate().getPublicKey(),
+ spMetadata.getKeyPair().getPrivate());
+ StaticKeyInfoCredentialResolver keyInfoResolver = new StaticKeyInfoCredentialResolver(credential);
+ EncryptedKeyResolver keyResolver = new InlineEncryptedKeyResolver();
+ Decrypter decrypter = new Decrypter(null, keyInfoResolver, keyResolver);
+ decrypter.setRootInNewDocument(true);
+ List<EncryptedAssertion> encryptedAssertions = processedSAMLResponse.getEncryptedAssertions();
+ if (encryptedAssertions != null) {
+ for (EncryptedAssertion encryptedAssertion : encryptedAssertions) {
+ Assertion assertion = null;
+ try {
+ assertion = decrypter.decrypt(encryptedAssertion);
+ } catch (DecryptionException e) {
+ s_logger.warn("SAML EncryptedAssertion error: " + e.toString());
+ }
+ if (assertion == null) {
+ continue;
+ }
+ Signature encSig = assertion.getSignature();
+ if (idpMetadata.getSigningCertificate() != null && encSig != null) {
+ BasicX509Credential sigCredential = new BasicX509Credential();
+ sigCredential.setEntityCertificate(idpMetadata.getSigningCertificate());
+ SignatureValidator validator = new SignatureValidator(sigCredential);
+ try {
+ validator.validate(encSig);
+ } catch (ValidationException e) {
+ s_logger.error("SAML Response's signature failed to be validated by IDP signing key:" + e.getMessage());
+ throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
+ "SAML Response's signature failed to be validated by IDP signing key",
+ params, responseType));
+ }
+ }
+ if (username == null) {
+ username = SAMLUtils.getValueFromAttributeStatements(assertion.getAttributeStatements(), 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;
- }
+ 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));
}
- List<AttributeStatement> attributeStatements = assertion.getAttributeStatements();
- if (attributeStatements != null && attributeStatements.size() > 0) {
- for (AttributeStatement attributeStatement: attributeStatements) {
- if (attributeStatement == null) {
- continue;
- }
- // Try capturing standard LDAP attributes
- for (Attribute attribute: attributeStatement.getAttributes()) {
- String attributeName = attribute.getName();
- String attributeValue = attribute.getAttributeValues().get(0).getDOM().getTextContent();
- if (attributeName.equalsIgnoreCase("uid") && username == null) {
- username = attributeValue;
- } else if (attributeName.equalsIgnoreCase("givenName")) {
- firstName = attributeValue;
- } else if (attributeName.equalsIgnoreCase(("sn"))) {
- lastName = attributeValue;
- } else if (attributeName.equalsIgnoreCase("mail")) {
- email = attributeValue;
- }
+ UserAccount userAccount = null;
+ List<UserAccountVO> possibleUserAccounts = _userAccountDao.getAllUsersByNameAndEntity(username, issuer.getValue());
+ if (possibleUserAccounts != null && possibleUserAccounts.size() > 0) {
+ if (possibleUserAccounts.size() == 1) {
+ userAccount = possibleUserAccounts.get(0);
+ } else if (possibleUserAccounts.size() > 1) {
+ if (domainId != null) {
+ userAccount = _userAccountDao.getUserAccount(username, domainId);
+ } else {
+ throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
+ "You have accounts in multiple domains, please re-login by specifying the domain you want to log into.",
+ params, responseType));
}
}
}
- if (username == null && email != null) {
- username = email;
- }
- 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 || !userAccount.getExternalEntity().equalsIgnoreCase(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 +335,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));
}
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java
----------------------------------------------------------------------
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 992e431..844176d 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;
@@ -59,8 +58,7 @@ public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthen
@Inject
ApiServerService _apiServer;
- @Inject
- ConfigurationDao _configDao;
+
SAML2AuthManager _samlAuthManager;
/////////////////////////////////////////////////////
@@ -93,7 +91,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;
@@ -110,7 +108,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)) {
@@ -122,25 +120,25 @@ 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);
+ if (idpMetadata == null) {
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());
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());
http://git-wip-us.apache.org/repos/asf/cloudstack/blob/595efe88/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/IdpResponse.java
----------------------------------------------------------------------
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 0000000..d95cc33
--- /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;
+ }
+}