From 19cc5eb9e6f1b5127bf01e4c1ae061875e61cb4c Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 1 Jun 2015 14:10:54 +0200 Subject: [PATCH 001/115] schema: Encode URL safe random vnc password string /+= may break on some environments, url safe encoded passwords will have -_, characters which are more acceptable Signed-off-by: Rohit Yadav --- engine/schema/src/com/cloud/vm/VMInstanceVO.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/schema/src/com/cloud/vm/VMInstanceVO.java b/engine/schema/src/com/cloud/vm/VMInstanceVO.java index d542f33237..3d2c3af677 100644 --- a/engine/schema/src/com/cloud/vm/VMInstanceVO.java +++ b/engine/schema/src/com/cloud/vm/VMInstanceVO.java @@ -206,7 +206,7 @@ public VMInstanceVO(long id, long serviceOfferingId, String name, String instanc SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); byte[] randomBytes = new byte[16]; random.nextBytes(randomBytes); - vncPassword = Base64.encodeBase64String(randomBytes); + vncPassword = Base64.encodeBase64URLSafeString(randomBytes); } catch (NoSuchAlgorithmException e) { s_logger.error("Unexpected exception in SecureRandom Algorithm selection ", e); } From d55d45348c978294d0ba28de146160769d87ba81 Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Mon, 1 Jun 2015 14:53:58 +0200 Subject: [PATCH 002/115] CLOUDSTACK-8530: KVM hosts without active agents should be in Disconnected state KVM hosts which are actuall up, but if their agents are shutdown should be put in disconnected state. This would avoid getting the VMs HA'd and other commands such as deploying a VM will exclude that host and save us from errors. The improvement is that, we first try to contact the KVM host itself. If it fails we assume that it's disconnected, and then ask its KVM neighbours if they can check its status. If all of the KVM neighbours tell us that it's Down and we're unable to reach the KVM host, then the host is possibly down. In case any of the KVM neighbours tell us that it's Up but we're unable to reach the KVM host then we can be sure that the agent is offline but the host is running. Signed-off-by: Rohit Yadav This closes #340 --- .../kvm/src/com/cloud/ha/KVMInvestigator.java | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/plugins/hypervisors/kvm/src/com/cloud/ha/KVMInvestigator.java b/plugins/hypervisors/kvm/src/com/cloud/ha/KVMInvestigator.java index e750ced7e1..b816d09dbc 100644 --- a/plugins/hypervisors/kvm/src/com/cloud/ha/KVMInvestigator.java +++ b/plugins/hypervisors/kvm/src/com/cloud/ha/KVMInvestigator.java @@ -18,13 +18,6 @@ */ package com.cloud.ha; -import java.util.List; - -import javax.ejb.Local; -import javax.inject.Inject; - -import org.apache.log4j.Logger; - import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; import com.cloud.agent.api.CheckOnHostCommand; @@ -35,6 +28,11 @@ import com.cloud.hypervisor.Hypervisor; import com.cloud.resource.ResourceManager; import com.cloud.utils.component.AdapterBase; +import org.apache.log4j.Logger; + +import javax.ejb.Local; +import javax.inject.Inject; +import java.util.List; @Local(value = Investigator.class) public class KVMInvestigator extends AdapterBase implements Investigator { @@ -60,22 +58,47 @@ public Status isAgentAlive(Host agent) { if (agent.getHypervisorType() != Hypervisor.HypervisorType.KVM && agent.getHypervisorType() != Hypervisor.HypervisorType.LXC) { return null; } + Status hostStatus = null; + Status neighbourStatus = null; CheckOnHostCommand cmd = new CheckOnHostCommand(agent); + + try { + Answer answer = _agentMgr.easySend(agent.getId(), cmd); + if (answer != null) { + hostStatus = answer.getResult() ? Status.Down : Status.Up; + } + } catch (Exception e) { + s_logger.debug("Failed to send command to host: " + agent.getId()); + } + if (hostStatus == null) { + hostStatus = Status.Disconnected; + } + List neighbors = _resourceMgr.listHostsInClusterByStatus(agent.getClusterId(), Status.Up); for (HostVO neighbor : neighbors) { if (neighbor.getId() == agent.getId() || (neighbor.getHypervisorType() != Hypervisor.HypervisorType.KVM && neighbor.getHypervisorType() != Hypervisor.HypervisorType.LXC)) { continue; } + s_logger.debug("Investigating host:" + agent.getId() + " via neighbouring host:" + neighbor.getId()); try { Answer answer = _agentMgr.easySend(neighbor.getId(), cmd); if (answer != null) { - return answer.getResult() ? Status.Down : Status.Up; + neighbourStatus = answer.getResult() ? Status.Down : Status.Up; + s_logger.debug("Neighbouring host:" + neighbor.getId() + " returned status:" + neighbourStatus + " for the investigated host:" + agent.getId()); + if (neighbourStatus == Status.Up) { + break; + } } } catch (Exception e) { s_logger.debug("Failed to send command to host: " + neighbor.getId()); } } - - return null; + if (neighbourStatus == Status.Up && (hostStatus == Status.Disconnected || hostStatus == Status.Down)) { + hostStatus = Status.Disconnected; + } + if (neighbourStatus == Status.Down && (hostStatus == Status.Disconnected || hostStatus == Status.Down)) { + hostStatus = Status.Down; + } + return hostStatus; } } From fb50283fbe7875ce64992e268fb9ad5792e75962 Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Wed, 3 Jun 2015 17:37:13 +0200 Subject: [PATCH 003/115] CID 1302974: Scanner in try-with-resource Signed-off-by: Rohit Yadav This closes #353 (cherry picked from commit 109b6e94d39d7c6db4ff721c4699e42abe47ec5e) Signed-off-by: Rohit Yadav --- .../org/apache/cloudstack/utils/linux/MemStat.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/plugins/hypervisors/kvm/src/org/apache/cloudstack/utils/linux/MemStat.java b/plugins/hypervisors/kvm/src/org/apache/cloudstack/utils/linux/MemStat.java index 1e3c872c35..1d6a4fc5b4 100644 --- a/plugins/hypervisors/kvm/src/org/apache/cloudstack/utils/linux/MemStat.java +++ b/plugins/hypervisors/kvm/src/org/apache/cloudstack/utils/linux/MemStat.java @@ -16,11 +16,10 @@ // under the License. package org.apache.cloudstack.utils.linux; -import java.util.HashMap; -import java.util.Map; - import java.io.File; import java.io.FileNotFoundException; +import java.util.HashMap; +import java.util.Map; import java.util.Scanner; public class MemStat { @@ -29,7 +28,7 @@ public class MemStat { protected final static String CACHE_KEY = "Cached"; protected final static String TOTAL_KEY = "MemTotal"; - private Map _memStats = new HashMap(); + private final Map _memStats = new HashMap(); public MemStat() { } @@ -51,9 +50,9 @@ public Double getCache() { } public void refresh() { - try { - Scanner fileScanner = new Scanner(new File(MEMINFO_FILE)); - parseFromScanner(fileScanner); + File f = new File(MEMINFO_FILE); + try (Scanner scanner = new Scanner(f)) { + parseFromScanner(scanner); } catch (FileNotFoundException ex) { throw new RuntimeException("File " + MEMINFO_FILE + " not found:" + ex.toString()); } From 5fb86ae316d5e6703e2c54075b8d01149ec8e343 Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Wed, 3 Jun 2015 17:31:19 +0200 Subject: [PATCH 004/115] CID 1302976 Scanner in try-with-resource Signed-off-by: Rohit Yadav This closes #352 (cherry picked from commit 3d4d15275340bd9bbec76afb535e4a48371ef421) Signed-off-by: Rohit Yadav --- .../src/org/apache/cloudstack/utils/linux/CPUStat.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/hypervisors/kvm/src/org/apache/cloudstack/utils/linux/CPUStat.java b/plugins/hypervisors/kvm/src/org/apache/cloudstack/utils/linux/CPUStat.java index 38b7e8ef9e..d8228a4202 100644 --- a/plugins/hypervisors/kvm/src/org/apache/cloudstack/utils/linux/CPUStat.java +++ b/plugins/hypervisors/kvm/src/org/apache/cloudstack/utils/linux/CPUStat.java @@ -17,12 +17,12 @@ package org.apache.cloudstack.utils.linux; -import org.apache.log4j.Logger; - import java.io.File; import java.io.FileNotFoundException; import java.util.Scanner; +import org.apache.log4j.Logger; + public class CPUStat { private static final Logger s_logger = Logger.getLogger(CPUStat.class); @@ -52,8 +52,9 @@ private void init() { private UptimeStats getUptimeAndCpuIdleTime() { UptimeStats uptime = new UptimeStats(0d, 0d); - try { - String[] stats = new Scanner(new File(_uptimeFile)).useDelimiter("\\Z").next().split("\\s+"); + File f = new File(_uptimeFile); + try (Scanner scanner = new Scanner(f);) { + String[] stats = scanner.useDelimiter("\\Z").next().split("\\s+"); uptime = new UptimeStats(Double.parseDouble(stats[0]), Double.parseDouble(stats[1])); } catch (FileNotFoundException ex) { s_logger.warn("File " + _uptimeFile + " not found:" + ex.toString()); From 64d72db0fce615bd3ada32a94634360995c97452 Mon Sep 17 00:00:00 2001 From: jeff Date: Thu, 11 Jun 2015 15:36:18 +0000 Subject: [PATCH 005/115] Allow EC2 to be run from Maven properly Signed-off-by: Rohit Yadav This closes #389 --- awsapi/pom.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/awsapi/pom.xml b/awsapi/pom.xml index 8d15a45497..9366ccbd9d 100644 --- a/awsapi/pom.xml +++ b/awsapi/pom.xml @@ -204,6 +204,19 @@ cloud-framework-db ${project.version} + + mysql + mysql-connector-java + ${cs.mysql.version} + runtime + + + + org.apache.ws.xmlschema + xmlschema-core + 2.2.1 + runtime + From 979956a4effba0f6bb427d4d501dd643d1692f5e Mon Sep 17 00:00:00 2001 From: Kishan Kavala Date: Tue, 9 Jun 2015 16:03:15 +0530 Subject: [PATCH 006/115] CLOUDSTACK-5409: Include projectid during ACL check while listing S2S Vpn gateways Signed-off-by: Rohit Yadav This closes #374 --- server/src/com/cloud/network/vpn/Site2SiteVpnManagerImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/com/cloud/network/vpn/Site2SiteVpnManagerImpl.java b/server/src/com/cloud/network/vpn/Site2SiteVpnManagerImpl.java index a470ba1a0a..bf4b8a36ac 100644 --- a/server/src/com/cloud/network/vpn/Site2SiteVpnManagerImpl.java +++ b/server/src/com/cloud/network/vpn/Site2SiteVpnManagerImpl.java @@ -623,7 +623,7 @@ public Pair, Integer> searchForVpnGateways(L Ternary domainIdRecursiveListProject = new Ternary(domainId, isRecursive, null); - _accountMgr.buildACLSearchParameters(caller, id, accountName, null, permittedAccounts, domainIdRecursiveListProject, listAll, false); + _accountMgr.buildACLSearchParameters(caller, id, accountName, cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, listAll, false); domainId = domainIdRecursiveListProject.first(); isRecursive = domainIdRecursiveListProject.second(); ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); From 9ff3fe371e3b5db77fc1eb6e7c60280d674fd949 Mon Sep 17 00:00:00 2001 From: Wido den Hollander Date: Mon, 15 Jun 2015 14:15:26 +0200 Subject: [PATCH 007/115] CLOUDSTACK-8559: IP Source spoofing should not be allowed We did not verify if the packets leaving an Instance had the correct source address. Any IP packet not matching the Instance IP(s) will be dropped (cherry picked from commit 3e3c11ffcaf6ab736800dfdc777cb0681f58ddf1) Signed-off-by: Rohit Yadav --- scripts/vm/network/security_group.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/vm/network/security_group.py b/scripts/vm/network/security_group.py index 146e403789..44d20fc254 100755 --- a/scripts/vm/network/security_group.py +++ b/scripts/vm/network/security_group.py @@ -474,6 +474,7 @@ def default_network_rules(vm_name, vm_id, vm_ip, vm_mac, vif, brname, sec_ips): #don't let vm spoof its ip address if vm_ip is not None: + execute("iptables -A " + vmchain_default + " -m physdev --physdev-is-bridged --physdev-in " + vif + " -m set ! --set " + vmipsetName + " src -j DROP") execute("iptables -A " + vmchain_default + " -m physdev --physdev-is-bridged --physdev-in " + vif + " -m set --set " + vmipsetName + " src -p udp --dport 53 -j RETURN ") execute("iptables -A " + vmchain_default + " -m physdev --physdev-is-bridged --physdev-in " + vif + " -m set --set " + vmipsetName + " src -j " + vmchain_egress) execute("iptables -A " + vmchain_default + " -m physdev --physdev-is-bridged --physdev-out " + vif + " -j " + vmchain) From ad1fbc1b79691b33bed776a4c73415ce3f90bd53 Mon Sep 17 00:00:00 2001 From: Wido den Hollander Date: Mon, 15 Jun 2015 17:30:06 +0200 Subject: [PATCH 008/115] CLOUDSTACK-8560: Stat the resulting image after copying from template and return the size This way we update the DB with the actual size of the disk after deployment from template (cherry picked from commit 4b4c52ea77aedde663cd0238b774aa243c856f42) Signed-off-by: Rohit Yadav Conflicts: plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java --- .../kvm/storage/LibvirtStorageAdaptor.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java index 5edc07b6db..856a78ab81 100644 --- a/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/com/cloud/hypervisor/kvm/storage/LibvirtStorageAdaptor.java @@ -39,6 +39,7 @@ import com.ceph.rbd.Rbd; import com.ceph.rbd.RbdException; import com.ceph.rbd.RbdImage; +import com.ceph.rbd.jna.RbdImageInfo; import com.ceph.rbd.jna.RbdSnapInfo; import org.apache.cloudstack.utils.qemu.QemuImg; @@ -1243,6 +1244,10 @@ public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMSt destFile = new QemuImgFile(destPath, destFormat); try { qemu.convert(srcFile, destFile); + Map destInfo = qemu.info(destFile); + Long virtualSize = Long.parseLong(destInfo.get(new String("virtual_size"))); + newDisk.setVirtualSize(virtualSize); + newDisk.setSize(virtualSize); } catch (QemuImgException e) { s_logger.error("Failed to convert " + srcFile.getFileName() + " to " + destFile.getFileName() + " the error was: " + e.getMessage()); newDisk = null; @@ -1264,18 +1269,19 @@ public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMSt try { srcFile = new QemuImgFile(sourcePath, sourceFormat); + String rbdDestPath = destPool.getSourceDir() + "/" + name; String rbdDestFile = KVMPhysicalDisk.RBDStringBuilder(destPool.getSourceHost(), destPool.getSourcePort(), destPool.getAuthUserName(), destPool.getAuthSecret(), - destPool.getSourceDir() + "/" + name); + rbdDestPath); destFile = new QemuImgFile(rbdDestFile, destFormat); - s_logger.debug("Starting copy from source image " + srcFile.getFileName() + " to RBD image " + destPool.getSourceDir() + "/" + name); + s_logger.debug("Starting copy from source image " + srcFile.getFileName() + " to RBD image " + rbdDestPath); qemu.convert(srcFile, destFile); - s_logger.debug("Succesfully converted source image " + srcFile.getFileName() + " to RBD image " + destPool.getSourceDir() + "/" + name); + s_logger.debug("Succesfully converted source image " + srcFile.getFileName() + " to RBD image " + rbdDestPath); - /* We still have to create and protect a RBD snapshot in order to do cloning */ + /* We have to stat the RBD image to see how big it became afterwards */ Rados r = new Rados(destPool.getAuthUserName()); r.confSet("mon_host", destPool.getSourceHost() + ":" + destPool.getSourcePort()); r.confSet("key", destPool.getAuthSecret()); @@ -1287,8 +1293,12 @@ public KVMPhysicalDisk copyPhysicalDisk(KVMPhysicalDisk disk, String name, KVMSt Rbd rbd = new Rbd(io); RbdImage image = rbd.open(name); - + RbdImageInfo rbdInfo = image.stat(); + newDisk.setSize(rbdInfo.size); + newDisk.setVirtualSize(rbdInfo.size); + s_logger.debug("After copy the resulting RBD image " + rbdDestPath + " is " + rbdInfo.size + " bytes long"); rbd.close(image); + r.ioCtxDestroy(io); } catch (QemuImgException e) { s_logger.error("Failed to convert from " + srcFile.getFileName() + " to " + destFile.getFileName() + " the error was: " + e.getMessage()); From 570d162692b74cfcfc81db548cd73ae8f0ac6f5f Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Thu, 4 Jun 2015 16:48:14 +0200 Subject: [PATCH 009/115] CLOUDSTACK-8537 add check for unique public key and account on ssh keypair registration Signed-off-by: Daan Hoogland --- .../src/com/cloud/user/dao/SSHKeyPairDao.java | 2 ++ .../com/cloud/user/dao/SSHKeyPairDaoImpl.java | 9 +++++++++ .../com/cloud/server/ManagementServerImpl.java | 17 +++++++++++------ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/engine/schema/src/com/cloud/user/dao/SSHKeyPairDao.java b/engine/schema/src/com/cloud/user/dao/SSHKeyPairDao.java index 7a4ac40363..e035e9688c 100644 --- a/engine/schema/src/com/cloud/user/dao/SSHKeyPairDao.java +++ b/engine/schema/src/com/cloud/user/dao/SSHKeyPairDao.java @@ -35,4 +35,6 @@ public interface SSHKeyPairDao extends GenericDao { public boolean deleteByName(long accountId, long domainId, String name); + public SSHKeyPairVO findByPublicKey(long accountId, long domainId, String publicKey); + } diff --git a/engine/schema/src/com/cloud/user/dao/SSHKeyPairDaoImpl.java b/engine/schema/src/com/cloud/user/dao/SSHKeyPairDaoImpl.java index cfaa993e60..51e5fbeefd 100644 --- a/engine/schema/src/com/cloud/user/dao/SSHKeyPairDaoImpl.java +++ b/engine/schema/src/com/cloud/user/dao/SSHKeyPairDaoImpl.java @@ -72,6 +72,15 @@ public SSHKeyPairVO findByPublicKey(String publicKey) { return findOneBy(sc); } + @Override + public SSHKeyPairVO findByPublicKey(long accountId, long domainId, String publicKey) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("accountId", SearchCriteria.Op.EQ, accountId); + sc.addAnd("domainId", SearchCriteria.Op.EQ, domainId); + sc.addAnd("publicKey", SearchCriteria.Op.EQ, publicKey); + return findOneBy(sc); + } + @Override public boolean deleteByName(long accountId, long domainId, String name) { SSHKeyPairVO pair = findByName(accountId, domainId, name); diff --git a/server/src/com/cloud/server/ManagementServerImpl.java b/server/src/com/cloud/server/ManagementServerImpl.java index 33fb7781c1..e9f2e0bc9f 100755 --- a/server/src/com/cloud/server/ManagementServerImpl.java +++ b/server/src/com/cloud/server/ManagementServerImpl.java @@ -37,9 +37,6 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; -import org.apache.cloudstack.api.command.user.snapshot.UpdateSnapshotPolicyCmd; -import org.apache.commons.codec.binary.Base64; -import org.apache.log4j.Logger; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.affinity.AffinityGroupProcessor; import org.apache.cloudstack.affinity.dao.AffinityGroupVMMapDao; @@ -408,6 +405,7 @@ import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotPoliciesCmd; import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotsCmd; import org.apache.cloudstack.api.command.user.snapshot.RevertSnapshotCmd; +import org.apache.cloudstack.api.command.user.snapshot.UpdateSnapshotPolicyCmd; import org.apache.cloudstack.api.command.user.ssh.CreateSSHKeyPairCmd; import org.apache.cloudstack.api.command.user.ssh.DeleteSSHKeyPairCmd; import org.apache.cloudstack.api.command.user.ssh.ListSSHKeyPairsCmd; @@ -511,6 +509,8 @@ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.utils.identity.ManagementServerNode; +import org.apache.commons.codec.binary.Base64; +import org.apache.log4j.Logger; import com.cloud.agent.AgentManager; import com.cloud.agent.api.GetVncPortAnswer; @@ -3616,9 +3616,14 @@ public SSHKeyPair registerSSHKeyPair(RegisterSSHKeyPairCmd cmd) { Account owner = _accountMgr.finalizeOwner(caller, cmd.getAccountName(), cmd.getDomainId(), cmd.getProjectId()); - SSHKeyPairVO s = _sshKeyPairDao.findByName(owner.getAccountId(), owner.getDomainId(), cmd.getName()); - if (s != null) { - throw new InvalidParameterValueException("A key pair with name '" + cmd.getName() + "' already exists."); + SSHKeyPairVO existingPair = _sshKeyPairDao.findByName(owner.getAccountId(), owner.getDomainId(), cmd.getName()); + if (existingPair != null) { + throw new InvalidParameterValueException("A key pair with name '" + cmd.getName() + "' already exists for this account."); + } + + existingPair = _sshKeyPairDao.findByPublicKey(owner.getAccountId(), owner.getDomainId(), cmd.getPublicKey()); + if (existingPair != null) { + throw new InvalidParameterValueException("A key pair with name '" + cmd.getPublicKey() + "' already exists for this account."); } String name = cmd.getName(); From 6e3c6e82990cafdab8453ed56530dc619ba70338 Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Tue, 9 Jun 2015 23:57:00 +0200 Subject: [PATCH 010/115] CLOUDSTACK-8537 refactor registerSSHKeyPair() for legibility and testability reasons Signed-off-by: Daan Hoogland --- .../cloud/server/ManagementServerImpl.java | 110 +++++++++++++----- 1 file changed, 83 insertions(+), 27 deletions(-) diff --git a/server/src/com/cloud/server/ManagementServerImpl.java b/server/src/com/cloud/server/ManagementServerImpl.java index e9f2e0bc9f..08fd9e9315 100755 --- a/server/src/com/cloud/server/ManagementServerImpl.java +++ b/server/src/com/cloud/server/ManagementServerImpl.java @@ -16,7 +16,9 @@ // under the License. package com.cloud.server; +import java.io.UnsupportedEncodingException; import java.lang.reflect.Field; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -940,7 +942,7 @@ protected void checkPortParameters(String publicPort, String privatePort, String @Override public boolean archiveEvents(ArchiveEventsCmd cmd) { - Account caller = CallContext.current().getCallingAccount(); + Account caller = getCaller(); List ids = cmd.getIds(); boolean result = true; List permittedAccountIds = new ArrayList(); @@ -967,7 +969,7 @@ public boolean archiveEvents(ArchiveEventsCmd cmd) { @Override public boolean deleteEvents(DeleteEventsCmd cmd) { - Account caller = CallContext.current().getCallingAccount(); + Account caller = getCaller(); List ids = cmd.getIds(); boolean result = true; List permittedAccountIds = new ArrayList(); @@ -1091,8 +1093,7 @@ public Pair, Integer> searchForServers(ListHostsCmd cmd) { @Override public Ternary, Integer>, List, Map> listHostsForMigrationOfVM(Long vmId, Long startIndex, Long pageSize) { - // access check - only root admin can migrate VM - Account caller = CallContext.current().getCallingAccount(); + Account caller = getCaller(); if (!_accountMgr.isRootAdmin(caller.getId())) { if (s_logger.isDebugEnabled()) { s_logger.debug("Caller is not a root admin, permission denied to migrate the VM"); @@ -1269,8 +1270,7 @@ private boolean hasSuitablePoolsForVolume(VolumeVO volume, Host host, VirtualMac @Override public Pair, List> listStoragePoolsForMigrationOfVolume(Long volumeId) { - // Access check - only root administrator can migrate volumes. - Account caller = CallContext.current().getCallingAccount(); + Account caller = getCaller(); if (!_accountMgr.isRootAdmin(caller.getId())) { if (s_logger.isDebugEnabled()) { s_logger.debug("Caller is not a root admin, permission denied to migrate the volume"); @@ -1762,7 +1762,7 @@ public Pair, Integer> searchForIPAddresses(ListPublicI List permittedAccounts = new ArrayList(); ListProjectResourcesCriteria listProjectResourcesCriteria = null; if (isAllocated) { - Account caller = CallContext.current().getCallingAccount(); + Account caller = getCaller(); Ternary domainIdRecursiveListProject = new Ternary( cmd.getDomainId(), cmd.isRecursive(), null); @@ -2255,8 +2255,7 @@ public DomainVO updateDomain(UpdateDomainCmd cmd) { throw new InvalidParameterValueException("ROOT domain can not be edited with a new name"); } - // check permissions - Account caller = CallContext.current().getCallingAccount(); + Account caller = getCaller(); _accountMgr.checkAccess(caller, domain); // domain name is unique under the parent domain @@ -3304,7 +3303,7 @@ private String signRequest(String request, String key) { @Override public ArrayList getCloudIdentifierResponse(long userId) { - Account caller = CallContext.current().getCallingAccount(); + Account caller = getCaller(); // verify that user exists User user = _accountMgr.getUserIncludingRemoved(userId); @@ -3344,7 +3343,7 @@ public ArrayList getCloudIdentifierResponse(long userId) { public Map listCapabilities(ListCapabilitiesCmd cmd) { Map capabilities = new HashMap(); - Account caller = CallContext.current().getCallingAccount(); + Account caller = getCaller(); boolean securityGroupsEnabled = false; boolean elasticLoadBalancerEnabled = false; boolean KVMSnapshotEnabled = false; @@ -3410,7 +3409,7 @@ public GuestOSHypervisorVO getGuestOsHypervisor(Long guestOsHypervisorId) { @Override public InstanceGroupVO updateVmGroup(UpdateVMGroupCmd cmd) { - Account caller = CallContext.current().getCallingAccount(); + Account caller = getCaller(); Long groupId = cmd.getId(); String groupName = cmd.getGroupName(); @@ -3528,7 +3527,7 @@ public List getHypervisors(Long zoneId) { @Override public SSHKeyPair createSSHKeyPair(CreateSSHKeyPairCmd cmd) { - Account caller = CallContext.current().getCallingAccount(); + Account caller = getCaller(); String accountName = cmd.getAccountName(); Long domainId = cmd.getDomainId(); Long projectId = cmd.getProjectId(); @@ -3552,7 +3551,7 @@ public SSHKeyPair createSSHKeyPair(CreateSSHKeyPairCmd cmd) { @Override public boolean deleteSSHKeyPair(DeleteSSHKeyPairCmd cmd) { - Account caller = CallContext.current().getCallingAccount(); + Account caller = getCaller(); String accountName = cmd.getAccountName(); Long domainId = cmd.getDomainId(); Long projectId = cmd.getProjectId(); @@ -3580,7 +3579,7 @@ public Pair, Integer> listSSHKeyPairs(ListSSHKeyPairs String name = cmd.getName(); String fingerPrint = cmd.getFingerprint(); - Account caller = CallContext.current().getCallingAccount(); + Account caller = getCaller(); List permittedAccounts = new ArrayList(); Ternary domainIdRecursiveListProject = new Ternary( @@ -3612,30 +3611,87 @@ public Pair, Integer> listSSHKeyPairs(ListSSHKeyPairs @Override @ActionEvent(eventType = EventTypes.EVENT_REGISTER_SSH_KEYPAIR, eventDescription = "registering ssh keypair", async = true) public SSHKeyPair registerSSHKeyPair(RegisterSSHKeyPairCmd cmd) { - Account caller = CallContext.current().getCallingAccount(); + Account owner = getOwner(cmd); + checkForKeyByName(cmd, owner); + checkForKeyByPublicKey(cmd, owner); - Account owner = _accountMgr.finalizeOwner(caller, cmd.getAccountName(), cmd.getDomainId(), cmd.getProjectId()); + String name = cmd.getName(); + String key = cmd.getPublicKey(); + try { + key = URLDecoder.decode(key, "UTF-8"); + } catch (UnsupportedEncodingException e) { + s_logger.warn("key decoding tried invain: " + e.getLocalizedMessage()); + } + String publicKey = getPublicKeyFromKeyKeyMaterial(key); + String fingerprint = getFingerprint(publicKey); - SSHKeyPairVO existingPair = _sshKeyPairDao.findByName(owner.getAccountId(), owner.getDomainId(), cmd.getName()); + return createAndSaveSSHKeyPair(name, fingerprint, publicKey, null, owner); + } + + /** + * @param cmd + * @param owner + * @throws InvalidParameterValueException + */ + private void checkForKeyByPublicKey(RegisterSSHKeyPairCmd cmd, Account owner) throws InvalidParameterValueException { + SSHKeyPairVO existingPair = _sshKeyPairDao.findByPublicKey(owner.getAccountId(), owner.getDomainId(), cmd.getPublicKey()); if (existingPair != null) { - throw new InvalidParameterValueException("A key pair with name '" + cmd.getName() + "' already exists for this account."); + throw new InvalidParameterValueException("A key pair with name '" + cmd.getPublicKey() + "' already exists for this account."); } + } - existingPair = _sshKeyPairDao.findByPublicKey(owner.getAccountId(), owner.getDomainId(), cmd.getPublicKey()); + /** + * @param cmd + * @param owner + * @throws InvalidParameterValueException + */ + private void checkForKeyByName(RegisterSSHKeyPairCmd cmd, Account owner) throws InvalidParameterValueException { + SSHKeyPairVO existingPair = _sshKeyPairDao.findByName(owner.getAccountId(), owner.getDomainId(), cmd.getName()); if (existingPair != null) { - throw new InvalidParameterValueException("A key pair with name '" + cmd.getPublicKey() + "' already exists for this account."); + throw new InvalidParameterValueException("A key pair with name '" + cmd.getName() + "' already exists for this account."); } + } - String name = cmd.getName(); - String publicKey = SSHKeysHelper.getPublicKeyFromKeyMaterial(cmd.getPublicKey()); + /** + * @param publicKey + * @return + */ + private String getFingerprint(String publicKey) { + String fingerprint = SSHKeysHelper.getPublicKeyFingerprint(publicKey); + return fingerprint; + } + + /** + * @param key + * @return + * @throws InvalidParameterValueException + */ + private String getPublicKeyFromKeyKeyMaterial(String key) throws InvalidParameterValueException { + String publicKey = SSHKeysHelper.getPublicKeyFromKeyMaterial(key); if (publicKey == null) { throw new InvalidParameterValueException("Public key is invalid"); } + return publicKey; + } - String fingerprint = SSHKeysHelper.getPublicKeyFingerprint(publicKey); + /** + * @param cmd + * @return + */ + private Account getOwner(RegisterSSHKeyPairCmd cmd) { + Account caller = getCaller(); - return createAndSaveSSHKeyPair(name, fingerprint, publicKey, null, owner); + Account owner = _accountMgr.finalizeOwner(caller, cmd.getAccountName(), cmd.getDomainId(), cmd.getProjectId()); + return owner; + } + + /** + * @return + */ + private Account getCaller() { + Account caller = CallContext.current().getCallingAccount(); + return caller; } private SSHKeyPair createAndSaveSSHKeyPair(String name, String fingerprint, String publicKey, String privateKey, Account owner) { @@ -3655,7 +3711,7 @@ private SSHKeyPair createAndSaveSSHKeyPair(String name, String fingerprint, Stri @Override public String getVMPassword(GetVMPasswordCmd cmd) { - Account caller = CallContext.current().getCallingAccount(); + Account caller = getCaller(); UserVmVO vm = _userVmDao.findById(cmd.getId()); if (vm == null) { @@ -3831,7 +3887,7 @@ public VirtualMachine upgradeSystemVM(UpgradeSystemVMCmd cmd) { } private VirtualMachine upgradeStoppedSystemVm(Long systemVmId, Long serviceOfferingId, Map customparameters) { - Account caller = CallContext.current().getCallingAccount(); + Account caller = getCaller(); VMInstanceVO systemVm = _vmInstanceDao.findByIdTypes(systemVmId, VirtualMachine.Type.ConsoleProxy, VirtualMachine.Type.SecondaryStorageVm); if (systemVm == null) { From 4d096ea0e10906138747fb2643a8fa84b5f11fd1 Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Mon, 15 Jun 2015 16:11:36 +0200 Subject: [PATCH 011/115] CLOUDSTACK-8537 test for the sake of testing the fix seems so trivial but no testing is available for it at all. when bugs arise test extension should be the start point here. Signed-off-by: Daan Hoogland This closes #357 --- .../cloud/server/ManagementServerImpl.java | 8 +-- .../server/ManagementServerImplTest.java | 67 +++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 server/test/com/cloud/server/ManagementServerImplTest.java diff --git a/server/src/com/cloud/server/ManagementServerImpl.java b/server/src/com/cloud/server/ManagementServerImpl.java index 08fd9e9315..5946b292f7 100755 --- a/server/src/com/cloud/server/ManagementServerImpl.java +++ b/server/src/com/cloud/server/ManagementServerImpl.java @@ -745,7 +745,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe @Inject private InstanceGroupDao _vmGroupDao; @Inject - private SSHKeyPairDao _sshKeyPairDao; + protected SSHKeyPairDao _sshKeyPairDao; @Inject private LoadBalancerDao _loadbalancerDao; @Inject @@ -3645,7 +3645,7 @@ private void checkForKeyByPublicKey(RegisterSSHKeyPairCmd cmd, Account owner) th * @param owner * @throws InvalidParameterValueException */ - private void checkForKeyByName(RegisterSSHKeyPairCmd cmd, Account owner) throws InvalidParameterValueException { + protected void checkForKeyByName(RegisterSSHKeyPairCmd cmd, Account owner) throws InvalidParameterValueException { SSHKeyPairVO existingPair = _sshKeyPairDao.findByName(owner.getAccountId(), owner.getDomainId(), cmd.getName()); if (existingPair != null) { throw new InvalidParameterValueException("A key pair with name '" + cmd.getName() + "' already exists for this account."); @@ -3679,7 +3679,7 @@ private String getPublicKeyFromKeyKeyMaterial(String key) throws InvalidParamete * @param cmd * @return */ - private Account getOwner(RegisterSSHKeyPairCmd cmd) { + protected Account getOwner(RegisterSSHKeyPairCmd cmd) { Account caller = getCaller(); Account owner = _accountMgr.finalizeOwner(caller, cmd.getAccountName(), cmd.getDomainId(), cmd.getProjectId()); @@ -3689,7 +3689,7 @@ private Account getOwner(RegisterSSHKeyPairCmd cmd) { /** * @return */ - private Account getCaller() { + protected Account getCaller() { Account caller = CallContext.current().getCallingAccount(); return caller; } diff --git a/server/test/com/cloud/server/ManagementServerImplTest.java b/server/test/com/cloud/server/ManagementServerImplTest.java new file mode 100644 index 0000000000..1e530e6372 --- /dev/null +++ b/server/test/com/cloud/server/ManagementServerImplTest.java @@ -0,0 +1,67 @@ +// 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 com.cloud.server; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; + +import org.apache.cloudstack.api.command.user.ssh.RegisterSSHKeyPairCmd; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.user.Account; +import com.cloud.user.SSHKeyPairVO; +import com.cloud.user.dao.SSHKeyPairDao; + +@RunWith(MockitoJUnitRunner.class) +public class ManagementServerImplTest { + + @Mock + RegisterSSHKeyPairCmd regCmd; + @Mock + SSHKeyPairVO existingPair; + @Mock + Account account; + @Mock + SSHKeyPairDao sshKeyPairDao; + ManagementServerImpl ms = new ManagementServerImpl(); + @Spy + ManagementServerImpl spy; + + @Test(expected = InvalidParameterValueException.class) + public void testExistingPairRegistration() { + String accountName = "account"; + String publicKeyString = "very public"; + // setup owner with domainid + Mockito.doReturn(account).when(spy).getCaller(); + Mockito.doReturn(account).when(spy).getOwner(regCmd); + // mock _sshKeyPairDao.findByName to return null + Mockito.doNothing().when(spy).checkForKeyByName(regCmd, account); + // mock _sshKeyPairDao.findByPublicKey to return a known object + Mockito.doReturn(accountName).when(regCmd).getAccountName(); + Mockito.doReturn(publicKeyString).when(regCmd).getPublicKey(); + Mockito.doReturn("name").when(regCmd).getName(); + spy._sshKeyPairDao = sshKeyPairDao; + Mockito.doReturn(1L).when(account).getAccountId(); + Mockito.doReturn(1L).when(account).getDomainId(); + Mockito.doReturn(existingPair).when(sshKeyPairDao).findByPublicKey(1L, 1L, publicKeyString); + spy.registerSSHKeyPair(regCmd); + } +} From 88fb8431f235b92f7de291413a6880a7ea08de1b Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Tue, 16 Jun 2015 17:12:43 +0200 Subject: [PATCH 012/115] CLOUDSTACK-8545 make reboot on out of band migration configurable --- .../VirtualNetworkApplianceManager.java | 9 ++- .../VirtualNetworkApplianceManagerImpl.java | 56 +++++++++---------- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/server/src/com/cloud/network/router/VirtualNetworkApplianceManager.java b/server/src/com/cloud/network/router/VirtualNetworkApplianceManager.java index 443402e7bc..4022b56bff 100644 --- a/server/src/com/cloud/network/router/VirtualNetworkApplianceManager.java +++ b/server/src/com/cloud/network/router/VirtualNetworkApplianceManager.java @@ -53,6 +53,7 @@ public interface VirtualNetworkApplianceManager extends Manager, VirtualNetworkA static final String RouterTemplateLxcCK = "router.template.lxc"; static final String SetServiceMonitorCK = "network.router.EnableServiceMonitoring"; static final String RouterAlertsCheckIntervalCK = "router.alerts.check.interval"; + static final String RouterReprovisionOnOutOfBandMigrationCK = "router.reboot.when.outofband.migrated"; static final ConfigKey RouterTemplateXen = new ConfigKey(String.class, RouterTemplateXenCK, "Advanced", "SystemVM Template (XenServer)", "Name of the default router template on Xenserver.", true, ConfigKey.Scope.Zone, null); @@ -64,12 +65,16 @@ public interface VirtualNetworkApplianceManager extends Manager, VirtualNetworkA "Name of the default router template on Hyperv.", true, ConfigKey.Scope.Zone, null); static final ConfigKey RouterTemplateLxc = new ConfigKey(String.class, RouterTemplateLxcCK, "Advanced", "SystemVM Template (LXC)", "Name of the default router template on LXC.", true, ConfigKey.Scope.Zone, null); - static final ConfigKey SetServiceMonitor = new ConfigKey(String.class, SetServiceMonitorCK, "Advanced", "true", "service monitoring in router enable/disable option, default true", true, ConfigKey.Scope.Zone, null); - static final ConfigKey RouterAlertsCheckInterval = new ConfigKey(Integer.class, RouterAlertsCheckIntervalCK, "Advanced", "1800", "Interval (in seconds) to check for alerts in Virtual Router.", false, ConfigKey.Scope.Global, null); + static final ConfigKey routerVersionCheckEnabled = new ConfigKey("Advanced", Boolean.class, "router.version.check", "true", + "If true, router minimum required version is checked before sending command", false); + static final ConfigKey UseExternalDnsServers = new ConfigKey(Boolean.class, "use.external.dns", "Advanced", "false", + "Bypass internal dns, use external dns1 and dns2", true, ConfigKey.Scope.Zone, null); + static final ConfigKey RouterReprovisionOnOutOfBandMigration = new ConfigKey(Boolean.class, RouterReprovisionOnOutOfBandMigrationCK, "Advanced", "false", + "Reboot routers when they are migrated out of band in order to reprovision", true, ConfigKey.Scope.Zone, null); public static final int DEFAULT_ROUTER_VM_RAMSIZE = 256; // 256M public static final int DEFAULT_ROUTER_CPU_MHZ = 500; // 500 MHz diff --git a/server/src/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index 46463cdf66..698f94dce5 100755 --- a/server/src/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -676,13 +676,6 @@ public VirtualRouter rebootRouter(final long routerId, final boolean reprogramNe } } - static final ConfigKey UseExternalDnsServers = new ConfigKey(Boolean.class, "use.external.dns", "Advanced", "false", - "Bypass internal dns, use external dns1 and dns2", true, ConfigKey.Scope.Zone, null); - - static final ConfigKey routerVersionCheckEnabled = new ConfigKey("Advanced", Boolean.class, "router.version.check", "true", - "If true, router minimum required version is checked before sending command", false); - - @Override public boolean configure(final String name, final Map params) throws ConfigurationException { @@ -4466,7 +4459,7 @@ public String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { - return new ConfigKey[] {UseExternalDnsServers, routerVersionCheckEnabled, SetServiceMonitor, RouterAlertsCheckInterval}; + return new ConfigKey[] {UseExternalDnsServers, routerVersionCheckEnabled, SetServiceMonitor, RouterAlertsCheckInterval, RouterReprovisionOnOutOfBandMigration}; } @Override @@ -4479,32 +4472,33 @@ public boolean postStateTransitionEvent(StateMachine2.Transition - if (opaque != null && opaque instanceof Pair) { - Pair pair = (Pair)opaque; - Object first = pair.first(); - Object second = pair.second(); - // powerHostId cannot be null in case of out-of-band VM movement - if (second != null && second instanceof Long) { - Long powerHostId = (Long)second; - Long hostId = null; - if (first != null && first instanceof Long) { - hostId = (Long)first; - } - // The following scenarios are due to out-of-band VM movement - // 1. If VM is in stopped state in CS due to 'PowerMissing' report from old host (hostId is null) and then there is a 'PowerOn' report from new host - // 2. If VM is in running state in CS and there is a 'PowerOn' report from new host - if (hostId == null || (hostId.longValue() != powerHostId.longValue())) { - s_logger.info("Schedule a router reboot task as router " + vo.getId() + " is powered-on out-of-band, need to reboot to refresh network rules"); - _rebootRouterExecutor.execute(new RebootTask(vo.getId())); - } - } + boolean reprovision_out_of_band = RouterReprovisionOnOutOfBandMigration.value(); + if ( + (vo.getType() == VirtualMachine.Type.DomainRouter) && + ((oldState == State.Stopped) || (reprovision_out_of_band && isOutOfBandMigrated(opaque))) && + (event == VirtualMachine.Event.FollowAgentPowerOnReport) && + (newState == State.Running)) { + s_logger.info("Schedule a router reboot task as router " + vo.getId() + " is powered-on out-of-band. we need to reboot to refresh network rules"); + _executor.schedule(new RebootTask(vo.getId()), 1000, TimeUnit.MICROSECONDS); + } + return true; + } + + private boolean isOutOfBandMigrated(Object opaque) { + if (opaque != null && opaque instanceof Pair) { + Pair pair = (Pair)opaque; + Object first = pair.first(); + Object second = pair.second(); + if (first != null && second != null && first instanceof Long && second instanceof Long) { + Long hostId = (Long)first; + Long powerHostId = (Long)second; + // If VM host known to CS is different from 'PowerOn' report host, then it is out-of-band movement + if (hostId.longValue() != powerHostId.longValue()) { + return true; } } } - return true; + return false; } protected class RebootTask extends ManagedContextRunnable { From 85e2d9611f90d00e0d61b2cfd6a1779d400eb60b Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Thu, 18 Jun 2015 08:04:50 +0200 Subject: [PATCH 013/115] CLOUDSTACK-8545 alert when out of band migration is detected and not acted upon --- .../router/VirtualNetworkApplianceManagerImpl.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/src/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java index 698f94dce5..e0d1edb5f8 100755 --- a/server/src/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java +++ b/server/src/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java @@ -4480,7 +4480,15 @@ public boolean postStateTransitionEvent(StateMachine2.Transition Date: Wed, 17 Jun 2015 15:16:54 +0530 Subject: [PATCH 014/115] Fixed issue in adding vm SG rules on vm reboot for xenserver 6.5 Signed-off-by: Rohit Yadav This closes #479 (cherry picked from commit 59e6596fefe55b56d414b1c4a8a665153aba1035) Signed-off-by: Rohit Yadav --- scripts/vm/hypervisor/xenserver/vmops | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/vm/hypervisor/xenserver/vmops b/scripts/vm/hypervisor/xenserver/vmops index e5e5c40ee4..b5ac17f146 100755 --- a/scripts/vm/hypervisor/xenserver/vmops +++ b/scripts/vm/hypervisor/xenserver/vmops @@ -938,10 +938,9 @@ def network_rules_for_rebooted_vm(session, vmName): for cmd in [delcmd, delcmd2, inscmd, inscmd2, inscmd3, inscmd4]: cmds = util.pread2(['/bin/bash', '-c', cmd]).split('\n') cmds.pop() - for c in cmds: + for c in filter(None,cmds): ipt = c.split(' ') ipt.insert(0, 'iptables') - ipt.pop() ipts.append(ipt) for ipt in ipts: From 04c7cf4e15a24f3717223c42a5adafe14404d416 Mon Sep 17 00:00:00 2001 From: Abhinandan Prateek Date: Fri, 19 Jun 2015 09:24:56 +0530 Subject: [PATCH 015/115] CLOUDSTACK-8570:Exception in calculating reserved capacity for dynamic service offering --- .../com/cloud/capacity/CapacityManagerImpl.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/server/src/com/cloud/capacity/CapacityManagerImpl.java b/server/src/com/cloud/capacity/CapacityManagerImpl.java index f91a20a19a..4c715ae8e8 100755 --- a/server/src/com/cloud/capacity/CapacityManagerImpl.java +++ b/server/src/com/cloud/capacity/CapacityManagerImpl.java @@ -645,8 +645,18 @@ public void updateCapacityForHost(final Host host) { ramOvercommitRatio = Float.parseFloat(vmDetailRam.getValue()); } ServiceOffering so = offeringsMap.get(vm.getServiceOfferingId()); - reservedMemory += ((so.getRamSize() * 1024L * 1024L) / ramOvercommitRatio) * clusterRamOvercommitRatio; - reservedCpu += (so.getCpu() * so.getSpeed() / cpuOvercommitRatio) * clusterCpuOvercommitRatio; + Map vmDetails = _userVmDetailsDao.listDetailsKeyPairs(vm.getId()); + if (so.isDynamic()) { + reservedMemory += + ((Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.memory.name())) * 1024L * 1024L) / ramOvercommitRatio) * + clusterRamOvercommitRatio; + reservedCpu += + ((Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.cpuNumber.name())) * Integer.parseInt(vmDetails.get(UsageEventVO.DynamicParameters.cpuSpeed.name()))) / cpuOvercommitRatio) * + clusterCpuOvercommitRatio; + } else { + reservedMemory += ((so.getRamSize() * 1024L * 1024L) / ramOvercommitRatio) * clusterRamOvercommitRatio; + reservedCpu += (so.getCpu() * so.getSpeed() / cpuOvercommitRatio) * clusterCpuOvercommitRatio; + } } else { // signal if not done already, that the VM has been stopped for skip.counting.hours, // hence capacity will not be reserved anymore. From eb904cd8fde640599fda87271729f53aa8022313 Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Fri, 19 Jun 2015 14:02:13 +0200 Subject: [PATCH 016/115] findbugs: repeated condition seems c&p error the tested states sugest that EXPUNGED should be the last one Signed-off-by: Rohit Yadav This closes #491 (cherry picked from commit bb613baa2b716dfe9aa7aed08b6cdd2027395456) Signed-off-by: Rohit Yadav --- server/src/com/cloud/storage/VolumeApiServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/com/cloud/storage/VolumeApiServiceImpl.java b/server/src/com/cloud/storage/VolumeApiServiceImpl.java index 0f17a57ad4..b45b8aa872 100644 --- a/server/src/com/cloud/storage/VolumeApiServiceImpl.java +++ b/server/src/com/cloud/storage/VolumeApiServiceImpl.java @@ -1105,7 +1105,7 @@ public boolean deleteVolume(long volumeId, Account caller) throws ConcurrentOper } try { - if (volume.getState() != Volume.State.Destroy && volume.getState() != Volume.State.Expunging && volume.getState() != Volume.State.Expunging) { + if (volume.getState() != Volume.State.Destroy && volume.getState() != Volume.State.Expunging && volume.getState() != Volume.State.Expunged) { Long instanceId = volume.getInstanceId(); if (!volService.destroyVolume(volume.getId())) { return false; From a8959bca553013d7b8182dfd355a2ecb799c3995 Mon Sep 17 00:00:00 2001 From: Daan Hoogland Date: Sun, 19 Apr 2015 18:10:03 +0200 Subject: [PATCH 017/115] 4.4.4 to 4.5.2 upgrade Signed-off-by: Rohit Yadav This closes #528 --- .../cloud/upgrade/DatabaseUpgradeChecker.java | 3 + .../cloud/upgrade/dao/Upgrade443to444.java | 64 +++++++++++++++++++ .../cloud/upgrade/dao/Upgrade444to450.java | 30 +++++++++ setup/db/db/schema-443to444.sql | 20 ++++++ 4 files changed, 117 insertions(+) create mode 100644 engine/schema/src/com/cloud/upgrade/dao/Upgrade443to444.java create mode 100644 engine/schema/src/com/cloud/upgrade/dao/Upgrade444to450.java create mode 100644 setup/db/db/schema-443to444.sql diff --git a/engine/schema/src/com/cloud/upgrade/DatabaseUpgradeChecker.java b/engine/schema/src/com/cloud/upgrade/DatabaseUpgradeChecker.java index 2ad1481995..6cdc4c1938 100755 --- a/engine/schema/src/com/cloud/upgrade/DatabaseUpgradeChecker.java +++ b/engine/schema/src/com/cloud/upgrade/DatabaseUpgradeChecker.java @@ -72,6 +72,7 @@ import com.cloud.upgrade.dao.Upgrade441to442; import com.cloud.upgrade.dao.Upgrade442to450; import com.cloud.upgrade.dao.Upgrade443to450; +import com.cloud.upgrade.dao.Upgrade444to450; import com.cloud.upgrade.dao.Upgrade450to451; import com.cloud.upgrade.dao.Upgrade451to452; import com.cloud.upgrade.dao.UpgradeSnapshot217to224; @@ -258,6 +259,8 @@ public DatabaseUpgradeChecker() { _upgradeMap.put("4.4.3", new DbUpgrade[] {new Upgrade443to450(), new Upgrade450to451(), new Upgrade451to452()}); + _upgradeMap.put("4.4.4", new DbUpgrade[] {new Upgrade444to450(), new Upgrade450to451(), new Upgrade451to452()}); + _upgradeMap.put("4.5.0", new DbUpgrade[] {new Upgrade450to451(), new Upgrade451to452()}); _upgradeMap.put("4.5.1", new DbUpgrade[] {new Upgrade451to452()}); diff --git a/engine/schema/src/com/cloud/upgrade/dao/Upgrade443to444.java b/engine/schema/src/com/cloud/upgrade/dao/Upgrade443to444.java new file mode 100644 index 0000000000..ded67d6495 --- /dev/null +++ b/engine/schema/src/com/cloud/upgrade/dao/Upgrade443to444.java @@ -0,0 +1,64 @@ +// 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 com.cloud.upgrade.dao; + +import java.io.File; +import java.sql.Connection; + +import org.apache.log4j.Logger; + +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.script.Script; + +public class Upgrade443to444 implements DbUpgrade { + final static Logger s_logger = Logger.getLogger(Upgrade443to444.class); + + @Override + public String[] getUpgradableVersionRange() { + return new String[] {"4.4.3", "4.4.4"}; + } + + @Override + public String getUpgradedVersion() { + return "4.4.4"; + } + + @Override + public boolean supportsRollingUpgrade() { + return false; + } + + @Override + public File[] getPrepareScripts() { + String script = Script.findScript("", "db/schema-443to444.sql"); + if (script == null) { + throw new CloudRuntimeException("Unable to find db/schema-empty.sql"); + } + + return new File[] {new File(script)}; + } + + @Override + public void performDataMigration(Connection conn) { + } + + @Override + public File[] getCleanupScripts() { + return null; + } +} diff --git a/engine/schema/src/com/cloud/upgrade/dao/Upgrade444to450.java b/engine/schema/src/com/cloud/upgrade/dao/Upgrade444to450.java new file mode 100644 index 0000000000..d872e528bf --- /dev/null +++ b/engine/schema/src/com/cloud/upgrade/dao/Upgrade444to450.java @@ -0,0 +1,30 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package com.cloud.upgrade.dao; + +import org.apache.log4j.Logger; + +public class Upgrade444to450 extends Upgrade442to450 implements DbUpgrade { + + final static Logger s_logger = Logger.getLogger(Upgrade444to450.class); + + @Override + public String[] getUpgradableVersionRange() { + return new String[] {"4.4.4", "4.5.0"}; + } +} diff --git a/setup/db/db/schema-443to444.sql b/setup/db/db/schema-443to444.sql new file mode 100644 index 0000000000..44e0406a8e --- /dev/null +++ b/setup/db/db/schema-443to444.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 upgrade from 4.4.3 to 4.4.4; +--; From 20ce346f3acb794b08a51841bab2188d426bf7dc Mon Sep 17 00:00:00 2001 From: Rohit Yadav Date: Thu, 28 May 2015 14:50:12 +0200 Subject: [PATCH 018/115] 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 This closes #489 --- api/src/com/cloud/user/User.java | 7 +- api/src/com/cloud/user/UserAccount.java | 4 + .../apache/cloudstack/api/ApiConstants.java | 3 +- .../classes/resources/messages.properties | 5 +- .../resources/messages_fr_FR.properties | 1 - .../classes/resources/messages_hu.properties | 1 - .../resources/messages_ja_JP.properties | 1 - client/tomcatconf/commands.properties.in | 3 + developer/developer-prefill.sql | 5 - developer/developer-saml.sql | 63 +++ developer/pom.xml | 58 +++ .../cloud/upgrade/dao/Upgrade451to452.java | 19 +- .../src/com/cloud/user/UserAccountVO.java | 11 + engine/schema/src/com/cloud/user/UserVO.java | 10 + .../com/cloud/user/dao/UserAccountDao.java | 4 + .../cloud/user/dao/UserAccountDaoImpl.java | 19 +- plugins/user-authenticators/saml2/pom.xml | 5 + .../cloudstack/saml2/spring-saml2-context.xml | 3 + .../api/command/AuthorizeSAMLSSOCmd.java | 105 +++++ .../GetServiceProviderMetaDataCmd.java | 82 +++- .../cloudstack/api/command/ListIdpsCmd.java | 114 +++++ .../api/command/ListSamlAuthorizationCmd.java | 95 ++++ .../SAML2LoginAPIAuthenticatorCmd.java | 285 +++++++----- .../SAML2LogoutAPIAuthenticatorCmd.java | 29 +- .../cloudstack/api/response/IdpResponse.java | 62 +++ .../response/SamlAuthorizationResponse.java | 68 +++ .../cloudstack/saml/SAML2AuthManager.java | 67 ++- .../cloudstack/saml/SAML2AuthManagerImpl.java | 430 ++++++++++++++---- .../saml/SAML2UserAuthenticator.java | 28 +- .../cloudstack/saml/SAMLPluginConstants.java | 30 ++ .../cloudstack/saml/SAMLProviderMetadata.java | 122 +++++ .../apache/cloudstack/saml/SAMLTokenDao.java | 23 + .../cloudstack/saml/SAMLTokenDaoImpl.java | 51 +++ .../apache/cloudstack/saml/SAMLTokenVO.java | 97 ++++ .../apache/cloudstack/saml}/SAMLUtils.java | 136 +++--- .../GetServiceProviderMetaDataCmdTest.java | 30 +- .../SAML2UserAuthenticatorTest.java | 8 +- .../org/apache/cloudstack}/SAMLUtilsTest.java | 29 +- .../SAML2LoginAPIAuthenticatorCmdTest.java | 45 +- .../SAML2LogoutAPIAuthenticatorCmdTest.java | 15 +- server/src/com/cloud/api/ApiServer.java | 4 +- server/src/com/cloud/api/ApiServlet.java | 2 +- .../src/com/cloud/configuration/Config.java | 72 --- setup/db/db/schema-451to452-cleanup.sql | 20 + setup/db/db/schema-451to452.sql | 35 ++ tools/apidoc/gen_toc.py | 3 + ui/css/cloudstack3.css | 19 +- ui/dictionary.jsp | 5 +- ui/index.jsp | 49 +- ui/scripts/accounts.js | 102 +++++ ui/scripts/accountsWizard.js | 63 ++- ui/scripts/cloudStack.js | 22 +- ui/scripts/docs.js | 8 + ui/scripts/sharedFunctions.js | 1 + ui/scripts/ui-custom/accountsWizard.js | 5 + ui/scripts/ui-custom/login.js | 133 ++++-- 56 files changed, 2165 insertions(+), 551 deletions(-) create mode 100644 developer/developer-saml.sql create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/AuthorizeSAMLSSOCmd.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListSamlAuthorizationCmd.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/IdpResponse.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlAuthorizationResponse.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLPluginConstants.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLProviderMetadata.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDao.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDaoImpl.java create mode 100644 plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenVO.java rename {utils/src/org/apache/cloudstack/utils/auth => plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml}/SAMLUtils.java (72%) rename plugins/user-authenticators/saml2/test/org/apache/cloudstack/{api/command => }/GetServiceProviderMetaDataCmdTest.java (78%) rename {utils/test/org/apache/cloudstack/utils/auth => plugins/user-authenticators/saml2/test/org/apache/cloudstack}/SAMLUtilsTest.java (66%) create mode 100644 setup/db/db/schema-451to452-cleanup.sql create mode 100644 setup/db/db/schema-451to452.sql diff --git a/api/src/com/cloud/user/User.java b/api/src/com/cloud/user/User.java index 33d6235f32..1f0dcfd2b4 100644 --- a/api/src/com/cloud/user/User.java +++ b/api/src/com/cloud/user/User.java @@ -23,7 +23,7 @@ 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 enum Source { public Source getSource(); + void setSource(Source source); + + public String getExternalEntity(); + + public void setExternalEntity(String entity); } diff --git a/api/src/com/cloud/user/UserAccount.java b/api/src/com/cloud/user/UserAccount.java index d44fcf72a8..0449514cc1 100644 --- a/api/src/com/cloud/user/UserAccount.java +++ b/api/src/com/cloud/user/UserAccount.java @@ -63,4 +63,8 @@ public interface UserAccount extends InternalIdentity { int getLoginAttempts(); public User.Source getSource(); + + public String getExternalEntity(); + + public void setExternalEntity(String entity); } diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index b6aed6f80f..2471e08895 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"; diff --git a/client/WEB-INF/classes/resources/messages.properties b/client/WEB-INF/classes/resources/messages.properties index 523c7cc86a..7223552961 100644 --- a/client/WEB-INF/classes/resources/messages.properties +++ b/client/WEB-INF/classes/resources/messages.properties @@ -112,6 +112,7 @@ label.action.attach.iso=Attach ISO label.action.cancel.maintenance.mode.processing=Cancelling Maintenance Mode.... label.action.cancel.maintenance.mode=Cancel Maintenance Mode label.action.change.password=Change Password +label.action.configure.samlauthorization=Configure SAML SSO Authorization label.action.change.service.processing=Changing Service.... label.action.change.service=Change Service label.action.copy.ISO.processing=Copying ISO.... @@ -757,7 +758,9 @@ label.local.storage=Local Storage label.local=Local label.login=Login label.logout=Logout -label.saml.login=SAML Login +label.saml.enable=Authorize SAML SSO +label.saml.entity=Identity Provider +label.add.LDAP.account=Add LDAP Account label.LUN.number=LUN \# label.lun=LUN label.make.project.owner=Make account project owner diff --git a/client/WEB-INF/classes/resources/messages_fr_FR.properties b/client/WEB-INF/classes/resources/messages_fr_FR.properties index eddc664751..f2f2c60159 100644 --- a/client/WEB-INF/classes/resources/messages_fr_FR.properties +++ b/client/WEB-INF/classes/resources/messages_fr_FR.properties @@ -1249,7 +1249,6 @@ label.s3.nfs.server=Serveur NFS S3 label.s3.secret_key=Cl\u00e9 Priv\u00e9e label.s3.socket_timeout=D\u00e9lai d\\'expiration de la socket label.s3.use_https=Utiliser HTTPS -label.saml.login=Identifiant SAML label.saturday=Samedi label.save.and.continue=Enregistrer et continuer label.save=Sauvegarder diff --git a/client/WEB-INF/classes/resources/messages_hu.properties b/client/WEB-INF/classes/resources/messages_hu.properties index 9ae73e8fe1..bc3b6e6d1a 100644 --- a/client/WEB-INF/classes/resources/messages_hu.properties +++ b/client/WEB-INF/classes/resources/messages_hu.properties @@ -1249,7 +1249,6 @@ label.s3.nfs.server=S3 NFS kiszolg\u00e1l\u00f3 label.s3.secret_key=Titkos kulcs label.s3.socket_timeout=Kapcsolat id\u0151t\u00fall\u00e9p\u00e9s label.s3.use_https=HTTPS haszn\u00e1lata -label.saml.login=SAML bel\u00e9p\u00e9s label.saturday=Szombat label.save.and.continue=Ment\u00e9s \u00e9s folytat\u00e1s label.save=Ment\u00e9s diff --git a/client/WEB-INF/classes/resources/messages_ja_JP.properties b/client/WEB-INF/classes/resources/messages_ja_JP.properties index d662ea468f..cf3f07a936 100644 --- a/client/WEB-INF/classes/resources/messages_ja_JP.properties +++ b/client/WEB-INF/classes/resources/messages_ja_JP.properties @@ -1248,7 +1248,6 @@ label.s3.nfs.server=S3 NFS \u30b5\u30fc\u30d0\u30fc label.s3.secret_key=\u79d8\u5bc6\u30ad\u30fc label.s3.socket_timeout=\u30bd\u30b1\u30c3\u30c8 \u30bf\u30a4\u30e0\u30a2\u30a6\u30c8 label.s3.use_https=HTTPS \u3092\u4f7f\u7528\u3059\u308b -label.saml.login=SAML \u30ed\u30b0\u30a4\u30f3 label.saturday=\u571f\u66dc\u65e5 label.save.and.continue=\u4fdd\u5b58\u3057\u3066\u7d9a\u884c label.save=\u4fdd\u5b58 diff --git a/client/tomcatconf/commands.properties.in b/client/tomcatconf/commands.properties.in index a87d1677f2..a66a3dce48 100644 --- a/client/tomcatconf/commands.properties.in +++ b/client/tomcatconf/commands.properties.in @@ -26,6 +26,9 @@ logout=15 samlSso=15 samlSlo=15 getSPMetadata=15 +listIdps=15 +authorizeSamlSso=7 +listSamlAuthorization=7 ### Account commands createAccount=7 diff --git a/developer/developer-prefill.sql b/developer/developer-prefill.sql index 27b36e7858..3097203b41 100644 --- a/developer/developer-prefill.sql +++ b/developer/developer-prefill.sql @@ -83,9 +83,4 @@ INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) VALUES ('Advanced', 'DEFAULT', 'management-server', 'developer', 'true'); --- Enable SAML plugin for developers by default -INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) - VALUES ('Advanced', 'DEFAULT', 'management-server', - 'saml2.enabled', 'true'); - commit; diff --git a/developer/developer-saml.sql b/developer/developer-saml.sql new file mode 100644 index 0000000000..18afb28878 --- /dev/null +++ b/developer/developer-saml.sql @@ -0,0 +1,63 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. + +-- SAML keystore for testing, allows testing on ssocirlce and other public IdPs +-- with pre-seeded SP metadata +USE cloud; + +-- Enable SAML plugin for developers by default +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'SAML2-PLUGIN', + 'saml2.enabled', 'true') + ON DUPLICATE KEY UPDATE value=VALUES(value); + +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'SAML2-PLUGIN', + 'saml2.default.idpid', 'https://idp.bhaisaab.org/idp/shibboleth') + ON DUPLICATE KEY UPDATE value=VALUES(value); + +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'SAML2-PLUGIN', + 'saml2.idp.metadata.url', 'http://idp.bhaisaab.org/idp/shibboleth') + ON DUPLICATE KEY UPDATE value=VALUES(value); + +-- Enable LDAP source +INSERT INTO `cloud`.`ldap_configuration` (hostname, port) + VALUES ('idp.bhaisaab.org', 389); + +-- Fix ldap configs +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'management-server', + 'ldap.basedn', 'ou=people,dc=idp,dc=bhaisaab,dc=org') + ON DUPLICATE KEY UPDATE value=VALUES(value); + +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'management-server', + 'ldap.bind.principal', 'cn=admin,dc=idp,dc=bhaisaab,dc=org') + ON DUPLICATE KEY UPDATE value=VALUES(value); + +INSERT INTO `cloud`.`configuration` (category, instance, component, name, value) + VALUES ('Advanced', 'DEFAULT', 'management-server', + 'ldap.bind.password', 'password') + ON DUPLICATE KEY UPDATE value=VALUES(value); + +-- Add default set of certificates for testing +LOCK TABLES `keystore` WRITE; +/*!40000 ALTER TABLE `keystore` DISABLE KEYS */; +INSERT INTO `keystore` VALUES (1,'SAMLSP_KEYPAIR','MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDQDirtAajTrScXCUgXrsOBgQ++y+IUcyzXwUGEYBlM9kBCmcdlbt5zuYQ8pOvoOQz6CAVqSjYNbnbg1ph37Zfv/tzGUg2V5cpB5BfEyt2KY0mBFNbwa0LKnCAYlBlarm4XZF+oZ0maH6cdboHdHiEqNRscylnXYA2zHKU3YoEfOR+9acl4z54PAyuPjr9SWUPTAyYf326i0q+h3J4nT6FwBFK+yKSC1PeVG/viYQ0otU1UUDkQ3pX81qfJfN/6Ih7W4v73LgUlZsTBMzlu/kJzMuP5Wc5IkU6Mt+8EHMZeLnN0ZErkMk0DCQE14hG8W7S8/inUJpwJlmb5E634Zq8u0I1zVUmSmAGqvvqKJBGnqY5X/j2bsA8B2qFsrcxasIlkKaWLvY+AvXD5X0OHwIZzRbuuCtguSz671C7Cwok8R3N+e9ATDHmG9gC10NJaB6dUBA9p2UdA82TR73x6nGe8pLJnGyecEQfxz1+ptPVAENj1Rl3Wrwu/dbPd/X6inlYpuwlsnWi3LYrkguT/9W3Z2uuq5PTVT04zcyev+50gVnDPzTrNCZfHpmMNQIqZGFmFKz4m+VUZmzZOdg1Vx51t9+t7iHGHqFk6/vnqqWiyEuTEFAaC9krm1VNzGvno5LyNm5Dk9JGAHFfjgNV++viGTpSPLeEeMnlvPuQJy5OJMwIDAQABAoICABME6Imn+C35izRA5fU8RaUGDlFrw+wIp1XF1d5rBoURkchE1ISCQRWlJOCCVwpwhK4qo4wW4qARtA5Tr7Zu4s/OpZH/mDxWuEmTt1SHEv9+mg6RwCBUPdPVt91nVHYEsg2zYEc9we2z7Qv0uSxkf7WjCypzmQjmP/paqQPKHnGjQDKJhCBmIlXO/WFvNDAr9tZIWGjbfPqndeS/DTocvm5GBuZn4xoOq99Woo0MQC6zfDEz8DOJlX56hPYXU0ZDbjxInfQsoc3MejoLG7n4xkxPn6WAvynFFsAoZFIk60Faz7UZIfuAWafoX9L0KpjkbT5Fob9CFEuQEzO7x9CIWoUr2PYn8HbThHDUOFAuVVpOLqleLPCrxkX/P01WTrLFuT6vSJKW2kxVwiHvZH6pNT01X/nlHDD6Jd9oWse2jIDBVor6fMnNDtgKl9azKgyakxoOGB7BMcb5u0Im8vFBCCRIyN3lrYjjR1F3H1tvY6Q0fEGLkilO334IyjC63he6lZ6NqslE/3QWEyyIiCL52rMzadN2SwVNawCa8YIR6+TpBjKyqY17LCP57v3UyM6J/kcUqXxDRcg1XnsjiWU+u0j9ZdlBgcbNuQeb1jD2QgICcyr/tWyJ2asyVfvARcD/xt5a9AnGjO0LnwMfw/DdBz1XCxz5uf3gOM69+nXk2gWhAoIBAQDr7NhlmVrASpOJHXXvqkpC2P4+hx7bmwKUZPbqm32pqCBypn6Qd2h4wdFzcP41wN6kpYqmmsPBoezctSgromfHeVjTyjhGku8b1HqtyRoX5sqIIPtv5pKKGS/tVWfyqQ8BspcdhZaR7+ZiRsLRwOXCscRq82+vbyq5Jd1bjsDGeLtcYyASv3II1xTBzSgNlvB+WiFXIlGWT9IPXwhv6IxZn7ME/y992d7bhgPxCcdTfKQNPBpcKerjcNxeDMto8vVijBDqujVpTf+3YvOOzWrcLn5UORFzpVho7oml5+5qnkFI/RZoiagUixFeQMv5+72qKJrxJu3VfI3mxlzZm5YjAoIBAQDhwjvzNWCQ2y7wwwnr88snVxAhd7kByvbKmnFVzHFTy2lheyWkiAqXj9juqsCtbkOK8a1ogmFAHz3i0/n+QhcJ20gudryniMt+u+wKxpmEKyqHKX3d4biPOvkKx7tdfwnlRKpSWXuynlDNVaQnJKUqBqDygLaE2s0LK3Fwdl+HN5ZPjRcuHkNpXA8t5lbm3tttMIs3JMneKAq77rodgRg+JcYhUNiybek3cZQcEiIGoh8UU6AhgQIOyMy5lkdG7XwZ2FEMQlqZo+T4HnkdTMU1CbTav1/ZzwDQP4/BJvKXhdRBuYHHAwhV6NIEMk5fzXcOoYmhfOMjvftrSxqUOImxAoIBAQDrhaEuJB8d0hVhD6EZ5kWGYHvHzjp2/1Ne80AwS5Pyl5309tNow1vvGYZQGaAd53Icqgo1clE0b8M3Pj5g+RtjXnfXzovJoIvFm6Pw887xx3uu1EZOmr710FkxNE62SCFsD26ekSsUe4rh10RMA6cbaz3riySW3YKoHO3Tpjo6qHJas7ZkIOzleFoHcximIGXrrWyVQPRz+zF4GOYiWeQq4KvltB8kIylAu5QZwCpV5Rsc/0BNe6c68QN9fIZgOhPQEoYc3lHN04kR+V2t1NH2BxAkYmhSq+ELt/6AOn6fv2brR4VkTPAXuhFXp5Y59B+OzESJs9RAiLxcgvBUaOdDAoIBAQCzlPJjUL5z/Cam1j76NoAP1y25saa1SmJuX9Rvz6UGZvR42qDi9GSYk5CYqbODQgbwa7bpP21kuHVeDgj6vE/fQ1NzwnfnPOXC9nGZUMmlXUEDK3o4GenZ5atda+wbP4b7nVdvEkdXmp/j9pARoxDPEV7OCJ0nqXUZwYEHWOI8iXdD6JPb168ADH72oBfYpsYdYVQclWMPGQMQ46Gg/qPuK9YjglAd/1hZBjwu6C2w4R2f6bWjcR/V6t0Pc/9W6GqjlHNEMTQoqzrkNDlbmUn2GraGm1z/wa5/+U+88eJfrdFeRtZ5HGxxCjalp+64PpTKSq1UjCeSsvlgK+oEpcTBAoIBAQCDDcS69BnjFWNa68FBrA2Uw6acQ6lnpXALohXCRL5qOTMe/FFDEBo0ADGrGpSo+cPaE2buNsYO79CafqTxPoZ38OAtTVmX3uL3z9+2ne2dc486gmAnKdJA8w9uawqMEkVpTA9f4WiBJJVzPwAv19AJCPKfUaB8IdNPV+HL8CdK+Dm+lZBADlB9RyvkJRLVJUAuK8/h9kbS3myKI6FIBeFFJpXRONkBSEkANknMqelvdf0GQsHliRslqIK2QVTIOmkJKecG35OhZ5WtU54oSxljlvmtvEKkEJAhEUyfFQRwQTTsDxkFFsfIVr9gv8K1RVEb4D00GUY7GSyAgPKPNsib','MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0A4q7QGo060nFwlIF67DgYEPvsviFHMs18FBhGAZTPZAQpnHZW7ec7mEPKTr6DkM+ggFako2DW524NaYd+2X7/7cxlINleXKQeQXxMrdimNJgRTW8GtCypwgGJQZWq5uF2RfqGdJmh+nHW6B3R4hKjUbHMpZ12ANsxylN2KBHzkfvWnJeM+eDwMrj46/UllD0wMmH99uotKvodyeJ0+hcARSvsikgtT3lRv74mENKLVNVFA5EN6V/NanyXzf+iIe1uL+9y4FJWbEwTM5bv5CczLj+VnOSJFOjLfvBBzGXi5zdGRK5DJNAwkBNeIRvFu0vP4p1CacCZZm+ROt+GavLtCNc1VJkpgBqr76iiQRp6mOV/49m7APAdqhbK3MWrCJZCmli72PgL1w+V9Dh8CGc0W7rgrYLks+u9QuwsKJPEdzfnvQEwx5hvYAtdDSWgenVAQPadlHQPNk0e98epxnvKSyZxsnnBEH8c9fqbT1QBDY9UZd1q8Lv3Wz3f1+op5WKbsJbJ1oty2K5ILk//Vt2drrquT01U9OM3Mnr/udIFZwz806zQmXx6ZjDUCKmRhZhSs+JvlVGZs2TnYNVcedbffre4hxh6hZOv756qloshLkxBQGgvZK5tVTcxr56OS8jZuQ5PSRgBxX44DVfvr4hk6Ujy3hHjJ5bz7kCcuTiTMCAwEAAQ==','samlsp-keypair',NULL),(2,'SAMLSP_X509CERT','rO0ABXNyAC1qYXZhLnNlY3VyaXR5LmNlcnQuQ2VydGlmaWNhdGUkQ2VydGlmaWNhdGVSZXCJJ2qdya48DAIAAlsABGRhdGF0AAJbQkwABHR5cGV0ABJMamF2YS9sYW5nL1N0cmluZzt4cHVyAAJbQqzzF/gGCFTgAgAAeHAAAASzMIIErzCCApcCBgFNmkdlAzANBgkqhkiG9w0BAQsFADAbMRkwFwYDVQQDExBBcGFjaGVDbG91ZFN0YWNrMB4XDTE1MDUyNzExMjc1OVoXDTE4MDUyODExMjc1OVowGzEZMBcGA1UEAxMQQXBhY2hlQ2xvdWRTdGFjazCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANAOKu0BqNOtJxcJSBeuw4GBD77L4hRzLNfBQYRgGUz2QEKZx2Vu3nO5hDyk6+g5DPoIBWpKNg1uduDWmHftl+/+3MZSDZXlykHkF8TK3YpjSYEU1vBrQsqcIBiUGVqubhdkX6hnSZofpx1ugd0eISo1GxzKWddgDbMcpTdigR85H71pyXjPng8DK4+Ov1JZQ9MDJh/fbqLSr6HcnidPoXAEUr7IpILU95Ub++JhDSi1TVRQORDelfzWp8l83/oiHtbi/vcuBSVmxMEzOW7+QnMy4/lZzkiRToy37wQcxl4uc3RkSuQyTQMJATXiEbxbtLz+KdQmnAmWZvkTrfhmry7QjXNVSZKYAaq++ookEaepjlf+PZuwDwHaoWytzFqwiWQppYu9j4C9cPlfQ4fAhnNFu64K2C5LPrvULsLCiTxHc3570BMMeYb2ALXQ0loHp1QED2nZR0DzZNHvfHqcZ7yksmcbJ5wRB/HPX6m09UAQ2PVGXdavC791s939fqKeVim7CWydaLctiuSC5P/1bdna66rk9NVPTjNzJ6/7nSBWcM/NOs0Jl8emYw1AipkYWYUrPib5VRmbNk52DVXHnW3363uIcYeoWTr++eqpaLIS5MQUBoL2SubVU3Ma+ejkvI2bkOT0kYAcV+OA1X76+IZOlI8t4R4yeW8+5AnLk4kzAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAHZWSGpypDmQLQWr2FCVQUnulbPuMMJ0sCH0rNLGLe8qNbZ0YeAuWFsg7+0kVGZ4OuDgioIhD0h3Q3huZtF/WF81eyZqPyVfkXG8egjK58AzMDPHZECeoSVGUCZuq3wjmbnT2sLLDvr8RrzMbbCEvkrYHWivQ18Lbd3eWYYnDbXZRy9GuSWrA9cMqXVYjSTxam9Kel33BIF6CAlMQN5o11oiAv+ciNoxHqGh+8xX3kFKP+x+SRt40NOEs537lEpj/6KdLvd/bP6J4K94jAX3lsdg6zDaBiQWl7P3t50AKtP384Qsb/33uXcbTyw/TkzvPcbmsgTbEUTZIOv44CxMstFrUCyT7ptrzLvDk7Iy2cMgWghULgDvKT3esPE9pleyHG8bkjGt9ypDF/Lmp7j/kILYbF7eq1wIbHOSam4p8WyddVsW4nesu6fqLiCGXum9paChIfvL3To/VHFFKduhJd0Y7LMgWO7pXxWh7XfgRmzQaEN1eJmj5315HEYTS2wXWjptwYDrhiobKuCbpADfOQks8xNKJFLMnXp+IvAqz+ZjkNOz60MLuQ3hvKLTo6nQcTYTfZZxo3Aap30/hA2GtxxSXK/xpBDm58jcVoudgCdxML/OqERBfcADBLvIw5h9+DlXjPUg25IefU0oA336YtnzftJ6cfQfatrc0tBqNEeXdAAFWC41MDk=','','samlsp-x509cert',NULL); +/*!40000 ALTER TABLE `keystore` ENABLE KEYS */; +UNLOCK TABLES; diff --git a/developer/pom.xml b/developer/pom.xml index e39820b01a..6717a6242a 100644 --- a/developer/pom.xml +++ b/developer/pom.xml @@ -172,6 +172,64 @@ + + + deploydb-saml + + + deploydb-saml + + + + + + org.codehaus.mojo + exec-maven-plugin + + + mysql + mysql-connector-java + ${cs.mysql.version} + + + 1.2.1 + + + process-resources + create-schema-simulator + + java + + + + + com.cloud.upgrade.DatabaseCreator + true + + + ${basedir}/../utils/conf/db.properties + ${basedir}/../utils/conf/db.properties.override + + ${basedir}/developer-saml.sql + + com.cloud.upgrade.DatabaseUpgradeChecker + --rootpassword=${db.root.password} + + + + catalina.home + ${basedir}/../utils + + + paths.script + ${basedir}/target/db + + + + + + + deploydb-simulator diff --git a/engine/schema/src/com/cloud/upgrade/dao/Upgrade451to452.java b/engine/schema/src/com/cloud/upgrade/dao/Upgrade451to452.java index 3b7b643fdb..870e534fb7 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 boolean supportsRollingUpgrade() { @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 void performDataMigration(Connection conn) { @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)}; } } diff --git a/engine/schema/src/com/cloud/user/UserAccountVO.java b/engine/schema/src/com/cloud/user/UserAccountVO.java index 5f33c47e64..80ee873f3e 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 User.Source getSource() { public void setSource(User.Source source) { this.source = source; } + + public String getExternalEntity() { + return externalEntity; + } + + public void setExternalEntity(String externalEntity) { + this.externalEntity = externalEntity; + } } diff --git a/engine/schema/src/com/cloud/user/UserVO.java b/engine/schema/src/com/cloud/user/UserVO.java index eb2813bf38..da7811ecc5 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 void setSource(Source source) { this.source = source; } + public String getExternalEntity() { + return externalEntity; + } + + public void setExternalEntity(String externalEntity) { + this.externalEntity = externalEntity; + } } diff --git a/engine/schema/src/com/cloud/user/dao/UserAccountDao.java b/engine/schema/src/com/cloud/user/dao/UserAccountDao.java index a26ff7f85f..1d005b2090 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.UserAccountVO; import com.cloud.utils.db.GenericDao; +import java.util.List; + public interface UserAccountDao extends GenericDao { + List getAllUsersByNameAndEntity(String username, String entity); + UserAccount getUserAccount(String username, Long domainId); boolean validateUsernameInDomain(String username, Long domainId); diff --git a/engine/schema/src/com/cloud/user/dao/UserAccountDaoImpl.java b/engine/schema/src/com/cloud/user/dao/UserAccountDaoImpl.java index 1449e6b82a..a8d9e39a63 100644 --- a/engine/schema/src/com/cloud/user/dao/UserAccountDaoImpl.java +++ b/engine/schema/src/com/cloud/user/dao/UserAccountDaoImpl.java @@ -16,15 +16,15 @@ // under the License. package com.cloud.user.dao; -import javax.ejb.Local; - -import org.springframework.stereotype.Component; - import com.cloud.user.UserAccount; import com.cloud.user.UserAccountVO; import com.cloud.utils.db.GenericDaoBase; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; +import org.springframework.stereotype.Component; + +import javax.ejb.Local; +import java.util.List; @Component @Local(value = {UserAccountDao.class}) @@ -38,6 +38,17 @@ public UserAccountDaoImpl() { userAccountSearch.done(); } + @Override + public List getAllUsersByNameAndEntity(String username, String entity) { + if (username == null) { + return null; + } + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("username", SearchCriteria.Op.EQ, username); + sc.addAnd("externalEntity", SearchCriteria.Op.EQ, entity); + return listBy(sc); + } + @Override public UserAccount getUserAccount(String username, Long domainId) { if ((username == null) || (domainId == null)) { diff --git a/plugins/user-authenticators/saml2/pom.xml b/plugins/user-authenticators/saml2/pom.xml index fed1a5406d..c83b1900c5 100644 --- a/plugins/user-authenticators/saml2/pom.xml +++ b/plugins/user-authenticators/saml2/pom.xml @@ -47,5 +47,10 @@ cloud-api ${project.version} + + org.apache.cloudstack + cloud-framework-config + ${project.version} + diff --git a/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml b/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml index 92f89b8dfb..d3a21947a2 100644 --- a/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml +++ b/plugins/user-authenticators/saml2/resources/META-INF/cloudstack/saml2/spring-saml2-context.xml @@ -33,4 +33,7 @@ + + + diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/AuthorizeSAMLSSOCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/AuthorizeSAMLSSOCmd.java new file mode 100644 index 0000000000..54ce418b2c --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/AuthorizeSAMLSSOCmd.java @@ -0,0 +1,105 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command; + +import com.cloud.domain.Domain; +import com.cloud.user.Account; +import com.cloud.user.UserAccount; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.IdpResponse; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.cloudstack.api.response.UserResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.saml.SAML2AuthManager; +import org.apache.log4j.Logger; + +import javax.inject.Inject; + +@APICommand(name = "authorizeSamlSso", description = "Allow or disallow a user to use SAML SSO", responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class AuthorizeSAMLSSOCmd extends BaseCmd { + public static final Logger s_logger = Logger.getLogger(AuthorizeSAMLSSOCmd.class.getName()); + + private static final String s_name = "authorizesamlssoresponse"; + + @Inject + SAML2AuthManager _samlAuthManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.USER_ID, type = CommandType.UUID, entityType = UserResponse.class, required = true, description = "User uuid") + private Long id; + + @Parameter(name = ApiConstants.ENABLE, type = CommandType.BOOLEAN, required = true, description = "If true, authorizes user to be able to use SAML for Single Sign. If False, disable user to user SAML SSO.") + private Boolean enable; + + public Boolean getEnable() { + return enable; + } + + public String getEntityId() { + return entityId; + } + + @Parameter(name = ApiConstants.ENTITY_ID, type = CommandType.STRING, entityType = IdpResponse.class, description = "The Identity Provider ID the user is allowed to get single signed on from") + private String entityId; + + public Long getId() { + return id; + } + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public void execute() { + // Check permissions + UserAccount userAccount = _accountService.getUserAccountById(getId()); + if (userAccount == null) { + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR , "Unable to find a user account with the given ID"); + } + Domain domain = _domainService.getDomain(userAccount.getDomainId()); + Account account = _accountService.getAccount(userAccount.getAccountId()); + _accountService.checkAccess(CallContext.current().getCallingAccount(), domain); + _accountService.checkAccess(CallContext.current().getCallingAccount(), SecurityChecker.AccessType.OperateEntry, true, account); + + CallContext.current().setEventDetails("UserId: " + getId()); + SuccessResponse response = new SuccessResponse(); + Boolean status = false; + + if (_samlAuthManager.authorizeUser(getId(), getEntityId(), getEnable())) { + status = true; + } + response.setResponseName(getCommandName()); + response.setSuccess(status); + setResponseObject(response); + } +} \ No newline at end of file diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java index e73083609f..3a52151394 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java @@ -14,7 +14,6 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. - package org.apache.cloudstack.api.command; import com.cloud.api.response.ApiResponseSerializer; @@ -30,21 +29,36 @@ import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; import org.apache.cloudstack.api.response.SAMLMetaDataResponse; import org.apache.cloudstack.saml.SAML2AuthManager; +import org.apache.cloudstack.saml.SAMLProviderMetadata; import org.apache.log4j.Logger; import org.opensaml.Configuration; import org.opensaml.DefaultBootstrap; import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.core.NameIDType; import org.opensaml.saml2.metadata.AssertionConsumerService; +import org.opensaml.saml2.metadata.ContactPerson; +import org.opensaml.saml2.metadata.ContactPersonTypeEnumeration; +import org.opensaml.saml2.metadata.EmailAddress; import org.opensaml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml2.metadata.GivenName; import org.opensaml.saml2.metadata.KeyDescriptor; +import org.opensaml.saml2.metadata.LocalizedString; import org.opensaml.saml2.metadata.NameIDFormat; +import org.opensaml.saml2.metadata.Organization; +import org.opensaml.saml2.metadata.OrganizationName; +import org.opensaml.saml2.metadata.OrganizationURL; import org.opensaml.saml2.metadata.SPSSODescriptor; import org.opensaml.saml2.metadata.SingleLogoutService; import org.opensaml.saml2.metadata.impl.AssertionConsumerServiceBuilder; +import org.opensaml.saml2.metadata.impl.ContactPersonBuilder; +import org.opensaml.saml2.metadata.impl.EmailAddressBuilder; import org.opensaml.saml2.metadata.impl.EntityDescriptorBuilder; +import org.opensaml.saml2.metadata.impl.GivenNameBuilder; import org.opensaml.saml2.metadata.impl.KeyDescriptorBuilder; import org.opensaml.saml2.metadata.impl.NameIDFormatBuilder; +import org.opensaml.saml2.metadata.impl.OrganizationBuilder; +import org.opensaml.saml2.metadata.impl.OrganizationNameBuilder; +import org.opensaml.saml2.metadata.impl.OrganizationURLBuilder; import org.opensaml.saml2.metadata.impl.SPSSODescriptorBuilder; import org.opensaml.saml2.metadata.impl.SingleLogoutServiceBuilder; import org.opensaml.xml.ConfigurationException; @@ -73,6 +87,7 @@ import java.io.IOException; import java.io.StringWriter; import java.util.List; +import java.util.Locale; import java.util.Map; @APICommand(name = "getSPMetadata", description = "Returns SAML2 CloudStack Service Provider MetaData", responseObject = SAMLMetaDataResponse.class, entityType = {}) @@ -118,8 +133,10 @@ public String authenticate(String command, Map params, HttpSes params, responseType)); } + final SAMLProviderMetadata spMetadata = _samlAuthManager.getSPMetadata(); + EntityDescriptor spEntityDescriptor = new EntityDescriptorBuilder().buildObject(); - spEntityDescriptor.setEntityID(_samlAuthManager.getServiceProviderId()); + spEntityDescriptor.setEntityID(spMetadata.getEntityId()); SPSSODescriptor spSSODescriptor = new SPSSODescriptorBuilder().buildObject(); spSSODescriptor.setWantAssertionsSigned(true); @@ -129,19 +146,23 @@ public String authenticate(String command, Map params, HttpSes keyInfoGeneratorFactory.setEmitEntityCertificate(true); KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance(); + KeyDescriptor signKeyDescriptor = new KeyDescriptorBuilder().buildObject(); + signKeyDescriptor.setUse(UsageType.SIGNING); + KeyDescriptor encKeyDescriptor = new KeyDescriptorBuilder().buildObject(); encKeyDescriptor.setUse(UsageType.ENCRYPTION); - KeyDescriptor signKeyDescriptor = new KeyDescriptorBuilder().buildObject(); - signKeyDescriptor.setUse(UsageType.SIGNING); + BasicX509Credential signingCredential = new BasicX509Credential(); + signingCredential.setEntityCertificate(spMetadata.getSigningCertificate()); + + BasicX509Credential encryptionCredential = new BasicX509Credential(); + encryptionCredential.setEntityCertificate(spMetadata.getEncryptionCertificate()); - BasicX509Credential credential = new BasicX509Credential(); - credential.setEntityCertificate(_samlAuthManager.getSpX509Certificate()); try { - encKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(credential)); - signKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(credential)); - spSSODescriptor.getKeyDescriptors().add(encKeyDescriptor); + signKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(signingCredential)); + encKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(encryptionCredential)); spSSODescriptor.getKeyDescriptors().add(signKeyDescriptor); + spSSODescriptor.getKeyDescriptors().add(encKeyDescriptor); } catch (SecurityException e) { s_logger.warn("Unable to add SP X509 descriptors:" + e.getMessage()); } @@ -159,19 +180,50 @@ public String authenticate(String command, Map params, HttpSes spSSODescriptor.getNameIDFormats().add(transientNameIDFormat); AssertionConsumerService assertionConsumerService = new AssertionConsumerServiceBuilder().buildObject(); - assertionConsumerService.setIndex(0); - assertionConsumerService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); - assertionConsumerService.setLocation(_samlAuthManager.getSpSingleSignOnUrl()); + assertionConsumerService.setIndex(1); + assertionConsumerService.setIsDefault(true); + assertionConsumerService.setBinding(SAMLConstants.SAML2_POST_BINDING_URI); + assertionConsumerService.setLocation(spMetadata.getSsoUrl()); + spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService); + + AssertionConsumerService assertionConsumerService2 = new AssertionConsumerServiceBuilder().buildObject(); + assertionConsumerService2.setIndex(2); + assertionConsumerService2.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); + assertionConsumerService2.setLocation(spMetadata.getSsoUrl()); + spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService2); SingleLogoutService ssoService = new SingleLogoutServiceBuilder().buildObject(); ssoService.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); - ssoService.setLocation(_samlAuthManager.getSpSingleLogOutUrl()); - + ssoService.setLocation(spMetadata.getSloUrl()); spSSODescriptor.getSingleLogoutServices().add(ssoService); - spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService); + + SingleLogoutService ssoService2 = new SingleLogoutServiceBuilder().buildObject(); + ssoService2.setBinding(SAMLConstants.SAML2_POST_BINDING_URI); + ssoService2.setLocation(spMetadata.getSloUrl()); + spSSODescriptor.getSingleLogoutServices().add(ssoService2); + spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor); + ContactPerson contactPerson = new ContactPersonBuilder().buildObject(); + GivenName givenName = new GivenNameBuilder().buildObject(); + givenName.setName(spMetadata.getContactPersonName()); + EmailAddress emailAddress = new EmailAddressBuilder().buildObject(); + emailAddress.setAddress(spMetadata.getContactPersonEmail()); + contactPerson.setType(ContactPersonTypeEnumeration.TECHNICAL); + contactPerson.setGivenName(givenName); + contactPerson.getEmailAddresses().add(emailAddress); + spEntityDescriptor.getContactPersons().add(contactPerson); + + Organization organization = new OrganizationBuilder().buildObject(); + OrganizationName organizationName = new OrganizationNameBuilder().buildObject(); + organizationName.setName(new LocalizedString(spMetadata.getOrganizationName(), Locale.getDefault().getLanguage())); + OrganizationURL organizationURL = new OrganizationURLBuilder().buildObject(); + organizationURL.setURL(new LocalizedString(spMetadata.getOrganizationUrl(), Locale.getDefault().getLanguage())); + organization.getOrganizationNames().add(organizationName); + organization.getURLs().add(organizationURL); + spEntityDescriptor.setOrganization(organization); + StringWriter stringWriter = new StringWriter(); try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java new file mode 100644 index 0000000000..7d7c95ef59 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java @@ -0,0 +1,114 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command; + +import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.user.Account; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ApiServerService; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.auth.APIAuthenticator; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.api.response.IdpResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.saml.SAML2AuthManager; +import org.apache.cloudstack.saml.SAMLProviderMetadata; +import org.apache.log4j.Logger; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@APICommand(name = "listIdps", description = "Returns list of discovered SAML Identity Providers", responseObject = IdpResponse.class, entityType = {}) +public class ListIdpsCmd extends BaseCmd implements APIAuthenticator { + public static final Logger s_logger = Logger.getLogger(ListIdpsCmd.class.getName()); + private static final String s_name = "listidpsresponse"; + + @Inject + ApiServerService _apiServer; + + SAML2AuthManager _samlAuthManager; + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_TYPE_NORMAL; + } + + @Override + public void execute() throws ServerApiException { + // We should never reach here + throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); + } + + @Override + public String authenticate(String command, Map params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException { + auditTrailSb.append("=== SAML List IdPs ==="); + ListResponse response = new ListResponse(); + List idpResponseList = new ArrayList(); + for (SAMLProviderMetadata metadata: _samlAuthManager.getAllIdPMetadata()) { + if (metadata == null) { + continue; + } + IdpResponse idpResponse = new IdpResponse(); + idpResponse.setId(metadata.getEntityId()); + if (metadata.getOrganizationName() == null || metadata.getOrganizationName().isEmpty()) { + idpResponse.setOrgName(metadata.getEntityId()); + } else { + idpResponse.setOrgName(metadata.getOrganizationName()); + } + idpResponse.setOrgUrl(metadata.getOrganizationUrl()); + idpResponse.setObjectName("idp"); + idpResponseList.add(idpResponse); + } + response.setResponses(idpResponseList, idpResponseList.size()); + response.setResponseName(getCommandName()); + return ApiResponseSerializer.toSerializedString(response, responseType); + } + + @Override + public APIAuthenticationType getAPIType() { + return APIAuthenticationType.LOGIN_API; + } + + @Override + public void setAuthenticators(List authenticators) { + for (PluggableAPIAuthenticator authManager: authenticators) { + if (authManager != null && authManager instanceof SAML2AuthManager) { + _samlAuthManager = (SAML2AuthManager) authManager; + } + } + if (_samlAuthManager == null) { + s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd"); + } + } +} \ No newline at end of file diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListSamlAuthorizationCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListSamlAuthorizationCmd.java new file mode 100644 index 0000000000..be958a1628 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListSamlAuthorizationCmd.java @@ -0,0 +1,95 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command; + +import com.cloud.user.Account; +import com.cloud.user.User; +import com.cloud.user.UserVO; +import com.cloud.user.dao.UserDao; +import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.SamlAuthorizationResponse; +import org.apache.cloudstack.api.response.UserResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.log4j.Logger; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; + +@APICommand(name = "listSamlAuthorization", description = "Lists authorized users who can used SAML SSO", responseObject = SamlAuthorizationResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class ListSamlAuthorizationCmd extends BaseListCmd { + public static final Logger s_logger = Logger.getLogger(ListSamlAuthorizationCmd.class.getName()); + private static final String s_name = "listsamlauthorizationsresponse"; + + @Inject + private UserDao _userDao; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.USER_ID, type = CommandType.UUID, entityType = UserResponse.class, required = false, description = "User uuid") + private Long userId; + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + public Long getUserId() { + return userId; + } + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public void execute() { + List users = new ArrayList(); + if (getUserId() != null) { + UserVO user = _userDao.getUser(getUserId()); + if (user != null) { + Account account = _accountService.getAccount(user.getAccountId()); + _accountService.checkAccess(CallContext.current().getCallingAccount(), SecurityChecker.AccessType.ListEntry, true, account); + users.add(user); + } + } else if (CallContext.current().getCallingAccount().getType() == Account.ACCOUNT_TYPE_ADMIN) { + users = _userDao.listAll(); + } + + ListResponse response = new ListResponse(); + List authorizationResponses = new ArrayList(); + for (User user: users) { + SamlAuthorizationResponse authorizationResponse = new SamlAuthorizationResponse(user.getUuid(), user.getSource().equals(User.Source.SAML2), user.getExternalEntity()); + authorizationResponse.setObjectName("samlauthorization"); + authorizationResponses.add(authorizationResponse); + } + response.setResponses(authorizationResponses); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} \ No newline at end of file diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java index a10afb6588..b05ebf656a 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java @@ -14,16 +14,14 @@ // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. - package org.apache.cloudstack.api.command; import com.cloud.api.response.ApiResponseSerializer; -import com.cloud.configuration.Config; -import com.cloud.domain.Domain; import com.cloud.exception.CloudAuthenticationException; import com.cloud.user.Account; import com.cloud.user.DomainManager; import com.cloud.user.UserAccount; +import com.cloud.user.UserAccountVO; import com.cloud.user.dao.UserAccountDao; import com.cloud.utils.HttpUtils; import com.cloud.utils.db.EntityManager; @@ -38,24 +36,29 @@ import org.apache.cloudstack.api.auth.APIAuthenticator; import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; import org.apache.cloudstack.api.response.LoginCmdResponse; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; import org.apache.cloudstack.saml.SAML2AuthManager; -import org.apache.cloudstack.utils.auth.SAMLUtils; +import org.apache.cloudstack.saml.SAMLPluginConstants; +import org.apache.cloudstack.saml.SAMLProviderMetadata; +import org.apache.cloudstack.saml.SAMLTokenVO; +import org.apache.cloudstack.saml.SAMLUtils; import org.apache.log4j.Logger; import org.opensaml.DefaultBootstrap; import org.opensaml.saml2.core.Assertion; -import org.opensaml.saml2.core.Attribute; -import org.opensaml.saml2.core.AttributeStatement; -import org.opensaml.saml2.core.AuthnRequest; -import org.opensaml.saml2.core.NameID; -import org.opensaml.saml2.core.NameIDType; +import org.opensaml.saml2.core.EncryptedAssertion; +import org.opensaml.saml2.core.Issuer; import org.opensaml.saml2.core.Response; import org.opensaml.saml2.core.StatusCode; +import org.opensaml.saml2.encryption.Decrypter; import org.opensaml.xml.ConfigurationException; -import org.opensaml.xml.io.MarshallingException; +import org.opensaml.xml.encryption.DecryptionException; +import org.opensaml.xml.encryption.EncryptedKeyResolver; +import org.opensaml.xml.encryption.InlineEncryptedKeyResolver; import org.opensaml.xml.io.UnmarshallingException; +import org.opensaml.xml.security.SecurityHelper; +import org.opensaml.xml.security.credential.Credential; +import org.opensaml.xml.security.keyinfo.StaticKeyInfoCredentialResolver; import org.opensaml.xml.security.x509.BasicX509Credential; +import org.opensaml.xml.signature.Signature; import org.opensaml.xml.signature.SignatureValidator; import org.opensaml.xml.validation.ValidationException; import org.xml.sax.SAXException; @@ -69,12 +72,8 @@ import javax.xml.stream.FactoryConfigurationError; import java.io.IOException; import java.net.URLEncoder; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; import java.util.List; import java.util.Map; -import java.util.UUID; @APICommand(name = "samlSso", description = "SP initiated SAML Single Sign On", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {}) public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { @@ -84,16 +83,14 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// ///////////////////////////////////////////////////// - @Parameter(name = ApiConstants.IDP_URL, type = CommandType.STRING, description = "Identity Provider SSO HTTP-Redirect binding URL", required = true) - private String idpUrl; + @Parameter(name = ApiConstants.IDP_ID, type = CommandType.STRING, description = "Identity Provider Entity ID", required = true) + private String idpId; @Inject ApiServerService _apiServer; @Inject EntityManager _entityMgr; @Inject - ConfigurationDao _configDao; - @Inject DomainManager _domainMgr; @Inject private UserAccountDao _userAccountDao; @@ -104,8 +101,8 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// - public String getIdpUrl() { - return idpUrl; + public String getIdpId() { + return idpId; } ///////////////////////////////////////////////////// @@ -128,30 +125,6 @@ public void execute() throws ServerApiException { throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); } - private String buildAuthnRequestUrl(String idpUrl) { - String spId = _samlAuthManager.getServiceProviderId(); - String consumerUrl = _samlAuthManager.getSpSingleSignOnUrl(); - String identityProviderUrl = _samlAuthManager.getIdpSingleSignOnUrl(); - - if (idpUrl != null) { - identityProviderUrl = idpUrl; - } - - String redirectUrl = ""; - try { - DefaultBootstrap.bootstrap(); - AuthnRequest authnRequest = SAMLUtils.buildAuthnRequestObject(spId, identityProviderUrl, consumerUrl); - PrivateKey privateKey = null; - if (_samlAuthManager.getSpKeyPair() != null) { - privateKey = _samlAuthManager.getSpKeyPair().getPrivate(); - } - redirectUrl = identityProviderUrl + "?" + SAMLUtils.generateSAMLRequestSignature("SAMLRequest=" + SAMLUtils.encodeSAMLRequest(authnRequest), privateKey); - } catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException | NoSuchAlgorithmException | InvalidKeyException | java.security.SignatureException e) { - s_logger.error("SAML AuthnRequest message building error: " + e.getMessage()); - } - return redirectUrl; - } - public Response processSAMLResponse(String responseMessage) { Response responseObject = null; try { @@ -167,13 +140,44 @@ public Response processSAMLResponse(String responseMessage) { @Override public String authenticate(final String command, final Map params, final HttpSession session, final String remoteAddress, final String responseType, final StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException { try { - if (!params.containsKey("SAMLResponse") && !params.containsKey("SAMLart")) { - String idpUrl = null; - final String[] idps = (String[])params.get(ApiConstants.IDP_URL); - if (idps != null && idps.length > 0) { - idpUrl = idps[0]; + if (!params.containsKey(SAMLPluginConstants.SAML_RESPONSE) && !params.containsKey("SAMLart")) { + String idpId = null; + String domainPath = null; + + if (params.containsKey(ApiConstants.IDP_ID)) { + idpId = ((String[])params.get(ApiConstants.IDP_ID))[0]; + } + + if (params.containsKey(ApiConstants.DOMAIN)) { + domainPath = ((String[])params.get(ApiConstants.DOMAIN))[0]; + } + + if (domainPath != null && !domainPath.isEmpty()) { + if (!domainPath.startsWith("/")) { + domainPath = "/" + domainPath; + } + if (!domainPath.endsWith("/")) { + domainPath = domainPath + "/"; + } + } + + SAMLProviderMetadata spMetadata = _samlAuthManager.getSPMetadata(); + SAMLProviderMetadata idpMetadata = _samlAuthManager.getIdPMetadata(idpId); + if (idpMetadata == null) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.PARAM_ERROR.getHttpCode(), + "IdP ID (" + idpId + ") is not found in our list of supported IdPs, cannot proceed.", + params, responseType)); + } + if (idpMetadata.getSsoUrl() == null || idpMetadata.getSsoUrl().isEmpty()) { + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.PARAM_ERROR.getHttpCode(), + "IdP ID (" + idpId + ") has no Single Sign On URL defined please contact " + + idpMetadata.getContactPersonName() + " <" + idpMetadata.getContactPersonEmail() + ">, cannot proceed.", + params, responseType)); } - String redirectUrl = this.buildAuthnRequestUrl(idpUrl); + String authnId = SAMLUtils.generateSecureRandomId(); + _samlAuthManager.saveToken(authnId, domainPath, idpMetadata.getEntityId()); + s_logger.debug("Sending SAMLRequest id=" + authnId); + String redirectUrl = SAMLUtils.buildAuthnRequestUrl(authnId, spMetadata, idpMetadata, SAML2AuthManager.SAMLSignatureAlgorithm.value()); resp.sendRedirect(redirectUrl); return ""; } if (params.containsKey("SAMLart")) { @@ -181,7 +185,7 @@ public String authenticate(final String command, final Map par "SAML2 HTTP Artifact Binding is not supported", params, responseType)); } else { - final String samlResponse = ((String[])params.get(SAMLUtils.SAML_RESPONSE))[0]; + final String samlResponse = ((String[])params.get(SAMLPluginConstants.SAML_RESPONSE))[0]; Response processedSAMLResponse = this.processSAMLResponse(samlResponse); String statusCode = processedSAMLResponse.getStatus().getStatusCode().getValue(); if (!statusCode.equals(StatusCode.SUCCESS_URI)) { @@ -190,10 +194,37 @@ public String authenticate(final String command, final Map par params, responseType)); } - if (_samlAuthManager.getIdpSigningKey() != null) { - org.opensaml.xml.signature.Signature sig = processedSAMLResponse.getSignature(); + String username = null; + Long domainId = null; + Issuer issuer = processedSAMLResponse.getIssuer(); + SAMLProviderMetadata spMetadata = _samlAuthManager.getSPMetadata(); + SAMLProviderMetadata idpMetadata = _samlAuthManager.getIdPMetadata(issuer.getValue()); + + String responseToId = processedSAMLResponse.getInResponseTo(); + s_logger.debug("Received SAMLResponse in response to id=" + responseToId); + SAMLTokenVO token = _samlAuthManager.getToken(responseToId); + if (token != null) { + if (token.getDomainId() != null) { + domainId = token.getDomainId(); + } + if (!(token.getEntity().equalsIgnoreCase(issuer.getValue()))) { + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), + "The SAML response contains Issuer Entity ID that is different from the original SAML request", + params, responseType)); + } + } else { + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), + "Received SAML response for a SSO request that we may not have made or has expired, please try logging in again", + params, responseType)); + } + + // Set IdpId for this session + session.setAttribute(SAMLPluginConstants.SAML_IDPID, issuer.getValue()); + + Signature sig = processedSAMLResponse.getSignature(); + if (idpMetadata.getSigningCertificate() != null && sig != null) { BasicX509Credential credential = new BasicX509Credential(); - credential.setEntityCertificate(_samlAuthManager.getIdpSigningKey()); + credential.setEntityCertificate(idpMetadata.getSigningCertificate()); SignatureValidator validator = new SignatureValidator(credential); try { validator.validate(sig); @@ -204,94 +235,106 @@ public String authenticate(final String command, final Map par params, responseType)); } } - - String domainString = _configDao.getValue(Config.SAMLUserDomain.key()); - - Long domainId = null; - Domain domain = _domainMgr.getDomain(domainString); - if (domain != null) { - domainId = domain.getId(); - } else { - try { - domainId = Long.parseLong(domainString); - } catch (NumberFormatException ignore) { - } - } - if (domainId == null) { - s_logger.error("The default domain ID for SAML users is not set correct, it should be a UUID. ROOT domain will be used."); + if (username == null) { + username = SAMLUtils.getValueFromAssertions(processedSAMLResponse.getAssertions(), SAML2AuthManager.SAMLUserAttributeName.value()); } - String username = null; - String password = SAMLUtils.generateSecureRandomId(); // Random password - String firstName = ""; - String lastName = ""; - String timeZone = "GMT"; - String email = ""; - short accountType = 0; // User account - - Assertion assertion = processedSAMLResponse.getAssertions().get(0); - NameID nameId = assertion.getSubject().getNameID(); - String sessionIndex = assertion.getAuthnStatements().get(0).getSessionIndex(); - session.setAttribute(SAMLUtils.SAML_NAMEID, nameId); - session.setAttribute(SAMLUtils.SAML_SESSION, sessionIndex); - - if (nameId.getFormat().equals(NameIDType.PERSISTENT) || nameId.getFormat().equals(NameIDType.EMAIL)) { - username = nameId.getValue(); - if (nameId.getFormat().equals(NameIDType.EMAIL)) { - email = username; + for (Assertion assertion: processedSAMLResponse.getAssertions()) { + if (assertion!= null && assertion.getSubject() != null && assertion.getSubject().getNameID() != null) { + session.setAttribute(SAMLPluginConstants.SAML_NAMEID, assertion.getSubject().getNameID().getValue()); + break; } } - List attributeStatements = assertion.getAttributeStatements(); - if (attributeStatements != null && attributeStatements.size() > 0) { - for (AttributeStatement attributeStatement: attributeStatements) { - if (attributeStatement == null) { - continue; - } - // Try capturing standard LDAP attributes - for (Attribute attribute: attributeStatement.getAttributes()) { - String attributeName = attribute.getName(); - String attributeValue = attribute.getAttributeValues().get(0).getDOM().getTextContent(); - if (attributeName.equalsIgnoreCase("uid") && username == null) { - username = attributeValue; - } else if (attributeName.equalsIgnoreCase("givenName")) { - firstName = attributeValue; - } else if (attributeName.equalsIgnoreCase(("sn"))) { - lastName = attributeValue; - } else if (attributeName.equalsIgnoreCase("mail")) { - email = attributeValue; + if (idpMetadata.getEncryptionCertificate() != null && spMetadata != null + && spMetadata.getKeyPair() != null && spMetadata.getKeyPair().getPrivate() != null) { + Credential credential = SecurityHelper.getSimpleCredential(idpMetadata.getEncryptionCertificate().getPublicKey(), + spMetadata.getKeyPair().getPrivate()); + StaticKeyInfoCredentialResolver keyInfoResolver = new StaticKeyInfoCredentialResolver(credential); + EncryptedKeyResolver keyResolver = new InlineEncryptedKeyResolver(); + Decrypter decrypter = new Decrypter(null, keyInfoResolver, keyResolver); + decrypter.setRootInNewDocument(true); + List encryptedAssertions = processedSAMLResponse.getEncryptedAssertions(); + if (encryptedAssertions != null) { + for (EncryptedAssertion encryptedAssertion : encryptedAssertions) { + Assertion assertion = null; + try { + assertion = decrypter.decrypt(encryptedAssertion); + } catch (DecryptionException e) { + s_logger.warn("SAML EncryptedAssertion error: " + e.toString()); + } + if (assertion == null) { + continue; + } + Signature encSig = assertion.getSignature(); + if (idpMetadata.getSigningCertificate() != null && encSig != null) { + BasicX509Credential sigCredential = new BasicX509Credential(); + sigCredential.setEntityCertificate(idpMetadata.getSigningCertificate()); + SignatureValidator validator = new SignatureValidator(sigCredential); + try { + validator.validate(encSig); + } catch (ValidationException e) { + s_logger.error("SAML Response's signature failed to be validated by IDP signing key:" + e.getMessage()); + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), + "SAML Response's signature failed to be validated by IDP signing key", + params, responseType)); + } + } + if (assertion.getSubject() != null && assertion.getSubject().getNameID() != null) { + session.setAttribute(SAMLPluginConstants.SAML_NAMEID, assertion.getSubject().getNameID().getValue()); + } + if (username == null) { + username = SAMLUtils.getValueFromAttributeStatements(assertion.getAttributeStatements(), SAML2AuthManager.SAMLUserAttributeName.value()); } } } } - if (username == null && email != null) { - username = email; + if (username == null) { + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), + "Failed to find admin configured username attribute in the SAML Response. Please ask your administrator to check SAML user attribute name.", params, responseType)); + } + + UserAccount userAccount = null; + List possibleUserAccounts = _userAccountDao.getAllUsersByNameAndEntity(username, issuer.getValue()); + if (possibleUserAccounts != null && possibleUserAccounts.size() > 0) { + if (possibleUserAccounts.size() == 1) { + userAccount = possibleUserAccounts.get(0); + } else if (possibleUserAccounts.size() > 1) { + if (domainId != null) { + userAccount = _userAccountDao.getUserAccount(username, domainId); + } else { + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), + "You have accounts in multiple domains, please re-login by specifying the domain you want to log into.", + params, responseType)); + } + } } - final String uniqueUserId = SAMLUtils.createSAMLId(username); - UserAccount userAccount = _userAccountDao.getUserAccount(username, domainId); - if (userAccount == null && uniqueUserId != null && username != null) { - CallContext.current().setEventDetails("SAML Account/User with UserName: " + username + ", FirstName :" + password + ", LastName: " + lastName); - userAccount = _accountService.createUserAccount(username, password, firstName, lastName, email, timeZone, - username, (short) accountType, domainId, null, null, UUID.randomUUID().toString(), uniqueUserId); + if (userAccount == null || userAccount.getExternalEntity() == null || !_samlAuthManager.isUserAuthorized(userAccount.getId(), issuer.getValue())) { + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), + "Your authenticated user is not authorized, please contact your administrator", + params, responseType)); } if (userAccount != null) { try { if (_apiServer.verifyUser(userAccount.getId())) { - LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, username, userAccount.getPassword(), domainId, null, remoteAddress, params); + LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, userAccount.getUsername(), userAccount.getUsername() + userAccount.getSource().toString(), + userAccount.getDomainId(), null, remoteAddress, params); resp.addCookie(new Cookie("userid", URLEncoder.encode(loginResponse.getUserId(), HttpUtils.UTF_8))); resp.addCookie(new Cookie("domainid", URLEncoder.encode(loginResponse.getDomainId(), HttpUtils.UTF_8))); resp.addCookie(new Cookie("role", URLEncoder.encode(loginResponse.getType(), HttpUtils.UTF_8))); resp.addCookie(new Cookie("username", URLEncoder.encode(loginResponse.getUsername(), HttpUtils.UTF_8))); - resp.addCookie(new Cookie("sessionkey", URLEncoder.encode(loginResponse.getSessionKey(), HttpUtils.UTF_8))); + resp.addCookie(new Cookie(ApiConstants.SESSIONKEY, URLEncoder.encode(loginResponse.getSessionKey(), HttpUtils.UTF_8))); resp.addCookie(new Cookie("account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8))); - resp.addCookie(new Cookie("timezone", URLEncoder.encode(loginResponse.getTimeZone(), HttpUtils.UTF_8))); + String timezone = loginResponse.getTimeZone(); + if (timezone != null) { + resp.addCookie(new Cookie("timezone", URLEncoder.encode(timezone, HttpUtils.UTF_8))); + } resp.addCookie(new Cookie("userfullname", URLEncoder.encode(loginResponse.getFirstName() + " " + loginResponse.getLastName(), HttpUtils.UTF_8).replace("+", "%20"))); - resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key())); + resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value()); return ApiResponseSerializer.toSerializedString(loginResponse, responseType); - } } catch (final CloudAuthenticationException ignored) { } @@ -302,7 +345,7 @@ public String authenticate(final String command, final Map par auditTrailSb.append(e.getMessage()); } throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), - "Unable to authenticate or retrieve user while performing SAML based SSO", + "Unable to authenticate user while performing SAML based SSO. Please make sure your user/account has been added, enable and authorized by the admin before you can authenticate. Please contact your administrator.", params, responseType)); } diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java index 992e4317f8..a012431b29 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.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 String authenticate(String command, Map params, HttpSes 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 String authenticate(String command, Map params, HttpSes 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,26 @@ public String authenticate(String command, Map params, HttpSes s_logger.error("SAMLResponse processing error: " + e.getMessage()); } try { - resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key())); + resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value()); } catch (IOException ignored) { } return responseString; } - NameID nameId = (NameID) session.getAttribute(SAMLUtils.SAML_NAMEID); - String sessionIndex = (String) session.getAttribute(SAMLUtils.SAML_SESSION); - if (nameId == null || sessionIndex == null) { + String idpId = (String) session.getAttribute(SAMLPluginConstants.SAML_IDPID); + SAMLProviderMetadata idpMetadata = _samlAuthManager.getIdPMetadata(idpId); + String nameId = (String) session.getAttribute(SAMLPluginConstants.SAML_NAMEID); + if (idpMetadata == null || nameId == null || nameId.isEmpty()) { try { - resp.sendRedirect(_configDao.getValue(Config.SAMLCloudStackRedirectionUrl.key())); + resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value()); } catch (IOException ignored) { } return responseString; } - LogoutRequest logoutRequest = SAMLUtils.buildLogoutRequest(_samlAuthManager.getIdpSingleLogOutUrl(), _samlAuthManager.getServiceProviderId(), nameId, sessionIndex); + LogoutRequest logoutRequest = SAMLUtils.buildLogoutRequest(idpMetadata.getSloUrl(), _samlAuthManager.getSPMetadata().getEntityId(), nameId); try { - String redirectUrl = _samlAuthManager.getIdpSingleLogOutUrl() + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(logoutRequest); + String redirectUrl = idpMetadata.getSloUrl() + "?SAMLRequest=" + SAMLUtils.encodeSAMLRequest(logoutRequest); resp.sendRedirect(redirectUrl); } catch (MarshallingException | IOException e) { s_logger.error("SAML SLO error: " + e.getMessage()); diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/IdpResponse.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/IdpResponse.java new file mode 100644 index 0000000000..d95cc3349c --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/IdpResponse.java @@ -0,0 +1,62 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +public class IdpResponse extends AuthenticationCmdResponse { + @SerializedName("id") + @Param(description = "The IdP Entity ID") + private String id; + + @SerializedName("orgName") + @Param(description = "The IdP Organization Name") + private String orgName; + + @SerializedName("orgUrl") + @Param(description = "The IdP Organization URL") + private String orgUrl; + + public IdpResponse() { + super(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getOrgName() { + return orgName; + } + + public void setOrgName(String orgName) { + this.orgName = orgName; + } + + public String getOrgUrl() { + return orgUrl; + } + + public void setOrgUrl(String orgUrl) { + this.orgUrl = orgUrl; + } +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlAuthorizationResponse.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlAuthorizationResponse.java new file mode 100644 index 0000000000..445ee887af --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlAuthorizationResponse.java @@ -0,0 +1,68 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.cloud.user.User; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; + +@EntityReference(value = User.class) +public class SamlAuthorizationResponse extends BaseResponse { + @SerializedName("userid") + @Param(description = "the user ID") + private String userId; + + @SerializedName("status") + @Param(description = "the SAML authorization status") + private Boolean status; + + @SerializedName("idpid") + @Param(description = "the authorized Identity Provider ID") + private String idpId; + + public SamlAuthorizationResponse(String userId, Boolean status, String idpId) { + this.userId = userId; + this.status = status; + this.idpId = idpId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public Boolean getStatus() { + return status; + } + + public void setStatus(Boolean status) { + this.status = status; + } + + public String getIdpId() { + return idpId; + } + + public void setIdpId(String idpId) { + this.idpId = idpId; + } +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java index 9c0d4b42fc..fc9a6db4d8 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManager.java @@ -17,23 +17,64 @@ package org.apache.cloudstack.saml; +import com.cloud.utils.component.PluggableService; import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.framework.config.ConfigKey; -import java.security.KeyPair; -import java.security.cert.X509Certificate; +import java.util.Collection; -public interface SAML2AuthManager extends PluggableAPIAuthenticator { - public String getServiceProviderId(); - public String getIdentityProviderId(); +public interface SAML2AuthManager extends PluggableAPIAuthenticator, PluggableService { - public X509Certificate getIdpSigningKey(); - public X509Certificate getIdpEncryptionKey(); - public X509Certificate getSpX509Certificate(); - public KeyPair getSpKeyPair(); + public static final ConfigKey SAMLIsPluginEnabled = new ConfigKey("Advanced", Boolean.class, "saml2.enabled", "false", + "Indicates whether SAML SSO plugin is enabled or not", true); - public String getSpSingleSignOnUrl(); - public String getIdpSingleSignOnUrl(); + public static final ConfigKey SAMLServiceProviderID = new ConfigKey("Advanced", String.class, "saml2.sp.id", "org.apache.cloudstack", + "SAML2 Service Provider Identifier String", true); - public String getSpSingleLogOutUrl(); - public String getIdpSingleLogOutUrl(); + public static final ConfigKey SAMLServiceProviderContactPersonName = new ConfigKey("Advanced", String.class, "saml2.sp.contact.person", "CloudStack Developers", + "SAML2 Service Provider Contact Person Name", true); + + public static final ConfigKey SAMLServiceProviderContactEmail = new ConfigKey("Advanced", String.class, "saml2.sp.contact.email", "dev@cloudstack.apache.org", + "SAML2 Service Provider Contact Email Address", true); + + public static final ConfigKey SAMLServiceProviderOrgName = new ConfigKey("Advanced", String.class, "saml2.sp.org.name", "Apache CloudStack", + "SAML2 Service Provider Organization Name", true); + + public static final ConfigKey SAMLServiceProviderOrgUrl = new ConfigKey("Advanced", String.class, "saml2.sp.org.url", "http://cloudstack.apache.org", + "SAML2 Service Provider Organization URL", true); + + public static final ConfigKey SAMLServiceProviderSingleSignOnURL = new ConfigKey("Advanced", String.class, "saml2.sp.sso.url", "http://localhost:8080/client/api?command=samlSso", + "SAML2 CloudStack Service Provider Single Sign On URL", true); + + public static final ConfigKey SAMLServiceProviderSingleLogOutURL = new ConfigKey("Advanced", String.class, "saml2.sp.slo.url", "http://localhost:8080/client/", + "SAML2 CloudStack Service Provider Single Log Out URL", true); + + public static final ConfigKey SAMLCloudStackRedirectionUrl = new ConfigKey("Advanced", String.class, "saml2.redirect.url", "http://localhost:8080/client", + "The CloudStack UI url the SSO should redirected to when successful", true); + + public static final ConfigKey SAMLUserAttributeName = new ConfigKey("Advanced", String.class, "saml2.user.attribute", "uid", + "Attribute name to be looked for in SAML response that will contain the username", true); + + public static final ConfigKey SAMLIdentityProviderMetadataURL = new ConfigKey("Advanced", String.class, "saml2.idp.metadata.url", "https://openidp.feide.no/simplesaml/saml2/idp/metadata.php", + "SAML2 Identity Provider Metadata XML Url", true); + + public static final ConfigKey SAMLDefaultIdentityProviderId = new ConfigKey("Advanced", String.class, "saml2.default.idpid", "https://openidp.feide.no", + "The default IdP entity ID to use only in case of multiple IdPs", true); + + public static final ConfigKey SAMLSignatureAlgorithm = new ConfigKey("Advanced", String.class, "saml2.sigalg", "SHA1", + "The algorithm to use to when signing a SAML request. Default is SHA1, allowed algorithms: SHA1, SHA256, SHA384, SHA512", true); + + public static final ConfigKey SAMLTimeout = new ConfigKey("Advanced", Integer.class, "saml2.timeout", "1800", + "SAML2 IDP Metadata refresh interval in seconds, minimum value is set to 300", true); + + public SAMLProviderMetadata getSPMetadata(); + public SAMLProviderMetadata getIdPMetadata(String entityId); + public Collection getAllIdPMetadata(); + + public boolean isUserAuthorized(Long userId, String entityId); + public boolean authorizeUser(Long userId, String entityId, boolean enable); + + public void saveToken(String authnId, String domain, String entity); + public SAMLTokenVO getToken(String authnId); + public void expireTokens(); } diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java index 36c9da5e1e..185955c338 100644 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java @@ -16,28 +16,46 @@ // under the License. package org.apache.cloudstack.saml; -import com.cloud.configuration.Config; +import com.cloud.domain.Domain; +import com.cloud.user.DomainManager; +import com.cloud.user.User; +import com.cloud.user.UserVO; +import com.cloud.user.dao.UserDao; +import com.cloud.utils.PropertiesUtil; import com.cloud.utils.component.AdapterBase; import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.api.command.AuthorizeSAMLSSOCmd; import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd; +import org.apache.cloudstack.api.command.ListIdpsCmd; +import org.apache.cloudstack.api.command.ListSamlAuthorizationCmd; import org.apache.cloudstack.api.command.SAML2LoginAPIAuthenticatorCmd; import org.apache.cloudstack.api.command.SAML2LogoutAPIAuthenticatorCmd; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.Configurable; import org.apache.cloudstack.framework.security.keystore.KeystoreDao; import org.apache.cloudstack.framework.security.keystore.KeystoreVO; -import org.apache.cloudstack.utils.auth.SAMLUtils; -import org.apache.log4j.Logger; import org.apache.commons.codec.binary.Base64; +import org.apache.commons.httpclient.HttpClient; +import org.apache.log4j.Logger; import org.opensaml.DefaultBootstrap; import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.metadata.ContactPerson; +import org.opensaml.saml2.metadata.EmailAddress; +import org.opensaml.saml2.metadata.EntitiesDescriptor; import org.opensaml.saml2.metadata.EntityDescriptor; import org.opensaml.saml2.metadata.IDPSSODescriptor; import org.opensaml.saml2.metadata.KeyDescriptor; +import org.opensaml.saml2.metadata.OrganizationDisplayName; +import org.opensaml.saml2.metadata.OrganizationName; +import org.opensaml.saml2.metadata.OrganizationURL; import org.opensaml.saml2.metadata.SingleLogoutService; import org.opensaml.saml2.metadata.SingleSignOnService; +import org.opensaml.saml2.metadata.provider.AbstractReloadingMetadataProvider; +import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider; import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider; import org.opensaml.saml2.metadata.provider.MetadataProviderException; import org.opensaml.xml.ConfigurationException; +import org.opensaml.xml.XMLObject; import org.opensaml.xml.parse.BasicParserPool; import org.opensaml.xml.security.credential.UsageType; import org.opensaml.xml.security.keyinfo.KeyInfoHelper; @@ -48,6 +66,7 @@ import javax.xml.stream.FactoryConfigurationError; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutput; @@ -63,61 +82,87 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; @Component @Local(value = {SAML2AuthManager.class, PluggableAPIAuthenticator.class}) -public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager { +public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager, Configurable { private static final Logger s_logger = Logger.getLogger(SAML2AuthManagerImpl.class); - private String serviceProviderId; - private String identityProviderId; + private SAMLProviderMetadata _spMetadata = new SAMLProviderMetadata(); + private Map _idpMetadataMap = new HashMap(); - private X509Certificate idpSigningKey; - private X509Certificate idpEncryptionKey; - private X509Certificate spX509Key; - private KeyPair spKeyPair; - - private String spSingleSignOnUrl; private String idpSingleSignOnUrl; - - private String spSingleLogOutUrl; private String idpSingleLogOutUrl; - private HTTPMetadataProvider idpMetaDataProvider; + private Timer _timer; + private int _refreshInterval = SAMLPluginConstants.SAML_REFRESH_INTERVAL; + private AbstractReloadingMetadataProvider _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 +170,7 @@ private boolean setup() { ObjectOutput out = new ObjectOutputStream(bos); out.writeObject(spX509Key); out.flush(); - _ksDao.save(SAMLUtils.SAMLSP_X509CERT, Base64.encodeBase64String(bos.toByteArray()), "", "samlsp-x509cert"); + _ksDao.save(SAMLPluginConstants.SAMLSP_X509CERT, Base64.encodeBase64String(bos.toByteArray()), "", "samlsp-x509cert"); bos.close(); } catch (NoSuchAlgorithmException | NoSuchProviderException | CertificateEncodingException | SignatureException | InvalidKeyException | IOException e) { s_logger.error("SAML Plugin won't be able to use X509 signed authentication"); @@ -142,61 +187,194 @@ private boolean setup() { } } } - - 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 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) { } } } + } + if (idpMetadata.getSigningCertificate() == null && unspecifiedKey != null) { + idpMetadata.setSigningCertificate(unspecifiedKey); + } + if (idpMetadata.getEncryptionCertificate() == null && unspecifiedKey != null) { + idpMetadata.setEncryptionCertificate(unspecifiedKey); + } + if (idpMap.containsKey(idpMetadata.getEntityId())) { + s_logger.warn("Duplicate IdP metadata found with entity Id: " + idpMetadata.getEntityId()); + } + idpMap.put(idpMetadata.getEntityId(), idpMetadata); + } + } + + private void discoverAndAddIdp(XMLObject metadata, Map idpMap) { + if (metadata instanceof EntityDescriptor) { + EntityDescriptor entityDescriptor = (EntityDescriptor) metadata; + addIdpToMap(entityDescriptor, idpMap); + } else if (metadata instanceof EntitiesDescriptor) { + EntitiesDescriptor entitiesDescriptor = (EntitiesDescriptor) metadata; + if (entitiesDescriptor.getEntityDescriptors() != null) { + for (EntityDescriptor entityDescriptor: entitiesDescriptor.getEntityDescriptors()) { + addIdpToMap(entityDescriptor, idpMap); + } + } + if (entitiesDescriptor.getEntitiesDescriptors() != null) { + for (EntitiesDescriptor entitiesDescriptorInner: entitiesDescriptor.getEntitiesDescriptors()) { + discoverAndAddIdp(entitiesDescriptorInner, idpMap); + } + } + } + } + + class MetadataRefreshTask extends TimerTask { + @Override + public void run() { + if (_idpMetaDataProvider == null) { + return; + } + s_logger.debug("Starting SAML IDP Metadata Refresh Task"); + Map metadataMap = new HashMap(); + try { + discoverAndAddIdp(_idpMetaDataProvider.getMetadata(), metadataMap); + _idpMetadataMap = metadataMap; + expireTokens(); + s_logger.debug("Finished refreshing SAML Metadata and expiring old auth tokens"); + } catch (MetadataProviderException e) { + s_logger.warn("SAML Metadata Refresh task failed with exception: " + e.getMessage()); + } + + } + } + + private boolean setup() { + if (!initSP()) { + s_logger.error("SAML Plugin failed to initialize, please fix the configuration and restart management server"); + return false; + } + _timer = new Timer(); + final HttpClient client = new HttpClient(); + final String idpMetaDataUrl = SAMLIdentityProviderMetadataURL.value(); + if (SAMLTimeout.value() != null && SAMLTimeout.value() > SAMLPluginConstants.SAML_REFRESH_INTERVAL) { + _refreshInterval = SAMLTimeout.value(); + } + try { + DefaultBootstrap.bootstrap(); + if (idpMetaDataUrl.startsWith("http")) { + _idpMetaDataProvider = new HTTPMetadataProvider(_timer, client, idpMetaDataUrl); } else { - s_logger.warn("Provided IDP XML Metadata does not contain IDPSSODescriptor, SAML authentication may not work"); + File metadataFile = PropertiesUtil.findConfigFile(idpMetaDataUrl); + s_logger.debug("Provided Metadata is not a URL, trying to read metadata file from local path: " + metadataFile.getAbsolutePath()); + _idpMetaDataProvider = new FilesystemMetadataProvider(_timer, metadataFile); } + _idpMetaDataProvider.setRequireValidMetadata(true); + _idpMetaDataProvider.setParserPool(new BasicParserPool()); + _idpMetaDataProvider.initialize(); + _timer.scheduleAtFixedRate(new MetadataRefreshTask(), 0, _refreshInterval * 1000); } catch (MetadataProviderException e) { s_logger.error("Unable to read SAML2 IDP MetaData URL, error:" + e.getMessage()); s_logger.error("SAML2 Authentication may be unavailable"); @@ -204,70 +382,138 @@ private boolean setup() { 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> getAuthCommands() { - List> cmdList = new ArrayList>(); - 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 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> getAuthCommands() { + List> cmdList = new ArrayList>(); + 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> getCommands() { + List> cmdList = new ArrayList>(); + if (!isSAMLPluginEnabled()) { + return cmdList; + } + cmdList.add(AuthorizeSAMLSSOCmd.class); + cmdList.add(ListSamlAuthorizationCmd.class); + return cmdList; } @Override - public KeyPair getSpKeyPair() { - return spKeyPair; + public ConfigKey[] getConfigKeys() { + return new ConfigKey[] { + SAMLIsPluginEnabled, SAMLServiceProviderID, + SAMLServiceProviderContactPersonName, SAMLServiceProviderContactEmail, + SAMLServiceProviderOrgName, SAMLServiceProviderOrgUrl, + SAMLServiceProviderSingleSignOnURL, SAMLServiceProviderSingleLogOutURL, + SAMLCloudStackRedirectionUrl, SAMLUserAttributeName, + SAMLIdentityProviderMetadataURL, SAMLDefaultIdentityProviderId, + SAMLSignatureAlgorithm, SAMLTimeout}; } } diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java index 68bd81c18f..5c8a39088a 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.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 Pair authenticate(String username, } final UserAccount userAccount = _userAccountDao.getUserAccount(username, domainId); - if (userAccount == null) { - s_logger.debug("Unable to find user with " + username + " in domain " + domainId); + if (userAccount == null || userAccount.getSource() != User.Source.SAML2) { + s_logger.debug("Unable to find user with " + username + " in domain " + domainId + ", or user source is not SAML2"); return new Pair(false, null); } else { User user = _userDao.getUser(userAccount.getId()); - if (user != null && SAMLUtils.checkSAMLUser(user.getUuid(), username) && - requestParameters != null && requestParameters.containsKey(SAMLUtils.SAML_RESPONSE)) { + if (user != null && requestParameters != null && requestParameters.containsKey(SAMLPluginConstants.SAML_RESPONSE)) { + final String samlResponse = ((String[])requestParameters.get(SAMLPluginConstants.SAML_RESPONSE))[0]; + Response responseObject = null; + try { + DefaultBootstrap.bootstrap(); + responseObject = SAMLUtils.decodeSAMLResponse(samlResponse); + } catch (ConfigurationException | FactoryConfigurationError | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) { + return new Pair(false, null); + } + if (!responseObject.getStatus().getStatusCode().getValue().equals(StatusCode.SUCCESS_URI)) { + return new Pair(false, null); + } return new Pair(true, null); } } diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLPluginConstants.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLPluginConstants.java new file mode 100644 index 0000000000..5f806e2f69 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLPluginConstants.java @@ -0,0 +1,30 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +package org.apache.cloudstack.saml; + +public class SAMLPluginConstants { + public static final int SAML_REFRESH_INTERVAL = 300; + + public static final String SAML_RESPONSE = "SAMLResponse"; + public static final String SAML_IDPID = "SAML_IDPID"; + public static final String SAML_SESSIONID = "SAML_SESSIONID"; + public static final String SAML_NAMEID = "SAML_NAMEID"; + public static final String SAMLSP_KEYPAIR = "SAMLSP_KEYPAIR"; + public static final String SAMLSP_X509CERT = "SAMLSP_X509CERT"; +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLProviderMetadata.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLProviderMetadata.java new file mode 100644 index 0000000000..c7138a1d79 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLProviderMetadata.java @@ -0,0 +1,122 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.cloudstack.saml; + +import java.security.KeyPair; +import java.security.cert.X509Certificate; + +public class SAMLProviderMetadata { + private String entityId; + private String organizationName; + private String organizationUrl; + private String contactPersonName; + private String contactPersonEmail; + private String ssoUrl; + private String sloUrl; + private KeyPair keyPair; + private X509Certificate signingCertificate; + private X509Certificate encryptionCertificate; + + public SAMLProviderMetadata() { + } + + public void setCommonCertificate(X509Certificate certificate) { + this.signingCertificate = certificate; + this.encryptionCertificate = certificate; + } + + public String getEntityId() { + return entityId; + } + + public void setEntityId(String entityId) { + this.entityId = entityId; + } + + public String getContactPersonName() { + return contactPersonName; + } + + public void setContactPersonName(String contactPersonName) { + this.contactPersonName = contactPersonName; + } + + public String getContactPersonEmail() { + return contactPersonEmail; + } + + public void setContactPersonEmail(String contactPersonEmail) { + this.contactPersonEmail = contactPersonEmail; + } + + public String getOrganizationName() { + return organizationName; + } + + public void setOrganizationName(String organizationName) { + this.organizationName = organizationName; + } + + public String getOrganizationUrl() { + return organizationUrl; + } + + public void setOrganizationUrl(String organizationUrl) { + this.organizationUrl = organizationUrl; + } + + public KeyPair getKeyPair() { + return keyPair; + } + + public void setKeyPair(KeyPair keyPair) { + this.keyPair = keyPair; + } + + public X509Certificate getSigningCertificate() { + return signingCertificate; + } + + public void setSigningCertificate(X509Certificate signingCertificate) { + this.signingCertificate = signingCertificate; + } + + public X509Certificate getEncryptionCertificate() { + return encryptionCertificate; + } + + public void setEncryptionCertificate(X509Certificate encryptionCertificate) { + this.encryptionCertificate = encryptionCertificate; + } + + public String getSsoUrl() { + return ssoUrl; + } + + public void setSsoUrl(String ssoUrl) { + this.ssoUrl = ssoUrl; + } + + public String getSloUrl() { + return sloUrl; + } + + public void setSloUrl(String sloUrl) { + this.sloUrl = sloUrl; + } +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDao.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDao.java new file mode 100644 index 0000000000..b045562009 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDao.java @@ -0,0 +1,23 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.saml; + +import com.cloud.utils.db.GenericDao; + +public interface SAMLTokenDao extends GenericDao { + public void expireTokens(); +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDaoImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDaoImpl.java new file mode 100644 index 0000000000..eb106d97ad --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenDaoImpl.java @@ -0,0 +1,51 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.saml; + +import com.cloud.utils.db.DB; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.TransactionLegacy; +import com.cloud.utils.exception.CloudRuntimeException; +import org.springframework.stereotype.Component; + +import javax.ejb.Local; +import java.sql.PreparedStatement; + +@DB +@Component +@Local(value = {SAMLTokenDao.class}) +public class SAMLTokenDaoImpl extends GenericDaoBase implements SAMLTokenDao { + + public SAMLTokenDaoImpl() { + super(); + } + + @Override + public void expireTokens() { + TransactionLegacy txn = TransactionLegacy.currentTxn(); + try { + txn.start(); + String sql = "DELETE FROM `saml_token` WHERE `created` < (NOW() - INTERVAL 1 HOUR)"; + PreparedStatement pstmt = txn.prepareAutoCloseStatement(sql); + pstmt.executeUpdate(); + txn.commit(); + } catch (Exception e) { + txn.rollback(); + throw new CloudRuntimeException("Unable to flush old SAML tokens due to exception", e); + } + } +} diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenVO.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenVO.java new file mode 100644 index 0000000000..c8ac2f1f43 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLTokenVO.java @@ -0,0 +1,97 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.saml; + +import com.cloud.utils.db.GenericDao; +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import java.util.Date; + +@Entity +@Table(name = "saml_token") +public class SAMLTokenVO implements Identity, InternalIdentity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "domain_id") + private Long domainId = null; + + @Column(name = "entity") + private String entity = null; + + @Column(name = GenericDao.CREATED_COLUMN) + private Date created; + + public SAMLTokenVO() { + } + + public SAMLTokenVO(String uuid, Long domainId, String entity) { + this.uuid = uuid; + this.domainId = domainId; + this.entity = entity; + } + + @Override + public long getId() { + return id; + } + + @Override + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public Long getDomainId() { + return domainId; + } + + public void setDomainId(long domainId) { + this.domainId = domainId; + } + + public String getEntity() { + return entity; + } + + public void setEntity(String entity) { + this.entity = entity; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } +} diff --git a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java similarity index 72% rename from utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java rename to plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java index a6d2d347be..0216ad7eb6 100644 --- a/utils/src/org/apache/cloudstack/utils/auth/SAMLUtils.java +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java @@ -17,37 +17,36 @@ // under the License. // -package org.apache.cloudstack.utils.auth; +package org.apache.cloudstack.saml; import com.cloud.utils.HttpUtils; -import org.apache.commons.codec.digest.DigestUtils; import org.apache.log4j.Logger; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.x509.X509V1CertificateGenerator; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.opensaml.Configuration; +import org.opensaml.DefaultBootstrap; import org.opensaml.common.SAMLVersion; import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.core.Assertion; +import org.opensaml.saml2.core.Attribute; +import org.opensaml.saml2.core.AttributeStatement; +import org.opensaml.saml2.core.AuthnContext; import org.opensaml.saml2.core.AuthnContextClassRef; import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration; import org.opensaml.saml2.core.AuthnRequest; import org.opensaml.saml2.core.Issuer; import org.opensaml.saml2.core.LogoutRequest; import org.opensaml.saml2.core.NameID; -import org.opensaml.saml2.core.NameIDPolicy; -import org.opensaml.saml2.core.NameIDType; import org.opensaml.saml2.core.RequestedAuthnContext; import org.opensaml.saml2.core.Response; -import org.opensaml.saml2.core.SessionIndex; import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder; import org.opensaml.saml2.core.impl.AuthnRequestBuilder; import org.opensaml.saml2.core.impl.IssuerBuilder; import org.opensaml.saml2.core.impl.LogoutRequestBuilder; import org.opensaml.saml2.core.impl.NameIDBuilder; -import org.opensaml.saml2.core.impl.NameIDPolicyBuilder; import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder; -import org.opensaml.saml2.core.impl.SessionIndexBuilder; import org.opensaml.xml.ConfigurationException; import org.opensaml.xml.XMLObject; import org.opensaml.xml.io.Marshaller; @@ -66,6 +65,7 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.FactoryConfigurationError; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -90,67 +90,85 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; +import java.util.List; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; public class SAMLUtils { public static final Logger s_logger = Logger.getLogger(SAMLUtils.class); - public static final String SAML_RESPONSE = "SAMLResponse"; - public static final String SAML_NS = "SAML-"; - public static final String SAML_NAMEID = "SAML_NAMEID"; - public static final String SAML_SESSION = "SAML_SESSION"; - public static final String SAMLSP_KEYPAIR = "SAMLSP_KEYPAIR"; - public static final String SAMLSP_X509CERT = "SAMLSP_X509CERT"; + public static String generateSecureRandomId() { + return new BigInteger(160, new SecureRandom()).toString(32); + } - public static String createSAMLId(String uid) { - if (uid == null) { + public static String getValueFromAttributeStatements(final List attributeStatements, final String attributeKey) { + if (attributeStatements == null || attributeStatements.size() < 1 || attributeKey == null) { return null; } - String hash = DigestUtils.sha256Hex(uid); - String samlUuid = SAML_NS + hash; - return samlUuid.substring(0, 40); + 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 boolean checkSAMLUser(String uuid, String username) { - if (uuid == null || uuid.isEmpty() || username == null || username.isEmpty()) { - return false; + public static String getValueFromAssertions(final List assertions, final String attributeKey) { + if (assertions == null || attributeKey == null) { + return null; } - return uuid.startsWith(SAML_NS) && createSAMLId(username).equals(uuid); + for (Assertion assertion : assertions) { + String value = getValueFromAttributeStatements(assertion.getAttributeStatements(), attributeKey); + if (value != null) { + return value; + } + } + return null; } - public static String generateSecureRandomId() { - return new BigInteger(160, new SecureRandom()).toString(32); + 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(String spId, String idpUrl, String consumerUrl) { - String authnId = generateSecureRandomId(); + public static AuthnRequest buildAuthnRequestObject(final String authnId, final String spId, final String idpUrl, final String consumerUrl) { // Issuer object IssuerBuilder issuerBuilder = new IssuerBuilder(); Issuer issuer = issuerBuilder.buildObject(); issuer.setValue(spId); - // NameIDPolicy - NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder(); - NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject(); - nameIdPolicy.setFormat(NameIDType.PERSISTENT); - nameIdPolicy.setSPNameQualifier(spId); - nameIdPolicy.setAllowCreate(true); - // AuthnContextClass AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder(); AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject( SAMLConstants.SAML20_NS, "AuthnContextClassRef", "saml"); - authnContextClassRef.setAuthnContextClassRef("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"); + authnContextClassRef.setAuthnContextClassRef(AuthnContext.PPT_AUTHN_CTX); - // AuthnContex + // AuthnContext RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder(); RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject(); - requestedAuthnContext - .setComparison(AuthnContextComparisonTypeEnumeration.EXACT); - requestedAuthnContext.getAuthnContextClassRefs().add( - authnContextClassRef); + requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT); + requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef); // Creation of AuthRequestObject AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder(); @@ -160,36 +178,27 @@ public static AuthnRequest buildAuthnRequestObject(String spId, String idpUrl, S authnRequest.setVersion(SAMLVersion.VERSION_20); authnRequest.setForceAuthn(false); authnRequest.setIsPassive(false); - authnRequest.setIssuer(issuer); authnRequest.setIssueInstant(new DateTime()); - authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); + authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI); authnRequest.setAssertionConsumerServiceURL(consumerUrl); authnRequest.setProviderName(spId); - authnRequest.setNameIDPolicy(nameIdPolicy); + authnRequest.setIssuer(issuer); authnRequest.setRequestedAuthnContext(requestedAuthnContext); return authnRequest; } - public static LogoutRequest buildLogoutRequest(String logoutUrl, String spId, NameID sessionNameId, String sessionIndex) { - IssuerBuilder issuerBuilder = new IssuerBuilder(); - Issuer issuer = issuerBuilder.buildObject(); + public static LogoutRequest buildLogoutRequest(String logoutUrl, String spId, String nameIdString) { + Issuer issuer = new IssuerBuilder().buildObject(); issuer.setValue(spId); - - SessionIndex sessionIndexElement = new SessionIndexBuilder().buildObject(); - sessionIndexElement.setSessionIndex(sessionIndex); - NameID nameID = new NameIDBuilder().buildObject(); - nameID.setValue(sessionNameId.getValue()); - nameID.setFormat(sessionNameId.getFormat()); - + nameID.setValue(nameIdString); LogoutRequest logoutRequest = new LogoutRequestBuilder().buildObject(); logoutRequest.setID(generateSecureRandomId()); logoutRequest.setDestination(logoutUrl); logoutRequest.setVersion(SAMLVersion.VERSION_20); logoutRequest.setIssueInstant(new DateTime()); logoutRequest.setIssuer(issuer); - logoutRequest.getSessionIndexes().add(sessionIndexElement); logoutRequest.setNameID(nameID); return logoutRequest; } @@ -226,13 +235,28 @@ public static Response decodeSAMLResponse(String responseMessage) return (Response) unmarshaller.unmarshall(element); } - public static String generateSAMLRequestSignature(String urlEncodedString, PrivateKey signingKey) + public static String generateSAMLRequestSignature(final String urlEncodedString, final PrivateKey signingKey, final String sigAlgorithmName) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException, UnsupportedEncodingException { if (signingKey == null) { return urlEncodedString; } - String url = urlEncodedString + "&SigAlg=" + URLEncoder.encode(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1, HttpUtils.UTF_8); - Signature signature = Signature.getInstance("SHA1withRSA"); + + String opensamlAlgoIdSignature = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1; + String javaSignatureAlgorithmName = "SHA1withRSA"; + + if (sigAlgorithmName.equalsIgnoreCase("SHA256")) { + opensamlAlgoIdSignature = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256; + javaSignatureAlgorithmName = "SHA256withRSA"; + } else if (sigAlgorithmName.equalsIgnoreCase("SHA384")) { + opensamlAlgoIdSignature = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA384; + javaSignatureAlgorithmName = "SHA384withRSA"; + } else if (sigAlgorithmName.equalsIgnoreCase("SHA512")) { + opensamlAlgoIdSignature = SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512; + javaSignatureAlgorithmName = "SHA512withRSA"; + } + + String url = urlEncodedString + "&SigAlg=" + URLEncoder.encode(opensamlAlgoIdSignature, HttpUtils.UTF_8); + Signature signature = Signature.getInstance(javaSignatureAlgorithmName); signature.initSign(signingKey); signature.update(url.getBytes()); String signatureString = Base64.encodeBytes(signature.sign(), Base64.DONT_BREAK_LINES); diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java similarity index 78% rename from plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java rename to plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java index cb16f0cc29..5b4d552223 100644 --- a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmdTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/GetServiceProviderMetaDataCmdTest.java @@ -17,13 +17,15 @@ * under the License. */ -package org.apache.cloudstack.api.command; +package org.apache.cloudstack; import com.cloud.utils.HttpUtils; import org.apache.cloudstack.api.ApiServerService; import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd; import org.apache.cloudstack.saml.SAML2AuthManager; -import org.apache.cloudstack.utils.auth.SAMLUtils; +import org.apache.cloudstack.saml.SAMLProviderMetadata; +import org.apache.cloudstack.saml.SAMLUtils; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -36,6 +38,7 @@ import javax.servlet.http.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; @@ -75,20 +78,21 @@ public void testAuthenticate() throws NoSuchFieldException, SecurityException, I String spId = "someSPID"; String url = "someUrl"; - X509Certificate cert = SAMLUtils.generateRandomX509Certificate(SAMLUtils.generateRandomKeyPair()); - Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId); - Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(cert); - Mockito.when(samlAuthManager.getIdpSingleLogOutUrl()).thenReturn(url); - Mockito.when(samlAuthManager.getSpSingleLogOutUrl()).thenReturn(url); + KeyPair kp = SAMLUtils.generateRandomKeyPair(); + X509Certificate cert = SAMLUtils.generateRandomX509Certificate(kp); + + SAMLProviderMetadata providerMetadata = new SAMLProviderMetadata(); + providerMetadata.setEntityId("random"); + providerMetadata.setSigningCertificate(cert); + providerMetadata.setEncryptionCertificate(cert); + providerMetadata.setKeyPair(kp); + providerMetadata.setSsoUrl("http://test.local"); + providerMetadata.setSloUrl("http://test.local"); + + Mockito.when(samlAuthManager.getSPMetadata()).thenReturn(providerMetadata); String result = cmd.authenticate("command", null, session, "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 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 83792c64d0..5b37388560 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.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 void authenticate() throws NoSuchFieldException, SecurityException, Illeg 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 void authenticate() throws NoSuchFieldException, SecurityException, Illeg Assert.assertFalse(pair.first()); // When there is SAMLRequest in params and user is same as the mocked one - params.put(SAMLUtils.SAML_RESPONSE, new Object[]{}); + params.put(SAMLPluginConstants.SAML_RESPONSE, new String[]{"RandomString"}); pair = authenticator.authenticate("someUID", "random", 1l, params); - Assert.assertTrue(pair.first()); + Assert.assertFalse(pair.first()); // When there is SAMLRequest in params but username is null pair = authenticator.authenticate(null, "random", 1l, params); diff --git a/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java similarity index 66% rename from utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java rename to plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java index bebfd13044..bd87831913 100644 --- a/utils/test/org/apache/cloudstack/utils/auth/SAMLUtilsTest.java +++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/SAMLUtilsTest.java @@ -17,14 +17,13 @@ // under the License. // -package org.apache.cloudstack.utils.auth; +package org.apache.cloudstack; import junit.framework.TestCase; +import org.apache.cloudstack.saml.SAMLUtils; import org.junit.Test; import org.opensaml.saml2.core.AuthnRequest; import org.opensaml.saml2.core.LogoutRequest; -import org.opensaml.saml2.core.NameID; -import org.opensaml.saml2.core.impl.NameIDBuilder; import java.security.KeyPair; import java.security.PrivateKey; @@ -32,18 +31,6 @@ public class SAMLUtilsTest extends TestCase { - @Test - public void testSAMLId() throws Exception { - assertEquals(SAMLUtils.createSAMLId(null), null); - assertEquals(SAMLUtils.createSAMLId("someUserName"), "SAML-305e19dd2581f33fd90b3949298ec8b17de"); - - assertTrue(SAMLUtils.checkSAMLUser(SAMLUtils.createSAMLId("someUserName"), "someUserName")); - assertFalse(SAMLUtils.checkSAMLUser(SAMLUtils.createSAMLId("someUserName"), "someOtherUserName")); - assertFalse(SAMLUtils.checkSAMLUser(SAMLUtils.createSAMLId(null), "someOtherUserName")); - assertFalse(SAMLUtils.checkSAMLUser("randomUID", "randomUID")); - assertFalse(SAMLUtils.checkSAMLUser(null, null)); - } - @Test public void testGenerateSecureRandomId() throws Exception { assertTrue(SAMLUtils.generateSecureRandomId().length() > 0); @@ -54,7 +41,8 @@ public 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); + String authnId = SAMLUtils.generateSecureRandomId(); + AuthnRequest req = SAMLUtils.buildAuthnRequestObject(authnId, spId, idpUrl, consumerUrl); assertEquals(req.getAssertionConsumerServiceURL(), consumerUrl); assertEquals(req.getDestination(), idpUrl); assertEquals(req.getIssuer().getValue(), spId); @@ -64,15 +52,10 @@ public void testBuildAuthnRequestObject() throws Exception { public void testBuildLogoutRequest() throws Exception { String logoutUrl = "http://logoutUrl"; String spId = "cloudstack"; - String sessionIndex = "12345"; - String nameIdString = "someNameID"; - NameID sessionNameId = new NameIDBuilder().buildObject(); - sessionNameId.setValue(nameIdString); - LogoutRequest req = SAMLUtils.buildLogoutRequest(logoutUrl, spId, sessionNameId, sessionIndex); + String nameId = "_12345"; + LogoutRequest req = SAMLUtils.buildLogoutRequest(logoutUrl, spId, nameId); assertEquals(req.getDestination(), logoutUrl); assertEquals(req.getIssuer().getValue(), spId); - assertEquals(req.getNameID().getValue(), nameIdString); - assertEquals(req.getSessionIndexes().get(0).getSessionIndex(), sessionIndex); } @Test diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmdTest.java index 30ecc938af..00455b97e2 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.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.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.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.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; @@ -75,9 +79,6 @@ public class SAML2LoginAPIAuthenticatorCmdTest { @Mock SAML2AuthManager samlAuthManager; - @Mock - ConfigurationDao configDao; - @Mock DomainManager domainMgr; @@ -104,6 +105,9 @@ private Response buildMockResponse() throws Exception { 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 void testAuthenticate() throws Exception { domainMgrField.setAccessible(true); domainMgrField.set(cmd, domainMgr); - Field configDaoField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_configDao"); - configDaoField.setAccessible(true); - configDaoField.set(cmd, configDao); - Field userAccountDaoField = SAML2LoginAPIAuthenticatorCmd.class.getDeclaredField("_userAccountDao"); userAccountDaoField.setAccessible(true); userAccountDaoField.set(cmd, userAccountDao); String spId = "someSPID"; String url = "someUrl"; - X509Certificate cert = SAMLUtils.generateRandomX509Certificate(SAMLUtils.generateRandomKeyPair()); - Mockito.when(samlAuthManager.getServiceProviderId()).thenReturn(spId); - Mockito.when(samlAuthManager.getIdpSigningKey()).thenReturn(null); - Mockito.when(samlAuthManager.getIdpSingleSignOnUrl()).thenReturn(url); - Mockito.when(samlAuthManager.getSpSingleSignOnUrl()).thenReturn(url); + KeyPair kp = SAMLUtils.generateRandomKeyPair(); + X509Certificate cert = SAMLUtils.generateRandomX509Certificate(kp); + + SAMLProviderMetadata providerMetadata = new SAMLProviderMetadata(); + providerMetadata.setEntityId("random"); + providerMetadata.setSigningCertificate(cert); + providerMetadata.setEncryptionCertificate(cert); + providerMetadata.setKeyPair(kp); + providerMetadata.setSsoUrl("http://test.local"); + providerMetadata.setSloUrl("http://test.local"); Mockito.when(session.getAttribute(Mockito.anyString())).thenReturn(null); - Mockito.when(configDao.getValue(Mockito.anyString())).thenReturn("someString"); Mockito.when(domain.getId()).thenReturn(1L); Mockito.when(domainMgr.getDomain(Mockito.anyString())).thenReturn(domain); UserAccountVO user = new UserAccountVO(); - user.setUsername(SAMLUtils.createSAMLId("someUID")); user.setId(1000L); Mockito.when(userAccountDao.getUserAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(user); Mockito.when(apiServer.verifyUser(Mockito.anyLong())).thenReturn(false); + Mockito.when(samlAuthManager.getSPMetadata()).thenReturn(providerMetadata); + Mockito.when(samlAuthManager.getIdPMetadata(Mockito.anyString())).thenReturn(providerMetadata); Map params = new HashMap(); @@ -179,16 +184,14 @@ public void testAuthenticate() throws Exception { 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 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 e9834c9dd1..eff4b296d6 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 @@ 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; @@ -47,9 +46,6 @@ public class SAML2LogoutAPIAuthenticatorCmdTest { @Mock SAML2AuthManager samlAuthManager; - @Mock - ConfigurationDao configDao; - @Mock HttpSession session; @@ -71,19 +67,10 @@ public void testAuthenticate() throws Exception { 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()); diff --git a/server/src/com/cloud/api/ApiServer.java b/server/src/com/cloud/api/ApiServer.java index 6dcf48ad0a..2ab1f7c67f 100755 --- a/server/src/com/cloud/api/ApiServer.java +++ b/server/src/com/cloud/api/ApiServer.java @@ -1062,8 +1062,8 @@ public ResponseObject loginUser(final HttpSession session, final String username final SecureRandom sesssionKeyRandom = new SecureRandom(); final byte sessionKeyBytes[] = new byte[20]; sesssionKeyRandom.nextBytes(sessionKeyBytes); - final String sessionKey = Base64.encodeBase64String(sessionKeyBytes); - session.setAttribute("sessionkey", sessionKey); + final String sessionKey = Base64.encodeBase64URLSafeString(sessionKeyBytes); + session.setAttribute(ApiConstants.SESSIONKEY, sessionKey); return createLoginResponse(session); } diff --git a/server/src/com/cloud/api/ApiServlet.java b/server/src/com/cloud/api/ApiServlet.java index 8d34dfe730..2bffc77ad8 100644 --- a/server/src/com/cloud/api/ApiServlet.java +++ b/server/src/com/cloud/api/ApiServlet.java @@ -231,7 +231,7 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp userId = (Long)session.getAttribute("userid"); final String account = (String)session.getAttribute("account"); final Object accountObj = session.getAttribute("accountobj"); - final String sessionKey = (String)session.getAttribute("sessionkey"); + final String sessionKey = (String)session.getAttribute(ApiConstants.SESSIONKEY); final String[] sessionKeyParam = (String[])params.get(ApiConstants.SESSIONKEY); if ((sessionKeyParam == null) || (sessionKey == null) || !sessionKey.equals(sessionKeyParam[0])) { try { diff --git a/server/src/com/cloud/configuration/Config.java b/server/src/com/cloud/configuration/Config.java index fc529b5180..0d6a4e66f4 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), diff --git a/setup/db/db/schema-451to452-cleanup.sql b/setup/db/db/schema-451to452-cleanup.sql new file mode 100644 index 0000000000..9f5e62a367 --- /dev/null +++ b/setup/db/db/schema-451to452-cleanup.sql @@ -0,0 +1,20 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. + +--; +-- Schema cleanup from 4.5.1 to 4.5.2; +--; diff --git a/setup/db/db/schema-451to452.sql b/setup/db/db/schema-451to452.sql new file mode 100644 index 0000000000..5c89008a83 --- /dev/null +++ b/setup/db/db/schema-451to452.sql @@ -0,0 +1,35 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. + +--; +-- Schema upgrade from 4.5.1 to 4.5.2; +--; + +DELETE FROM `cloud`.`configuration` WHERE name like 'saml%'; + +ALTER TABLE `cloud`.`user` ADD COLUMN `external_entity` text DEFAULT NULL COMMENT "reference to external federation entity"; + +DROP TABLE IF EXISTS `cloud`.`saml_token`; +CREATE TABLE `cloud`.`saml_token` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `uuid` varchar(255) UNIQUE NOT NULL COMMENT 'The Authn Unique Id', + `domain_id` bigint unsigned DEFAULT NULL, + `entity` text NOT NULL COMMENT 'Identity Provider Entity Id', + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `fk_saml_token__domain_id` FOREIGN KEY(`domain_id`) REFERENCES `domain`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index 95f06c8c1a..4fac7ee379 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -115,6 +115,9 @@ 'logout': 'Authentication', 'saml': 'Authentication', 'getSPMetadata': 'Authentication', + 'listIdps': 'Authentication', + 'authorizeSamlSso': 'Authentication', + 'listSamlAuthorization': 'Authentication', 'Capacity': 'System Capacity', 'NetworkDevice': 'Network Device', 'ExternalLoadBalancer': 'Ext Load Balancer', diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css index 0a2c57fdb3..ef33deae82 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; @@ -12730,6 +12728,14 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it background-position: -196px -704px; } +.configureSamlAuthorization .icon { + background-position: -165px -122px; +} + +.configureSamlAuthorization:hover .icon { + background-position: -165px -704px; +} + .viewConsole .icon { background-position: -231px -2px; } @@ -12953,13 +12959,6 @@ div.ui-dialog div.autoscaler div.field-group div.form-container form div.form-it border-radius: 4px; border-radius: 4px 4px 4px 4px; border: 1px solid #AFAFAF; - -moz-box-shadow: inset 0px 1px #727272; - -webkit-box-shadow: inset 0px 1px #727272; - -o-box-shadow: inset 0px 1px #727272; - box-shadow: inset 0px 1px #727272; - -moz-box-shadow: inset 0px 1px 0px #727272; - -webkit-box-shadow: inset 0px 1px 0px #727272; - -o-box-shadow: inset 0px 1px 0px #727272; } .manual-account-details > *:nth-child(even) { diff --git a/ui/dictionary.jsp b/ui/dictionary.jsp index 317acfa081..414dab8783 100644 --- a/ui/dictionary.jsp +++ b/ui/dictionary.jsp @@ -141,6 +141,7 @@ dictionary = { 'label.action.cancel.maintenance.mode': '', 'label.action.cancel.maintenance.mode.processing': '', 'label.action.change.password': '', +'label.action.configure.samlauthorization': '', 'label.action.change.service': '', 'label.action.change.service.processing': '', 'label.action.copy.ISO': '', @@ -761,7 +762,9 @@ dictionary = { 'label.local.storage': '', 'label.login': '', 'label.logout': '', -'label.saml.login': '', +'label.saml.enable': '', +'label.saml.entity': '', +'label.add.LDAP.account': '', 'label.lun': '', 'label.LUN.number': '', 'label.make.project.owner': '', diff --git a/ui/index.jsp b/ui/index.jsp index 4b601a57b0..c3bae19a54 100644 --- a/ui/index.jsp +++ b/ui/index.jsp @@ -51,28 +51,45 @@
- -
- - +
+
- -
- - + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
- -
- - + +
+
+ + +
+
+ +
+ + " />
- - " /> -
"/>