Skip to content
This repository has been archived by the owner on Feb 7, 2019. It is now read-only.

Commit

Permalink
Fixes #10879: Adapt inventory processor so it can read agent certificate
Browse files Browse the repository at this point in the history
  • Loading branch information
VinceMacBuche committed Jun 9, 2017
1 parent 1021d74 commit 12965b6
Show file tree
Hide file tree
Showing 13 changed files with 300 additions and 188 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -101,43 +103,94 @@ 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")
}
}
}
}

/*
* Retrieve the agent information from JSON. "agentType" is mandatory,
* 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)
}
}

Expand All @@ -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)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,20 @@

package com.normation.inventory.domain


import com.normation.utils.Utils._
import com.normation.utils.HashcodeCaching
import org.bouncycastle.openssl.PEMParser
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
Expand All @@ -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 = {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ case class FileSystem(
, totalSpace : Option[MemorySize] = None
) extends NodeElement with HashcodeCaching


case class Network (
name : String
, description : Option[String] = None
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Loading

0 comments on commit 12965b6

Please sign in to comment.