diff --git a/change-validation/src/main/resources/change-validation.conf b/change-validation/src/main/resources/change-validation.conf index 81b1795b1..3ae2d0e6f 100644 --- a/change-validation/src/main/resources/change-validation.conf +++ b/change-validation/src/main/resources/change-validation.conf @@ -1,3 +1,10 @@ + +# Properties to decide if the change validation plugins use supervised groups (default) or +# if all changes need to have change request. This feature does not override validated user one. +# Values: supervisedGroups , anyChanges +change.enable=supervisedGroups + + # Email server configuration smtp.hostServer="" smtp.port=587 diff --git a/change-validation/src/main/scala/bootstrap/rudder/plugin/ChangeValidationConf.scala b/change-validation/src/main/scala/bootstrap/rudder/plugin/ChangeValidationConf.scala index 494a26226..83b1f5836 100644 --- a/change-validation/src/main/scala/bootstrap/rudder/plugin/ChangeValidationConf.scala +++ b/change-validation/src/main/scala/bootstrap/rudder/plugin/ChangeValidationConf.scala @@ -67,6 +67,7 @@ import com.normation.plugins.changevalidation.WoValidatedUserJdbcRepository import com.normation.plugins.changevalidation.WoValidatedUserRepository import com.normation.plugins.changevalidation.WoWorkflowJdbcRepository import com.normation.plugins.changevalidation.api.{ChangeRequestApi, ChangeRequestApiImpl, SupervisedTargetsApi, SupervisedTargetsApiImpl, ValidatedUserApiImpl} +import com.normation.plugins.changevalidation.CheckValidationKind import com.normation.rudder.AuthorizationType import com.normation.rudder.AuthorizationType.Deployer import com.normation.rudder.AuthorizationType.Validator @@ -86,14 +87,16 @@ import com.normation.rudder.services.workflows.NodeGroupChangeRequest import com.normation.rudder.services.workflows.RuleChangeRequest import com.normation.rudder.services.workflows.WorkflowLevelService import com.normation.rudder.services.workflows.WorkflowService + import net.liftweb.common.Box import net.liftweb.common.Full + import com.normation.box._ import com.normation.plugins.changevalidation.EmailNotificationService import com.normation.plugins.changevalidation.NotificationService import com.normation.plugins.changevalidation.RoValidatedUserRepository + import net.liftweb.common.EmptyBox -import net.liftweb.common.Failure /* * The validation workflow level @@ -207,10 +210,12 @@ class ChangeValidationWorkflowLevelService( */ object ChangeValidationConf extends RudderPluginModule { + val configFilePath = "/opt/rudder/etc/plugins/change-validation.conf" + lazy val notificationService = new NotificationService( new EmailNotificationService() , RudderConfig.linkUtil - , "/opt/rudder/etc/plugins/change-validation.conf" + , configFilePath ) // by build convention, we have only one of that on the classpath lazy val pluginStatusService = new CheckRudderPluginEnableImpl(RudderConfig.nodeInfoService) @@ -256,19 +261,19 @@ object ChangeValidationConf extends RudderPluginModule { } - // other service instanciation / initialization + // other service instantiation / initialization RudderConfig.workflowLevelService.overrideLevel( new ChangeValidationWorkflowLevelService( pluginStatusService , RudderConfig.workflowLevelService.defaultWorkflowService , validationWorkflowService - , Seq( new NodeGroupValidationNeeded( + , Seq( new CheckValidationKind(configFilePath, new NodeGroupValidationNeeded( supervisedTargetRepo.load _ , roChangeRequestRepository , RudderConfig.roRuleRepository , RudderConfig.roNodeGroupRepository , RudderConfig.nodeInfoService - ) + )) ) , () => RudderConfig.configService.rudder_workflow_enabled().toBox , roValidatedUserRepository diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/NotificationService.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/NotificationService.scala index 48eaff721..77ad62538 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/NotificationService.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/NotificationService.scala @@ -9,6 +9,7 @@ import bootstrap.liftweb.FileSystemResource import com.github.mustachejava.DefaultMustacheFactory import com.github.mustachejava.MustacheFactory import com.normation.NamedZioLogger + import com.normation.errors._ import com.normation.plugins.changevalidation.TwoValidationStepsWorkflowServiceImpl.Cancelled import com.normation.plugins.changevalidation.TwoValidationStepsWorkflowServiceImpl.Deployed @@ -17,18 +18,52 @@ import com.normation.plugins.changevalidation.TwoValidationStepsWorkflowServiceI import com.normation.rudder.domain.workflows.ChangeRequest import com.normation.rudder.domain.workflows.WorkflowNode import com.normation.rudder.web.model.LinkUtil + import com.typesafe.config.Config import com.typesafe.config.ConfigFactory -import zio.ZIO +import zio.ZIO import jakarta.mail.Session import jakarta.mail._ import jakarta.mail.internet.InternetAddress import jakarta.mail.internet.MimeMessage -import zio.syntax._ +import zio.syntax._ import scala.jdk.CollectionConverters._ +sealed trait ChangeEnableFor { + def name: String +} + +object ReadConfigFile { + protected[changevalidation] def getConfig(path: String): IOResult[Config] = { + val file = new File(path) + IOResult.effectM { + for { + configResource <- if (file.exists && file.canRead) { + FileSystemResource(file).succeed + } else { + Inconsistency(s"Configuration file not found: ${file.getPath}").fail + } + } yield { + ConfigFactory.load(ConfigFactory.parseFile(configResource.file)) + } + } + } +} + +object ChangeEnableFor { + + object SupervisedGroups extends ChangeEnableFor { val name = "supervisedGroups" } + object AnyChanges extends ChangeEnableFor { val name = "anyChanges" } + + def all = ca.mrvisser.sealerate.values[ChangeEnableFor] + + def parse(s: String): Option[ChangeEnableFor] = { + all.collectFirst { case x if(x.name == s) => x} + } +} + final case class Email(value: String) final case class Username(value: String) @@ -109,12 +144,16 @@ class EmailNotificationService { } } + + class NotificationService( - emailService : EmailNotificationService - , linkUtil : LinkUtil - , configMailPath: String + emailService: EmailNotificationService + , linkUtil : LinkUtil + , configFile : String ) { + val logger = NamedZioLogger("plugin.change-validation") + // we want all our string to be trimmed implicit class ConfigExtension(config: Config) { def getTrimmedString(path: String) = config.getString(path).trim @@ -142,15 +181,14 @@ class NotificationService( } } - val logger = NamedZioLogger("plugin.change-validation") def sendNotification(step: WorkflowNode, cr: ChangeRequest): IOResult[Unit] = { for { - serverConfig <- getSMTPConf(configMailPath) + serverConfig <- getSMTPConf(configFile) _ <- ZIO.when(serverConfig.smtpHostServer.nonEmpty) { for { - emailConf <- getStepMailConf(step, configMailPath) - rudderBaseUrl <- getRudderBaseUrl(configMailPath) + emailConf <- getStepMailConf(step, configFile) + rudderBaseUrl <- getRudderBaseUrl(configFile) params = extractChangeRequestInfo(rudderBaseUrl, cr) mf = new DefaultMustacheFactory() emailBody <- getContentFromTemplate(mf, emailConf, params) @@ -161,24 +199,9 @@ class NotificationService( } yield () } - protected[changevalidation] def getConfig(path: String): IOResult[Config] = { - val file = new File(path) - IOResult.effectM { - for { - configResource <- if (file.exists && file.canRead) { - FileSystemResource(file).succeed - } else { - Inconsistency(s"Configuration file not found: ${file.getPath}").fail - } - } yield { - ConfigFactory.load(ConfigFactory.parseFile(configResource.file)) - } - } - } - protected[changevalidation] def getRudderBaseUrl(path: String): IOResult[String] = { for { - config <- getConfig(path) + config <- ReadConfigFile.getConfig(path) rudderBaseUrl <- IOResult.effect(s"An error occurs while parsing RUDDER base url in ${path}"){ config.getTrimmedString("rudder.base.url") } @@ -187,7 +210,7 @@ class NotificationService( protected[changevalidation] def getSMTPConf(path: String): IOResult[SMTPConf] = { for { - config <- getConfig(path) + config <- ReadConfigFile.getConfig(path) smtp <- IOResult.effect(s"An error occurs while parsing SMTP conf in ${path}") { val hostServer = config.getTrimmedString("smtp.hostServer") val port = config.getInt("smtp.port") @@ -213,7 +236,7 @@ class NotificationService( protected[changevalidation] def getStepMailConf(step: WorkflowNode, path: String): IOResult[EmailConf] = { for { - config <- getConfig(path) + config <- ReadConfigFile.getConfig(path) s <- step match { case Validation => "validation".succeed case Deployment => "deployment".succeed diff --git a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ValidationNeeded.scala b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ValidationNeeded.scala index 4ac320886..f4a23c13e 100644 --- a/change-validation/src/main/scala/com/normation/plugins/changevalidation/ValidationNeeded.scala +++ b/change-validation/src/main/scala/com/normation/plugins/changevalidation/ValidationNeeded.scala @@ -13,16 +13,15 @@ import com.normation.rudder.services.workflows.DirectiveChangeRequest import com.normation.rudder.services.workflows.GlobalParamChangeRequest import com.normation.rudder.services.workflows.NodeGroupChangeRequest import com.normation.rudder.services.workflows.RuleChangeRequest +import com.normation.NamedZioLogger + import net.liftweb.common.Box import net.liftweb.common.Full -import com.normation.box._ -object bddMock { - val USER_AUTH_NEEDED = Map( - "admin" -> false, - "Jean" -> true - ) -} +import com.normation.box._ +import com.normation.errors.IOResult +import com.normation.zio._ +import zio.syntax._ /** * Check is an external validation is needed for the change, given some @@ -38,6 +37,55 @@ trait ValidationNeeded { def forGlobalParam(actor: EventActor, change: GlobalParamChangeRequest): Box[Boolean] } +object CheckValidationKind { + val defaultChangeMode = ChangeEnableFor.SupervisedGroups +} +class CheckValidationKind(configFile: String, backend: ValidationNeeded) extends ValidationNeeded { + val logger = NamedZioLogger("plugin.change-validation") + + val changeEnableFor = { + ( + for { + c <- ReadConfigFile.getConfig(configFile) + v <- IOResult.effect(ChangeEnableFor.parse(c.getString("change.enable")).getOrElse(CheckValidationKind.defaultChangeMode)). + chainError(s"Error when reading configuration parameter 'change.enable' in '${configFile}', using " + + s"default value '${CheckValidationKind.defaultChangeMode.name}'" + ) + _ <- logger.info(s"Change validation mode for supervised groups: '${v.name}' ") + } yield { + v + } + ).catchAll(err => logger.info(err.fullMsg) *> CheckValidationKind.defaultChangeMode.succeed).runNow + } + + + def checkValidationMode(f: () => Box[Boolean]) = { + if (changeEnableFor == ChangeEnableFor.SupervisedGroups) { + logger.logEffect.debug(s"Change request follows supervised group logic") + f() + } else { + logger.logEffect.debug(s"Change request must be validation by configuration") + Full(true) + } + } + + override def forRule(actor: EventActor, change: RuleChangeRequest): Box[Boolean] = { + checkValidationMode(() => backend.forRule(actor, change)) + } + + override def forDirective(actor: EventActor, change: DirectiveChangeRequest): Box[Boolean] = { + checkValidationMode(() => backend.forDirective(actor, change)) + } + + override def forNodeGroup(actor: EventActor, change: NodeGroupChangeRequest): Box[Boolean] = { + checkValidationMode(() => backend.forNodeGroup(actor, change)) + } + + override def forGlobalParam(actor: EventActor, change: GlobalParamChangeRequest): Box[Boolean] = { + checkValidationMode(() => backend.forGlobalParam(actor, change)) + } +} + /* * A version of the "validationNeeded" plugin which bases its oracle on a list * of group. The list of group is used to mark nodes. @@ -54,11 +102,11 @@ trait ValidationNeeded { * Note that a validated user will always bypass this validation (see https://issues.rudder.io/issues/22188#note-5) */ class NodeGroupValidationNeeded( - monitoredTargets: () => Box[Set[SimpleTarget]] - , repos : RoChangeRequestRepository - , ruleLib : RoRuleRepository - , groupLib : RoNodeGroupRepository - , nodeInfoService : NodeInfoService + monitoredTargets : () => Box[Set[SimpleTarget]] + , repos : RoChangeRequestRepository + , ruleLib : RoRuleRepository + , groupLib : RoNodeGroupRepository + , nodeInfoService : NodeInfoService ) extends ValidationNeeded { /*