diff --git a/inventory-api/src/main/scala/com/normation/inventory/domain/AgentTypes.scala b/inventory-api/src/main/scala/com/normation/inventory/domain/AgentTypes.scala index 6521fd86..67b0bd39 100644 --- a/inventory-api/src/main/scala/com/normation/inventory/domain/AgentTypes.scala +++ b/inventory-api/src/main/scala/com/normation/inventory/domain/AgentTypes.scala @@ -46,41 +46,43 @@ import com.normation.utils.HashcodeCaching * */ sealed abstract class AgentType { - def toString() : String - def fullname() : String = "CFEngine "+this + def toString : String + def fullname : String = "CFEngine "+this // Tag used in fusion inventory ( > 2.3 ) lazy val tagValue = s"cfengine-${this}".toLowerCase - def toRulesPath() : String + def toRulesPath : String // the name to look for in the inventory to know the agent version def inventorySoftwareName: String // and a transformation function from reported software version name to agent version name def toAgentVersionName(softwareVersionName: String): String -} -final case object NOVA_AGENT extends AgentType with HashcodeCaching { - override def toString() = A_NOVA_AGENT - override def toRulesPath() = "/cfengine-nova" - override val inventorySoftwareName = "cfengine nova" - override def toAgentVersionName(softwareVersionName: String) = s"cfe-${softwareVersionName}" } -final case object COMMUNITY_AGENT extends AgentType with HashcodeCaching { - override def toString() = A_COMMUNITY_AGENT - override def toRulesPath() = "/cfengine-community" - override val inventorySoftwareName = "rudder-agent" - override def toAgentVersionName(softwareVersionName: String) = softwareVersionName -} +object AgentType { -final case object DSC_AGENT extends AgentType with HashcodeCaching { - override def toString() = A_DSC_AGENT - override def toRulesPath() = "/dsc" - override val inventorySoftwareName = "Rudder agent" - override def toAgentVersionName(softwareVersionName: String) = softwareVersionName+" (dsc)" -} + final case object CfeEnterprise extends AgentType with HashcodeCaching { + override def toString = A_NOVA_AGENT + override def toRulesPath = "/cfengine-nova" + override val inventorySoftwareName = "cfengine nova" + override def toAgentVersionName(softwareVersionName: String) = s"cfe-${softwareVersionName}" + } -object AgentType { - def allValues = NOVA_AGENT :: COMMUNITY_AGENT :: DSC_AGENT :: Nil + final case object CfeCommunity extends AgentType with HashcodeCaching { + override def toString = A_COMMUNITY_AGENT + override def toRulesPath = "/cfengine-community" + override val inventorySoftwareName = "rudder-agent" + override def toAgentVersionName(softwareVersionName: String) = softwareVersionName + } + + final case object Dsc extends AgentType with HashcodeCaching { + override def toString = A_DSC_AGENT + override def toRulesPath = "/dsc" + override val inventorySoftwareName = "Rudder agent" + override def toAgentVersionName(softwareVersionName: String) = softwareVersionName+" (dsc)" + } + + def allValues = CfeEnterprise :: CfeCommunity :: Dsc :: Nil def fromValue(value : String) : Box[AgentType] = { // Check if the value is correct compared to the agent tag name (fusion > 2.3) or its toString value (added by CFEngine) @@ -101,22 +103,68 @@ object AgentType { final case class AgentVersion(value: String) final case class AgentInfo( - name : AgentType + agentType : AgentType //for now, the version must be an option, because we don't add it in the inventory //and must try to find it from packages - , version: Option[AgentVersion] + , version : Option[AgentVersion] + , securityToken : SecurityToken ) object AgentInfoSerialisation { import net.liftweb.json.JsonDSL._ + import AgentType._ + import net.liftweb.json._ implicit class ToJson(agent: AgentInfo) { - def toJsonString = compactRender( - ("agentType" -> agent.name.toString()) - ~ ("version" -> agent.version.map( _.value )) - ) + def toJsonString = + compactRender( + ("agentType" -> agent.agentType.toString()) + ~ ("version" -> agent.version.map( _.value )) + ~ ("securityToken" -> + ("value" -> agent.securityToken.key) + ~ ("type" -> SecurityToken.kind(agent.securityToken)) + ) + ) + } + + def parseSecurityToken(agentType : AgentType, tokenJson: JValue, tokenDefault : Option[String]) : Box[SecurityToken]= { + import net.liftweb.json.compactRender + + def extractValue(tokenJson : JValue) = { + tokenJson \ "value" match { + case JString(s) => Some(s) + case _ => None + } + } + + agentType match { + case Dsc => tokenJson \ "type" match { + case JString(Certificate.kind) => extractValue(tokenJson) match { + case Some(token) => Full(Certificate(token)) + case None => Failure("No value defined for security token") + } + case JString(PublicKey.kind) => Failure("Cannot have a public Key for dsc agent, only a certificate is valid") + case JNothing => Failure("No value define for security token") + case invalidJson => Failure(s"Invalid value for security token, ${compactRender(invalidJson)}") + } + case _ => tokenJson \ "type" match { + case JString(Certificate.kind) => extractValue(tokenJson) match { + case Some(token) => Full(Certificate(token)) + case None => Failure("No value defined for security token") + } + case JString(PublicKey.kind) => extractValue(tokenJson) match { + case Some(token) => Full(PublicKey(token)) + case None => Failure("No value defined for security token") + } + case invalidJson => + tokenDefault match { + case Some(default) => Full(PublicKey(default)) + case None => Failure(s"Invalid value for security token, ${compactRender(invalidJson)}, and no public key were stored") + } + } + } } /* @@ -124,20 +172,25 @@ object AgentInfoSerialisation { * but version isn't, and even if we don't parse it correctly, we * successfully return an agent (without version). */ - def parseJson(s: String): Box[AgentInfo] = { + def parseJson(s: String, optToken : Option[String]): Box[AgentInfo] = { for { json <- try { Full(parse(s)) } catch { case ex: Exception => Failure(s"Can not parse agent info: ${ex.getMessage }", Full(ex), Empty) } - info <- (json \ "agentType") match { - case JString(tpe) => AgentType.fromValue(tpe).map { agentType => - (json \ "version") match { - case JString(version) => AgentInfo(agentType, Some(AgentVersion(version))) - case _ => AgentInfo(agentType, None) - } - } - case _ => Failure(s"Error when trying to parse string as JSON Agent Info (missing required field 'agentType'): ${s}") + agentType <- (json \ "agentType") match { + case JString(tpe) => AgentType.fromValue(tpe) + case JNothing => Failure("No value defined for security token") + case invalidJson => Failure(s"Error when trying to parse string as JSON Agent Info (missing required field 'agentType'): ${compactRender(invalidJson)}") + } + agentVersion = json \ "version" match { + case JString(version) => Some(AgentVersion(version)) + case _ => None + } + token <- json \ "securityToken" match { + case JObject(json) => parseSecurityToken(agentType, json, optToken) + case _ => parseSecurityToken(agentType, JNothing, optToken) } + } yield { - info + AgentInfo(agentType, agentVersion, token) } } @@ -146,15 +199,16 @@ object AgentInfoSerialisation { * - try to parse in json: if ok, we have the new version * - else, try to parse in old format, put None to version. */ - def parseCompatNonJson(s: String): Box[AgentInfo] = { - parseJson(s).or( + def parseCompatNonJson(s: String, optToken : Option[String]): Box[AgentInfo] = { + parseJson(s, optToken).or( for { - tpe <- AgentType.fromValue(s) ?~! ( - s"Error when mapping '${s}' to an agent info. We are expecting either a json like "+ + agentType <- AgentType.fromValue(s) ?~! ( + s"Error when mapping '${s}' to an agent info. We are expecting either a json like "+ s"{'agentType': type, 'version': opt_version}, or an agentType with allowed values in ${AgentType.allValues.mkString(", ")}" ) + token <- parseSecurityToken(agentType, JNothing, optToken) } yield { - AgentInfo(tpe, None) + AgentInfo(agentType, None, token) } ) } diff --git a/inventory-api/src/main/scala/com/normation/inventory/domain/DataTypes.scala b/inventory-api/src/main/scala/com/normation/inventory/domain/DataTypes.scala index 2481ab74..ad0b9a52 100644 --- a/inventory-api/src/main/scala/com/normation/inventory/domain/DataTypes.scala +++ b/inventory-api/src/main/scala/com/normation/inventory/domain/DataTypes.scala @@ -37,7 +37,6 @@ package com.normation.inventory.domain - import com.normation.utils.Utils._ import com.normation.utils.HashcodeCaching import org.bouncycastle.openssl.PEMParser @@ -45,13 +44,13 @@ import java.io.StringReader import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter import net.liftweb.common._ +import org.bouncycastle.cert.X509CertificateHolder /** * A file that contains all the simple data types, like Version, * MemorySize, Manufacturer, etc. */ - /** * A simple class to denote a manufacturer * TODO : Should be merge with SoftwareEditor @@ -63,10 +62,30 @@ final case class Manufacturer(name:String) extends HashcodeCaching { assert(!isE */ final case class SoftwareEditor(val name:String) extends HashcodeCaching { assert(!isEmpty(name)) } +sealed trait SecurityToken { + def key : String +} + +case object SecurityToken { + def kind(token : SecurityToken) = { + token match { + case _: PublicKey => PublicKey.kind + case _: Certificate => Certificate.kind + } + } + +} + +object PublicKey { + val kind = "publicKey" +} +object Certificate { + val kind = "certificate" +} /** * A simple class to denote a software cryptographic public key */ -final case class PublicKey(value : String) extends HashcodeCaching { assert(!isEmpty(value)) +final case class PublicKey(value : String) extends SecurityToken with HashcodeCaching { assert(!isEmpty(value)) // Value of the key may be stored (with old fusion inventory version) as one line and without rsa header and footer, we should add them if missing and format the key val key = { @@ -93,6 +112,32 @@ final case class PublicKey(value : String) extends HashcodeCaching { assert(!isE } +final case class Certificate(value : String) extends SecurityToken with HashcodeCaching { assert(!isEmpty(value)) + + // Value of the key may be stored (with old fusion inventory version) as one line and without rsa header and footer, we should add them if missing and format the key + val key = { + if (value.startsWith("-----BEGIN CERTIFICATE-----")) { + value + } else { + s"""-----BEGIN CERTIFICATE----- + |${value.grouped(80).mkString("\n")} + |-----END CERTIFICATE-----""".stripMargin + } + } + def cert : Box[X509CertificateHolder] = { + try { + val reader = new PEMParser(new StringReader(key)) + reader.readObject() match { + case a : X509CertificateHolder => + Full(a) + case _ => Failure(s"Key '${key}' cannot be parsed as a valid certificate") + } + } catch { + case e:Exception => Failure(s"Key '${key}' cannot be parsed as a valid certificate") + } + } + +} /** * A simple class to denote version diff --git a/inventory-api/src/main/scala/com/normation/inventory/domain/NodeInventory.scala b/inventory-api/src/main/scala/com/normation/inventory/domain/NodeInventory.scala index 489852a6..6e306159 100644 --- a/inventory-api/src/main/scala/com/normation/inventory/domain/NodeInventory.scala +++ b/inventory-api/src/main/scala/com/normation/inventory/domain/NodeInventory.scala @@ -60,7 +60,6 @@ case class FileSystem( , totalSpace : Option[MemorySize] = None ) extends NodeElement with HashcodeCaching - case class Network ( name : String , description : Option[String] = None @@ -122,7 +121,6 @@ object InetAddressUtils { } } - sealed trait OsType { def kernelName : String def name : String //name is normalized and not destined to be printed - use localization for that @@ -223,7 +221,6 @@ object BsdType { case object UnknownBsdType extends BsdType with HashcodeCaching { val name = "UnknownBSD" } case object FreeBSD extends BsdType with HashcodeCaching { val name = "FreeBSD" } - /** * The different OS type. For now, we know: * - Linux @@ -249,7 +246,6 @@ case class UnknownOS( , override val kernelVersion : Version = new Version("N/A") ) extends OsDetails(UnknownOSType, fullName, version, servicePack, kernelVersion) with HashcodeCaching - case class Linux( override val os : OsType , override val fullName : String @@ -292,9 +288,6 @@ case class Windows( , productId : Option[String] = None ) extends OsDetails(os, fullName, version, servicePack, kernelVersion) with HashcodeCaching - - - case class NodeSummary( id : NodeId , status:InventoryStatus @@ -345,9 +338,8 @@ case class NodeInventory( , archDescription : Option[String] = None , lastLoggedUser : Option[String] = None , lastLoggedUserTime : Option[DateTime] = None - , agents : Seq[AgentInfo] = Seq() - , publicKeys : Seq[PublicKey] = Seq() - , serverIps : Seq[String] = Seq() + , agents : Seq[AgentInfo] = Seq() + , serverIps : Seq[String] = Seq() , machineId : Option[(MachineUuid,InventoryStatus)] = None //if we want several ids, we would have to ass an "alternate machine" field , softwareIds : Seq[SoftwareUuid] = Seq() , accounts : Seq[String] = Seq() diff --git a/inventory-api/src/main/scala/com/normation/inventory/services/provisionning/CheckInventoryDigest.scala b/inventory-api/src/main/scala/com/normation/inventory/services/provisionning/CheckInventoryDigest.scala index 6e203b0a..ae31a894 100644 --- a/inventory-api/src/main/scala/com/normation/inventory/services/provisionning/CheckInventoryDigest.scala +++ b/inventory-api/src/main/scala/com/normation/inventory/services/provisionning/CheckInventoryDigest.scala @@ -44,9 +44,7 @@ import java.security.PublicKey import org.apache.commons.io.IOUtils import javax.xml.bind.DatatypeConverter import com.normation.inventory.services.core.ReadOnlyFullInventoryRepository -import com.normation.inventory.domain._ - - +import com.normation.inventory.domain.{ PublicKey => AgentKey, _ } /** * We are using a simple date structure that handle the digest file @@ -108,26 +106,36 @@ class ParseInventoryDigestFileV1 extends ParseInventoryDigestFile with Loggable trait CheckInventoryDigest { - /** * Here, we want to calculate the digest. The good library for that is most likely * bouncy castle: https://www.bouncycastle.org/ */ - def check(pubKey: PublicKey, digest: InventoryDigest, inventoryStream: InputStream): Box[Boolean] = { - try { - val signature = Signature.getInstance("SHA512withRSA", "BC"); - signature.initVerify(pubKey); - val data = IOUtils.toByteArray(inventoryStream) - signature.update(data); - digest match { - case InventoryDigestV1(_,digest) => - val sig = DatatypeConverter.parseHexBinary(digest) - - Full(signature.verify(sig)) - } - } catch { - case e : Exception => - Failure(e.getMessage()) + def check(securityToken: SecurityToken, digest: InventoryDigest, inventoryStream: InputStream): Box[Boolean] = { + securityToken match { + case rudderKey : com.normation.inventory.domain.PublicKey => + rudderKey.publicKey match { + case Full(pubKey) => + try { + val signature = Signature.getInstance("SHA512withRSA", "BC"); + signature.initVerify(pubKey); + val data = IOUtils.toByteArray(inventoryStream) + signature.update(data); + digest match { + case InventoryDigestV1(_,digest) => + val sig = DatatypeConverter.parseHexBinary(digest) + + Full(signature.verify(sig)) + } + } catch { + case e : Exception => + Failure(e.getMessage()) + } + case eb : EmptyBox => + eb + } + + // We don't sign with certificate for now + case _ : Certificate => Full(true) } } @@ -138,7 +146,7 @@ trait CheckInventoryDigest { */ trait GetKey { - def getKey (receivedInventory : InventoryReport) : Box[(PublicKey, KeyStatus)] + def getKey (receivedInventory : InventoryReport) : Box[(SecurityToken, KeyStatus)] } @@ -151,14 +159,13 @@ class InventoryDigestServiceV1( * either an inventory has already been treated before, it will look into ldap repository * or if there was no inventory before, it will look for the key in the received inventory */ - def getKey (receivedInventory : InventoryReport) : Box[(PublicKey, KeyStatus)] = { + def getKey (receivedInventory : InventoryReport) : Box[(SecurityToken, KeyStatus)] = { - def extractKey (node : NodeInventory) : Box[(PublicKey)]= { + def extractKey (node : NodeInventory) : Box[SecurityToken]= { for { - cfengineKey <- Box(node.publicKeys.headOption) ?~! "There is no public key in inventory" - publicKey <- cfengineKey.publicKey + agent <- Box(node.agents.headOption) ?~! "There is no public key in inventory" } yield { - publicKey + agent.securityToken } } @@ -167,17 +174,23 @@ class InventoryDigestServiceV1( val status = storedInventory.node.main.keyStatus val inventory : NodeInventory = status match { case UndefinedKey => - storedInventory.node.publicKeys.headOption match { + storedInventory.node.agents.map(_.securityToken).headOption match { case None => // There is no key and status is undefined, use received key receivedInventory.node - case Some(key) => - key.publicKey match { - // Stored key is valid, use it ! - case Full(_) => storedInventory.node - // Key stored is not valid and status is undefined try received key, - // There treat the case of the bootstrapped key for rudder root server - case _ => receivedInventory.node + case Some(securityToken) => + securityToken match { + case key : AgentKey => + key.publicKey match { + // Stored key is valid, use it ! + case Full(_) => storedInventory.node + // Key stored is not valid and status is undefined try received key, + // There treat the case of the bootstrapped key for rudder root server + case _ => receivedInventory.node + } + case cert : Certificate => + // We don't sign inventory with cert for now, use sorted one + storedInventory.node } } // Certified node always use stored inventory key diff --git a/inventory-fusion/src/main/scala/com/normation/inventory/provisioning/fusion/FusionReportUnmarshaller.scala b/inventory-fusion/src/main/scala/com/normation/inventory/provisioning/fusion/FusionReportUnmarshaller.scala index 2764f0fa..839e2a30 100644 --- a/inventory-fusion/src/main/scala/com/normation/inventory/provisioning/fusion/FusionReportUnmarshaller.scala +++ b/inventory-fusion/src/main/scala/com/normation/inventory/provisioning/fusion/FusionReportUnmarshaller.scala @@ -39,6 +39,7 @@ package com.normation.inventory.provisioning package fusion import com.normation.inventory.domain._ +import com.normation.inventory.domain.AgentType._ import com.normation.inventory.provisioning.fusion._ import java.io.InputStream import org.joda.time.DateTime @@ -292,34 +293,43 @@ class FusionReportUnmarshaller( // as a temporary solution, we are getting information from packages - val versions = { - def findAgent(software: Seq[Software], agentType: AgentType) = ( - software.find(p => p.name.getOrElse("") - .toLowerCase.contains(agentType.inventorySoftwareName )) - .flatMap(s => s.version.map(v => AgentVersion(agentType.toAgentVersionName(v.value)))) - ) - Map[AgentType, Option[AgentVersion]]( - // for nova, we get the cfengine version, which not exactly what we want, but still better than nothing - (NOVA_AGENT , findAgent(report.applications, NOVA_AGENT)) - // for community, we only want rudder-agent version - , (COMMUNITY_AGENT , findAgent(report.applications, COMMUNITY_AGENT)) - ) - } + def findAgent(software: Seq[Software], agentType: AgentType) = ( + software.find(p => p.name.getOrElse("") + .toLowerCase.contains(agentType.inventorySoftwareName )) + .flatMap(s => s.version.map(v => AgentVersion(agentType.toAgentVersionName(v.value)))) + ) (xml \\ "RUDDER").headOption match { case Some(rudder) => // Fetch all the agents configuration - val agents = ((rudder \\ "AGENT").map { agentXML => + val agents = (rudder \\ "AGENT").flatMap { agentXML => val agent = for { agentName <- boxFromOption(optText(agentXML \ "AGENT_NAME"), "could not parse agent name (tag AGENT_NAME) from rudder specific inventory") agentType <- (AgentType.fromValue(agentName)) rootUser <- boxFromOption(optText(agentXML \\ "OWNER") ,"could not parse rudder user (tag OWNER) from rudder specific inventory") policyServerId <- boxFromOption(optText(agentXML \\ "POLICY_SERVER_UUID") ,"could not parse policy server id (tag POLICY_SERVER_UUID) from specific inventory") + + optCert = optText(agentXML \ "AGENT_CERT") + optKey = optText(agentXML \ "AGENT_KEY").orElse(optText(agentXML \ "CFENGINE_KEY")) + securityToken : SecurityToken <- agentType match { + case Dsc => optCert match { + case Some(cert) => Full(Certificate(cert)) + case None => Failure("could not parse agent certificate (tag AGENT_CERT), which is mandatory for dsc agent") + } + case _ => + (optCert,optKey) match { + case (Some(cert),_) => Full(Certificate(cert)) + case (None,Some(key)) => Full(PublicKey(key)) + case (None,None) => Failure("could not parse agent security Token (tag AGENT_KEY/CFENGINE_KEY/AGENT_CERT), which is mandatory for cfengine agent") + } + } + } yield { - //cfkey is not mandatory - val agentKey = optText(agentXML \ "AGENT_KEY").orElse(optText(agentXML \ "CFENGINE_KEY")) - (agentType, rootUser, policyServerId, agentKey) + + val version = findAgent(report.applications,agentType) + + (AgentInfo(agentType,version,securityToken), rootUser, policyServerId) } agent match { case eb: EmptyBox => @@ -328,7 +338,7 @@ class FusionReportUnmarshaller( e case Full(x) => Full(x) } - }).flatten + } ( for { agentOK <- if(agents.size < 1) { @@ -341,7 +351,6 @@ class FusionReportUnmarshaller( policyServerId <- uniqueValueInSeq(agents.map(_._3), "could not parse policy server id (tag POLICY_SERVER_UUID) from specific inventory") } yield { - val keys = agents.map{case (_,_,_,key) => key.map(PublicKey)}.flatten report.copy ( node = report.node.copy ( @@ -350,8 +359,7 @@ class FusionReportUnmarshaller( , policyServerId = NodeId(policyServerId) , id = NodeId(uuid) ) - , agents = agents.map(t => AgentInfo(t._1, versions.get(t._1).flatten)) - , publicKeys = keys + , agents = agents.map(_._1) ) ) } ) match { diff --git a/inventory-fusion/src/main/scala/com/normation/inventory/provisioning/fusion/RudderParsingRules.scala b/inventory-fusion/src/main/scala/com/normation/inventory/provisioning/fusion/RudderParsingRules.scala index d38dea3e..993c4dc8 100644 --- a/inventory-fusion/src/main/scala/com/normation/inventory/provisioning/fusion/RudderParsingRules.scala +++ b/inventory-fusion/src/main/scala/com/normation/inventory/provisioning/fusion/RudderParsingRules.scala @@ -43,6 +43,7 @@ import net.liftweb.common._ import java.security.MessageDigest import com.normation.utils.UuidRegex import com.normation.inventory.provisioning.fusion.OptText.optText +import com.normation.inventory.domain.AgentType.Dsc /** * Special handling of some tags. @@ -138,7 +139,6 @@ object RudderMachineIdParsing extends FusionReportParsingExtension with Loggable } } - /** * * @@ -177,24 +177,6 @@ object RudderCpuParsing extends FusionReportParsingExtension with Loggable { } } -/** -* -*/ -class RudderPublicKeyParsing(keyNormalizer:PrintedKeyNormalizer) extends FusionReportParsingExtension { - override def isDefinedAt(x:(Node,InventoryReport)) = { x._1.label == "CFKEY" } - override def apply(x:(Node,InventoryReport)) : InventoryReport = { - optText(x._1) match { - case None => x._2 - case Some(key) => - keyNormalizer(key) match { - case "" => x._2 //we have an empty key ! - case k => x._2.copy( node = x._2.node.copy( publicKeys = (new PublicKey(key) +: x._2.node.publicKeys ) ) ) - - } - } - } -} - /** * */ @@ -208,7 +190,6 @@ object RudderRootUserParsing extends FusionReportParsingExtension { } } - /** * */ @@ -218,14 +199,29 @@ object RudderAgentNameParsing extends FusionReportParsingExtension with Loggable x._2.copy( node = x._2.node.copy( agents = x._2.node.agents ++ processAgentName(x._1) ) ) } def processAgentName(xml:NodeSeq) : Seq[AgentInfo] = { - (xml \ "AGENTNAME").flatMap(e => optText(e).flatMap( a => - AgentType.fromValue(a) match { - case Full(x) => Full(AgentInfo(x, None)) - case e:EmptyBox => - logger.error("Ignore agent type '%s': unknown value. Authorized values are %s".format(a, AgentType.allValues.mkString(", "))) + + val keys = (xml \ "CFKEY").map { optText } + val agents = (xml \ "AGENTNAME").map( optText) + + agents.zipAll(keys, None , None).flatMap{ + case (None,None) => Empty + case (None,Some(_)) => Empty + // A key but no agent, skip + case (Some(agent), None) => + // No key error + logger.error(s"No key for agent ${agent} defined, a key is mandatory") Empty + case (Some(agent),Some(key)) => + AgentType.fromValue(agent) match { + case Full(Dsc) => + Failure("Dsc agent cannot be added using AGENTNAME tag") + case Full(agent) => + Full(AgentInfo(agent, None, PublicKey(key))) + case e:EmptyBox => + logger.error("Ignore agent type '%s': unknown value. Authorized values are %s".format(agent, AgentType.allValues.mkString(", "))) + Empty + } } - ) ) } } @@ -244,5 +240,3 @@ object RudderServerRoleParsing extends FusionReportParsingExtension { (xml \ "SERVER_ROLE").flatMap(e => optText(e).map(ServerRole(_))) } } - - diff --git a/inventory-fusion/src/test/resources/fusion-report/dsc-agent.ocs b/inventory-fusion/src/test/resources/fusion-report/dsc-agent.ocs index 82bea2a5..44996495 100644 --- a/inventory-fusion/src/test/resources/fusion-report/dsc-agent.ocs +++ b/inventory-fusion/src/test/resources/fusion-report/dsc-agent.ocs @@ -351,14 +351,35 @@ windows-dsc - -----BEGIN RSA PUBLIC KEY----- -MIIBCAKCAQEAp8XambRZMLpI9wCqkeZNGQuG02wjjiQD9NIUrESkcfMYZ5qvLhX1 -bjufiDTbs1M2ySEGGCDHJN991C67kvPvstFtKx+w8m6DBC9DkpwLR0wGgWN0VCqC -cG8UwJIRz9SrPTQKRLFWI/fl8b92xcZvDPVXv1r0yS75toX3Ja4dBwyOqaiUGLBO -kOpwXSfrmHJ16IDWwLUZlesfomgCyzfZ8AUuTOX7JbvxLI9neXysoueXqDMIXklS -013pCqdZW/kCp3/aPwBjoK5EFnR8XxVLCGSXGXz6JG2Nng7ox8bbjm0VurFQErce -VyodQDN1ecAsViRI1V18Z+PvZll8isonWQIBIw== ------END RSA PUBLIC KEY----- + -----BEGIN CERTIFICATE----- +MIIE0zCCArugAwIBAgIJAJqeHdZEkElqMA0GCSqGSIb3DQEBCwUAMBExDzANBgNV +BAMMBkFHRU5UMTAeFw0xNzA2MDcxMzA1NTlaFw0yNzA2MDUxMzA1NTlaMBExDzAN +BgNVBAMMBkFHRU5UMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOPd +mhaXZcezSjgTi6Zn38alftrYwfAyuKoznll4oM2T5DkYaZ2Lyclvv7jar/4ozUsA +Jmzw6O8QjCiaufVkHCytGJoQmtRPhywgOp0S0vqUVCXsdfh5dhwZ3Qjy107M6Z6E +6lyjggsgGZM23LBUNjsMyY7FefSHGpLpeWXgX/1CW4XeBaBq6mzo5A/1N3iyKLL4 +yOGcNli11H4snlmqGJ+YDfNfCIeTkx2wdFftOxRkPFZ1TETmZSt5idrNvGU2IOnp +tXnT1kN/iknW6ZLu0wLRGe/b0Hde6ZqfPZLdrBn2oeMBvkM4eOl5GpFIFQa0uSEh +tr57vCqC6KSVvejjNr2VwKs11+9Bkdim6h7ndX74tFrR3N7cBczkXYoc+tlP2MHX +t/DqXxkB3OhmDVfmlzC/CV9M5rE7GSpWRx8O46wYrkf+6J1Z4p0rOjZNDPolEJWY +KxKzS//34wgqPfqY7iSav7ElpAFcZRFFfIKldpjpM2M5Gul0dXpBMEqHkmlcRDMT +VBrHYuTyyXhtdApdVZXDocfAma7UOkis5WcVn2+6O/pI88yVXkkgK4lSNOPBMgc0 +6XLI2YgoGB3hnpGTfKbTbj3uGV8AI2/N3x91gGXRu4EtrvYtiiuxmpT1NJLmHVAA +vkfQwNm+CFqXzNDn+OPOM+cgsc08Qd++UXUmOI5lAgMBAAGjLjAsMAsGA1UdDwQE +AwIEsDAdBgNVHQ4EFgQU+S14UemkyCDsEaa5Gto8Qt9xIFEwDQYJKoZIhvcNAQEL +BQADggIBADTLZLpX/ffSNzxgla7MsboBFVW8JzaFtG8JOe+oYxeI9nHCB6M6fJao +hBhF7nUA0MyjIoEMfFVPF7LUTf3k0XvJNvXMJGNxlye1jsIKTWJo29ESERUc9eRk +LDIC6fbWV0PVjo1VolxnKZoLznOIRITPxAxfmn48n2/YJ5eGT8MF4cgTTpIP0cbD +I7UDDB1GRpLfu/K42Ll4CrTAbLABtdcGv4Rn4CtDXu+KrSpOJG675S6UhjT7ozjj +J/QDV8XcNWW0BF+2w2zzJsmSo8NjExuNXgsVh29uhCPQGYrjO03xBtz1GmWKX8wS +RGqgVQI0dHHlcfYSLaaA/zkwU9wkvmiwUS7ODMfYSrCTVNBkRvV/oIKmN9mY9I3R +//wCOPI18kk6fONEHjOng9NJD6LGVlGrnR+k3MFVWqORuvIUFEhD4YynHPZqu1g8 +60ePwvw8pouI4yoRtCQBZEw2rgnS0bGiY3bpou3sTq+eLxQs/3DmvJkhLGzgfeai +MFMym+kchpMxU69krkiZXDWwkXxo3txXnPevPx12pQKBSQJONfeADDok6GQYEOeH +GvNfE6prJSpDchFWdRKK3/RcknoKwq0hDyFICOhZkb3Zc4oWknDgJz/huorU/nnb +QHjxOfVdBMBBXq0oHSUsOzf9rIxMlWQmCQyNDB6qD+bXZnAZp7j7 +-----END CERTIFICATE----- + agent1\vagrant 192.168.42.0 root diff --git a/inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestPostUnmarshaller.scala b/inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestPostUnmarshaller.scala index 739e9372..64ee2de3 100644 --- a/inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestPostUnmarshaller.scala +++ b/inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestPostUnmarshaller.scala @@ -47,8 +47,7 @@ import scala.xml.XML import net.liftweb.common.EmptyBox import net.liftweb.common.Full import java.io.File -import com.normation.inventory.domain.COMMUNITY_AGENT -import com.normation.inventory.domain.NOVA_AGENT +import com.normation.inventory.domain.AgentType._ import com.normation.inventory.domain.Windows import com.normation.inventory.domain.Windows2012 import com.normation.inventory.services.provisioning.PreUnmarshall @@ -56,7 +55,6 @@ import java.io.InputStream import org.xml.sax.SAXParseException import scala.xml.NodeSeq - /** * A simple test class to check that the demo data file is up to date * with the schema (there may still be a desynchronization if both @@ -108,7 +106,6 @@ class TestPreUnmarshaller extends Specification { linux.toOption must not beNone } - "With a valid inventory in Windows" in { val windows = post.check("fusion-report/WIN-AI8CLNPLOV5-2014-06-20-18-15-49.ocs") windows.toOption must not beNone diff --git a/inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestReportParsing.scala b/inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestReportParsing.scala index 32cf5ebb..e651f920 100644 --- a/inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestReportParsing.scala +++ b/inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestReportParsing.scala @@ -45,6 +45,7 @@ import net.liftweb.common._ import scala.xml.XML import java.io.File import com.normation.inventory.domain._ +import com.normation.inventory.domain.AgentType._ /** * A simple test class to check that the demo data file is up to date @@ -132,28 +133,28 @@ class TestReportParsing extends Specification with Loggable { "Agent in Inventory" should { "should be empty when there is no agent" in { - val agents = parser.parse("fusion-report/rudder-tag/minimal-zero-agent.ocs").node.agents.map(_.name).toList + val agents = parser.parse("fusion-report/rudder-tag/minimal-zero-agent.ocs").node.agents.map(_.agentType).toList agents must be empty } "should have one agent when using community" in { - val agents = parser.parse("fusion-report/rudder-tag/minimal-one-agent.ocs").node.agents.map(_.name).toList - agents == (COMMUNITY_AGENT :: Nil) + val agents = parser.parse("fusion-report/rudder-tag/minimal-one-agent.ocs").node.agents.map(_.agentType).toList + agents == (CfeCommunity :: Nil) } "should have two agent when using community and nova" in { - val agents = parser.parse("fusion-report/rudder-tag/minimal-two-agents.ocs").node.agents.map(_.name).toList - agents == (COMMUNITY_AGENT :: NOVA_AGENT :: Nil) + val agents = parser.parse("fusion-report/rudder-tag/minimal-two-agents.ocs").node.agents.map(_.agentType).toList + agents == (CfeCommunity :: CfeEnterprise :: Nil) } "should be empty when there is two agents, using two different policy servers" in { - val agents = parser.parse("fusion-report/rudder-tag/minimal-two-agents-fails.ocs").node.agents.map(_.name).toList + val agents = parser.parse("fusion-report/rudder-tag/minimal-two-agents-fails.ocs").node.agents.map(_.agentType).toList agents must be empty } "should have dsc agent agent when using rudder-agent based on dsc" in { - val agents = parser.parse("fusion-report/dsc-agent.ocs").node.agents.map(_.name).toList - agents == (DSC_AGENT :: Nil) + val agents = parser.parse("fusion-report/dsc-agent.ocs").node.agents.map(_.agentType).toList + agents == (Dsc :: Nil) } } diff --git a/inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestSignatureService.scala b/inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestSignatureService.scala index 351d2fac..82f44d8b 100644 --- a/inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestSignatureService.scala +++ b/inventory-fusion/src/test/scala/com/normation/inventory/provisioning/fusion/TestSignatureService.scala @@ -34,7 +34,6 @@ ************************************************************************************* */ - package com.normation.inventory.provisioning.fusion import org.junit.runner._ @@ -48,7 +47,7 @@ import java.security.PublicKey import com.normation.inventory.domain.KeyStatus import java.security.Security import org.bouncycastle.jce.provider.BouncyCastleProvider - +import com.normation.inventory.domain.SecurityToken @RunWith(classOf[JUnitRunner]) class TestSignatureService extends Specification with Loggable { @@ -87,19 +86,19 @@ class TestSignatureService extends Specification with Loggable { * either an inventory has already been treated before, it will look into ldap repository * or if there was no inventory before, it will look for the key in the received inventory */ - def getKey (receivedInventory : InventoryReport) : Box[(PublicKey, KeyStatus)] = { + def getKey (receivedInventory : InventoryReport) : Box[(SecurityToken, KeyStatus)] = { for { - cfengineKey <- Box(receivedInventory.node.publicKeys.headOption) ?~! "There is no public key in inventory" + cfengineKey <- Box(receivedInventory.node.agents.headOption) ?~! "There is no public key in inventory" keyStatus = receivedInventory.node.main.keyStatus - publicKey <- cfengineKey.publicKey + publicKey = cfengineKey.securityToken//.publicKey } yield { - (publicKey,keyStatus) + (publicKey,keyStatus) } } } val keyNorm = new PrintedKeyNormalizer - val extension = new RudderPublicKeyParsing(keyNorm) + val extension = RudderAgentNameParsing val parser = new FusionReportUnmarshaller( new StringUuidGeneratorImpl @@ -126,7 +125,6 @@ class TestSignatureService extends Specification with Loggable { } } - "a signed report" should { "Be ok if checked with correct signature" in { (for { diff --git a/inventory-provisioning-core/src/main/scala/com/normation/inventory/ldap/provisioning/NodeIdFinder.scala b/inventory-provisioning-core/src/main/scala/com/normation/inventory/ldap/provisioning/NodeIdFinder.scala index 4e77c684..e4fad344 100644 --- a/inventory-provisioning-core/src/main/scala/com/normation/inventory/ldap/provisioning/NodeIdFinder.scala +++ b/inventory-provisioning-core/src/main/scala/com/normation/inventory/ldap/provisioning/NodeIdFinder.scala @@ -67,7 +67,6 @@ class UseExistingNodeIdFinder(inventoryDitService:InventoryDitService, ldap:LDAP } } - /* * Retrieve the id from the cfengine public key */ @@ -78,7 +77,7 @@ class ComparePublicKeyIdFinder( ) extends NodeInventoryDNFinder with Loggable { override def tryWith(entity:NodeInventory) : Box[(NodeId,InventoryStatus)] = { - val keys = entity.publicKeys + val keys = entity.agents.map(_.securityToken) if(keys.size > 0) { val keysFilter = OR((keys.map( k => EQ(A_PKEYS,k.key))):_*) diff --git a/inventory-provisioning-web/src/main/scala/com/normation/inventory/provisioning/endpoint/config/AppConfig.scala b/inventory-provisioning-web/src/main/scala/com/normation/inventory/provisioning/endpoint/config/AppConfig.scala index 73c8c388..9bfa8eb9 100644 --- a/inventory-provisioning-web/src/main/scala/com/normation/inventory/provisioning/endpoint/config/AppConfig.scala +++ b/inventory-provisioning-web/src/main/scala/com/normation/inventory/provisioning/endpoint/config/AppConfig.scala @@ -109,7 +109,6 @@ class AppConfig { @Bean def removedNodesDit = new InventoryDit(REMOVED_INVENTORIES_DN,SOFTWARE_INVENTORIES_DN,"Removed Servers") - @Bean def inventoryDitService = new InventoryDitServiceImpl(pendingNodesDit,acceptedNodesDit, removedNodesDit) @@ -130,7 +129,6 @@ class AppConfig { RudderPolicyServerParsing :: RudderMachineIdParsing :: RudderCpuParsing :: - new RudderPublicKeyParsing(keyNorm) :: RudderRootUserParsing :: RudderAgentNameParsing :: RudderServerRoleParsing :: @@ -205,7 +203,6 @@ class AppConfig { NamedNodeInventoryDNFinderAction("use_existing_id", new UseExistingNodeIdFinder(inventoryDitService,roLdapConnectionProvider,acceptedNodesDit.BASE_DN.getParent)) )) - @Bean def machineFinder() : MachineDNFinderAction = new MachineDNFinderService(Seq( @@ -265,7 +262,6 @@ class AppConfig { , postCommitPipeline ) - /* * configure the file handler */ @@ -276,7 +272,6 @@ class AppConfig { c } - /* * The REST end point where OCSi report are * uploaded diff --git a/inventory-repository/src/main/scala/com/normation/inventory/ldap/core/InventoryMapper.scala b/inventory-repository/src/main/scala/com/normation/inventory/ldap/core/InventoryMapper.scala index 2a0a7903..938282ce 100644 --- a/inventory-repository/src/main/scala/com/normation/inventory/ldap/core/InventoryMapper.scala +++ b/inventory-repository/src/main/scala/com/normation/inventory/ldap/core/InventoryMapper.scala @@ -52,7 +52,6 @@ import InetAddressUtils._ import com.normation.utils.Control.sequence import com.normation.inventory.domain.NodeTimezone - class DateTimeSerializer extends Serializer[DateTime] { private val IntervalClass = classOf[DateTime] @@ -123,7 +122,6 @@ class InventoryMapper( } } - //////////////////////////////////////////////////////////////////// ///////////////////////// Machine Elements ///////////////////////// //////////////////////////////////////////////////////////////////// @@ -350,7 +348,6 @@ class InventoryMapper( ///////////////////////// Video ///////////////////////// - def entryFromVideo(elt:Video,dit:InventoryDit,machineId:MachineUuid) : LDAPEntry = { val e = dit.MACHINES.VIDEO.model(machineId,elt.name) e.setOpt(elt.description, A_DESCRIPTION, {x:String => x}) @@ -427,7 +424,6 @@ class InventoryMapper( root.setOpt(machine.manufacturer,A_MANUFACTURER, {x:Manufacturer => x.name}) root.setOpt(machine.systemSerialNumber,A_SERIAL_NUMBER, {x:String => x}) - val tree = LDAPTree(root) //now, add machine elements as children machine.bios.foreach { x => tree.addChild(entryFromBios(x, dit, machine.id)) } @@ -555,7 +551,6 @@ class InventoryMapper( ///////////////////////// Networks ///////////////////////// - def entryFromNetwork(elt:Network, dit:InventoryDit, serverId:NodeId) : LDAPEntry = { val e = dit.NODES.NETWORK.model(serverId,elt.name) e.setOpt(elt.description, A_DESCRIPTION, {x:String => x}) @@ -602,7 +597,6 @@ class InventoryMapper( ///////////////////////// VM INFO ///////////////////////// - def entryFromVMInfo(elt:VirtualMachine, dit:InventoryDit, serverId:NodeId) : LDAPEntry = { val e = dit.NODES.VM.model(serverId,elt.uuid.value) e.setOpt(elt.description, A_DESCRIPTION, {x:String => x}) @@ -633,11 +627,8 @@ class InventoryMapper( } } - //////////////////Node/ NodeInventory ///////////////////////// - - // User defined properties : the regexp that the data should abide by // {KEY}VALUE private[this] val userDefinedPropertyRegex = """\{([^\}]+)\}(.+)""".r @@ -720,7 +711,6 @@ class InventoryMapper( root.setOpt(server.inventoryDate, A_INVENTORY_DATE, { x: DateTime => GeneralizedTime(x).toString }) root.setOpt(server.receiveDate, A_RECEIVE_DATE, { x: DateTime => GeneralizedTime(x).toString }) root +=! (A_AGENTS_NAME, server.agents.map(x => x.toJsonString):_*) - root +=! (A_PKEYS, server.publicKeys.map(x => x.key):_*) root +=! (A_SOFTWARE_DN, server.softwareIds.map(x => dit.SOFTWARE.SOFT.dn(x).toString):_*) root +=! (A_EV, server.environmentVariables.map(x => Serialization.write(x)):_*) root +=! (A_PROCESS, server.processes.map(x => Serialization.write(x)):_*) @@ -867,9 +857,17 @@ class InventoryMapper( hostname <- requiredAttr(A_HOSTNAME) rootUser <- requiredAttr(A_ROOT_USER) policyServerId <- requiredAttr(A_POLICY_SERVER_UUID) - agentNames <- sequence(entry.valuesFor(A_AGENTS_NAME).toSeq) { x => - AgentInfoSerialisation.parseCompatNonJson(x) //compat to be able to migrate easely from Rudder < 4.0 - } + publicKeys = entry.valuesFor(A_PKEYS).map(Some(_)) + agentNames <- { + val agents = entry.valuesFor(A_AGENTS_NAME).toSeq.map(Some(_)) + val agentWithKeys = agents.zipAll(publicKeys, None,None).filter(_._1.isDefined) + sequence(agentWithKeys) { + case (Some(agent),key) => + AgentInfoSerialisation.parseCompatNonJson(agent,key) + case _ => + Failure("Should not happen") + } + } //now, look for the OS type osDetails <- mapOsDetailsFromEntry(entry) //now, optionnal things @@ -928,7 +926,6 @@ class InventoryMapper( , lastLoggedUser , lastLoggedUserTime , agentNames - , publicKeys.toSeq , serverIps , machineId , softwareIds