diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/EventLogRepository.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/EventLogRepository.scala index 9c4a29e052f..b8010099e69 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/EventLogRepository.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/EventLogRepository.scala @@ -374,6 +374,8 @@ trait EventLogRepository { */ def getEventLogByCriteria(criteria: Option[String], limit :Option[Int] = None, orderBy: Option[String] = None) : IOResult[Vector[EventLog]] + def count: IOResult[Long] + def getEventLogByChangeRequest( changeRequest : ChangeRequestId , xpath : String diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/jdbc/EventLogJdbcRepository.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/jdbc/EventLogJdbcRepository.scala index 68aa070dcdb..60b5eea4e62 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/jdbc/EventLogJdbcRepository.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/jdbc/EventLogJdbcRepository.scala @@ -200,6 +200,16 @@ class EventLogJdbcRepository( }).transact(xa)) } + def count: IOResult[Long] = { + val q ="SELECT count(*) FROM eventlog" + //sql"SELECT count(*) FROM eventlog".query[Long].option.transact(xa).unsafeRunSync + transactIOResult(s"Error when retrieving event logs count with request: ${q}")(xa => (for { + entries <- query[Long](q).unique + } yield { + entries + }).transact(xa)) + } + def getEventLogByCriteria(criteria : Option[String], optLimit:Option[Int] = None, orderBy:Option[String]) : IOResult[Vector[EventLog]] = { val where = criteria.map(c => s"where ${c}").getOrElse("") diff --git a/webapp/sources/rudder/rudder-rest/pom.xml b/webapp/sources/rudder/rudder-rest/pom.xml index ee598f7b4c4..d073c007ca0 100644 --- a/webapp/sources/rudder/rudder-rest/pom.xml +++ b/webapp/sources/rudder/rudder-rest/pom.xml @@ -98,6 +98,11 @@ along with Rudder. If not, see . + + org.springframework.security + spring-security-core + ${spring-security-version} + com.normation.rudder diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/internal/EventLogAPI.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/internal/EventLogAPI.scala new file mode 100644 index 00000000000..492641e31c3 --- /dev/null +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/internal/EventLogAPI.scala @@ -0,0 +1,163 @@ +/* +************************************************************************************* +* Copyright 2019 Normation SAS +************************************************************************************* +* +* This file is part of Rudder. +* +* Rudder is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* In accordance with the terms of section 7 (7. Additional Terms.) of +* the GNU General Public License version 3, the copyright holders add +* the following Additional permissions: +* Notwithstanding to the terms of section 5 (5. Conveying Modified Source +* Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General +* Public License version 3, when you create a Related Module, this +* Related Module is not considered as a part of the work and may be +* distributed under the license agreement of your choice. +* A "Related Module" means a set of sources files including their +* documentation that, without modification of the Source Code, enables +* supplementary functions or services in addition to those offered by +* the Software. +* +* Rudder is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with Rudder. If not, see . + +* +************************************************************************************* +*/ + +package com.normation.rudder.rest.internal +import com.normation.box._ +import com.normation.eventlog._ +import com.normation.rudder.db.Doobie +import com.normation.rudder.repository.EventLogRepository +import com.normation.rudder.rest.RestExtractorService +import com.normation.rudder.rest.RestUtils._ +import com.normation.rudder.web.components.DateFormaterService +import com.normation.rudder.web.services._ +import net.liftweb.common._ +import net.liftweb.http.rest.RestHelper +import net.liftweb.http.{JsonResponse, LiftResponse, Req, S} +import net.liftweb.json.JValue +import net.liftweb.json.JsonDSL._ + +class EventLogAPI ( + repos: EventLogRepository + , restExtractor : RestExtractorService + , eventLogDetail : EventListDisplayer + , doobie : Doobie +) extends RestHelper with Loggable { + + def serialize(event: EventLog): JValue = { + import net.liftweb.json.JsonDSL._ + + ( ("id" -> (event.id.map(_.toString).getOrElse("Unknown"): String)) + ~ ("date" -> DateFormaterService.getFormatedDate(event.creationDate)) + ~ ("actor" -> event.principal.name) + ~ ("type" -> S.?("rudder.log.eventType.names." + event.eventType.serialize)) + ~ ("description" -> eventLogDetail.displayDescription(event).toString) + ~ ("hasDetails" -> (if(event.details != ) true else false)) + ) + } + + def responseFormater(draw: Int, totalRecord: Long, totalFiltered: Long, logs: Vector[EventLog], errorMsg: Option[String] = None): JValue = { + errorMsg match { + case Some(msg) => + ( ("draw" -> draw) + ~ ("recordsTotal" -> totalRecord) + ~ ("recordsFiltered" -> totalFiltered) + ~ ("data" -> logs.map(serialize)) + ~ ("error" -> msg) + ) + case _ => + ( ("draw" -> draw) + ~ ("recordsTotal" -> totalRecord) + ~ ("recordsFiltered" -> totalFiltered) + ~ ("data" -> logs.map(serialize)) + ) + } + } + + def getEventLogBySlice(start: Int, nbelement: Int, criteria : Option[String], optLimit:Option[Int] = None, orderBy:Option[String]): Box[(Int,Vector[EventLog])] = { + repos.getEventLogByCriteria(criteria, optLimit, orderBy).toBox match { + case Full(events) => Full((events.size, events.slice(start,start+nbelement))) + case eb: EmptyBox => eb ?~! s"Error when trying fetch eventlogs from database for page ${(start/nbelement)+1}" + } + } + + def requestDispatch: PartialFunction[Req, () => Box[LiftResponse]] = { + case Get(Nil, req) => + + val draw = req.params.get("draw") match { + case Some(value :: Nil) => Full(value.toInt) + case None => Failure("Missing 'draw' field from datatable's request") + } + val start = req.params.get("start") match { + case Some(value :: Nil) => Full(value) + case None => Failure("Missing 'start' field from datatable's request") + } + val length = req.params.get("length") match { + case Some(value :: Nil) => Full(value) + case None => Failure("Missing 'length' field from datatable's request") + } + + val response = (draw, start, length) match { + case (Full(d), Full(s), Full(l)) => + repos.count.toBox match { + case Full(totalRecord) => + getEventLogBySlice(s.toInt, l.toInt, None, None, Some("creationdate DESC" )) match { + case Full((totalFiltered, events)) => + responseFormater(d, totalRecord, totalFiltered, events) + case eb: EmptyBox => + val fail = eb ?~! "Failed to get eventlogs" + responseFormater(d, totalRecord, 0, Vector.empty, Some(fail.messageChain)) + } + case eb: EmptyBox => + val fail = eb ?~! "Failed to get event log's count" + responseFormater(d, 0, 0, Vector.empty, Some(fail.messageChain)) + } + + case (ebDraw: EmptyBox,_, _) => + val fail = ebDraw ?~! "Missing parameter in request" + responseFormater(0, 0, 0, Vector.empty, Some(fail.messageChain)) + + case (_,ebStart: EmptyBox, _) => + val fail = ebStart ?~! "Missing parameter in request" + responseFormater(0, 0, 0, Vector.empty, Some(fail.messageChain)) + + case (_,_, ebLength: EmptyBox) => + val fail = ebLength ?~! "Missing parameter in request" + responseFormater(0, 0, 0, Vector.empty, Some(fail.messageChain)) + } + JsonResponse(response, Nil, Nil, 200) + + case Get(id :: "details" :: Nil, _) => + repos.getEventLogByCriteria(Some(s"id = $id")).toBox match { + case Full(e) => + e.headOption match { + case Some(eventLog) => + val crid = eventLog.id.flatMap(repos.getEventLogWithChangeRequest(_).toBox match { + case Full(Some((_, crId))) => crId + case _ => None + }) + val htmlDetails = eventLogDetail.displayDetails(eventLog, crid) + toJsonResponse(None, "content" -> htmlDetails.toString())("eventdetails", prettify = false) + case None => + toJsonError(None, s"EventLog $id not found")("eventdetails", prettify = false) + } + case eb: EmptyBox => + val e = eb ?~! s"Error when trying to retrieve eventlog : $id" + toJsonError(None, e.messageChain)("eventdetails", prettify = false) + } + } + serve("secure" / "api" / "eventlog" prefix requestDispatch) +} diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/CurrentUserService.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/CurrentUserService.scala new file mode 100644 index 00000000000..533218b7d44 --- /dev/null +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/CurrentUserService.scala @@ -0,0 +1,86 @@ +/* +************************************************************************************* +* Copyright 2011 Normation SAS +************************************************************************************* +* +* This file is part of Rudder. +* +* Rudder is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* In accordance with the terms of section 7 (7. Additional Terms.) of +* the GNU General Public License version 3, the copyright holders add +* the following Additional permissions: +* Notwithstanding to the terms of section 5 (5. Conveying Modified Source +* Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General +* Public License version 3, when you create a Related Module, this +* Related Module is not considered as a part of the work and may be +* distributed under the license agreement of your choice. +* A "Related Module" means a set of sources files including their +* documentation that, without modification of the Source Code, enables +* supplementary functions or services in addition to those offered by +* the Software. +* +* Rudder is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with Rudder. If not, see . + +* +************************************************************************************* +*/ + +package com.normation.rudder.web.services + +import com.normation.rudder.{AuthorizationType, Rights, RudderAccount, User} +import com.normation.rudder.api.ApiAuthorization +import net.liftweb.http.SessionVar +import org.springframework.security.core.context.SecurityContextHolder + +/** + * An utility class that get the currently logged user + * (if any) + * + */ +object CurrentUserService extends SessionVar[Option[RudderUserDetail]] ({ + SecurityContextHolder.getContext.getAuthentication match { + case null => None + case auth => auth.getPrincipal match { + case u:RudderUserDetail => Some(u) + case _ => None + } + } + +}) with User { + + def getRights : Rights = this.get match { + case Some(u) => u.authz + case None => new Rights(AuthorizationType.NoRights) + } + + def account : RudderAccount = this.get match { + case None => RudderAccount.User("unknown", "") + case Some(u) => u.account + } + + def checkRights(auth:AuthorizationType) : Boolean = { + val authz = getRights.authorizationTypes + if (authz.contains(AuthorizationType.NoRights)) false + else auth match{ + case AuthorizationType.NoRights => false + case _ => authz.contains(auth) + } + } + + def getApiAuthz: ApiAuthorization = { + this.get match { + case None => ApiAuthorization.None + case Some(u) => u.apiAuthz + } + } +} diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/DiffDisplayer.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/DiffDisplayer.scala new file mode 100644 index 00000000000..646f285fb44 --- /dev/null +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/DiffDisplayer.scala @@ -0,0 +1,257 @@ +/* +************************************************************************************* +* Copyright 2013 Normation SAS +************************************************************************************* +* +* This file is part of Rudder. +* +* Rudder is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* In accordance with the terms of section 7 (7. Additional Terms.) of +* the GNU General Public License version 3, the copyright holders add +* the following Additional permissions: +* Notwithstanding to the terms of section 5 (5. Conveying Modified Source +* Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General +* Public License version 3, when you create a Related Module, this +* Related Module is not considered as a part of the work and may be +* distributed under the license agreement of your choice. +* A "Related Module" means a set of sources files including their +* documentation that, without modification of the Source Code, enables +* supplementary functions or services in addition to those offered by +* the Software. +* +* Rudder is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with Rudder. If not, see . + +* +************************************************************************************* +*/ + +package com.normation.rudder.web.services + +import com.normation.rudder.domain.{NodeDit, RudderDit} + +import scala.xml.NodeSeq +import com.normation.rudder.domain.policies.DirectiveId +import com.normation.rudder.repository.{FullNodeGroupCategory, RoRuleRepository} +import com.normation.rudder.rule.category.{RuleCategory, RuleCategoryId, RuleCategoryService} +import net.liftweb.common.Full +import net.liftweb.common.EmptyBox +import net.liftweb.common.Loggable +import com.normation.rudder.domain.policies._ +import com.normation.rudder.repository.ldap.{LDAPEntityMapper, RoLDAPNodeGroupRepository, RoLDAPRuleRepository, ScalaLock} +import com.normation.rudder.web.model.LinkUtil +import RudderProperties._ +import com.normation.inventory.ldap.core.{InventoryDit, InventoryDitService, InventoryDitServiceImpl, InventoryMapper} +import com.normation.ldap.sdk.ROPooledSimpleAuthConnectionProvider +import com.normation.rudder.domain.logger.ApplicationLogger +import com.normation.rudder.domain.queries.{DitQueryData, ObjectCriterion, SubGroupChoice} +import com.normation.rudder.services.queries.{CmdbQueryParser, DefaultStringQueryParser, JsonQueryLexer} +import com.typesafe.config.ConfigException + +import scala.language.implicitConversions + +trait DiffItem[T] { + + def display(implicit displayer : T => NodeSeq) : NodeSeq + +} + +case class Added[T]( + value:T +) extends DiffItem[T] { + val newValue = Some(value) + + def display(implicit displayer : T => NodeSeq) : NodeSeq = +
  • + + {displayer(value)} +
  • +} + +case class Deleted[T]( + value:T +) extends DiffItem[T] { + + def display(implicit displayer : T => NodeSeq) : NodeSeq = +
  • + -  {displayer(value)} +
  • +} + +case class Unchanged[T]( + value:T +) extends DiffItem[T] { + + def display(implicit displayer : T => NodeSeq) : NodeSeq = +
  • +    {displayer(value)} +
  • +} + +// Not used yet, but for later use +case class Modified[T]( + oldValue:T + , newValue:T +) extends DiffItem[T] { + + private[this] val delete = Deleted(oldValue) + private[this] val add = Added(oldValue) + + def display(implicit displayer : T => NodeSeq) : NodeSeq = + delete.display ++ add.display +} + +class DiffDisplayer(linkUtil: LinkUtil) extends Loggable { + + private[this] implicit def displayDirective(directiveId: DirectiveId) = { + Directive {linkUtil.createDirectiveLink(directiveId)} + } + def displayDirectiveChangeList ( + oldDirectives:Seq[DirectiveId] + , newDirectives:Seq[DirectiveId] + ) : NodeSeq = { + + // First, find unchanged and deleted (have find no clean way to make a 3 way partition) + val (unchanged,deleted) = oldDirectives.partition(newDirectives.contains) + // Get the added ones + val added = newDirectives.filterNot(unchanged.contains).map(Added(_)) + val deletedMap = deleted.map(Deleted(_)) + val unchangedMap = unchanged.map(Unchanged(_)) + + // Finally mix all maps together in one and display it + val changeMap:Seq[DiffItem[DirectiveId]] = deletedMap ++ unchangedMap ++ added +
      + { for { + change <- changeMap + } yield { + // Implicit used here (displayDirective) + change.display + } } +
    + } + + // Almost the same as display Directive see comments there for more details + def displayRuleTargets ( + oldTargets:Seq[RuleTarget] + , newTargets:Seq[RuleTarget] + , groupLib: FullNodeGroupCategory + ) : NodeSeq = { + + implicit def displayNodeGroup(target: RuleTarget) : NodeSeq= { + target match { + case TargetUnion(targets) => + all Nodes from:
      {targets.map(t =>
    • {displayNodeGroup(t)}
    • )}
    + case TargetIntersection(targets) => + Nodes that belongs to all these groups:
      {targets.map(t =>
    • {displayNodeGroup(t)}
    • )}
    + case TargetExclusion(included,excluded) => + Include {displayNodeGroup(included)} +
    Exclude {displayNodeGroup(excluded)} + + case GroupTarget(nodeGroupId) => + Group {linkUtil.createGroupLink(nodeGroupId)} + case x => groupLib.allTargets.get(x).map{ targetInfo => + + {targetInfo.name} + {if (targetInfo.isSystem) (System)} + + }.getOrElse( {x.target}) + } + } + + (oldTargets,newTargets) match { + case (Seq(TargetExclusion(newIncluded,newExcluded)),Seq(TargetExclusion(oldIncluded,oldExcluded))) => + def displayKind(kind: TargetComposition) : NodeSeq= { + kind match { + case _:TargetUnion => + all Nodes from: + case _:TargetIntersection => + Nodes that belongs to all these groups: + } + } + + val includedKind = { + ((newIncluded,oldIncluded) match { + case (_:TargetUnion,_:TargetUnion) | (_:TargetIntersection,_:TargetIntersection) => + Seq(Unchanged (newIncluded)) + case _ => + (Seq(Deleted(oldIncluded),Added(newIncluded))) + }).flatMap(_.display(displayKind)) + } + val excludedKind = { + ((newExcluded,oldExcluded) match { + case (_:TargetUnion,_:TargetUnion) | (_:TargetIntersection,_:TargetIntersection) => + Seq(Unchanged (newExcluded)) + case _ => + (Seq(Deleted(oldExcluded),Added(newExcluded))) + }).flatMap(_.display(displayKind)) + } + val includedTargets = displayRuleTargets(newIncluded.targets.toSeq,oldIncluded.targets.toSeq, groupLib) + val excludedTargets = displayRuleTargets(newExcluded.targets.toSeq,oldExcluded.targets.toSeq, groupLib) + Include ++ includedKind ++ includedTargets ++ + Exclude ++ excludedKind ++ excludedTargets + + case (_,_) => + val (unchanged,deleted) = oldTargets.partition(newTargets.contains) + val added = newTargets.filterNot(unchanged.contains).map(Added(_)) + val deletedMap = deleted.map(Deleted(_)) + val unchangedMap = unchanged.map(Unchanged(_)) + + val changeMap:Seq[DiffItem[RuleTarget]] = deletedMap ++ unchangedMap ++ added +
      + { for { + change <- changeMap + } yield { + // Implicit used here (displayNodeGroup) + change.display + } + } +
    + } + } + + // + private[this] val ruleCategoryService = new RuleCategoryService() + + def displayRuleCategory ( + rootCategory: RuleCategory + , oldCategory : RuleCategoryId + , newCategory : Option[RuleCategoryId] + ) = { + + def getCategoryFullName(category : RuleCategoryId) = { + ruleCategoryService.shortFqdn(rootCategory, category) match { + case Full(fqdn) => fqdn + case eb : EmptyBox => + logger.error(s"Error while looking for category ${category.value}") + category.value + } + } + implicit def displayRuleCategory(ruleCategoryId: RuleCategoryId) = { + {getCategoryFullName(ruleCategoryId)} + } + + newCategory match { + case Some(newCategory) => + val changes = Seq(Deleted(oldCategory),Added(newCategory)) +
      + { for { + change <- changes + } yield { + // Implicit used here (displayRuleCategory) + change.display + } + } +
    + case None => displayRuleCategory(oldCategory) + + } + } +} diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/EventLogDetailsService.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/EventLogDetailsService.scala new file mode 100644 index 00000000000..295dfa74aee --- /dev/null +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/EventLogDetailsService.scala @@ -0,0 +1,1699 @@ +/* +************************************************************************************* +* Copyright 2011 Normation SAS +************************************************************************************* +* +* This file is part of Rudder. +* +* Rudder is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* In accordance with the terms of section 7 (7. Additional Terms.) of +* the GNU General Public License version 3, the copyright holders add +* the following Additional permissions: +* Notwithstanding to the terms of section 5 (5. Conveying Modified Source +* Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General +* Public License version 3, when you create a Related Module, this +* Related Module is not considered as a part of the work and may be +* distributed under the license agreement of your choice. +* A "Related Module" means a set of sources files including their +* documentation that, without modification of the Source Code, enables +* supplementary functions or services in addition to those offered by +* the Software. +* +* Rudder is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with Rudder. If not, see . + +* +************************************************************************************* +*/ +package com.normation.rudder.web.services + +import scala.xml._ +import com.normation.eventlog.EventLog +import com.normation.rudder.domain.eventlog._ +import net.liftweb.common._ +import net.liftweb.util.Helpers._ +import com.normation.rudder.domain.policies._ +import com.normation.rudder.domain.nodes._ +import com.normation.rudder.services.eventlog.EventLogDetailsService +import com.normation.rudder.web.components.DateFormaterService +import com.normation.cfclerk.domain.TechniqueName +import com.normation.inventory.domain.NodeId +import com.normation.rudder.domain.queries.Query +import net.liftweb.http.js._ +import net.liftweb.http.js.JE._ +import net.liftweb.http.js.JsCmds._ +import net.liftweb.http.SHtml +import net.liftweb.http.S +import com.normation.rudder.batch.SuccessStatus +import com.normation.rudder.batch.ErrorStatus +import com.normation.rudder.repository._ +import com.normation.rudder.services.nodes.NodeInfoService +import com.normation.rudder.services.modification.ModificationService +import com.normation.rudder.services.user.PersonIdentService +import org.eclipse.jgit.lib.PersonIdent +import org.joda.time.DateTime +import com.normation.rudder.services.eventlog.RollbackInfo +import com.normation.rudder.domain.workflows.ChangeRequestId + +import scala.util.Try +import scala.util.Success +import scala.util.{Failure => Catch} +import com.normation.rudder.domain.eventlog.WorkflowStepChanged +import com.normation.rudder.domain.workflows.WorkflowStepChange +import com.normation.rudder.domain.parameters._ +import com.normation.rudder.api._ +import net.liftweb.json._ +import com.normation.rudder.reports.HeartbeatConfiguration +import com.normation.rudder.reports.AgentRunInterval +import com.normation.rudder.rule.category.RuleCategory +import com.normation.rudder.rule.category.RoRuleCategoryRepository +import org.joda.time.format.DateTimeFormat +import com.normation.rudder.web.model.LinkUtil +import org.joda.time.format.ISODateTimeFormat +import com.normation.box._ +import scala.concurrent.Future + +/** + * Used to display the event list, in the pending modification (AsyncDeployment), + * or in the administration EventLogsViewer + */ +class EventListDisplayer( + logDetailsService : EventLogDetailsService + , repos : EventLogRepository + , nodeGroupRepository : RoNodeGroupRepository + , directiveRepository : RoDirectiveRepository + , nodeInfoService : NodeInfoService + , ruleCatRepository : RoRuleCategoryRepository + , modificationService : ModificationService + , personIdentService : PersonIdentService + , linkUtil : LinkUtil +) extends Loggable { + + private[this] val xmlPretty = new scala.xml.PrettyPrinter(80, 2) + + private[this] val gridName = "eventLogsGrid" + + def display(refreshEvents:() => Box[Seq[EventLog]]) : NodeSeq = { + val limit: Int = 500 + //common part between last events and interval + def displayEvents(events: Box[Seq[EventLog]]) :JsCmd = { + events match { + case Full(events) => + val lines = { + val el = events.map(EventLogLine(_)).toList.sortWith(_.event.creationDate.getMillis > _.event.creationDate.getMillis) + if(el.size > limit) JsTableData(el.take(limit)) else JsTableData(el) + } + val limitMsg = if(events.size > limit)
    {limit} results taken over {events.size} results
    else
    + val call = JsRaw(s"refreshTable('${gridName}',${lines.json.toJsCmd})") & SetHtml("limitEventDisplayer", limitMsg) + call + case eb : EmptyBox => + val fail = eb ?~! "Could not get latest event logs" + logger.error(fail.messageChain) + val xml =
    Error when trying to get last event logs. Error message was: {fail.messageChain}
    + SetHtml("eventLogsError",xml) + } + } + + def getLastEvents : JsCmd = { + displayEvents(refreshEvents()) + } + + + def getEventsInterval(jsonInterval: String): JsCmd = { + import java.sql.Timestamp + val format = DateTimeFormat.forPattern("YYYY-MM-dd HH:mm:ss") + + displayEvents(for { + parsed <- tryo(parse(jsonInterval)) ?~! s"Error when trying to parse '${jsonInterval}' as a JSON datastructure with fields 'start' and 'end'" + startStr <- parsed \ "start" match { + case JString(startStr) if startStr.nonEmpty => + val date = tryo(DateTime.parse(startStr, format)) ?~! s"Error when trying to parse start date '${startStr}" + date match { + case Full(d) => Full(Some(new Timestamp(d.getMillis))) + case eb: EmptyBox => + eb ?~! s"Invalid start date" + } + case _ => Full(None) + } + endStr <- parsed \ "end" match { + case JString(endStr) if endStr.nonEmpty => + val date = tryo(DateTime.parse(endStr, format)) ?~! s"Error when trying to parse end date '${endStr}" + date match { + case Full(d) => Full(Some(new Timestamp(d.getMillis))) + case eb: EmptyBox => + eb ?~! s"Invalid end date" + } + case _ => Full(None) + } + whereStatement = (startStr, endStr) match { + case (None, None) => None + case (Some(start), None) => Some(s" creationdate > '$start'") + case (None, Some(end)) => Some(s" creationdate < '$end'") + case (Some(start), Some(end)) => + val orderedDate = if(start.after(end)) (end, start) else (start, end) + Some(s" creationdate > '${orderedDate._1}' and creationdate < '${orderedDate._2}'") + } + logs <- repos.getEventLogByCriteria(whereStatement, None, Some("id DESC")).toBox + } yield { + logs + }) + } + + val refresh = AnonFunc(SHtml.ajaxInvoke( () => getLastEvents)) + + Script(OnLoad(JsRaw(s""" + var pickEventLogsInInterval = ${AnonFunc(SHtml.ajaxCall(JsRaw( + """'{"start":"'+$(".pickStartInput").val()+'", "end":"'+$(".pickEndInput").val()+'"}'""" + ), getEventsInterval)._2).toJsCmd} + var refreshEventLogs = ${refresh.toJsCmd}; + createEventLogTable('${gridName}',[], '${S.contextPath}', refreshEventLogs, pickEventLogsInInterval) + refreshEventLogs(); + """))) + + } + + /* + * Javascript object containing all data to create a line in event logs table + * { "id" : Event log id [Int] + * , "date": date the event log was produced [Date/String] + * , "actor": Name of the actor making the event [String] + * , "type" : Type of the event log [String] + * , "description" : Description of the event [String] + * , "details" : function/ajax call, setting the details content, takes the id of the element to set [Function(String)] + * , "hasDetails" : do our event needs to display details (do we need to be able to open the row [Boolean] + * } + */ + case class EventLogLine(event : EventLog) extends JsTableLine { +// val detailsCallback = { +// AnonFunc("details",SHtml.ajaxCall(JsVar("details"), {(abc) => +// val crId = event.id.flatMap(repos.getEventLogWithChangeRequest(_) match { +// case Full(Some((_,crId))) => crId +// case _ => None +// }) +// SetHtml(abc,displayDetails(event,crId)) +// } +// ) +// ) +// } + + val json = { + JsObj( + "id" -> (event.id.map(_.toString).getOrElse("Unknown"): String) + , "date" -> DateFormaterService.getFormatedDate(event.creationDate) + , "actor" -> event.principal.name + , "type" -> S.?("rudder.log.eventType.names." + event.eventType.serialize) + , "description" -> displayDescription(event).toString +// , "details" -> detailsCallback + , "hasDetails" -> boolToJsExp(event.details != ) + ) + } + } + + //////////////////// Display description/details of //////////////////// + + //convention: "X" means "ignore" + + def displayDescription(event:EventLog) = { + import linkUtil._ + def crDesc(x:EventLog, actionName: NodeSeq) = { + val id = (x.details \ "rule" \ "id").text + val name = (x.details \ "rule" \ "displayName").text + Text("Rule ") ++ { + if(id.size < 1) Text(name) + else {name} ++ actionName + } + } + + def piDesc(x:EventLog, actionName: NodeSeq) = { + val id = (x.details \ "directive" \ "id").text + val name = (x.details \ "directive" \ "displayName").text + Text("Directive ") ++ { + if(id.size < 1) Text(name) + else {name} ++ actionName + } + } + + def groupDesc(x:EventLog, actionName: NodeSeq) = { + val id = (x.details \ "nodeGroup" \ "id").text + val name = (x.details \ "nodeGroup" \ "displayName").text + Text("Group ") ++ { + if(id.size < 1) Text(name) + else {name} ++ actionName + } + } + + def nodeDesc(x:EventLog, actionName: NodeSeq) = { + val id = (x.details \\ "node" \ "id").text + val name = (x.details \\ "node" \ "hostname").text + Text("Node ") ++ { + if ((id.size < 1)||(actionName==Text(" deleted"))) Text(name) + else {name} ++ actionName + } + } + + def techniqueDesc(x:EventLog, actionName: NodeSeq) = { + val name = (x.details \ "activeTechnique" \ "techniqueName").text + Text("Technique %s".format(name)) ++ actionName + } + + def globalParamDesc(x:EventLog, actionName: NodeSeq) = { + val name = (x.details \ "globalParameter" \ "name").text + Text(s"Global Parameter ${name} ${actionName}") + } + + def changeRequestDesc(x:EventLog, actionName: NodeSeq) = { + val name = (x.details \ "changeRequest" \ "name").text + val idNode = (x.details \ "changeRequest" \ "id").text.trim + val xml = Try(idNode.toInt) match { + case Success(id) => + if (actionName==Text(" deleted")) + Text(name) + else + {name} + + case Catch(e) => + logger.error(s"could not translate ${idNode} to a correct chage request identifier: ${e.getMessage()}") + Text(name) + } + Text("Change request ") ++ xml ++ actionName + } + + def workflowStepDesc(x:EventLog) = { + logDetailsService.getWorkflotStepChange(x.details) match { + case Full(WorkflowStepChange(crId,from,to)) => + Text("Change request #") ++ + {crId} ++ + Text(s" status modified from ${from} to ${to}") + + case eb:EmptyBox => val fail = eb ?~! "could not display workflow step event log" + logger.error(fail.msg) + Text("Change request status modified") + } + } + + def apiAccountDesc(x:EventLog, actionName: NodeSeq) = { + val name = (x.details \ "apiAccount" \ "name").text + Text(s"API Account ${name} ${actionName}") + } + + event match { + case x:ActivateRedButton => Text("Stop Rudder agents on all nodes") + case x:ReleaseRedButton => Text("Start again Rudder agents on all nodes") + case x:AcceptNodeEventLog => nodeDesc(x, Text(" accepted")) + case x:RefuseNodeEventLog => nodeDesc(x, Text(" refused")) + case x:DeleteNodeEventLog => nodeDesc(x, Text(" deleted")) + case x:LoginEventLog => Text("User '%s' login".format(x.principal.name)) + case x:LogoutEventLog => Text("User '%s' logout".format(x.principal.name)) + case x:BadCredentialsEventLog => Text("User '%s' failed to login: bad credentials".format(x.principal.name)) + case x:AutomaticStartDeployement => Text("Automatically update policies") + case x:ManualStartDeployement => Text("Manually update policies") + case x:ApplicationStarted => Text("Rudder starts") + case x:ModifyRule => crDesc(x,Text(" modified")) + case x:DeleteRule => crDesc(x,Text(" deleted")) + case x:AddRule => crDesc(x,Text(" added")) + case x:ModifyDirective => piDesc(x,Text(" modified")) + case x:DeleteDirective => piDesc(x,Text(" deleted")) + case x:AddDirective => piDesc(x,Text(" added")) + case x:ModifyNodeGroup => groupDesc(x,Text(" modified")) + case x:DeleteNodeGroup => groupDesc(x,Text(" deleted")) + case x:AddNodeGroup => groupDesc(x,Text(" added")) + case x:ClearCacheEventLog => Text("Clear caches") + case x:UpdatePolicyServer => Text("Change Policy Server authorized network") + case x:ReloadTechniqueLibrary => Text("Technique library updated") + case x:ModifyTechnique => techniqueDesc(x, Text(" modified")) + case x:DeleteTechnique => techniqueDesc(x, Text(" deleted")) + case x:SuccessfulDeployment => Text("Successful policy update") + case x:FailedDeployment => Text("Failed policy update") + case x:ExportGroupsArchive => Text("New groups archive") + case x:ExportTechniqueLibraryArchive => Text("New Directive library archive") + case x:ExportRulesArchive => Text("New Rules archives") + case x:ExportParametersArchive => Text("New Parameters archives") + case x:ExportFullArchive => Text("New full archive") + case x:ImportGroupsArchive => Text("Restoring group archive") + case x:ImportTechniqueLibraryArchive => Text("Restoring Directive library archive") + case x:ImportRulesArchive => Text("Restoring Rules archive") + case x:ImportParametersArchive => Text("Restoring Parameters archive") + case x:ImportFullArchive => Text("Restoring full archive") + case _:AddChangeRequest => changeRequestDesc(event,Text(" created")) + case _:DeleteChangeRequest => changeRequestDesc(event,Text(" deleted")) + case _:ModifyChangeRequest => changeRequestDesc(event,Text(" modified")) + case _:Rollback => Text("Restore a previous state of configuration policy") + case x:WorkflowStepChanged => workflowStepDesc(x) + case x:AddGlobalParameter => globalParamDesc(x, Text(" added")) + case x:ModifyGlobalParameter => globalParamDesc(x, Text(" modified")) + case x:DeleteGlobalParameter => globalParamDesc(x, Text(" deleted")) + case x:CreateAPIAccountEventLog => apiAccountDesc(x, Text(" added")) + case x:ModifyAPIAccountEventLog => apiAccountDesc(x, Text(" modified")) + case x:DeleteAPIAccountEventLog => apiAccountDesc(x, Text(" deleted")) + case x:ModifyGlobalProperty => Text(s"Modify '${x.propertyName}' global property") + case x:ModifyNode => nodeDesc(x, Text(" modified")) + case _ => Text("Unknow event type") + } + } + + def displayDetails(event:EventLog,changeRequestId:Option[ChangeRequestId]): NodeSeq = { + + val groupLib = nodeGroupRepository.getFullGroupLibrary().toBox.openOr(return
    System error when trying to get the group library
    ) + val rootRuleCategory = ruleCatRepository.getRootCategory.toBox.openOr(return
    System error when trying to get the rule categories
    ) + + val generatedByChangeRequest = + changeRequestId match { + case None => NodeSeq.Empty + case Some(id) =>

    This change was introduced by change request {SHtml.a(() => S.redirectTo(linkUtil.changeRequestLink(id)),Text(s"#${id}"))}

    + } + def xmlParameters(eventId: Option[Int]) = { + eventId match { + case None => NodeSeq.Empty + case Some(id) => + + + } + } + + def showConfirmationDialog(action : RollBackAction, commiter:PersonIdent) : JsCmd = { + val cancel : JsCmd = { + SetHtml("confirm%d".format(event.id.getOrElse(0)), NodeSeq.Empty) & + JsRaw(""" $('#rollback%d').show();""".format(event.id.getOrElse(0))) + } + + val dialog = +

    + + {"Are you sure you want to restore configuration policy %s this change".format(action.name)} +

    + { + SHtml.ajaxButton( + "Cancel" + , () => cancel + , ("class","btn btn-default") + ) + } + { + SHtml.ajaxButton( + "Confirm" + , () => { + event.id match { + case Some(id) => val select = action.selectRollbackedEventsRequest(id) + repos.getEventLogByCriteria(Some(select)).toBox match { + case Full(events) => + val rollbackedEvents = events.filter(_.canRollBack) + action.action(event,commiter,rollbackedEvents,event) + S.redirectTo("eventLogs") + case eb => S.error("Problem while performing a rollback") + logger.error("Problem while performing a rollback : ",eb) + cancel + } + case None => val failure = "Problem while performing a rollback, could not find event id" + S.error(failure) + logger.error(failure) + cancel + } + } + ,("class" ,"btn btn-danger") + ) + } + + def showDialog : JsCmd = { + SetHtml("confirm%d".format(event.id.getOrElse(0)), dialog) & + JsRaw(""" $('#rollback%d').hide(); + $('#confirm%d').stop(true, true).slideDown(1000); """.format(event.id.getOrElse(0),event.id.getOrElse(0))) + } + + showDialog + } + + def addRestoreAction() = + personIdentService.getPersonIdentOrDefault(CurrentUserService.actor.name).toBox match { + case Full(commiter) => + var rollbackAction : RollBackAction = null + + if (event.canRollBack) + modificationService.getCommitsfromEventLog(event).map{ commit => +
    +
    Rollback +
    + Restore configuration policy to +
      { + SHtml.radio( + Seq("before","after") + , Full("before") + , {value:String => + rollbackAction = value match { + case "after" => RollbackTo + case "before" => RollbackBefore + } + } + , ("class", "radio") + ).flatMap(e => +
    • + +
    • + ) + } +
    + this change + + { SHtml.ajaxSubmit( + "Restore" + , () => showConfirmationDialog(rollbackAction,commiter) + , ("style","vertical-align:50%;") + , ("class","btn btn-default btn-sm") + ) + } + +
    + +
    +
    + }.getOrElse(NodeSeq.Empty) + else + NodeSeq.Empty + case eb:EmptyBox => logger.error("this should not happen, as person identifier service always return a working value") + NodeSeq.Empty + } + + val reasonHtml = { + val r = event.eventDetails.reason.getOrElse("") + if(r == "") NodeSeq.Empty + else
    Reason: {r}
    + } + + def errorMessage(e:EmptyBox) = { + logger.debug(e ?~! "Error when parsing details.", e) + +
    +

    Details for that node were not in a recognized format. + Raw data are displayed next:

    + { xmlParameters(event.id) } +
    +
    + } + { + (event match { + /* + * bug in scalac : https://issues.scala-lang.org/browse/SI-6897 + * A workaround is to type with a Nodeseq + */ + case add:AddRule => + "*" #> { val xml : NodeSeq = logDetailsService.getRuleAddDetails(add.details) match { + case Full(addDiff) => +
    + { generatedByChangeRequest } + { addRestoreAction } + { ruleDetails(crDetailsXML, addDiff.rule, groupLib, rootRuleCategory)} + { reasonHtml } + { xmlParameters(event.id) } +
    + case Failure(m,_,_) =>

    {m}

    + case e:EmptyBox => errorMessage(e) + } + xml + } + + case del:DeleteRule => + "*" #> { val xml : NodeSeq = logDetailsService.getRuleDeleteDetails(del.details) match { + case Full(delDiff) => +
    + { addRestoreAction } + { generatedByChangeRequest } + { ruleDetails(crDetailsXML, delDiff.rule, groupLib, rootRuleCategory) } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => errorMessage(e) + } + xml } + + case mod:ModifyRule => + val diffDisplayer = new DiffDisplayer(linkUtil) + "*" #> { val xml : NodeSeq = logDetailsService.getRuleModifyDetails(mod.details) match { + case Full(modDiff) => +
    + { addRestoreAction } + { generatedByChangeRequest } +

    Rule overview:

    +
      +
    • Rule ID: { modDiff.id.value }
    • +
    • Name: { + modDiff.modName.map(diff => diff.newValue).getOrElse(modDiff.name) + }
    • +
    + { + val modCategory = modDiff.modCategory.map { + case SimpleDiff(oldOne,newOne) => +
  • Rule category changed:
  • ++ + diffDisplayer.displayRuleCategory(rootRuleCategory, oldOne, Some(newOne)) + } + + ( + "#name" #> mapSimpleDiff(modDiff.modName) & + "#category" #> modCategory & + "#isEnabled *" #> mapSimpleDiff(modDiff.modIsActivatedStatus) & + "#isSystem *" #> mapSimpleDiff(modDiff.modIsSystem) & + "#shortDescription *" #> mapSimpleDiff(modDiff.modShortDescription) & + "#longDescription *" #> mapSimpleDiff(modDiff.modLongDescription) & + "#target" #> ( + modDiff.modTarget.map{ + case SimpleDiff(oldOnes,newOnes) => +
  • Group Targets changed:
  • ++ + diffDisplayer.displayRuleTargets(oldOnes.toSeq,newOnes.toSeq, groupLib) + } ) & + "#policies" #> ( + modDiff.modDirectiveIds.map{ + case SimpleDiff(oldOnes,newOnes) => +
  • Directives changed:
  • ++ + diffDisplayer.displayDirectiveChangeList(oldOnes.toSeq,newOnes.toSeq) + } ) + )(crModDetailsXML) + } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => errorMessage(e) + } + xml } + + ///////// Directive ///////// + + case x:ModifyDirective => + "*" #> { val xml : NodeSeq = logDetailsService.getDirectiveModifyDetails(x.details) match { + case Full(modDiff) => +
    + { addRestoreAction } + { generatedByChangeRequest } +

    Directive overview:

    +
      +
    • Directive ID: { modDiff.id.value }
    • +
    • Name: { + modDiff.modName.map(diff => diff.newValue.toString).getOrElse(modDiff.name) + }
    • +
    + {( + "#name" #> mapSimpleDiff(modDiff.modName, modDiff.id) & + "#priority *" #> mapSimpleDiff(modDiff.modPriority) & + "#isEnabled *" #> mapSimpleDiff(modDiff.modIsActivated) & + "#isSystem *" #> mapSimpleDiff(modDiff.modIsSystem) & + "#shortDescription *" #> mapSimpleDiff(modDiff.modShortDescription) & + "#longDescription *" #> mapSimpleDiff(modDiff.modLongDescription) & + "#ptVersion *" #> mapSimpleDiff(modDiff.modTechniqueVersion) & + "#parameters" #> ( + modDiff.modParameters.map { diff => + "#diff" #> displaydirectiveInnerFormDiff(diff, event.id) + } + ) + )(piModDirectiveDetailsXML)} + { reasonHtml } + { xmlParameters(event.id) } +
    + case Failure(m, _, _) =>

    {m}

    + case e:EmptyBox => errorMessage(e) + } + xml } + + case x:AddDirective => + "*" #> { val xml : NodeSeq = logDetailsService.getDirectiveAddDetails(x.details) match { + case Full((diff,sectionVal)) => +
    + { addRestoreAction } + { generatedByChangeRequest } + { directiveDetails(piDetailsXML, diff.techniqueName, + diff.directive, sectionVal) } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => errorMessage(e) + } + xml} + + case x:DeleteDirective => + "*" #> { val xml : NodeSeq = logDetailsService.getDirectiveDeleteDetails(x.details) match { + case Full((diff,sectionVal)) => +
    + { addRestoreAction } + { generatedByChangeRequest } + { directiveDetails(piDetailsXML, diff.techniqueName, + diff.directive, sectionVal) } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => errorMessage(e) + } + xml } + + ///////// Node Group ///////// + + case x:ModifyNodeGroup => + "*" #> { val xml : NodeSeq = logDetailsService.getNodeGroupModifyDetails(x.details) match { + case Full(modDiff) => +
    + { addRestoreAction } + { generatedByChangeRequest } +

    Group overview:

    +
      +
    • Node Group ID: { modDiff.id.value }
    • +
    • Name: { + modDiff.modName.map(diff => diff.newValue.toString).getOrElse(modDiff.name) + }
    • +
    + {( + "#name" #> mapSimpleDiff(modDiff.modName) & + "#isEnabled *" #> mapSimpleDiff(modDiff.modIsActivated) & + "#isSystem *" #> mapSimpleDiff(modDiff.modIsSystem) & + "#isDynamic *" #> mapSimpleDiff(modDiff.modIsDynamic) & + "#shortDescription *" #> mapSimpleDiff(modDiff.modDescription) & + "#query" #> ( + modDiff.modQuery.map { diff => + val mapOptionQuery = (opt:Option[Query]) => + opt match { + case None => Text("None") + case Some(q) => Text(q.toJSONString) + } + + ".diffOldValue *" #> mapOptionQuery(diff.oldValue) & + ".diffNewValue *" #> mapOptionQuery(diff.newValue) + } + ) & + "#nodes" #> ( + modDiff.modNodeList.map { diff => + ".diffOldValue *" #> nodeGroupDetails(diff.oldValue) & + ".diffNewValue *" #> nodeGroupDetails(diff.newValue) + } + ) + )(groupModDetailsXML)} + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => errorMessage(e) + } + xml } + + case x:AddNodeGroup => + "*" #> { val xml : NodeSeq = logDetailsService.getNodeGroupAddDetails(x.details) match { + case Full(diff) => +
    + { addRestoreAction } + { generatedByChangeRequest } + { groupDetails(groupDetailsXML, diff.group) } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => errorMessage(e) + } + xml } + + case x:DeleteNodeGroup => + "*" #> { val xml : NodeSeq = logDetailsService.getNodeGroupDeleteDetails(x.details) match { + case Full(diff) => +
    + { addRestoreAction } + { generatedByChangeRequest } + { groupDetails(groupDetailsXML, diff.group) } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => errorMessage(e) + } + xml } + + ///////// Node Group ///////// + + case x:AcceptNodeEventLog => + "*" #> { val xml : NodeSeq = logDetailsService.getAcceptNodeLogDetails(x.details) match { + case Full(details) => +
    + { addRestoreAction } +

    Node accepted overview:

    + { nodeDetails(details) } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => errorMessage(e) + } + xml } + + case x:RefuseNodeEventLog => + "*" #> { val xml : NodeSeq = logDetailsService.getRefuseNodeLogDetails(x.details) match { + case Full(details) => +
    + { addRestoreAction } +

    Node refused overview:

    + { nodeDetails(details) } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => errorMessage(e) + } + xml } + + case x:DeleteNodeEventLog => + "*" #> { val xml : NodeSeq = logDetailsService.getDeleteNodeLogDetails(x.details) match { + case Full(details) => +
    + { addRestoreAction } +

    Node deleted overview:

    + { nodeDetails(details) } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => errorMessage(e) + } + xml } + + ////////// deployment ////////// + case x:SuccessfulDeployment => + "*" #> { val xml : NodeSeq = logDetailsService.getDeploymentStatusDetails(x.details) match { + case Full(SuccessStatus(id,started,ended,_)) => +
    + { addRestoreAction } +

    Successful policy update:

    +
      +
    • ID: {id}
    • +
    • Start time: {DateFormaterService.getFormatedDate(started)}
    • +
    • End Time: {DateFormaterService.getFormatedDate(ended)}
    • +
    + { reasonHtml } + { xmlParameters(event.id) } +
    + case Full(_) => errorMessage(Failure("Unconsistant policy update status")) + case e:EmptyBox => errorMessage(e) + } + xml } + + case x:FailedDeployment => + "*" #> { val xml : NodeSeq = logDetailsService.getDeploymentStatusDetails(x.details) match { + case Full(ErrorStatus(id,started,ended,failure)) => +
    + { addRestoreAction } +

    Failed policy update:

    +
      +
    • ID: {id}
    • +
    • Start time: {DateFormaterService.getFormatedDate(started)}
    • +
    • End Time: {DateFormaterService.getFormatedDate(ended)}
    • +
    • Error stack trace: {failure.messageChain}
    • +
    + { reasonHtml } + { xmlParameters(event.id) } +
    + case Full(_) => errorMessage(Failure("Unconsistant policy update status")) + case e:EmptyBox => errorMessage(e) + } + xml } + + case x:AutomaticStartDeployement => + "*" #> +
    + { addRestoreAction } + { xmlParameters(event.id) } +
    + + ////////// change authorized networks ////////// + + case x:UpdatePolicyServer => + "*" #> { val xml : NodeSeq = logDetailsService.getUpdatePolicyServerDetails(x.details) match { + case Full(details) => + + def networksToXML(nets:Seq[String]) = { +
      { nets.map { n =>
    • {n}
    • } }
    + } + +
    + { addRestoreAction }{ + ( + ".diffOldValue *" #> networksToXML(details.oldNetworks) & + ".diffNewValue *" #> networksToXML(details.newNetworks) + )(authorizedNetworksXML) + } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => errorMessage(e) + } + xml } + + // Technique library reloaded + + case x:ReloadTechniqueLibrary => + "*" #> { val xml : NodeSeq = logDetailsService.getTechniqueLibraryReloadDetails(x.details) match { + case Full(details) => +
    + { addRestoreAction } + The Technique library was reloaded and following Techniques were updated: +
      { details.map {technique => +
    • { "%s (version %s)".format(technique.name.value, technique.version.toString)}
    • + } }
    + { reasonHtml } + { xmlParameters(event.id) } +
    + + case e:EmptyBox => errorMessage(e) + } + xml } + + // Technique modified + case x:ModifyTechnique => + "*" #> { val xml : NodeSeq = logDetailsService.getTechniqueModifyDetails(x.details) match { + case Full(modDiff) => +
    + { addRestoreAction } +

    Technique overview:

    +
      +
    • Technique ID: { modDiff.id.value }
    • +
    • Name: { modDiff.name }
    • +
    + {( + "#isEnabled *" #> mapSimpleDiff(modDiff.modIsEnabled) + ).apply(liModDetailsXML("isEnabled", "Activation status")) + } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => errorMessage(e) + } + xml } + + // Technique modified + case x:DeleteTechnique => + "*" #> { val xml : NodeSeq = logDetailsService.getTechniqueDeleteDetails(x.details) match { + case Full(techDiff) => +
    + { addRestoreAction } + { techniqueDetails(techniqueDetailsXML, techDiff.technique) } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => errorMessage(e) + } + xml } + + // archiving & restoring + + case x:ExportEventLog => + "*" #> { val xml : NodeSeq = logDetailsService.getNewArchiveDetails(x.details, x) match { + case Full(gitArchiveId) => + addRestoreAction ++ + displayExportArchiveDetails(gitArchiveId, xmlParameters(event.id)) + case e:EmptyBox => errorMessage(e) + } + xml } + + case x:Rollback => + "*" #> { val xml : NodeSeq = logDetailsService.getRollbackDetails(x.details) match { + case Full(eventLogs) => + addRestoreAction ++ + displayRollbackDetails(eventLogs,event.id.get) + case e:EmptyBox => logger.warn(e) + errorMessage(e) + } + xml } + + case x:ImportEventLog => + "*" #> { val xml : NodeSeq = logDetailsService.getRestoreArchiveDetails(x.details, x) match { + case Full(gitArchiveId) => + addRestoreAction ++ + displayImportArchiveDetails(gitArchiveId, xmlParameters(event.id)) + case e:EmptyBox => errorMessage(e) + } + xml } + + case x:ChangeRequestEventLog => + "*" #> { logDetailsService.getChangeRequestDetails(x.details) match { + case Full(diff) => + val (name,desc) = x.id match { + case None => (Text(diff.changeRequest.info.name),Text(diff.changeRequest.info.description)) + case Some(id) => + val modName = displaySimpleDiff(diff.diffName, s"name${id}", diff.changeRequest.info.name) + val modDesc = displaySimpleDiff(diff.diffDescription, s"description${id}", diff.changeRequest.info.description) + (modName,modDesc) + } +
    +

    Change request details:

    +
      +
    • Id: {diff.changeRequest.id}
    • +
    • Name: {name}
    • +
    • Description: {desc}
    • +
    +
    + case e:EmptyBox => logger.warn(e) + errorMessage(e) + } + } + + case x:WorkflowStepChanged => + "*" #> { logDetailsService.getWorkflotStepChange(x.details) match { + case Full(step) => +
    +

    Change request status modified:

    +
      +
    • Id: {step.id}
    • +
    • From status: {step.from}
    • +
    • To status: {step.to}
    • + {reasonHtml} +
    +
    + case e:EmptyBox => logger.warn(e) + errorMessage(e) + } + } + + // Global parameters + + case x:AddGlobalParameter => + "*" #> { logDetailsService.getGlobalParameterAddDetails(x.details) match { + case Full(globalParamDiff) => +
    + { addRestoreAction } + { generatedByChangeRequest } + { globalParameterDetails(globalParamDetailsXML, globalParamDiff.parameter)} + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => logger.warn(e) + errorMessage(e) + } + } + + case x:DeleteGlobalParameter => + "*" #> { logDetailsService.getGlobalParameterDeleteDetails(x.details) match { + case Full(globalParamDiff) => +
    + { addRestoreAction } + { generatedByChangeRequest } + { globalParameterDetails(globalParamDetailsXML, globalParamDiff.parameter)} + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => logger.warn(e) + errorMessage(e) + } + } + + case mod:ModifyGlobalParameter => + "*" #> { logDetailsService.getGlobalParameterModifyDetails(mod.details) match { + case Full(modDiff) => +
    + { addRestoreAction } + { generatedByChangeRequest } +

    Global Parameter overview:

    +
      +
    • Global Parameter name: { modDiff.name.value }
    • +
    + {( + "#name" #> modDiff.name.value & + "#value" #> mapSimpleDiff(modDiff.modValue) & + "#description *" #> mapSimpleDiff(modDiff.modDescription) & + "#overridable *" #> mapSimpleDiff(modDiff.modOverridable) + )(globalParamModDetailsXML) + } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => logger.warn(e) + errorMessage(e) + } + } + + case x:CreateAPIAccountEventLog => + "*" #> { logDetailsService.getApiAccountAddDetails(x.details) match { + case Full(apiAccountDiff) => +
    + { addRestoreAction } + { generatedByChangeRequest } + { apiAccountDetails(apiAccountDetailsXML, apiAccountDiff.apiAccount)} + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => logger.warn(e) + errorMessage(e) + } + } + + case x:DeleteAPIAccountEventLog => + "*" #> { logDetailsService.getApiAccountDeleteDetails(x.details) match { + case Full(apiAccountDiff) => +
    + { addRestoreAction } + { generatedByChangeRequest } + { apiAccountDetails(apiAccountDetailsXML, apiAccountDiff.apiAccount)} + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => logger.warn(e) + errorMessage(e) + } + } + + case mod:ModifyAPIAccountEventLog => + "*" #> { logDetailsService.getApiAccountModifyDetails(mod.details) match { + case Full(apiAccountDiff) => + +
    + { addRestoreAction } + { generatedByChangeRequest } +

    API account overview:

    +
      +
    • Account ID: { apiAccountDiff.id.value }
    • +
    + {( + "#name" #> mapSimpleDiff(apiAccountDiff.modName) & + "#token" #> mapSimpleDiff(apiAccountDiff.modToken) & + "#description *" #> mapSimpleDiff(apiAccountDiff.modDescription) & + "#isEnabled *" #> mapSimpleDiff(apiAccountDiff.modIsEnabled) & + "#tokenGenerationDate *" #> mapSimpleDiff(apiAccountDiff.modTokenGenerationDate) & + "#expirationDate *" #> mapSimpleDiffT[Option[DateTime]](apiAccountDiff.modExpirationDate, _.fold("")(_.toString(ISODateTimeFormat.dateTime())) + ) & + "#accountKind *" #> mapSimpleDiff(apiAccountDiff.modAccountKind) & + //make list of ACL unsderstandable + "#acls *" #> mapSimpleDiff(apiAccountDiff.modAccountAcl.map(o => { + val f = (l: List[ApiAclElement]) => l.sortBy(_.path.value).map(x => s"[${x.actions.map(_.name.toUpperCase()).mkString(",")}] ${x.path.value}").mkString(" | ") + SimpleDiff(f(o.oldValue), f(o.newValue)) + })) + )(apiAccountModDetailsXML) + } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => logger.warn(e) + errorMessage(e) + } + } + + case mod:ModifyGlobalProperty => + "*" #> { logDetailsService.getModifyGlobalPropertyDetails(mod.details) match { + case Full((oldProp,newProp)) => +
    +

    Global property overview:

    +
      +
    • Name: { mod.propertyName }
    • +
    • Value: + { val diff = Some(SimpleDiff(oldProp.value,newProp.value)) + displaySimpleDiff(diff,"value",newProp.value) }
    • +
    + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => logger.warn(e) + errorMessage(e) + } + } + + // Node modifiction + case mod:ModifyNode => + "*" #> { logDetailsService.getModifyNodeDetails(mod.details) match { + case Full(modDiff) => + logger.info(modDiff.modAgentRun) +
    + { addRestoreAction } + { generatedByChangeRequest } +

    Node '{modDiff.id.value}' modified:

    +
      +
    • Node ID:{modDiff.id.value}
    • +
    + { + mapComplexDiff(modDiff.modAgentRun){ (optAr:Option[AgentRunInterval]) => + optAr match { + case None => No value + case Some(ar) => agentRunDetails(ar) + } + } + } + { + mapComplexDiff(modDiff.modHeartbeat){ (optHb:Option[HeartbeatConfiguration]) => + optHb match { + case None => No value + case Some(hb) => heartbeatDetails(hb) + } + } + } + { + mapComplexDiff(modDiff.modProperties){ (props:Seq[NodeProperty]) => + nodePropertiesDetails(props) + } + } + { + mapComplexDiff(modDiff.modPolicyMode){ (optMode:Option[PolicyMode]) => + optMode match { + case None => Use global policy mode + case Some(mode) => {mode.name} + } + } + } + { reasonHtml } + { xmlParameters(event.id) } +
    + case e:EmptyBox => logger.warn(e) + errorMessage(e) + } + } + + // other case: do not display details at all + case _ => "*" #> "" + + })(event.details)} + } + + private[this] def agentRunDetails(ar: AgentRunInterval): NodeSeq = { + ( + "#override" #> ar.overrides.map(_.toString()).getOrElse("false") + & "#interval" #> ar.interval + & "#startMinute" #> ar.startMinute + & "#startHour" #> ar.startHour + & "#splaytime" #> ar.splaytime + ).apply( +
      +
    • Override global value:
    • +
    • Period:
    • +
    • Start at minute:
    • +
    • Start at hour:
    • +
    • Splay time:
    • +
    + ) + } + + private[this] def heartbeatDetails(hb: HeartbeatConfiguration): NodeSeq = { + ( + "#override" #> hb.overrides + & "#interval" #> hb.heartbeatPeriod + ).apply( +
      +
    • Override global value:
    • +
    • Period:
    • +
    + ) + } + + private[this] def nodePropertiesDetails(props: Seq[NodeProperty]): NodeSeq = { + ( + "#kv *" #> props.map { p => s"${p.name}: ${p.value}" } + ).apply( +
      +
    • +
    + ) + } + + private[this] def displaySimpleDiff[T] ( + diff : Option[SimpleDiff[T]] + , name : String + , default : String + ) = diff.map(value => displayFormDiff(value, name)).getOrElse(Text(default)) + private[this] def displayFormDiff[T](diff: SimpleDiff[T], name:String)(implicit fun: T => String = (t:T) => t.toString) = { +
    {fun(diff.oldValue)}
    +
    {fun(diff.newValue)}
    +
      ++
    +      Script(
    +        OnLoad(
    +          JsRaw(
    +            s"""
    +            var before = "before${name}";
    +            var after  = "after${name}";
    +            var result = "result${name}";
    +            makeDiff(before,after,result);"""
    +          )
    +        )
    +      )
    +  }
    +  private[this] def displaydirectiveInnerFormDiff(diff: SimpleDiff[SectionVal], eventId: Option[Int]): NodeSeq = {
    +    eventId match {
    +      case None => NodeSeq.Empty
    +      case Some(id) =>
    +        (
    +          
    {xmlPretty.format(SectionVal.toXml(diff.oldValue))}
    +
    {xmlPretty.format(SectionVal.toXml(diff.newValue))}
    +
    
    +          ) ++ Script(OnLoad(JsRaw(s"""
    +            var before = "before${id}";
    +            var after  = "after${id}";
    +            var result = "result${id}";
    +            makeDiff(before,after,result);
    +            """)))
    +    }
    +  }
    +
    +  private[this] def displayExportArchiveDetails(gitArchiveId: GitArchiveId, rawData: NodeSeq) =
    +    
    +

    Details of the new archive:

    +
      +
    • Git path of the archive: {gitArchiveId.path.value}
    • +
    • Commit ID (hash): {gitArchiveId.commit.value}
    • +
    • Commiter name: {gitArchiveId.commiter.getName}
    • +
    • Commiter email: {gitArchiveId.commiter.getEmailAddress}
    • +
    + {rawData} +
    + + private[this] def displayImportArchiveDetails(gitCommitId: GitCommitId, rawData: NodeSeq) = +
    +

    Details of the restored archive:

    +
      +
    • Commit ID (hash): {gitCommitId.value}
    • +
    + {rawData} +
    + + private[this] def nodeGroupDetails(nodes: Set[NodeId]): NodeSeq = { + val res = nodes.toSeq match { + case Seq() => NodeSeq.Empty + case t => + nodes + .toSeq + .map { id => linkUtil.createNodeLink(id) } + .reduceLeft[NodeSeq]((a,b) => a ++ ++ b) + } + ( + ".groupSeparator" #> ", " + ).apply(res) + + } + + private[this] def ruleDetails(xml:NodeSeq, rule:Rule, groupLib: FullNodeGroupCategory, rootRuleCategory: RuleCategory) = { + val diffDisplayer = new DiffDisplayer(linkUtil) + ( + "#ruleID" #> rule.id.value & + "#ruleName" #> rule.name & + "#category" #> diffDisplayer.displayRuleCategory(rootRuleCategory, rule.categoryId, None) & + "#target" #> diffDisplayer.displayRuleTargets(rule.targets.toSeq, rule.targets.toSeq, groupLib) & + "#policy" #> diffDisplayer.displayDirectiveChangeList(rule.directiveIds.toSeq, rule.directiveIds.toSeq) & + "#isEnabled" #> rule.isEnabled & + "#isSystem" #> rule.isSystem & + "#shortDescription" #> rule.shortDescription & + "#longDescription" #> rule.longDescription + ) (xml) + } + + private[this] def directiveDetails(xml:NodeSeq, ptName: TechniqueName, directive:Directive, sectionVal:SectionVal) = ( + "#directiveID" #> directive.id.value & + "#directiveName" #> directive.name & + "#ptVersion" #> directive.techniqueVersion.toString & + "#ptName" #> ptName.value & + "#ptVersion" #> directive.techniqueVersion.toString & + "#ptName" #> ptName.value & + "#priority" #> directive.priority & + "#isEnabled" #> directive.isEnabled & + "#isSystem" #> directive.isSystem & + "#shortDescription" #> directive.shortDescription & + "#longDescription" #> directive.longDescription + )(xml) + + private[this] def groupDetails(xml:NodeSeq, group: NodeGroup) = ( + "#groupID" #> group.id.value & + "#groupName" #> group.name & + "#shortDescription" #> group.description & + "#shortDescription" #> group.description & + "#query" #> (group.query match { + case None => Text("None") + case Some(q) => Text(q.toJSONString) + } ) & + "#isDynamic" #> group.isDynamic & + "#nodes" #>( + { + val l = group.serverList.toList + l match { + case Nil => Text("None") + case _ => l + .map(id => {id.value}) + .reduceLeft[NodeSeq]((a,b) => a ++ ++ b) + } + } + ) & + "#isEnabled" #> group.isEnabled & + "#isSystem" #> group.isSystem + )(xml) + + private[this] def techniqueDetails(xml: NodeSeq, technique: ActiveTechnique) = ( + "#techniqueID" #> technique.id.value & + "#techniqueName" #> technique.techniqueName.value & + "#isEnabled" #> technique.isEnabled & + "#isSystem" #> technique.isSystem + )(xml) + + private[this] def globalParameterDetails(xml: NodeSeq, globalParameter: GlobalParameter) = ( + "#name" #> globalParameter.name.value & + "#value" #> globalParameter.value & + "#description" #> globalParameter.description & + "#overridable" #> globalParameter.overridable + )(xml) + + private[this] def apiAccountDetails(xml: NodeSeq, apiAccount: ApiAccount) = ( + "#id" #> apiAccount.id.value & + "#name" #> apiAccount.name.value & + "#token" #> apiAccount.token.value & + "#description" #> apiAccount.description & + "#isEnabled" #> apiAccount.isEnabled & + "#creationDate" #> DateFormaterService.getFormatedDate(apiAccount.creationDate) & + "#tokenGenerationDate" #> DateFormaterService.getFormatedDate(apiAccount.tokenGenerationDate) + )(xml) + + + private[this] def mapSimpleDiffT[T](opt:Option[SimpleDiff[T]], t: T => String) = opt.map { diff => + ".diffOldValue *" #> t(diff.oldValue) & + ".diffNewValue *" #> t(diff.newValue) + } + + private[this] def mapSimpleDiff[T](opt:Option[SimpleDiff[T]]) = mapSimpleDiffT(opt, (x:T) => x.toString) + + private[this] def mapSimpleDiff[T](opt:Option[SimpleDiff[T]], id: DirectiveId) = opt.map { diff => + ".diffOldValue *" #> diff.oldValue.toString & + ".diffNewValue *" #> diff.newValue.toString & + "#directiveID" #> id.value + } + + private[this] def mapComplexDiff[T](opt:Option[SimpleDiff[T]])(display: T => NodeSeq) = { + opt match { + case None => NodeSeq.Empty + case Some(diff) => + ( + ".diffOldValue *" #> display(diff.oldValue) & + ".diffNewValue *" #> display(diff.newValue) + ).apply( +
      +
    • Old value: old value
    • +
    • New value: new value
    • +
    + ) + } + } + + private[this] def nodeDetails(details:InventoryLogDetails) = ( + "#nodeID" #> details.nodeId.value & + "#nodeName" #> details.hostname & + "#os" #> details.fullOsName & + "#version" #> DateFormaterService.getFormatedDate(details.inventoryVersion) + )( +
      +
    • Node ID:
    • +
    • Name:
    • +
    • Operating System:
    • +
    • Date inventory last received:
    • +
    + ) + private[this] val crDetailsXML = +
    +

    Rule overview:

    +
      +
    • ID: 
    • +
    • Name: 
    • +
    • Category: 
    • +
    • Description: 
    • +
    • Target: 
    • +
    • Directives: 
    • +
    • Enabled: 
    • +
    • System: 
    • +
    • Details: 
    • +
    +
    + + private[this] val piDetailsXML = +
    +

    Directive overview:

    +
      +
    • ID: 
    • +
    • Name: 
    • +
    • Description: 
    • +
    • Technique name: 
    • +
    • Technique version: 
    • +
    • Priority: 
    • +
    • Enabled: 
    • +
    • System: 
    • +
    • Details: 
    • +
    +
    + + private[this] val groupDetailsXML = +
    +

    Group overview:

    +
      +
    • ID:
    • +
    • Name:
    • +
    • Description:
    • +
    • Enabled:
    • +
    • Dynamic:
    • +
    • System:
    • +
    • Query:
    • +
    • Node list:
    • +
    +
    + + private[this] val techniqueDetailsXML = +
    +

    Technique overview:

    +
      +
    • ID: 
    • +
    • Name: 
    • +
    • Enabled: 
    • +
    • System: 
    • +
    +
    + + private[this] val globalParamDetailsXML = +
    +

    Global Parameter overview:

    +
      +
    • Name: 
    • +
    • Value: 
    • +
    • Description: 
    • +
    • Overridable: 
    • +
    +
    + + private[this] val apiAccountDetailsXML = +
    +

    API account overview:

    +
      +
    • Rudder ID:
    • +
    • Name: 
    • +
    • Token: 
    • +
    • Description: 
    • +
    • Enabled: 
    • +
    • Creation date: 
    • +
    • Token Generation date: 
    • +
    • Token Expiration date: 
    • +
    • Account Kind: 
    • +
    • ACLs: 
    • +
    +
    + + private[this] val apiAccountModDetailsXML = + + {liModDetailsXML("name", "Name")} + {liModDetailsXML("token", "Token")} + {liModDetailsXML("description", "Description")} + {liModDetailsXML("isEnabled", "Enabled")} + {liModDetailsXML("tokenGenerationDate", "Token Generation Date")} + {liModDetailsXML("expirationDate", "Token Expiration Date")} + {liModDetailsXML("accountKind", "Account Kind")} + {liModDetailsXML("acls", "ACL list")} + + + + private[this] def liModDetailsXML(id:String, name:String) = ( +
    + {name} changed: +
      +
    • Old value: old value
    • +
    • New value: new value
    • +
    +
    + ) + + private[this] def liModDirectiveDetailsXML(id:String, name:String) = ( +
    + {name} changed: +
      +
    • Differences:
    • +
    +
    + ) + + private[this] val groupModDetailsXML = + + {liModDetailsXML("name", "Name")} + {liModDetailsXML("shortDescription", "Description")} + {liModDetailsXML("nodes", "Node list")} + {liModDetailsXML("query", "Query")} + {liModDetailsXML("isDynamic", "Dynamic group")} + {liModDetailsXML("isEnabled", "Activation status")} + {liModDetailsXML("isSystem", "System")} + + + private[this] val crModDetailsXML = + + {liModDetailsXML("name", "Name")} + {liModDetailsXML("category", "Category")} + {liModDetailsXML("shortDescription", "Description")} + {liModDetailsXML("longDescription", "Details")} + {liModDetailsXML("target", "Target")} + {liModDetailsXML("policies", "Directives")} + {liModDetailsXML("isEnabled", "Activation status")} + {liModDetailsXML("isSystem", "System")} + + + private[this] val piModDirectiveDetailsXML = + + {liModDetailsXML("name", "Name")} + {liModDetailsXML("shortDescription", "Description")} + {liModDetailsXML("longDescription", "Details")} + {liModDetailsXML("ptVersion", "Target")} + {liModDirectiveDetailsXML("parameters", "Policy parameters")} + {liModDetailsXML("priority", "Priority")} + {liModDetailsXML("isEnabled", "Activation status")} + {liModDetailsXML("isSystem", "System")} + + + private[this] val globalParamModDetailsXML = + + {liModDetailsXML("name", "Name")} + {liModDetailsXML("value", "Value")} + {liModDetailsXML("description", "Description")} + {liModDetailsXML("overridable", "Overridable")} + + + + private[this] def displayRollbackDetails(rollbackInfo:RollbackInfo,id:Int) = { + val rollbackedEvents = rollbackInfo.rollbacked +
    +
    +
    +

    Details of the rollback:

    +
    + A rollback to {rollbackInfo.rollbackType} event + { SHtml.a(() => + SetHtml("currentId%s".format(id),Text(rollbackInfo.target.id.toString)) & + JsRaw("""$('#%1$s').dataTable().fnFilter("%2$s|%3$s",0,true,false); + $("#cancel%3$s").show(); + scrollToElement('%2$s', ".rudder_col"); + if($('#%2$s').prop('open') != "opened") + $('#%2$s').click();""".format(gridName,rollbackInfo.target.id,id)) + , Text(rollbackInfo.target.id.toString) + ) + } has been completed. + +

    + Events that were rollbacked can be consulted in the table below. +
    + Those events are no longer applied by the configuration policy. + +
    + + + + + + + + + + + {rollbackedEvents.map{ ev => + + + + + + + } } + +
    IDDateActorEvent type
    + {SHtml.a(() => + SetHtml("currentId%s".format(id),Text(ev.id.toString)) & + JsRaw("""$('#%1$s').dataTable().fnFilter("%2$s|%3$s",0,true,false); + $("#cancel%3$s").show(); + scrollToElement('%2$s', ".rudder_col"); + if($('#%2$s').prop('open') != "opened") + $('#%2$s').click();""".format(gridName,ev.id,id)) + , Text(ev.id.toString) + ) + } + {ev.date} {ev.author} {S.?("rudder.log.eventType.names." + ev.eventType)}
    + +
    + +
    +
    +
    ++ Script(JsRaw(s""" + $$('#rollbackTable${id}').dataTable({ + "asStripeClasses": [ 'color1', 'color2' ], + "bAutoWidth": false, + "bFilter" :true, + "bPaginate" :true, + "bLengthChange": true, + "bStateSave": true, + "fnStateSave": function (oSettings, oData) { + localStorage.setItem( 'DataTables_rollbackTable${id}', JSON.stringify(oData) ); + }, + "fnStateLoad": function (oSettings) { + return JSON.parse( localStorage.getItem('DataTables_rollbackTable${id}') ); + }, + "sPaginationType": "full_numbers", + "bJQueryUI": true, + "oLanguage": { + "sSearch": "" + }, + "aaSorting":[], + "aoColumns": [ + { "sWidth": "100px" } + , { "sWidth": "100px" } + , { "sWidth": "100px" } + , { "sWidth": "100px" } + ], + "sDom": '<"dataTables_wrapper_top"f>rt<"dataTables_wrapper_bottom"lip>' + }); + """)) + } + + private[this] def authorizedNetworksXML() = ( +
    + Networks authorized on policy server were updated: + + + + + + + + +
    from:to:
    old valuenew value
    +
    + ) + + trait RollBackAction { + def name : String + def op : String + def action : (EventLog,PersonIdent,Seq[EventLog],EventLog) => Box[GitCommitId] + def selectRollbackedEventsRequest(id:Int) = s" id ${op} ${id} and modificationid IS NOT NULL" + } + + case object RollbackTo extends RollBackAction{ + val name = "after" + val op = ">" + def action = modificationService.restoreToEventLog _ + } + + case object RollbackBefore extends RollBackAction{ + val name = "before" + val op = ">=" + def action = modificationService.restoreBeforeEventLog _ + } + +} diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/JsTableData.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/JsTableData.scala new file mode 100644 index 00000000000..4baa7aeaedf --- /dev/null +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/JsTableData.scala @@ -0,0 +1,71 @@ +/* +************************************************************************************* +* Copyright 2014 Normation SAS +************************************************************************************* +* +* This file is part of Rudder. +* +* Rudder is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* In accordance with the terms of section 7 (7. Additional Terms.) of +* the GNU General Public License version 3, the copyright holders add +* the following Additional permissions: +* Notwithstanding to the terms of section 5 (5. Conveying Modified Source +* Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General +* Public License version 3, when you create a Related Module, this +* Related Module is not considered as a part of the work and may be +* distributed under the license agreement of your choice. +* A "Related Module" means a set of sources files including their +* documentation that, without modification of the Source Code, enables +* supplementary functions or services in addition to those offered by +* the Software. +* +* Rudder is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with Rudder. If not, see . + +* +************************************************************************************* +*/ + +package com.normation.rudder.web.services + +import net.liftweb.http.js.JsExp +import net.liftweb.http.js.JsObj +import net.liftweb.http.js.JE.JsArray +import com.normation.rudder.domain.reports.ComplianceLevel +import net.liftweb.common.Loggable + + +/* + * That class represent a Line in a DataTable. + */ +trait JsTableLine extends Loggable { + + def json : JsObj + + // this is needed because DataTable doesn't escape HTML element when using table.rows.add + def escapeHTML(s: String): JsExp = JsExp.strToJsExp(xml.Utility.escape(s)) + + import com.normation.rudder.domain.reports.ComplianceLevelSerialisation._ + def jsCompliance(compliance: ComplianceLevel) = compliance.toJsArray() + +} + +/* + * That class a set of Data to use in datatable + * It should be used as data parameter when creating datatable in javascript + */ +case class JsTableData[T <: JsTableLine] ( lines : List[T] ) { + + def json = JsArray(lines.map(_.json)) + +} + diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/RudderProperties.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/RudderProperties.scala new file mode 100644 index 00000000000..083bad1faf6 --- /dev/null +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/RudderProperties.scala @@ -0,0 +1,51 @@ +package com.normation.rudder.web.services + +import java.io.File + +import com.normation.rudder.domain.logger.ApplicationLogger +import com.typesafe.config.{Config, ConfigFactory} +import org.springframework.core.io.ClassPathResource +/** + * Define a resource for configuration. + * For now, config properties can only be loaded from either + * a file in the classpath, or a file in the file system. + */ +sealed trait ConfigResource +final case class ClassPathResource(name: String) extends ConfigResource +final case class FileSystemResource(file: File) extends ConfigResource + +object RudderProperties { + + val JVM_CONFIG_FILE_KEY = "rudder.configFile" + val DEFAULT_CONFIG_FILE_NAME = "configuration.properties" + + /** + * Where to go to look for properties + */ + val configResource = System.getProperty(JVM_CONFIG_FILE_KEY) match { + case null | "" => //use default location in classpath + ApplicationLogger.info("JVM property -D%s is not defined, use configuration file in classpath".format(JVM_CONFIG_FILE_KEY)) + ClassPathResource(DEFAULT_CONFIG_FILE_NAME) + case x => //so, it should be a full path, check it + val config = new File(x) + if(config.exists && config.canRead) { + ApplicationLogger.info("Use configuration file defined by JVM property -D%s : %s".format(JVM_CONFIG_FILE_KEY, config.getPath)) + FileSystemResource(config) + } else { + ApplicationLogger.error("Can not find configuration file specified by JVM property %s: %s ; abort".format(JVM_CONFIG_FILE_KEY, config.getPath)) + throw new javax.servlet.UnavailableException("Configuration file not found: %s".format(config.getPath)) + } + } + + // some value used as defaults for migration + val migrationConfig = + s"""rudder.batch.reportscleaner.compliancelevels.delete.TTL=15 + """ + + val config : Config = { + (configResource match { + case ClassPathResource(name) => ConfigFactory.load(name) + case FileSystemResource(file) => ConfigFactory.load(ConfigFactory.parseFile(file)) + }).withFallback(ConfigFactory.parseString(migrationConfig)) + } +} \ No newline at end of file diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/RudderUserDetails.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/RudderUserDetails.scala new file mode 100644 index 00000000000..7af61571633 --- /dev/null +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/RudderUserDetails.scala @@ -0,0 +1,64 @@ +package com.normation.rudder.web.services + +import java.util.Collection +import com.normation.rudder.{AuthorizationType, Rights, Role, RudderAccount} +import com.normation.rudder.api.ApiAuthorization +import com.normation.utils.HashcodeCaching +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import scala.collection.JavaConverters.asJavaCollectionConverter + +/** + * We don't use at all Spring Authority to implements + * our authorizations. + * That because we want something more typed than String for + * authority, and as a bonus, that allows to be able to switch + * from Spring more easily + * + * So we have one Authority type known by Spring Security for + * authenticated user: ROLE_USER + * And one other for API accounts: ROLE_REMOTE + */ +sealed trait RudderAuthType { + def grantedAuthorities: Collection[GrantedAuthority] +} + + +final object RudderAuthType { + // build a GrantedAuthority from the string + private def buildAuthority(s: String): Collection[GrantedAuthority] = { + Seq(new GrantedAuthority { override def getAuthority: String = s }).asJavaCollection + } + + final case object User extends RudderAuthType { + override val grantedAuthorities = buildAuthority("ROLE_USER") + } + final case object Api extends RudderAuthType { + override val grantedAuthorities = buildAuthority("ROLE_REMOTE") + + val apiRudderRights = new Rights(AuthorizationType.NoRights) + val apiRudderRole: Set[Role] = Set(Role.NoRights) + } +} + +/** + * Our simple model for for user authentication and authorizations. + * Note that authorizations are not managed by spring, but by the + * 'authz' token of RudderUserDetail. + */ +final case class RudderUserDetail( + account : RudderAccount + , roles : Set[Role] + , apiAuthz: ApiAuthorization +) extends UserDetails with HashcodeCaching { + // merge roles rights + val authz = new Rights(roles.flatMap(_.rights.authorizationTypes).toSeq:_*) + override val (getUsername, getPassword, getAuthorities) = account match { + case RudderAccount.User(login, password) => (login , password , RudderAuthType.User.grantedAuthorities) + case RudderAccount.Api(api) => (api.name.value, api.token.value, RudderAuthType.Api.grantedAuthorities) + } + override val isAccountNonExpired = true + override val isAccountNonLocked = true + override val isCredentialsNonExpired = true + override val isEnabled = true +} \ No newline at end of file diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/Boot.scala b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/Boot.scala index 669d74ede5f..1e2660b2e43 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/Boot.scala @@ -282,6 +282,7 @@ class Boot extends Loggable { LiftRules.statelessDispatch.append(RudderConfig.restQuicksearch) LiftRules.statelessDispatch.append(RudderConfig.restCompletion) LiftRules.statelessDispatch.append(RudderConfig.sharedFileApi) + LiftRules.statelessDispatch.append(RudderConfig.eventLogApi) // REST API (all public/internal API) // we need to add "info" API here to have all used API (even plugins) diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala index b92eeb2909a..01112241f17 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala @@ -806,8 +806,10 @@ object RudderConfig extends Loggable { api } + // Internal APIs val sharedFileApi = new SharedFilesAPI(restExtractorService,RUDDER_DIR_SHARED_FILES_FOLDER) + val eventLogApi= new EventLogAPI(eventLogRepository, restExtractorService, eventListDisplayerImpl, doobie) lazy val asyncWorkflowInfo = new AsyncWorkflowInfo lazy val configService: ReadConfigService with UpdateConfigService = { diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/EventListDisplayer.scala b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/EventListDisplayer.scala index fb67f5e6ce2..b2a0ddbc23e 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/EventListDisplayer.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/com/normation/rudder/web/services/EventListDisplayer.scala @@ -80,6 +80,7 @@ import com.normation.rudder.rule.category.RoRuleCategoryRepository import org.joda.time.format.DateTimeFormat import com.normation.rudder.web.model.LinkUtil import org.joda.time.format.ISODateTimeFormat +import scala.concurrent.Future import com.normation.box._ /** @@ -204,6 +205,7 @@ class EventListDisplayer( ) ) } + val json = { JsObj( "id" -> (event.id.map(_.toString).getOrElse("Unknown"): String) @@ -367,8 +369,10 @@ class EventListDisplayer( val generatedByChangeRequest = changeRequestId match { case None => NodeSeq.Empty - case Some(id) =>

    This change was introduced by change request {SHtml.a(() => S.redirectTo(linkUtil.changeRequestLink(id)),Text(s"#${id}"))}

    - } + case Some(id) => +// val htmlCR =

    This change was introduced by change request {SHtml.a(() => S.redirectTo(linkUtil.changeRequestLink(id)),Text(s"#${id}"))}

    + NodeSeq.Empty + } def xmlParameters(eventId: Option[Int]) = { eventId match { case None => NodeSeq.Empty @@ -433,52 +437,55 @@ class EventListDisplayer( } def addRestoreAction() = - personIdentService.getPersonIdentOrDefault(CurrentUser.actor.name).toBox match { + personIdentService.getPersonIdentOrDefault(CurrentUserService.actor.name).toBox match { case Full(commiter) => var rollbackAction : RollBackAction = null - if (event.canRollBack) - modificationService.getCommitsfromEventLog(event).map{ commit => -
    -
    Rollback -
    - Restore configuration policy to -
      { - SHtml.radio( - Seq("before","after") - , Full("before") - , {value:String => - rollbackAction = value match { - case "after" => RollbackTo - case "before" => RollbackBefore - } - } - , ("class", "radio") - ).flatMap(e => -
    • - -
    • - ) - } -
    - this change - - { SHtml.ajaxSubmit( - "Restore" - , () => showConfirmationDialog(rollbackAction,commiter) - , ("style","vertical-align:50%;") - , ("class","btn btn-default btn-sm") - ) - } - -
    - -
    -
    - }.getOrElse(NodeSeq.Empty) - else + if (event.canRollBack) { +// val rollDisplay = modificationService.getCommitsfromEventLog(event).map { commit => +//
    +//
    +// Rollback +//
    +// Restore configuration policy to +//
      +// {SHtml.radio( +// Seq("before", "after") +// , Full("before") +// , { value: String => +// rollbackAction = value match { +// case "after" => RollbackTo +// case "before" => RollbackBefore +// } +// } +// , ("class", "radio") +// ).flatMap(e => +//
    • +// +//
    • +// )} +//
    +// this change +// +// {SHtml.ajaxSubmit( +// "Restore" +// , () => showConfirmationDialog(rollbackAction, commiter) +// , ("style", "vertical-align:50%;") +// , ("class", "btn btn-default btn-sm") +// )} +// +//
    +// +//
    +//
    +// }.getOrElse(NodeSeq.Empty) + NodeSeq.Empty + } + else NodeSeq.Empty case eb:EmptyBox => logger.error("this should not happen, as person identifier service always return a working value") NodeSeq.Empty @@ -784,7 +791,7 @@ class EventListDisplayer( { reasonHtml } { xmlParameters(event.id) } - case Full(_) => errorMessage(Failure("Inconsistant policy update status")) + case Full(_) => errorMessage(Failure("Unconsistant policy update status")) case e:EmptyBox => errorMessage(e) } xml } @@ -804,7 +811,7 @@ class EventListDisplayer( { reasonHtml } { xmlParameters(event.id) } - case Full(_) => errorMessage(Failure("Inconsistant policy update status")) + case Full(_) => errorMessage(Failure("Unconsistant policy update status")) case e:EmptyBox => errorMessage(e) } xml } @@ -909,8 +916,9 @@ class EventListDisplayer( case x:Rollback => "*" #> { val xml : NodeSeq = logDetailsService.getRollbackDetails(x.details) match { case Full(eventLogs) => - addRestoreAction ++ - displayRollbackDetails(eventLogs,event.id.get) +// addRestoreAction ++ +// displayRollbackDetails(eventLogs,event.id.get) + NodeSeq.Empty // To prevent a stateful error case e:EmptyBox => logger.warn(e) errorMessage(e) } @@ -1147,12 +1155,6 @@ class EventListDisplayer( } } } - { - mapComplexDiff(modDiff.modKeyValue) { keyValue => {keyValue.key} } - } - { - mapComplexDiff(modDiff.modKeyStatus) { keyStatus => {keyStatus.value} } - } { reasonHtml } { xmlParameters(event.id) } diff --git a/webapp/sources/rudder/rudder-web/src/main/webapp/javascript/rudder/rudder-datatable.js b/webapp/sources/rudder/rudder-web/src/main/webapp/javascript/rudder/rudder-datatable.js index 4f701571f62..f74f000758f 100644 --- a/webapp/sources/rudder/rudder-web/src/main/webapp/javascript/rudder/rudder-datatable.js +++ b/webapp/sources/rudder/rudder-web/src/main/webapp/javascript/rudder/rudder-datatable.js @@ -1351,6 +1351,11 @@ function createEventLogTable(gridId, data, contextPath, refresh, pickEventLogsIn var params = { "bFilter" : true + , "serverSide" : true + , "ajax" : { + "type" : "GET" + , "url" : contextPath + "/secure/api/eventlog" + } , "bPaginate" : true , "bLengthChange": true , "sPaginationType": "full_numbers" @@ -1386,7 +1391,12 @@ function createEventLogTable(gridId, data, contextPath, refresh, pickEventLogsIn var detailsTd = $("."+detailsId); detailsTd.attr("id",detailsId); // Set data in the open row with the details function from data - fnData.details(detailsId); +// fnData.details(detailsId); + $.getJSON(contextPath + '/secure/api/eventlog/' + fnData.id + "/details", function(data) { + console.log(data["data"]["content"]) + var html = $.parseHTML( data["data"]["content"] ); + $("td#"+detailsId).append( html ); + }); // Set final css var color = 'color1'; if(tableRow.hasClass('color2'))